import { useState, useMemo, lazy, Suspense, useEffect, useDeferredValue } from "react" import { Trash2, Sparkles, User, Scroll, Shield, Clock, Search, Plus, BookOpen, Share2, List, MessagesSquare, X, Copy, Check, Terminal as TerminalIcon, Send, Maximize2, Minimize2 } from "lucide-react" import { api, type DedupeResult } from "@/lib/api" import { useMemory, useMemoryGraph, useQueryClient } from "@/lib/queries" import { useDialog } from "@/lib/useDialog" import { cn } from "@/lib/utils" import { GraphErrorBoundary } from "./GraphErrorBoundary" // Sigma/graphology nur laden, wenn der Graph-Tab geöffnet wird (Code-Splitting). const GraphView = lazy(() => import("./GraphView").then((m) => ({ default: m.GraphView }))) const CATEGORIES = ["identity", "knowledge", "rules", "events"] // Herkunft: automatisch gelernt (Mem0-Extraktion / Agent / Hermes) vs. manuell/Vorlage. const AUTO_SOURCES = new Set(["auto", "agent", "hermes"]) const CAT_CONFIG: Record = { identity: { label: "Identität", icon: User, bg: "bg-cyan-500/10", text: "text-cyan-400" }, knowledge: { label: "Wissen", icon: Shield, bg: "bg-indigo-500/10", text: "text-indigo-400" }, rules: { label: "Regeln", icon: Scroll, bg: "bg-violet-500/10", text: "text-violet-400" }, events: { label: "Ereignisse", icon: Clock, bg: "bg-amber-500/10", text: "text-amber-400" }, } const DEFAULT_CAT = { label: "Gedächtnis", icon: BookOpen, bg: "bg-muted/10", text: "text-muted-foreground" } const BORDER_CLASSES: Record = { identity: "border-l-cyan-500/80", knowledge: "border-l-indigo-500/80", rules: "border-l-violet-500/80", events: "border-l-amber-500/80", } // Hex-Farben für Inline-Styles (Graph-Punkte, Detail-Panel) — Tailwind-Klassen aus // CAT_CONFIG greifen dort nicht. const CAT_COLOR_MAP: Record = { identity: "#00f5ff", knowledge: "#3b82f6", rules: "#d946ef", events: "#fbbf24", } // Onboarding-Prompt: der Nutzer startet damit ein Gespräch mit Hermes (Terminal/Telegram). const ONBOARDING_PROMPT = `Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu: 1) wer ich bin und woran ich gerade arbeite, 2) wie ich angesprochen werden möchte, 3) meine bevorzugten Tools, Sprachen und Arbeitsweise, 4) wichtige Regeln/Konventionen, die du beachten sollst, 5) meine Infrastruktur (Server, Dienste – ohne Geheimnisse). Frag immer nur eine sache auf einmal und fass am Ende zusammen, was du dir gemerkt hast.` export function MemoryView() { const [filter, setFilter] = useState("") const [q, setQ] = useState("") const [content, setContent] = useState("") const [category, setCategory] = useState("knowledge") const [deduping, setDeduping] = useState(false) const [copied, setCopied] = useState(false) const [view, setView] = useState<"liste" | "graph">("liste") const [showAdd, setShowAdd] = useState(false) const [selected, setSelected] = useState(null) const [isExpanded, setIsExpanded] = useState(false) // Nur EINE Graph-Instanz mounten: die CSS-Klassen (hidden/lg:hidden) verstecken zwar, // aber eine per display:none versteckte ForceGraph2D-Instanz simuliert und rendert // trotzdem weiter — das hat die CPU-Last der Ansicht glatt verdoppelt. const [isDesktop, setIsDesktop] = useState( () => typeof window !== "undefined" && window.matchMedia("(min-width: 1024px)").matches ) useEffect(() => { const mq = window.matchMedia("(min-width: 1024px)") const onChange = (e: MediaQueryListEvent) => setIsDesktop(e.matches) mq.addEventListener("change", onChange) return () => mq.removeEventListener("change", onChange) }, []) const qc = useQueryClient() const { showAlert, showConfirm, dialogElement } = useDialog() const { data: allItems = [] } = useMemory({}) const { data: items = [], error: itemsErr } = useMemory({ q, category: filter }) const { data: graph } = useMemoryGraph(true) const error = itemsErr ? String(itemsErr) : "" // Escape-Taste schließt den Vollbildmodus useEffect(() => { if (!isExpanded) return const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setIsExpanded(false) } } window.addEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown) }, [isExpanded]) const reloadMemory = () => { qc.invalidateQueries({ queryKey: ["memory"] }) qc.invalidateQueries({ queryKey: ["memory-graph"] }) } const stats = useMemo(() => { const total = allItems.length const auto = allItems.filter((m) => AUTO_SOURCES.has(m.source)).length return { total, auto, manual: total - auto, cats: new Set(allItems.map((m) => m.category)).size } }, [allItems]) const empty = allItems.length === 0 // Graph clientseitig nach Suchtext filtern (Knoten + deren Kanten). // useDeferredValue: der Graph-Umbau (Re-Layout) hinkt der Tipp-Eingabe nach, // statt bei jedem Tastendruck die D3-Simulation neu anzuwerfen. const deferredQ = useDeferredValue(q) const graphData = useMemo(() => { const g = graph ?? { nodes: [], edges: [] } if (!deferredQ.trim()) return g const ql = deferredQ.toLowerCase() const ns = g.nodes.filter((n) => n.content.toLowerCase().includes(ql)) const ids = new Set(ns.map((n) => n.id)) return { nodes: ns, edges: g.edges.filter((e) => ids.has(e.source) && ids.has(e.target)) } }, [graph, deferredQ]) // Liste nach Kategorie gruppieren (skaliert: klare Sektionen statt einer flachen Wand). const grouped = useMemo(() => { const g: Record = {} items.forEach((m) => { (g[m.category] ??= []).push(m) }) return g }, [items]) const otherItems = useMemo(() => items.filter((m) => !CATEGORIES.includes(m.category)), [items]) async function add() { if (!content.trim()) return await api("/api/memory", { method: "POST", body: JSON.stringify({ content, category, source: "ui" }) }) setContent(""); setShowAdd(false); reloadMemory() } function del(id: string) { showConfirm("Eintrag löschen?", "Diesen Gedächtnis-Eintrag dauerhaft entfernen?", async () => { try { await api(`/api/memory/${id}`, { method: "DELETE" }); reloadMemory() } catch (e: any) { showAlert("Fehler", `Löschen fehlgeschlagen: ${e.message || e}`) } }) } async function copyPrompt() { try { await navigator.clipboard.writeText(ONBOARDING_PROMPT) setCopied(true) setTimeout(() => setCopied(false), 1800) } catch { showAlert("Kopieren fehlgeschlagen", "Markiere den Prompt und kopiere ihn manuell (Strg+C).") } } function goTerminal() { copyPrompt() // "konsole" ist die echte View-Id (nav.ts) — "terminal" existierte nie, der Knopf lief ins Leere. window.dispatchEvent(new CustomEvent("mc-navigate", { detail: { view: "konsole" } })) } async function cleanup() { setDeduping(true) try { const dry = await api("/api/memory/dedupe", { method: "POST", body: JSON.stringify({ apply: false }) }) if (dry.duplicate_count === 0) { showAlert("Ergebnis", "Keine Dubletten gefunden — alles sauber."); return } // Konkrete Vorschau statt bloßer Zahl: was bleibt, was fliegt (erste 5 Gruppen). const shorten = (s: string) => (s.length > 90 ? s.slice(0, 90) + "…" : s) const preview = dry.groups.slice(0, 5).map((g) => `✓ bleibt: „${shorten(g.keep.content)}"\n✗ weg: ${g.remove.map((r) => `„${shorten(r.content)}"`).join(", ")}` ).join("\n\n") + (dry.groups.length > 5 ? `\n\n… und ${dry.groups.length - 5} weitere Gruppe(n).` : "") showConfirm("Deduplizierung bestätigen", `${dry.duplicate_count} Dublette(n) in ${dry.groups.length} Gruppe(n) gefunden:\n\n${preview}\n\nZusammenführen?`, async () => { try { await api("/api/memory/dedupe", { method: "POST", body: JSON.stringify({ apply: true }) }); reloadMemory() } catch (e: any) { showAlert("Fehler", `Fehler beim Löschen: ${e.message}`) } }) } catch (e: any) { showAlert("Fehler", `Fehler bei der Deduplizierung: ${e.message}`) } finally { setDeduping(false) } } // Suche in ALLEN Elementen, damit Klicks auf dem Graphen nicht durch lokale Filter der linken Liste blockiert werden. const sel = useMemo(() => allItems.find((m) => m.id === selected), [allItems, selected]) const neighbors = useMemo(() => { if (!sel || !graph) return [] const connectedIds = new Set([ ...graph.edges.filter((e) => e.source === sel.id).map((e) => e.target), ...graph.edges.filter((e) => e.target === sel.id).map((e) => e.source) ]) return allItems.filter((m) => connectedIds.has(m.id)) }, [sel, graph, allItems]) return (
{empty ? (

Lass Hermes dich kennenlernen

Statt Fakten einzutippen: führ ein kurzes Onboarding-Gespräch mit Hermes. Was ihr besprecht, merkt sich das Gedächtnis automatisch — du musst nichts manuell pflegen.

{/* Vorgeschlagener Prompt */}
Sende das an Hermes
{ONBOARDING_PROMPT}

Funktioniert genauso über Telegram —{" "} .

) : ( /* Der voll integrierte, schwebende Wissens-Hub */
{/* 1. GraphView nimmt auf Desktop 100% des Hintergrunds ein */} {isDesktop && (
Graph wird geladen…
}>
)} {/* 2. Schwebendes linkes Panel (Wissens-Liste) */}
{/* Header & Steuerung direkt im Panel */}

Gedächtnis-Pool

IDEs & Gateway lesen und lernen hier per MCP.

{/* Maximize/Minimize Button für Desktop */}
{/* Statistiken (Chips) */}
{stats.total} Fakten {stats.auto} auto {stats.cats} Kat.
{/* Steuerungsleiste: Suchen, Eintrag, Deduplizieren */}
setQ(e.target.value)} aria-label="Gedächtnis durchsuchen" type="search" placeholder="Semantisch suchen…" className="w-full h-8 pl-8 pr-3 rounded-lg border border-border/50 bg-background/40 text-[11px] outline-none focus:ring-1 focus:ring-primary/50 text-foreground" />
{/* Mobile View Switcher */}
{([["liste", List], ["graph", Share2]] as const).map(([v, Icon]) => ( ))}
{/* Kategorie-Filter */}
{CATEGORIES.map((c) => { const conf = CAT_CONFIG[c] || DEFAULT_CAT return ( ) })}
{/* Listenelemente (Scrollbar im Glaspanel) */}
{showAdd && (
Neuer Eintrag