"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { X, Upload, Database, Trash2, RefreshCcw, Loader2, CheckCircle2, AlertTriangle, Pencil, Check, } from "lucide-react"; type TableInfo = { name: string; rows: number | null; columns: string[] }; export function KnowledgeBaseModal({ open, onClose, onChange, }: { open: boolean; onClose: () => void; onChange?: (version: number) => void; }) { const [tables, setTables] = useState([]); const [version, setVersion] = useState(0); const [loading, setLoading] = useState(false); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [renameTarget, setRenameTarget] = useState(null); const [renameValue, setRenameValue] = useState(""); const [uploadMode, setUploadMode] = useState<"replace" | "append">("replace"); const [tableNameOverride, setTableNameOverride] = useState(""); const [dragOver, setDragOver] = useState(false); const fileInputRef = useRef(null); const fetchTables = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch("/api/backend/kb/tables"); if (!res.ok) throw new Error(`HTTP ${res.status}`); const d = await res.json(); setTables(d.tables ?? []); setVersion(d.version ?? 0); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } }, []); useEffect(() => { if (open) fetchTables(); }, [open, fetchTables]); // Auto-clear toast messages useEffect(() => { if (!success) return; const t = setTimeout(() => setSuccess(null), 3000); return () => clearTimeout(t); }, [success]); const notifyChange = useCallback( (v: number) => { onChange?.(v); // Broadcast a window-level event so other components (LiveDashboard, // chat results, etc.) can refetch when the KB changes. try { window.dispatchEvent(new CustomEvent("finai:kb-changed", { detail: { version: v } })); } catch {} }, [onChange] ); const doUpload = useCallback( async (file: File) => { setBusy(`upload:${file.name}`); setError(null); setSuccess(null); try { const fd = new FormData(); fd.append("file", file); if (tableNameOverride.trim()) fd.append("table", tableNameOverride.trim()); const url = `/api/backend/kb/upload?mode=${uploadMode}${ tableNameOverride.trim() ? `&table=${encodeURIComponent(tableNameOverride.trim())}` : "" }`; const res = await fetch(url, { method: "POST", body: fd }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.detail || `HTTP ${res.status}`); setSuccess(`Loaded ${d.rows} rows into ${d.table}`); setTableNameOverride(""); await fetchTables(); notifyChange(d.version ?? version + 1); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(null); if (fileInputRef.current) fileInputRef.current.value = ""; } }, [tableNameOverride, uploadMode, fetchTables, notifyChange, version] ); const doDelete = useCallback( async (name: string) => { if (!confirm(`Drop table "${name}"? This cannot be undone.`)) return; setBusy(`delete:${name}`); setError(null); try { const res = await fetch(`/api/backend/kb/tables/${encodeURIComponent(name)}`, { method: "DELETE", }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.detail || `HTTP ${res.status}`); setSuccess(`Dropped ${name}`); await fetchTables(); notifyChange(d.version ?? version + 1); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(null); } }, [fetchTables, notifyChange, version] ); const doRename = useCallback( async (oldName: string, newName: string) => { if (!newName.trim() || newName === oldName) { setRenameTarget(null); return; } setBusy(`rename:${oldName}`); setError(null); try { const res = await fetch("/api/backend/kb/rename", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ old_name: oldName, new_name: newName.trim() }), }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.detail || `HTTP ${res.status}`); setSuccess(`Renamed ${oldName} → ${newName}`); setRenameTarget(null); await fetchTables(); notifyChange(d.version ?? version + 1); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(null); } }, [fetchTables, notifyChange, version] ); if (!open) return null; return ( e.stopPropagation()} className="flex max-h-[90vh] w-full max-w-4xl flex-col overflow-hidden rounded-2xl border border-mb-amber-200/60 bg-white shadow-2xl" > {/* Header */}

Knowledge Base

Add, rename, or remove tables · changes propagate everywhere

v{version}
{/* Toasts */} {(error || success) && (
{error && (
{error}
)} {success && (
{success}
)}
)} {/* Body */}
{/* Tables list */}
Tables ({tables.length})
{loading && tables.length === 0 ? (
) : tables.length === 0 ? (
No tables yet. Upload a CSV to get started →
) : (
    {tables.map((t) => (
  • {renameTarget === t.name ? (
    setRenameValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") doRename(t.name, renameValue); else if (e.key === "Escape") setRenameTarget(null); }} className="min-w-0 flex-1 rounded-md border border-mb-amber-300 px-2 py-0.5 text-[13px] outline-none focus:border-mb-amber-500" />
    ) : (
    {t.name} {t.rows == null ? "—" : t.rows.toLocaleString("en-IN")} rows
    )} {t.columns.length > 0 && renameTarget !== t.name && (

    {t.columns.slice(0, 8).join(", ")} {t.columns.length > 8 ? ` … +${t.columns.length - 8}` : ""}

    )}
    {renameTarget !== t.name && (
    )}
  • ))}
)}
{/* Upload pane */}
Upload CSV
setTableNameOverride(e.target.value)} placeholder="auto-derived from filename" className="w-full rounded-md border border-mb-amber-200 px-2.5 py-1.5 text-[13px] outline-none focus:border-mb-amber-500" />
{(["replace", "append"] as const).map((m) => ( ))}

{uploadMode === "replace" ? "Drops existing table (if any) and creates a new one." : "Appends rows to the existing table (must already exist)."}

Once you upload, the dashboard, chat suggestions, and query engine all see the new tables instantly — no restart needed.
); }