groove-dev 0.27.68 → 0.27.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +137 -3
  4. package/node_modules/@groove-dev/gui/dist/assets/index-DhnTm_1P.js +8614 -0
  5. package/node_modules/@groove-dev/gui/dist/assets/index-oQ0ejlfH.css +1 -0
  6. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  7. package/node_modules/@groove-dev/gui/package.json +1 -1
  8. package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +208 -124
  9. package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +12 -1
  10. package/node_modules/@groove-dev/gui/src/components/network/identity-bar.jsx +4 -40
  11. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +600 -0
  12. package/node_modules/@groove-dev/gui/src/components/network/token-waterfall.jsx +111 -0
  13. package/node_modules/@groove-dev/gui/src/stores/groove.js +60 -0
  14. package/node_modules/@groove-dev/gui/src/views/network.jsx +6 -0
  15. package/package.json +1 -1
  16. package/packages/cli/package.json +1 -1
  17. package/packages/daemon/package.json +1 -1
  18. package/packages/daemon/src/api.js +137 -3
  19. package/packages/gui/dist/assets/index-DhnTm_1P.js +8614 -0
  20. package/packages/gui/dist/assets/index-oQ0ejlfH.css +1 -0
  21. package/packages/gui/dist/index.html +2 -2
  22. package/packages/gui/package.json +1 -1
  23. package/packages/gui/src/components/network/activity-chart.jsx +208 -124
  24. package/packages/gui/src/components/network/compute-header.jsx +12 -1
  25. package/packages/gui/src/components/network/identity-bar.jsx +4 -40
  26. package/packages/gui/src/components/network/performance-dashboard.jsx +600 -0
  27. package/packages/gui/src/components/network/token-waterfall.jsx +111 -0
  28. package/packages/gui/src/stores/groove.js +60 -0
  29. package/packages/gui/src/views/network.jsx +6 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-Cz4tj733.js +0 -8614
  31. package/node_modules/@groove-dev/gui/dist/assets/index-YeunozTU.css +0 -1
  32. package/packages/gui/dist/assets/index-Cz4tj733.js +0 -8614
  33. package/packages/gui/dist/assets/index-YeunozTU.css +0 -1
