Add global knowledge base with RAG search
- 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>
This commit is contained in:
@@ -75,4 +75,12 @@ export const api = {
|
||||
|
||||
deleteTimer: (timerId: string) =>
|
||||
request<void>(`/timers/${timerId}`, { method: 'DELETE' }),
|
||||
|
||||
getKb: () => request<{ content: string }>('/kb'),
|
||||
|
||||
putKb: (content: string) =>
|
||||
request<{ content: string }>('/kb', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ content }),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Sidebar from './Sidebar.vue'
|
||||
import WorkflowView from './WorkflowView.vue'
|
||||
import ReportView from './ReportView.vue'
|
||||
import CreateForm from './CreateForm.vue'
|
||||
import KbEditor from './KbEditor.vue'
|
||||
import { api } from '../api'
|
||||
import type { Project } from '../types'
|
||||
|
||||
@@ -12,6 +13,7 @@ const selectedProjectId = ref('')
|
||||
const reportWorkflowId = ref('')
|
||||
const error = ref('')
|
||||
const creating = ref(false)
|
||||
const showKb = ref(false)
|
||||
|
||||
const isReportPage = computed(() => !!reportWorkflowId.value)
|
||||
|
||||
@@ -54,6 +56,7 @@ function onSelectProject(id: string) {
|
||||
selectedProjectId.value = id
|
||||
reportWorkflowId.value = ''
|
||||
creating.value = false
|
||||
showKb.value = false
|
||||
history.pushState(null, '', `/projects/${id}`)
|
||||
}
|
||||
|
||||
@@ -110,10 +113,12 @@ async function onDeleteProject(id: string) {
|
||||
@select="onSelectProject"
|
||||
@create="onStartCreate"
|
||||
@delete="onDeleteProject"
|
||||
@openKb="showKb = true; selectedProjectId = ''; creating = false"
|
||||
/>
|
||||
<main class="main-content">
|
||||
<div v-if="error" class="error-banner" @click="error = ''">{{ error }}</div>
|
||||
<div v-if="creating" class="empty-state">
|
||||
<KbEditor v-if="showKb" />
|
||||
<div v-else-if="creating" class="empty-state">
|
||||
<CreateForm @submit="onConfirmCreate" @cancel="creating = false" />
|
||||
</div>
|
||||
<div v-else-if="!selectedProjectId" class="empty-state">
|
||||
|
||||
130
web/src/components/KbEditor.vue
Normal file
130
web/src/components/KbEditor.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { api } from '../api'
|
||||
|
||||
const content = ref('')
|
||||
const saving = ref(false)
|
||||
const loading = ref(true)
|
||||
const message = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const kb = await api.getKb()
|
||||
content.value = kb.content
|
||||
} catch (e: any) {
|
||||
message.value = 'Failed to load: ' + e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
message.value = ''
|
||||
try {
|
||||
await api.putKb(content.value)
|
||||
message.value = 'Saved & indexed'
|
||||
setTimeout(() => { message.value = '' }, 2000)
|
||||
} catch (e: any) {
|
||||
message.value = 'Error: ' + e.message
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kb-view">
|
||||
<div class="kb-header">
|
||||
<h2>Knowledge Base</h2>
|
||||
<div class="kb-actions">
|
||||
<span v-if="message" class="kb-message">{{ message }}</span>
|
||||
<button class="btn-save" @click="save" :disabled="saving">
|
||||
{{ saving ? 'Saving...' : 'Save' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="kb-loading">Loading...</div>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="content"
|
||||
class="kb-textarea"
|
||||
placeholder="Write your knowledge base in Markdown... Use ## headings to split into searchable chunks."
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kb-view {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kb-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.kb-header h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.kb-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.kb-message {
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
padding: 6px 16px;
|
||||
background: var(--accent);
|
||||
color: var(--bg-primary);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-save:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.kb-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.kb-textarea {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
resize: none;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.kb-textarea::placeholder {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { Project } from '../types'
|
||||
|
||||
defineProps<{
|
||||
@@ -10,14 +11,22 @@ 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>
|
||||
@@ -41,6 +50,14 @@ function onDelete(e: Event, id: string) {
|
||||
<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>
|
||||
|
||||
@@ -153,4 +170,59 @@ function onDelete(e: Event, id: string) {
|
||||
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>
|
||||
|
||||
@@ -82,7 +82,7 @@ function handleWsMessage(msg: WsMessage) {
|
||||
break
|
||||
case 'ReportReady':
|
||||
if (workflow.value && msg.workflow_id === workflow.value.id) {
|
||||
workflow.value = { ...workflow.value, status: workflow.value.status }
|
||||
loadData()
|
||||
}
|
||||
break
|
||||
case 'ProjectUpdate':
|
||||
|
||||
Reference in New Issue
Block a user