@brainpilot/web 0.0.3 → 0.0.5
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/dist/assets/index-C-8G4D4j.js +448 -0
- package/dist/assets/index-C501m5OS.css +1 -0
- package/dist/index.html +2 -2
- package/index.html +13 -0
- package/package.json +9 -3
- package/src/App.tsx +10 -0
- package/src/__tests__/api.test.ts +103 -0
- package/src/__tests__/messageGroups.test.ts +80 -0
- package/src/__tests__/newUiComponents.test.tsx +101 -0
- package/src/__tests__/newUiEvents.test.ts +236 -0
- package/src/components/chat/AskUserCard.tsx +123 -0
- package/src/components/chat/AutoRetryIndicator.tsx +71 -0
- package/src/components/chat/ComposerInput.tsx +73 -0
- package/src/components/chat/ComposerSendButton.tsx +26 -0
- package/src/components/chat/MarkdownMessage.tsx +24 -0
- package/src/components/chat/MessageStream.tsx +464 -0
- package/src/components/chat/PromptComposer.tsx +398 -0
- package/src/components/chat/SystemMessageBubble.tsx +46 -0
- package/src/components/demo/DemoFileTree.tsx +146 -0
- package/src/components/demo/DemoView.tsx +668 -0
- package/src/components/demo/TraceNodeModal.tsx +76 -0
- package/src/components/demo/demoBundle.ts +218 -0
- package/src/components/demo/demoCache.ts +42 -0
- package/src/components/files/FilePreviewView.tsx +153 -0
- package/src/components/files/FileSidebar.tsx +664 -0
- package/src/components/files/filePreview.ts +113 -0
- package/src/components/primitives/CustomSelect.tsx +200 -0
- package/src/components/primitives/IconButton.tsx +27 -0
- package/src/components/quota/DiskQuotaCriticalDialog.tsx +56 -0
- package/src/components/quota/DiskQuotaWarningDialog.tsx +65 -0
- package/src/components/quota/QuotaFileManager.tsx +197 -0
- package/src/components/search/SearchDialog.tsx +101 -0
- package/src/components/session/AgentNetwork.tsx +1240 -0
- package/src/components/session/AgentTraceViews.tsx +381 -0
- package/src/components/session/AnalyticsTab.tsx +386 -0
- package/src/components/session/GlobalOverview.tsx +108 -0
- package/src/components/session/NodeTooltip.tsx +127 -0
- package/src/components/session/TimelineTab.tsx +320 -0
- package/src/components/session/TraceGraphView.tsx +301 -0
- package/src/components/session/TraceNodeDetail.tsx +142 -0
- package/src/components/session/agentAnalytics.ts +397 -0
- package/src/components/session/agentNetworkShared.ts +329 -0
- package/src/components/session/traceLayout.ts +150 -0
- package/src/components/settings/SettingsDialog.tsx +719 -0
- package/src/components/shell/DesktopShell.tsx +236 -0
- package/src/components/shell/SandboxBuildingOverlay.tsx +73 -0
- package/src/components/shell/SandboxStatus.tsx +287 -0
- package/src/components/shell/TerminalDrawer.tsx +387 -0
- package/src/components/sidebar/Sidebar.tsx +187 -0
- package/src/config.ts +10 -0
- package/src/contexts/AppProviders.tsx +20 -0
- package/src/contexts/AuthContext.tsx +61 -0
- package/src/contexts/PreferencesContext.tsx +125 -0
- package/src/contexts/SSEContext.tsx +175 -0
- package/src/contexts/SandboxContext.tsx +310 -0
- package/src/contexts/SessionContext.tsx +608 -0
- package/src/contexts/draftStore.ts +103 -0
- package/src/contexts/messageFilters.ts +29 -0
- package/src/contexts/messageGroups.ts +77 -0
- package/src/contexts/messageReducer.ts +401 -0
- package/src/contexts/newUiEvents.ts +190 -0
- package/src/contracts/backend.ts +846 -0
- package/src/contracts/demoBundle.ts +83 -0
- package/src/i18n/messages/analytics.ts +96 -0
- package/src/i18n/messages/chat.ts +108 -0
- package/src/i18n/messages/contexts.ts +40 -0
- package/src/i18n/messages/demo.ts +80 -0
- package/src/i18n/messages/files.ts +82 -0
- package/src/i18n/messages/network.ts +186 -0
- package/src/i18n/messages/profile.ts +40 -0
- package/src/i18n/messages/quota.ts +36 -0
- package/src/i18n/messages/sandbox.ts +116 -0
- package/src/i18n/messages/search.ts +16 -0
- package/src/i18n/messages/settings.ts +184 -0
- package/src/i18n/messages/shell.ts +38 -0
- package/src/i18n/messages/sidebar.ts +52 -0
- package/src/i18n/messages/terminal.ts +22 -0
- package/src/i18n/messages/trace.ts +84 -0
- package/src/i18n/messages.ts +32 -0
- package/src/i18n/translate.ts +46 -0
- package/src/i18n/types.ts +15 -0
- package/src/i18n/useT.ts +15 -0
- package/src/main.tsx +13 -0
- package/src/mocks/backend.ts +722 -0
- package/src/styles/global.css +7429 -0
- package/src/styles/tokens.css +161 -0
- package/src/utils/api.ts +627 -0
- package/src/utils/download.ts +18 -0
- package/src/utils/format.ts +7 -0
- package/src/utils/zip.ts +119 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +22 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +13 -0
- package/vite.config.ts +13 -0
- package/dist/assets/index-Cd0Mi_WU.css +0 -1
- package/dist/assets/index-FGg-DeYR.js +0 -448
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/* --------------------------------------------------------------------------
|
|
2
|
+
* AnalyticsTab — session-level statistics rendered as hand-rolled SVG (the
|
|
3
|
+
* house style; no chart library). Always global: not affected by node
|
|
4
|
+
* selection. Three groups: overview cards, trends/distribution, advanced.
|
|
5
|
+
* ------------------------------------------------------------------------ */
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import {
|
|
8
|
+
AlertTriangle,
|
|
9
|
+
Inbox,
|
|
10
|
+
Timer,
|
|
11
|
+
TrendingUp,
|
|
12
|
+
Users,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import { AgentStatus, ChatMessage } from "../../contracts/backend";
|
|
15
|
+
import { AgentEdge, getAgentAccentVar } from "./agentNetworkShared";
|
|
16
|
+
import {
|
|
17
|
+
computeAgentLoad,
|
|
18
|
+
computeErrorCount,
|
|
19
|
+
computeLifecycleHeatmap,
|
|
20
|
+
computeMessageTrend,
|
|
21
|
+
computeResponseLatencies,
|
|
22
|
+
computeTypeDistribution,
|
|
23
|
+
estimateTokens,
|
|
24
|
+
formatDuration,
|
|
25
|
+
summarizeLatencies,
|
|
26
|
+
} from "./agentAnalytics";
|
|
27
|
+
import { useT } from "../../i18n/useT";
|
|
28
|
+
|
|
29
|
+
interface AnalyticsTabProps {
|
|
30
|
+
agents: AgentStatus[];
|
|
31
|
+
messages: ChatMessage[];
|
|
32
|
+
edges: AgentEdge[];
|
|
33
|
+
now: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AnalyticsTab({ agents, messages, edges, now }: AnalyticsTabProps) {
|
|
37
|
+
const t = useT();
|
|
38
|
+
const trend = useMemo(() => computeMessageTrend(messages, now), [messages, now]);
|
|
39
|
+
const load = useMemo(() => computeAgentLoad(edges), [edges]);
|
|
40
|
+
const typeDist = useMemo(() => computeTypeDistribution(messages), [messages]);
|
|
41
|
+
const latencyStats = useMemo(
|
|
42
|
+
() => summarizeLatencies(computeResponseLatencies(messages)),
|
|
43
|
+
[messages],
|
|
44
|
+
);
|
|
45
|
+
const tokens = useMemo(() => estimateTokens(messages), [messages]);
|
|
46
|
+
const heatmap = useMemo(
|
|
47
|
+
() => computeLifecycleHeatmap(messages, agents.map((a) => a.name), now),
|
|
48
|
+
[messages, agents, now],
|
|
49
|
+
);
|
|
50
|
+
const errorCount = useMemo(() => computeErrorCount(messages), [messages]);
|
|
51
|
+
|
|
52
|
+
const totalMessages = edges.reduce((s, e) => s + e.messages.length, 0);
|
|
53
|
+
const liveCount = agents.length;
|
|
54
|
+
const runningCount = agents.filter(
|
|
55
|
+
(a) => a.status === "running" || a.status === "in_progress",
|
|
56
|
+
).length;
|
|
57
|
+
|
|
58
|
+
if (totalMessages === 0) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="agent-analytics__empty">
|
|
61
|
+
<Inbox size={20} />
|
|
62
|
+
<p>{t("analytics.empty")}</p>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="agent-analytics">
|
|
69
|
+
{/* ---- Group 1: overview cards ---- */}
|
|
70
|
+
<div className="agent-analytics__cards">
|
|
71
|
+
<div className="agent-analytics__card">
|
|
72
|
+
<span className="agent-analytics__card-label">
|
|
73
|
+
<Inbox size={12} /> {t("analytics.card.totalMessages")}
|
|
74
|
+
</span>
|
|
75
|
+
<span className="agent-analytics__card-value">{totalMessages}</span>
|
|
76
|
+
<Sparkline values={trend.map((t) => t.count)} />
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div className="agent-analytics__card">
|
|
80
|
+
<span className="agent-analytics__card-label">
|
|
81
|
+
<Users size={12} /> {t("analytics.card.activeAgents")}
|
|
82
|
+
</span>
|
|
83
|
+
<span className="agent-analytics__card-value">
|
|
84
|
+
{liveCount}
|
|
85
|
+
</span>
|
|
86
|
+
<span className="agent-analytics__card-sub">{t("analytics.card.runningNow", { count: runningCount })}</span>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div className="agent-analytics__card">
|
|
90
|
+
<span className="agent-analytics__card-label">
|
|
91
|
+
<Timer size={12} /> {t("analytics.card.avgLatency")}
|
|
92
|
+
</span>
|
|
93
|
+
<span className="agent-analytics__card-value">
|
|
94
|
+
{latencyStats ? formatDuration(latencyStats.mean) : "—"}
|
|
95
|
+
</span>
|
|
96
|
+
<span className="agent-analytics__card-sub">
|
|
97
|
+
{latencyStats ? t("analytics.card.median", { value: formatDuration(latencyStats.median) }) : t("analytics.card.noPairs")}
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div
|
|
102
|
+
className={`agent-analytics__card ${errorCount > 0 ? "agent-analytics__card--danger" : "agent-analytics__card--ok"}`}
|
|
103
|
+
>
|
|
104
|
+
<span className="agent-analytics__card-label">
|
|
105
|
+
<AlertTriangle size={12} /> {t("analytics.card.errors")}
|
|
106
|
+
</span>
|
|
107
|
+
<span className="agent-analytics__card-value">{errorCount}</span>
|
|
108
|
+
<span className="agent-analytics__card-sub">{errorCount === 0 ? t("analytics.card.allClear") : t("analytics.card.needsAttention")}</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* ---- Group 2: trends & distribution ---- */}
|
|
113
|
+
<section className="agent-analytics__chart">
|
|
114
|
+
<h4 className="agent-analytics__chart-title">
|
|
115
|
+
<TrendingUp size={13} /> {t("analytics.chart.volume")}
|
|
116
|
+
</h4>
|
|
117
|
+
<LineChart values={trend.map((t) => t.count)} />
|
|
118
|
+
</section>
|
|
119
|
+
|
|
120
|
+
<section className="agent-analytics__chart">
|
|
121
|
+
<h4 className="agent-analytics__chart-title">{t("analytics.chart.load")}</h4>
|
|
122
|
+
<BarChart rows={load} />
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<section className="agent-analytics__chart">
|
|
126
|
+
<h4 className="agent-analytics__chart-title">{t("analytics.chart.types")}</h4>
|
|
127
|
+
<PieChart dist={typeDist} />
|
|
128
|
+
</section>
|
|
129
|
+
|
|
130
|
+
{/* ---- Group 3: advanced ---- */}
|
|
131
|
+
{latencyStats ? (
|
|
132
|
+
<section className="agent-analytics__chart">
|
|
133
|
+
<h4 className="agent-analytics__chart-title">{t("analytics.chart.latency")}</h4>
|
|
134
|
+
<BoxPlot stats={latencyStats} />
|
|
135
|
+
</section>
|
|
136
|
+
) : null}
|
|
137
|
+
|
|
138
|
+
<section className="agent-analytics__chart">
|
|
139
|
+
<h4 className="agent-analytics__chart-title">{t("analytics.chart.tokens")}</h4>
|
|
140
|
+
<table className="agent-analytics__table">
|
|
141
|
+
<thead>
|
|
142
|
+
<tr>
|
|
143
|
+
<th>{t("analytics.table.agent")}</th>
|
|
144
|
+
<th>{t("analytics.table.msgs")}</th>
|
|
145
|
+
<th>{t("analytics.table.avgLen")}</th>
|
|
146
|
+
<th>{t("analytics.table.tokens")}</th>
|
|
147
|
+
</tr>
|
|
148
|
+
</thead>
|
|
149
|
+
<tbody>
|
|
150
|
+
{tokens.map((row) => (
|
|
151
|
+
<tr key={row.name}>
|
|
152
|
+
<td>{row.name}</td>
|
|
153
|
+
<td>{row.sentMsgs}</td>
|
|
154
|
+
<td>{row.avgLen}</td>
|
|
155
|
+
<td>{row.tokens.toLocaleString()}</td>
|
|
156
|
+
</tr>
|
|
157
|
+
))}
|
|
158
|
+
</tbody>
|
|
159
|
+
</table>
|
|
160
|
+
</section>
|
|
161
|
+
|
|
162
|
+
{heatmap.agents.length > 0 ? (
|
|
163
|
+
<section className="agent-analytics__chart">
|
|
164
|
+
<h4 className="agent-analytics__chart-title">{t("analytics.chart.heatmap")}</h4>
|
|
165
|
+
<HeatmapGrid heatmap={heatmap} />
|
|
166
|
+
</section>
|
|
167
|
+
) : null}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* --------------------------------------------------------------------------
|
|
173
|
+
* Hand-rolled SVG primitives
|
|
174
|
+
* ------------------------------------------------------------------------ */
|
|
175
|
+
|
|
176
|
+
function Sparkline({ values }: { values: number[] }) {
|
|
177
|
+
const w = 200;
|
|
178
|
+
const h = 28;
|
|
179
|
+
if (values.length === 0) return null;
|
|
180
|
+
const max = Math.max(1, ...values);
|
|
181
|
+
const step = values.length > 1 ? w / (values.length - 1) : w;
|
|
182
|
+
const points = values.map((v, i) => `${i * step},${h - (v / max) * (h - 2) - 1}`).join(" ");
|
|
183
|
+
return (
|
|
184
|
+
<svg className="agent-analytics__sparkline" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" aria-hidden="true">
|
|
185
|
+
<polyline points={points} fill="none" stroke="var(--color-info)" strokeWidth="1.5" />
|
|
186
|
+
</svg>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function LineChart({ values }: { values: number[] }) {
|
|
191
|
+
const t = useT();
|
|
192
|
+
const w = 320;
|
|
193
|
+
const h = 110;
|
|
194
|
+
const pad = 6;
|
|
195
|
+
const max = Math.max(1, ...values);
|
|
196
|
+
const step = values.length > 1 ? (w - pad * 2) / (values.length - 1) : w;
|
|
197
|
+
const pts = values.map((v, i) => ({
|
|
198
|
+
x: pad + i * step,
|
|
199
|
+
y: h - pad - (v / max) * (h - pad * 2),
|
|
200
|
+
}));
|
|
201
|
+
const line = pts.map((p) => `${p.x},${p.y}`).join(" ");
|
|
202
|
+
const area = `${pad},${h - pad} ${line} ${pad + (values.length - 1) * step},${h - pad}`;
|
|
203
|
+
return (
|
|
204
|
+
<svg className="agent-analytics__svg" viewBox={`0 0 ${w} ${h}`} role="img" aria-label={t("analytics.aria.volume")}>
|
|
205
|
+
<polygon points={area} fill="color-mix(in srgb, var(--color-info) 16%, transparent)" stroke="none" />
|
|
206
|
+
<polyline points={line} fill="none" stroke="var(--color-info)" strokeWidth="1.75" strokeLinejoin="round" />
|
|
207
|
+
</svg>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function BarChart({ rows }: { rows: { name: string; total: number }[] }) {
|
|
212
|
+
const t = useT();
|
|
213
|
+
if (rows.length === 0) return <p className="agent-analytics__hint">No traffic yet.</p>;
|
|
214
|
+
const max = Math.max(1, ...rows.map((r) => r.total));
|
|
215
|
+
const rowH = 22;
|
|
216
|
+
const labelW = 96;
|
|
217
|
+
const w = 320;
|
|
218
|
+
const barMax = w - labelW - 36;
|
|
219
|
+
const h = rows.length * rowH + 4;
|
|
220
|
+
return (
|
|
221
|
+
<svg className="agent-analytics__svg" viewBox={`0 0 ${w} ${h}`} role="img" aria-label={t("analytics.aria.load")}>
|
|
222
|
+
{rows.map((r, i) => {
|
|
223
|
+
const y = i * rowH + 4;
|
|
224
|
+
const bw = (r.total / max) * barMax;
|
|
225
|
+
return (
|
|
226
|
+
<g key={r.name}>
|
|
227
|
+
<text x={0} y={y + 11} className="agent-analytics__bar-label">
|
|
228
|
+
{r.name.length > 12 ? `${r.name.slice(0, 11)}…` : r.name}
|
|
229
|
+
</text>
|
|
230
|
+
<rect
|
|
231
|
+
x={labelW}
|
|
232
|
+
y={y + 2}
|
|
233
|
+
width={Math.max(2, bw)}
|
|
234
|
+
height={rowH - 8}
|
|
235
|
+
rx={2}
|
|
236
|
+
fill={getAgentAccentVar(r.name)}
|
|
237
|
+
opacity={0.85}
|
|
238
|
+
/>
|
|
239
|
+
<text x={labelW + bw + 6} y={y + 11} className="agent-analytics__bar-value">
|
|
240
|
+
{r.total}
|
|
241
|
+
</text>
|
|
242
|
+
</g>
|
|
243
|
+
);
|
|
244
|
+
})}
|
|
245
|
+
</svg>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function PieChart({ dist }: { dist: { delegate: number; result: number; other: number } }) {
|
|
250
|
+
const t = useT();
|
|
251
|
+
const segments = [
|
|
252
|
+
{ key: "delegate", value: dist.delegate, color: "var(--color-info)" },
|
|
253
|
+
{ key: "result", value: dist.result, color: "var(--color-success)" },
|
|
254
|
+
{ key: "other", value: dist.other, color: "var(--color-text-subtle)" },
|
|
255
|
+
].filter((s) => s.value > 0);
|
|
256
|
+
const total = segments.reduce((s, seg) => s + seg.value, 0);
|
|
257
|
+
if (total === 0) return <p className="agent-analytics__hint">No typed messages.</p>;
|
|
258
|
+
|
|
259
|
+
const cx = 55;
|
|
260
|
+
const cy = 55;
|
|
261
|
+
const r = 48;
|
|
262
|
+
let angle = -Math.PI / 2;
|
|
263
|
+
const arcs = segments.map((seg) => {
|
|
264
|
+
const frac = seg.value / total;
|
|
265
|
+
const start = angle;
|
|
266
|
+
const end = angle + frac * Math.PI * 2;
|
|
267
|
+
angle = end;
|
|
268
|
+
const x1 = cx + r * Math.cos(start);
|
|
269
|
+
const y1 = cy + r * Math.sin(start);
|
|
270
|
+
const x2 = cx + r * Math.cos(end);
|
|
271
|
+
const y2 = cy + r * Math.sin(end);
|
|
272
|
+
const large = end - start > Math.PI ? 1 : 0;
|
|
273
|
+
// Full-circle guard (single segment = 100%).
|
|
274
|
+
const d =
|
|
275
|
+
segments.length === 1
|
|
276
|
+
? `M ${cx - r} ${cy} a ${r} ${r} 0 1 0 ${r * 2} 0 a ${r} ${r} 0 1 0 ${-r * 2} 0`
|
|
277
|
+
: `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`;
|
|
278
|
+
return { d, color: seg.color, key: seg.key };
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div className="agent-analytics__pie-wrap">
|
|
283
|
+
<svg className="agent-analytics__pie" viewBox="0 0 110 110" role="img" aria-label={t("analytics.aria.types")}>
|
|
284
|
+
{arcs.map((a) => (
|
|
285
|
+
<path key={a.key} d={a.d} fill={a.color} stroke="var(--color-surface)" strokeWidth="1" />
|
|
286
|
+
))}
|
|
287
|
+
<circle cx={cx} cy={cy} r={22} fill="var(--color-surface-raised)" />
|
|
288
|
+
<text x={cx} y={cy + 4} textAnchor="middle" className="agent-analytics__pie-total">
|
|
289
|
+
{total}
|
|
290
|
+
</text>
|
|
291
|
+
</svg>
|
|
292
|
+
<ul className="agent-analytics__legend">
|
|
293
|
+
{segments.map((s) => (
|
|
294
|
+
<li key={s.key}>
|
|
295
|
+
<i style={{ background: s.color }} /> {s.key} ({s.value})
|
|
296
|
+
</li>
|
|
297
|
+
))}
|
|
298
|
+
</ul>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function BoxPlot({
|
|
304
|
+
stats,
|
|
305
|
+
}: {
|
|
306
|
+
stats: { min: number; q1: number; median: number; q3: number; max: number };
|
|
307
|
+
}) {
|
|
308
|
+
const t = useT();
|
|
309
|
+
const w = 320;
|
|
310
|
+
const h = 56;
|
|
311
|
+
const pad = 14;
|
|
312
|
+
const span = Math.max(1, stats.max - stats.min);
|
|
313
|
+
const scale = (v: number) => pad + ((v - stats.min) / span) * (w - pad * 2);
|
|
314
|
+
const midY = h / 2;
|
|
315
|
+
return (
|
|
316
|
+
<div>
|
|
317
|
+
<svg className="agent-analytics__svg" viewBox={`0 0 ${w} ${h}`} role="img" aria-label={t("analytics.aria.latency")}>
|
|
318
|
+
{/* whisker */}
|
|
319
|
+
<line x1={scale(stats.min)} x2={scale(stats.max)} y1={midY} y2={midY} stroke="var(--color-border-strong)" strokeWidth="1.5" />
|
|
320
|
+
<line x1={scale(stats.min)} x2={scale(stats.min)} y1={midY - 8} y2={midY + 8} stroke="var(--color-border-strong)" />
|
|
321
|
+
<line x1={scale(stats.max)} x2={scale(stats.max)} y1={midY - 8} y2={midY + 8} stroke="var(--color-border-strong)" />
|
|
322
|
+
{/* box */}
|
|
323
|
+
<rect
|
|
324
|
+
x={scale(stats.q1)}
|
|
325
|
+
y={midY - 12}
|
|
326
|
+
width={Math.max(2, scale(stats.q3) - scale(stats.q1))}
|
|
327
|
+
height={24}
|
|
328
|
+
rx={3}
|
|
329
|
+
fill="color-mix(in srgb, var(--color-info) 20%, transparent)"
|
|
330
|
+
stroke="var(--color-info)"
|
|
331
|
+
/>
|
|
332
|
+
{/* median */}
|
|
333
|
+
<line x1={scale(stats.median)} x2={scale(stats.median)} y1={midY - 12} y2={midY + 12} stroke="var(--color-info)" strokeWidth="2" />
|
|
334
|
+
</svg>
|
|
335
|
+
<div className="agent-analytics__boxplot-legend">
|
|
336
|
+
<span>min {formatDuration(stats.min)}</span>
|
|
337
|
+
<span>med {formatDuration(stats.median)}</span>
|
|
338
|
+
<span>max {formatDuration(stats.max)}</span>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function HeatmapGrid({
|
|
345
|
+
heatmap,
|
|
346
|
+
}: {
|
|
347
|
+
heatmap: { agents: string[]; buckets: number; counts: number[][]; max: number };
|
|
348
|
+
}) {
|
|
349
|
+
const t = useT();
|
|
350
|
+
const cell = 14;
|
|
351
|
+
const gap = 2;
|
|
352
|
+
const labelW = 90;
|
|
353
|
+
const w = labelW + heatmap.buckets * (cell + gap);
|
|
354
|
+
const h = heatmap.agents.length * (cell + gap) + 2;
|
|
355
|
+
return (
|
|
356
|
+
<svg className="agent-analytics__svg" viewBox={`0 0 ${w} ${h}`} role="img" aria-label={t("analytics.aria.heatmap")}>
|
|
357
|
+
{heatmap.agents.map((name, ai) => (
|
|
358
|
+
<g key={name}>
|
|
359
|
+
<text x={0} y={ai * (cell + gap) + cell - 2} className="agent-analytics__bar-label">
|
|
360
|
+
{name.length > 11 ? `${name.slice(0, 10)}…` : name}
|
|
361
|
+
</text>
|
|
362
|
+
{heatmap.counts[ai].map((count, bi) => {
|
|
363
|
+
const intensity = heatmap.max > 0 ? count / heatmap.max : 0;
|
|
364
|
+
return (
|
|
365
|
+
<rect
|
|
366
|
+
key={bi}
|
|
367
|
+
x={labelW + bi * (cell + gap)}
|
|
368
|
+
y={ai * (cell + gap)}
|
|
369
|
+
width={cell}
|
|
370
|
+
height={cell}
|
|
371
|
+
rx={2}
|
|
372
|
+
fill={
|
|
373
|
+
count === 0
|
|
374
|
+
? "var(--color-surface-soft)"
|
|
375
|
+
: `color-mix(in srgb, var(--color-info) ${Math.round(15 + intensity * 75)}%, transparent)`
|
|
376
|
+
}
|
|
377
|
+
>
|
|
378
|
+
<title>{`${name} · bucket ${bi + 1}: ${count} msg`}</title>
|
|
379
|
+
</rect>
|
|
380
|
+
);
|
|
381
|
+
})}
|
|
382
|
+
</g>
|
|
383
|
+
))}
|
|
384
|
+
</svg>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* --------------------------------------------------------------------------
|
|
2
|
+
* GlobalOverview — the Detail tab's "nothing selected" state. Replaces the old
|
|
3
|
+
* EmptyDetail with an at-a-glance session summary so the panel is informative
|
|
4
|
+
* even before the user clicks a node or edge.
|
|
5
|
+
* ------------------------------------------------------------------------ */
|
|
6
|
+
import { Activity, Inbox, Network, Timer, Users } from "lucide-react";
|
|
7
|
+
import { AgentStatus, ChatMessage } from "../../contracts/backend";
|
|
8
|
+
import { useT } from "../../i18n/useT";
|
|
9
|
+
import { AgentEdge, relativeTime } from "./agentNetworkShared";
|
|
10
|
+
import {
|
|
11
|
+
computeResponseLatencies,
|
|
12
|
+
formatDuration,
|
|
13
|
+
summarizeLatencies,
|
|
14
|
+
} from "./agentAnalytics";
|
|
15
|
+
|
|
16
|
+
interface GlobalOverviewProps {
|
|
17
|
+
agents: AgentStatus[];
|
|
18
|
+
edges: AgentEdge[];
|
|
19
|
+
messages: ChatMessage[];
|
|
20
|
+
totalNodes: number;
|
|
21
|
+
liveCount: number;
|
|
22
|
+
now: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function GlobalOverview({
|
|
26
|
+
agents,
|
|
27
|
+
edges,
|
|
28
|
+
messages,
|
|
29
|
+
totalNodes,
|
|
30
|
+
liveCount,
|
|
31
|
+
now,
|
|
32
|
+
}: GlobalOverviewProps) {
|
|
33
|
+
const t = useT();
|
|
34
|
+
const totalMessages = edges.reduce((sum, e) => sum + e.messages.length, 0);
|
|
35
|
+
const dormantCount = Math.max(0, totalNodes - liveCount);
|
|
36
|
+
const runningCount = agents.filter(
|
|
37
|
+
(a) => a.status === "running" || a.status === "in_progress",
|
|
38
|
+
).length;
|
|
39
|
+
|
|
40
|
+
const lastActivityIso = edges.reduce<string>((latest, e) => {
|
|
41
|
+
return e.lastTimestamp > latest ? e.lastTimestamp : latest;
|
|
42
|
+
}, "");
|
|
43
|
+
|
|
44
|
+
const latencyStats = summarizeLatencies(computeResponseLatencies(messages));
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="agent-network__overview">
|
|
48
|
+
<header className="agent-network__overview-head">
|
|
49
|
+
<span className="agent-network__overview-icon">
|
|
50
|
+
<Network size={16} />
|
|
51
|
+
</span>
|
|
52
|
+
<h3>{t("overview.title")}</h3>
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
<dl className="agent-network__overview-stats">
|
|
56
|
+
<div>
|
|
57
|
+
<dt>
|
|
58
|
+
<Users size={13} /> {t("overview.agents")}
|
|
59
|
+
</dt>
|
|
60
|
+
<dd>
|
|
61
|
+
{t("overview.liveDormant", { live: liveCount, dormant: dormantCount })}
|
|
62
|
+
<span className="agent-network__overview-sub">{t("overview.total", { total: totalNodes })}</span>
|
|
63
|
+
</dd>
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<dt>
|
|
67
|
+
<Activity size={13} /> {t("overview.runningNow")}
|
|
68
|
+
</dt>
|
|
69
|
+
<dd>{runningCount}</dd>
|
|
70
|
+
</div>
|
|
71
|
+
<div>
|
|
72
|
+
<dt>
|
|
73
|
+
<Inbox size={13} /> {t("overview.messages")}
|
|
74
|
+
</dt>
|
|
75
|
+
<dd>
|
|
76
|
+
{totalMessages}
|
|
77
|
+
<span className="agent-network__overview-sub">{t("overview.acrossLinks", { count: edges.length })}</span>
|
|
78
|
+
</dd>
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<dt>
|
|
82
|
+
<Timer size={13} /> {t("overview.avgResponse")}
|
|
83
|
+
</dt>
|
|
84
|
+
<dd>
|
|
85
|
+
{latencyStats ? formatDuration(latencyStats.mean) : "—"}
|
|
86
|
+
{latencyStats ? (
|
|
87
|
+
<span className="agent-network__overview-sub">
|
|
88
|
+
{t("overview.median", { value: formatDuration(latencyStats.median) })}
|
|
89
|
+
</span>
|
|
90
|
+
) : null}
|
|
91
|
+
</dd>
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<dt>
|
|
95
|
+
<Activity size={13} /> {t("overview.lastActivity")}
|
|
96
|
+
</dt>
|
|
97
|
+
<dd>{lastActivityIso ? relativeTime(lastActivityIso, now) : "—"}</dd>
|
|
98
|
+
</div>
|
|
99
|
+
</dl>
|
|
100
|
+
|
|
101
|
+
<p className="agent-network__overview-tip">
|
|
102
|
+
{t("overview.tipPrefix")}
|
|
103
|
+
<strong>{t("network.tab.analytics")}</strong> / <strong>{t("network.tab.timeline")}</strong>
|
|
104
|
+
{t("overview.tipSuffix")}
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* --------------------------------------------------------------------------
|
|
2
|
+
* NodeTooltip — a small hover card shown beside an agent node in the network
|
|
3
|
+
* graph. Rendered as an absolutely-positioned <div> INSIDE the viewport
|
|
4
|
+
* container (NOT inside the SVG) so it isn't clipped by foreignObject and can
|
|
5
|
+
* use normal DOM layout. The parent computes the anchor rect (node bounding
|
|
6
|
+
* box, relative to the viewport container) and passes it in; the tooltip flips
|
|
7
|
+
* to the left side when there isn't room on the right.
|
|
8
|
+
* ------------------------------------------------------------------------ */
|
|
9
|
+
import { Inbox, Send } from "lucide-react";
|
|
10
|
+
import {
|
|
11
|
+
getAgentIcon,
|
|
12
|
+
getAgentProfile,
|
|
13
|
+
relativeTime,
|
|
14
|
+
statusKind,
|
|
15
|
+
} from "./agentNetworkShared";
|
|
16
|
+
|
|
17
|
+
export interface NodeTooltipData {
|
|
18
|
+
name: string;
|
|
19
|
+
isLive: boolean;
|
|
20
|
+
status: string;
|
|
21
|
+
task: string;
|
|
22
|
+
updatedAt?: string;
|
|
23
|
+
sent: number;
|
|
24
|
+
received: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface NodeTooltipProps {
|
|
28
|
+
data: NodeTooltipData;
|
|
29
|
+
now: number;
|
|
30
|
+
/** Node bounding box, in pixels relative to the viewport container. */
|
|
31
|
+
anchor: { left: number; top: number; width: number; height: number };
|
|
32
|
+
/** Viewport container size, for collision detection. */
|
|
33
|
+
container: { width: number; height: number };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TOOLTIP_WIDTH = 240;
|
|
37
|
+
const GAP = 12;
|
|
38
|
+
|
|
39
|
+
function truncate(text: string, max: number): string {
|
|
40
|
+
if (text.length <= max) return text;
|
|
41
|
+
return `${text.slice(0, max - 1)}…`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function NodeTooltip({ data, now, anchor, container }: NodeTooltipProps) {
|
|
45
|
+
const Icon = getAgentIcon(data.name);
|
|
46
|
+
const profile = getAgentProfile(data.name);
|
|
47
|
+
const kind = data.isLive ? statusKind(data.status) : "dormant";
|
|
48
|
+
|
|
49
|
+
// Decide side: prefer right of the node; flip left if it would overflow.
|
|
50
|
+
const wouldOverflowRight = anchor.left + anchor.width + GAP + TOOLTIP_WIDTH > container.width;
|
|
51
|
+
const side: "left" | "right" = wouldOverflowRight ? "left" : "right";
|
|
52
|
+
|
|
53
|
+
const left =
|
|
54
|
+
side === "right"
|
|
55
|
+
? anchor.left + anchor.width + GAP
|
|
56
|
+
: Math.max(GAP, anchor.left - GAP - TOOLTIP_WIDTH);
|
|
57
|
+
|
|
58
|
+
// Vertically center on the node, clamped to the container.
|
|
59
|
+
const rawTop = anchor.top + anchor.height / 2;
|
|
60
|
+
const top = Math.min(Math.max(GAP, rawTop), container.height - GAP);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={`agent-network__tooltip agent-network__tooltip--${side}`}
|
|
65
|
+
role="tooltip"
|
|
66
|
+
style={{ left, top, width: TOOLTIP_WIDTH }}
|
|
67
|
+
>
|
|
68
|
+
<div className="agent-network__tooltip-head">
|
|
69
|
+
<span
|
|
70
|
+
className="agent-network__tooltip-avatar"
|
|
71
|
+
style={{ ["--agent-accent" as string]: accentVar(profile.accent) }}
|
|
72
|
+
>
|
|
73
|
+
<Icon size={14} />
|
|
74
|
+
</span>
|
|
75
|
+
<div className="agent-network__tooltip-id">
|
|
76
|
+
<strong>{data.name}</strong>
|
|
77
|
+
<span>{profile.role}</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="agent-network__tooltip-row">
|
|
82
|
+
<span className={`agent-network__tooltip-status agent-network__tooltip-status--${kind}`}>
|
|
83
|
+
<i className={`agent-network__tooltip-dot agent-network__tooltip-dot--${kind}`} />
|
|
84
|
+
{data.isLive ? data.status : "dormant"}
|
|
85
|
+
</span>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{data.isLive ? (
|
|
89
|
+
<p className="agent-network__tooltip-task">
|
|
90
|
+
{data.task ? truncate(data.task, 60) : "Idle — waiting for instructions"}
|
|
91
|
+
</p>
|
|
92
|
+
) : (
|
|
93
|
+
<p className="agent-network__tooltip-task agent-network__tooltip-task--dormant">
|
|
94
|
+
Not yet spawned by Principal
|
|
95
|
+
</p>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{data.isLive && data.updatedAt ? (
|
|
99
|
+
<p className="agent-network__tooltip-meta">Updated {relativeTime(data.updatedAt, now)}</p>
|
|
100
|
+
) : null}
|
|
101
|
+
|
|
102
|
+
<div className="agent-network__tooltip-counts">
|
|
103
|
+
<span>
|
|
104
|
+
<Send size={11} /> Sent {data.sent}
|
|
105
|
+
</span>
|
|
106
|
+
<span>
|
|
107
|
+
<Inbox size={11} /> Received {data.received}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function accentVar(accent: string): string {
|
|
115
|
+
switch (accent) {
|
|
116
|
+
case "info":
|
|
117
|
+
return "var(--color-info)";
|
|
118
|
+
case "success":
|
|
119
|
+
return "var(--color-success)";
|
|
120
|
+
case "warning":
|
|
121
|
+
return "var(--color-warning)";
|
|
122
|
+
case "danger":
|
|
123
|
+
return "var(--color-danger)";
|
|
124
|
+
default:
|
|
125
|
+
return "var(--color-text-subtle)";
|
|
126
|
+
}
|
|
127
|
+
}
|