Politur: Gedaechtnis-Graph Performance + Treffsicherheit grundsaniert
- Doppel-Mount behoben: GraphView lief unsichtbar doppelt (Desktop+Mobile), jetzt nur eine Instanz via matchMedia - ctx.shadowBlur pro Knoten/Frame durch billigen Halo-Kreis ersetzt (Blur nur fuer aktiven Knoten) - Kantenfarben beim Datenaufbau vorberechnet statt nodes.find() pro Kante pro Frame - Knoten-Objekte ueber Daten-Updates wiederverwendet: Positionen ueberleben Refetch/Suche, Reheat nur bei Strukturaenderung - Hit-Zone zoom-kompensiert (min. ~14 Bildschirm-Pixel) - Knoten sind rausgezoomt jetzt klick- und hoverbar - Suche via useDeferredValue vom Graph-Umbau entkoppelt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, lazy, Suspense, useEffect } from "react"
|
||||
import { useState, useMemo, lazy, Suspense, useEffect, useDeferredValue } from "react"
|
||||
import {
|
||||
Trash2, Sparkles, User, Scroll, Shield, Clock, Search, Plus, BookOpen,
|
||||
Share2, List, MessagesSquare, X, Copy, Check, Terminal as TerminalIcon, Send,
|
||||
@@ -52,6 +52,19 @@ export function MemoryView() {
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
// Nur EINE Graph-Instanz mounten: die CSS-Klassen (hidden/lg:hidden) verstecken zwar,
|
||||
// aber eine per display:none versteckte ForceGraph2D-Instanz simuliert und rendert
|
||||
// trotzdem weiter — das hat die CPU-Last der Ansicht glatt verdoppelt.
|
||||
const [isDesktop, setIsDesktop] = useState(
|
||||
() => typeof window !== "undefined" && window.matchMedia("(min-width: 1024px)").matches
|
||||
)
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia("(min-width: 1024px)")
|
||||
const onChange = (e: MediaQueryListEvent) => setIsDesktop(e.matches)
|
||||
mq.addEventListener("change", onChange)
|
||||
return () => mq.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const { data: allItems = [] } = useMemory({})
|
||||
@@ -84,14 +97,17 @@ export function MemoryView() {
|
||||
const empty = allItems.length === 0
|
||||
|
||||
// Graph clientseitig nach Suchtext filtern (Knoten + deren Kanten).
|
||||
// useDeferredValue: der Graph-Umbau (Re-Layout) hinkt der Tipp-Eingabe nach,
|
||||
// statt bei jedem Tastendruck die D3-Simulation neu anzuwerfen.
|
||||
const deferredQ = useDeferredValue(q)
|
||||
const graphData = useMemo(() => {
|
||||
const g = graph ?? { nodes: [], edges: [] }
|
||||
if (!q.trim()) return g
|
||||
const ql = q.toLowerCase()
|
||||
if (!deferredQ.trim()) return g
|
||||
const ql = deferredQ.toLowerCase()
|
||||
const ns = g.nodes.filter((n) => n.content.toLowerCase().includes(ql))
|
||||
const ids = new Set(ns.map((n) => n.id))
|
||||
return { nodes: ns, edges: g.edges.filter((e) => ids.has(e.source) && ids.has(e.target)) }
|
||||
}, [graph, q])
|
||||
}, [graph, deferredQ])
|
||||
|
||||
// Liste nach Kategorie gruppieren (skaliert: klare Sektionen statt einer flachen Wand).
|
||||
const grouped = useMemo(() => {
|
||||
@@ -224,13 +240,15 @@ export function MemoryView() {
|
||||
)}>
|
||||
|
||||
{/* 1. GraphView nimmt auf Desktop 100% des Hintergrunds ein */}
|
||||
<div className="absolute inset-0 z-0 hidden lg:block">
|
||||
<GraphErrorBoundary>
|
||||
<Suspense fallback={<div className="absolute inset-0 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen…</div>}>
|
||||
<GraphView data={graphData} selectedNodeId={selected} onNodeSelect={setSelected} />
|
||||
</Suspense>
|
||||
</GraphErrorBoundary>
|
||||
</div>
|
||||
{isDesktop && (
|
||||
<div className="absolute inset-0 z-0 hidden lg:block">
|
||||
<GraphErrorBoundary>
|
||||
<Suspense fallback={<div className="absolute inset-0 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen…</div>}>
|
||||
<GraphView data={graphData} selectedNodeId={selected} onNodeSelect={setSelected} />
|
||||
</Suspense>
|
||||
</GraphErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 2. Schwebendes linkes Panel (Wissens-Liste) */}
|
||||
<div className={cn(
|
||||
@@ -396,14 +414,16 @@ export function MemoryView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. Mobiler Fallback für den Graph (unter lg) */}
|
||||
<div className={cn("w-full h-[500px] relative lg:hidden", view === "liste" && "hidden")}>
|
||||
<GraphErrorBoundary>
|
||||
<Suspense fallback={<div className="h-full 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={graphData} selectedNodeId={selected} onNodeSelect={setSelected} />
|
||||
</Suspense>
|
||||
</GraphErrorBoundary>
|
||||
</div>
|
||||
{/* 3. Mobiler Fallback für den Graph (unter lg) — nur mounten, wenn wirklich sichtbar */}
|
||||
{!isDesktop && view === "graph" && (
|
||||
<div className="w-full h-[500px] relative lg:hidden">
|
||||
<GraphErrorBoundary>
|
||||
<Suspense fallback={<div className="h-full 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={graphData} selectedNodeId={selected} onNodeSelect={setSelected} />
|
||||
</Suspense>
|
||||
</GraphErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 4. Schwebendes rechtes Panel (Details zum ausgewählten Knoten) */}
|
||||
{sel && (
|
||||
|
||||
Reference in New Issue
Block a user