From bdd5a7a27440984adea33fd401b20dc9715fa2d7 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Mon, 9 Feb 2026 07:36:47 +0800 Subject: [PATCH] Update knowledge frontend --- web/pages/KnowledgeBase.tsx | 605 ++++++++++++++++++++++++------------ web/services/backendApi.ts | 34 +- web/types.ts | 5 + 3 files changed, 445 insertions(+), 199 deletions(-) diff --git a/web/pages/KnowledgeBase.tsx b/web/pages/KnowledgeBase.tsx index 74f2bd4..9fa3596 100644 --- a/web/pages/KnowledgeBase.tsx +++ b/web/pages/KnowledgeBase.tsx @@ -1,9 +1,14 @@ - import React, { useEffect, useState, useRef } from 'react'; -import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X } from 'lucide-react'; -import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog } from '../components/UI'; +import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X, Pencil, Trash2, Settings2 } from 'lucide-react'; +import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog, Badge } from '../components/UI'; import { KnowledgeBase } from '../types'; -import { createKnowledgeBase, deleteKnowledgeDocument, fetchKnowledgeBases, uploadKnowledgeDocument } from '../services/backendApi'; +import { createKnowledgeBase, deleteKnowledgeBase, deleteKnowledgeDocument, fetchKnowledgeBases, updateKnowledgeBase, uploadKnowledgeDocument } from '../services/backendApi'; + +const EMBEDDING_OPTIONS = [ + 'text-embedding-3-small', + 'text-embedding-3-large', + 'bge-small-zh', +]; export const KnowledgeBasePage: React.FC = () => { const [view, setView] = useState<'list' | 'detail'>('list'); @@ -11,11 +16,18 @@ export const KnowledgeBasePage: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); const [kbs, setKbs] = useState([]); const [isUploadOpen, setIsUploadOpen] = useState(false); - const [isCreateKbOpen, setIsCreateKbOpen] = useState(false); - const [newKbName, setNewKbName] = useState(''); + const [isKbModalOpen, setIsKbModalOpen] = useState(false); + const [editingKb, setEditingKb] = useState(null); const [isLoading, setIsLoading] = useState(true); - const filteredKbs = kbs.filter(kb => kb.name.toLowerCase().includes(searchTerm.toLowerCase())); + const [kbName, setKbName] = useState(''); + const [kbDescription, setKbDescription] = useState(''); + const [kbEmbeddingModel, setKbEmbeddingModel] = useState('text-embedding-3-small'); + const [kbChunkSize, setKbChunkSize] = useState(500); + const [kbChunkOverlap, setKbChunkOverlap] = useState(50); + const [isSavingKb, setIsSavingKb] = useState(false); + + const filteredKbs = kbs.filter((kb) => kb.name.toLowerCase().includes(searchTerm.toLowerCase())); const refreshKnowledgeBases = async () => { setIsLoading(true); @@ -26,9 +38,9 @@ export const KnowledgeBasePage: React.FC = () => { const nextSelected = list.find((item) => item.id === selectedKb.id) || null; setSelectedKb(nextSelected); } - } catch (error) { + } catch (error: any) { console.error(error); - alert('加载知识库失败,请检查后端服务。'); + alert(error?.message || '加载知识库失败,请检查后端服务。'); } finally { setIsLoading(false); } @@ -43,37 +55,104 @@ export const KnowledgeBasePage: React.FC = () => { setView('detail'); }; - const handleImportClick = () => { - setIsUploadOpen(true); + const openCreateKb = () => { + setEditingKb(null); + setKbName(''); + setKbDescription(''); + setKbEmbeddingModel('text-embedding-3-small'); + setKbChunkSize(500); + setKbChunkOverlap(50); + setIsKbModalOpen(true); }; - const handleCreateKb = async () => { - if (!newKbName.trim()) return; + const openEditKb = (kb: KnowledgeBase) => { + setEditingKb(kb); + setKbName(kb.name || ''); + setKbDescription(kb.description || ''); + setKbEmbeddingModel(kb.embeddingModel || 'text-embedding-3-small'); + setKbChunkSize(kb.chunkSize ?? 500); + setKbChunkOverlap(kb.chunkOverlap ?? 50); + setIsKbModalOpen(true); + }; + + const handleSaveKb = async () => { + if (!kbName.trim()) { + alert('请输入知识库名称'); + return; + } + if (kbChunkSize <= 0) { + alert('Chunk Size 必须大于 0'); + return; + } + if (kbChunkOverlap < 0) { + alert('Chunk Overlap 不能小于 0'); + return; + } + if (kbChunkOverlap >= kbChunkSize) { + alert('Chunk Overlap 必须小于 Chunk Size'); + return; + } + try { - await createKnowledgeBase(newKbName.trim()); + setIsSavingKb(true); + if (editingKb) { + await updateKnowledgeBase(editingKb.id, { + name: kbName.trim(), + description: kbDescription, + embeddingModel: kbEmbeddingModel, + chunkSize: kbChunkSize, + chunkOverlap: kbChunkOverlap, + }); + } else { + await createKnowledgeBase({ + name: kbName.trim(), + description: kbDescription, + embeddingModel: kbEmbeddingModel, + chunkSize: kbChunkSize, + chunkOverlap: kbChunkOverlap, + }); + } + setIsKbModalOpen(false); await refreshKnowledgeBases(); - setIsCreateKbOpen(false); - setNewKbName(''); - } catch (error) { + } catch (error: any) { console.error(error); - alert('新建知识库失败。'); + alert(error?.message || (editingKb ? '更新知识库失败。' : '新建知识库失败。')); + } finally { + setIsSavingKb(false); + } + }; + + const handleDeleteKb = async (kb: KnowledgeBase) => { + if (!confirm(`确认删除知识库「${kb.name}」吗?此操作会删除其所有文档和向量数据。`)) return; + try { + await deleteKnowledgeBase(kb.id); + if (selectedKb?.id === kb.id) { + setSelectedKb(null); + setView('list'); + } + await refreshKnowledgeBases(); + } catch (error: any) { + console.error(error); + alert(error?.message || '删除知识库失败。'); } }; if (view === 'detail' && selectedKb) { return (
- setView('list')} - onImport={handleImportClick} + setView('list')} + onImport={() => setIsUploadOpen(true)} + onEdit={() => openEditKb(selectedKb)} + onDelete={() => handleDeleteKb(selectedKb)} onDeleteDocument={async (docId) => { try { await deleteKnowledgeDocument(selectedKb.id, docId); await refreshKnowledgeBases(); - } catch (error) { + } catch (error: any) { console.error(error); - alert('删除文档失败。'); + alert(error?.message || '删除文档失败。'); } }} /> @@ -83,6 +162,24 @@ export const KnowledgeBasePage: React.FC = () => { onClose={() => setIsUploadOpen(false)} onUploaded={refreshKnowledgeBases} /> + + setIsKbModalOpen(false)} + onSave={handleSaveKb} + saving={isSavingKb} + editingKb={editingKb} + kbName={kbName} + setKbName={setKbName} + kbDescription={kbDescription} + setKbDescription={setKbDescription} + kbEmbeddingModel={kbEmbeddingModel} + setKbEmbeddingModel={setKbEmbeddingModel} + kbChunkSize={kbChunkSize} + setKbChunkSize={setKbChunkSize} + kbChunkOverlap={kbChunkOverlap} + setKbChunkOverlap={setKbChunkOverlap} + />
); } @@ -93,49 +190,74 @@ export const KnowledgeBasePage: React.FC = () => {

知识库

- {/* Search Bar - Layout aligned with History Page and width filled */}
- - setSearchTerm(e.target.value)} - /> + + setSearchTerm(e.target.value)} + />
- {filteredKbs.map(kb => ( - ( + +
+ + +
handleSelect(kb)}>
+ {kb.embeddingModel || 'embedding'}

{kb.name}

-

文档数量: {kb.documents.length}

-

创建人: {kb.creator}

-

创建时间: {kb.createdAt}

+

文档数量: {kb.documents.length}

+

分片数量: {kb.chunkSize ?? 500}/{kb.chunkOverlap ?? 50}

+

创建时间: {kb.createdAt}

))} - - {/* Add New Placeholder */} -
setIsCreateKbOpen(true)} - className="border border-dashed border-white/10 rounded-xl p-6 flex flex-col items-center justify-center text-muted-foreground hover:bg-white/5 hover:border-primary/30 transition-all cursor-pointer min-h-[200px]" + +
- - 新建知识库 + + 新建知识库
+ {!isLoading && filteredKbs.length === 0 && (
暂无知识库
)} @@ -144,46 +266,133 @@ export const KnowledgeBasePage: React.FC = () => { )}
- {/* New Knowledge Base Dialog */} - setIsCreateKbOpen(false)} - title="新建知识库" - footer={ - <> - - - - } - > -
-
- - setNewKbName(e.target.value)} - placeholder="请输入知识库名称..." - autoFocus - onKeyDown={(e) => e.key === 'Enter' && handleCreateKb()} - /> -
-

