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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user