import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Save, Loader2, RefreshCw, Pencil, Trash2, FileText, Plus, BookOpen, GripVertical } from "lucide-react"; import { api } from "@/lib/api"; import { useProjectStore } from "@/store/projectStore"; import { Textarea } from "@/components/ui/textarea"; import { KnowledgeBaseForm, type KnowledgeBaseFormValues } from "@/components/KnowledgeBaseForm"; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; interface KnowledgeBase { id: string; name: string; description?: string; project_id?: number | null; embedding_model?: string | null; chunk_size: number; chunk_overlap: number; top_k: number; is_active: boolean; updated_at: string; documents?: Array<{ id: string }>; } interface KnowledgeDocument { id: string; title: string; content: string; metadata?: Record; created_at: string; updated_at: string; } const defaultKnowledgeDocumentForm = { title: '', content: '', metadata: '', }; export function KnowledgeBases() { const { t } = useTranslation(); const { currentProject } = useProjectStore(); const [knowledgeBases, setKnowledgeBases] = useState([]); const [isLoading, setIsLoading] = useState(false); // KB Form Dialog State const [isFormOpen, setIsFormOpen] = useState(false); const [editingKb, setEditingKb] = useState(null); const [isSavingKb, setIsSavingKb] = useState(false); const [reindexingKbId, setReindexingKbId] = useState(''); // Docs Dialog State const [docsDialogOpen, setDocsDialogOpen] = useState(false); const [selectedKbForDocs, setSelectedKbForDocs] = useState(null); const [knowledgeDocuments, setKnowledgeDocuments] = useState([]); const [isLoadingDocs, setIsLoadingDocs] = useState(false); // Doc Form State inside Docs Dialog const [isSavingDoc, setIsSavingDoc] = useState(false); const [editingDocId, setEditingDocId] = useState(''); const [docForm, setDocForm] = useState(defaultKnowledgeDocumentForm); const [uploadingDocs, setUploadingDocs] = useState(false); const [uploadFiles, setUploadFiles] = useState([]); useEffect(() => { void fetchKnowledgeBases(); }, [currentProject?.id]); const fetchKnowledgeBases = async () => { if (!currentProject) { setKnowledgeBases([]); return; } setIsLoading(true); try { const data = await api.get(`/api/v1/knowledge-bases?project_id=${currentProject.id}`); // 从 localStorage 中恢复顺序 const savedOrderStr = localStorage.getItem(`knowledge_bases_order_${currentProject.id}`); if (savedOrderStr) { try { const savedOrder = JSON.parse(savedOrderStr) as string[]; data.sort((a, b) => { const indexA = savedOrder.indexOf(a.id); const indexB = savedOrder.indexOf(b.id); if (indexA === -1 && indexB === -1) return 0; if (indexA === -1) return 1; if (indexB === -1) return -1; return indexA - indexB; }); } catch (e) { console.error("Failed to parse saved kb order", e); } } setKnowledgeBases(data); } catch (err: any) { console.error(err); } finally { setIsLoading(false); } }; const handleCreateKb = () => { setEditingKb(null); setIsFormOpen(true); }; const handleEditKb = (kb: KnowledgeBase) => { setEditingKb(kb); setIsFormOpen(true); }; const handleSaveKb = async (data: KnowledgeBaseFormValues) => { if (!currentProject) return; setIsSavingKb(true); try { const payload = { ...data, project_id: currentProject.id, }; if (editingKb) { await api.put(`/api/v1/knowledge-bases/${editingKb.id}`, payload); } else { await api.post('/api/v1/knowledge-bases', payload); } setIsFormOpen(false); await fetchKnowledgeBases(); } finally { setIsSavingKb(false); } }; const handleDeleteKb = async (id: string) => { if (!window.confirm(t('confirmDeleteKnowledgeBase', 'Are you sure to delete this knowledge base?'))) { return; } try { await api.delete(`/api/v1/knowledge-bases/${id}`); await fetchKnowledgeBases(); } catch (err: any) { console.error(err); } }; const handleReindexKb = async (id: string) => { setReindexingKbId(id); try { await api.post(`/api/v1/knowledge-bases/${id}/reindex`, {}); alert(t('knowledgeBaseReindexSuccess', 'Knowledge base reindexed successfully')); } catch (err: any) { console.error(err); alert(t('knowledgeBaseReindexFailed', 'Failed to reindex knowledge base')); } finally { setReindexingKbId(''); } }; // --- Document Management Methods --- const handleManageDocs = async (kb: KnowledgeBase) => { setSelectedKbForDocs(kb); setDocsDialogOpen(true); setEditingDocId(''); setDocForm(defaultKnowledgeDocumentForm); await fetchKnowledgeDocuments(kb.id); }; const fetchKnowledgeDocuments = async (kbId: string) => { if (!kbId) return; setIsLoadingDocs(true); try { const data = await api.get(`/api/v1/knowledge-bases/${kbId}/documents`); setKnowledgeDocuments(data); } catch (err: any) { console.error(err); } finally { setIsLoadingDocs(false); } }; const resetDocForm = () => { setEditingDocId(''); setDocForm(defaultKnowledgeDocumentForm); }; const handleSaveDoc = async () => { if (!selectedKbForDocs) return; if (!docForm.title.trim() || !docForm.content.trim()) { alert(t('knowledgeDocumentTitleRequired', 'Document title and content are required')); return; } let parsedMetadata = {}; if (docForm.metadata.trim()) { try { parsedMetadata = JSON.parse(docForm.metadata.trim()); } catch { alert(t('knowledgeDocumentMetadataInvalid', 'Document metadata must be valid JSON')); return; } } setIsSavingDoc(true); try { const payload = { title: docForm.title.trim(), content: docForm.content.trim(), metadata: parsedMetadata, }; if (editingDocId) { await api.put(`/api/v1/knowledge-bases/${selectedKbForDocs.id}/documents/${editingDocId}`, payload); } else { await api.post(`/api/v1/knowledge-bases/${selectedKbForDocs.id}/documents`, payload); } resetDocForm(); await fetchKnowledgeDocuments(selectedKbForDocs.id); await fetchKnowledgeBases(); // To update doc count } catch (err: any) { console.error(err); } finally { setIsSavingDoc(false); } }; const handleEditDoc = (doc: KnowledgeDocument) => { setEditingDocId(doc.id); setDocForm({ title: doc.title || '', content: doc.content || '', metadata: doc.metadata && Object.keys(doc.metadata).length > 0 ? JSON.stringify(doc.metadata, null, 2) : '', }); }; const handleDeleteDoc = async (docId: string) => { if (!selectedKbForDocs) return; if (!window.confirm(t('confirmDeleteKnowledgeDocument', 'Are you sure to delete this document?'))) return; try { await api.delete(`/api/v1/knowledge-bases/${selectedKbForDocs.id}/documents/${docId}`); if (editingDocId === docId) resetDocForm(); await fetchKnowledgeDocuments(selectedKbForDocs.id); await fetchKnowledgeBases(); } catch (err: any) { console.error(err); } }; const handleUploadDocs = async () => { if (!selectedKbForDocs || uploadFiles.length === 0) return; setUploadingDocs(true); try { const formData = new FormData(); uploadFiles.forEach((file) => formData.append('files', file)); await api.post(`/api/v1/knowledge-bases/${selectedKbForDocs.id}/documents/upload`, formData); setUploadFiles([]); await fetchKnowledgeDocuments(selectedKbForDocs.id); await fetchKnowledgeBases(); } catch (err: any) { console.error(err); alert(t('knowledgeDocumentUploadFailed', 'Failed to upload knowledge documents')); } finally { setUploadingDocs(false); } }; const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { setKnowledgeBases((items) => { const oldIndex = items.findIndex((item) => item.id === active.id); const newIndex = items.findIndex((item) => item.id === over.id); const newItems = arrayMove(items, oldIndex, newIndex); // 保存新的顺序到 localStorage if (currentProject) { localStorage.setItem( `knowledge_bases_order_${currentProject.id}`, JSON.stringify(newItems.map(i => i.id)) ); } return newItems; }); } }; const SortableKbCard = ({ kb }: { kb: KnowledgeBase }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: kb.id }); const style = { transform: CSS.Transform.toString(transform), transition, }; return (

{kb.name} {!kb.is_active && ( Inactive )}

{kb.documents?.length || 0} Documents

Model {kb.embedding_model ? kb.embedding_model.substring(0, 15) + (kb.embedding_model.length > 15 ? '...' : '') : 'Default'}
Chunking {kb.chunk_size} / {kb.chunk_overlap}
{kb.description && (

{kb.description}

)}
); }; return (
{t('knowledgeBases')}
{!currentProject ? (
{t('selectProjectBeforeManageKnowledgeBase')}
) : isLoading ? (
) : knowledgeBases.length === 0 ? (

{t('noKnowledgeBases')}

) : ( kb.id)} strategy={rectSortingStrategy} >
{knowledgeBases.map((kb) => ( ))}
)}
{/* Form Dialog */} {editingKb ? t('updateKnowledgeBase', 'Edit Knowledge Base') : t('createKnowledgeBase', 'New Knowledge Base')}
setIsFormOpen(false)} isSubmitting={isSavingKb} />
{/* Docs Management Dialog */} { setDocsDialogOpen(open); if (!open) { setSelectedKbForDocs(null); setKnowledgeDocuments([]); } }}> {selectedKbForDocs ? t('knowledgeDocumentManagerTitle', { name: selectedKbForDocs.name }) : 'Manage Documents'}
{/* Upload Section */}
{t('knowledgeDocumentUploadTitle')}
{t('knowledgeDocumentUploadHint')}
setUploadFiles(Array.from(e.target.files || []))} disabled={uploadingDocs} className="flex-1" />
{uploadFiles.length > 0 && (
{t('knowledgeDocumentUploadSelected', { count: uploadFiles.length })}
)}
{/* Edit/Create Doc Form */}
{editingDocId ? t('updateKnowledgeDocument') : t('createKnowledgeDocument')}
setDocForm((p) => ({ ...p, title: e.target.value }))} disabled={isSavingDoc} />