groove-dev 0.23.0 → 0.24.1

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 (35) hide show
  1. package/node_modules/@groove-dev/gui/dist/assets/index-C0naHS9e.css +1 -0
  2. package/node_modules/@groove-dev/gui/dist/assets/index-DJbDjzF2.js +587 -0
  3. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  4. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +11 -9
  5. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +102 -0
  6. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +106 -39
  7. package/node_modules/@groove-dev/gui/src/components/dashboard/header-bar.jsx +27 -8
  8. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +269 -0
  9. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +35 -9
  10. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +117 -0
  11. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +154 -36
  12. package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +28 -8
  13. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +7 -0
  14. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +97 -54
  15. package/package.json +1 -1
  16. package/packages/gui/dist/assets/index-C0naHS9e.css +1 -0
  17. package/packages/gui/dist/assets/index-DJbDjzF2.js +587 -0
  18. package/packages/gui/dist/index.html +2 -2
  19. package/packages/gui/src/components/dashboard/activity-feed.jsx +11 -9
  20. package/packages/gui/src/components/dashboard/cache-ring.jsx +102 -0
  21. package/packages/gui/src/components/dashboard/fleet-panel.jsx +106 -39
  22. package/packages/gui/src/components/dashboard/header-bar.jsx +27 -8
  23. package/packages/gui/src/components/dashboard/intel-panel.jsx +269 -0
  24. package/packages/gui/src/components/dashboard/kpi-card.jsx +35 -9
  25. package/packages/gui/src/components/dashboard/routing-chart.jsx +117 -0
  26. package/packages/gui/src/components/dashboard/token-chart.jsx +154 -36
  27. package/packages/gui/src/lib/hooks/use-dashboard.js +28 -8
  28. package/packages/gui/src/lib/theme-hex.js +7 -0
  29. package/packages/gui/src/views/dashboard.jsx +97 -54
  30. package/node_modules/@groove-dev/gui/dist/assets/index-Cg9SzKgD.css +0 -1
  31. package/node_modules/@groove-dev/gui/dist/assets/index-QmFja2dw.js +0 -582
  32. package/node_modules/@groove-dev/gui/src/components/dashboard/savings-panel.jsx +0 -122
  33. package/packages/gui/dist/assets/index-Cg9SzKgD.css +0 -1
  34. package/packages/gui/dist/assets/index-QmFja2dw.js +0 -582
  35. package/packages/gui/src/components/dashboard/savings-panel.jsx +0 -122
@@ -1,8 +1,9 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { memo } from 'react';
2
3
  import { cn } from '../../lib/cn';
3
4
  import { HEX } from '../../lib/theme-hex';
4
5
 
5
- function MiniSparkline({ data, color = HEX.accent, width = 80, height = 24 }) {
6
+ function MiniSparkline({ data, color = HEX.accent, width = 72, height = 22 }) {
6
7
  if (!data || data.length < 2) return <div style={{ width, height }} />;
7
8
  const vals = data.map((d) => d.v);
8
9
  const min = Math.min(...vals);
@@ -15,31 +16,56 @@ function MiniSparkline({ data, color = HEX.accent, width = 80, height = 24 }) {
15
16
  return `${x},${y}`;
16
17
  }).join(' ');
17
18
 
19
+ const gradId = `kpi-${color.replace('#', '')}`;
20
+
18
21
  return (
19
22
  <svg width={width} height={height} className="flex-shrink-0">
20
23
  <defs>
21
- <linearGradient id={`kpi-${color}`} x1="0" y1="0" x2="0" y2="1">
22
- <stop offset="0%" stopColor={color} stopOpacity="0.3" />
24
+ <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
25
+ <stop offset="0%" stopColor={color} stopOpacity="0.2" />
23
26
  <stop offset="100%" stopColor={color} stopOpacity="0" />
24
27
  </linearGradient>
25
28
  </defs>
26
- <polygon points={`0,${height} ${points} ${width},${height}`} fill={`url(#kpi-${color})`} />
27
- <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" />
29
+ <polygon points={`0,${height} ${points} ${width},${height}`} fill={`url(#${gradId})`} />
30
+ <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeOpacity="0.8" />
28
31
  </svg>
29
32
  );
30
33
  }