@@ -0,0 +1,600 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useRef, useEffect, useState, useMemo, memo } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { HEX, hexAlpha } from '../../lib/theme-hex';
5
+ import { cn } from '../../lib/cn';
6
+ import { Badge } from '../ui/badge';
7
+ import { ScrollArea } from '../ui/scroll-area';
8
+ import { TokenWaterfall } from './token-waterfall';
9
+ import { Zap, ArrowRight, Cpu } from 'lucide-react';
10
+
11
+ function shortAddr(addr) {
12
+ if (!addr || typeof addr !== 'string') return '\u2014';
13
+ if (addr.length < 14) return addr;
14
+ return `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`;
15
+ }
16
+
17
+ // ── Live Inference Metrics Strip ─────────────────────────
18
+
19
+ const LiveMetricsStrip = memo(function LiveMetricsStrip() {
20
+ const timing = useGrooveStore((s) => s.networkTokenTiming);
21
+
22
+ const tps = timing?.tps;
23
+ const ttft = timing?.ttft_ms;
24
+ const tokensGen = timing?.tokens_generated ?? 0;
25
+
26
+ const { p2pCount, relayCount } = useMemo(() => {
27
+ if (!timing?.stages) return { p2pCount: 0, relayCount: 0 };
28
+ let p2p = 0, relay = 0;
29
+ for (const s of timing.stages) {
30
+ if (s.via === 'p2p') p2p++; else relay++;
31
+ }
32
+ return { p2pCount: p2p, relayCount: relay };
33
+ }, [timing]);
34
+
35
+ const metrics = [
36
+ { label: 'TPS', value: tps != null ? `${tps.toFixed(1)}` : '\u2014', unit: 't/s', color: HEX.accent },
37
+ { label: 'TTFT', value: ttft != null ? `${(ttft / 1000).toFixed(2)}` : '\u2014', unit: 's', color: HEX.info },
38
+ { label: 'TOKENS', value: tokensGen > 0 ? String(tokensGen) : '\u2014', unit: '', color: HEX.text0 },
39
+ { label: 'P2P', value: `${p2pCount}/${p2pCount + relayCount}`, unit: 'hops', color: p2pCount >= relayCount ? HEX.success : HEX.warning },
40
+ ];
41
+
42
+ return (
43
+ <div className="flex items-stretch border-b border-border-subtle bg-surface-0">
44
+ {metrics.map((m, i) => (
45
+ <div key={m.label} className={cn('px-4 py-2.5 min-w-[100px]', i < metrics.length - 1 && 'border-r border-border-subtle')}>
46
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider">{m.label}</div>
47
+ <div className="flex items-baseline gap-1 mt-0.5">
48
+ <span className="text-lg font-mono font-semibold tabular-nums leading-none" style={{ color: m.color }}>{m.value}</span>
49
+ {m.unit && <span className="text-2xs font-mono text-text-3">{m.unit}</span>}
50
+ </div>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ );
55
+ });
56
+
57
+ // ── TPS Trend Chart (canvas) ──────────────────────────────
58
+
59
+ const TpsChart = memo(function TpsChart() {
60
+ const benchmarks = useGrooveStore((s) => s.networkBenchmarks);
61
+ const tpsSnaps = useGrooveStore((s) => s.networkPerfSnapshots);
62
+ const containerRef = useRef(null);
63
+ const canvasRef = useRef(null);
64
+ const [size, setSize] = useState({ width: 0, height: 0 });
65
+
66
+ const chartData = useMemo(() => {
67
+ const historical = benchmarks.map((b) => ({ t: b.t || Date.now(), tps: b.tps || 0 }));
68
+ const live = tpsSnaps || [];
69
+ const merged = [...historical, ...live];
70
+ if (merged.length < 2) return [];
71
+ return merged.slice(-100);
72
+ }, [benchmarks, tpsSnaps]);
73
+
74
+ const latestTps = chartData.length > 0 ? chartData[chartData.length - 1].tps : 0;
75
+
76
+ useEffect(() => {
77
+ const el = containerRef.current;
78
+ if (!el) return;
79
+ const obs = new ResizeObserver((entries) => {
80
+ const { width: cw, height: ch } = entries[0].contentRect;
81
+ if (cw > 0 && ch > 0) setSize({ width: Math.floor(cw), height: Math.floor(ch) });
82
+ });
83
+ obs.observe(el);
84
+ return () => obs.disconnect();
85
+ }, []);
86
+
87
+ useEffect(() => {
88
+ const canvas = canvasRef.current;
89
+ const { width, height } = size;
90
+ if (!canvas || !chartData.length || width <= 0 || height <= 0) return;
91
+
92
+ const ctx = canvas.getContext('2d');
93
+ const dpr = window.devicePixelRatio || 1;
94
+ canvas.width = width * dpr;
95
+ canvas.height = height * dpr;
96
+ ctx.scale(dpr, dpr);
97
+ ctx.clearRect(0, 0, width, height);
98
+
99
+ const pad = { top: 24, right: 12, bottom: 8, left: 12 };
100
+ const w = width - pad.left - pad.right;
101
+ const h = height - pad.top - pad.bottom;
102
+ if (w <= 0 || h <= 0) return;
103
+
104
+ const vals = chartData.map((d) => d.tps);
105
+ const maxVal = Math.max(...vals, 1);
106
+
107
+ const xAt = (i) => pad.left + (i / Math.max(chartData.length - 1, 1)) * w;
108
+ const yAt = (v) => pad.top + h - (v / maxVal) * h;
109
+
110
+ ctx.setLineDash([2, 4]);
111
+ ctx.strokeStyle = hexAlpha(HEX.text4, 0.2);
112
+ ctx.lineWidth = 1;
113
+ for (let i = 1; i <= 3; i++) {
114
+ const y = pad.top + (h / 4) * i;
115
+ ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(pad.left + w, y); ctx.stroke();
116
+ }
117
+ ctx.setLineDash([]);
118
+
119
+ ctx.font = "9px 'JetBrains Mono Variable', monospace";
120
+ ctx.textAlign = 'left';
121
+ ctx.fillStyle = hexAlpha(HEX.text3, 0.5);
122
+ ctx.fillText(`${maxVal.toFixed(1)} t/s`, pad.left + 4, pad.top + 10);
123
+
124
+ ctx.beginPath();
125
+ ctx.moveTo(pad.left, pad.top + h);
126
+ for (let i = 0; i < chartData.length; i++) ctx.lineTo(xAt(i), yAt(vals[i]));
127
+ ctx.lineTo(xAt(chartData.length - 1), pad.top + h);
128
+ ctx.closePath();
129
+ const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + h);
130
+ grad.addColorStop(0, hexAlpha(HEX.accent, 0.2));
131
+ grad.addColorStop(0.7, hexAlpha(HEX.accent, 0.04));
132
+ grad.addColorStop(1, hexAlpha(HEX.accent, 0));
133
+ ctx.fillStyle = grad;
134
+ ctx.fill();
135
+
136
+ ctx.beginPath();
137
+ ctx.strokeStyle = HEX.accent;
138
+ ctx.lineWidth = 1.5;
139
+ ctx.lineJoin = 'round';
140
+ for (let i = 0; i < chartData.length; i++) {
141
+ i === 0 ? ctx.moveTo(xAt(i), yAt(vals[i])) : ctx.lineTo(xAt(i), yAt(vals[i]));
142
+ }
143
+ ctx.stroke();
144
+
145
+ ctx.font = "9px 'Inter Variable', sans-serif";
146
+ ctx.textAlign = 'right';
147
+ ctx.fillStyle = HEX.accent;
148
+ ctx.fillText('TPS', width - pad.right - 4, 14);
149
+ }, [chartData, size]);
150
+
151
+ return (
152
+ <div className="flex flex-col h-full">
153
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0 flex items-center justify-between">
154
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">TPS Trend</span>
155
+ <span className="text-sm font-mono font-semibold tabular-nums" style={{ color: HEX.accent }}>
156
+ {latestTps > 0 ? `${latestTps.toFixed(1)} t/s` : '\u2014'}
157
+ </span>
158
+ </div>
159
+ <div ref={containerRef} className="relative flex-1 min-h-0" style={{ minHeight: 120 }}>
160
+ {chartData.length < 2 ? (
161
+ <div className="absolute inset-0 flex items-center justify-center">
162
+ <span className="text-xs font-mono text-text-3">Collecting performance data\u2026</span>
163
+ </div>
164
+ ) : size.width > 0 && size.height > 0 ? (
165
+ <canvas
166
+ ref={canvasRef}
167
+ style={{ width: size.width, height: size.height }}
168
+ className="absolute inset-0 block"
169
+ />
170
+ ) : null}
171
+ </div>
172
+ </div>
173
+ );
174
+ });
175
+
176
+ // ── Bottleneck Breakdown ──────────────────────────────────
177
+
178
+ const BOTTLENECK_PHASES = [
179
+ { key: 'serialize_ms', label: 'Serialize', color: HEX.info },
180
+ { key: 'send_ms', label: 'Send', color: HEX.accent },
181
+ { key: 'wait_ms', label: 'Wait', color: HEX.text3 },
182
+ { key: 'forward_ms', label: 'Forward', color: HEX.success },
183
+ { key: 'queue_ms', label: 'Queue', color: HEX.warning },
184
+ ];
185
+
186
+ const BottleneckBreakdown = memo(function BottleneckBreakdown() {
187
+ const timing = useGrooveStore((s) => s.networkTokenTiming);
188
+
189
+ const phaseTotals = useMemo(() => {
190
+ const stages = timing?.stages || [];
191
+ if (!stages.length) return null;
192
+ const totals = {};
193
+ let sum = 0;
194
+ for (const phase of BOTTLENECK_PHASES) {
195
+ const val = stages.reduce((acc, s) => acc + (s[phase.key] || 0), 0) / stages.length;
196
+ totals[phase.key] = val;
197
+ sum += val;
198
+ }
199
+ if (sum === 0) return null;
200
+ const overhead = stages.reduce((acc, s) => {
201
+ const rtt = s.rtt_ms || 0;
202
+ const accounted = BOTTLENECK_PHASES.reduce((a, p) => a + (s[p.key] || 0), 0);
203
+ return acc + Math.max(0, rtt - accounted);
204
+ }, 0) / stages.length;
205
+ totals._overhead = overhead;
206
+ totals._sum = sum + overhead;
207
+ return totals;
208
+ }, [timing]);
209
+
210
+ if (!phaseTotals) {
211
+ return (
212
+ <div className="flex items-center justify-center py-6">
213
+ <span className="text-xs font-mono text-text-3">No stage data available</span>
214
+ </div>
215
+ );
216
+ }
217
+
218
+ const total = phaseTotals._sum;
219
+
220
+ return (
221
+ <div className="flex flex-col gap-2 px-3 py-2">
222
+ <div className="h-5 rounded-sm overflow-hidden flex bg-surface-2">
223
+ {BOTTLENECK_PHASES.map((p) => {
224
+ const pct = ((phaseTotals[p.key] || 0) / total) * 100;
225
+ if (pct < 0.5) return null;
226
+ return (
227
+ <div
228
+ key={p.key}
229
+ className="h-full transition-all"
230
+ style={{ width: `${pct}%`, background: p.color }}
231
+ title={`${p.label}: ${pct.toFixed(1)}%`}
232
+ />
233
+ );
234
+ })}
235
+ {phaseTotals._overhead > 0 && (
236
+ <div
237
+ className="h-full transition-all"
238
+ style={{ width: `${(phaseTotals._overhead / total) * 100}%`, background: HEX.orange }}
239
+ title={`Overhead: ${((phaseTotals._overhead / total) * 100).toFixed(1)}%`}
240
+ />
241
+ )}
242
+ </div>
243
+ <div className="flex flex-wrap gap-x-3 gap-y-1">
244
+ {BOTTLENECK_PHASES.map((p) => {
245
+ const pct = ((phaseTotals[p.key] || 0) / total) * 100;
246
+ return (
247
+ <div key={p.key} className="flex items-center gap-1">
248
+ <span className="w-2 h-2 rounded-sm flex-shrink-0" style={{ background: p.color }} />
249
+ <span className="text-2xs font-mono text-text-3">{p.label}</span>
250
+ <span className="text-2xs font-mono text-text-2 tabular-nums">{pct.toFixed(0)}%</span>
251
+ </div>
252
+ );
253
+ })}
254
+ {phaseTotals._overhead > 0 && (
255
+ <div className="flex items-center gap-1">
256
+ <span className="w-2 h-2 rounded-sm flex-shrink-0" style={{ background: HEX.orange }} />
257
+ <span className="text-2xs font-mono text-text-3">Overhead</span>
258
+ <span className="text-2xs font-mono text-text-2 tabular-nums">
259
+ {((phaseTotals._overhead / total) * 100).toFixed(0)}%
260
+ </span>
261
+ </div>
262
+ )}
263
+ </div>
264
+ </div>
265
+ );
266
+ });
267
+
268
+ // ── Session Summary ───────────────────────────────────────
269
+
270
+ const SessionSummary = memo(function SessionSummary() {
271
+ const benchmarks = useGrooveStore((s) => s.networkBenchmarks);
272
+ const timing = useGrooveStore((s) => s.networkTokenTiming);
273
+
274
+ const latest = benchmarks.length > 0 ? benchmarks[benchmarks.length - 1] : null;
275
+ const ttft = latest?.ttft_ms ?? timing?.ttft_ms;
276
+ const tokens = latest?.tokens_generated ?? timing?.tokens_generated ?? 0;
277
+ const totalMs = latest?.total_network_ms ?? latest?.total_compute_ms;
278
+ const p2p = latest?.p2p_sends ?? 0;
279
+ const relay = latest?.relay_sends ?? 0;
280
+ const totalSends = p2p + relay;
281
+ const p2pPct = totalSends > 0 ? (p2p / totalSends) * 100 : 0;
282
+
283
+ const stats = [
284
+ { label: 'TTFT', value: ttft != null ? `${(ttft / 1000).toFixed(2)}s` : '\u2014', color: HEX.accent },
285
+ { label: 'Tokens', value: tokens > 0 ? String(tokens) : '\u2014', color: HEX.text0 },
286
+ { label: 'Total Time', value: totalMs != null ? `${(totalMs / 1000).toFixed(1)}s` : '\u2014', color: HEX.text0 },
287
+ { label: 'TPS', value: latest?.tps != null ? `${latest.tps.toFixed(1)}` : '\u2014', color: HEX.accent },
288
+ ];
289
+
290
+ return (
291
+ <div className="flex flex-col gap-3 px-3 py-2">
292
+ <div className="grid grid-cols-2 gap-2">
293
+ {stats.map((s) => (
294
+ <div key={s.label}>
295
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider">{s.label}</div>
296
+ <div className="text-sm font-mono font-semibold tabular-nums" style={{ color: s.color }}>{s.value}</div>
297
+ </div>
298
+ ))}
299
+ </div>
300
+
301
+ {totalSends > 0 && (
302
+ <div>
303
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider mb-1">P2P vs Relay</div>
304
+ <div className="h-3 rounded-sm overflow-hidden flex bg-surface-2">
305
+ <div
306
+ className="h-full transition-all"
307
+ style={{ width: `${p2pPct}%`, background: HEX.success }}
308
+ title={`P2P: ${p2p} (${p2pPct.toFixed(0)}%)`}
309
+ />
310
+ <div
311
+ className="h-full transition-all"
312
+ style={{ width: `${100 - p2pPct}%`, background: HEX.orange }}
313
+ title={`Relay: ${relay} (${(100 - p2pPct).toFixed(0)}%)`}
314
+ />
315
+ </div>
316
+ <div className="flex items-center justify-between mt-1">
317
+ <div className="flex items-center gap-1">
318
+ <span className="w-2 h-2 rounded-sm" style={{ background: HEX.success }} />
319
+ <span className="text-2xs font-mono text-text-3">P2P {p2p}</span>
320
+ </div>
321
+ <div className="flex items-center gap-1">
322
+ <span className="w-2 h-2 rounded-sm" style={{ background: HEX.orange }} />
323
+ <span className="text-2xs font-mono text-text-3">Relay {relay}</span>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ )}
328
+ </div>
329
+ );
330
+ });
331
+
332
+ // ── Gauge Bar (reusable) ─────────────────────────────────
333
+
334
+ function GaugeBar({ value, max, peakValue, color, label, unit }) {
335
+ const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0;
336
+ const peakPct = peakValue && max > 0 ? Math.min((peakValue / max) * 100, 100) : null;
337
+ const gaugeColor = pct > 90 ? HEX.danger : pct > 70 ? HEX.warning : color;
338
+
339
+ return (
340
+ <div className="flex flex-col gap-0.5">
341
+ <div className="flex items-center justify-between">
342
+ <span className="text-2xs font-mono text-text-4 uppercase">{label}</span>
343
+ <span className="text-2xs font-mono text-text-2 tabular-nums">
344
+ {typeof value === 'number' ? `${Math.round(value)}` : '\u2014'}
345
+ {unit ? ` ${unit}` : ''}
346
+ {max > 0 && typeof value === 'number' ? ` / ${Math.round(max)}` : ''}
347
+ </span>
348
+ </div>
349
+ <div className="h-2 rounded-sm overflow-hidden bg-surface-3 relative">
350
+ <div
351
+ className="h-full rounded-sm transition-all duration-300"
352
+ style={{ width: `${pct}%`, background: gaugeColor }}
353
+ />
354
+ {peakPct != null && (
355
+ <div
356
+ className="absolute top-0 h-full w-px"
357
+ style={{ left: `${peakPct}%`, background: HEX.danger, opacity: 0.7 }}
358
+ title={`Peak: ${Math.round(peakValue)} ${unit || ''}`}
359
+ />
360
+ )}
361
+ </div>
362
+ </div>
363
+ );
364
+ }
365
+
366
+ // ── Per-Node Live Gauges ─────────────────────────────────
367
+
368
+ const NodeGaugeCard = memo(function NodeGaugeCard({ nodeId, telemetry }) {
369
+ const device = telemetry.device || 'unknown';
370
+ const gpuModel = telemetry.gpu_model || device;
371
+ const layers = Array.isArray(telemetry.layers) ? `L${telemetry.layers[0]}\u2013${telemetry.layers[1]}` : null;
372
+ const isCuda = device === 'cuda';
373
+ const isMps = device === 'mps';
374
+
375
+ const stale = telemetry.updatedAt && (Date.now() - telemetry.updatedAt > 10000);
376
+
377
+ return (
378
+ <div className={cn(
379
+ 'rounded-md border bg-surface-0 p-3 flex flex-col gap-2 transition-opacity',
380
+ stale ? 'border-border-subtle opacity-60' : 'border-border-subtle',
381
+ )}>
382
+ <div className="flex items-center justify-between gap-2">
383
+ <div className="flex items-center gap-1.5 min-w-0">
384
+ <Cpu size={12} className="text-text-3 flex-shrink-0" />
385
+ <span className="text-xs font-mono text-text-1 truncate">{shortAddr(nodeId)}</span>
386
+ </div>
387
+ <div className="flex items-center gap-1 flex-shrink-0">
388
+ <Badge variant={isCuda ? 'info' : isMps ? 'purple' : 'default'} className="text-2xs px-1.5 py-0 leading-tight">
389
+ {device}
390
+ </Badge>
391
+ {layers && (
392
+ <span className="text-2xs font-mono text-text-3 bg-surface-3 px-1.5 py-0 rounded">{layers}</span>
393
+ )}
394
+ </div>
395
+ </div>
396
+
397
+ <div className="text-2xs font-mono text-text-3 truncate">{gpuModel}</div>
398
+
399
+ {(isCuda || isMps) && (telemetry.vram_total_mb > 0 || telemetry.vram_used_mb > 0) && (
400
+ <GaugeBar
401
+ label="VRAM"
402
+ value={telemetry.vram_used_mb}
403
+ max={telemetry.vram_total_mb || telemetry.vram_peak_mb || telemetry.vram_used_mb}
404
+ peakValue={telemetry.vram_total_mb ? telemetry.vram_peak_mb : undefined}
405
+ color={HEX.purple}
406
+ unit="MB"
407
+ />
408
+ )}
409
+
410
+ <GaugeBar
411
+ label="RAM"
412
+ value={telemetry.ram_pct}
413
+ max={100}
414
+ color={HEX.info}
415
+ unit="%"
416
+ />
417
+
418
+ <GaugeBar
419
+ label="CPU"
420
+ value={telemetry.cpu_pct}
421
+ max={100}
422
+ color={HEX.accent}
423
+ unit="%"
424
+ />
425
+
426
+ {telemetry.forward_ms != null && (
427
+ <div className="flex items-center justify-between">
428
+ <span className="text-2xs font-mono text-text-4 uppercase">Forward</span>
429
+ <span className="text-xs font-mono font-semibold tabular-nums" style={{ color: telemetry.forward_ms > 200 ? HEX.warning : HEX.success }}>
430
+ {telemetry.forward_ms.toFixed(1)}ms
431
+ </span>
432
+ </div>
433
+ )}
434
+ </div>
435
+ );
436
+ });
437
+
438
+ const NodeGauges = memo(function NodeGauges() {
439
+ const telemetryMap = useGrooveStore((s) => s.networkNodeTelemetry);
440
+
441
+ const nodes = useMemo(() => {
442
+ return Object.entries(telemetryMap)
443
+ .sort(([, a], [, b]) => (a.layers?.[0] ?? 0) - (b.layers?.[0] ?? 0));
444
+ }, [telemetryMap]);
445
+
446
+ if (!nodes.length) {
447
+ return (
448
+ <div className="flex items-center justify-center py-6">
449
+ <span className="text-xs font-mono text-text-3">No node telemetry received yet</span>
450
+ </div>
451
+ );
452
+ }
453
+
454
+ return (
455
+ <div className="grid grid-cols-1 gap-2 p-3">
456
+ {nodes.map(([nodeId, tel]) => (
457
+ <NodeGaugeCard key={nodeId} nodeId={nodeId} telemetry={tel} />
458
+ ))}
459
+ </div>
460
+ );
461
+ });
462
+
463
+ // ── Pipeline Visualization ───────────────────────────────
464
+
465
+ const PipelineVisualization = memo(function PipelineVisualization() {
466
+ const traces = useGrooveStore((s) => s.networkTraces);
467
+ const benchmarks = useGrooveStore((s) => s.networkBenchmarks);
468
+
469
+ const pipeline = useMemo(() => {
470
+ if (Array.isArray(traces) && traces.length > 0) {
471
+ const latest = traces[traces.length - 1];
472
+ if (latest?.pipeline && Array.isArray(latest.pipeline)) return latest.pipeline;
473
+ }
474
+ if (benchmarks.length > 0) {
475
+ const latest = benchmarks[benchmarks.length - 1];
476
+ if (latest?.pipeline && Array.isArray(latest.pipeline)) return latest.pipeline;
477
+ }
478
+ return null;
479
+ }, [traces, benchmarks]);
480
+
481
+ if (!pipeline || !pipeline.length) {
482
+ return (
483
+ <div className="flex items-center justify-center py-6">
484
+ <span className="text-xs font-mono text-text-3">No pipeline data available</span>
485
+ </div>
486
+ );
487
+ }
488
+
489
+ return (
490
+ <div className="flex items-center gap-0 px-3 py-3 overflow-x-auto">
491
+ {pipeline.map((node, i) => {
492
+ const layers = Array.isArray(node.layers) ? `L${node.layers[0]}\u2013${node.layers[1]}` : '';
493
+ const isCuda = node.device === 'cuda';
494
+ const scoreColor = (node.score || 0) >= 80 ? HEX.success : (node.score || 0) >= 60 ? HEX.warning : HEX.danger;
495
+
496
+ return (
497
+ <div key={node.node_id || i} className="flex items-center gap-0 flex-shrink-0">
498
+ <div className="rounded-md border border-border-subtle bg-surface-0 px-3 py-2 min-w-[140px]">
499
+ <div className="flex items-center gap-1.5 mb-1">
500
+ <Cpu size={10} className="text-text-3" />
501
+ <span className="text-2xs font-mono text-text-1">{shortAddr(node.node_id)}</span>
502
+ </div>
503
+ <div className="text-2xs font-mono text-text-3 truncate mb-1">
504
+ {node.gpu_model || node.device || '\u2014'}
505
+ </div>
506
+ <div className="flex items-center gap-2">
507
+ {layers && <span className="text-2xs font-mono text-text-2 bg-surface-3 px-1 rounded">{layers}</span>}
508
+ <Badge variant={isCuda ? 'info' : 'purple'} className="text-2xs px-1 py-0 leading-tight">{node.device}</Badge>
509
+ </div>
510
+ {node.score != null && (
511
+ <div className="mt-1.5 flex items-center gap-1">
512
+ <span className="text-2xs font-mono text-text-4">Score</span>
513
+ <span className="text-xs font-mono font-semibold tabular-nums" style={{ color: scoreColor }}>
514
+ {node.score.toFixed(1)}
515
+ </span>
516
+ </div>
517
+ )}
518
+ </div>
519
+ {i < pipeline.length - 1 && (
520
+ <div className="px-1 flex-shrink-0">
521
+ <ArrowRight size={14} className="text-text-4" />
522
+ </div>
523
+ )}
524
+ </div>
525
+ );
526
+ })}
527
+ </div>
528
+ );
529
+ });
530
+
531
+ // ── Main Dashboard ────────────────────────────────────────
532
+
533
+ export const PerformanceDashboard = memo(function PerformanceDashboard({ active }) {
534
+ const fetchNetworkBenchmarks = useGrooveStore((s) => s.fetchNetworkBenchmarks);
535
+ const fetchNetworkTraces = useGrooveStore((s) => s.fetchNetworkTraces);
536
+
537
+ useEffect(() => {
538
+ if (active) {
539
+ fetchNetworkBenchmarks();
540
+ fetchNetworkTraces();
541
+ }
542
+ }, [active, fetchNetworkBenchmarks, fetchNetworkTraces]);
543
+
544
+ return (
545
+ <ScrollArea className="h-full">
546
+ <LiveMetricsStrip />
547
+
548
+ <div className="p-4 grid grid-cols-1 lg:grid-cols-2 gap-3">
549
+ {/* Left column */}
550
+ <div className="flex flex-col gap-3">
551
+ <div className="rounded-md border border-border-subtle bg-surface-0 overflow-hidden" style={{ minHeight: 200 }}>
552
+ <TpsChart />
553
+ </div>
554
+
555
+ <div className="rounded-md border border-border-subtle bg-surface-0 overflow-hidden">
556
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
557
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Token Waterfall</span>
558
+ </div>
559
+ <TokenWaterfall />
560
+ </div>
561
+
562
+ <div className="rounded-md border border-border-subtle bg-surface-0 overflow-hidden">
563
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
564
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Bottleneck Breakdown</span>
565
+ </div>
566
+ <BottleneckBreakdown />
567
+ </div>
568
+ </div>
569
+
570
+ {/* Right column */}
571
+ <div className="flex flex-col gap-3">
572
+ <div className="rounded-md border border-border-subtle bg-surface-0 overflow-hidden">
573
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0 flex items-center gap-1.5">
574
+ <Zap size={10} className="text-accent" />
575
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Node Resource Gauges</span>
576
+ </div>
577
+ <NodeGauges />
578
+ </div>
579
+
580
+ <div className="rounded-md border border-border-subtle bg-surface-0 overflow-hidden">
581
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
582
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Session Summary</span>
583
+ </div>
584
+ <SessionSummary />
585
+ </div>
586
+ </div>
587
+ </div>
588
+
589
+ {/* Bottom row — pipeline */}
590
+ <div className="px-4 pb-4">
591
+ <div className="rounded-md border border-border-subtle bg-surface-0 overflow-hidden">
592
+ <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
593
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Pipeline</span>
594
+ </div>
595
+ <PipelineVisualization />
596
+ </div>
597
+ </div>
598
+ </ScrollArea>
599
+ );
600
+ });