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>
97 lines
3.4 KiB
Vue
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>
|