groove-dev 0.22.31 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@groove-dev/cli/src/setup.js +7 -9
- package/node_modules/@groove-dev/daemon/src/api.js +87 -4
- package/node_modules/@groove-dev/daemon/src/process.js +1 -0
- package/node_modules/@groove-dev/daemon/src/teams.js +77 -5
- package/node_modules/@groove-dev/daemon/src/validate.js +1 -0
- package/node_modules/@groove-dev/daemon/test/teams.test.js +5 -5
- package/node_modules/@groove-dev/gui/dist/assets/index-CqdQP7yG.js +587 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DvcNOnKP.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +139 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +16 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +105 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +106 -38
- package/node_modules/@groove-dev/gui/src/components/dashboard/header-bar.jsx +28 -9
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +269 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +35 -9
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +121 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +152 -34
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +28 -8
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +7 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +4 -2
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +97 -52
- package/package.json +1 -1
- package/packages/cli/src/setup.js +7 -9
- package/packages/daemon/src/api.js +87 -4
- package/packages/daemon/src/process.js +1 -0
- package/packages/daemon/src/teams.js +77 -5
- package/packages/daemon/src/validate.js +1 -0
- package/packages/gui/dist/assets/index-CqdQP7yG.js +587 -0
- package/packages/gui/dist/assets/index-DvcNOnKP.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/agents/agent-mdfiles.jsx +139 -0
- package/packages/gui/src/components/agents/agent-panel.jsx +4 -1
- package/packages/gui/src/components/dashboard/activity-feed.jsx +16 -14
- package/packages/gui/src/components/dashboard/cache-ring.jsx +105 -0
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +106 -38
- package/packages/gui/src/components/dashboard/header-bar.jsx +28 -9
- package/packages/gui/src/components/dashboard/intel-panel.jsx +269 -0
- package/packages/gui/src/components/dashboard/kpi-card.jsx +35 -9
- package/packages/gui/src/components/dashboard/routing-chart.jsx +121 -0
- package/packages/gui/src/components/dashboard/token-chart.jsx +152 -34
- package/packages/gui/src/lib/hooks/use-dashboard.js +28 -8
- package/packages/gui/src/lib/theme-hex.js +7 -0
- package/packages/gui/src/stores/groove.js +4 -2
- package/packages/gui/src/views/dashboard.jsx +97 -52
- package/node_modules/@groove-dev/gui/dist/assets/index-CL4GvVoL.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-D_tSBDCx.js +0 -577
- package/node_modules/@groove-dev/gui/src/components/dashboard/savings-panel.jsx +0 -122
- package/packages/gui/dist/assets/index-CL4GvVoL.css +0 -1
- package/packages/gui/dist/assets/index-D_tSBDCx.js +0 -577
- package/packages/gui/src/components/dashboard/savings-panel.jsx +0 -122
|
@@ -1,64 +1,132 @@
|
|
|
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-[#24282f] 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-[11px] font-semibold text-[#e6e6e6] font-sans truncate leading-none">{agent.name}</div>
|
|
33
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
34
|
+
<span className="text-[8px] font-mono text-[#505862] uppercase tracking-wider">{agent.role}</span>
|
|
35
|
+
<span className="text-[8px] text-[#2a2e36]">/</span>
|
|
36
|
+
<span className="text-[8px] font-mono text-[#3a3f4b]">{shortModel(agent.model)}</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Tokens + cost */}
|
|
41
|
+
<div className="text-right flex-shrink-0">
|
|
42
|
+
<div className="text-[11px] font-mono text-[#bcc2cd] tabular-nums leading-none">{fmtNum(agent.tokens || 0)}</div>
|
|
43
|
+
{(agent.costUsd || 0) > 0 && (
|
|
44
|
+
<div className="text-[8px] font-mono text-[#3a3f4b] mt-0.5">{fmtDollar(agent.costUsd)}</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Quality / tool success */}
|
|
49
|
+
{successRate != null && (
|
|
50
|
+
<span
|
|
51
|
+
className="text-[7px] 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-[7px] font-mono text-[#3a3f4b] 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-[8px] font-mono text-[#505862] tabular-nums">{contextPct}%</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="h-[2px] bg-[#1a1e25] 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-
|
|
18
|
-
No agents
|
|
98
|
+
<div className="flex-1 flex items-center justify-center text-[9px] text-[#3a3f4b] font-mono p-4">
|
|
99
|
+
No agents
|
|
19
100
|
</div>
|
|
20
101
|
);
|
|
21
102
|
}
|
|
22
103
|
|
|
23
104
|
// Group by team
|
|
24
105
|
const teams = {};
|
|
25
|
-
for (const a of
|
|
26
|
-
const team = a.teamId || '
|
|
106
|
+
for (const a of agentBreakdown) {
|
|
107
|
+
const team = a.teamId || 'ungrouped';
|
|
27
108
|
if (!teams[team]) teams[team] = [];
|
|
28
109
|
teams[team].push(a);
|
|
29
110
|
}
|
|
30
111
|
|
|
112
|
+
const rotatingSet = new Set(rotating);
|
|
113
|
+
|
|
31
114
|
return (
|
|
32
115
|
<ScrollArea className="flex-1">
|
|
33
|
-
<div className="
|
|
116
|
+
<div className="py-1">
|
|
34
117
|
{Object.entries(teams).map(([team, members]) => (
|
|
35
118
|
<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
|
-
))}
|
|
119
|
+
<div className="px-3 pt-2 pb-1">
|
|
120
|
+
<span className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest">{team}</span>
|
|
58
121
|
</div>
|
|
122
|
+
{members.map((a) => (
|
|
123
|
+
<AgentRow key={a.id} agent={a} isRotating={rotatingSet.has(a.id)} />
|
|
124
|
+
))}
|
|
59
125
|
</div>
|
|
60
126
|
))}
|
|
61
127
|
</div>
|
|
62
128
|
</ScrollArea>
|
|
63
129
|
);
|
|
64
|
-
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
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-[#262a32]">
|
|
10
|
+
<h2 className="text-[12px] font-semibold text-[#e6e6e6] font-sans tracking-wide uppercase">Command Center</h2>
|
|
11
|
+
|
|
12
|
+
{activeTeam && (
|
|
13
|
+
<>
|
|
14
|
+
<span className="text-[#2a2e36]">/</span>
|
|
15
|
+
<span className="text-[9px] font-mono text-[#505862]">{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-[9px] font-mono text-[#505862]">
|
|
23
|
+
<span>
|
|
24
|
+
<span className="text-[#8b929e]">{runningCount}</span>
|
|
25
|
+
<span className="text-[#3a3f4b]">/{totalCount}</span>
|
|
26
|
+
<span className="ml-1">agents</span>
|
|
27
|
+
</span>
|
|
28
|
+
{uptime > 0 && (
|
|
29
|
+
<span>Up {fmtUptime(uptime)}</span>
|
|
30
|
+
)}
|
|
16
31
|
{lastFetch > 0 && (
|
|
17
|
-
<span className="flex items-center gap-1 text-
|
|
18
|
-
<RefreshCw size={
|
|
32
|
+
<span className="flex items-center gap-1 text-[#3a3f4b]">
|
|
33
|
+
<RefreshCw size={8} />
|
|
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.6" />
|
|
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-[9px] font-mono">
|
|
37
|
+
<span className="text-[#6e7681]">{label}</span>
|
|
38
|
+
<span className="text-[#8b929e] tabular-nums">{fmtNum(value)}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div className="h-[2px] bg-[#1a1e25] 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-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5">Rotations</div>
|
|
64
|
+
<div className="text-[18px] font-mono font-semibold text-[#bcc2cd] tabular-nums leading-none">
|
|
65
|
+
{rotation?.totalRotations || 0}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5">Saved</div>
|
|
70
|
+
<div className="text-[18px] font-mono font-semibold text-[#4ae168] tabular-nums leading-none">
|
|
71
|
+
{fmtNum(totalSaved)}
|
|
72
|
+
</div>
|
|
73
|
+
{hypothetical > 0 && (
|
|
74
|
+
<div className="text-[8px] font-mono text-[#505862] 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-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest 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-[9px] font-mono px-2 py-1 bg-[#1a1e25] rounded">
|
|
95
|
+
<span className="text-[#8b929e] font-medium capitalize truncate flex-1">{r.agentName || r.role}</span>
|
|
96
|
+
<span className="text-[#505862] tabular-nums">{fmtPct((r.contextUsage || 0) * 100)}</span>
|
|
97
|
+
<span className="text-[#3a3f4b]">{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-[9px] text-[#3a3f4b] 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-[#1a1e25] 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-[9px] font-mono text-[#8b929e] flex-1 truncate">{p.key}</span>
|
|
126
|
+
<span className="text-[10px] font-mono font-semibold text-[#bcc2cd] tabular-nums">
|
|
127
|
+
{fmtPct(p.threshold * 100)}
|
|
128
|
+
</span>
|
|
129
|
+
<span
|
|
130
|
+
className={cn(
|
|
131
|
+
'text-[7px] font-mono font-bold uppercase px-1 py-px rounded-sm',
|
|
132
|
+
p.converged
|
|
133
|
+
? 'text-[#4ae168] bg-[rgba(74,225,104,0.1)]'
|
|
134
|
+
: 'text-[#505862] bg-[rgba(80,88,98,0.1)]',
|
|
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-[7px] font-mono text-[#3a3f4b] 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-[7px] font-mono text-[#3a3f4b] 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-[9px] text-[#3a3f4b] 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-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5">Cycles</div>
|
|
190
|
+
<div className="text-[16px] font-mono font-semibold text-[#bcc2cd] tabular-nums leading-none">
|
|
191
|
+
{journalist.cycleCount || 0}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
{journalist.lastCycleAt && (
|
|
195
|
+
<div>
|
|
196
|
+
<div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5">Last</div>
|
|
197
|
+
<div className="text-[10px] font-mono text-[#6e7681]">{timeAgo(journalist.lastCycleAt)}</div>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
{journalist.synthesizing && (
|
|
201
|
+
<span className="text-[7px] font-mono font-bold text-[#33afbc] 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-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-1">Summary</div>
|
|
211
|
+
<div className="text-[9px] font-sans text-[#6e7681] leading-relaxed bg-[#1a1e25] 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-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest 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-[9px] font-mono px-2 py-1 bg-[#1a1e25] rounded">
|
|
224
|
+
<span className="text-[#505862]">#{h.cycle}</span>
|
|
225
|
+
<span className="text-[#6e7681] flex-1 truncate">{h.agentCount} agents</span>
|
|
226
|
+
<span className="text-[#3a3f4b]">{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 border-b border-[#262a32]">
|
|
242
|
+
<TabsTrigger value="rotation" className="text-[9px] px-2.5 py-1.5 gap-1">
|
|
243
|
+
<RotateCw size={10} />
|
|
244
|
+
Rotation
|
|
245
|
+
</TabsTrigger>
|
|
246
|
+
<TabsTrigger value="adaptive" className="text-[9px] px-2.5 py-1.5 gap-1">
|
|
247
|
+
<Brain size={10} />
|
|
248
|
+
Adaptive
|
|
249
|
+
</TabsTrigger>
|
|
250
|
+
<TabsTrigger value="journalist" className="text-[9px] px-2.5 py-1.5 gap-1">
|
|
251
|
+
<Radio size={10} />
|
|
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 };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { memo } from 'react';
|
|
2
3
|
import { cn } from '../../lib/cn';
|
|
3
4
|
import { HEX } from '../../lib/theme-hex';
|
|
4
5
|
|
|
5
|
-
function MiniSparkline({ data, color = HEX.accent, width =
|
|
6
|
+
function MiniSparkline({ data, color = HEX.accent, width = 72, height = 22 }) {
|
|
6
7
|
if (!data || data.length < 2) return <div style={{ width, height }} />;
|
|
7
8
|
const vals = data.map((d) => d.v);
|
|
8
9
|
const min = Math.min(...vals);
|
|
@@ -15,31 +16,56 @@ function MiniSparkline({ data, color = HEX.accent, width = 80, height = 24 }) {
|
|
|
15
16
|
return `${x},${y}`;
|
|
16
17
|
}).join(' ');
|
|
17
18
|
|
|
19
|
+
const gradId = `kpi-${color.replace('#', '')}`;
|
|
20
|
+
|
|
18
21
|
return (
|
|
19
22
|
<svg width={width} height={height} className="flex-shrink-0">
|
|
20
23
|
<defs>
|
|
21
|
-
<linearGradient id={
|
|
22
|
-
<stop offset="0%" stopColor={color} stopOpacity="0.
|
|
24
|
+
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
|
|
25
|
+
<stop offset="0%" stopColor={color} stopOpacity="0.15" />
|
|
23
26
|
<stop offset="100%" stopColor={color} stopOpacity="0" />
|
|
24
27
|
</linearGradient>
|
|
25
28
|
</defs>
|
|
26
|
-
<polygon points={`0,${height} ${points} ${width},${height}`} fill={`url(
|
|
27
|
-
<polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" />
|
|
29
|
+
<polygon points={`0,${height} ${points} ${width},${height}`} fill={`url(#${gradId})`} />
|
|
30
|
+
<polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeOpacity="0.7" />
|
|
28
31
|
</svg>
|
|
29
32
|
);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
const KpiCard = memo(function KpiCard({ label, value, sparkData, color = HEX.accent, className }) {
|
|
33
36
|
return (
|
|
34
37
|
<div className={cn(
|
|
35
|
-
'flex items-center gap-
|
|
38
|
+
'flex items-center gap-2.5 px-3 py-2.5 min-w-0',
|
|
39
|
+
'bg-surface-1',
|
|
36
40
|
className,
|
|
37
41
|
)}>
|
|
38
42
|
<div className="flex-1 min-w-0">
|
|
39
|
-
<div className="text-
|
|
40
|
-
<div className="text-
|
|
43
|
+
<div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5 truncate">{label}</div>
|
|
44
|
+
<div className="text-[15px] font-semibold font-mono text-[#bcc2cd] tabular-nums leading-none">{value}</div>
|
|
41
45
|
</div>
|
|
42
46
|
<MiniSparkline data={sparkData} color={color} />
|
|
43
47
|
</div>
|
|
44
48
|
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export function KpiStrip({ kpis }) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex flex-wrap" style={{ background: '#1a1e25' }}>
|
|
54
|
+
{kpis.map((kpi, i) => (
|
|
55
|
+
<KpiCard
|
|
56
|
+
key={kpi.label}
|
|
57
|
+
label={kpi.label}
|
|
58
|
+
value={kpi.value}
|
|
59
|
+
sparkData={kpi.sparkData}
|
|
60
|
+
color={kpi.color}
|
|
61
|
+
className={cn(
|
|
62
|
+
'flex-1 basis-[12.5%] min-w-[140px]',
|
|
63
|
+
'border-b border-r border-[#262a32]',
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
45
69
|
}
|
|
70
|
+
|
|
71
|
+
export { KpiCard };
|