import { useState, useEffect } from "react"; import { Upload, Trash2, FileText, Check, Loader2, Database, X, ChevronLeft, Link, } from "lucide-react"; import { toast } from "sonner"; import { getDocuments, uploadDocument, processDocument, deleteDocument, getDocumentTypes, getDatabaseClientTypes, connectDatabase, getDatabaseClients, deleteDatabaseClient, ingestDatabaseClient, type ApiDocument, type DocumentStatus, type DocTypeInfo, type DbType, type DbTypeInfo, type DatabaseClient, } from "../../services/api"; interface KnowledgeManagementProps { open: boolean; onClose: () => void; } type View = "main" | "db-select" | "db-credentials"; 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; }; 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 [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); // ── DB credentials form state ─────────────────────────────────────────────── const [connectionName, setConnectionName] = useState(""); const [dbForm, setDbForm] = useState>({}); const [connecting, setConnecting] = useState(false); 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([ { doc_type: "pdf", max_size: 10, status: "active", message: null }, { doc_type: "csv", max_size: 10, status: "active", message: null }, { doc_type: "xlsx", max_size: 10, status: "active", message: null }, ]) ); }, [open]); const acceptedExtensions = docTypes.map((t) => `.${t.doc_type}`).join(","); const supportedFormatsText = docTypes.map((t) => t.doc_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); setUploadError(null); for (let i = 0; i < files.length; i++) { const file = files[i]; try { const uploadRes = await uploadDocument(userId, file); const newDoc: ApiDocument = { id: uploadRes.data.id, filename: uploadRes.data.filename, status: "pending", file_size: file.size, file_type: file.name.split(".").pop() ?? "", created_at: new Date().toISOString(), }; setDocuments((prev) => [newDoc, ...prev]); await processDocumentById(userId, uploadRes.data.id); } catch (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 ) ); try { await processDocument(userId, docId); setDocuments((prev) => prev.map((d) => d.id === docId ? { ...d, status: "completed" as DocumentStatus } : d ) ); } 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 ( ); }; 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}` : "Knowledge Base"; const headerBack = view === "db-select" ? () => setView("main") : view === "db-credentials" ? () => setView("db-select") : null; // ── Render ─────────────────────────────────────────────────────────────────── return (
{/* Header */}
{headerBack ? ( ) : (
)}

{headerTitle}

{/* Content */}
{/* ── VIEW: main ── */} {view === "main" && (
{/* Upload zone */} {uploadError && (

{uploadError}

)} {/* Connect DB row */} {/* Database connections list */} {dbClients.length > 0 && (
Databases · {dbClients.length}
{dbClients.map((client) => (
{client.db_type}

{client.name}

{client.db_type} · {client.status}

))}
)} {/* Documents list */}
Documents · {documents.length} {documents.length > 0 && ( )}
{loadingDocs ? (
) : docsError ? (

{docsError}

) : documents.length === 0 ? (

No documents yet

Upload files to get started

) : (
{documents.map((doc) => (

{doc.filename}

{formatFileSize(doc.file_size)} · {formatDate(doc.created_at)}

{renderStatus(doc)}
))}
)}
)} {/* ── VIEW: db-select ── */} {view === "db-select" && (

Choose a database to connect to your knowledge base.

{loadingDbTypes ? (
) : (
{dbTypeInfos.map((info) => { const active = info.status === "active"; return ( ); })}
)}
)} {/* ── VIEW: db-credentials ── */} {view === "db-credentials" && (
{/* Selected DB chip */} {selectedDbInfo && (
{selectedDbInfo.display_name} {selectedDbInfo.display_name}
)}
{/* Connection Name */}
setConnectionName(e.target.value)} className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition" />
{/* Dynamic fields from API */} {selectedDbInfo?.fields.map((field) => (
{field.type === "select" ? ( ) : field.type === "boolean" ? (
setDbForm((f) => ({ ...f, [field.name]: e.target.checked }))} className="w-4 h-4 accent-[#FF8F00]" />
) : field.name === "service_account_json" ? (