"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { KnowledgeGraph } from "./KnowledgeGraph"; type Drawer = { id: string; content: string; wing: string; room: string; hall: string; source_file?: string; filed_at?: string; metadata?: Record; }; type Fact = { id: string; subject: string; predicate: string; object: string; confidence: number; valid_to?: string | null }; type Snapshot = { generated_at: string; status: { wings?: Record; knowledge_graph?: { entities: number; triples: number; current_facts: number }; config?: { history_entries: number } }; rooms: Record>; drawers: Drawer[]; facts: Fact[]; truncated?: { drawers: boolean; facts: boolean }; }; type SearchMatch = { id: string; content: string; metadata: Record; score: number }; const hallNames: Record = { hall_facts: "Facts", hall_events: "Events", hall_discoveries: "Discoveries", hall_preferences: "Preferences", hall_advice: "Advice", hall_general: "General", }; const HALL_OPTIONS = Object.keys(hallNames); function matchToDrawer(match: SearchMatch): Drawer { const metadata = match.metadata ?? {}; return { id: match.id, content: match.content, wing: typeof metadata.wing === "string" ? metadata.wing : "unknown", room: typeof metadata.room === "string" ? metadata.room : "general", hall: typeof metadata.hall === "string" ? metadata.hall : "hall_general", source_file: typeof metadata.source_file === "string" ? metadata.source_file : "", metadata, }; } export function MemoryDashboard({ cwd, onClose }: { cwd: string | null; onClose: () => void }) { const [snapshot, setSnapshot] = useState(null); const [query, setQuery] = useState(""); const [wing, setWing] = useState("all"); const [view, setView] = useState<"memories" | "graph">("memories"); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState(""); // Editing const [editingId, setEditingId] = useState(null); const [editDraft, setEditDraft] = useState(null); const [confirmingDeleteId, setConfirmingDeleteId] = useState(null); const [adding, setAdding] = useState(false); const [addDraft, setAddDraft] = useState({ wing: "equaxis", room: "general", hall: "hall_general", content: "" }); // Semantic search results (null = show local filtered view) const [semanticResults, setSemanticResults] = useState(null); const [searchingSemantic, setSearchingSemantic] = useState(false); const load = useCallback(() => { if (!cwd) return; setLoading(true); setError(null); fetch(`/api/memory?cwd=${encodeURIComponent(cwd)}`, { cache: "no-store" }) .then(async (response) => { const body = await response.json(); if (!response.ok) throw new Error(String(body?.error ?? `HTTP ${response.status}`)); const snapshotValue: Snapshot = body; setSnapshot(snapshotValue); }) .catch((reason: unknown) => setError(reason instanceof Error ? reason.message : String(reason))) .finally(() => setLoading(false)); }, [cwd]); useEffect(() => { load(); }, [cwd, load]); useEffect(() => { const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const showNotice = useCallback((message: string) => { setNotice(message); window.setTimeout(() => setNotice(""), 2500); }, []); const post = useCallback(async (action: string, payload: Record) => { if (!cwd) throw new Error("No workspace selected"); const response = await fetch("/api/memory", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cwd, action, ...payload }), }); const body = await response.json().catch(() => ({})); if (!response.ok) throw new Error(String(body?.error ?? `HTTP ${response.status}`)); return body; }, [cwd]); const saveEdit = useCallback(async () => { if (!editDraft) return; setBusy(true); setError(null); try { await post("update", { drawer_id: editDraft.id, content: editDraft.content, wing: editDraft.wing.trim() || editDraft.wing, room: editDraft.room.trim() || editDraft.room, hall: editDraft.hall, source_file: editDraft.source_file, }); setEditingId(null); setEditDraft(null); showNotice("Memory updated"); load(); } catch (reason: unknown) { setError(reason instanceof Error ? reason.message : String(reason)); } finally { setBusy(false); } }, [editDraft, post, load, showNotice]); const deleteDrawer = useCallback(async (drawerId: string) => { setBusy(true); setError(null); try { await post("delete", { drawer_id: drawerId }); setConfirmingDeleteId(null); showNotice("Memory deleted"); load(); } catch (reason: unknown) { setError(reason instanceof Error ? reason.message : String(reason)); } finally { setBusy(false); } }, [post, load, showNotice]); const addMemory = useCallback(async () => { const content = addDraft.content.trim(); if (!content) return; setBusy(true); setError(null); try { await post("remember", { content, wing: addDraft.wing.trim() || "equaxis", room: addDraft.room.trim() || "general", hall: addDraft.hall, source_file: "pi-web", }); setAddDraft({ wing: addDraft.wing.trim() || "equaxis", room: addDraft.room.trim() || "general", hall: addDraft.hall, content: "" }); setAdding(false); showNotice("Memory added"); load(); } catch (reason: unknown) { setError(reason instanceof Error ? reason.message : String(reason)); } finally { setBusy(false); } }, [addDraft, post, load, showNotice]); const runSemanticSearch = useCallback(async () => { const text = query.trim(); if (!text || !cwd) return; setSearchingSemantic(true); setError(null); try { const response = await fetch(`/api/memory?cwd=${encodeURIComponent(cwd)}&q=${encodeURIComponent(text)}`, { cache: "no-store" }); const body = await response.json(); if (!response.ok) throw new Error(String(body?.error ?? `HTTP ${response.status}`)); setSemanticResults((body?.matches as SearchMatch[] | undefined) ?? []); } catch (reason: unknown) { setError(reason instanceof Error ? reason.message : String(reason)); } finally { setSearchingSemantic(false); } }, [query, cwd]); const wings = Object.entries(snapshot?.status.wings ?? {}).sort((a, b) => b[1] - a[1]); const filtered = useMemo(() => { const text = query.trim().toLowerCase(); return (snapshot?.drawers ?? []).filter((item) => { if (wing !== "all" && item.wing !== wing) return false; return !text || `${item.content} ${item.wing} ${item.room} ${item.hall}`.toLowerCase().includes(text); }); }, [query, snapshot, wing]); const facts = useMemo(() => { const text = query.trim().toLowerCase(); return (snapshot?.facts ?? []).filter((fact) => !text || `${fact.subject} ${fact.predicate} ${fact.object}`.toLowerCase().includes(text)); }, [query, snapshot]); const total = Object.values(snapshot?.status.wings ?? {}).reduce((sum, count) => sum + count, 0); const visibleDrawers = semanticResults !== null ? semanticResults.map(matchToDrawer) : filtered; const scoreById = useMemo(() => { const map = new Map(); for (const match of semanticResults ?? []) map.set(match.id, match.score); return map; }, [semanticResults]); return (
EQUAXIS MEMORY

Memory atlas

{cwd ?? "No workspace selected"}

{notice && {notice}}
{error ?
{error}
: !snapshot ?
{loading ? "Loading memory..." : "Select an Equaxis workspace to inspect memory."}
: <>
Drawers{total}long-term entries
Wings{wings.length}memory namespaces
Facts{snapshot.status.knowledge_graph?.current_facts ?? 0}current graph links
History{snapshot.status.config?.history_entries ?? 0}short-term records
{ setQuery(event.target.value); if (!event.target.value.trim()) setSemanticResults(null); }} placeholder="Search memory" aria-label="Search memory" onKeyDown={(event) => { if (event.key === "Enter") void runSemanticSearch(); }} />
{adding && (