Files
codebase-memory-mcp/graph-ui/src/components/GraphLoader.tsx
T
Martin Vogel 9969253bdc feat(graph-ui): type-in node budget with live loading progress
Add a node-budget control next to Refresh: type any count (snapped to
5,000 steps, 5,000 default, 10M max) and the graph reloads with that many
nodes plus every edge between them. The choice persists per project.

Loading gets real feedback: the layout response streams with a live
MB counter under an animated constellation (static under
prefers-reduced-motion), replacing the bare spinner.

Rendering scales with the budget: instance matrices are rebuilt only when
the node set or highlight changes instead of every frame, sphere
tessellation steps down as counts grow, and past 75k nodes the cloud
switches to soft point sprites — one position per node — so six-figure
budgets stay interactive. The truncation notice now names the loaded edge
count and points at the budget control.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-07 23:09:20 +02:00

85 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { LoadProgress } from "../hooks/useGraphData";
/* Small constellation whose nodes pulse and whose edges draw themselves in a
* staggered loop — a miniature of the graph being loaded. Pure SVG/CSS
* (keyframes in globals.css) so it costs nothing while the real payload
* streams in, and it degrades to a static constellation under
* prefers-reduced-motion. */
const LOADER_NODES: Array<{ cx: number; cy: number; r: number; delay: string }> = [
{ cx: 60, cy: 18, r: 5, delay: "0s" },
{ cx: 22, cy: 42, r: 4, delay: "0.35s" },
{ cx: 98, cy: 40, r: 4, delay: "0.7s" },
{ cx: 42, cy: 78, r: 4.5, delay: "1.05s" },
{ cx: 84, cy: 82, r: 3.5, delay: "1.4s" },
{ cx: 60, cy: 52, r: 6, delay: "0.15s" },
{ cx: 14, cy: 88, r: 3, delay: "1.75s" },
];
const LOADER_EDGES: Array<{ x1: number; y1: number; x2: number; y2: number; delay: string }> = [
{ x1: 60, y1: 18, x2: 60, y2: 52, delay: "0s" },
{ x1: 22, y1: 42, x2: 60, y2: 52, delay: "0.3s" },
{ x1: 98, y1: 40, x2: 60, y2: 52, delay: "0.6s" },
{ x1: 42, y1: 78, x2: 60, y2: 52, delay: "0.9s" },
{ x1: 84, y1: 82, x2: 60, y2: 52, delay: "1.2s" },
{ x1: 42, y1: 78, x2: 14, y2: 88, delay: "1.5s" },
{ x1: 22, y1: 42, x2: 60, y2: 18, delay: "1.8s" },
];
function formatMegabytes(bytes: number): string {
return (bytes / (1024 * 1024)).toFixed(1);
}
interface GraphLoaderProps {
nodeBudget: number;
progress: LoadProgress;
}
export function GraphLoader({ nodeBudget, progress }: GraphLoaderProps) {
const receiving = progress.receivedBytes > 0;
return (
<div className="text-center" role="status" aria-live="polite">
<svg
width="120"
height="104"
viewBox="0 0 120 104"
className="mx-auto"
aria-hidden="true"
>
{LOADER_EDGES.map((e, i) => (
<line
key={`e${i}`}
x1={e.x1}
y1={e.y1}
x2={e.x2}
y2={e.y2}
className="graph-loader-edge"
style={{ animationDelay: e.delay }}
/>
))}
{LOADER_NODES.map((n, i) => (
<circle
key={`n${i}`}
cx={n.cx}
cy={n.cy}
r={n.r}
className="graph-loader-node"
style={{ animationDelay: n.delay }}
/>
))}
</svg>
<p className="text-white/50 text-sm mt-4">
{receiving ? "Receiving graph" : "Computing layout"} — up to{" "}
{nodeBudget.toLocaleString("en-US")} nodes
</p>
<p className="text-cyan-300/60 text-xs font-mono mt-1 h-4">
{receiving
? progress.totalBytes
? `${formatMegabytes(progress.receivedBytes)} of ${formatMegabytes(progress.totalBytes)} MB`
: `${formatMegabytes(progress.receivedBytes)} MB received`
: " "}
</p>
</div>
);
}