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 { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"; import { Save, Loader2, Database, RefreshCw, Pencil, Trash2, FileText, Plus } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { api } from "@/lib/api"; import { useProjectStore } from "@/store/projectStore"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; 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 KnowledgeBaseForm { name: string; description: string; embedding_model: string; chunk_size: number; chunk_overlap: number; top_k: number; is_active: boolean; } interface KnowledgeDocument { id: string; title: string; content: string; metadata?: Record; created_at: string; updated_at: string; } interface EmbeddingModelConfig { id: string; name?: string; provider: string; model: string; } const defaultKnowledgeBaseForm: KnowledgeBaseForm = { name: '', description: '', embedding_model: '', chunk_size: 512, chunk_overlap: 50, top_k: 3, is_active: true, }; const defaultKnowledgeDocumentForm = { title: '', content: '', metadata: '', }; export function KnowledgeBases() { const { t } = useTranslation(); const { currentProject } = useProjectStore(); const [isLoadingKnowledgeBases, setIsLoadingKnowledgeBases] = useState(false); const [isSavingKnowledgeBase, setIsSavingKnowledgeBase] = useState(false); const [deletingKnowledgeBaseId, setDeletingKnowledgeBaseId] = useState(''); const [reindexingKnowledgeBaseId, setReindexingKnowledgeBaseId] = useState(''); const [knowledgeBases, setKnowledgeBases] = useState([]); const [editingKnowledgeBaseId, setEditingKnowledgeBaseId] = useState(''); const [knowledgeBaseForm, setKnowledgeBaseForm] = useState(defaultKnowledgeBaseForm); const [selectedKnowledgeBaseId, setSelectedKnowledgeBaseId] = useState(''); const [knowledgeDocuments, setKnowledgeDocuments] = useState([]); const [isLoadingKnowledgeDocuments, setIsLoadingKnowledgeDocuments] = useState(false); const [isSavingKnowledgeDocument, setIsSavingKnowledgeDocument] = useState(false); const [deletingKnowledgeDocumentId, setDeletingKnowledgeDocumentId] = useState(''); const [editingKnowledgeDocumentId, setEditingKnowledgeDocumentId] = useState(''); const [knowledgeDocumentForm, setKnowledgeDocumentForm] = useState(defaultKnowledgeDocumentForm); const [uploadingKnowledgeDocuments, setUploadingKnowledgeDocuments] = useState(false); const [knowledgeUploadFiles, setKnowledgeUploadFiles] = useState([]); const [embeddingModels, setEmbeddingModels] = useState([]); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); useEffect(() => { void fetchKnowledgeBases(); }, [currentProject?.id]); useEffect(() => { void fetchEmbeddingModels(); }, []); const fetchEmbeddingModels = async () => { try { const data = await api.get('/api/v1/embedding-models'); setEmbeddingModels(data); } catch (err) { console.error('Failed to fetch embedding models', err); } }; const fetchKnowledgeBases = async () => { if (!currentProject) { setKnowledgeBases([]); return; } setIsLoadingKnowledgeBases(true); try { const data = await api.get(`/api/v1/knowledge-bases?project_id=${currentProject.id}`); setKnowledgeBases(data); if (editingKnowledgeBaseId && !data.find((item) => item.id === editingKnowledgeBaseId)) { setEditingKnowledgeBaseId(''); setKnowledgeBaseForm(defaultKnowledgeBaseForm); } if (selectedKnowledgeBaseId && !data.find((item) => item.id === selectedKnowledgeBaseId)) { setSelectedKnowledgeBaseId(''); setKnowledgeDocuments([]); setEditingKnowledgeDocumentId(''); setKnowledgeDocumentForm(defaultKnowledgeDocumentForm); } } catch (err: any) { setError(err.message || t('knowledgeBaseLoadFailed', 'Failed to load knowledge bases')); } finally { setIsLoadingKnowledgeBases(false); } }; const resetKnowledgeBaseForm = () => { setEditingKnowledgeBaseId(''); setKnowledgeBaseForm(defaultKnowledgeBaseForm); }; const validateKnowledgeBaseForm = () => { if (!currentProject) { return t('selectProjectBeforeManageKnowledgeBase', 'Please select a project before managing knowledge bases'); } if (!knowledgeBaseForm.name.trim()) { return t('knowledgeBaseNameRequired', 'Knowledge base name is required'); } if (knowledgeBaseForm.chunk_size < 64 || knowledgeBaseForm.chunk_size > 4096) { return t('knowledgeBaseChunkSizeRange', 'Chunk size must be between 64 and 4096'); } if (knowledgeBaseForm.chunk_overlap < 0 || knowledgeBaseForm.chunk_overlap > 512) { return t('knowledgeBaseChunkOverlapRange', 'Chunk overlap must be between 0 and 512'); } if (knowledgeBaseForm.chunk_overlap >= knowledgeBaseForm.chunk_size) { return t('knowledgeBaseChunkOverlapTooLarge', 'Chunk overlap must be less than chunk size'); } if (knowledgeBaseForm.top_k < 1 || knowledgeBaseForm.top_k > 20) { return t('knowledgeBaseTopKRange', 'Top K must be between 1 and 20'); } return ''; }; const handleSaveKnowledgeBase = async () => { setError(''); setSuccess(''); const validationMessage = validateKnowledgeBaseForm(); if (validationMessage) { setError(validationMessage); return; } if (!currentProject) return; setIsSavingKnowledgeBase(true); try { const payload = { name: knowledgeBaseForm.name.trim(), description: knowledgeBaseForm.description.trim() || null, embedding_model: knowledgeBaseForm.embedding_model.trim() || null, chunk_size: knowledgeBaseForm.chunk_size, chunk_overlap: knowledgeBaseForm.chunk_overlap, top_k: knowledgeBaseForm.top_k, is_active: knowledgeBaseForm.is_active, project_id: currentProject.id, }; if (editingKnowledgeBaseId) { await api.put(`/api/v1/knowledge-bases/${editingKnowledgeBaseId}`, payload); setSuccess(t('knowledgeBaseUpdated', 'Knowledge base updated successfully')); } else { await api.post('/api/v1/knowledge-bases', payload); setSuccess(t('knowledgeBaseCreated', 'Knowledge base created successfully')); } await fetchKnowledgeBases(); resetKnowledgeBaseForm(); } catch (err: any) { setError(err.message || t('knowledgeBaseSaveFailed', 'Failed to save knowledge base')); } finally { setIsSavingKnowledgeBase(false); } }; const handleEditKnowledgeBase = (item: KnowledgeBase) => { setEditingKnowledgeBaseId(item.id); setKnowledgeBaseForm({ name: item.name || '', description: item.description || '', embedding_model: item.embedding_model || '', chunk_size: item.chunk_size, chunk_overlap: item.chunk_overlap, top_k: item.top_k, is_active: item.is_active, }); }; const handleDeleteKnowledgeBase = async (id: string) => { if (!window.confirm(t('confirmDeleteKnowledgeBase', 'Are you sure to delete this knowledge base?'))) { return; } setError(''); setSuccess(''); setDeletingKnowledgeBaseId(id); try { await api.delete(`/api/v1/knowledge-bases/${id}`); setSuccess(t('knowledgeBaseDeleted', 'Knowledge base deleted successfully')); if (editingKnowledgeBaseId === id) { resetKnowledgeBaseForm(); } if (selectedKnowledgeBaseId === id) { setSelectedKnowledgeBaseId(''); setKnowledgeDocuments([]); setEditingKnowledgeDocumentId(''); setKnowledgeDocumentForm(defaultKnowledgeDocumentForm); } await fetchKnowledgeBases(); } catch (err: any) { setError(err.message || t('knowledgeBaseDeleteFailed', 'Failed to delete knowledge base')); } finally { setDeletingKnowledgeBaseId(''); } }; const handleReindexKnowledgeBase = async (id: string) => { setError(''); setSuccess(''); setReindexingKnowledgeBaseId(id); try { await api.post(`/api/v1/knowledge-bases/${id}/reindex`, {}); setSuccess(t('knowledgeBaseReindexSuccess', 'Knowledge base reindexed successfully')); } catch (err: any) { setError(err.message || t('knowledgeBaseReindexFailed', 'Failed to reindex knowledge base')); } finally { setReindexingKnowledgeBaseId(''); } }; const resetKnowledgeDocumentForm = () => { setEditingKnowledgeDocumentId(''); setKnowledgeDocumentForm(defaultKnowledgeDocumentForm); }; const fetchKnowledgeDocuments = async (kbId: string) => { if (!kbId) { setKnowledgeDocuments([]); return; } setIsLoadingKnowledgeDocuments(true); try { const data = await api.get(`/api/v1/knowledge-bases/${kbId}/documents`); setKnowledgeDocuments(data); if (editingKnowledgeDocumentId && !data.find((item) => item.id === editingKnowledgeDocumentId)) { resetKnowledgeDocumentForm(); } } catch (err: any) { setError(err.message || t('knowledgeDocumentLoadFailed', 'Failed to load knowledge documents')); } finally { setIsLoadingKnowledgeDocuments(false); } }; const handleOpenKnowledgeDocuments = async (kbId: string) => { if (selectedKnowledgeBaseId === kbId) { setSelectedKnowledgeBaseId(''); setKnowledgeDocuments([]); resetKnowledgeDocumentForm(); return; } setSelectedKnowledgeBaseId(kbId); resetKnowledgeDocumentForm(); await fetchKnowledgeDocuments(kbId); }; const validateKnowledgeDocumentForm = () => { if (!selectedKnowledgeBaseId) { return t('selectKnowledgeBaseToManageDocuments', 'Please select a knowledge base to manage documents'); } if (!knowledgeDocumentForm.title.trim()) { return t('knowledgeDocumentTitleRequired', 'Document title is required'); } if (!knowledgeDocumentForm.content.trim()) { return t('knowledgeDocumentContentRequired', 'Document content is required'); } const metadataText = knowledgeDocumentForm.metadata.trim(); if (!metadataText) { return ''; } try { JSON.parse(metadataText); return ''; } catch { return t('knowledgeDocumentMetadataInvalid', 'Document metadata must be valid JSON'); } }; const handleSaveKnowledgeDocument = async () => { setError(''); setSuccess(''); const validationMessage = validateKnowledgeDocumentForm(); if (validationMessage) { setError(validationMessage); return; } if (!selectedKnowledgeBaseId) return; setIsSavingKnowledgeDocument(true); try { const metadataText = knowledgeDocumentForm.metadata.trim(); const payload = { title: knowledgeDocumentForm.title.trim(), content: knowledgeDocumentForm.content.trim(), metadata: metadataText ? JSON.parse(metadataText) : {}, }; if (editingKnowledgeDocumentId) { await api.put(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/${editingKnowledgeDocumentId}`, payload); setSuccess(t('knowledgeDocumentUpdated', 'Knowledge document updated successfully')); } else { await api.post(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents`, payload); setSuccess(t('knowledgeDocumentCreated', 'Knowledge document created successfully')); } await fetchKnowledgeDocuments(selectedKnowledgeBaseId); await fetchKnowledgeBases(); resetKnowledgeDocumentForm(); } catch (err: any) { setError(err.message || t('knowledgeDocumentSaveFailed', 'Failed to save knowledge document')); } finally { setIsSavingKnowledgeDocument(false); } }; const handleEditKnowledgeDocument = (item: KnowledgeDocument) => { setEditingKnowledgeDocumentId(item.id); setKnowledgeDocumentForm({ title: item.title || '', content: item.content || '', metadata: item.metadata && Object.keys(item.metadata).length > 0 ? JSON.stringify(item.metadata, null, 2) : '', }); }; const handleDeleteKnowledgeDocument = async (docId: string) => { if (!selectedKnowledgeBaseId) return; if (!window.confirm(t('confirmDeleteKnowledgeDocument', 'Are you sure to delete this document?'))) { return; } setError(''); setSuccess(''); setDeletingKnowledgeDocumentId(docId); try { await api.delete(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/${docId}`); if (editingKnowledgeDocumentId === docId) { resetKnowledgeDocumentForm(); } setSuccess(t('knowledgeDocumentDeleted', 'Knowledge document deleted successfully')); await fetchKnowledgeDocuments(selectedKnowledgeBaseId); await fetchKnowledgeBases(); } catch (err: any) { setError(err.message || t('knowledgeDocumentDeleteFailed', 'Failed to delete knowledge document')); } finally { setDeletingKnowledgeDocumentId(''); } }; const handleUploadKnowledgeDocuments = async () => { setError(''); setSuccess(''); if (!selectedKnowledgeBaseId) { setError(t('selectKnowledgeBaseToManageDocuments', 'Please select a knowledge base to manage documents')); return; } if (knowledgeUploadFiles.length === 0) { setError(t('knowledgeDocumentUploadEmpty', 'Please select files to upload')); return; } setUploadingKnowledgeDocuments(true); try { const formData = new FormData(); knowledgeUploadFiles.forEach((file) => formData.append('files', file)); await api.post(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/upload`, formData); setSuccess(t('knowledgeDocumentUploadSuccess', { count: knowledgeUploadFiles.length })); setKnowledgeUploadFiles([]); await fetchKnowledgeDocuments(selectedKnowledgeBaseId); await fetchKnowledgeBases(); } catch (err: any) { setError(err.message || t('knowledgeDocumentUploadFailed', 'Failed to upload knowledge documents')); } finally { setUploadingKnowledgeDocuments(false); } }; const selectedKnowledgeBase = knowledgeBases.find((item) => item.id === selectedKnowledgeBaseId) || null; const selectedEmbeddingModel = embeddingModels.find(m => m.id === knowledgeBaseForm.embedding_model); return (
{t('knowledgeBases')}
{error &&
{error}
} {success &&
{success}
} {t('knowledgeBaseSettings')} {t('knowledgeBaseSettingsDesc')} {!currentProject ? (
{t('selectProjectBeforeManageKnowledgeBase')}
) : null}
setKnowledgeBaseForm((prev) => ({ ...prev, name: e.target.value }))} disabled={!currentProject} />
setKnowledgeBaseForm((prev) => ({ ...prev, description: e.target.value }))} disabled={!currentProject} />
setKnowledgeBaseForm((prev) => ({ ...prev, chunk_size: Number(e.target.value) || 0 }))} disabled={!currentProject} />
setKnowledgeBaseForm((prev) => ({ ...prev, chunk_overlap: Number(e.target.value) || 0 }))} disabled={!currentProject} />
setKnowledgeBaseForm((prev) => ({ ...prev, top_k: Number(e.target.value) || 0 }))} disabled={!currentProject} />
setKnowledgeBaseForm((prev) => ({ ...prev, is_active: checked }))} disabled={!currentProject} />
{editingKnowledgeBaseId ? ( ) : null}
{t('knowledgeBaseList')}
{isLoadingKnowledgeBases ? (
) : knowledgeBases.length === 0 ? (
{t('noKnowledgeBases')}
) : (
{knowledgeBases.map((item) => (
{item.name}
{t('knowledgeBaseMeta', { count: item.documents?.length || 0, updatedAt: new Date(item.updated_at).toLocaleString(), })}
{item.description ? (
{item.description}
) : null}
))}
)}
{selectedKnowledgeBase ? t('knowledgeDocumentManagerTitle', { name: selectedKnowledgeBase.name }) : t('knowledgeDocumentManagerTitleEmpty')}
{!selectedKnowledgeBase ? (
{t('selectKnowledgeBaseToManageDocuments')}
) : (
{t('knowledgeDocumentUploadTitle')}
setKnowledgeUploadFiles(Array.from(e.target.files || []))} disabled={uploadingKnowledgeDocuments} />
{t('knowledgeDocumentUploadHint')}
{knowledgeUploadFiles.length > 0 ? t('knowledgeDocumentUploadSelected', { count: knowledgeUploadFiles.length }) : t('knowledgeDocumentUploadNone')}
setKnowledgeDocumentForm((prev) => ({ ...prev, title: e.target.value }))} disabled={isSavingKnowledgeDocument} />