Files
playchat/frontend/src/App.vue
T
colcaroliandClaude Opus 5 f085db421c Browse the knowledge base from a single side panel menu
Add a read-only view of backend/knowledge_base/**.md so the files that get
injected into the system prompt can be inspected from the GUI:

- GET /api/kb/files lists the Markdown files (title, preview, size, mtime and
  whether the file is already embedded), GET /api/kb/file returns one in full.
  Paths are resolved against the knowledge base directory and rejected unless
  they stay inside it and end in .md.
- KnowledgeBasePanel.vue groups the files by folder (collapsible, so web_search
  can be folded away), filters by title/path/preview and renders the selected
  file with MarkdownContent.

The header had grown to four side panel toggles, so collapse them into one
menu: panels.ts now holds the list of panels along with the component each one
renders, App.vue picks it with <component :is>, and SidePanelMenu.vue shows the
active panel on its button. Re-picking the open panel (or "パネルを閉じる")
closes it; a click outside or Esc closes just the menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:38:54 +09:00

97 lines
3.4 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import SessionList from './components/SessionList.vue'
import ChatWindow from './components/ChatWindow.vue'
import MessageInput from './components/MessageInput.vue'
import ModelSelector from './components/ModelSelector.vue'
import SidePanelMenu from './components/SidePanelMenu.vue'
import ToastList from './components/ToastList.vue'
import MemoryMonitor from './components/MemoryMonitor.vue'
import Clock from './components/Clock.vue'
import WordPool from './components/WordPool.vue'
import LoginGate from './components/LoginGate.vue'
import { SIDE_PANELS, type PanelId } from './panels'
import { useAuthStore } from './stores/auth'
const authStore = useAuthStore()
/** 右サイドに開いているパネル。null はどれも開いていない状態。 */
const activePanel = ref<PanelId | null>(null)
const activePanelDef = computed(() => SIDE_PANELS.find((p) => p.id === activePanel.value) ?? null)
function handleUnauthorized() {
authStore.markUnauthenticated()
}
onMounted(() => {
authStore.checkStatus()
window.addEventListener('playchat:unauthorized', handleUnauthorized)
})
onUnmounted(() => {
window.removeEventListener('playchat:unauthorized', handleUnauthorized)
})
</script>
<template>
<template v-if="authStore.loading" />
<LoginGate v-else-if="authStore.required && !authStore.authenticated" />
<template v-else>
<div class="app">
<div class="sidebar">
<SessionList />
<MemoryMonitor />
<WordPool />
</div>
<div class="main">
<header>
<span class="title">PlayChat</span>
<ModelSelector />
<Clock />
<div class="header-actions">
<SidePanelMenu :active="activePanel" @select="activePanel = $event" />
<button v-if="authStore.required" class="log-toggle" @click="authStore.logout()">
ログアウト
</button>
</div>
</header>
<div class="body">
<div class="chat-pane">
<ChatWindow />
<MessageInput />
</div>
<div v-if="activePanelDef" class="log-pane">
<component :is="activePanelDef.component" />
</div>
</div>
</div>
</div>
<ToastList />
</template>
</template>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body, #app { height: 100%; }
body { background: #111; color: #eee; font-family: system-ui, sans-serif; }
</style>
<style scoped>
.app { display: flex; height: 100vh; }
.sidebar { width: 260px; flex-shrink: 0; display: flex; flex-direction: column; border-right: 1px solid #333; overflow: hidden; }
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
header {
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; height: 48px;
border-bottom: 1px solid #333; background: #1a1a1a; flex-shrink: 0;
}
.title { font-size: 16px; font-weight: 600; color: #fff; }
.header-actions { display: flex; align-items: center; gap: 8px; }
.log-toggle {
background: #2a2a2a; color: #aaa; border: 1px solid #444;
border-radius: 6px; padding: 4px 14px; font-size: 13px; cursor: pointer;
}
.body { flex: 1; display: flex; overflow: hidden; }
.chat-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.log-pane { width: 400px; min-width: 300px; border-left: 1px solid #2a2a2a; overflow: hidden; }
</style>