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
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Groove GUI</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DJbDjzF2.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C0naHS9e.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { memo } from 'react';
|
|
2
3
|
import { RotateCw, Zap, AlertTriangle, CheckCircle, UserPlus } from 'lucide-react';
|
|
3
4
|
import { timeAgo } from '../../lib/format';
|
|
4
|
-
import { cn } from '../../lib/cn';
|
|
5
5
|
|
|
6
6
|
const ICONS = {
|
|
7
7
|
spawn: UserPlus,
|
|
@@ -19,10 +19,10 @@ const ICON_COLORS = {
|
|
|
19
19
|
default: 'text-text-3',
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const ActivityFeed = memo(function ActivityFeed({ events = [] }) {
|
|
23
23
|
if (!events.length) {
|
|
24
24
|
return (
|
|
25
|
-
<div className="text-xs text-text-
|
|
25
|
+
<div className="text-xs text-text-3 font-mono py-2.5 text-center">
|
|
26
26
|
No recent activity
|
|
27
27
|
</div>
|
|
28
28
|
);
|
|
@@ -30,17 +30,19 @@ export function ActivityFeed({ events = [] }) {
|
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
32
|
<div className="flex items-center gap-3 overflow-x-auto py-2 px-3">
|
|
33
|
-
{events.slice(-
|
|
33
|
+
{events.slice(-15).reverse().map((event, i) => {
|
|
34
34
|
const Icon = ICONS[event.type] || ICONS.default;
|
|
35
35
|
const color = ICON_COLORS[event.type] || ICON_COLORS.default;
|
|
36
36
|
return (
|
|
37
|
-
<div key={i} className="flex items-center gap-1.5 flex-shrink-0
|
|
38
|
-
<Icon size={
|
|
39
|
-
<span className="text-text-2 whitespace-nowrap">{event.text}</span>
|
|
40
|
-
<span className="text-text-4">{timeAgo(event.timestamp)}</span>
|
|
37
|
+
<div key={i} className="flex items-center gap-1.5 flex-shrink-0">
|
|
38
|
+
<Icon size={11} className={color} />
|
|
39
|
+
<span className="text-xs font-sans text-text-2 whitespace-nowrap">{event.text}</span>
|
|
40
|
+
<span className="text-2xs font-mono text-text-4">{timeAgo(event.timestamp || event.t)}</span>
|
|
41
41
|
</div>
|
|
42
42
|
);
|
|
43
43
|
})}
|
|
44
44
|
</div>
|
|
45
45
|
);
|
|
46
|
-
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export { ActivityFeed };
|
|
@@ -0,0 +1,102 @@
|
|
|
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 } from '../../lib/format';
|
|
5
|
+
|
|
6
|
+
const CacheRing = memo(function CacheRing({ cacheRead = 0, cacheCreation = 0, totalInput = 0, size = 140 }) {
|
|
7
|
+
const canvasRef = useRef(null);
|
|
8
|
+
const total = cacheRead + cacheCreation + totalInput;
|
|
9
|
+
const hitRate = total > 0 ? (cacheRead / total) * 100 : 0;
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const canvas = canvasRef.current;
|
|
13
|
+
if (!canvas) return;
|
|
14
|
+
const dpr = window.devicePixelRatio || 1;
|
|
15
|
+
canvas.width = size * dpr;
|
|
16
|
+
canvas.height = size * dpr;
|
|
17
|
+
const ctx = canvas.getContext('2d');
|
|
18
|
+
ctx.scale(dpr, dpr);
|
|
19
|
+
ctx.clearRect(0, 0, size, size);
|
|
20
|
+
|
|
21
|
+
const cx = size / 2;
|
|
22
|
+
const cy = size / 2;
|
|
23
|
+
const radius = (size - 12) / 2;
|
|
24
|
+
const strokeWidth = 5;
|
|
25
|
+
|
|
26
|
+
const startAngle = (135 * Math.PI) / 180;
|
|
27
|
+
const endAngle = (405 * Math.PI) / 180;
|
|
28
|
+
const sweep = endAngle - startAngle;
|
|
29
|
+
|
|
30
|
+
// Background track
|
|
31
|
+
ctx.beginPath();
|
|
32
|
+
ctx.arc(cx, cy, radius, startAngle, endAngle);
|
|
33
|
+
ctx.strokeStyle = HEX.surface4;
|
|
34
|
+
ctx.lineWidth = strokeWidth;
|
|
35
|
+
ctx.lineCap = 'round';
|
|
36
|
+
ctx.stroke();
|
|
37
|
+
|
|
38
|
+
if (total > 0) {
|
|
39
|
+
const readPct = cacheRead / total;
|
|
40
|
+
const createPct = cacheCreation / total;
|
|
41
|
+
|
|
42
|
+
if (readPct > 0) {
|
|
43
|
+
const segEnd = startAngle + sweep * readPct;
|
|
44
|
+
ctx.beginPath();
|
|
45
|
+
ctx.arc(cx, cy, radius, startAngle, segEnd);
|
|
46
|
+
ctx.strokeStyle = HEX.accent;
|
|
47
|
+
ctx.lineWidth = strokeWidth;
|
|
48
|
+
ctx.lineCap = 'round';
|
|
49
|
+
ctx.stroke();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (createPct > 0) {
|
|
53
|
+
const segStart = startAngle + sweep * readPct;
|
|
54
|
+
const segEnd = segStart + sweep * createPct;
|
|
55
|
+
ctx.beginPath();
|
|
56
|
+
ctx.arc(cx, cy, radius, segStart, segEnd);
|
|
57
|
+
ctx.strokeStyle = HEX.purple;
|
|
58
|
+
ctx.lineWidth = strokeWidth;
|
|
59
|
+
ctx.lineCap = 'butt';
|
|
60
|
+
ctx.stroke();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Center text
|
|
65
|
+
ctx.textAlign = 'center';
|
|
66
|
+
ctx.textBaseline = 'middle';
|
|
67
|
+
ctx.font = `600 ${size * 0.2}px 'JetBrains Mono Variable', monospace`;
|
|
68
|
+
ctx.fillStyle = HEX.text0;
|
|
69
|
+
ctx.fillText(`${Math.round(hitRate)}%`, cx, cy - 3);
|
|
70
|
+
|
|
71
|
+
ctx.font = `500 ${size * 0.08}px 'JetBrains Mono Variable', monospace`;
|
|
72
|
+
ctx.fillStyle = HEX.text3;
|
|
73
|
+
ctx.fillText('CACHE', cx, cy + size * 0.13);
|
|
74
|
+
}, [cacheRead, cacheCreation, totalInput, size, total, hitRate]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex flex-col items-center justify-center h-full px-3 py-3">
|
|
78
|
+
<canvas
|
|
79
|
+
ref={canvasRef}
|
|
80
|
+
className="flex-shrink-0"
|
|
81
|
+
style={{ width: size, height: size }}
|
|
82
|
+
/>
|
|
83
|
+
<div className="w-full mt-3 space-y-1.5 max-w-[160px]">
|
|
84
|
+
<StatRow color={HEX.accent} label="Read" value={fmtNum(cacheRead)} />
|
|
85
|
+
<StatRow color={HEX.purple} label="Create" value={fmtNum(cacheCreation)} />
|
|
86
|
+
<StatRow color={HEX.surface5} label="Miss" value={fmtNum(Math.max(totalInput - cacheRead - cacheCreation, 0))} />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function StatRow({ color, label, value }) {
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex items-center gap-2 text-xs font-mono">
|
|
95
|
+
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: color }} />
|
|
96
|
+
<span className="text-text-3 uppercase tracking-wider flex-1">{label}</span>
|
|
97
|
+
<span className="text-text-1 tabular-nums">{value}</span>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { CacheRing };
|
|
@@ -1,64 +1,131 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { StatusDot } from '../ui/status-dot';
|
|
5
|
-
import { fmtNum, fmtPct } from '../../lib/format';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { fmtNum, fmtDollar } from '../../lib/format';
|
|
6
4
|
import { cn } from '../../lib/cn';
|
|
5
|
+
import { statusColor } from '../../lib/status';
|
|
7
6
|
import { ScrollArea } from '../ui/scroll-area';
|
|
8
7
|
|
|
9
|
-
const
|
|
10
|
-
running: 'success', starting: 'warning', stopped: 'default',
|
|
11
|
-
crashed: 'danger', completed: 'accent', killed: 'default',
|
|
12
|
-
};
|
|
8
|
+
const COST_SOURCE_LABEL = { actual: 'ACT', estimated: 'EST', local: 'LOC' };
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const AgentRow = memo(function AgentRow({ agent, isRotating }) {
|
|
11
|
+
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
12
|
+
const contextPct = Math.round((agent.contextUsage || 0) * 100);
|
|
13
|
+
const sColor = isRotating ? '#c678dd' : statusColor(agent.status);
|
|
14
|
+
const quality = agent.quality;
|
|
15
|
+
const successRate = quality?.toolSuccessRate != null ? Math.round(quality.toolSuccessRate * 100) : null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex items-center gap-2 px-3 py-1.5 hover:bg-surface-3 transition-colors">
|
|
19
|
+
{/* Status square */}
|
|
20
|
+
<span className="relative flex-shrink-0 w-[6px] h-[6px]">
|
|
21
|
+
<span className="absolute inset-0 rounded-sm" style={{ background: sColor }} />
|
|
22
|
+
{isAlive && (
|
|
23
|
+
<span
|
|
24
|
+
className="absolute inset-[-2px] rounded-sm"
|
|
25
|
+
style={{ background: sColor, opacity: 0.15, animation: 'node-pulse-bar 2s ease-in-out infinite' }}
|
|
26
|
+
/>
|
|
27
|
+
)}
|
|
28
|
+
</span>
|
|
29
|
+
|
|
30
|
+
{/* Name + role/model */}
|
|
31
|
+
<div className="flex-1 min-w-0">
|
|
32
|
+
<div className="text-xs font-semibold text-text-0 font-sans truncate leading-none">{agent.name}</div>
|
|
33
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
34
|
+
<span className="text-2xs font-mono text-text-3 uppercase tracking-wider">{agent.role}</span>
|
|
35
|
+
<span className="text-2xs text-text-4">/</span>
|
|
36
|
+
<span className="text-2xs font-mono text-text-3">{shortModel(agent.model)}</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Tokens + cost */}
|
|
41
|
+
<div className="text-right flex-shrink-0">
|
|
42
|
+
<div className="text-xs font-mono text-text-1 tabular-nums leading-none">{fmtNum(agent.tokens || 0)}</div>
|
|
43
|
+
{(agent.costUsd || 0) > 0 && (
|
|
44
|
+
<div className="text-2xs font-mono text-text-3 mt-0.5">{fmtDollar(agent.costUsd)}</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Quality / tool success */}
|
|
49
|
+
{successRate != null && (
|
|
50
|
+
<span
|
|
51
|
+
className="text-2xs font-mono font-bold uppercase px-1 py-px rounded-sm flex-shrink-0"
|
|
52
|
+
style={{
|
|
53
|
+
color: successRate >= 90 ? '#4ae168' : successRate >= 70 ? '#e5c07b' : '#e06c75',
|
|
54
|
+
background: successRate >= 90 ? 'rgba(74,225,104,0.1)' : successRate >= 70 ? 'rgba(229,192,123,0.1)' : 'rgba(224,108,117,0.1)',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{successRate}%
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{/* Cost source badge */}
|
|
62
|
+
{agent.costSource && agent.costSource !== 'actual' && (
|
|
63
|
+
<span className="text-2xs font-mono text-text-4 uppercase tracking-wider flex-shrink-0">
|
|
64
|
+
{COST_SOURCE_LABEL[agent.costSource] || ''}
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Context bar */}
|
|
69
|
+
<div className="w-12 flex-shrink-0">
|
|
70
|
+
<div className="flex items-center justify-end gap-1 mb-0.5">
|
|
71
|
+
<span className="text-2xs font-mono text-text-2 tabular-nums">{contextPct}%</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="h-[2px] bg-surface-0 rounded-full overflow-hidden">
|
|
74
|
+
<div
|
|
75
|
+
className="h-full rounded-full transition-all duration-700"
|
|
76
|
+
style={{
|
|
77
|
+
width: `${Math.max(contextPct, 1)}%`,
|
|
78
|
+
background: contextPct > 80 ? '#e06c75' : contextPct > 60 ? '#e5c07b' : isAlive ? '#61afef' : '#333842',
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function shortModel(id) {
|
|
88
|
+
if (!id || id === 'auto' || id === 'default') return 'default';
|
|
89
|
+
const claude = id.match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)/);
|
|
90
|
+
if (claude) return `${claude[1][0].toUpperCase()}${claude[1].slice(1)} ${claude[2]}.${claude[3]}`;
|
|
91
|
+
if (id.startsWith('gemini-')) return id.replace('gemini-', 'Gem ').replace('-preview', '');
|
|
92
|
+
return id.length > 12 ? id.slice(0, 12) + '...' : id;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const FleetPanel = memo(function FleetPanel({ agentBreakdown, rotating = [] }) {
|
|
96
|
+
if (!agentBreakdown?.length) {
|
|
16
97
|
return (
|
|
17
|
-
<div className="flex-1 flex items-center justify-center text-xs text-text-
|
|
18
|
-
No agents
|
|
98
|
+
<div className="flex-1 flex items-center justify-center text-xs text-text-3 font-mono p-4">
|
|
99
|
+
No agents
|
|
19
100
|
</div>
|
|
20
101
|
);
|
|
21
102
|
}
|
|
22
103
|
|
|
23
|
-
// Group by team
|
|
24
104
|
const teams = {};
|
|
25
|
-
for (const a of
|
|
26
|
-
const team = a.teamId || '
|
|
105
|
+
for (const a of agentBreakdown) {
|
|
106
|
+
const team = a.teamId || 'ungrouped';
|
|
27
107
|
if (!teams[team]) teams[team] = [];
|
|
28
108
|
teams[team].push(a);
|
|
29
109
|
}
|
|
30
110
|
|
|
111
|
+
const rotatingSet = new Set(rotating);
|
|
112
|
+
|
|
31
113
|
return (
|
|
32
114
|
<ScrollArea className="flex-1">
|
|
33
|
-
<div className="
|
|
115
|
+
<div className="py-1">
|
|
34
116
|
{Object.entries(teams).map(([team, members]) => (
|
|
35
117
|
<div key={team}>
|
|
36
|
-
<div className="
|
|
37
|
-
|
|
38
|
-
{members.map((a) => (
|
|
39
|
-
<div key={a.id} className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-surface-5 transition-colors">
|
|
40
|
-
<StatusDot status={a.status} size="sm" />
|
|
41
|
-
<Avatar name={a.name} role={a.role} size="sm" />
|
|
42
|
-
<div className="flex-1 min-w-0">
|
|
43
|
-
<div className="text-xs text-text-0 font-sans truncate">{a.name}</div>
|
|
44
|
-
</div>
|
|
45
|
-
<span className="text-2xs font-mono text-text-3">{fmtNum(a.tokensUsed || 0)}</span>
|
|
46
|
-
{/* Mini context bar */}
|
|
47
|
-
<div className="w-10 h-1 bg-surface-0 rounded-full overflow-hidden">
|
|
48
|
-
<div
|
|
49
|
-
className="h-full rounded-full"
|
|
50
|
-
style={{
|
|
51
|
-
width: `${Math.min(a.contextUsage || 0, 100)}%`,
|
|
52
|
-
background: (a.contextUsage || 0) >= 80 ? 'var(--color-danger)' : (a.contextUsage || 0) >= 60 ? 'var(--color-warning)' : 'var(--color-success)',
|
|
53
|
-
}}
|
|
54
|
-
/>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
))}
|
|
118
|
+
<div className="px-3 pt-2 pb-1">
|
|
119
|
+
<span className="text-2xs font-mono text-text-3 uppercase tracking-widest">{team}</span>
|
|
58
120
|
</div>
|
|
121
|
+
{members.map((a) => (
|
|
122
|
+
<AgentRow key={a.id} agent={a} isRotating={rotatingSet.has(a.id)} />
|
|
123
|
+
))}
|
|
59
124
|
</div>
|
|
60
125
|
))}
|
|
61
126
|
</div>
|
|
62
127
|
</ScrollArea>
|
|
63
128
|
);
|
|
64
|
-
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export { FleetPanel };
|
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { memo } from 'react';
|
|
2
3
|
import { fmtUptime, timeAgo } from '../../lib/format';
|
|
3
4
|
import { StatusDot } from '../ui/status-dot';
|
|
4
5
|
import { RefreshCw } from 'lucide-react';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const DashboardHeader = memo(function DashboardHeader({ connected, runningCount, totalCount, uptime, lastFetch, activeTeam }) {
|
|
7
8
|
return (
|
|
8
|
-
<div className="flex items-center gap-4 px-4 py-2
|
|
9
|
-
<h2 className="text-
|
|
9
|
+
<div className="flex items-center gap-4 px-4 py-2 bg-surface-1 border-b border-border">
|
|
10
|
+
<h2 className="text-xs font-semibold text-text-0 font-sans tracking-wide uppercase">Command Center</h2>
|
|
11
|
+
|
|
12
|
+
{activeTeam && (
|
|
13
|
+
<>
|
|
14
|
+
<span className="text-text-4">/</span>
|
|
15
|
+
<span className="text-xs font-mono text-text-2">{activeTeam.name}</span>
|
|
16
|
+
</>
|
|
17
|
+
)}
|
|
18
|
+
|
|
10
19
|
<div className="flex-1" />
|
|
11
20
|
|
|
12
21
|
{connected && (
|
|
13
|
-
<div className="flex items-center gap-
|
|
14
|
-
<span>
|
|
15
|
-
|
|
22
|
+
<div className="flex items-center gap-3.5 text-xs font-mono text-text-2">
|
|
23
|
+
<span>
|
|
24
|
+
<span className="text-text-1">{runningCount}</span>
|
|
25
|
+
<span className="text-text-3">/{totalCount}</span>
|
|
26
|
+
<span className="ml-1 text-text-3">agents</span>
|
|
27
|
+
</span>
|
|
28
|
+
{uptime > 0 && (
|
|
29
|
+
<span className="text-text-3">Up {fmtUptime(uptime)}</span>
|
|
30
|
+
)}
|
|
16
31
|
{lastFetch > 0 && (
|
|
17
32
|
<span className="flex items-center gap-1 text-text-4">
|
|
18
|
-
<RefreshCw size={
|
|
33
|
+
<RefreshCw size={9} />
|
|
34
|
+
<span>{timeAgo(lastFetch)}</span>
|
|
19
35
|
</span>
|
|
20
36
|
)}
|
|
21
37
|
</div>
|
|
22
38
|
)}
|
|
39
|
+
|
|
23
40
|
<StatusDot status={connected ? 'running' : 'crashed'} size="sm" />
|
|
24
41
|
</div>
|
|
25
42
|
);
|
|
26
|
-
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export { DashboardHeader };
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
|
|
4
|
+
import { ScrollArea } from '../ui/scroll-area';
|
|
5
|
+
import { fmtNum, fmtPct, timeAgo } from '../../lib/format';
|
|
6
|
+
import { cn } from '../../lib/cn';
|
|
7
|
+
import { HEX } from '../../lib/theme-hex';
|
|
8
|
+
import { RotateCw, Brain, Radio } from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
/* ── Tiny SVG sparkline for inline use ──────────────────────── */
|
|
11
|
+
function TinySparkline({ data, color = HEX.accent, width = 60, height = 16 }) {
|
|
12
|
+
if (!data || data.length < 2) return <div style={{ width, height }} />;
|
|
13
|
+
const vals = Array.isArray(data[0]) ? data : data.map((d) => (typeof d === 'number' ? d : d.v));
|
|
14
|
+
const min = Math.min(...vals);
|
|
15
|
+
const max = Math.max(...vals);
|
|
16
|
+
const range = max - min || 1;
|
|
17
|
+
|
|
18
|
+
const points = vals.map((v, i) => {
|
|
19
|
+
const x = (i / (vals.length - 1)) * width;
|
|
20
|
+
const y = height - ((v - min) / range) * (height - 2) - 1;
|
|
21
|
+
return `${x},${y}`;
|
|
22
|
+
}).join(' ');
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<svg width={width} height={height} className="flex-shrink-0">
|
|
26
|
+
<polyline points={points} fill="none" stroke={color} strokeWidth="1" strokeLinejoin="round" strokeOpacity="0.7" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ── Savings bar ────────────────────────────────────────────── */
|
|
32
|
+
function SavingsBar({ label, value, total, color }) {
|
|
33
|
+
const pct = total > 0 ? (value / total) * 100 : 0;
|
|
34
|
+
return (
|
|
35
|
+
<div className="space-y-0.5">
|
|
36
|
+
<div className="flex items-center justify-between text-xs font-mono">
|
|
37
|
+
<span className="text-text-2">{label}</span>
|
|
38
|
+
<span className="text-text-1 tabular-nums">{fmtNum(value)}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div className="h-[2px] bg-surface-0 rounded-full overflow-hidden">
|
|
41
|
+
<div
|
|
42
|
+
className="h-full rounded-full transition-all duration-500"
|
|
43
|
+
style={{ width: `${Math.min(pct, 100)}%`, background: color }}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── Rotation Tab ───────────────────────────────────────────── */
|
|
51
|
+
function RotationTab({ tokens, rotation }) {
|
|
52
|
+
const savings = tokens?.savings || {};
|
|
53
|
+
const totalSaved = savings.total || 0;
|
|
54
|
+
const totalUsed = tokens?.totalTokens || 0;
|
|
55
|
+
const hypothetical = totalUsed + totalSaved;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<ScrollArea className="flex-1">
|
|
59
|
+
<div className="p-3 space-y-4">
|
|
60
|
+
{/* Big numbers */}
|
|
61
|
+
<div className="flex gap-4">
|
|
62
|
+
<div>
|
|
63
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Rotations</div>
|
|
64
|
+
<div className="text-xl font-mono font-semibold text-text-0 tabular-nums leading-none">
|
|
65
|
+
{rotation?.totalRotations || 0}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Saved</div>
|
|
70
|
+
<div className="text-xl font-mono font-semibold text-success tabular-nums leading-none">
|
|
71
|
+
{fmtNum(totalSaved)}
|
|
72
|
+
</div>
|
|
73
|
+
{hypothetical > 0 && (
|
|
74
|
+
<div className="text-2xs font-mono text-text-3 mt-0.5">
|
|
75
|
+
{fmtPct((totalSaved / hypothetical) * 100)} of total
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Savings breakdown */}
|
|
82
|
+
<div className="space-y-2">
|
|
83
|
+
<SavingsBar label="Rotation" value={savings.fromRotation || 0} total={hypothetical} color={HEX.accent} />
|
|
84
|
+
<SavingsBar label="Conflict prevention" value={savings.fromConflictPrevention || 0} total={hypothetical} color={HEX.purple} />
|
|
85
|
+
<SavingsBar label="Cold-start skip" value={savings.fromColdStartSkip || 0} total={hypothetical} color={HEX.info} />
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Rotation history */}
|
|
89
|
+
{rotation?.history?.length > 0 && (
|
|
90
|
+
<div>
|
|
91
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1.5">Recent</div>
|
|
92
|
+
<div className="space-y-1">
|
|
93
|
+
{rotation.history.slice(-8).reverse().map((r, i) => (
|
|
94
|
+
<div key={i} className="flex items-center gap-2 text-xs font-mono px-2 py-1 bg-surface-0 rounded">
|
|
95
|
+
<span className="text-text-1 font-medium capitalize truncate flex-1">{r.agentName || r.role}</span>
|
|
96
|
+
<span className="text-text-3 tabular-nums">{fmtPct((r.contextUsage || 0) * 100)}</span>
|
|
97
|
+
<span className="text-text-4">{timeAgo(r.timestamp)}</span>
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</ScrollArea>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ── Adaptive Tab ───────────────────────────────────────────── */
|
|
109
|
+
function AdaptiveTab({ adaptive }) {
|
|
110
|
+
if (!adaptive?.length) {
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex-1 flex items-center justify-center text-xs text-text-3 font-mono p-4">
|
|
113
|
+
No adaptive profiles
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<ScrollArea className="flex-1">
|
|
120
|
+
<div className="p-3 space-y-3">
|
|
121
|
+
{adaptive.map((p) => (
|
|
122
|
+
<div key={p.key} className="bg-surface-0 rounded px-2.5 py-2 space-y-1.5">
|
|
123
|
+
{/* Profile header */}
|
|
124
|
+
<div className="flex items-center gap-2">
|
|
125
|
+
<span className="text-xs font-mono text-text-1 flex-1 truncate">{p.key}</span>
|
|
126
|
+
<span className="text-xs font-mono font-semibold text-text-0 tabular-nums">
|
|
127
|
+
{fmtPct(p.threshold * 100)}
|
|
128
|
+
</span>
|
|
129
|
+
<span
|
|
130
|
+
className={cn(
|
|
131
|
+
'text-2xs font-mono font-bold uppercase px-1 py-px rounded-sm',
|
|
132
|
+
p.converged
|
|
133
|
+
? 'text-success bg-success/10'
|
|
134
|
+
: 'text-text-3 bg-surface-3',
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
{p.converged ? 'CONV' : `${p.adjustments} adj`}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Threshold drift sparkline */}
|
|
142
|
+
{p.thresholdHistory?.length > 1 && (
|
|
143
|
+
<div className="flex items-center gap-2">
|
|
144
|
+
<span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Drift</span>
|
|
145
|
+
<TinySparkline
|
|
146
|
+
data={p.thresholdHistory.map((h) => h.v)}
|
|
147
|
+
color={p.converged ? HEX.success : HEX.accent}
|
|
148
|
+
width={80}
|
|
149
|
+
height={14}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Quality scores sparkline */}
|
|
155
|
+
{p.recentScores?.length > 1 && (
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Quality</span>
|
|
158
|
+
<TinySparkline
|
|
159
|
+
data={p.recentScores}
|
|
160
|
+
color={HEX.warning}
|
|
161
|
+
width={80}
|
|
162
|
+
height={14}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
</ScrollArea>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ── Journalist Tab ─────────────────────────────────────────── */
|
|
174
|
+
function JournalistTab({ journalist }) {
|
|
175
|
+
if (!journalist) {
|
|
176
|
+
return (
|
|
177
|
+
<div className="flex-1 flex items-center justify-center text-xs text-text-3 font-mono p-4">
|
|
178
|
+
Journalist inactive
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<ScrollArea className="flex-1">
|
|
185
|
+
<div className="p-3 space-y-3">
|
|
186
|
+
{/* Status row */}
|
|
187
|
+
<div className="flex items-center gap-3">
|
|
188
|
+
<div>
|
|
189
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Cycles</div>
|
|
190
|
+
<div className="text-lg font-mono font-semibold text-text-0 tabular-nums leading-none">
|
|
191
|
+
{journalist.cycleCount || 0}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
{journalist.lastCycleAt && (
|
|
195
|
+
<div>
|
|
196
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Last</div>
|
|
197
|
+
<div className="text-xs font-mono text-text-2">{timeAgo(journalist.lastCycleAt)}</div>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
{journalist.synthesizing && (
|
|
201
|
+
<span className="text-2xs font-mono font-bold text-accent uppercase tracking-wider animate-pulse">
|
|
202
|
+
Synthesizing
|
|
203
|
+
</span>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Last summary */}
|
|
208
|
+
{journalist.lastSummary && (
|
|
209
|
+
<div>
|
|
210
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1">Summary</div>
|
|
211
|
+
<div className="text-xs font-sans text-text-2 leading-relaxed bg-surface-0 rounded px-2.5 py-2 max-h-32 overflow-y-auto">
|
|
212
|
+
{journalist.lastSummary}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{/* Recent history */}
|
|
218
|
+
{journalist.recentHistory?.length > 0 && (
|
|
219
|
+
<div>
|
|
220
|
+
<div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1.5">History</div>
|
|
221
|
+
<div className="space-y-1">
|
|
222
|
+
{journalist.recentHistory.slice().reverse().map((h, i) => (
|
|
223
|
+
<div key={i} className="flex items-center gap-2 text-xs font-mono px-2 py-1 bg-surface-0 rounded">
|
|
224
|
+
<span className="text-text-3">#{h.cycle}</span>
|
|
225
|
+
<span className="text-text-2 flex-1 truncate">{h.agentCount} agents</span>
|
|
226
|
+
<span className="text-text-4">{timeAgo(h.timestamp)}</span>
|
|
227
|
+
</div>
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</ScrollArea>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ── Intel Panel (main export) ──────────────────────────────── */
|
|
238
|
+
const IntelPanel = memo(function IntelPanel({ tokens, rotation, adaptive, journalist }) {
|
|
239
|
+
return (
|
|
240
|
+
<Tabs defaultValue="rotation" className="flex flex-col h-full">
|
|
241
|
+
<TabsList className="flex-shrink-0 px-1">
|
|
242
|
+
<TabsTrigger value="rotation" className="text-xs px-2.5 py-1.5 gap-1">
|
|
243
|
+
<RotateCw size={11} />
|
|
244
|
+
Rotation
|
|
245
|
+
</TabsTrigger>
|
|
246
|
+
<TabsTrigger value="adaptive" className="text-xs px-2.5 py-1.5 gap-1">
|
|
247
|
+
<Brain size={11} />
|
|
248
|
+
Adaptive
|
|
249
|
+
</TabsTrigger>
|
|
250
|
+
<TabsTrigger value="journalist" className="text-xs px-2.5 py-1.5 gap-1">
|
|
251
|
+
<Radio size={11} />
|
|
252
|
+
Journalist
|
|
253
|
+
</TabsTrigger>
|
|
254
|
+
</TabsList>
|
|
255
|
+
|
|
256
|
+
<TabsContent value="rotation" className="flex-1 min-h-0 overflow-hidden">
|
|
257
|
+
<RotationTab tokens={tokens} rotation={rotation} />
|
|
258
|
+
</TabsContent>
|
|
259
|
+
<TabsContent value="adaptive" className="flex-1 min-h-0 overflow-hidden">
|
|
260
|
+
<AdaptiveTab adaptive={adaptive} />
|
|
261
|
+
</TabsContent>
|
|
262
|
+
<TabsContent value="journalist" className="flex-1 min-h-0 overflow-hidden">
|
|
263
|
+
<JournalistTab journalist={journalist} />
|
|
264
|
+
</TabsContent>
|
|
265
|
+
</Tabs>
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
export { IntelPanel };
|