31
34
 
32
- export function KpiCard({ label, value, sparkData, color = HEX.accent, className }) {
35
+ const KpiCard = memo(function KpiCard({ label, value, sparkData, color = HEX.accent, className }) {
33
36
  return (
34
37
  <div className={cn(
35
- 'flex items-center gap-3 px-4 py-3 bg-surface-1 border-b border-border-subtle min-w-0',
38
+ 'flex items-center gap-2.5 px-3 py-2.5 min-w-0',
39
+ 'bg-surface-1',
36
40
  className,
37
41
  )}>
38
42
  <div className="flex-1 min-w-0">
39
- <div className="text-2xs text-text-3 font-sans uppercase tracking-wider mb-0.5 truncate">{label}</div>
40
- <div className="text-lg font-semibold font-mono text-text-0 tabular-nums">{value}</div>
43
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5 truncate">{label}</div>
44
+ <div className="text-base font-semibold font-mono text-text-0 tabular-nums leading-none">{value}</div>
41
45
  </div>
42
46
  <MiniSparkline data={sparkData} color={color} />
43
47
  </div>
44
48
  );
49
+ });
50
+
51
+ export function KpiStrip({ kpis }) {
52
+ return (
53
+ <div className="flex flex-wrap border-b border-border" style={{ background: 'var(--color-surface-0)' }}>
54
+ {kpis.map((kpi) => (
55
+ <KpiCard
56
+ key={kpi.label}
57
+ label={kpi.label}
58
+ value={kpi.value}
59
+ sparkData={kpi.sparkData}
60
+ color={kpi.color}
61
+ className={cn(
62
+ 'flex-1 basis-[12.5%] min-w-[140px]',
63
+ 'border-b border-r border-border',
64
+ )}
65
+ />
66
+ ))}
67
+ </div>
68
+ );
45
69
  }
70
+
71
+ export { KpiCard };
@@ -0,0 +1,117 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useRef, useEffect, memo } from 'react';
3
+ import { HEX } from '../../lib/theme-hex';
4
+ import { fmtNum, fmtDollar } from '../../lib/format';
5
+
6
+ const TIER_COLORS = {
7
+ heavy: HEX.danger,
8
+ medium: HEX.warning,
9
+ light: HEX.success,
10
+ };
11
+
12
+ const TIER_LABELS = {
13
+ heavy: 'Heavy',
14
+ medium: 'Medium',
15
+ light: 'Light',
16
+ };
17
+
18
+ const RoutingChart = memo(function RoutingChart({ routing, size = 120 }) {
19
+ const canvasRef = useRef(null);
20
+ if (!routing) return null;
21
+
22
+ const { byTier = {}, costByTier = {}, totalDecisions = 0, autoRoutedCount = 0 } = routing;
23
+ const tiers = ['heavy', 'medium', 'light'];
24
+ const total = tiers.reduce((s, t) => s + (byTier[t] || 0), 0);
25
+
26
+ useEffect(() => {
27
+ const canvas = canvasRef.current;
28
+ if (!canvas) return;
29
+ const dpr = window.devicePixelRatio || 1;
30
+ canvas.width = size * dpr;
31
+ canvas.height = size * dpr;
32
+ const ctx = canvas.getContext('2d');
33
+ ctx.scale(dpr, dpr);
34
+ ctx.clearRect(0, 0, size, size);
35
+
36
+ const cx = size / 2;
37
+ const cy = size / 2;
38
+ const radius = (size - 12) / 2;
39
+ const strokeWidth = 5;
40
+
41
+ if (total === 0) {
42
+ ctx.beginPath();
43
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2);
44
+ ctx.strokeStyle = HEX.surface4;
45
+ ctx.lineWidth = strokeWidth;
46
+ ctx.stroke();
47
+ } else {
48
+ let angle = -Math.PI / 2;
49
+ for (const tier of tiers) {
50
+ const count = byTier[tier] || 0;
51
+ if (count === 0) continue;
52
+ const sweep = (count / total) * Math.PI * 2;
53
+ ctx.beginPath();
54
+ ctx.arc(cx, cy, radius, angle, angle + sweep);
55
+ ctx.strokeStyle = TIER_COLORS[tier];
56
+ ctx.lineWidth = strokeWidth;
57
+ ctx.lineCap = 'butt';
58
+ ctx.stroke();
59
+ angle += sweep;
60
+ }
61
+ }
62
+
63
+ // Center text
64
+ ctx.textAlign = 'center';
65
+ ctx.textBaseline = 'middle';
66
+ ctx.font = `600 ${size * 0.19}px 'JetBrains Mono Variable', monospace`;
67
+ ctx.fillStyle = HEX.text0;
68
+ ctx.fillText(fmtNum(totalDecisions), cx, cy - 3);
69
+
70
+ ctx.font = `500 ${size * 0.09}px 'JetBrains Mono Variable', monospace`;
71
+ ctx.fillStyle = HEX.text3;
72
+ ctx.fillText('ROUTES', cx, cy + size * 0.13);
73
+ }, [routing, size, total, totalDecisions]);
74
+
75
+ return (
76
+ <div className="flex flex-col items-center justify-center h-full px-3 py-3">
77
+ <canvas
78
+ ref={canvasRef}
79
+ className="flex-shrink-0"
80
+ style={{ width: size, height: size }}
81
+ />
82
+
83
+ <div className="w-full mt-3 space-y-2 max-w-[180px]">
84
+ {tiers.map((tier) => {
85
+ const count = byTier[tier] || 0;
86
+ const cost = costByTier[tier] || 0;
87
+ const pct = total > 0 ? (count / total) * 100 : 0;
88
+ return (
89
+ <div key={tier} className="space-y-0.5">
90
+ <div className="flex items-center gap-2 text-xs font-mono">
91
+ <span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: TIER_COLORS[tier] }} />
92
+ <span className="text-text-3 uppercase tracking-wider flex-1">{TIER_LABELS[tier]}</span>
93
+ <span className="text-text-1 tabular-nums">{count}</span>
94
+ <span className="text-text-4">/</span>
95
+ <span className="text-text-2 tabular-nums">{fmtDollar(cost)}</span>
96
+ </div>
97
+ <div className="h-[2px] bg-surface-0 rounded-full overflow-hidden ml-3.5">
98
+ <div
99
+ className="h-full rounded-full transition-all duration-500"
100
+ style={{ width: `${Math.min(pct, 100)}%`, background: TIER_COLORS[tier] }}
101
+ />
102
+ </div>
103
+ </div>
104
+ );
105
+ })}
106
+ </div>
107
+
108
+ {autoRoutedCount > 0 && (
109
+ <div className="mt-2.5 text-2xs font-mono text-text-3 uppercase tracking-wider">
110
+ {autoRoutedCount} auto-routed
111
+ </div>
112
+ )}
113
+ </div>
114
+ );
115
+ });
116
+
117
+ export { RoutingChart };
@@ -1,10 +1,27 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useRef, useEffect } from 'react';
3
- import { HEX } from '../../lib/theme-hex';
4
- import { fmtNum, fmtDollar } from '../../lib/format';
2
+ import { useRef, useEffect, useState, useCallback, memo } from 'react';
3
+ import { HEX, hexAlpha } from '../../lib/theme-hex';
4
+ import { fmtNum, fmtDollar, fmtPct } from '../../lib/format';
5
5
 
