import { useState, useEffect, useCallback, useRef } from 'react'; import { api } from '../api'; export default function KBPanel() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [docs, setDocs] = useState(0); const [chunks, setChunks] = useState(0); const [modality, setModality] = useState('all'); const [topK, setTopK] = useState(10); const [threshold, setThreshold] = useState(0.35); const [mode, setMode] = useState('naive'); const [ingestMode, setIngestMode] = useState('multimodal'); const [mmMethod, setMmMethod] = useState('pure'); const [chunkSize, setChunkSize] = useState(512); const [chunkOverlap, setChunkOverlap] = useState(50); const [enableKG, setEnableKG] = useState(false); const [preview, setPreview] = useState(null); const [previewTab, setPreviewTab] = useState('results'); const [indexedFiles, setIndexedFiles] = useState([]); const [fileFilter, setFileFilter] = useState(''); const [uploadFiles, setUploadFiles] = useState([]); const [uploading, setUploading] = useState(false); const [ingestSource, setIngestSource] = useState('upload'); const [ingestText, setIngestText] = useState(''); const [textSource, setTextSource] = useState('web_ui'); const [ingestTab, setIngestTab] = useState('upload'); const fileInputRef = useRef(null); const dropRef = useRef(null); const refreshStats = useCallback(async () => { try { const s = await api.stats(); setDocs(s.documents || 0); setChunks(s.chunks || 0); } catch {} }, []); useEffect(() => { refreshStats(); }, [refreshStats]); // Load indexed files from backend (cross-browser persistent) useEffect(() => { api.listDocuments().then(data => { const docs = (data.documents || []).map(d => ({ name: d.name, size: d.size, type: d.type, time: d.time, docId: d.doc_id, })); if (docs.length) setIndexedFiles(docs); }).catch(() => {}); }, []); const search = async () => { if (!query.trim()) return; try { const data = await api.search(query, topK, mode, modality); setResults(data.results || []); setPreviewTab('results'); } catch (e) { alert('Search failed: ' + e.message); } }; const uploadFile = async (file) => { try { const data = await api.uploadFile(file, ingestSource, ingestMode, mmMethod, chunkSize, chunkOverlap, enableKG); // Reload from server to get authoritative file list const docs = await api.listDocuments().then(r => (r.documents || []).map(d => ({ name: d.name, size: d.size, type: d.type, time: d.time, docId: d.doc_id, }))).catch(() => null); if (docs) setIndexedFiles(docs); refreshStats(); return true; } catch (e) { alert('Upload failed: ' + e.message); return false; } }; const uploadAll = async () => { setUploading(true); let ok = 0, fail = 0; for (const f of uploadFiles) { (await uploadFile(f)) ? ok++ : fail++; } setUploadFiles([]); setUploading(false); if (ok) alert(`${ok} uploaded${fail ? ', ' + fail + ' failed' : ''}`); }; const clearKB = async () => { if (!confirm('Delete ALL documents?')) return; await api.clearKB(); setIndexedFiles([]); refreshStats(); setResults([]); }; const deleteDoc = async (f, e) => { e?.stopPropagation(); if (f.docId) await api.deleteDocument(f.docId).catch(() => {}); // Reload from server const docs = await api.listDocuments().then(r => (r.documents || []).map(d => ({ name: d.name, size: d.size, type: d.type, time: d.time, docId: d.doc_id, }))).catch(() => null); if (docs) setIndexedFiles(docs); refreshStats(); }; const previewFile = async (f) => { if (!f.docId) return; try { const data = await api.getDocument(f.docId); setPreview({ name: f.name, items: data.items || [] }); setPreviewTab('preview'); } catch { setPreview(null); } }; const ingestTextContent = async () => { if (!ingestText.trim()) return; try { await fetch('/api/v1/rag/ingest', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: ingestText, source: textSource }), }); // Reload from server const docs = await api.listDocuments().then(r => (r.documents || []).map(d => ({ name: d.name, size: d.size, type: d.type, time: d.time, docId: d.doc_id, }))).catch(() => null); if (docs) setIndexedFiles(docs); setIngestText(''); refreshStats(); } catch (e) { alert('Ingest failed: ' + e.message); } }; const handleDrop = (e) => { e.preventDefault(); [...e.dataTransfer.files].forEach(f => setUploadFiles(prev => [...prev, f])); }; const filteredFiles = fileFilter ? indexedFiles.filter(f => f.name.toLowerCase().includes(fileFilter.toLowerCase())) : indexedFiles; return (
{item.text?.slice(0, 500)}}