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>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { fetchKbFile, fetchKbFiles, type KbFileContent, type KbFileInfo } from '../api/client'
|
||||
|
||||
export interface KbGroup {
|
||||
/** ナレッジベース直下は空文字。 */
|
||||
directory: string
|
||||
files: KbFileInfo[]
|
||||
}
|
||||
|
||||
export const useKnowledgeBaseStore = defineStore('knowledgeBase', () => {
|
||||
const files = ref<KbFileInfo[]>([])
|
||||
/** 開いているファイルの全文。一覧に戻ると null に戻す。 */
|
||||
const openFile = ref<KbFileContent | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
/** タイトル・パス・プレビューに対する絞り込み文字列。 */
|
||||
const filter = ref('')
|
||||
/** 折りたたまれているフォルダ名。web_search のようにファイル数が多いフォルダを畳むために使う。 */
|
||||
const collapsed = ref<Set<string>>(new Set())
|
||||
|
||||
const filteredFiles = computed(() => {
|
||||
const q = filter.value.trim().toLowerCase()
|
||||
if (!q) return files.value
|
||||
return files.value.filter((f) =>
|
||||
`${f.title} ${f.path} ${f.preview}`.toLowerCase().includes(q),
|
||||
)
|
||||
})
|
||||
|
||||
/** ディレクトリごとにまとめる。一覧はサーバー側でパス順に並んでいるので、そのままの順序でグループ化できる。 */
|
||||
const groups = computed<KbGroup[]>(() => {
|
||||
const result: KbGroup[] = []
|
||||
for (const file of filteredFiles.value) {
|
||||
const last = result[result.length - 1]
|
||||
if (last && last.directory === file.directory) last.files.push(file)
|
||||
else result.push({ directory: file.directory, files: [file] })
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
files.value = await fetchKbFiles()
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'ナレッジベースの取得に失敗しました'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function open(path: string): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
openFile.value = await fetchKbFile(path)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'ファイルの読み込みに失敗しました'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
openFile.value = null
|
||||
}
|
||||
|
||||
function toggleGroup(directory: string): void {
|
||||
const next = new Set(collapsed.value)
|
||||
if (next.has(directory)) next.delete(directory)
|
||||
else next.add(directory)
|
||||
collapsed.value = next
|
||||
}
|
||||
|
||||
return { files, openFile, loading, error, filter, collapsed, filteredFiles, groups, load, open, close, toggleGroup }
|
||||
})
|
||||
Reference in New Issue
Block a user