groove-dev 0.22.31 → 0.24.0
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/cli/src/setup.js +7 -9
- package/node_modules/@groove-dev/daemon/src/api.js +87 -4
- package/node_modules/@groove-dev/daemon/src/process.js +1 -0
- package/node_modules/@groove-dev/daemon/src/teams.js +77 -5
- package/node_modules/@groove-dev/daemon/src/validate.js +1 -0
- package/node_modules/@groove-dev/daemon/test/teams.test.js +5 -5
- package/node_modules/@groove-dev/gui/dist/assets/index-CqdQP7yG.js +587 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DvcNOnKP.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +139 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +16 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +105 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +106 -38
- package/node_modules/@groove-dev/gui/src/components/dashboard/header-bar.jsx +28 -9
- 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 +121 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +152 -34
- 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/stores/groove.js +4 -2
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +97 -52
- package/package.json +1 -1
- package/packages/cli/src/setup.js +7 -9
- package/packages/daemon/src/api.js +87 -4
- package/packages/daemon/src/process.js +1 -0
- package/packages/daemon/src/teams.js +77 -5
- package/packages/daemon/src/validate.js +1 -0
- package/packages/gui/dist/assets/index-CqdQP7yG.js +587 -0
- package/packages/gui/dist/assets/index-DvcNOnKP.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/agents/agent-mdfiles.jsx +139 -0
- package/packages/gui/src/components/agents/agent-panel.jsx +4 -1
- package/packages/gui/src/components/dashboard/activity-feed.jsx +16 -14
- package/packages/gui/src/components/dashboard/cache-ring.jsx +105 -0
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +106 -38
- package/packages/gui/src/components/dashboard/header-bar.jsx +28 -9
- 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 +121 -0
- package/packages/gui/src/components/dashboard/token-chart.jsx +152 -34
- 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/stores/groove.js +4 -2
- package/packages/gui/src/views/dashboard.jsx +97 -52
- package/node_modules/@groove-dev/gui/dist/assets/index-CL4GvVoL.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-D_tSBDCx.js +0 -577
- package/node_modules/@groove-dev/gui/src/components/dashboard/savings-panel.jsx +0 -122
- package/packages/gui/dist/assets/index-CL4GvVoL.css +0 -1
- package/packages/gui/dist/assets/index-D_tSBDCx.js +0 -577
- package/packages/gui/src/components/dashboard/savings-panel.jsx +0 -122
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useRef, useEffect, memo } from 'react';
|
|
3
|
+
import { HEX, hexAlpha } 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
|
+
// Full 360-degree donut
|
|
42
|
+
if (total === 0) {
|
|
43
|
+
// Empty state — full gray ring
|
|
44
|
+
ctx.beginPath();
|
|
45
|
+
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
46
|
+
ctx.strokeStyle = HEX.surface4;
|
|
47
|
+
ctx.lineWidth = strokeWidth;
|
|
48
|
+
ctx.stroke();
|
|
49
|
+
} else {
|
|
50
|
+
let angle = -Math.PI / 2; // Start at top
|
|
51
|
+
for (const tier of tiers) {
|
|
52
|
+
const count = byTier[tier] || 0;
|
|
53
|
+
if (count === 0) continue;
|
|
54
|
+
const sweep = (count / total) * Math.PI * 2;
|
|
55
|
+
ctx.beginPath();
|
|
56
|
+
ctx.arc(cx, cy, radius, angle, angle + sweep);
|
|
57
|
+
ctx.strokeStyle = TIER_COLORS[tier];
|
|
58
|
+
ctx.lineWidth = strokeWidth;
|
|
59
|
+
ctx.lineCap = 'butt';
|
|
60
|
+
ctx.stroke();
|
|
61
|
+
angle += sweep;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Center text
|
|
66
|
+
ctx.textAlign = 'center';
|
|
67
|
+
ctx.textBaseline = 'middle';
|
|
68
|
+
ctx.font = `600 ${size * 0.17}px 'JetBrains Mono Variable', monospace`;
|
|
69
|
+
ctx.fillStyle = HEX.text0;
|
|
70
|
+
ctx.fillText(fmtNum(totalDecisions), cx, cy - 3);
|
|
71
|
+
|
|
72
|
+
ctx.font = `500 ${size * 0.08}px 'JetBrains Mono Variable', monospace`;
|
|
73
|
+
ctx.fillStyle = HEX.text4;
|
|
74
|
+
ctx.fillText('ROUTES', cx, cy + size * 0.12);
|
|
75
|
+
}, [routing, size, total, totalDecisions]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex flex-col items-center justify-center h-full px-3 py-3">
|
|
79
|
+
<canvas
|
|
80
|
+
ref={canvasRef}
|
|
81
|
+
className="flex-shrink-0"
|
|
82
|
+
style={{ width: size, height: size }}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* Tier breakdown */}
|
|
86
|
+
<div className="w-full mt-3 space-y-2 max-w-[180px]">
|
|
87
|
+
{tiers.map((tier) => {
|
|
88
|
+
const count = byTier[tier] || 0;
|
|
89
|
+
const cost = costByTier[tier] || 0;
|
|
90
|
+
const pct = total > 0 ? (count / total) * 100 : 0;
|
|
91
|
+
return (
|
|
92
|
+
<div key={tier} className="space-y-0.5">
|
|
93
|
+
<div className="flex items-center gap-2 text-[9px] font-mono">
|
|
94
|
+
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: TIER_COLORS[tier] }} />
|
|
95
|
+
<span className="text-[#505862] uppercase tracking-wider flex-1">{TIER_LABELS[tier]}</span>
|
|
96
|
+
<span className="text-[#8b929e] tabular-nums">{count}</span>
|
|
97
|
+
<span className="text-[#3a3f4b]">/</span>
|
|
98
|
+
<span className="text-[#6e7681] tabular-nums">{fmtDollar(cost)}</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div className="h-[2px] bg-[#1a1e25] rounded-full overflow-hidden ml-3.5">
|
|
101
|
+
<div
|
|
102
|
+
className="h-full rounded-full transition-all duration-500"
|
|
103
|
+
style={{ width: `${Math.min(pct, 100)}%`, background: TIER_COLORS[tier] }}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Auto-routed badge */}
|
|
112
|
+
{autoRoutedCount > 0 && (
|
|
113
|
+
<div className="mt-2.5 text-[8px] font-mono text-[#505862] uppercase tracking-wider">
|
|
114
|
+
{autoRoutedCount} auto-routed
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
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.surface3;
|
|
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
119
|
ctx.fillStyle = HEX.text3;
|
|
82
|
-
ctx.font =
|
|
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);
|
|
@@ -95,18 +133,98 @@ export function TokenChart({ data, width, height }) {
|
|
|
95
133
|
}
|
|
96
134
|
|
|
97
135
|
// Legend
|
|
98
|
-
ctx.font = '
|
|
136
|
+
ctx.font = "9px '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.text3;
|
|
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.text3;
|
|
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.text3;
|
|
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',
|
|
@@ -310,9 +310,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
310
310
|
localStorage.setItem('groove:activeTeamId', id);
|
|
311
311
|
},
|
|
312
312
|
|
|
313
|
-
async createTeam(name) {
|
|
313
|
+
async createTeam(name, workingDir) {
|
|
314
314
|
try {
|
|
315
|
-
const
|
|
315
|
+
const body = { name };
|
|
316
|
+
if (workingDir) body.workingDir = workingDir;
|
|
317
|
+
const team = await api.post('/teams', body);
|
|
316
318
|
// Only set activeTeamId — the WS team:created handler adds to the teams array
|
|
317
319
|
set({ activeTeamId: team.id });
|
|
318
320
|
localStorage.setItem('groove:activeTeamId', team.id);
|