Politur: Gedaechnis-Graph um Quadranten-Clustering und forceCollide-Kollisionstuning erweitert (vollstaendige Entzerrung und farbige, gut sichtbare Kanten)
This commit is contained in:
+44
File diff suppressed because one or more lines are too long
-44
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||||
|
|||||||
Generated
+23
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user