@schlessera/brain-ui-react 0.34.0 → 0.35.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/README.md +12 -0
- package/dist/components/activity/activity-page.d.ts.map +1 -1
- package/dist/components/activity/activity-page.js +8 -149
- package/dist/components/activity/activity-page.js.map +1 -1
- package/dist/components/activity/activity-rollups.d.ts +5 -0
- package/dist/components/activity/activity-rollups.d.ts.map +1 -0
- package/dist/components/activity/activity-rollups.js +39 -0
- package/dist/components/activity/activity-rollups.js.map +1 -0
- package/dist/components/activity/activity-run-detail.d.ts +11 -0
- package/dist/components/activity/activity-run-detail.d.ts.map +1 -0
- package/dist/components/activity/activity-run-detail.js +111 -0
- package/dist/components/activity/activity-run-detail.js.map +1 -0
- package/dist/components/activity/activity-run-list.d.ts +15 -0
- package/dist/components/activity/activity-run-list.d.ts.map +1 -0
- package/dist/components/activity/activity-run-list.js +25 -0
- package/dist/components/activity/activity-run-list.js.map +1 -0
- package/dist/components/chat/brain-markdown-code.d.ts +4 -0
- package/dist/components/chat/brain-markdown-code.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-code.js +47 -0
- package/dist/components/chat/brain-markdown-code.js.map +1 -0
- package/dist/components/chat/brain-markdown-entities.d.ts +14 -0
- package/dist/components/chat/brain-markdown-entities.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-entities.js +93 -0
- package/dist/components/chat/brain-markdown-entities.js.map +1 -0
- package/dist/components/chat/brain-markdown-links.d.ts +36 -0
- package/dist/components/chat/brain-markdown-links.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-links.js +176 -0
- package/dist/components/chat/brain-markdown-links.js.map +1 -0
- package/dist/components/chat/brain-markdown-share.d.ts +15 -0
- package/dist/components/chat/brain-markdown-share.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-share.js +40 -0
- package/dist/components/chat/brain-markdown-share.js.map +1 -0
- package/dist/components/chat/brain-markdown.d.ts +1 -34
- package/dist/components/chat/brain-markdown.d.ts.map +1 -1
- package/dist/components/chat/brain-markdown.js +9 -402
- package/dist/components/chat/brain-markdown.js.map +1 -1
- package/dist/components/chat/composer.d.ts.map +1 -1
- package/dist/components/chat/composer.js +14 -1
- package/dist/components/chat/composer.js.map +1 -1
- package/dist/components/chat/session-drawer.d.ts.map +1 -1
- package/dist/components/chat/session-drawer.js +25 -10
- package/dist/components/chat/session-drawer.js.map +1 -1
- package/dist/components/chat/tool-views.d.ts +2 -2
- package/dist/components/chat/tool-views.d.ts.map +1 -1
- package/dist/components/chat/tool-views.js +2 -16
- package/dist/components/chat/tool-views.js.map +1 -1
- package/dist/components/chat/use-markdown-highlight.d.ts +12 -0
- package/dist/components/chat/use-markdown-highlight.d.ts.map +1 -0
- package/dist/components/chat/use-markdown-highlight.js +56 -0
- package/dist/components/chat/use-markdown-highlight.js.map +1 -0
- package/dist/components/connectivity/connection-gate.d.ts.map +1 -1
- package/dist/components/connectivity/connection-gate.js +94 -8
- package/dist/components/connectivity/connection-gate.js.map +1 -1
- package/dist/components/connectivity/connection-state.d.ts +12 -0
- package/dist/components/connectivity/connection-state.d.ts.map +1 -0
- package/dist/components/connectivity/connection-state.js +23 -0
- package/dist/components/connectivity/connection-state.js.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts +2 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.js +4 -0
- package/dist/components/graph/graph-canvas-lazy.js.map +1 -0
- package/dist/components/graph/graph-controls.js +4 -1
- package/dist/components/graph/graph-controls.js.map +1 -1
- package/dist/components/graph/graph-maintenance.d.ts +8 -0
- package/dist/components/graph/graph-maintenance.d.ts.map +1 -0
- package/dist/components/graph/graph-maintenance.js +115 -0
- package/dist/components/graph/graph-maintenance.js.map +1 -0
- package/dist/components/graph/graph-page.d.ts.map +1 -1
- package/dist/components/graph/graph-page.js +11 -306
- package/dist/components/graph/graph-page.js.map +1 -1
- package/dist/components/graph/graph-scene.d.ts +8 -0
- package/dist/components/graph/graph-scene.d.ts.map +1 -0
- package/dist/components/graph/graph-scene.js +208 -0
- package/dist/components/graph/graph-scene.js.map +1 -0
- package/dist/components/graph/graph-spinner.d.ts +2 -0
- package/dist/components/graph/graph-spinner.d.ts.map +1 -0
- package/dist/components/graph/graph-spinner.js +6 -0
- package/dist/components/graph/graph-spinner.js.map +1 -0
- package/dist/components/layout/app-shell.d.ts.map +1 -1
- package/dist/components/layout/app-shell.js +2 -1
- package/dist/components/layout/app-shell.js.map +1 -1
- package/dist/components/quick-actions/add-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/add-modal.js +47 -4
- package/dist/components/quick-actions/add-modal.js.map +1 -1
- package/dist/components/quick-actions/search-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/search-modal.js +46 -18
- package/dist/components/quick-actions/search-modal.js.map +1 -1
- package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/streaming-modal.js +4 -1
- package/dist/components/quick-actions/streaming-modal.js.map +1 -1
- package/dist/components/settings/devices-agents-tab.d.ts +5 -0
- package/dist/components/settings/devices-agents-tab.d.ts.map +1 -0
- package/dist/components/settings/devices-agents-tab.js +96 -0
- package/dist/components/settings/devices-agents-tab.js.map +1 -0
- package/dist/components/settings/one-time-agent-credential.d.ts +3 -0
- package/dist/components/settings/one-time-agent-credential.d.ts.map +1 -0
- package/dist/components/settings/one-time-agent-credential.js +55 -0
- package/dist/components/settings/one-time-agent-credential.js.map +1 -0
- package/dist/components/settings/settings-panel.d.ts.map +1 -1
- package/dist/components/settings/settings-panel.js +16 -3
- package/dist/components/settings/settings-panel.js.map +1 -1
- package/dist/hooks/use-vpn-status.d.ts +6 -1
- package/dist/hooks/use-vpn-status.d.ts.map +1 -1
- package/dist/hooks/use-vpn-status.js +67 -30
- package/dist/hooks/use-vpn-status.js.map +1 -1
- package/dist/hooks/use-websocket.d.ts +3 -2
- package/dist/hooks/use-websocket.d.ts.map +1 -1
- package/dist/hooks/use-websocket.js +65 -254
- package/dist/hooks/use-websocket.js.map +1 -1
- package/dist/hooks/websocket-handlers/activity.d.ts +5 -0
- package/dist/hooks/websocket-handlers/activity.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/activity.js +10 -0
- package/dist/hooks/websocket-handlers/activity.js.map +1 -0
- package/dist/hooks/websocket-handlers/chat.d.ts +17 -0
- package/dist/hooks/websocket-handlers/chat.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/chat.js +175 -0
- package/dist/hooks/websocket-handlers/chat.js.map +1 -0
- package/dist/hooks/websocket-handlers/connection.d.ts +6 -0
- package/dist/hooks/websocket-handlers/connection.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/connection.js +27 -0
- package/dist/hooks/websocket-handlers/connection.js.map +1 -0
- package/dist/hooks/websocket-handlers/index.d.ts +4 -0
- package/dist/hooks/websocket-handlers/index.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/index.js +19 -0
- package/dist/hooks/websocket-handlers/index.js.map +1 -0
- package/dist/hooks/websocket-handlers/mask.d.ts +4 -0
- package/dist/hooks/websocket-handlers/mask.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/mask.js +14 -0
- package/dist/hooks/websocket-handlers/mask.js.map +1 -0
- package/dist/hooks/websocket-handlers/provider.d.ts +4 -0
- package/dist/hooks/websocket-handlers/provider.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/provider.js +31 -0
- package/dist/hooks/websocket-handlers/provider.js.map +1 -0
- package/dist/hooks/websocket-handlers/types.d.ts +24 -0
- package/dist/hooks/websocket-handlers/types.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/types.js +2 -0
- package/dist/hooks/websocket-handlers/types.js.map +1 -0
- package/dist/lib/api-client.d.ts +32 -0
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +11 -0
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/format-time.d.ts +3 -0
- package/dist/lib/format-time.d.ts.map +1 -0
- package/dist/lib/format-time.js +25 -0
- package/dist/lib/format-time.js.map +1 -0
- package/dist/lib/push-registration.d.ts +7 -0
- package/dist/lib/push-registration.d.ts.map +1 -0
- package/dist/lib/push-registration.js +26 -0
- package/dist/lib/push-registration.js.map +1 -0
- package/dist/stores/activity-store-helpers.d.ts +50 -0
- package/dist/stores/activity-store-helpers.d.ts.map +1 -0
- package/dist/stores/activity-store-helpers.js +143 -0
- package/dist/stores/activity-store-helpers.js.map +1 -0
- package/dist/stores/activity-store-types.d.ts +48 -0
- package/dist/stores/activity-store-types.d.ts.map +1 -0
- package/dist/stores/activity-store-types.js +2 -0
- package/dist/stores/activity-store-types.js.map +1 -0
- package/dist/stores/activity-store.d.ts +2 -78
- package/dist/stores/activity-store.d.ts.map +1 -1
- package/dist/stores/activity-store.js +22 -142
- package/dist/stores/activity-store.js.map +1 -1
- package/dist/stores/connection-store.d.ts +14 -2
- package/dist/stores/connection-store.d.ts.map +1 -1
- package/dist/stores/connection-store.js +17 -0
- package/dist/stores/connection-store.js.map +1 -1
- package/dist/stores/file-store.d.ts.map +1 -1
- package/dist/stores/file-store.js +54 -18
- package/dist/stores/file-store.js.map +1 -1
- package/dist/stores/principal-store.d.ts +16 -0
- package/dist/stores/principal-store.d.ts.map +1 -0
- package/dist/stores/principal-store.js +29 -0
- package/dist/stores/principal-store.js.map +1 -0
- package/dist/stores/ui-store.d.ts +1 -1
- package/dist/stores/ui-store.d.ts.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/activity/activity-page.tsx +6 -477
- package/src/components/activity/activity-rollups.tsx +94 -0
- package/src/components/activity/activity-run-detail.tsx +301 -0
- package/src/components/activity/activity-run-list.tsx +97 -0
- package/src/components/chat/brain-markdown-code.tsx +60 -0
- package/src/components/chat/brain-markdown-entities.tsx +116 -0
- package/src/components/chat/brain-markdown-links.tsx +247 -0
- package/src/components/chat/brain-markdown-share.ts +47 -0
- package/src/components/chat/brain-markdown.tsx +27 -535
- package/src/components/chat/composer.tsx +14 -1
- package/src/components/chat/session-drawer.tsx +29 -10
- package/src/components/chat/tool-views.tsx +2 -13
- package/src/components/chat/use-markdown-highlight.ts +63 -0
- package/src/components/connectivity/connection-gate.tsx +128 -14
- package/src/components/connectivity/connection-state.ts +37 -0
- package/src/components/graph/graph-canvas-lazy.tsx +4 -0
- package/src/components/graph/graph-controls.tsx +4 -1
- package/src/components/graph/graph-maintenance.tsx +304 -0
- package/src/components/graph/graph-page.tsx +11 -840
- package/src/components/graph/graph-scene.tsx +548 -0
- package/src/components/graph/graph-spinner.tsx +9 -0
- package/src/components/layout/app-shell.tsx +2 -0
- package/src/components/quick-actions/add-modal.tsx +49 -4
- package/src/components/quick-actions/search-modal.tsx +46 -18
- package/src/components/quick-actions/streaming-modal.tsx +3 -1
- package/src/components/settings/devices-agents-tab.tsx +231 -0
- package/src/components/settings/one-time-agent-credential.tsx +131 -0
- package/src/components/settings/settings-panel.tsx +20 -5
- package/src/hooks/use-vpn-status.ts +71 -31
- package/src/hooks/use-websocket.ts +74 -275
- package/src/hooks/websocket-handlers/activity.ts +13 -0
- package/src/hooks/websocket-handlers/chat.ts +210 -0
- package/src/hooks/websocket-handlers/connection.ts +30 -0
- package/src/hooks/websocket-handlers/index.ts +59 -0
- package/src/hooks/websocket-handlers/mask.ts +17 -0
- package/src/hooks/websocket-handlers/provider.ts +35 -0
- package/src/hooks/websocket-handlers/types.ts +27 -0
- package/src/lib/api-client.ts +40 -1
- package/src/lib/format-time.ts +20 -0
- package/src/lib/push-registration.ts +29 -0
- package/src/stores/activity-store-helpers.ts +164 -0
- package/src/stores/activity-store-types.ts +55 -0
- package/src/stores/activity-store.ts +21 -217
- package/src/stores/connection-store.ts +33 -2
- package/src/stores/file-store.ts +42 -12
- package/src/stores/principal-store.ts +38 -0
- package/src/stores/ui-store.ts +1 -1
|
@@ -1,51 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Activity as ActivityIcon,
|
|
4
4
|
AlertTriangle,
|
|
5
|
-
ArrowLeft,
|
|
6
|
-
Bot,
|
|
7
|
-
ChevronRight,
|
|
8
|
-
Clock,
|
|
9
5
|
RefreshCw,
|
|
10
6
|
Timer,
|
|
11
7
|
} from "lucide-react";
|
|
12
|
-
import { useShallow } from "zustand/react/shallow";
|
|
13
|
-
import { isFailureOutcome } from "@schlessera/brain-ui-sdk/protocol";
|
|
14
8
|
import type { ActivitySpan } from "@schlessera/brain-ui-sdk/protocol";
|
|
15
9
|
|
|
16
10
|
import {
|
|
17
11
|
api,
|
|
18
12
|
type ActivityIntent,
|
|
19
13
|
type ActivityRollups,
|
|
20
|
-
type ActivityRunRollup,
|
|
21
14
|
type ActivityRunSummary,
|
|
22
15
|
} from "../../lib/api-client.js";
|
|
23
16
|
import { sendClientMessage } from "../../hooks/use-websocket.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
useActivityStore,
|
|
27
|
-
narrativeEventsFor,
|
|
28
|
-
runEvents,
|
|
29
|
-
runSpans,
|
|
30
|
-
} from "../../stores/activity-store.js";
|
|
17
|
+
import { useActivityStore } from "../../stores/activity-store.js";
|
|
31
18
|
import { useChatStore } from "../../stores/chat-store.js";
|
|
32
19
|
import { useUIStore } from "../../stores/ui-store.js";
|
|
33
20
|
import { cn } from "../../lib/utils.js";
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} from "../chat/tool-views.js";
|
|
39
|
-
import {
|
|
40
|
-
SpanEventBlock,
|
|
41
|
-
SpanPayload,
|
|
42
|
-
SpanStatusDot,
|
|
43
|
-
formatAggregateCost,
|
|
44
|
-
formatEffectiveCost,
|
|
45
|
-
formatSpanUsage,
|
|
46
|
-
runCostText,
|
|
47
|
-
spanToolLabel,
|
|
48
|
-
} from "./span-bits.js";
|
|
21
|
+
import { formatRelativeTime } from "../chat/tool-views.js";
|
|
22
|
+
import { RollupCards } from "./activity-rollups.js";
|
|
23
|
+
import { LiveRow, RunRow } from "./activity-run-list.js";
|
|
24
|
+
import { RunDetail } from "./activity-run-detail.js";
|
|
49
25
|
import { PushToggle } from "./push-toggle.js";
|
|
50
26
|
import { SettingsPanel } from "../settings/settings-panel.js";
|
|
51
27
|
|
|
@@ -321,450 +297,3 @@ export function ActivityPage() {
|
|
|
321
297
|
</div>
|
|
322
298
|
);
|
|
323
299
|
}
|
|
324
|
-
|
|
325
|
-
/** Today's key in the server's configured zone (mirrors its day formatter). */
|
|
326
|
-
function todayKey(timeZone: string): string {
|
|
327
|
-
let fmt: Intl.DateTimeFormat;
|
|
328
|
-
try {
|
|
329
|
-
fmt = new Intl.DateTimeFormat("en-CA", { timeZone, dateStyle: "short" });
|
|
330
|
-
} catch {
|
|
331
|
-
fmt = new Intl.DateTimeFormat("en-CA", { timeZone: "UTC", dateStyle: "short" });
|
|
332
|
-
}
|
|
333
|
-
return fmt.format(new Date());
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function RollupCards({ rollups }: { rollups: ActivityRollups }) {
|
|
337
|
-
// days[0] is merely the newest day WITH runs — on a quiet day that is
|
|
338
|
-
// yesterday, so look today up by its actual key (0 when absent).
|
|
339
|
-
const today = rollups.days.find((d) => d.day === todayKey(rollups.timeZone));
|
|
340
|
-
const week = rollups.days.reduce(
|
|
341
|
-
(acc, d) => ({
|
|
342
|
-
runs: acc.runs + d.runs,
|
|
343
|
-
failures: acc.failures + d.failures,
|
|
344
|
-
costUsd: acc.costUsd + d.costUsd,
|
|
345
|
-
effectiveCostUsd: acc.effectiveCostUsd + (d.effectiveCostUsd ?? 0),
|
|
346
|
-
unpricedRuns: acc.unpricedRuns + (d.unpricedRuns ?? 0),
|
|
347
|
-
tokens: acc.tokens + d.inputTokens + d.outputTokens,
|
|
348
|
-
}),
|
|
349
|
-
{ runs: 0, failures: 0, costUsd: 0, effectiveCostUsd: 0, unpricedRuns: 0, tokens: 0 }
|
|
350
|
-
);
|
|
351
|
-
// A server that predates pricing omits the effective fields entirely; the
|
|
352
|
-
// card then keeps its single list-price number instead of claiming an
|
|
353
|
-
// effective $0.00 it never computed.
|
|
354
|
-
const hasEffective = rollups.days.some((d) => d.effectiveCostUsd !== undefined);
|
|
355
|
-
return (
|
|
356
|
-
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
357
|
-
<StatCard label="Runs today" value={String(today?.runs ?? 0)} />
|
|
358
|
-
<StatCard
|
|
359
|
-
label="Failures (7d)"
|
|
360
|
-
value={String(week.failures)}
|
|
361
|
-
alert={week.failures > 0}
|
|
362
|
-
/>
|
|
363
|
-
<StatCard
|
|
364
|
-
label="Spend (7d)"
|
|
365
|
-
value={
|
|
366
|
-
hasEffective
|
|
367
|
-
? formatAggregateCost(week.effectiveCostUsd, week.unpricedRuns)
|
|
368
|
-
: `$${week.costUsd.toFixed(2)}`
|
|
369
|
-
}
|
|
370
|
-
secondary={
|
|
371
|
-
hasEffective && (
|
|
372
|
-
<>
|
|
373
|
-
list ${week.costUsd.toFixed(2)}
|
|
374
|
-
{week.unpricedRuns > 0 && (
|
|
375
|
-
<>
|
|
376
|
-
{/* Two cards per row below `sm`: the qualifier compacts. */}
|
|
377
|
-
<span className="hidden sm:inline"> · {week.unpricedRuns} unpriced</span>
|
|
378
|
-
<span className="sm:hidden"> · {week.unpricedRuns}?</span>
|
|
379
|
-
</>
|
|
380
|
-
)}
|
|
381
|
-
</>
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
/>
|
|
385
|
-
<StatCard label="Tokens (7d)" value={formatTokenCount(week.tokens)} />
|
|
386
|
-
</div>
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function StatCard({
|
|
391
|
-
label,
|
|
392
|
-
value,
|
|
393
|
-
secondary,
|
|
394
|
-
alert,
|
|
395
|
-
}: {
|
|
396
|
-
label: string;
|
|
397
|
-
value: string;
|
|
398
|
-
/** Muted small line between the value and the label (e.g. the list-cost qualifier). */
|
|
399
|
-
secondary?: ReactNode;
|
|
400
|
-
alert?: boolean;
|
|
401
|
-
}) {
|
|
402
|
-
return (
|
|
403
|
-
<div className="rounded-lg border border-border-subtle bg-surface p-3">
|
|
404
|
-
<div className={cn("text-lg font-semibold", alert && "text-destructive")}>{value}</div>
|
|
405
|
-
{secondary && (
|
|
406
|
-
<div className="truncate text-[10px] text-muted-foreground/70">{secondary}</div>
|
|
407
|
-
)}
|
|
408
|
-
<div className="text-[11px] text-muted-foreground">{label}</div>
|
|
409
|
-
</div>
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function LiveRow({
|
|
414
|
-
span,
|
|
415
|
-
onOpen,
|
|
416
|
-
}: {
|
|
417
|
-
span: ActivitySpan;
|
|
418
|
-
onOpen: (row: { runId: string; origin: string; sessionId?: string | null }) => void;
|
|
419
|
-
}) {
|
|
420
|
-
const children = useActivityStore(
|
|
421
|
-
useShallow((s) => Object.values(s.spans[span.runId] ?? {}).filter((x) => x.parentSpanId))
|
|
422
|
-
);
|
|
423
|
-
const now = useNow();
|
|
424
|
-
const current = children.filter((c) => c.outcome === undefined).at(-1);
|
|
425
|
-
const currentLabel = current ? spanToolLabel(current) : null;
|
|
426
|
-
return (
|
|
427
|
-
<button
|
|
428
|
-
type="button"
|
|
429
|
-
onClick={() => onOpen({ runId: span.runId, origin: span.origin, sessionId: span.sessionId })}
|
|
430
|
-
className="flex w-full items-center gap-2 rounded-lg border border-border-subtle bg-surface px-3 py-2 text-left text-xs transition-colors hover:bg-surface-raised"
|
|
431
|
-
>
|
|
432
|
-
<span className="h-2 w-2 shrink-0 animate-pulse rounded-full bg-primary" />
|
|
433
|
-
{span.origin === "cron" ? (
|
|
434
|
-
<Clock className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
435
|
-
) : (
|
|
436
|
-
<Bot className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
437
|
-
)}
|
|
438
|
-
<span className="truncate font-medium">
|
|
439
|
-
{span.jobName ?? span.name}
|
|
440
|
-
</span>
|
|
441
|
-
{/* A span-only run (plain cron job) has no current step — name+elapsed
|
|
442
|
-
is its whole live story. */}
|
|
443
|
-
{currentLabel && (
|
|
444
|
-
<span className="truncate text-muted-foreground">· {currentLabel}</span>
|
|
445
|
-
)}
|
|
446
|
-
<span className="ml-auto shrink-0 font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/60">
|
|
447
|
-
{formatDuration(now - span.startedAt)}
|
|
448
|
-
</span>
|
|
449
|
-
</button>
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function RunRow({
|
|
454
|
-
run,
|
|
455
|
-
onOpen,
|
|
456
|
-
}: {
|
|
457
|
-
run: ActivityRunSummary;
|
|
458
|
-
onOpen: (row: ActivityRunSummary) => void;
|
|
459
|
-
}) {
|
|
460
|
-
const failed = isFailureOutcome(run.outcome);
|
|
461
|
-
// Effective cost only — list price lives on the Spend card and the detail
|
|
462
|
-
// view. "—" is unknown, never $0.00 (AE3); a pre-pricing server that never
|
|
463
|
-
// sent the field keeps the original list-cost span instead (see runCostText).
|
|
464
|
-
const cost = runCostText(run);
|
|
465
|
-
return (
|
|
466
|
-
<button
|
|
467
|
-
type="button"
|
|
468
|
-
onClick={() => onOpen(run)}
|
|
469
|
-
className={cn(
|
|
470
|
-
"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-xs transition-colors hover:bg-surface-raised",
|
|
471
|
-
failed && "border border-destructive/30 bg-destructive/5"
|
|
472
|
-
)}
|
|
473
|
-
>
|
|
474
|
-
{run.origin === "cron" ? (
|
|
475
|
-
<Clock className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
476
|
-
) : (
|
|
477
|
-
<Bot className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
478
|
-
)}
|
|
479
|
-
<span className="truncate">{run.jobName ?? run.name}</span>
|
|
480
|
-
{failed && <AlertTriangle className="h-3 w-3 shrink-0 text-destructive" />}
|
|
481
|
-
{run.outcome && run.outcome !== "success" && (
|
|
482
|
-
<span className={cn("text-[10px] uppercase", failed ? "text-destructive" : "text-muted-foreground")}>
|
|
483
|
-
{run.outcome}
|
|
484
|
-
</span>
|
|
485
|
-
)}
|
|
486
|
-
<span className="ml-auto flex shrink-0 items-center gap-2 font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/60">
|
|
487
|
-
{cost !== null && <span>{cost}</span>}
|
|
488
|
-
{run.durationMs !== null && (
|
|
489
|
-
<span className="flex items-center gap-0.5">
|
|
490
|
-
<Timer className="h-3 w-3" />
|
|
491
|
-
{formatDuration(run.durationMs)}
|
|
492
|
-
</span>
|
|
493
|
-
)}
|
|
494
|
-
<span>{formatRelativeTime(run.startedAt)}</span>
|
|
495
|
-
</span>
|
|
496
|
-
</button>
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Chat-less run detail (cron runs; pruned runs show their rollup) — the full
|
|
502
|
-
* view of one run: its rollup header, the span tree with per-span usage,
|
|
503
|
-
* attributes and recorded events, and a raw-trace escape hatch for anything
|
|
504
|
-
* the rendered view does not have an opinion about.
|
|
505
|
-
*/
|
|
506
|
-
function RunDetail({ runId, onBack }: { runId: string; onBack: () => void }) {
|
|
507
|
-
const streamed = useActivityStore(useShallow((s) => runSpans(s, runId)));
|
|
508
|
-
const events = useActivityStore(useShallow((s) => runEvents(s, runId)));
|
|
509
|
-
const applySnapshot = useActivityStore((s) => s.applySnapshot);
|
|
510
|
-
const [pruned, setPruned] = useState<object | null>(null);
|
|
511
|
-
const [rollup, setRollup] = useState<ActivityRunRollup | null>(null);
|
|
512
|
-
const [missing, setMissing] = useState(false);
|
|
513
|
-
const [rawOpen, setRawOpen] = useState(false);
|
|
514
|
-
|
|
515
|
-
useEffect(() => {
|
|
516
|
-
api
|
|
517
|
-
// Payload bodies are excluded from run detail by default; this view's
|
|
518
|
-
// rows expand to them, so opt in.
|
|
519
|
-
.activityRun(runId, { includePayloads: true })
|
|
520
|
-
.then((detail) => {
|
|
521
|
-
if (detail.rollup) setRollup(detail.rollup);
|
|
522
|
-
if (detail.detailPruned) {
|
|
523
|
-
setPruned(detail.rollup ?? {});
|
|
524
|
-
} else if (detail.spans) {
|
|
525
|
-
applySnapshot({
|
|
526
|
-
type: "activity_snapshot",
|
|
527
|
-
view: "run",
|
|
528
|
-
runId,
|
|
529
|
-
spans: detail.spans,
|
|
530
|
-
events: detail.events ?? [],
|
|
531
|
-
highWaterSeq: { [runId]: detail.highWaterSeq ?? 0 },
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
})
|
|
535
|
-
.catch(() => setMissing(true));
|
|
536
|
-
}, [runId, applySnapshot]);
|
|
537
|
-
|
|
538
|
-
const root = streamed.find((s) => !s.parentSpanId);
|
|
539
|
-
const title = rollup?.jobName ?? rollup?.name ?? (root ? spanToolLabel(root) : runId);
|
|
540
|
-
// The root carries the run's usage for backend-recorded turns; a run whose
|
|
541
|
-
// usage only exists on children (the span-sink shape) sums them instead.
|
|
542
|
-
// Never both, so nothing is double-counted.
|
|
543
|
-
const totalUsage = useMemo(() => {
|
|
544
|
-
if (root?.usage) return formatSpanUsage(root);
|
|
545
|
-
const children = streamed.filter((s) => s.parentSpanId && s.usage);
|
|
546
|
-
if (children.length === 0) return null;
|
|
547
|
-
const sum = children.reduce(
|
|
548
|
-
(acc, s) => ({
|
|
549
|
-
inputTokens: acc.inputTokens + (s.usage?.inputTokens ?? 0),
|
|
550
|
-
outputTokens: acc.outputTokens + (s.usage?.outputTokens ?? 0),
|
|
551
|
-
cacheReadTokens: acc.cacheReadTokens + (s.usage?.cacheReadTokens ?? 0),
|
|
552
|
-
cacheCreationTokens: acc.cacheCreationTokens + (s.usage?.cacheCreationTokens ?? 0),
|
|
553
|
-
}),
|
|
554
|
-
{ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 }
|
|
555
|
-
);
|
|
556
|
-
return formatSpanUsage({ ...children[0]!, usage: { ...sum, model: undefined } });
|
|
557
|
-
}, [root, streamed]);
|
|
558
|
-
|
|
559
|
-
return (
|
|
560
|
-
<div className="flex h-full flex-col overflow-y-auto">
|
|
561
|
-
<div className="flex items-center gap-2 border-b border-border-subtle px-4 py-3">
|
|
562
|
-
<button
|
|
563
|
-
type="button"
|
|
564
|
-
onClick={onBack}
|
|
565
|
-
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
|
|
566
|
-
aria-label="Back"
|
|
567
|
-
>
|
|
568
|
-
<ArrowLeft className="h-4 w-4" />
|
|
569
|
-
</button>
|
|
570
|
-
<div className="min-w-0 flex-1">
|
|
571
|
-
<h1 className="truncate text-sm font-medium">{title}</h1>
|
|
572
|
-
<p className="truncate font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/60">
|
|
573
|
-
{runId}
|
|
574
|
-
</p>
|
|
575
|
-
</div>
|
|
576
|
-
</div>
|
|
577
|
-
<div className="mx-auto w-full max-w-3xl space-y-1 p-4">
|
|
578
|
-
{missing && <p className="text-xs text-muted-foreground">Unknown run.</p>}
|
|
579
|
-
{rollup && (
|
|
580
|
-
<div className="mb-2 flex flex-wrap items-center gap-x-3 gap-y-0.5 rounded-lg border border-border-subtle bg-surface px-3 py-2 text-[11px] text-muted-foreground">
|
|
581
|
-
<span className="text-foreground/80">{rollup.origin}</span>
|
|
582
|
-
{rollup.outcome && (
|
|
583
|
-
<span className={cn(isFailureOutcome(rollup.outcome) && "text-destructive")}>
|
|
584
|
-
{rollup.outcome}
|
|
585
|
-
</span>
|
|
586
|
-
)}
|
|
587
|
-
<span>{formatRelativeTime(rollup.startedAt)}</span>
|
|
588
|
-
{rollup.durationMs !== null && <span>{formatDuration(rollup.durationMs)}</span>}
|
|
589
|
-
<span>
|
|
590
|
-
list{" "}
|
|
591
|
-
<span className="font-[family-name:var(--font-mono)] text-foreground/80">
|
|
592
|
-
{formatEffectiveCost(rollup.costUsd)}
|
|
593
|
-
</span>
|
|
594
|
-
</span>
|
|
595
|
-
<span>
|
|
596
|
-
effective{" "}
|
|
597
|
-
<span className="font-[family-name:var(--font-mono)] text-foreground/80">
|
|
598
|
-
{formatEffectiveCost(
|
|
599
|
-
rollup.effectiveCostUsd,
|
|
600
|
-
rollup.pricingEstimate,
|
|
601
|
-
rollup.billingMode
|
|
602
|
-
)}
|
|
603
|
-
</span>
|
|
604
|
-
</span>
|
|
605
|
-
{rollup.billingMode && (
|
|
606
|
-
<span>{rollup.billingMode === "api" ? "API billed" : "subscription billed"}</span>
|
|
607
|
-
)}
|
|
608
|
-
{rollup.pricingEstimate && <span>~ estimated rates</span>}
|
|
609
|
-
{totalUsage && (
|
|
610
|
-
<span className="font-[family-name:var(--font-mono)] text-foreground/80">
|
|
611
|
-
{totalUsage}
|
|
612
|
-
</span>
|
|
613
|
-
)}
|
|
614
|
-
{rollup.failureReason && (
|
|
615
|
-
<span className="w-full whitespace-pre-wrap break-words pt-1 text-destructive/80">
|
|
616
|
-
{rollup.failureReason}
|
|
617
|
-
</span>
|
|
618
|
-
)}
|
|
619
|
-
</div>
|
|
620
|
-
)}
|
|
621
|
-
{pruned && (
|
|
622
|
-
<div className="rounded-lg border border-border-subtle bg-surface p-3 text-xs text-muted-foreground">
|
|
623
|
-
Detail pruned — only the rollup remains.
|
|
624
|
-
<pre className="mt-2 overflow-x-auto text-[11px]">{JSON.stringify(pruned, null, 2)}</pre>
|
|
625
|
-
</div>
|
|
626
|
-
)}
|
|
627
|
-
{streamed.map((span) => (
|
|
628
|
-
<DetailSpanRow key={span.spanId} span={span} depth={depthOf(span, streamed)} />
|
|
629
|
-
))}
|
|
630
|
-
{!pruned && !missing && streamed.length === 0 && (
|
|
631
|
-
<p className="text-xs text-muted-foreground">No spans recorded for this run.</p>
|
|
632
|
-
)}
|
|
633
|
-
{/* The escape hatch: whatever the rendered tree has no opinion about
|
|
634
|
-
is still readable here, so the view is never LESS than the trace. */}
|
|
635
|
-
{streamed.length > 0 && (
|
|
636
|
-
<div className="pt-3">
|
|
637
|
-
<button
|
|
638
|
-
type="button"
|
|
639
|
-
onClick={() => setRawOpen((v) => !v)}
|
|
640
|
-
className="flex items-center gap-1 text-[11px] text-muted-foreground transition-colors hover:text-foreground"
|
|
641
|
-
>
|
|
642
|
-
<ChevronRight
|
|
643
|
-
className={cn("h-3 w-3 transition-transform", rawOpen && "rotate-90")}
|
|
644
|
-
/>
|
|
645
|
-
Raw trace
|
|
646
|
-
</button>
|
|
647
|
-
{rawOpen && (
|
|
648
|
-
<pre className="mt-1 max-h-[60vh] overflow-auto rounded-lg border border-border-subtle bg-surface p-2 font-[family-name:var(--font-mono)] text-[10px] leading-relaxed text-muted-foreground">
|
|
649
|
-
{JSON.stringify({ runId, rollup, spans: streamed, events }, null, 2)}
|
|
650
|
-
</pre>
|
|
651
|
-
)}
|
|
652
|
-
</div>
|
|
653
|
-
)}
|
|
654
|
-
</div>
|
|
655
|
-
</div>
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* One span tree row. EVERY span expands — a tool span to its recorded payload
|
|
661
|
-
* (AE7), any span to its usage, attributes and recorded events. The uniform
|
|
662
|
-
* affordance is the point: a cron root or a turn root used to be a dead dot
|
|
663
|
-
* with a duration, which is strictly less than the trace it was summarizing.
|
|
664
|
-
*/
|
|
665
|
-
function DetailSpanRow({ span, depth }: { span: ActivitySpan; depth: number }) {
|
|
666
|
-
const [expanded, setExpanded] = useState(false);
|
|
667
|
-
const usage = formatSpanUsage(span);
|
|
668
|
-
// Approval wait is not execution — same boundary the subagent drill-in uses.
|
|
669
|
-
const duration =
|
|
670
|
-
span.endedAt !== undefined
|
|
671
|
-
? formatDuration(span.endedAt - (span.waitUntil ?? span.startedAt))
|
|
672
|
-
: "…";
|
|
673
|
-
return (
|
|
674
|
-
<div style={{ paddingLeft: `${depth * 16}px` }}>
|
|
675
|
-
<div
|
|
676
|
-
className="flex cursor-pointer items-center gap-2 text-xs text-muted-foreground hover:text-foreground"
|
|
677
|
-
onClick={() => setExpanded((v) => !v)}
|
|
678
|
-
>
|
|
679
|
-
<SpanStatusDot span={span} />
|
|
680
|
-
<span className="truncate font-[family-name:var(--font-mono)]">
|
|
681
|
-
{spanToolLabel(span)}
|
|
682
|
-
</span>
|
|
683
|
-
{span.kind !== "tool" && (
|
|
684
|
-
<span className="shrink-0 rounded bg-surface-raised px-1 text-[9px] uppercase text-muted-foreground/70">
|
|
685
|
-
{span.kind}
|
|
686
|
-
</span>
|
|
687
|
-
)}
|
|
688
|
-
{span.outcome && span.outcome !== "success" && (
|
|
689
|
-
<span className="text-[10px] uppercase">{span.outcome}</span>
|
|
690
|
-
)}
|
|
691
|
-
{span.outcomeReason && (
|
|
692
|
-
<span className="truncate text-[10px] text-muted-foreground/60">
|
|
693
|
-
{span.outcomeReason}
|
|
694
|
-
</span>
|
|
695
|
-
)}
|
|
696
|
-
<span className="ml-auto flex shrink-0 items-center gap-2 font-[family-name:var(--font-mono)] text-[10px] text-muted-foreground/50">
|
|
697
|
-
{usage && <span className="hidden sm:inline">{usage}</span>}
|
|
698
|
-
<span>{duration}</span>
|
|
699
|
-
</span>
|
|
700
|
-
<ChevronRight
|
|
701
|
-
className={cn("h-3 w-3 shrink-0 transition-transform", expanded && "rotate-90")}
|
|
702
|
-
/>
|
|
703
|
-
</div>
|
|
704
|
-
{expanded && <DetailSpanBody span={span} usage={usage} />}
|
|
705
|
-
</div>
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/** Everything recorded against one span, under its row. */
|
|
710
|
-
function DetailSpanBody({ span, usage }: { span: ActivitySpan; usage: string | null }) {
|
|
711
|
-
const narrative = useActivityStore(useShallow((s) => narrativeEventsFor(s, span.spanId)));
|
|
712
|
-
const attrs = Object.entries(span.attrs ?? {});
|
|
713
|
-
return (
|
|
714
|
-
<div className="mb-1 ml-1 space-y-1.5 border-l border-border-subtle pl-2 pt-1">
|
|
715
|
-
<div className="flex flex-wrap gap-x-3 gap-y-0.5 px-2 text-[10px] text-muted-foreground/70">
|
|
716
|
-
<span>started {new Date(span.startedAt).toLocaleString()}</span>
|
|
717
|
-
{span.waitUntil !== undefined && (
|
|
718
|
-
<span>approval wait {formatDuration(span.waitUntil - span.startedAt)}</span>
|
|
719
|
-
)}
|
|
720
|
-
{usage && <span className="font-[family-name:var(--font-mono)]">{usage}</span>}
|
|
721
|
-
{span.usage?.costUsd !== undefined && (
|
|
722
|
-
<span className="font-[family-name:var(--font-mono)]">
|
|
723
|
-
{formatEffectiveCost(span.usage.costUsd)} list
|
|
724
|
-
</span>
|
|
725
|
-
)}
|
|
726
|
-
</div>
|
|
727
|
-
{span.subagent?.summary && (
|
|
728
|
-
<p className="px-2 text-[11px] text-muted-foreground">{span.subagent.summary}</p>
|
|
729
|
-
)}
|
|
730
|
-
{attrs.length > 0 && (
|
|
731
|
-
<dl className="grid grid-cols-[minmax(0,auto)_minmax(0,1fr)] gap-x-2 gap-y-0.5 px-2 text-[10px]">
|
|
732
|
-
{attrs.map(([key, value]) => (
|
|
733
|
-
<Fragment key={key}>
|
|
734
|
-
<dt className="truncate font-[family-name:var(--font-mono)] text-muted-foreground/60">
|
|
735
|
-
{key}
|
|
736
|
-
</dt>
|
|
737
|
-
<dd className="min-w-0 break-words font-[family-name:var(--font-mono)] text-muted-foreground">
|
|
738
|
-
{typeof value === "string" ? value : JSON.stringify(value)}
|
|
739
|
-
</dd>
|
|
740
|
-
</Fragment>
|
|
741
|
-
))}
|
|
742
|
-
</dl>
|
|
743
|
-
)}
|
|
744
|
-
{narrative.length > 0 && (
|
|
745
|
-
<div className="space-y-1.5 px-2">
|
|
746
|
-
{narrative.map((event) => (
|
|
747
|
-
<SpanEventBlock key={`${event.spanId}:${event.eventIndex}`} event={event} />
|
|
748
|
-
))}
|
|
749
|
-
</div>
|
|
750
|
-
)}
|
|
751
|
-
{span.kind === "tool" && <SpanPayload spanId={span.spanId} />}
|
|
752
|
-
{span.kind !== "tool" && narrative.length === 0 && attrs.length === 0 && (
|
|
753
|
-
<p className="px-2 text-[11px] text-muted-foreground/60">
|
|
754
|
-
Nothing else recorded for this step.
|
|
755
|
-
</p>
|
|
756
|
-
)}
|
|
757
|
-
</div>
|
|
758
|
-
);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function depthOf(span: ActivitySpan, all: ActivitySpan[]): number {
|
|
762
|
-
let depth = 0;
|
|
763
|
-
let current: ActivitySpan | undefined = span;
|
|
764
|
-
while (current?.parentSpanId) {
|
|
765
|
-
depth += 1;
|
|
766
|
-
current = all.find((s) => s.spanId === current!.parentSpanId);
|
|
767
|
-
if (depth > 6) break;
|
|
768
|
-
}
|
|
769
|
-
return depth;
|
|
770
|
-
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ActivityRollups } from "../../lib/api-client.js";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import { formatTokenCount } from "../chat/tool-views.js";
|
|
6
|
+
import { formatAggregateCost } from "./span-bits.js";
|
|
7
|
+
|
|
8
|
+
/** Today's key in the server's configured zone (mirrors its day formatter). */
|
|
9
|
+
function todayKey(timeZone: string): string {
|
|
10
|
+
let fmt: Intl.DateTimeFormat;
|
|
11
|
+
try {
|
|
12
|
+
fmt = new Intl.DateTimeFormat("en-CA", { timeZone, dateStyle: "short" });
|
|
13
|
+
} catch {
|
|
14
|
+
fmt = new Intl.DateTimeFormat("en-CA", { timeZone: "UTC", dateStyle: "short" });
|
|
15
|
+
}
|
|
16
|
+
return fmt.format(new Date());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function RollupCards({ rollups }: { rollups: ActivityRollups }) {
|
|
20
|
+
// days[0] is merely the newest day WITH runs — on a quiet day that is
|
|
21
|
+
// yesterday, so look today up by its actual key (0 when absent).
|
|
22
|
+
const today = rollups.days.find((d) => d.day === todayKey(rollups.timeZone));
|
|
23
|
+
const week = rollups.days.reduce(
|
|
24
|
+
(acc, d) => ({
|
|
25
|
+
runs: acc.runs + d.runs,
|
|
26
|
+
failures: acc.failures + d.failures,
|
|
27
|
+
costUsd: acc.costUsd + d.costUsd,
|
|
28
|
+
effectiveCostUsd: acc.effectiveCostUsd + (d.effectiveCostUsd ?? 0),
|
|
29
|
+
unpricedRuns: acc.unpricedRuns + (d.unpricedRuns ?? 0),
|
|
30
|
+
tokens: acc.tokens + d.inputTokens + d.outputTokens,
|
|
31
|
+
}),
|
|
32
|
+
{ runs: 0, failures: 0, costUsd: 0, effectiveCostUsd: 0, unpricedRuns: 0, tokens: 0 }
|
|
33
|
+
);
|
|
34
|
+
// A server that predates pricing omits the effective fields entirely; the
|
|
35
|
+
// card then keeps its single list-price number instead of claiming an
|
|
36
|
+
// effective $0.00 it never computed.
|
|
37
|
+
const hasEffective = rollups.days.some((d) => d.effectiveCostUsd !== undefined);
|
|
38
|
+
return (
|
|
39
|
+
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
40
|
+
<StatCard label="Runs today" value={String(today?.runs ?? 0)} />
|
|
41
|
+
<StatCard
|
|
42
|
+
label="Failures (7d)"
|
|
43
|
+
value={String(week.failures)}
|
|
44
|
+
alert={week.failures > 0}
|
|
45
|
+
/>
|
|
46
|
+
<StatCard
|
|
47
|
+
label="Spend (7d)"
|
|
48
|
+
value={
|
|
49
|
+
hasEffective
|
|
50
|
+
? formatAggregateCost(week.effectiveCostUsd, week.unpricedRuns)
|
|
51
|
+
: `$${week.costUsd.toFixed(2)}`
|
|
52
|
+
}
|
|
53
|
+
secondary={
|
|
54
|
+
hasEffective && (
|
|
55
|
+
<>
|
|
56
|
+
list ${week.costUsd.toFixed(2)}
|
|
57
|
+
{week.unpricedRuns > 0 && (
|
|
58
|
+
<>
|
|
59
|
+
{/* Two cards per row below `sm`: the qualifier compacts. */}
|
|
60
|
+
<span className="hidden sm:inline"> · {week.unpricedRuns} unpriced</span>
|
|
61
|
+
<span className="sm:hidden"> · {week.unpricedRuns}?</span>
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
/>
|
|
68
|
+
<StatCard label="Tokens (7d)" value={formatTokenCount(week.tokens)} />
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function StatCard({
|
|
74
|
+
label,
|
|
75
|
+
value,
|
|
76
|
+
secondary,
|
|
77
|
+
alert,
|
|
78
|
+
}: {
|
|
79
|
+
label: string;
|
|
80
|
+
value: string;
|
|
81
|
+
/** Muted small line between the value and the label (e.g. the list-cost qualifier). */
|
|
82
|
+
secondary?: ReactNode;
|
|
83
|
+
alert?: boolean;
|
|
84
|
+
}) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="rounded-lg border border-border-subtle bg-surface p-3">
|
|
87
|
+
<div className={cn("text-lg font-semibold", alert && "text-destructive")}>{value}</div>
|
|
88
|
+
{secondary && (
|
|
89
|
+
<div className="truncate text-[10px] text-muted-foreground/70">{secondary}</div>
|
|
90
|
+
)}
|
|
91
|
+
<div className="text-[11px] text-muted-foreground">{label}</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|