import React, { useEffect, useState, useRef } from 'react'; import { Search, Plus, FileText, Upload, ArrowLeft, CloudUpload, File as FileIcon, X, Pencil, Trash2, Settings2, MoreHorizontal } from 'lucide-react'; import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Card, Dialog, Badge } from '../components/UI'; import { KnowledgeBase } from '../types'; import { createKnowledgeBase, deleteKnowledgeBase, deleteKnowledgeDocument, fetchKnowledgeBases, fetchLLMModels, 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'); const [selectedKb, setSelectedKb] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [kbs, setKbs] = useState([]); const [isUploadOpen, setIsUploadOpen] = useState(false); const [isKbModalOpen, setIsKbModalOpen] = useState(false); const [editingKb, setEditingKb] = useState(null); const [isLoading, setIsLoading] = useState(true); const [embeddingOptions, setEmbeddingOptions] = useState(EMBEDDING_OPTIONS); const [hasDbEmbeddingModels, setHasDbEmbeddingModels] = useState(false); const [openMenuKbId, setOpenMenuKbId] = useState(null); 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); try { const list = await fetchKnowledgeBases(); setKbs(list); if (selectedKb) { const nextSelected = list.find((item) => item.id === selectedKb.id) || null; setSelectedKb(nextSelected); } } catch (error: any) { console.error(error); alert(error?.message || '加载知识库失败,请检查后端服务。'); } finally { setIsLoading(false); } }; useEffect(() => { refreshKnowledgeBases(); const loadEmbeddingModels = async () => { try { const models = await fetchLLMModels(); const fromDb = Array.from(new Set(models .filter((m) => m.type === 'embedding') .map((m) => (m.modelName || m.name || '').trim()) .filter(Boolean))); setHasDbEmbeddingModels(fromDb.length > 0); // Prefer DB embedding models first; keep defaults as fallback at the end. const defaultsTail = EMBEDDING_OPTIONS.filter((item) => !fromDb.includes(item)); const ordered = [...fromDb, ...defaultsTail]; setEmbeddingOptions(ordered.length > 0 ? ordered : EMBEDDING_OPTIONS); } catch { setEmbeddingOptions(EMBEDDING_OPTIONS); setHasDbEmbeddingModels(false); } }; loadEmbeddingModels(); }, []); useEffect(() => { const onDocClick = () => setOpenMenuKbId(null); document.addEventListener('click', onDocClick); return () => document.removeEventListener('click', onDocClick); }, []); const handleSelect = (kb: KnowledgeBase) => { setSelectedKb(kb); setView('detail'); }; const openCreateKb = () => { setEditingKb(null); setKbName(''); setKbDescription(''); setKbEmbeddingModel('text-embedding-3-small'); setKbChunkSize(500); setKbChunkOverlap(50); setIsKbModalOpen(true); }; 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 { 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(); } catch (error: any) { console.error(error); 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={() => setIsUploadOpen(true)} onEdit={() => openEditKb(selectedKb)} onDelete={() => handleDeleteKb(selectedKb)} onDeleteDocument={async (docId) => { try { await deleteKnowledgeDocument(selectedKb.id, docId); await refreshKnowledgeBases(); } catch (error: any) { console.error(error); alert(error?.message || '删除文档失败。'); } }} /> 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} />
); } return (

知识库

setSearchTerm(e.target.value)} />
{filteredKbs.map((kb) => ( handleSelect(kb)} >
{openMenuKbId === kb.id && (
e.stopPropagation()} >
)}

{kb.name}

{kb.embeddingModel || 'embedding'}

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

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

创建时间: {kb.createdAt}

))}
新建知识库
{!isLoading && filteredKbs.length === 0 && (
暂无知识库
)} {isLoading && (
加载中...
)}
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} embeddingOptions={embeddingOptions} hasDbEmbeddingModels={hasDbEmbeddingModels} />
); }; 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; embeddingOptions: string[]; hasDbEmbeddingModels: boolean; }> = ({ isOpen, onClose, onSave, saving, editingKb, kbName, setKbName, kbDescription, setKbDescription, kbEmbeddingModel, setKbEmbeddingModel, kbChunkSize, setKbChunkSize, kbChunkOverlap, setKbChunkOverlap, embeddingOptions, hasDbEmbeddingModels, }) => ( } >
setKbName(e.target.value)} placeholder="请输入知识库名称..." autoFocus onKeyDown={(e) => e.key === 'Enter' && onSave()} />