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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-BrDDv3Dl.js"></script> <script type="module" crossorigin src="/assets/index-BcsTzr2v.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-1USExdfg.css"> <link rel="stylesheet" crossorigin href="/assets/index-1USExdfg.css">
</head> </head>
<body> <body>
+23
View File
@@ -14,6 +14,7 @@
"@tanstack/react-query": "^5.101.1", "@tanstack/react-query": "^5.101.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"d3-force": "^3.0.0",
"graphology": "^0.26.0", "graphology": "^0.26.0",
"graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-forceatlas2": "^0.10.1",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
@@ -27,6 +28,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@types/d3-force": "^3.0.10",
"@types/node": "^22.10.1", "@types/node": "^22.10.1",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
@@ -2345,6 +2347,13 @@
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/d3-force": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-interpolate": { "node_modules/@types/d3-interpolate": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
@@ -2838,6 +2847,20 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-force": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-quadtree": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-force-3d": { "node_modules/d3-force-3d": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
+2
View File
@@ -15,6 +15,7 @@
"@tanstack/react-query": "^5.101.1", "@tanstack/react-query": "^5.101.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"d3-force": "^3.0.0",
"graphology": "^0.26.0", "graphology": "^0.26.0",
"graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-forceatlas2": "^0.10.1",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
@@ -28,6 +29,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@types/d3-force": "^3.0.10",
"@types/node": "^22.10.1", "@types/node": "^22.10.1",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
+61 -10
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react" import { useEffect, useMemo, useRef, useState } from "react"
import ForceGraph2D from "react-force-graph-2d" import ForceGraph2D from "react-force-graph-2d"
import { forceCollide, forceX, forceY } from "d3-force"
import { Trash2, Sparkles, Share2, RefreshCw } from "lucide-react" import { Trash2, Sparkles, Share2, RefreshCw } from "lucide-react"
import { type MemoryGraph } from "@/lib/api" 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", identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
} }
const AUTO = new Set(["auto", "agent", "hermes"]) 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 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 }) { export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id: string) => void }) {
const [selected, setSelected] = useState<string | null>(null) const [selected, setSelected] = useState<string | null>(null)
@@ -60,17 +68,51 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
return d return d
}, [data]) }, [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(() => { useEffect(() => {
const fg = fgRef.current const fg = fgRef.current
if (!fg) return if (!fg) return
// Sicherstellen, dass D3-Kräfte existieren, bevor wir aufgerufen werden // 1. Clustering-Kräfte (Knoten zu ihren jeweiligen Sektions-Foci ziehen)
const charge = fg.d3Force("charge") fg.d3Force("x", forceX().x((node: any) => getClusterX(node.category)).strength(0.14))
if (charge) charge.strength(-140) 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") 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]) }, [graphData])
const activeNode = hovered || selected const activeNode = hovered || selected
@@ -126,7 +168,7 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
graphData={graphData} graphData={graphData}
backgroundColor="rgba(0,0,0,0)" backgroundColor="rgba(0,0,0,0)"
nodeRelSize={4} nodeRelSize={4}
cooldownTicks={100} cooldownTicks={120}
// Hover- und Klick-Logik // Hover- und Klick-Logik
onNodeClick={(node: any) => setSelected(node.id)} onNodeClick={(node: any) => setSelected(node.id)}
@@ -142,13 +184,22 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
linkColor={(link: any) => { linkColor={(link: any) => {
const sId = typeof link.source === "object" ? link.source.id : link.source const sId = typeof link.source === "object" ? link.source.id : link.source
const tId = typeof link.target === "object" ? link.target.id : link.target 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 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) => { linkWidth={(link: any) => {
const sId = typeof link.source === "object" ? link.source.id : link.source const sId = typeof link.source === "object" ? link.source.id : link.source
const tId = typeof link.target === "object" ? link.target.id : link.target 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 // Daten-Partikel fließen auf den aktiven Verbindungen