groove-dev 0.8.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/CLAUDE.md +197 -0
- package/LICENSE +40 -0
- package/README.md +115 -0
- package/docs/GUI_DESIGN_SPEC.md +402 -0
- package/favicon.png +0 -0
- package/groove-logo-short.png +0 -0
- package/groove-logo.png +0 -0
- package/package.json +70 -0
- package/packages/cli/bin/groove.js +98 -0
- package/packages/cli/package.json +15 -0
- package/packages/cli/src/client.js +25 -0
- package/packages/cli/src/commands/agents.js +38 -0
- package/packages/cli/src/commands/approve.js +50 -0
- package/packages/cli/src/commands/config.js +35 -0
- package/packages/cli/src/commands/kill.js +15 -0
- package/packages/cli/src/commands/nuke.js +19 -0
- package/packages/cli/src/commands/providers.js +40 -0
- package/packages/cli/src/commands/rotate.js +16 -0
- package/packages/cli/src/commands/spawn.js +91 -0
- package/packages/cli/src/commands/start.js +31 -0
- package/packages/cli/src/commands/status.js +38 -0
- package/packages/cli/src/commands/stop.js +15 -0
- package/packages/cli/src/commands/team.js +77 -0
- package/packages/daemon/package.json +18 -0
- package/packages/daemon/src/adaptive.js +237 -0
- package/packages/daemon/src/api.js +533 -0
- package/packages/daemon/src/classifier.js +126 -0
- package/packages/daemon/src/credentials.js +121 -0
- package/packages/daemon/src/firstrun.js +93 -0
- package/packages/daemon/src/index.js +208 -0
- package/packages/daemon/src/introducer.js +238 -0
- package/packages/daemon/src/journalist.js +600 -0
- package/packages/daemon/src/lockmanager.js +58 -0
- package/packages/daemon/src/pm.js +108 -0
- package/packages/daemon/src/process.js +361 -0
- package/packages/daemon/src/providers/aider.js +72 -0
- package/packages/daemon/src/providers/base.js +38 -0
- package/packages/daemon/src/providers/claude-code.js +167 -0
- package/packages/daemon/src/providers/codex.js +68 -0
- package/packages/daemon/src/providers/gemini.js +62 -0
- package/packages/daemon/src/providers/index.js +38 -0
- package/packages/daemon/src/providers/ollama.js +94 -0
- package/packages/daemon/src/registry.js +89 -0
- package/packages/daemon/src/rotator.js +185 -0
- package/packages/daemon/src/router.js +132 -0
- package/packages/daemon/src/state.js +34 -0
- package/packages/daemon/src/supervisor.js +178 -0
- package/packages/daemon/src/teams.js +203 -0
- package/packages/daemon/src/terminal/base.js +27 -0
- package/packages/daemon/src/terminal/generic.js +27 -0
- package/packages/daemon/src/terminal/tmux.js +64 -0
- package/packages/daemon/src/tokentracker.js +124 -0
- package/packages/daemon/src/validate.js +122 -0
- package/packages/daemon/templates/api-builder.json +18 -0
- package/packages/daemon/templates/fullstack.json +18 -0
- package/packages/daemon/templates/monorepo.json +24 -0
- package/packages/gui/dist/assets/index-BO95Rm1F.js +73 -0
- package/packages/gui/dist/assets/index-CPzm9ZE9.css +1 -0
- package/packages/gui/dist/favicon.png +0 -0
- package/packages/gui/dist/groove-logo-short.png +0 -0
- package/packages/gui/dist/groove-logo.png +0 -0
- package/packages/gui/dist/index.html +13 -0
- package/packages/gui/index.html +12 -0
- package/packages/gui/package.json +22 -0
- package/packages/gui/public/favicon.png +0 -0
- package/packages/gui/public/groove-logo-short.png +0 -0
- package/packages/gui/public/groove-logo.png +0 -0
- package/packages/gui/src/App.jsx +215 -0
- package/packages/gui/src/components/AgentActions.jsx +347 -0
- package/packages/gui/src/components/AgentChat.jsx +479 -0
- package/packages/gui/src/components/AgentNode.jsx +117 -0
- package/packages/gui/src/components/AgentPanel.jsx +115 -0
- package/packages/gui/src/components/AgentStats.jsx +333 -0
- package/packages/gui/src/components/ApprovalQueue.jsx +156 -0
- package/packages/gui/src/components/EmptyState.jsx +100 -0
- package/packages/gui/src/components/SpawnPanel.jsx +515 -0
- package/packages/gui/src/components/TeamSelector.jsx +162 -0
- package/packages/gui/src/main.jsx +9 -0
- package/packages/gui/src/stores/groove.js +247 -0
- package/packages/gui/src/theme.css +67 -0
- package/packages/gui/src/views/AgentTree.jsx +148 -0
- package/packages/gui/src/views/CommandCenter.jsx +620 -0
- package/packages/gui/src/views/JournalistFeed.jsx +149 -0
- package/packages/gui/vite.config.js +19 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
// GROOVE GUI — Command Center Dashboard
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
5
|
+
import { useGrooveStore } from '../stores/groove';
|
|
6
|
+
|
|
7
|
+
const GREEN = '#4ae168';
|
|
8
|
+
const ACCENT = '#33afbc';
|
|
9
|
+
const AMBER = '#e5c07b';
|
|
10
|
+
const RED = '#e06c75';
|
|
11
|
+
const PURPLE = '#c678dd';
|
|
12
|
+
const BLUE = '#61afef';
|
|
13
|
+
const COLORS = [ACCENT, AMBER, GREEN, PURPLE, RED, BLUE, '#d19a66', '#56b6c2'];
|
|
14
|
+
const COST_PER_1K = { heavy: 0.045, medium: 0.009, light: 0.0024 };
|
|
15
|
+
|
|
16
|
+
export default function CommandCenter() {
|
|
17
|
+
const [data, setData] = useState(null);
|
|
18
|
+
const agents = useGrooveStore((s) => s.agents);
|
|
19
|
+
const dashTelemetry = useGrooveStore((s) => s.dashTelemetry);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
fetchDashboard();
|
|
23
|
+
const interval = setInterval(fetchDashboard, 4000);
|
|
24
|
+
return () => clearInterval(interval);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
async function fetchDashboard() {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch('/api/dashboard');
|
|
30
|
+
const d = await res.json();
|
|
31
|
+
setData(d);
|
|
32
|
+
|
|
33
|
+
// Build telemetry in the Zustand store so it persists across tab switches
|
|
34
|
+
useGrooveStore.setState((s) => {
|
|
35
|
+
const telem = { ...s.dashTelemetry };
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
for (const agent of d.agents.breakdown) {
|
|
38
|
+
if (!telem[agent.id]) telem[agent.id] = [];
|
|
39
|
+
const arr = telem[agent.id];
|
|
40
|
+
const last = arr[arr.length - 1];
|
|
41
|
+
if (!last || agent.tokens !== last.v || now - last.t > 10000) {
|
|
42
|
+
arr.push({ t: now, v: agent.tokens || 0, name: agent.name });
|
|
43
|
+
if (arr.length > 200) telem[agent.id] = arr.slice(-200);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { dashTelemetry: telem };
|
|
47
|
+
});
|
|
48
|
+
} catch { /* ignore */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!data) {
|
|
52
|
+
return (
|
|
53
|
+
<div style={s.loadingRoot}>
|
|
54
|
+
<div style={s.loadingText}>COMMAND CENTER</div>
|
|
55
|
+
<div style={s.loadingBar}><div style={s.loadingFill} /></div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { tokens, routing, rotation, adaptive, journalist, uptime } = data;
|
|
61
|
+
const agentBreakdown = data.agents.breakdown;
|
|
62
|
+
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
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div style={s.root}>
|
|
70
|
+
|
|
71
|
+
{/* ── HERO ROW — Gauges + Money Shot ── */}
|
|
72
|
+
<div style={s.heroRow}>
|
|
73
|
+
<div style={s.heroGaugeGroup}>
|
|
74
|
+
<GaugeChart value={tokens.savings.percentage || 0} max={100} label="EFFICIENCY" unit="%" color={GREEN} />
|
|
75
|
+
<GaugeChart value={maxCtx} max={100} label="PEAK CONTEXT" unit="%" color={maxCtx > 80 ? RED : maxCtx > 60 ? AMBER : ACCENT} />
|
|
76
|
+
</div>
|
|
77
|
+
<div style={s.heroCenter}>
|
|
78
|
+
<div style={s.heroDollar}>{estDollarSaved > 0 ? `$${estDollarSaved.toFixed(2)}` : '$0.00'}</div>
|
|
79
|
+
<div style={s.heroCenterLabel}>ESTIMATED SAVINGS</div>
|
|
80
|
+
<div style={s.heroSubStats}>
|
|
81
|
+
<span>{fmtNum(tokens.totalTokens)} used</span>
|
|
82
|
+
<span>{fmtNum(tokens.savings.total)} saved</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div style={s.heroGaugeGroup}>
|
|
86
|
+
<GaugeChart value={data.agents.running} max={Math.max(data.agents.total, 1)} label="AGENTS" unit={`/${data.agents.total}`} color={ACCENT} />
|
|
87
|
+
<GaugeChart value={rotation.totalRotations} max={Math.max(rotation.totalRotations, 10)} label="ROTATIONS" unit="" color={PURPLE} />
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* ── MAIN CHART — Full-width live telemetry ── */}
|
|
92
|
+
<div style={s.chartPanel}>
|
|
93
|
+
<div style={s.chartHead}>
|
|
94
|
+
<span>LIVE TELEMETRY</span>
|
|
95
|
+
<span style={s.chartHeadRight}>
|
|
96
|
+
{data.agents.breakdown.filter((a) => a.tokens > 0).map((a, i) => (
|
|
97
|
+
<span key={a.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 3, marginLeft: 10 }}>
|
|
98
|
+
<span style={{ width: 6, height: 6, borderRadius: '50%', background: COLORS[i % COLORS.length], display: 'inline-block' }} />
|
|
99
|
+
<span style={{ fontSize: 9 }}>{a.name} {fmtNum(a.tokens)}</span>
|
|
100
|
+
</span>
|
|
101
|
+
))}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
<TelemetryChart tokenTimeline={dashTelemetry} agents={agents} />
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* ── BOTTOM ROW — Three panels ── */}
|
|
108
|
+
<div style={s.bottomRow}>
|
|
109
|
+
|
|
110
|
+
{/* AGENT FLEET */}
|
|
111
|
+
<div style={s.panel}>
|
|
112
|
+
<div style={s.panelHead}>AGENT FLEET</div>
|
|
113
|
+
<div style={s.scrollInner}>
|
|
114
|
+
{agentBreakdown.length === 0 ? (
|
|
115
|
+
<div style={s.empty}>No agents spawned</div>
|
|
116
|
+
) : agentBreakdown.map((a, i) => (
|
|
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} />
|
|
129
|
+
<div style={s.divider} />
|
|
130
|
+
<AdaptiveBlock adaptive={adaptive} />
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* JOURNALIST + ROTATION */}
|
|
135
|
+
<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>
|
|
142
|
+
<div style={s.scrollInner}>
|
|
143
|
+
<div style={s.journStats}>
|
|
144
|
+
<span>{journalist.cycleCount || 0} cycles</span>
|
|
145
|
+
<span>{journalist.intervalMs ? `${journalist.intervalMs / 1000}s interval` : '120s interval'}</span>
|
|
146
|
+
</div>
|
|
147
|
+
{journalist.lastSummary ? (
|
|
148
|
+
<div style={s.journSummary}>{journalist.lastSummary}</div>
|
|
149
|
+
) : (
|
|
150
|
+
<div style={s.journSummary}>Waiting for first synthesis cycle...</div>
|
|
151
|
+
)}
|
|
152
|
+
<div style={{ ...s.divider, margin: '8px 0' }} />
|
|
153
|
+
<div style={s.miniHead}>ROTATION HISTORY</div>
|
|
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>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── GAUGE CHART — Semicircle arc gauge ──
|
|
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;
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flex: 1 }}>
|
|
182
|
+
<svg width="80" height="48" viewBox="0 0 80 48">
|
|
183
|
+
{/* Track */}
|
|
184
|
+
<path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`}
|
|
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>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── AGENT CARD ──
|
|
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)';
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div style={s.agentCard}>
|
|
214
|
+
<div style={s.agentCardRow}>
|
|
215
|
+
<span style={{ ...s.dot, background: statusColor, ...(alive ? { animation: 'pulse 2s infinite' } : {}) }} />
|
|
216
|
+
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-bright)' }}>{agent.name}</span>
|
|
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>
|
|
220
|
+
</div>
|
|
221
|
+
<div style={s.agentBarRow}>
|
|
222
|
+
<div style={s.agentBarTrack}>
|
|
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} />
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function CtxGauge({ value }) {
|
|
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
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── ROUTING BLOCK ──
|
|
276
|
+
function RoutingBlock({ routing }) {
|
|
277
|
+
const tiers = [
|
|
278
|
+
{ label: 'HEAVY', cost: '$0.045', color: RED, count: routing.byTier.heavy },
|
|
279
|
+
{ label: 'MEDIUM', cost: '$0.009', color: AMBER, count: routing.byTier.medium },
|
|
280
|
+
{ label: 'LIGHT', cost: '$0.002', color: GREEN, count: routing.byTier.light },
|
|
281
|
+
];
|
|
282
|
+
const max = Math.max(...tiers.map((t) => t.count), 1);
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div>
|
|
286
|
+
<div style={s.miniHead}>MODEL ROUTING</div>
|
|
287
|
+
{tiers.map((t) => (
|
|
288
|
+
<div key={t.label} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '3px 0' }}>
|
|
289
|
+
<span style={{ fontSize: 8, fontWeight: 700, color: t.color, minWidth: 42 }}>{t.label}</span>
|
|
290
|
+
<div style={{ flex: 1, height: 4, background: '#2c313a', borderRadius: 1, overflow: 'hidden' }}>
|
|
291
|
+
<div style={{ width: `${Math.max((t.count / max) * 100, t.count > 0 ? 3 : 0)}%`, height: '100%', background: t.color, borderRadius: 1 }} />
|
|
292
|
+
</div>
|
|
293
|
+
<span style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-bright)', minWidth: 16, textAlign: 'right' }}>{t.count}</span>
|
|
294
|
+
</div>
|
|
295
|
+
))}
|
|
296
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 8, color: 'var(--text-dim)', marginTop: 4 }}>
|
|
297
|
+
<span>{routing.autoRoutedCount} auto</span>
|
|
298
|
+
<span>{routing.totalDecisions} total</span>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── ADAPTIVE BLOCK ──
|
|
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
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── TELEMETRY CHART — Full-width area chart with per-agent lines ──
|
|
328
|
+
function TelemetryChart({ tokenTimeline, agents }) {
|
|
329
|
+
const containerRef = useRef();
|
|
330
|
+
const canvasRef = useRef();
|
|
331
|
+
|
|
332
|
+
const draw = useCallback(() => {
|
|
333
|
+
const container = containerRef.current;
|
|
334
|
+
const canvas = canvasRef.current;
|
|
335
|
+
if (!container || !canvas) return;
|
|
336
|
+
|
|
337
|
+
const dpr = window.devicePixelRatio || 1;
|
|
338
|
+
const w = container.clientWidth;
|
|
339
|
+
const h = container.clientHeight;
|
|
340
|
+
if (w === 0 || h === 0) return;
|
|
341
|
+
|
|
342
|
+
canvas.width = w * dpr;
|
|
343
|
+
canvas.height = h * dpr;
|
|
344
|
+
canvas.style.width = w + 'px';
|
|
345
|
+
canvas.style.height = h + 'px';
|
|
346
|
+
const ctx = canvas.getContext('2d');
|
|
347
|
+
ctx.scale(dpr, dpr);
|
|
348
|
+
ctx.clearRect(0, 0, w, h);
|
|
349
|
+
|
|
350
|
+
const padL = 40, padR = 10, padT = 10, padB = 20;
|
|
351
|
+
const chartW = w - padL - padR;
|
|
352
|
+
const chartH = h - padT - padB;
|
|
353
|
+
|
|
354
|
+
// Grid
|
|
355
|
+
ctx.strokeStyle = '#2c313a';
|
|
356
|
+
ctx.lineWidth = 0.5;
|
|
357
|
+
for (let i = 0; i <= 4; i++) {
|
|
358
|
+
const y = padT + (i / 4) * chartH;
|
|
359
|
+
ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(w - padR, y); ctx.stroke();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Gather ALL agent timelines — including completed agents (historical data)
|
|
363
|
+
const agentIds = Object.keys(tokenTimeline);
|
|
364
|
+
|
|
365
|
+
if (agentIds.length === 0) {
|
|
366
|
+
ctx.fillStyle = '#3e4451';
|
|
367
|
+
ctx.font = '11px monospace';
|
|
368
|
+
ctx.textAlign = 'center';
|
|
369
|
+
ctx.fillText('Waiting for agent telemetry...', w / 2, h / 2);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Find global time range and max value
|
|
374
|
+
let minT = Infinity, maxT = 0, maxV = 0;
|
|
375
|
+
for (const id of agentIds) {
|
|
376
|
+
const pts = tokenTimeline[id] || [];
|
|
377
|
+
for (const p of pts) {
|
|
378
|
+
if (p.t < minT) minT = p.t;
|
|
379
|
+
if (p.t > maxT) maxT = p.t;
|
|
380
|
+
if (p.v > maxV) maxV = p.v;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (maxT === minT) maxT = minT + 60000;
|
|
385
|
+
if (maxV === 0) maxV = 100;
|
|
386
|
+
const timeRange = maxT - minT;
|
|
387
|
+
|
|
388
|
+
// Y-axis labels
|
|
389
|
+
ctx.fillStyle = '#3e4451';
|
|
390
|
+
ctx.font = '8px monospace';
|
|
391
|
+
ctx.textAlign = 'right';
|
|
392
|
+
for (let i = 0; i <= 4; i++) {
|
|
393
|
+
const val = maxV * (1 - i / 4);
|
|
394
|
+
const y = padT + (i / 4) * chartH;
|
|
395
|
+
ctx.fillText(fmtNum(Math.round(val)), padL - 4, y + 3);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// X-axis time labels
|
|
399
|
+
ctx.textAlign = 'center';
|
|
400
|
+
const timeLabels = 5;
|
|
401
|
+
for (let i = 0; i <= timeLabels; i++) {
|
|
402
|
+
const t = minT + (i / timeLabels) * timeRange;
|
|
403
|
+
const x = padL + (i / timeLabels) * chartW;
|
|
404
|
+
const d = new Date(t);
|
|
405
|
+
ctx.fillText(`${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`, x, h - 4);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Draw each agent — thin flat lines, subtle fill, no neon/glow
|
|
409
|
+
agentIds.forEach((id, idx) => {
|
|
410
|
+
const pts = tokenTimeline[id] || [];
|
|
411
|
+
if (pts.length < 2) return;
|
|
412
|
+
|
|
413
|
+
const color = COLORS[idx % COLORS.length];
|
|
414
|
+
|
|
415
|
+
// Map points to canvas coords
|
|
416
|
+
const coords = pts.map((p) => ({
|
|
417
|
+
x: padL + ((p.t - minT) / timeRange) * chartW,
|
|
418
|
+
y: padT + (1 - p.v / maxV) * chartH,
|
|
419
|
+
}));
|
|
420
|
+
|
|
421
|
+
// Subtle fill area
|
|
422
|
+
ctx.beginPath();
|
|
423
|
+
ctx.moveTo(coords[0].x, padT + chartH);
|
|
424
|
+
for (const c of coords) ctx.lineTo(c.x, c.y);
|
|
425
|
+
ctx.lineTo(coords[coords.length - 1].x, padT + chartH);
|
|
426
|
+
ctx.closePath();
|
|
427
|
+
const grad = ctx.createLinearGradient(0, padT, 0, padT + chartH);
|
|
428
|
+
grad.addColorStop(0, color + '18');
|
|
429
|
+
grad.addColorStop(1, color + '03');
|
|
430
|
+
ctx.fillStyle = grad;
|
|
431
|
+
ctx.fill();
|
|
432
|
+
|
|
433
|
+
// Thin flat line — 1px, no shadow/glow
|
|
434
|
+
ctx.beginPath();
|
|
435
|
+
for (let i = 0; i < coords.length; i++) {
|
|
436
|
+
i === 0 ? ctx.moveTo(coords[i].x, coords[i].y) : ctx.lineTo(coords[i].x, coords[i].y);
|
|
437
|
+
}
|
|
438
|
+
ctx.strokeStyle = color;
|
|
439
|
+
ctx.lineWidth = 1;
|
|
440
|
+
ctx.stroke();
|
|
441
|
+
|
|
442
|
+
// Small end marker — flat, no glow
|
|
443
|
+
const last = coords[coords.length - 1];
|
|
444
|
+
ctx.beginPath();
|
|
445
|
+
ctx.arc(last.x, last.y, 2, 0, Math.PI * 2);
|
|
446
|
+
ctx.fillStyle = color;
|
|
447
|
+
ctx.fill();
|
|
448
|
+
});
|
|
449
|
+
}, [tokenTimeline, agents]);
|
|
450
|
+
|
|
451
|
+
useEffect(() => { draw(); }, [draw]);
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
const obs = new ResizeObserver(draw);
|
|
454
|
+
if (containerRef.current) obs.observe(containerRef.current);
|
|
455
|
+
return () => obs.disconnect();
|
|
456
|
+
}, [draw]);
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<div ref={containerRef} style={{ flex: 1, minHeight: 0, position: 'relative' }}>
|
|
460
|
+
<canvas ref={canvasRef} style={{ display: 'block', position: 'absolute', top: 0, left: 0 }} />
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── HELPERS ──
|
|
466
|
+
function fmtNum(n) {
|
|
467
|
+
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
|
|
468
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
469
|
+
return String(n || 0);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function fmtUptime(sec) {
|
|
473
|
+
const h = Math.floor(sec / 3600);
|
|
474
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
475
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
476
|
+
return `${m}m ${Math.floor(sec % 60)}s`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function timeAgo(ts) {
|
|
480
|
+
const m = Math.floor((Date.now() - new Date(ts).getTime()) / 60000);
|
|
481
|
+
if (m >= 60) return `${Math.floor(m / 60)}h ago`;
|
|
482
|
+
if (m > 0) return `${m}m ago`;
|
|
483
|
+
return 'now';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── STYLES ──
|
|
487
|
+
const s = {
|
|
488
|
+
root: {
|
|
489
|
+
width: '100%', height: '100%',
|
|
490
|
+
display: 'flex', flexDirection: 'column',
|
|
491
|
+
overflow: 'hidden',
|
|
492
|
+
padding: 12,
|
|
493
|
+
gap: 10,
|
|
494
|
+
background: 'var(--bg-base)',
|
|
495
|
+
},
|
|
496
|
+
loadingRoot: {
|
|
497
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
|
498
|
+
height: '100%', gap: 12,
|
|
499
|
+
},
|
|
500
|
+
loadingText: {
|
|
501
|
+
fontSize: 11, fontWeight: 700, color: 'var(--text-dim)',
|
|
502
|
+
letterSpacing: 3, textTransform: 'uppercase',
|
|
503
|
+
},
|
|
504
|
+
loadingBar: { width: 120, height: 2, background: 'var(--bg-surface)', borderRadius: 1, overflow: 'hidden' },
|
|
505
|
+
loadingFill: { width: '40%', height: '100%', background: ACCENT, animation: 'pulse 1.5s infinite' },
|
|
506
|
+
|
|
507
|
+
// Hero row
|
|
508
|
+
heroRow: {
|
|
509
|
+
display: 'flex', alignItems: 'stretch', gap: 10,
|
|
510
|
+
flexShrink: 0, height: 90,
|
|
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,
|
|
529
|
+
},
|
|
530
|
+
heroSubStats: {
|
|
531
|
+
display: 'flex', gap: 12, marginTop: 4,
|
|
532
|
+
fontSize: 9, color: '#5c6370',
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
// Main chart
|
|
536
|
+
chartPanel: {
|
|
537
|
+
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
|
+
},
|
|
542
|
+
chartHead: {
|
|
543
|
+
fontSize: 9, fontWeight: 700, color: 'var(--text-dim)',
|
|
544
|
+
textTransform: 'uppercase', letterSpacing: 1.5,
|
|
545
|
+
paddingBottom: 6, flexShrink: 0,
|
|
546
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
547
|
+
},
|
|
548
|
+
chartHeadRight: { display: 'flex', alignItems: 'center', color: 'var(--text-dim)' },
|
|
549
|
+
|
|
550
|
+
// Bottom row — three panels
|
|
551
|
+
bottomRow: {
|
|
552
|
+
flex: 3, minHeight: 0,
|
|
553
|
+
display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10,
|
|
554
|
+
},
|
|
555
|
+
panel: {
|
|
556
|
+
minHeight: 0, overflow: 'hidden',
|
|
557
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
558
|
+
borderRadius: 8,
|
|
559
|
+
padding: '8px 10px', display: 'flex', flexDirection: 'column',
|
|
560
|
+
},
|
|
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
|
+
|
|
571
|
+
// Shared
|
|
572
|
+
divider: { height: 1, background: 'var(--border)', margin: '6px 0', flexShrink: 0 },
|
|
573
|
+
miniHead: {
|
|
574
|
+
fontSize: 8, fontWeight: 700, color: 'var(--text-dim)',
|
|
575
|
+
textTransform: 'uppercase', letterSpacing: 1, marginBottom: 4,
|
|
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: {
|
|
596
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
597
|
+
padding: '2px 0', fontSize: 10, color: 'var(--text-primary)',
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
// Journalist
|
|
601
|
+
journStats: {
|
|
602
|
+
display: 'flex', justifyContent: 'space-between',
|
|
603
|
+
fontSize: 9, color: 'var(--text-dim)', marginBottom: 6,
|
|
604
|
+
},
|
|
605
|
+
journSummary: {
|
|
606
|
+
fontSize: 10, color: 'var(--text-primary)', lineHeight: 1.6,
|
|
607
|
+
padding: '6px 8px', background: 'var(--bg-base)', border: '1px solid var(--border)',
|
|
608
|
+
overflowY: 'auto', whiteSpace: 'pre-wrap', minHeight: 60, maxHeight: 200, flex: 1,
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
// Rotation
|
|
612
|
+
rotEntry: {
|
|
613
|
+
display: 'flex', alignItems: 'center', gap: 6,
|
|
614
|
+
padding: '3px 0', fontSize: 10,
|
|
615
|
+
},
|
|
616
|
+
rotDot: { width: 5, height: 5, borderRadius: '50%', background: ACCENT, flexShrink: 0 },
|
|
617
|
+
rotName: { color: 'var(--text-bright)', fontWeight: 600, flex: 1 },
|
|
618
|
+
rotSaved: { color: GREEN, fontSize: 9, fontWeight: 600 },
|
|
619
|
+
rotTime: { color: 'var(--text-dim)', fontSize: 9, flexShrink: 0 },
|
|
620
|
+
};
|