"use client"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ArrowLeft, BookOpen, Bot, ClipboardList, Layers, Library, MessageSquare, Network, NotebookPen, PenLine, Workflow, type LucideIcon, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { apiFetch, apiUrl } from "@/lib/api"; import { L1View } from "@/components/memory/MemorySection"; type Surface = | "chat" | "notebook" | "quiz" | "kb" | "book" | "partner" | "cowriter"; interface NavEntry { key: Surface; label: string; icon: LucideIcon; } const L1_NAV: NavEntry[] = [ { key: "chat", icon: MessageSquare, label: "Chat" }, { key: "notebook", icon: NotebookPen, label: "Notebook" }, { key: "quiz", icon: ClipboardList, label: "Quiz" }, { key: "kb", icon: BookOpen, label: "Knowledge base" }, { key: "book", icon: Library, label: "Book" }, { key: "partner", icon: Bot, label: "Partner" }, { key: "cowriter", icon: PenLine, label: "Co-writer" }, ]; interface SnapshotResponse { surface: Surface; entities: unknown[]; pending_changes: unknown[]; } export interface MemoryL1WorkbenchProps { initialSurface?: Surface; initialFocusRef?: string; } export default function MemoryL1Workbench({ initialSurface, initialFocusRef, }: MemoryL1WorkbenchProps) { const { t } = useTranslation(); // ``initialSurface`` and ``initialFocusRef`` seed the picker + entity // highlight on first mount. The URL only drives initial state; once the // user clicks around the rail it's all local state again. const [surface, setSurface] = useState(initialSurface || "notebook"); const [counts, setCounts] = useState>( {} as Record, ); const [pending, setPending] = useState>( {} as Record, ); const [focusRef, setFocusRef] = useState( initialFocusRef ?? null, ); const [toast, setToast] = useState(""); // Fetch counts + pending badges for the left rail in parallel. const loadCounts = useCallback(async () => { const entries = await Promise.all( L1_NAV.map(async ({ key }) => { try { const res = await apiFetch(apiUrl(`/api/v1/memory/snapshot/${key}`)); const data = (await res.json()) as SnapshotResponse; return [ key, data?.entities?.length ?? 0, data?.pending_changes?.length ?? 0, ] as const; } catch { return [key, 0, 0] as const; } }), ); setCounts( Object.fromEntries(entries.map(([k, n]) => [k, n])) as Record< Surface, number >, ); setPending( Object.fromEntries(entries.map(([k, , p]) => [k, p])) as Record< Surface, number >, ); }, []); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect void loadCounts(); }, [loadCounts]); // Re-fetch counts when the tab regains focus so the rail stays in sync. useEffect(() => { const refetch = () => { if (typeof document !== "undefined" && document.hidden) return; void loadCounts(); }; window.addEventListener("focus", refetch); document.addEventListener("visibilitychange", refetch); return () => { window.removeEventListener("focus", refetch); document.removeEventListener("visibilitychange", refetch); }; }, [loadCounts]); useEffect(() => { if (!toast) return; const id = setTimeout(() => setToast(""), 2500); return () => clearTimeout(id); }, [toast]); const nicelabel = useMemo(() => { const entry = L1_NAV.find((n) => n.key === surface); return entry ? t(entry.label) : surface; }, [surface, t]); return (
{/* Left rail */} {/* Center: snapshot / changes / kb-queries preview */}
{ setSurface(s as Surface); setFocusRef(null); }} focusRef={focusRef} onClearFocus={() => setFocusRef(null)} onToast={setToast} t={t} compact />
{toast && (
{toast}
)}
); } function LayerSwitcher({ t }: { t: (k: string) => string }) { // L1 is the current page, so it stays as a non-link pill; L2 + L3 // link to their respective hubs. const entries: { key: "L1" | "L2" | "L3"; href: string; icon: LucideIcon; label: string; }[] = [ { key: "L1", href: "/memory/l1", icon: Layers, label: t("L1") }, { key: "L2", href: "/memory/l2", icon: Workflow, label: t("L2") }, { key: "L3", href: "/memory/l3", icon: Network, label: t("L3") }, ]; return ( ); } function Breadcrumb({ label, t }: { label: string; t: (k: string) => string }) { return (
{t("Memory")} / {t("L1 ยท Workspace mirror")} / {label}
); }