groove-dev 0.9.1 → 0.9.2
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/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-iBqV1c12.js +73 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/views/CommandCenter.jsx +212 -333
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/gui/dist/assets/index-iBqV1c12.js +73 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/views/CommandCenter.jsx +212 -333
- package/node_modules/@groove-dev/gui/dist/assets/index-B-shwTbq.js +0 -73
- package/packages/gui/dist/assets/index-B-shwTbq.js +0 -73
|
@@ -10,7 +10,7 @@ const AMBER = '#e5c07b';
|
|
|
10
10
|
const RED = '#e06c75';
|
|
11
11
|
const PURPLE = '#c678dd';
|
|
12
12
|
const BLUE = '#61afef';
|
|
13
|
-
const COLORS = [ACCENT, AMBER, GREEN, PURPLE, RED, BLUE, '#d19a66'
|
|
13
|
+
const COLORS = [ACCENT, AMBER, GREEN, PURPLE, RED, BLUE, '#d19a66'];
|
|
14
14
|
const COST_PER_1K = { heavy: 0.045, medium: 0.009, light: 0.0024 };
|
|
15
15
|
|
|
16
16
|
export default function CommandCenter() {
|
|
@@ -29,8 +29,6 @@ export default function CommandCenter() {
|
|
|
29
29
|
const res = await fetch('/api/dashboard');
|
|
30
30
|
const d = await res.json();
|
|
31
31
|
setData(d);
|
|
32
|
-
|
|
33
|
-
// Build telemetry in the Zustand store so it persists across tab switches
|
|
34
32
|
useGrooveStore.setState((s) => {
|
|
35
33
|
const telem = { ...s.dashTelemetry };
|
|
36
34
|
const now = Date.now();
|
|
@@ -60,107 +58,112 @@ export default function CommandCenter() {
|
|
|
60
58
|
const { tokens, routing, rotation, adaptive, journalist, uptime } = data;
|
|
61
59
|
const agentBreakdown = data.agents.breakdown;
|
|
62
60
|
const estDollarSaved = (tokens.savings.total / 1000) * COST_PER_1K.medium;
|
|
63
|
-
// Show max context reached across ALL agents (historical peak, not just running)
|
|
64
|
-
const maxCtx = agentBreakdown.length > 0
|
|
65
|
-
? Math.round(Math.max(...agentBreakdown.map((a) => a.contextUsage || 0)) * 100)
|
|
66
|
-
: 0;
|
|
67
61
|
|
|
68
62
|
return (
|
|
69
63
|
<div style={s.root}>
|
|
70
64
|
|
|
71
|
-
{/*
|
|
72
|
-
<div style={s.
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
{/* ROW 1 — Stat Cards */}
|
|
66
|
+
<div style={s.statRow}>
|
|
67
|
+
<StatCard
|
|
68
|
+
label="Total Tokens"
|
|
69
|
+
value={fmtNum(tokens.totalTokens)}
|
|
70
|
+
sub={`${data.agents.total} agent${data.agents.total !== 1 ? 's' : ''}`}
|
|
71
|
+
/>
|
|
72
|
+
<StatCard
|
|
73
|
+
label="Estimated Savings"
|
|
74
|
+
value={estDollarSaved > 0 ? `$${estDollarSaved.toFixed(2)}` : '$0.00'}
|
|
75
|
+
sub={`${fmtNum(tokens.savings.total)} tokens saved`}
|
|
76
|
+
color={GREEN}
|
|
77
|
+
/>
|
|
78
|
+
<StatCard
|
|
79
|
+
label="Efficiency"
|
|
80
|
+
value={`${tokens.savings.percentage || 0}%`}
|
|
81
|
+
sub="vs uncoordinated"
|
|
82
|
+
color={tokens.savings.percentage > 0 ? GREEN : undefined}
|
|
83
|
+
/>
|
|
84
|
+
<StatCard
|
|
85
|
+
label="Rotations"
|
|
86
|
+
value={rotation.totalRotations}
|
|
87
|
+
sub={fmtUptime(uptime)}
|
|
88
|
+
/>
|
|
89
89
|
</div>
|
|
90
90
|
|
|
91
|
-
{/*
|
|
92
|
-
<div style={s.
|
|
93
|
-
<div style={s.
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
<span style={{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
{/* ROW 2 — Area Chart + Donut */}
|
|
92
|
+
<div style={s.midRow}>
|
|
93
|
+
<div style={s.chartPanel}>
|
|
94
|
+
<div style={s.panelHead}>
|
|
95
|
+
<span>Token Usage</span>
|
|
96
|
+
<span style={s.panelHeadRight}>
|
|
97
|
+
{agentBreakdown.filter((a) => a.tokens > 0).map((a, i) => (
|
|
98
|
+
<span key={a.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, marginLeft: 12 }}>
|
|
99
|
+
<span style={{ width: 6, height: 2, background: COLORS[i % COLORS.length], display: 'inline-block', borderRadius: 1 }} />
|
|
100
|
+
<span style={{ fontSize: 9 }}>{a.name}</span>
|
|
101
|
+
</span>
|
|
102
|
+
))}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
<TelemetryChart tokenTimeline={dashTelemetry} agents={agents} />
|
|
106
|
+
</div>
|
|
107
|
+
<div style={s.donutPanel}>
|
|
108
|
+
<div style={s.panelHead}>Model Routing</div>
|
|
109
|
+
<DonutChart routing={routing} />
|
|
103
110
|
</div>
|
|
104
|
-
<TelemetryChart tokenTimeline={dashTelemetry} agents={agents} />
|
|
105
111
|
</div>
|
|
106
112
|
|
|
107
|
-
{/*
|
|
113
|
+
{/* ROW 3 — Savings Bars + Agent Fleet */}
|
|
108
114
|
<div style={s.bottomRow}>
|
|
109
|
-
|
|
110
|
-
{/* AGENT FLEET */}
|
|
111
115
|
<div style={s.panel}>
|
|
112
|
-
<div style={s.panelHead}>
|
|
116
|
+
<div style={s.panelHead}>Savings Breakdown</div>
|
|
113
117
|
<div style={s.scrollInner}>
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<AgentCard key={a.id} agent={a} total={tokens.totalTokens} color={COLORS[i % COLORS.length]} />
|
|
118
|
-
))}
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
{/* SAVINGS + ROUTING + ADAPTIVE */}
|
|
123
|
-
<div style={s.panel}>
|
|
124
|
-
<div style={s.panelHead}>SAVINGS & ROUTING</div>
|
|
125
|
-
<div style={s.scrollInner}>
|
|
126
|
-
<SavingsBlock savings={tokens.savings} />
|
|
127
|
-
<div style={s.divider} />
|
|
128
|
-
<RoutingBlock routing={routing} />
|
|
118
|
+
<HorizBar label="Rotation" value={tokens.savings.fromRotation} max={tokens.savings.total || 1} color={ACCENT} />
|
|
119
|
+
<HorizBar label="Conflict Prevention" value={tokens.savings.fromConflictPrevention} max={tokens.savings.total || 1} color={AMBER} />
|
|
120
|
+
<HorizBar label="Cold-Start Skip" value={tokens.savings.fromColdStartSkip} max={tokens.savings.total || 1} color={GREEN} />
|
|
129
121
|
<div style={s.divider} />
|
|
130
|
-
<
|
|
122
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#5c6370', padding: '4px 0' }}>
|
|
123
|
+
<span>Without Groove</span>
|
|
124
|
+
<span style={{ color: RED }}>{fmtNum(tokens.savings.estimatedWithoutGroove)}</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: '#5c6370', padding: '4px 0' }}>
|
|
127
|
+
<span>With Groove</span>
|
|
128
|
+
<span style={{ color: GREEN }}>{fmtNum(tokens.totalTokens)}</span>
|
|
129
|
+
</div>
|
|
130
|
+
{journalist.lastSummary && (
|
|
131
|
+
<>
|
|
132
|
+
<div style={s.divider} />
|
|
133
|
+
<div style={s.panelHead}>Journalist</div>
|
|
134
|
+
<div style={s.journSummary}>{journalist.lastSummary}</div>
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
131
137
|
</div>
|
|
132
138
|
</div>
|
|
133
|
-
|
|
134
|
-
{/* JOURNALIST + ROTATION */}
|
|
135
139
|
<div style={s.panel}>
|
|
136
|
-
<div style={s.panelHead}>
|
|
137
|
-
JOURNALIST
|
|
138
|
-
<span style={{ ...s.liveBadge, background: journalist.running ? GREEN : 'var(--text-dim)' }}>
|
|
139
|
-
{journalist.running ? 'LIVE' : 'IDLE'}
|
|
140
|
-
</span>
|
|
141
|
-
</div>
|
|
140
|
+
<div style={s.panelHead}>Agent Fleet</div>
|
|
142
141
|
<div style={s.scrollInner}>
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
{rotation.history.length === 0 ? (
|
|
155
|
-
<div style={s.empty}>No rotations yet</div>
|
|
156
|
-
) : rotation.history.slice().reverse().slice(0, 10).map((r, i) => (
|
|
157
|
-
<div key={i} style={s.rotEntry}>
|
|
158
|
-
<span style={s.rotDot} />
|
|
159
|
-
<span style={s.rotName}>{r.agentName}</span>
|
|
160
|
-
<span style={s.rotSaved}>{fmtNum(r.oldTokens)} saved</span>
|
|
161
|
-
<span style={s.rotTime}>{timeAgo(r.timestamp)}</span>
|
|
162
|
-
</div>
|
|
142
|
+
{agentBreakdown.length === 0 ? (
|
|
143
|
+
<div style={s.empty}>No agents spawned</div>
|
|
144
|
+
) : agentBreakdown.map((a, i) => (
|
|
145
|
+
<HorizBar
|
|
146
|
+
key={a.id}
|
|
147
|
+
label={a.name}
|
|
148
|
+
value={a.tokens}
|
|
149
|
+
max={Math.max(...agentBreakdown.map((x) => x.tokens), 1)}
|
|
150
|
+
color={COLORS[i % COLORS.length]}
|
|
151
|
+
sub={`${a.role} · ${a.model || 'auto'}`}
|
|
152
|
+
/>
|
|
163
153
|
))}
|
|
154
|
+
{rotation.history.length > 0 && (
|
|
155
|
+
<>
|
|
156
|
+
<div style={s.divider} />
|
|
157
|
+
<div style={s.panelHead}>Rotation History</div>
|
|
158
|
+
{rotation.history.slice().reverse().slice(0, 8).map((r, i) => (
|
|
159
|
+
<div key={i} style={s.rotEntry}>
|
|
160
|
+
<span style={s.rotName}>{r.agentName}</span>
|
|
161
|
+
<span style={{ color: GREEN, fontSize: 9 }}>{fmtNum(r.oldTokens)} saved</span>
|
|
162
|
+
<span style={s.rotTime}>{timeAgo(r.timestamp)}</span>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</>
|
|
166
|
+
)}
|
|
164
167
|
</div>
|
|
165
168
|
</div>
|
|
166
169
|
</div>
|
|
@@ -168,163 +171,101 @@ export default function CommandCenter() {
|
|
|
168
171
|
);
|
|
169
172
|
}
|
|
170
173
|
|
|
171
|
-
// ──
|
|
172
|
-
function GaugeChart({ value, max, label, unit, color }) {
|
|
173
|
-
const pct = max > 0 ? Math.min(value / max, 1) : 0;
|
|
174
|
-
const r = 32;
|
|
175
|
-
const cx = 40;
|
|
176
|
-
const cy = 38;
|
|
177
|
-
const circumHalf = Math.PI * r;
|
|
178
|
-
const dashLen = pct * circumHalf;
|
|
174
|
+
// ── STAT CARD ──
|
|
179
175
|
|
|
176
|
+
function StatCard({ label, value, sub, color }) {
|
|
180
177
|
return (
|
|
181
|
-
<div style={
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
fill="none" stroke="#2c313a" strokeWidth="4" strokeLinecap="round" />
|
|
186
|
-
{/* Value arc */}
|
|
187
|
-
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
|
|
188
|
-
fill="none" stroke={color} strokeWidth="4" strokeLinecap="round"
|
|
189
|
-
strokeDasharray={`${dashLen} ${circumHalf}`}
|
|
190
|
-
style={{ transition: 'stroke-dasharray 0.5s ease' }} />
|
|
191
|
-
{/* Value text */}
|
|
192
|
-
<text x={cx} y={cy - 6} textAnchor="middle" fill="#e6e6e6"
|
|
193
|
-
fontSize="14" fontWeight="700" fontFamily="JetBrains Mono, monospace">
|
|
194
|
-
{typeof value === 'number' ? Math.round(value) : value}
|
|
195
|
-
</text>
|
|
196
|
-
<text x={cx} y={cy + 4} textAnchor="middle" fill="#5c6370"
|
|
197
|
-
fontSize="7" fontFamily="JetBrains Mono, monospace">
|
|
198
|
-
{unit}
|
|
199
|
-
</text>
|
|
200
|
-
</svg>
|
|
201
|
-
<span style={{ fontSize: 7, fontWeight: 700, color: '#5c6370', textTransform: 'uppercase', letterSpacing: 1, marginTop: -2 }}>{label}</span>
|
|
178
|
+
<div style={s.statCard}>
|
|
179
|
+
<div style={{ fontSize: 22, fontWeight: 700, color: color || '#e6e6e6', lineHeight: 1 }}>{value}</div>
|
|
180
|
+
<div style={{ fontSize: 9, color: '#5c6370', marginTop: 6, textTransform: 'uppercase', letterSpacing: 1 }}>{label}</div>
|
|
181
|
+
{sub && <div style={{ fontSize: 9, color: '#3e4451', marginTop: 3 }}>{sub}</div>}
|
|
202
182
|
</div>
|
|
203
183
|
);
|
|
204
184
|
}
|
|
205
185
|
|
|
206
|
-
// ──
|
|
207
|
-
function AgentCard({ agent, total, color }) {
|
|
208
|
-
const pct = total > 0 ? (agent.tokens / total) * 100 : 0;
|
|
209
|
-
const alive = agent.status === 'running';
|
|
210
|
-
const statusColor = alive ? GREEN : agent.status === 'completed' ? ACCENT : agent.status === 'crashed' ? RED : 'var(--text-dim)';
|
|
186
|
+
// ── HORIZONTAL BAR ──
|
|
211
187
|
|
|
188
|
+
function HorizBar({ label, value, max, color, sub }) {
|
|
189
|
+
const pct = max > 0 ? (value / max) * 100 : 0;
|
|
212
190
|
return (
|
|
213
|
-
<div style={
|
|
214
|
-
<div style={
|
|
215
|
-
<span style={{
|
|
216
|
-
<span style={{ fontSize: 11,
|
|
217
|
-
<span style={{ fontSize: 9, color: 'var(--text-dim)' }}>{agent.role}</span>
|
|
218
|
-
{agent.routingMode === 'auto' && <span style={s.tagAuto}>AUTO</span>}
|
|
219
|
-
<span style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-primary)', marginLeft: 'auto' }}>{fmtNum(agent.tokens)}</span>
|
|
191
|
+
<div style={{ padding: '6px 0' }}>
|
|
192
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
|
193
|
+
<span style={{ fontSize: 11, color: '#abb2bf' }}>{label}</span>
|
|
194
|
+
<span style={{ fontSize: 11, color: '#e6e6e6', fontWeight: 600 }}>{fmtNum(value)}</span>
|
|
220
195
|
</div>
|
|
221
|
-
<div style={
|
|
222
|
-
<div style={
|
|
223
|
-
<div style={{ width: `${Math.max(pct, 0.5)}%`, height: '100%', background: color, borderRadius: 1 }} />
|
|
224
|
-
</div>
|
|
225
|
-
<span style={{ fontSize: 8, color: 'var(--text-dim)' }}>{agent.model || 'default'}</span>
|
|
226
|
-
<CtxGauge value={agent.contextUsage} />
|
|
196
|
+
<div style={{ height: 4, background: '#1e222a', borderRadius: 2 }}>
|
|
197
|
+
<div style={{ width: `${Math.max(pct, value > 0 ? 1 : 0)}%`, height: '100%', background: color, borderRadius: 2, transition: 'width 0.3s' }} />
|
|
227
198
|
</div>
|
|
199
|
+
{sub && <div style={{ fontSize: 8, color: '#3e4451', marginTop: 2 }}>{sub}</div>}
|
|
228
200
|
</div>
|
|
229
201
|
);
|
|
230
202
|
}
|
|
231
203
|
|
|
232
|
-
|
|
233
|
-
const pct = Math.round((value || 0) * 100);
|
|
234
|
-
const color = pct > 80 ? RED : pct > 60 ? AMBER : GREEN;
|
|
235
|
-
return (
|
|
236
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 3 }}>
|
|
237
|
-
<div style={{ width: 24, height: 3, background: '#2c313a', borderRadius: 1, overflow: 'hidden' }}>
|
|
238
|
-
<div style={{ width: `${pct}%`, height: '100%', background: color, borderRadius: 1 }} />
|
|
239
|
-
</div>
|
|
240
|
-
<span style={{ fontSize: 8, color: 'var(--text-dim)', minWidth: 18, textAlign: 'right' }}>{pct}%</span>
|
|
241
|
-
</div>
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ── SAVINGS BLOCK ──
|
|
246
|
-
function SavingsBlock({ savings }) {
|
|
247
|
-
const items = [
|
|
248
|
-
{ label: 'Rotation', value: savings.fromRotation, color: ACCENT },
|
|
249
|
-
{ label: 'Conflicts', value: savings.fromConflictPrevention, color: AMBER },
|
|
250
|
-
{ label: 'Cold-start', value: savings.fromColdStartSkip, color: GREEN },
|
|
251
|
-
];
|
|
252
|
-
const total = savings.total || 1;
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<div>
|
|
256
|
-
<div style={s.miniHead}>TOKEN SAVINGS</div>
|
|
257
|
-
<div style={s.stackedBar}>
|
|
258
|
-
{items.map((it, i) => it.value > 0 && (
|
|
259
|
-
<div key={i} style={{ width: `${(it.value / total) * 100}%`, height: '100%', background: it.color }} />
|
|
260
|
-
))}
|
|
261
|
-
</div>
|
|
262
|
-
{items.map((it, i) => (
|
|
263
|
-
<div key={i} style={s.savRow}>
|
|
264
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
265
|
-
<div style={{ width: 6, height: 6, background: it.color, borderRadius: 1, flexShrink: 0 }} />
|
|
266
|
-
<span>{it.label}</span>
|
|
267
|
-
</div>
|
|
268
|
-
<span style={{ fontWeight: 600 }}>{fmtNum(it.value)}</span>
|
|
269
|
-
</div>
|
|
270
|
-
))}
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
204
|
+
// ── DONUT CHART ──
|
|
274
205
|
|
|
275
|
-
|
|
276
|
-
function RoutingBlock({ routing }) {
|
|
206
|
+
function DonutChart({ routing }) {
|
|
277
207
|
const tiers = [
|
|
278
|
-
{ label: '
|
|
279
|
-
{ label: '
|
|
280
|
-
{ label: '
|
|
208
|
+
{ label: 'Heavy', color: RED, count: routing.byTier.heavy, cost: '$0.045/1k' },
|
|
209
|
+
{ label: 'Medium', color: AMBER, count: routing.byTier.medium, cost: '$0.009/1k' },
|
|
210
|
+
{ label: 'Light', color: GREEN, count: routing.byTier.light, cost: '$0.002/1k' },
|
|
281
211
|
];
|
|
282
|
-
const
|
|
212
|
+
const total = tiers.reduce((s, t) => s + t.count, 0) || 1;
|
|
213
|
+
|
|
214
|
+
const r = 50;
|
|
215
|
+
const cx = 60;
|
|
216
|
+
const cy = 60;
|
|
217
|
+
const circumference = 2 * Math.PI * r;
|
|
218
|
+
let offset = 0;
|
|
283
219
|
|
|
284
220
|
return (
|
|
285
|
-
<div>
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
221
|
+
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, minHeight: 0 }}>
|
|
222
|
+
<svg width="120" height="120" viewBox="0 0 120 120">
|
|
223
|
+
<circle cx={cx} cy={cy} r={r} fill="none" stroke="#1e222a" strokeWidth="10" />
|
|
224
|
+
{tiers.map((t) => {
|
|
225
|
+
if (t.count === 0) return null;
|
|
226
|
+
const pct = t.count / total;
|
|
227
|
+
const dashLen = pct * circumference;
|
|
228
|
+
const el = (
|
|
229
|
+
<circle
|
|
230
|
+
key={t.label}
|
|
231
|
+
cx={cx} cy={cy} r={r}
|
|
232
|
+
fill="none" stroke={t.color} strokeWidth="10"
|
|
233
|
+
strokeDasharray={`${dashLen} ${circumference - dashLen}`}
|
|
234
|
+
strokeDashoffset={-offset}
|
|
235
|
+
strokeLinecap="butt"
|
|
236
|
+
transform={`rotate(-90 ${cx} ${cy})`}
|
|
237
|
+
style={{ transition: 'stroke-dasharray 0.5s' }}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
offset += dashLen;
|
|
241
|
+
return el;
|
|
242
|
+
})}
|
|
243
|
+
<text x={cx} y={cy - 4} textAnchor="middle" fill="#e6e6e6" fontSize="16" fontWeight="700" fontFamily="JetBrains Mono, monospace">
|
|
244
|
+
{total > 1 ? total : routing.totalDecisions}
|
|
245
|
+
</text>
|
|
246
|
+
<text x={cx} y={cy + 10} textAnchor="middle" fill="#5c6370" fontSize="8" fontFamily="JetBrains Mono, monospace">
|
|
247
|
+
decisions
|
|
248
|
+
</text>
|
|
249
|
+
</svg>
|
|
250
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, width: '100%', padding: '0 8px' }}>
|
|
251
|
+
{tiers.map((t) => (
|
|
252
|
+
<div key={t.label} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 10 }}>
|
|
253
|
+
<span style={{ width: 8, height: 8, borderRadius: 2, background: t.color, flexShrink: 0 }} />
|
|
254
|
+
<span style={{ color: '#abb2bf', flex: 1 }}>{t.label}</span>
|
|
255
|
+
<span style={{ color: '#5c6370', fontSize: 9 }}>{t.cost}</span>
|
|
256
|
+
<span style={{ color: '#e6e6e6', fontWeight: 600, minWidth: 20, textAlign: 'right' }}>{t.count}</span>
|
|
292
257
|
</div>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
<span>{routing.autoRoutedCount} auto</span>
|
|
298
|
-
<span>{routing.totalDecisions} total</span>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
<div style={{ fontSize: 9, color: '#3e4451' }}>
|
|
261
|
+
{routing.autoRoutedCount} auto-routed
|
|
299
262
|
</div>
|
|
300
263
|
</div>
|
|
301
264
|
);
|
|
302
265
|
}
|
|
303
266
|
|
|
304
|
-
// ──
|
|
305
|
-
function AdaptiveBlock({ adaptive }) {
|
|
306
|
-
if (!adaptive || adaptive.length === 0) return null;
|
|
307
|
-
return (
|
|
308
|
-
<div>
|
|
309
|
-
<div style={s.miniHead}>ADAPTIVE THRESHOLDS</div>
|
|
310
|
-
{adaptive.map((p) => (
|
|
311
|
-
<div key={p.key} style={{ padding: '4px 0' }}>
|
|
312
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9, marginBottom: 2 }}>
|
|
313
|
-
<span style={{ color: 'var(--text-bright)', fontWeight: 600 }}>{p.key}</span>
|
|
314
|
-
<span style={{ color: p.converged ? GREEN : AMBER, fontSize: 8 }}>
|
|
315
|
-
{p.converged ? 'CONVERGED' : `${p.adjustments} adj`}
|
|
316
|
-
</span>
|
|
317
|
-
</div>
|
|
318
|
-
<div style={{ height: 5, background: '#2c313a', borderRadius: 2, overflow: 'hidden' }}>
|
|
319
|
-
<div style={{ width: `${Math.round(p.threshold * 100)}%`, height: '100%', background: p.converged ? GREEN : ACCENT, borderRadius: 2 }} />
|
|
320
|
-
</div>
|
|
321
|
-
</div>
|
|
322
|
-
))}
|
|
323
|
-
</div>
|
|
324
|
-
);
|
|
325
|
-
}
|
|
267
|
+
// ── TELEMETRY CHART ──
|
|
326
268
|
|
|
327
|
-
// ── TELEMETRY CHART — Full-width area chart with per-agent lines ──
|
|
328
269
|
function TelemetryChart({ tokenTimeline, agents }) {
|
|
329
270
|
const containerRef = useRef();
|
|
330
271
|
const canvasRef = useRef();
|
|
@@ -352,85 +293,75 @@ function TelemetryChart({ tokenTimeline, agents }) {
|
|
|
352
293
|
const chartH = h - padT - padB;
|
|
353
294
|
|
|
354
295
|
// Grid
|
|
355
|
-
ctx.strokeStyle = '#
|
|
296
|
+
ctx.strokeStyle = '#1e222a';
|
|
356
297
|
ctx.lineWidth = 0.5;
|
|
357
298
|
for (let i = 0; i <= 4; i++) {
|
|
358
299
|
const y = padT + (i / 4) * chartH;
|
|
359
300
|
ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(w - padR, y); ctx.stroke();
|
|
360
301
|
}
|
|
361
302
|
|
|
362
|
-
// Gather ALL agent timelines — including completed agents (historical data)
|
|
363
303
|
const agentIds = Object.keys(tokenTimeline);
|
|
364
|
-
|
|
365
304
|
if (agentIds.length === 0) {
|
|
366
305
|
ctx.fillStyle = '#3e4451';
|
|
367
|
-
ctx.font = '
|
|
306
|
+
ctx.font = '10px monospace';
|
|
368
307
|
ctx.textAlign = 'center';
|
|
369
308
|
ctx.fillText('Waiting for agent telemetry...', w / 2, h / 2);
|
|
370
309
|
return;
|
|
371
310
|
}
|
|
372
311
|
|
|
373
|
-
// Find global time range and max value
|
|
374
312
|
let minT = Infinity, maxT = 0, maxV = 0;
|
|
375
313
|
for (const id of agentIds) {
|
|
376
|
-
const
|
|
377
|
-
for (const p of pts) {
|
|
314
|
+
for (const p of tokenTimeline[id] || []) {
|
|
378
315
|
if (p.t < minT) minT = p.t;
|
|
379
316
|
if (p.t > maxT) maxT = p.t;
|
|
380
317
|
if (p.v > maxV) maxV = p.v;
|
|
381
318
|
}
|
|
382
319
|
}
|
|
383
|
-
|
|
384
320
|
if (maxT === minT) maxT = minT + 60000;
|
|
385
321
|
if (maxV === 0) maxV = 100;
|
|
386
322
|
const timeRange = maxT - minT;
|
|
387
323
|
|
|
388
|
-
// Y
|
|
324
|
+
// Y labels
|
|
389
325
|
ctx.fillStyle = '#3e4451';
|
|
390
326
|
ctx.font = '8px monospace';
|
|
391
327
|
ctx.textAlign = 'right';
|
|
392
328
|
for (let i = 0; i <= 4; i++) {
|
|
393
329
|
const val = maxV * (1 - i / 4);
|
|
394
|
-
|
|
395
|
-
ctx.fillText(fmtNum(Math.round(val)), padL - 4, y + 3);
|
|
330
|
+
ctx.fillText(fmtNum(Math.round(val)), padL - 4, padT + (i / 4) * chartH + 3);
|
|
396
331
|
}
|
|
397
332
|
|
|
398
|
-
// X
|
|
333
|
+
// X labels
|
|
399
334
|
ctx.textAlign = 'center';
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const t = minT + (i / timeLabels) * timeRange;
|
|
403
|
-
const x = padL + (i / timeLabels) * chartW;
|
|
335
|
+
for (let i = 0; i <= 4; i++) {
|
|
336
|
+
const t = minT + (i / 4) * timeRange;
|
|
404
337
|
const d = new Date(t);
|
|
405
|
-
ctx.fillText(`${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`,
|
|
338
|
+
ctx.fillText(`${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`, padL + (i / 4) * chartW, h - 4);
|
|
406
339
|
}
|
|
407
340
|
|
|
408
|
-
// Draw
|
|
341
|
+
// Draw agent areas + lines
|
|
409
342
|
agentIds.forEach((id, idx) => {
|
|
410
343
|
const pts = tokenTimeline[id] || [];
|
|
411
344
|
if (pts.length < 2) return;
|
|
412
345
|
|
|
413
346
|
const color = COLORS[idx % COLORS.length];
|
|
414
|
-
|
|
415
|
-
// Map points to canvas coords
|
|
416
347
|
const coords = pts.map((p) => ({
|
|
417
348
|
x: padL + ((p.t - minT) / timeRange) * chartW,
|
|
418
349
|
y: padT + (1 - p.v / maxV) * chartH,
|
|
419
350
|
}));
|
|
420
351
|
|
|
421
|
-
//
|
|
352
|
+
// Gradient fill
|
|
422
353
|
ctx.beginPath();
|
|
423
354
|
ctx.moveTo(coords[0].x, padT + chartH);
|
|
424
355
|
for (const c of coords) ctx.lineTo(c.x, c.y);
|
|
425
356
|
ctx.lineTo(coords[coords.length - 1].x, padT + chartH);
|
|
426
357
|
ctx.closePath();
|
|
427
358
|
const grad = ctx.createLinearGradient(0, padT, 0, padT + chartH);
|
|
428
|
-
grad.addColorStop(0, color + '
|
|
359
|
+
grad.addColorStop(0, color + '20');
|
|
429
360
|
grad.addColorStop(1, color + '03');
|
|
430
361
|
ctx.fillStyle = grad;
|
|
431
362
|
ctx.fill();
|
|
432
363
|
|
|
433
|
-
//
|
|
364
|
+
// Line
|
|
434
365
|
ctx.beginPath();
|
|
435
366
|
for (let i = 0; i < coords.length; i++) {
|
|
436
367
|
i === 0 ? ctx.moveTo(coords[i].x, coords[i].y) : ctx.lineTo(coords[i].x, coords[i].y);
|
|
@@ -439,7 +370,7 @@ function TelemetryChart({ tokenTimeline, agents }) {
|
|
|
439
370
|
ctx.lineWidth = 1;
|
|
440
371
|
ctx.stroke();
|
|
441
372
|
|
|
442
|
-
//
|
|
373
|
+
// End dot
|
|
443
374
|
const last = coords[coords.length - 1];
|
|
444
375
|
ctx.beginPath();
|
|
445
376
|
ctx.arc(last.x, last.y, 2, 0, Math.PI * 2);
|
|
@@ -463,6 +394,7 @@ function TelemetryChart({ tokenTimeline, agents }) {
|
|
|
463
394
|
}
|
|
464
395
|
|
|
465
396
|
// ── HELPERS ──
|
|
397
|
+
|
|
466
398
|
function fmtNum(n) {
|
|
467
399
|
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
|
|
468
400
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
@@ -484,13 +416,14 @@ function timeAgo(ts) {
|
|
|
484
416
|
}
|
|
485
417
|
|
|
486
418
|
// ── STYLES ──
|
|
419
|
+
|
|
487
420
|
const s = {
|
|
488
421
|
root: {
|
|
489
422
|
width: '100%', height: '100%',
|
|
490
423
|
display: 'flex', flexDirection: 'column',
|
|
491
424
|
overflow: 'hidden',
|
|
492
|
-
padding:
|
|
493
|
-
gap:
|
|
425
|
+
padding: 14,
|
|
426
|
+
gap: 12,
|
|
494
427
|
background: 'var(--bg-base)',
|
|
495
428
|
},
|
|
496
429
|
loadingRoot: {
|
|
@@ -501,120 +434,66 @@ const s = {
|
|
|
501
434
|
fontSize: 11, fontWeight: 700, color: 'var(--text-dim)',
|
|
502
435
|
letterSpacing: 3, textTransform: 'uppercase',
|
|
503
436
|
},
|
|
504
|
-
loadingBar: { width: 120, height: 2, background: '
|
|
437
|
+
loadingBar: { width: 120, height: 2, background: '#282c34', borderRadius: 1, overflow: 'hidden' },
|
|
505
438
|
loadingFill: { width: '40%', height: '100%', background: ACCENT, animation: 'pulse 1.5s infinite' },
|
|
506
439
|
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
display: '
|
|
510
|
-
flexShrink: 0,
|
|
511
|
-
},
|
|
512
|
-
heroGaugeGroup: {
|
|
513
|
-
flex: 1, display: 'flex', gap: 4,
|
|
514
|
-
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
515
|
-
borderRadius: 8, padding: '6px 4px', alignItems: 'center',
|
|
516
|
-
},
|
|
517
|
-
heroCenter: {
|
|
518
|
-
flex: 1.2, background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
519
|
-
borderRadius: 8,
|
|
520
|
-
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
|
521
|
-
padding: '8px 16px',
|
|
522
|
-
},
|
|
523
|
-
heroDollar: {
|
|
524
|
-
fontSize: 28, fontWeight: 800, color: GREEN, lineHeight: 1,
|
|
525
|
-
},
|
|
526
|
-
heroCenterLabel: {
|
|
527
|
-
fontSize: 7, fontWeight: 700, color: 'var(--text-dim)',
|
|
528
|
-
textTransform: 'uppercase', letterSpacing: 1.2, marginTop: 4,
|
|
440
|
+
// Stat cards row
|
|
441
|
+
statRow: {
|
|
442
|
+
display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12,
|
|
443
|
+
flexShrink: 0,
|
|
529
444
|
},
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
445
|
+
statCard: {
|
|
446
|
+
background: '#282c34', borderRadius: 12, padding: '16px 18px',
|
|
447
|
+
display: 'flex', flexDirection: 'column',
|
|
533
448
|
},
|
|
534
449
|
|
|
535
|
-
//
|
|
536
|
-
|
|
450
|
+
// Mid row — chart + donut
|
|
451
|
+
midRow: {
|
|
452
|
+
display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 12,
|
|
537
453
|
flex: 2, minHeight: 0,
|
|
538
|
-
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
539
|
-
borderRadius: 8,
|
|
540
|
-
padding: '8px 12px', display: 'flex', flexDirection: 'column',
|
|
541
454
|
},
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
455
|
+
chartPanel: {
|
|
456
|
+
background: '#282c34', borderRadius: 12, padding: '12px 14px',
|
|
457
|
+
display: 'flex', flexDirection: 'column', minHeight: 0,
|
|
458
|
+
},
|
|
459
|
+
donutPanel: {
|
|
460
|
+
background: '#282c34', borderRadius: 12, padding: '12px 14px',
|
|
461
|
+
display: 'flex', flexDirection: 'column', minHeight: 0,
|
|
547
462
|
},
|
|
548
|
-
chartHeadRight: { display: 'flex', alignItems: 'center', color: 'var(--text-dim)' },
|
|
549
463
|
|
|
550
|
-
// Bottom row
|
|
464
|
+
// Bottom row
|
|
551
465
|
bottomRow: {
|
|
466
|
+
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12,
|
|
552
467
|
flex: 3, minHeight: 0,
|
|
553
|
-
display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10,
|
|
554
468
|
},
|
|
555
469
|
panel: {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
borderRadius: 8,
|
|
559
|
-
padding: '8px 10px', display: 'flex', flexDirection: 'column',
|
|
470
|
+
background: '#282c34', borderRadius: 12, padding: '12px 14px',
|
|
471
|
+
display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden',
|
|
560
472
|
},
|
|
561
|
-
panelHead: {
|
|
562
|
-
fontSize: 9, fontWeight: 700, color: 'var(--text-dim)',
|
|
563
|
-
textTransform: 'uppercase', letterSpacing: 1.5,
|
|
564
|
-
paddingBottom: 6, marginBottom: 6, flexShrink: 0,
|
|
565
|
-
borderBottom: '1px solid var(--border)',
|
|
566
|
-
display: 'flex', alignItems: 'center', gap: 8,
|
|
567
|
-
},
|
|
568
|
-
scrollInner: { flex: 1, minHeight: 0, overflowY: 'auto' },
|
|
569
|
-
empty: { color: 'var(--text-dim)', fontSize: 10, textAlign: 'center', padding: 16, opacity: 0.6 },
|
|
570
473
|
|
|
571
474
|
// Shared
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
},
|
|
577
|
-
dot: { width: 6, height: 6, borderRadius: '50%', flexShrink: 0 },
|
|
578
|
-
liveBadge: {
|
|
579
|
-
fontSize: 7, fontWeight: 700, color: '#1a1d23',
|
|
580
|
-
padding: '1px 5px', borderRadius: 2, letterSpacing: 0.5, marginLeft: 'auto',
|
|
581
|
-
},
|
|
582
|
-
|
|
583
|
-
// Agent cards
|
|
584
|
-
agentCard: { padding: '5px 0', borderBottom: '1px solid var(--bg-base)' },
|
|
585
|
-
agentCardRow: { display: 'flex', alignItems: 'center', gap: 6 },
|
|
586
|
-
agentBarRow: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 3 },
|
|
587
|
-
agentBarTrack: { flex: 1, height: 2, background: '#2c313a', borderRadius: 1, overflow: 'hidden' },
|
|
588
|
-
tagAuto: {
|
|
589
|
-
fontSize: 7, fontWeight: 700, color: ACCENT,
|
|
590
|
-
border: `1px solid ${ACCENT}`, padding: '0 3px', lineHeight: '11px', letterSpacing: 0.5,
|
|
591
|
-
},
|
|
592
|
-
|
|
593
|
-
// Savings
|
|
594
|
-
stackedBar: { height: 8, background: '#2c313a', borderRadius: 2, overflow: 'hidden', display: 'flex', marginBottom: 4 },
|
|
595
|
-
savRow: {
|
|
475
|
+
panelHead: {
|
|
476
|
+
fontSize: 10, fontWeight: 600, color: '#5c6370',
|
|
477
|
+
textTransform: 'uppercase', letterSpacing: 1,
|
|
478
|
+
marginBottom: 8, flexShrink: 0,
|
|
596
479
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
597
|
-
padding: '2px 0', fontSize: 10, color: 'var(--text-primary)',
|
|
598
480
|
},
|
|
481
|
+
panelHeadRight: { display: 'flex', alignItems: 'center', color: '#3e4451' },
|
|
482
|
+
scrollInner: { flex: 1, minHeight: 0, overflowY: 'auto' },
|
|
483
|
+
empty: { color: '#3e4451', fontSize: 10, textAlign: 'center', padding: 20 },
|
|
484
|
+
divider: { height: 1, background: '#1e222a', margin: '8px 0', flexShrink: 0 },
|
|
599
485
|
|
|
600
486
|
// Journalist
|
|
601
|
-
journStats: {
|
|
602
|
-
display: 'flex', justifyContent: 'space-between',
|
|
603
|
-
fontSize: 9, color: 'var(--text-dim)', marginBottom: 6,
|
|
604
|
-
},
|
|
605
487
|
journSummary: {
|
|
606
|
-
fontSize: 10, color: '
|
|
607
|
-
padding: '
|
|
608
|
-
overflowY: 'auto', whiteSpace: 'pre-wrap', minHeight: 60, maxHeight: 200, flex: 1,
|
|
488
|
+
fontSize: 10, color: '#8b929e', lineHeight: 1.5,
|
|
489
|
+
padding: '4px 0', whiteSpace: 'pre-wrap', maxHeight: 80, overflowY: 'auto',
|
|
609
490
|
},
|
|
610
491
|
|
|
611
492
|
// Rotation
|
|
612
493
|
rotEntry: {
|
|
613
|
-
display: 'flex', alignItems: 'center', gap:
|
|
614
|
-
padding: '
|
|
494
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
495
|
+
padding: '4px 0', fontSize: 10,
|
|
615
496
|
},
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
rotSaved: { color: GREEN, fontSize: 9, fontWeight: 600 },
|
|
619
|
-
rotTime: { color: 'var(--text-dim)', fontSize: 9, flexShrink: 0 },
|
|
497
|
+
rotName: { color: '#abb2bf', flex: 1 },
|
|
498
|
+
rotTime: { color: '#3e4451', fontSize: 9 },
|
|
620
499
|
};
|