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.
- package/node_modules/@groove-dev/gui/dist/assets/index-C0naHS9e.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DJbDjzF2.js +587 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +11 -9
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +102 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +106 -39
- package/node_modules/@groove-dev/gui/src/components/dashboard/header-bar.jsx +27 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +269 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +35 -9
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +117 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +154 -36
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +28 -8
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +7 -0
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +97 -54
- package/package.json +1 -1
- package/packages/gui/dist/assets/index-C0naHS9e.css +1 -0
- package/packages/gui/dist/assets/index-DJbDjzF2.js +587 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/dashboard/activity-feed.jsx +11 -9
- package/packages/gui/src/components/dashboard/cache-ring.jsx +102 -0
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +106 -39
- package/packages/gui/src/components/dashboard/header-bar.jsx +27 -8
- package/packages/gui/src/components/dashboard/intel-panel.jsx +269 -0
- package/packages/gui/src/components/dashboard/kpi-card.jsx +35 -9
- package/packages/gui/src/components/dashboard/routing-chart.jsx +117 -0
- package/packages/gui/src/components/dashboard/token-chart.jsx +154 -36
- package/packages/gui/src/lib/hooks/use-dashboard.js +28 -8
- package/packages/gui/src/lib/theme-hex.js +7 -0
- package/packages/gui/src/views/dashboard.jsx +97 -54
- package/node_modules/@groove-dev/gui/dist/assets/index-Cg9SzKgD.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-QmFja2dw.js +0 -582
- package/node_modules/@groove-dev/gui/src/components/dashboard/savings-panel.jsx +0 -122
- package/packages/gui/dist/assets/index-Cg9SzKgD.css +0 -1
- package/packages/gui/dist/assets/index-QmFja2dw.js +0 -582
- 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 =
|
|
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={
|
|
22
|
-
<stop offset="0%" stopColor={color} stopOpacity="0.
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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
|
|
40
|
-
<div className="text-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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,
|
|
52
|
-
grad.addColorStop(1,
|
|
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.
|
|
61
|
-
const x =
|
|
62
|
-
const y =
|
|
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.
|
|
73
|
-
const x =
|
|
74
|
-
const y =
|
|
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.
|
|
82
|
-
ctx.font =
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
103
|
-
|
|
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({
|
|
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,
|
|
40
|
+
tokens: add(prev.tokens, totalUsed),
|
|
33
41
|
cost: add(prev.cost, d.tokens?.totalCostUsd),
|
|
34
|
-
saved: add(prev.saved,
|
|
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
|
-
|
|
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',
|