- 知识库用于存储私域文档,AI 小助手在回答问题时会优先检索绑定的知识库内容。 -

-
-
+ setIsKbModalOpen(false)} + onSave={handleSaveKb} + saving={isSavingKb} + editingKb={editingKb} + kbName={kbName} + setKbName={setKbName} + kbDescription={kbDescription} + setKbDescription={setKbDescription} + kbEmbeddingModel={kbEmbeddingModel} + setKbEmbeddingModel={setKbEmbeddingModel} + kbChunkSize={kbChunkSize} + setKbChunkSize={setKbChunkSize} + kbChunkOverlap={kbChunkOverlap} + setKbChunkOverlap={setKbChunkOverlap} + />
); }; -const KnowledgeBaseDetail: React.FC<{ - kb: KnowledgeBase; +const KnowledgeBaseModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + onSave: () => Promise; + saving: boolean; + editingKb: KnowledgeBase | null; + kbName: string; + setKbName: (v: string) => void; + kbDescription: string; + setKbDescription: (v: string) => void; + kbEmbeddingModel: string; + setKbEmbeddingModel: (v: string) => void; + kbChunkSize: number; + setKbChunkSize: (v: number) => void; + kbChunkOverlap: number; + setKbChunkOverlap: (v: number) => void; +}> = ({ + isOpen, + onClose, + onSave, + saving, + editingKb, + kbName, + setKbName, + kbDescription, + setKbDescription, + kbEmbeddingModel, + setKbEmbeddingModel, + kbChunkSize, + setKbChunkSize, + kbChunkOverlap, + setKbChunkOverlap, +}) => ( + + + + + } + > +
+
+ + setKbName(e.target.value)} + placeholder="请输入知识库名称..." + autoFocus + onKeyDown={(e) => e.key === 'Enter' && onSave()} + /> +
+ +
+ +