Feat: Gedaechtnis-Graph-Ansicht (Reagraph) + /api/memory/graph

Obsidian-artige Visualisierung des Gedaechtnisses: Knoten = Fakten, Kanten =
semantische Aehnlichkeit (Kosinus der gespeicherten Embeddings, kNN je Knoten),
Farbe = Kategorie, Groesse = Vernetzung. Klick auf Knoten -> Detailpanel mit
verwandten Fakten + vergessen.

- mem0_service/app.py: /graph rechnet Aehnlichkeitskanten aus den Chroma-Embeddings.
- backend: services.memory.graph() + /api/memory/graph (Passthrough).
- frontend: GraphView (reagraph, WebGL), Graph/Liste-Umschalter in MemoryView,
  GraphErrorBoundary, lazy-load (three.js nur bei Bedarf -> Hauptbundle bleibt schlank).
  reagraph auf 4.22.0 gepinnt (4.23+ braucht @react-three/fiber v9 = React 19; Projekt ist React 18).

Live gegen die Box verifiziert (Graph rendert, Kategorien-Farben, Kanten, dunkel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 20:30:17 +02:00
parent 5c3f50dfa5
commit 56243e1835
15 changed files with 5360 additions and 130 deletions
+50 -12
View File
@@ -1,10 +1,15 @@
import { useState } from "react"
import { Trash2, Sparkles, User, Scroll, Shield, Tag, Clock, Search, Plus, BookOpen } from "lucide-react"
import { useState, lazy, Suspense } from "react"
import { Trash2, Sparkles, User, Scroll, Shield, Tag, Clock, Search, Plus, BookOpen, Share2, List } from "lucide-react"
import { api, type DedupeResult } from "@/lib/api"
import { useMemory, useQueryClient } from "@/lib/queries"
import { useMemory, useMemoryGraph, useQueryClient } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
import { cn } from "@/lib/utils"
import { GraphErrorBoundary } from "./GraphErrorBoundary"
// reagraph + three.js sind schwer → erst laden, wenn der Graph-Tab geöffnet wird.
const GraphView = lazy(() => import("./GraphView").then((m) => ({ default: m.GraphView })))
const CATEGORIES = ["user", "instruction", "stable", "versioned", "ephemeral"]
@@ -35,13 +40,18 @@ export function MemoryView() {
const [content, setContent] = useState("")
const [category, setCategory] = useState("stable")
const [deduping, setDeduping] = useState(false)
const [view, setView] = useState<"liste" | "graph">("liste")
const qc = useQueryClient()
const { showAlert, showConfirm, dialogElement } = useDialog()
const { data: items = [], error: itemsErr } = useMemory({ q, category: filter })
const { data: graph } = useMemoryGraph(view === "graph")
const error = itemsErr ? String(itemsErr) : ""
const reloadMemory = () => qc.invalidateQueries({ queryKey: ["memory"] })
const reloadMemory = () => {
qc.invalidateQueries({ queryKey: ["memory"] })
qc.invalidateQueries({ queryKey: ["memory-graph"] })
}
async function add() {
if (!content.trim()) return
@@ -104,14 +114,32 @@ export function MemoryView() {
</p>
</div>
<button
onClick={cleanup}
disabled={deduping}
className="flex h-9 px-4 items-center gap-1.5 text-xs font-semibold rounded-lg border border-border/60 bg-card hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50 shrink-0 self-start"
>
<Sparkles className="h-4 w-4 text-primary animate-pulse" />
<span>Deduplizieren</span>
</button>
<div className="flex items-center gap-2 shrink-0 self-start">
<div className="flex items-center gap-1 p-1 bg-card/30 border border-border/40 rounded-lg">
<button
onClick={() => setView("liste")}
className={cn("flex h-7 px-2.5 items-center gap-1.5 text-[11px] font-semibold rounded-md transition-all cursor-pointer",
view === "liste" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}
>
<List className="h-3.5 w-3.5" /> Liste
</button>
<button
onClick={() => setView("graph")}
className={cn("flex h-7 px-2.5 items-center gap-1.5 text-[11px] font-semibold rounded-md transition-all cursor-pointer",
view === "graph" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}
>
<Share2 className="h-3.5 w-3.5" /> Graph
</button>
</div>
<button
onClick={cleanup}
disabled={deduping}
className="flex h-9 px-4 items-center gap-1.5 text-xs font-semibold rounded-lg border border-border/60 bg-card hover:border-primary/50 transition-all cursor-pointer shadow-md disabled:opacity-50"
>
<Sparkles className="h-4 w-4 text-primary animate-pulse" />
<span>Deduplizieren</span>
</button>
</div>
</div>
{/* Add New Fact Box */}
@@ -151,6 +179,14 @@ export function MemoryView() {
</div>
</div>
{view === "graph" ? (
<GraphErrorBoundary>
<Suspense fallback={<div className="h-[480px] rounded-2xl border border-border/60 bg-card/30 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen</div>}>
<GraphView data={graph ?? { nodes: [], edges: [] }} onDelete={del} />
</Suspense>
</GraphErrorBoundary>
) : (
<>
{/* Filter / Search HUD */}
<div className="flex flex-col md:flex-row items-stretch md:items-center gap-3">
<div className="relative flex-1">
@@ -264,6 +300,8 @@ export function MemoryView() {
})
)}
</div>
</>
)}
{dialogElement}
</div>