b383711f6d
Saubere Neuordnung der Gedaechtnis-Kategorien an der etablierten Memory-Taxonomie (semantisch/prozedural/episodisch), bewusst knapp (Best Practice: 3-5, klar beschrieben): identity (Identitaet & Vorlieben) · knowledge (Wissen & Fakten) · rules (Regeln & Konventionen) · events (Ereignisse & Entscheidungen) Loest die alten gemischten 5 (user/instruction/stable/versioned/ephemeral) ab. Auto-Einordnung: OSS-mem0 kann nicht nativ kategorisieren (Cloud-Feature) -> nach der Fakt-Extraktion ordnet dasselbe (Thinking-freie) Hirn jeden neuen Fakt per JSON-Call genau einer Kategorie zu (classify_facts im Sidecar /learn). Behebt den "alles ist stable"- Bug. Manuelle Eintraege: Kategorie weiter waehlbar (Default knowledge). Umgesetzt in mem0_service, backend (services/routers), mcp_memory (Tool-Docs) und Frontend (MemoryView + GraphView: Labels/Farben/Filter). Kein Migrationsbedarf (leerer Start). Live verifiziert: gemischter Absatz -> identity/rules/knowledge/events korrekt zugeordnet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
5.1 KiB
TypeScript
119 lines
5.1 KiB
TypeScript
import { useMemo, useState } from "react"
|
|
import { GraphCanvas, darkTheme } from "reagraph"
|
|
import { Trash2, Sparkles, Share2 } from "lucide-react"
|
|
import { type MemoryGraph } from "@/lib/api"
|
|
|
|
const CAT_COLOR: Record<string, string> = {
|
|
identity: "#22d3ee", knowledge: "#6366f1", rules: "#a78bfa", events: "#fbbf24",
|
|
}
|
|
const CAT_LABEL: Record<string, string> = {
|
|
identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
|
|
}
|
|
const AUTO = new Set(["auto", "agent", "hermes"])
|
|
|
|
export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id: string) => void }) {
|
|
const [selected, setSelected] = useState<string | null>(null)
|
|
|
|
const degree = useMemo(() => {
|
|
const d: Record<string, number> = {}
|
|
data.edges.forEach((e) => { d[e.source] = (d[e.source] || 0) + 1; d[e.target] = (d[e.target] || 0) + 1 })
|
|
return d
|
|
}, [data])
|
|
|
|
const nodes = useMemo(() => data.nodes.map((n) => ({
|
|
id: n.id,
|
|
label: n.content.length > 26 ? n.content.slice(0, 25) + "…" : n.content,
|
|
fill: CAT_COLOR[n.category] || "#64748b",
|
|
size: 6 + Math.min(degree[n.id] || 0, 6) * 2,
|
|
})), [data, degree])
|
|
|
|
const edges = useMemo(() => data.edges.map((e) => ({
|
|
id: `${e.source}->${e.target}`,
|
|
source: e.source,
|
|
target: e.target,
|
|
size: 0.4 + e.weight,
|
|
})), [data])
|
|
|
|
const sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null
|
|
const neighbors = useMemo(() => {
|
|
if (!selected) return []
|
|
const ids = new Set<string>()
|
|
data.edges.forEach((e) => {
|
|
if (e.source === selected) ids.add(e.target)
|
|
if (e.target === selected) ids.add(e.source)
|
|
})
|
|
return data.nodes.filter((n) => ids.has(n.id))
|
|
}, [selected, data])
|
|
|
|
if (!data.nodes.length) {
|
|
return (
|
|
<div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center">
|
|
Noch keine Fakten — der Graph füllt sich, sobald Hermes lernt oder du Einträge anlegst.
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-3">
|
|
<div className="relative h-[480px] rounded-2xl border border-border/60 bg-[#070a0f] overflow-hidden">
|
|
<GraphCanvas
|
|
nodes={nodes}
|
|
edges={edges}
|
|
theme={darkTheme}
|
|
layoutType="forceDirected2d"
|
|
labelType="nodes"
|
|
edgeArrowPosition="none"
|
|
draggable
|
|
onNodeClick={(n: any) => setSelected(n.id)}
|
|
onCanvasClick={() => setSelected(null)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 min-h-[480px]">
|
|
{sel ? (
|
|
<>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="w-3 h-3 rounded-full" style={{ background: CAT_COLOR[sel.category], boxShadow: `0 0 0 2px ${AUTO.has(sel.source) ? "#34d399" : "#475569"}` }} />
|
|
<span className="text-[10px] font-bold uppercase tracking-wider" style={{ color: CAT_COLOR[sel.category] }}>
|
|
{CAT_LABEL[sel.category] || sel.category}
|
|
</span>
|
|
<span className={`ml-auto text-[10px] font-mono flex items-center gap-1 ${AUTO.has(sel.source) ? "text-emerald-400" : "text-muted-foreground/70"}`}>
|
|
{AUTO.has(sel.source) && <Sparkles className="h-2.5 w-2.5" />}{sel.source}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm text-foreground leading-relaxed mb-4 break-words">{sel.content}</div>
|
|
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70 mb-2 flex items-center gap-1">
|
|
<Share2 className="h-3 w-3" /> verwandte Fakten
|
|
</div>
|
|
<div className="flex flex-col gap-1.5 mb-4">
|
|
{neighbors.length ? neighbors.map((n) => (
|
|
<button
|
|
key={n.id}
|
|
onClick={() => setSelected(n.id)}
|
|
className="flex items-center gap-2 text-[11px] text-muted-foreground hover:text-foreground text-left transition-colors"
|
|
>
|
|
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: CAT_COLOR[n.category] }} />
|
|
<span className="truncate">{n.content}</span>
|
|
</button>
|
|
)) : <span className="text-[11px] text-muted-foreground/60">—</span>}
|
|
</div>
|
|
<button
|
|
onClick={() => { onDelete(sel.id); setSelected(null) }}
|
|
className="flex items-center gap-1.5 text-[11px] text-red-400 border border-red-500/20 rounded-lg px-2.5 py-1.5 hover:bg-red-500/5 transition-all"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" /> vergessen
|
|
</button>
|
|
</>
|
|
) : (
|
|
<div className="h-full flex flex-col items-center justify-center text-center text-muted-foreground/70 gap-2 pt-24">
|
|
<Share2 className="h-6 w-6" />
|
|
<div className="text-xs max-w-[180px] leading-relaxed">
|
|
Einen Knoten wählen, um den Fakt und seine semantischen Nachbarn zu sehen.
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|