import { useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { FileText, Newspaper, NotePencil, Stack, Trash } from "@phosphor-icons/react"; import { Button } from "@/components/ui/Button"; import { Section } from "@/components/ui/Card"; import { Collapsible } from "@/components/ui/Collapsible"; import { Markdown } from "@/components/ui/Markdown"; import { Spinner } from "@/components/ui/Spinner"; import { useAuth } from "@/context/AuthContext"; import { useSettings } from "@/context/SettingsContext"; import { useToast } from "@/context/ToastContext"; import { useWorkspace } from "@/context/WorkspaceContext"; import { useDocuments, useKbStats } from "@/hooks/useServerData"; import { useSingleFlight } from "@/hooks/useSingleFlight"; import { clearKnowledgeBase, deleteDocument, summarizeDocument } from "@/lib/api"; import { truncate } from "@/lib/utils"; export function KnowledgeBase() { const { session } = useAuth(); const { selectedModel, activeApiKey } = useSettings(); const toast = useToast(); const qc = useQueryClient(); const { setSuggestions } = useWorkspace(); const scope = session?.userId ?? "anonymous"; const { data: stats } = useKbStats(); const { data: docs } = useDocuments(); const [summaries, setSummaries] = useState>({}); const [busyTitle, setBusyTitle] = useState(null); const invalidate = () => { qc.invalidateQueries({ queryKey: ["documents", scope] }); qc.invalidateQueries({ queryKey: ["kb-stats", scope] }); }; // Latched: summarize is an LLM call, and `busyTitle` was written but never // checked, so repeated clicks on the same document each spent a completion. const { run: summarize } = useSingleFlight(async (title: string) => { if (!activeApiKey) { toast.warning("An API key is required to summarize. Set one above."); return; } setBusyTitle(title); try { const res = await summarizeDocument(title, activeApiKey, selectedModel); setSummaries((prev) => ({ ...prev, [title]: res.summary })); } catch (err) { toast.error((err as Error).message); } finally { setBusyTitle(null); } }); const remove = async (title: string) => { try { const res = await deleteDocument(title); setSummaries((prev) => { const next = { ...prev }; delete next[title]; return next; }); toast.success(`Deleted ${res.deleted} chunks.`); invalidate(); } catch (err) { toast.error((err as Error).message); } }; const clearAll = async () => { try { const res = await clearKnowledgeBase(); setSummaries({}); setSuggestions([]); toast.success(`Cleared ${res.cleared} chunks.`); invalidate(); } catch (err) { toast.error((err as Error).message); } }; const documents = docs?.documents ?? []; return (
}>
{documents.length > 0 && (
{documents.slice(0, 30).map((d) => (
{d.source === "upload" ? ( ) : ( )} {truncate(d.title, 30)}
))}
)} {Object.entries(summaries).map(([title, text]) => ( {text} ))}
); } function Metric({ label, value }: { label: string; value: number }) { return (
{value}
{label}
); }