import { useState, useEffect, useRef } from "react"; import { Upload, Trash2, FileText, FileSpreadsheet, File, Check, Loader2, Database, X, ChevronLeft, Link, Table, } from "lucide-react"; import { toast } from "sonner"; import { getDocuments, uploadDocument, processDocument, deleteDocument, getDocumentTypes, getDatabaseClientTypes, connectDatabase, getDatabaseClients, deleteDatabaseClient, ingestDatabaseClient, getDataCatalog, type ApiDocument, type DocumentStatus, type DocTypeInfo, type DbType, type DbTypeInfo, type DatabaseClient, type DataCatalogSource, } from "../../services/api"; interface KnowledgeManagementProps { open: boolean; onClose: () => void; } type View = "main" | "db-select" | "db-credentials" | "catalog"; const LOGO_MAP: Record = { postgres: "https://cdn.simpleicons.org/postgresql/336791", mysql: "https://cdn.simpleicons.org/mysql/4479A1", supabase: "https://cdn.simpleicons.org/supabase/3ECF8E", sqlserver: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/microsoftsqlserver/microsoftsqlserver-plain.svg", bigquery: "https://cdn.simpleicons.org/googlebigquery/4285F4", snowflake: "https://cdn.simpleicons.org/snowflake/29B5E8", }; const getUserId = (): string | null => { const stored = localStorage.getItem("chatbot_user"); if (!stored) return null; return (JSON.parse(stored).user_id as string) ?? null; }; const FILE_ICON_MAP: Record = { pdf: { icon: FileText, bg: "bg-red-50", color: "text-red-400" }, docx: { icon: FileText, bg: "bg-blue-50", color: "text-blue-400" }, txt: { icon: File, bg: "bg-slate-100", color: "text-slate-400" }, csv: { icon: FileSpreadsheet, bg: "bg-green-50", color: "text-green-500" }, xlsx: { icon: FileSpreadsheet, bg: "bg-emerald-50",color: "text-emerald-500"}, }; const getFileIcon = (fileType: string) => FILE_ICON_MAP[fileType.toLowerCase()] ?? { icon: File, bg: "bg-slate-100", color: "text-slate-400" }; export default function KnowledgeManagement({ open, onClose, }: KnowledgeManagementProps) { // ── Document state ────────────────────────────────────────────────────────── const [docTypes, setDocTypes] = useState([]); const [documents, setDocuments] = useState([]); const [loadingDocs, setLoadingDocs] = useState(false); const [docsError, setDocsError] = useState(null); const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadError, setUploadError] = useState(null); const [processing, setProcessing] = useState(null); const [deleting, setDeleting] = useState(null); // ── Navigation state ──────────────────────────────────────────────────────── const [view, setView] = useState("main"); const [selectedDbType, setSelectedDbType] = useState(null); // ── DB type & client state ────────────────────────────────────────────────── const [dbTypeInfos, setDbTypeInfos] = useState([]); const [dbClients, setDbClients] = useState([]); const [loadingDbTypes, setLoadingDbTypes] = useState(false); const [ingesting, setIngesting] = useState(null); const [deletingClient, setDeletingClient] = useState(null); const pollingTimers = useRef>>(new Map()); // ── Data Catalog state ────────────────────────────────────────────────────── const [catalogSources, setCatalogSources] = useState([]); const [loadingCatalog, setLoadingCatalog] = useState(false); // ── DB credentials form state ─────────────────────────────────────────────── const [connectionName, setConnectionName] = useState(""); const [dbForm, setDbForm] = useState>({}); const [connecting, setConnecting] = useState(false); useEffect(() => { return () => { pollingTimers.current.forEach((timer) => clearInterval(timer)); pollingTimers.current.clear(); }; }, []); const startPollingDocument = (userId: string, docId: string) => { if (pollingTimers.current.has(docId)) return; const timer = setInterval(async () => { try { const docs = await getDocuments(userId); const updated = docs.find((d) => d.id === docId); if (updated && (updated.status === "completed" || updated.status === "failed")) { clearInterval(timer); pollingTimers.current.delete(docId); setDocuments((prev) => prev.map((d) => (d.id === docId ? { ...d, status: updated.status } : d)) ); } } catch { // ignore transient errors, keep polling } }, 3000); pollingTimers.current.set(docId, timer); }; useEffect(() => { if (!open) return; const userId = getUserId(); if (!userId) return; loadDocuments(userId); loadDbData(userId); getDocumentTypes() .then((types) => setDocTypes(types.filter((t) => t.status === "active"))) .catch(() => setDocTypes([ { type: "pdf", max_size_mb: 10, status: "active", message: null }, { type: "csv", max_size_mb: 10, status: "active", message: null }, { type: "xlsx", max_size_mb: 10, status: "active", message: null }, ]) ); }, [open]); const acceptedExtensions = docTypes.map((t) => `.${t.type}`).join(","); const supportedFormatsText = docTypes.map((t) => t.type.toUpperCase()).join(", "); const loadDbData = async (userId: string) => { setLoadingDbTypes(true); try { const [types, clients] = await Promise.all([ getDatabaseClientTypes(), getDatabaseClients(userId), ]); setDbTypeInfos(types); setDbClients(clients); } catch { // non-blocking; silently fail } finally { setLoadingDbTypes(false); } }; const handleClose = () => { setView("main"); setSelectedDbType(null); setConnectionName(""); setDbForm({}); onClose(); }; // ── Document handlers ─────────────────────────────────────────────────────── const loadDocuments = async (userId: string) => { setLoadingDocs(true); setDocsError(null); try { setDocuments(await getDocuments(userId)); } catch (err) { setDocsError( err instanceof Error ? err.message : "Failed to load documents" ); } finally { setLoadingDocs(false); } }; const handleFileUpload = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; const userId = getUserId(); if (!userId) return; setUploading(true); setUploadProgress(0); setUploadError(null); for (let i = 0; i < files.length; i++) { const file = files[i]; console.log(`[upload] ── file selected: "${file.name}", ${(file.size / 1024).toFixed(1)} KB`); const tClick = performance.now(); try { console.log(`[upload] ── calling uploadDocument (pre-fetch delay: ${(performance.now() - tClick).toFixed(0)} ms)`); const uploadRes = await uploadDocument(userId, file, setUploadProgress); console.log(`[upload] ── uploadDocument returned, updating UI`); const newDoc: ApiDocument = { ...uploadRes.data, id: uploadRes.data.id, }; setDocuments((prev) => [newDoc, ...prev]); console.log(`[upload] ── done in ${(performance.now() - tClick).toFixed(0)} ms total`); } catch (err) { console.error(`[upload] ✗ failed after ${(performance.now() - tClick).toFixed(0)} ms:`, err); setUploadError(err instanceof Error ? err.message : "Upload failed"); } } setUploading(false); e.target.value = ""; }; const processDocumentById = async (userId: string, docId: string) => { setProcessing(docId); setDocuments((prev) => prev.map((d) => d.id === docId ? { ...d, status: "processing" as DocumentStatus } : d ) ); const tProcess = performance.now(); try { console.log(`[process] ── calling processDocument for doc: ${docId}`); await processDocument(userId, docId); console.log(`[process] ── accepted in ${(performance.now() - tProcess).toFixed(0)} ms, polling started`); startPollingDocument(userId, docId); } catch { setDocuments((prev) => prev.map((d) => d.id === docId ? { ...d, status: "failed" as DocumentStatus } : d ) ); } finally { setProcessing(null); } }; const handleDeleteDocument = async (docId: string) => { const userId = getUserId(); if (!userId) return; setDeleting(docId); try { await deleteDocument(userId, docId); setDocuments((prev) => prev.filter((d) => d.id !== docId)); } catch (err) { console.error("Delete failed:", err); } finally { setDeleting(null); } }; const deleteAllDocuments = async () => { if (!window.confirm("Are you sure you want to delete all documents?")) return; const userId = getUserId(); if (!userId) return; for (const doc of documents) { try { await deleteDocument(userId, doc.id); } catch { // continue deleting others } } setDocuments([]); }; // ── DB handlers ───────────────────────────────────────────────────────────── const handleDbConnect = async () => { const userId = getUserId(); if (!userId || !selectedDbType || !connectionName.trim()) return; setConnecting(true); try { await connectDatabase(userId, selectedDbType, connectionName.trim(), dbForm); const clients = await getDatabaseClients(userId); setDbClients(clients); toast.success("Database connected successfully"); setView("main"); setConnectionName(""); setDbForm({}); } catch (err) { toast.error( err instanceof Error ? err.message : "Failed to connect to database" ); } finally { setConnecting(false); } }; const handleIngest = async (clientId: string) => { const userId = getUserId(); if (!userId) return; setIngesting(clientId); try { const res = await ingestDatabaseClient(clientId, userId); toast.success(`Ingested ${res.chunks_ingested} chunks successfully`); } catch (err) { toast.error(err instanceof Error ? err.message : "Ingestion failed"); } finally { setIngesting(null); } }; const handleDeleteClient = async (clientId: string) => { const userId = getUserId(); if (!userId) return; setDeletingClient(clientId); try { await deleteDatabaseClient(clientId, userId); setDbClients((prev) => prev.filter((c) => c.id !== clientId)); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to delete connection"); } finally { setDeletingClient(null); } }; // ── Helpers ───────────────────────────────────────────────────────────────── const formatFileSize = (bytes: number) => { if (bytes < 1024) return bytes + " B"; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB"; return (bytes / (1024 * 1024)).toFixed(2) + " MB"; }; const formatDate = (isoString: string) => { return new Date(isoString).toLocaleString(); }; const renderStatus = (doc: ApiDocument) => { if (doc.status === "completed") { return (
Processed
); } if (doc.status === "processing" || processing === doc.id) { return (
Processing...
); } return ( ); }; const handleViewCatalog = async () => { const uid = getUserId(); setLoadingCatalog(true); setView("catalog"); try { if (!uid) throw new Error("no user"); const catalog = await getDataCatalog(uid); setCatalogSources(catalog.sources.filter((s) => s.source_type !== "unstructured")); } catch { setCatalogSources([]); } finally { setLoadingCatalog(false); } }; if (!open) return null; // ── Header title & back button logic ──────────────────────────────────────── const selectedDbInfo = dbTypeInfos.find((d) => d.db_type === selectedDbType); const headerTitle = view === "db-select" ? "Connect Database" : view === "db-credentials" ? `Connect to ${selectedDbInfo?.display_name ?? selectedDbType}` : view === "catalog" ? "Data Catalog" : "Knowledge Base"; const headerBack = view === "db-select" ? () => setView("main") : view === "db-credentials" ? () => setView("db-select") : view === "catalog" ? () => setView("main") : null; // ── Render ─────────────────────────────────────────────────────────────────── return (
{/* Header */}
{headerBack ? ( ) : (
)}

{headerTitle}

{view === "main" && (