6
- export function TokenChart({ data, width, height }) {
6
+ const TokenChart = memo(function TokenChart({ data, width, height }) {
7
7
  const canvasRef = useRef(null);
8
+ const [hover, setHover] = useState(null); // { x, index }
9
+
10
+ const pad = { top: 16, right: 62, bottom: 30, left: 62 };
11
+ const w = width - pad.left - pad.right;
12
+ const h = height - pad.top - pad.bottom;
13
+
14
+ const onMouseMove = useCallback((e) => {
15
+ const canvas = canvasRef.current;
16
+ if (!canvas || !data?.length) return;
17
+ const rect = canvas.getBoundingClientRect();
18
+ const x = e.clientX - rect.left - pad.left;
19
+ if (x < 0 || x > w) { setHover(null); return; }
20
+ const index = Math.round((x / w) * (data.length - 1));
21
+ setHover({ x: pad.left + (index / (data.length - 1)) * w, index });
22
+ }, [data, w, pad.left]);
23
+
24
+ const onMouseLeave = useCallback(() => setHover(null), []);
8
25
 
9
26
  useEffect(() => {
10
27
  const canvas = canvasRef.current;
@@ -17,17 +34,15 @@ export function TokenChart({ data, width, height }) {
17
34
  ctx.scale(dpr, dpr);
18
35
  ctx.clearRect(0, 0, width, height);
19
36
 
20
- const pad = { top: 12, right: 60, bottom: 28, left: 60 };
21
- const w = width - pad.left - pad.right;
22
- const h = height - pad.top - pad.bottom;
23
-
24
37
  const tokens = data.map((d) => d.tokens || 0);
25
38
  const costs = data.map((d) => d.costUsd || 0);
39
+ const caches = data.map((d) => d.cacheHitRate ?? null);
26
40
  const maxT = Math.max(...tokens, 1);
27
41
  const maxC = Math.max(...costs, 0.01);
42
+ const hasCacheData = caches.some((c) => c !== null && c > 0);
28
43
 
29
44
  // Grid lines
30
- ctx.strokeStyle = HEX.surface4;
45
+ ctx.strokeStyle = HEX.surface5;
31
46
  ctx.lineWidth = 0.5;
32
47
  for (let i = 0; i <= 4; i++) {
33
48
  const y = pad.top + (h / 4) * i;
@@ -37,19 +52,23 @@ export function TokenChart({ data, width, height }) {
37
52
  ctx.stroke();
38
53
  }
39
54
 
40
- // Token area
55
+ // Helper: map data index to x
56
+ const xAt = (i) => pad.left + (i / (data.length - 1)) * w;
57
+ const yToken = (v) => pad.top + h - (v / maxT) * h;
58
+ const yCost = (v) => pad.top + h - (v / maxC) * h;
59
+ const yCache = (v) => pad.top + h - (v * h);
60
+
61
+ // Token area fill
41
62
  ctx.beginPath();
42
63
  ctx.moveTo(pad.left, pad.top + h);
43
- data.forEach((d, i) => {
44
- const x = pad.left + (i / (data.length - 1)) * w;
45
- const y = pad.top + h - ((d.tokens || 0) / maxT) * h;
46
- ctx.lineTo(x, y);
47
- });
48
- ctx.lineTo(pad.left + w, pad.top + h);
64
+ for (let i = 0; i < data.length; i++) {
65
+ ctx.lineTo(xAt(i), yToken(tokens[i]));
66
+ }
67
+ ctx.lineTo(xAt(data.length - 1), pad.top + h);
49
68
  ctx.closePath();
50
69
  const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + h);
51
- grad.addColorStop(0, 'rgba(51, 175, 188, 0.2)');
52
- grad.addColorStop(1, 'rgba(51, 175, 188, 0.01)');
70
+ grad.addColorStop(0, hexAlpha(HEX.accent, 0.12));
71
+ grad.addColorStop(1, hexAlpha(HEX.accent, 0.01));
53
72
  ctx.fillStyle = grad;
54
73
  ctx.fill();
55
74
 
@@ -57,29 +76,48 @@ export function TokenChart({ data, width, height }) {
57
76
  ctx.beginPath();
58
77
  ctx.strokeStyle = HEX.accent;
59
78
  ctx.lineWidth = 1.5;
60
- data.forEach((d, i) => {
61
- const x = pad.left + (i / (data.length - 1)) * w;
62
- const y = pad.top + h - ((d.tokens || 0) / maxT) * h;
79
+ for (let i = 0; i < data.length; i++) {
80
+ const x = xAt(i);
81
+ const y = yToken(tokens[i]);
63
82
  i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
64
- });
83
+ }
65
84
  ctx.stroke();
66
85
 
67
- // Cost line
86
+ // Cost line (dashed)
68
87
  ctx.beginPath();
69
88
  ctx.strokeStyle = HEX.warning;
70
89
  ctx.lineWidth = 1;
71
90
  ctx.setLineDash([4, 3]);
72
- data.forEach((d, i) => {
73
- const x = pad.left + (i / (data.length - 1)) * w;
74
- const y = pad.top + h - ((d.costUsd || 0) / maxC) * h;
91
+ for (let i = 0; i < data.length; i++) {
92
+ const x = xAt(i);
93
+ const y = yCost(costs[i]);
75
94
  i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
76
- });
95
+ }
77
96
  ctx.stroke();
78
97
  ctx.setLineDash([]);
79
98
 
99
+ // Cache hit rate line (dotted, if data available)
100
+ if (hasCacheData) {
101
+ ctx.beginPath();
102
+ ctx.strokeStyle = hexAlpha(HEX.info, 0.5);
103
+ ctx.lineWidth = 1;
104
+ ctx.setLineDash([2, 3]);
105
+ let started = false;
106
+ for (let i = 0; i < data.length; i++) {
107
+ const c = caches[i];
108
+ if (c === null || c === undefined) continue;
109
+ const x = xAt(i);
110
+ const y = yCache(c);
111
+ if (!started) { ctx.moveTo(x, y); started = true; }
112
+ else ctx.lineTo(x, y);
113
+ }
114
+ ctx.stroke();
115
+ ctx.setLineDash([]);
116
+ }
117
+
80
118
  // Left axis labels (tokens)
81
- ctx.fillStyle = HEX.text3;
82
- ctx.font = '10px var(--font-mono)';
119
+ ctx.fillStyle = HEX.text2;
120
+ ctx.font = "10px 'JetBrains Mono Variable', monospace";
83
121
  ctx.textAlign = 'right';
84
122
  for (let i = 0; i <= 4; i++) {
85
123
  const val = (maxT / 4) * (4 - i);
@@ -88,25 +126,105 @@ export function TokenChart({ data, width, height }) {
88
126
 
89
127
  // Right axis labels (cost)
90
128
  ctx.textAlign = 'left';
91
- ctx.fillStyle = HEX.text4;
129
+ ctx.fillStyle = HEX.text3;
92
130
  for (let i = 0; i <= 4; i++) {
93
131
  const val = (maxC / 4) * (4 - i);
94
132
  ctx.fillText(fmtDollar(val), pad.left + w + 6, pad.top + (h / 4) * i + 4);
95
133
  }
96
134
 
97
135
  // Legend
98
- ctx.font = '10px sans-serif';
136
+ ctx.font = "10px 'Inter Variable', sans-serif";
137
+ ctx.textAlign = 'left';
138
+ const legendY = height - 8;
139
+ let lx = pad.left;
140
+
99
141
  ctx.fillStyle = HEX.accent;
100
- ctx.fillText('● Tokens', pad.left, height - 6);
142
+ ctx.fillRect(lx, legendY - 3, 8, 1.5);
143
+ lx += 11;
144
+ ctx.fillStyle = HEX.text2;
145
+ ctx.fillText('Tokens', lx, legendY);
146
+ lx += 44;
147
+
101
148
  ctx.fillStyle = HEX.warning;
102
- ctx.fillText('● Cost', pad.left + 70, height - 6);
103
- }, [data, width, height]);
149
+ ctx.setLineDash([3, 2]);
150
+ ctx.beginPath();
151
+ ctx.moveTo(lx, legendY - 2);
152
+ ctx.lineTo(lx + 8, legendY - 2);
153
+ ctx.strokeStyle = HEX.warning;
154
+ ctx.lineWidth = 1;
155
+ ctx.stroke();
156
+ ctx.setLineDash([]);
157
+ lx += 11;
158
+ ctx.fillStyle = HEX.text2;
159
+ ctx.fillText('Cost', lx, legendY);
160
+
161
+ if (hasCacheData) {
162
+ lx += 36;
163
+ ctx.fillStyle = hexAlpha(HEX.info, 0.5);
164
+ ctx.setLineDash([2, 2]);
165
+ ctx.beginPath();
166
+ ctx.moveTo(lx, legendY - 2);
167
+ ctx.lineTo(lx + 8, legendY - 2);
168
+ ctx.strokeStyle = hexAlpha(HEX.info, 0.5);
169
+ ctx.lineWidth = 1;
170
+ ctx.stroke();
171
+ ctx.setLineDash([]);
172
+ lx += 11;
173
+ ctx.fillStyle = HEX.text2;
174
+ ctx.fillText('Cache', lx, legendY);
175
+ }
176
+
177
+ // Hover crosshair
178
+ if (hover && hover.index >= 0 && hover.index < data.length) {
179
+ const hx = hover.x;
180
+ ctx.beginPath();
181
+ ctx.moveTo(hx, pad.top);
182
+ ctx.lineTo(hx, pad.top + h);
183
+ ctx.strokeStyle = hexAlpha(HEX.text2, 0.3);
184
+ ctx.lineWidth = 1;
185
+ ctx.setLineDash([]);
186
+ ctx.stroke();
187
+
188
+ // Tooltip background
189
+ const d = data[hover.index];
190
+ const lines = [
191
+ `${fmtNum(d.tokens || 0)} tok`,
192
+ `${fmtDollar(d.costUsd || 0)}`,
193
+ ];
194
+ if (d.cacheHitRate != null) lines.push(`${fmtPct(d.cacheHitRate * 100)} cache`);
195
+
196
+ const tooltipW = 80;
197
+ const tooltipH = lines.length * 14 + 8;
198
+ let tx = hx + 8;
199
+ if (tx + tooltipW > width - 4) tx = hx - tooltipW - 8;
200
+ const ty = pad.top + 8;
201
+
202
+ ctx.fillStyle = hexAlpha(HEX.surface1, 0.95);
203
+ ctx.strokeStyle = HEX.surface4;
204
+ ctx.lineWidth = 1;
205
+ ctx.beginPath();
206
+ ctx.roundRect(tx, ty, tooltipW, tooltipH, 3);
207
+ ctx.fill();
208
+ ctx.stroke();
209
+
210
+ ctx.font = "9px 'JetBrains Mono Variable', monospace";
211
+ ctx.fillStyle = HEX.text1;
212
+ ctx.textAlign = 'left';
213
+ lines.forEach((line, i) => {
214
+ ctx.fillText(line, tx + 6, ty + 14 + i * 14);
215
+ });
216
+ }
217
+ }, [data, width, height, hover, w, h, pad.left, pad.top, pad.right, pad.bottom]);
104
218
 
105
219
  return (
106
220
  <canvas
107
221
  ref={canvasRef}
108
222
  style={{ width, height }}
109
- className="block"
223
+ className="block cursor-crosshair"
224
+ onMouseMove={onMouseMove}
225
+ onMouseLeave={onMouseLeave}
110
226
  />
111
227
  );
112
- }
228
+ });
229
+
230
+ export { TokenChart };
@@ -9,7 +9,10 @@ export function useDashboard() {
9
9
 
10
10
  const [data, setData] = useState(null);
11
11
  const [loading, setLoading] = useState(true);
12
- const [kpiHistory, setKpiHistory] = useState({ tokens: [], cost: [], saved: [], efficiency: [], cache: [] });
12
+ const [kpiHistory, setKpiHistory] = useState({
13
+ tokens: [], cost: [], saved: [], efficiency: [],
14
+ cache: [], inputOutput: [], agents: [], turns: [],
15
+ });
13
16
  const lastFetch = useRef(0);
14
17
 
15
18
  useEffect(() => {
@@ -28,15 +31,20 @@ export function useDashboard() {
28
31
  setKpiHistory((prev) => {
29
32
  const now = Date.now();
30
33
  const add = (arr, val) => [...arr.slice(-59), { t: now, v: val || 0 }];
34
+ const totalUsed = d.tokens?.totalTokens || 0;
35
+ const totalSaved = d.tokens?.savings?.total || 0;
36
+ const hypothetical = totalUsed + totalSaved;
37
+ const input = d.tokens?.totalInputTokens || 0;
38
+ const output = d.tokens?.totalOutputTokens || 0;
31
39
  return {
32
- tokens: add(prev.tokens, d.tokens?.totalUsed),
40
+ tokens: add(prev.tokens, totalUsed),
33
41
  cost: add(prev.cost, d.tokens?.totalCostUsd),
34
- saved: add(prev.saved, d.tokens?.totalSaved),
35
- efficiency: add(prev.efficiency, (() => {
36
- const h = (d.tokens?.totalUsed || 0) + (d.tokens?.totalSaved || 0);
37
- return h > 0 ? ((d.tokens?.totalSaved || 0) / h) * 100 : 0;
38
- })()),
42
+ saved: add(prev.saved, totalSaved),
43
+ efficiency: add(prev.efficiency, hypothetical > 0 ? (totalSaved / hypothetical) * 100 : 0),
39
44
  cache: add(prev.cache, d.tokens?.cacheHitRate),
45
+ inputOutput: add(prev.inputOutput, output > 0 ? input / output : 0),
46
+ agents: add(prev.agents, d.agents?.running || 0),
47
+ turns: add(prev.turns, d.tokens?.totalTurns),
40
48
  };
41
49
  });
42
50
  } catch {
@@ -49,5 +57,17 @@ export function useDashboard() {
49
57
  return () => { alive = false; clearInterval(interval); };
50
58
  }, [connected]);
51
59
 
52
- return { data, loading, agents, connected, kpiHistory, lastFetch: lastFetch.current };
60
+ // Derive enriched sub-objects from data
61
+ const agentBreakdown = data?.agents?.breakdown || [];
62
+ const routing = data?.routing || null;
63
+ const rotation = data?.rotation || null;
64
+ const adaptive = data?.adaptive || [];
65
+ const journalist = data?.journalist || null;
66
+ const rotating = rotation?.rotating || [];
67
+
68
+ return {
69
+ data, loading, agents, connected, kpiHistory,
70
+ lastFetch: lastFetch.current,
71
+ agentBreakdown, routing, rotation, adaptive, journalist, rotating,
72
+ };
53
73
  }
@@ -1,6 +1,13 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  // Raw hex values for Canvas 2D rendering (CSS vars don't work in canvas)
3
3
 
4
+ export function hexAlpha(hex, alpha) {
5
+ const r = parseInt(hex.slice(1, 3), 16);
6
+ const g = parseInt(hex.slice(3, 5), 16);
7
+ const b = parseInt(hex.slice(5, 7), 16);
8
+ return `rgba(${r},${g},${b},${alpha})`;
9
+ }
10
+
4
11
  export const HEX = {
5
12
  // Surfaces
6
13
  surface0: '#1a1e25',