- KB module: fastembed (AllMiniLML6V2) for CPU embedding, SQLite for vector storage with brute-force cosine similarity search - Chunking by ## headings, embeddings stored as BLOB in kb_chunks table - API: GET/PUT /api/kb for full-text read/write with auto re-indexing - Agent tools: kb_search (top-5 semantic search) and kb_read (full text) available in both planning and execution phases - Frontend: Settings menu in sidebar footer, KB editor as independent view with markdown textarea and save button - Also: extract shared db_err/ApiResult to api/mod.rs, add context management design doc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
229 lines
4.3 KiB
Vue
229 lines
4.3 KiB
Vue
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import type { Project } from '../types'
|
||
|
||
defineProps<{
|
||
projects: Project[]
|
||
selectedId: string
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
select: [id: string]
|
||
create: []
|
||
delete: [id: string]
|
||
openKb: []
|
||
}>()
|
||
|
||
const showSettings = ref(false)
|
||
|
||
function onDelete(e: Event, id: string) {
|
||
e.stopPropagation()
|
||
if (confirm('确定删除这个项目?')) {
|
||
emit('delete', id)
|
||
}
|
||
}
|
||
|
||
function onOpenKb() {
|
||
showSettings.value = false
|
||
emit('openKb')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<aside class="sidebar">
|
||
<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>
|
||
</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-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);
|
||
}
|
||
|
||
.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>
|