Politur: Gedaechnis-Graph um Quadranten-Clustering und forceCollide-Kollisionstuning erweitert (vollstaendige Entzerrung und farbige, gut sichtbare Kanten)

This commit is contained in:
Hitonabi
2026-07-09 12:20:43 +02:00
parent 520cfd9fe4
commit 35e8951a0d
7 changed files with 132 additions and 56 deletions
+61 -10
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react"
import ForceGraph2D from "react-force-graph-2d"
import { forceCollide, forceX, forceY } from "d3-force"
import { Trash2, Sparkles, Share2, RefreshCw } from "lucide-react"
import { type MemoryGraph } from "@/lib/api"
@@ -10,9 +11,16 @@ const CAT_LABEL: Record<string, string> = {
identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
}
const AUTO = new Set(["auto", "agent", "hermes"])
const DIM_EDGE = "rgba(15, 23, 42, 0.01)"
const EDGE = "rgba(255, 255, 255, 0.05)"
const EDGE_HI = "rgba(99, 102, 241, 0.85)"
const DIM_EDGE = "rgba(255, 255, 255, 0.02)"
// Hilfsfunktion zur Konvertierung von Hex in RGBA für farbkodierte Verbindungen
function hexToRgba(hex: string, alpha: number): string {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id: string) => void }) {
const [selected, setSelected] = useState<string | null>(null)
@@ -60,17 +68,51 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
return d
}, [data])
// D3 Force Engine tunen (Luftige Struktur, kein Überlappen)
// Quadranten-Fokuskoordinaten für sauberes Clustering
const getClusterX = (category: string) => {
return {
identity: -160,
knowledge: 160,
rules: -160,
events: 160,
}[category] || 0
}
const getClusterY = (category: string) => {
return {
identity: -160,
knowledge: -160,
rules: 160,
events: 160,
}[category] || 0
}
// D3 Force Engine tunen (Luftige Struktur, kein Überlappen, Clustering)
useEffect(() => {
const fg = fgRef.current
if (!fg) return
// Sicherstellen, dass D3-Kräfte existieren, bevor wir aufgerufen werden
const charge = fg.d3Force("charge")
if (charge) charge.strength(-140)
// 1. Clustering-Kräfte (Knoten zu ihren jeweiligen Sektions-Foci ziehen)
fg.d3Force("x", forceX().x((node: any) => getClusterX(node.category)).strength(0.14))
fg.d3Force("y", forceY().y((node: any) => getClusterY(node.category)).strength(0.14))
// 2. Kollisionskraft (Verhindert, dass Knoten und Textlabels ineinandergeschoben werden)
fg.d3Force("collide", forceCollide().radius((node: any) => {
const nodeDegree = degree[node.id] || 0
// Radius = Knotengröße + großzügiger Textpuffer
return 28 + Math.min(nodeDegree, 12) * 1.5
}).iterations(2))
// 3. Abstoßung (Charge) moderat, da collide und foci die Hauptarbeit leisten
const charge = fg.d3Force("charge")
if (charge) charge.strength(-60)
// 4. Federn (Links)
const link = fg.d3Force("link")
if (link) link.distance(60)
if (link) link.distance(50)
// Simulation neu anschubsen bei neuen Daten
fg.alpha(0.8).restart()
}, [graphData])
const activeNode = hovered || selected
@@ -126,7 +168,7 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
graphData={graphData}
backgroundColor="rgba(0,0,0,0)"
nodeRelSize={4}
cooldownTicks={100}
cooldownTicks={120}
// Hover- und Klick-Logik
onNodeClick={(node: any) => setSelected(node.id)}
@@ -142,13 +184,22 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
linkColor={(link: any) => {
const sId = typeof link.source === "object" ? link.source.id : link.source
const tId = typeof link.target === "object" ? link.target.id : link.target
// Highlight-Kanten leuchten in Indigo auf
if (activeNode === sId || activeNode === tId) return EDGE_HI
return activeNode ? DIM_EDGE : EDGE
// Wenn ein anderer Knoten aktiv ist, blenden wir diese Kante fast komplett aus
if (activeNode) return DIM_EDGE
// Standard-Zustand: Kante leuchtet dezent in der Farbe der Kategorie des Quell-Knotens
const sourceNode = graphData.nodes.find(n => n.id === sId)
const catColor = sourceNode ? CAT_COLOR[sourceNode.category] : "#64748b"
return hexToRgba(catColor, 0.16)
}}
linkWidth={(link: any) => {
const sId = typeof link.source === "object" ? link.source.id : link.source
const tId = typeof link.target === "object" ? link.target.id : link.target
return (activeNode === sId || activeNode === tId) ? 2.2 : 0.8
return (activeNode === sId || activeNode === tId) ? 2.5 : 1.2
}}
// Daten-Partikel fließen auf den aktiven Verbindungen