tori/web/src/components/Sidebar.vue
Fam Zheng 40f200db4f KB multi-article support: CRUD articles, per-article indexing, sidebar KB mode
- Replace singleton kb_content table with kb_articles (id, title, content)
- Add article_id to kb_chunks for per-article chunk tracking
- Auto-migrate old kb_content data on startup
- KbManager: index/delete per article, search across all with article_title
- API: full CRUD on /kb/articles, keep GET /kb for agent tool
- Agent: kb_search shows article labels, kb_read concatenates all articles
- Frontend: Sidebar KB mode with article list, KbEditor for single article

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 08:56:08 +00:00

296 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref } from 'vue'
import type { Project, KbArticleSummary } from '../types'
defineProps<{
projects: Project[]
selectedId: string
kbMode: boolean
kbArticles: KbArticleSummary[]
selectedArticleId: string
}>()
const emit = defineEmits<{
select: [id: string]
create: []
delete: [id: string]
openKb: []
closeKb: []
selectArticle: [id: string]
createArticle: []
deleteArticle: [id: string]
}>()
const showSettings = ref(false)
function onDelete(e: Event, id: string) {
e.stopPropagation()
if (confirm('确定删除这个项目?')) {
emit('delete', id)
}
}
function onDeleteArticle(e: Event, id: string) {
e.stopPropagation()
if (confirm('确定删除这篇文章?')) {
emit('deleteArticle', id)
}
}
function onOpenKb() {
showSettings.value = false
emit('openKb')
}
</script>
<template>
<aside class="sidebar">
<!-- KB Mode -->
<template v-if="kbMode">
<div class="sidebar-header">
<button class="btn-back" @click="emit('closeKb')"> Back</button>
<h1 class="logo">Knowledge Base</h1>
<button class="btn-new" @click="emit('createArticle')">+ 新文章</button>
</div>
<nav class="project-list">
<div
v-for="article in kbArticles"
:key="article.id"
class="project-item"
:class="{ active: article.id === selectedArticleId }"
@click="emit('selectArticle', article.id)"
>
<div class="project-row">
<span class="project-name">{{ article.title }}</span>
<button class="btn-delete" @click="onDeleteArticle($event, article.id)" title="删除文章">×</button>
</div>
<span class="project-time">{{ new Date(article.updated_at).toLocaleDateString() }}</span>
</div>
<div v-if="kbArticles.length === 0" class="empty-hint">还没有文章</div>
</nav>
</template>
<!-- Normal Mode -->
<template v-else>
<div class="sidebar-header">
<h1 class="logo">Tori</h1>
<button class="btn-new" @click="emit('create')">+ 新项目</button>
</div>
<nav class="project-list">
<div
v-for="project in projects"
:key="project.id"
class="project-item"
:class="{ active: project.id === selectedId }"
@click="emit('select', project.id)"
>
<div class="project-row">
<span class="project-name">{{ project.name }}</span>
<button class="btn-delete" @click="onDelete($event, project.id)" title="删除项目">×</button>
</div>
<span class="project-time">{{ new Date(project.updated_at).toLocaleDateString() }}</span>
</div>
</nav>
<div class="sidebar-footer">
<div class="settings-wrapper">
<button class="btn-settings" @click="showSettings = !showSettings">Settings</button>
<div v-if="showSettings" class="settings-menu">
<button class="settings-item" @click="onOpenKb">Knowledge Base</button>
</div>
</div>
</div>
</template>
</aside>
</template>
<style scoped>
.sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border);
}
.logo {
font-size: 20px;
font-weight: 700;
color: var(--accent);
margin-bottom: 12px;
}
.btn-back {
width: 100%;
padding: 6px 8px;
background: transparent;
color: var(--text-secondary);
border: none;
font-size: 13px;
cursor: pointer;
text-align: left;
border-radius: 6px;
margin-bottom: 8px;
}
.btn-back:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-new {
width: 100%;
padding: 8px;
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px dashed var(--border);
font-size: 13px;
}
.btn-new:hover {
background: var(--accent);
color: var(--bg-primary);
border-style: solid;
}
.project-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.project-item {
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 2px;
}
.project-item:hover {
background: var(--bg-tertiary);
}
.project-item.active {
background: var(--bg-tertiary);
border-left: 3px solid var(--accent);
}
.project-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.project-name {
font-size: 14px;
font-weight: 500;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn-delete {
display: none;
width: 20px;
height: 20px;
padding: 0;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 16px;
line-height: 1;
cursor: pointer;
border-radius: 4px;
flex-shrink: 0;
}
.btn-delete:hover {
background: var(--error, #e74c3c);
color: #fff;
}
.project-item:hover .btn-delete {
display: flex;
align-items: center;
justify-content: center;
}
.project-time {
font-size: 11px;
color: var(--text-secondary);
}
.empty-hint {
padding: 20px;
text-align: center;
color: var(--text-secondary);
font-size: 13px;
}
.sidebar-footer {
padding: 12px 16px;
border-top: 1px solid var(--border);
}
.settings-wrapper {
position: relative;
}
.btn-settings {
width: 100%;
padding: 8px;
background: transparent;
color: var(--text-secondary);
border: none;
font-size: 13px;
cursor: pointer;
text-align: left;
border-radius: 6px;
}
.btn-settings:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.settings-menu {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
margin-bottom: 4px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.settings-item {
width: 100%;
padding: 8px 12px;
background: none;
border: none;
color: var(--text-primary);
font-size: 13px;
text-align: left;
cursor: pointer;
border-radius: 6px;
}
.settings-item:hover {
background: var(--bg-tertiary);
}
</style>