@schlessera/brain-ui-react 0.20.0 → 0.21.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/dist/components/activity/activity-page.d.ts.map +1 -1
- package/dist/components/activity/activity-page.js +49 -10
- package/dist/components/activity/activity-page.js.map +1 -1
- package/dist/components/activity/span-bits.d.ts +30 -3
- package/dist/components/activity/span-bits.d.ts.map +1 -1
- package/dist/components/activity/span-bits.js +59 -6
- package/dist/components/activity/span-bits.js.map +1 -1
- package/dist/stores/activity-store.d.ts +10 -0
- package/dist/stores/activity-store.d.ts.map +1 -1
- package/dist/stores/activity-store.js +34 -1
- package/dist/stores/activity-store.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/activity/activity-page.tsx +168 -21
- package/src/components/activity/span-bits.tsx +74 -17
- package/src/stores/activity-store.ts +36 -3
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { useShallow } from "zustand/react/shallow";
|
|
3
3
|
import { isFailureOutcome, SPAN_TOOL_NAME_PREFIX } from "@schlessera/brain-ui-sdk/protocol";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ActivitySpan,
|
|
6
|
+
ActivitySpanEvent,
|
|
7
|
+
ActivitySpanOutcome,
|
|
8
|
+
BillingMode,
|
|
9
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
5
10
|
|
|
6
11
|
import {
|
|
7
12
|
useActivityStore,
|
|
@@ -9,7 +14,7 @@ import {
|
|
|
9
14
|
loadSpanPayloads,
|
|
10
15
|
} from "../../stores/activity-store.js";
|
|
11
16
|
import { cn } from "../../lib/utils.js";
|
|
12
|
-
import { getToolLabel } from "../chat/tool-views.js";
|
|
17
|
+
import { getToolLabel, formatTokenCount } from "../chat/tool-views.js";
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* THE status dot for activity spans — one outcome→color mapping so every
|
|
@@ -92,35 +97,86 @@ export function SpanPayload({ spanId }: { spanId: string }) {
|
|
|
92
97
|
return (
|
|
93
98
|
<div className="space-y-1.5 px-2 py-1">
|
|
94
99
|
{events.map((event) => (
|
|
95
|
-
<
|
|
96
|
-
<div className="mb-0.5 text-[10px] uppercase text-muted-foreground/60">
|
|
97
|
-
{event.eventType === "tool_input" ? "Input" : "Output"}
|
|
98
|
-
</div>
|
|
99
|
-
<pre className="max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md bg-background/60 p-2 font-[family-name:var(--font-mono)] text-[11px] leading-relaxed text-muted-foreground">
|
|
100
|
-
{typeof event.payload === "string" ? event.payload : JSON.stringify(event.payload)}
|
|
101
|
-
</pre>
|
|
102
|
-
{event.truncated && (
|
|
103
|
-
<p className="mt-0.5 text-[10px] italic text-muted-foreground/60">… truncated</p>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
100
|
+
<SpanEventBlock key={`${event.spanId}:${event.eventIndex}`} event={event} />
|
|
106
101
|
))}
|
|
107
102
|
</div>
|
|
108
103
|
);
|
|
109
104
|
}
|
|
110
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Display label for a span event type. Known types get prose; an unknown one
|
|
108
|
+
* (a span-sink producer is free to invent them) prints its own type rather
|
|
109
|
+
* than being mislabelled as output.
|
|
110
|
+
*/
|
|
111
|
+
export function eventTypeLabel(eventType: string): string {
|
|
112
|
+
if (eventType === "tool_input") return "Input";
|
|
113
|
+
if (eventType === "tool_output") return "Output";
|
|
114
|
+
if (eventType.startsWith("transcript_")) return `Transcript · ${eventType.slice("transcript_".length)}`;
|
|
115
|
+
if (eventType === "job_output") return "Job output";
|
|
116
|
+
return eventType.replace(/_/g, " ");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* ONE labelled block per recorded event — the shared renderer behind the tool
|
|
121
|
+
* payload expander and the run detail's narrative stream, so every event type
|
|
122
|
+
* lands somewhere visible instead of only the two the expander knows.
|
|
123
|
+
*/
|
|
124
|
+
export function SpanEventBlock({ event }: { event: ActivitySpanEvent }) {
|
|
125
|
+
const text =
|
|
126
|
+
typeof event.payload === "string" ? event.payload : JSON.stringify(event.payload, null, 2);
|
|
127
|
+
return (
|
|
128
|
+
<div>
|
|
129
|
+
<div className="mb-0.5 text-[10px] uppercase text-muted-foreground/60">
|
|
130
|
+
{eventTypeLabel(event.eventType)}
|
|
131
|
+
</div>
|
|
132
|
+
<pre className="max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md bg-background/60 p-2 font-[family-name:var(--font-mono)] text-[11px] leading-relaxed text-muted-foreground">
|
|
133
|
+
{text}
|
|
134
|
+
</pre>
|
|
135
|
+
{event.truncated && (
|
|
136
|
+
<p className="mt-0.5 text-[10px] italic text-muted-foreground/60">… truncated</p>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A span's own token/model line, or null when the backend recorded no usage
|
|
144
|
+
* for it (every tool span, and any cron root without a span-sink producer).
|
|
145
|
+
* Cache reads are named separately: they are the cheap half of the bill and
|
|
146
|
+
* folding them into "in" would misstate what the run actually consumed.
|
|
147
|
+
*/
|
|
148
|
+
export function formatSpanUsage(span: ActivitySpan): string | null {
|
|
149
|
+
const usage = span.usage;
|
|
150
|
+
if (!usage) return null;
|
|
151
|
+
const parts: string[] = [];
|
|
152
|
+
if (usage.model) parts.push(usage.model);
|
|
153
|
+
if (usage.inputTokens) parts.push(`${formatTokenCount(usage.inputTokens)} in`);
|
|
154
|
+
if (usage.outputTokens) parts.push(`${formatTokenCount(usage.outputTokens)} out`);
|
|
155
|
+
if (usage.cacheReadTokens) parts.push(`${formatTokenCount(usage.cacheReadTokens)} cached`);
|
|
156
|
+
if (usage.cacheCreationTokens)
|
|
157
|
+
parts.push(`${formatTokenCount(usage.cacheCreationTokens)} cache write`);
|
|
158
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
111
161
|
/**
|
|
112
162
|
* THE effective-cost glyph — one three-state rule so no surface ever renders
|
|
113
163
|
* an unknown cost as $0.00 (AE3): absent/NULL is "—" (we don't know), 0 is
|
|
114
|
-
* "
|
|
164
|
+
* "subbed" when the run was subscription-billed and "free" otherwise (a
|
|
165
|
+
* genuinely zero-rate model, e.g. an OpenRouter free tier), positive is
|
|
115
166
|
* dollars, "~"-prefixed when computed from estimated rates. Sub-cent costs
|
|
116
167
|
* floor at "<$0.01" rather than rounding down to a zero look-alike.
|
|
168
|
+
*
|
|
169
|
+
* The zero split matters: a seat plan absorbing the run and a model that
|
|
170
|
+
* costs nothing are both $0 additive, but only one of them stays $0 once the
|
|
171
|
+
* subscription is cancelled.
|
|
117
172
|
*/
|
|
118
173
|
export function formatEffectiveCost(
|
|
119
174
|
costUsd: number | null | undefined,
|
|
120
|
-
estimate?: boolean
|
|
175
|
+
estimate?: boolean,
|
|
176
|
+
billingMode?: BillingMode | null
|
|
121
177
|
): string {
|
|
122
178
|
if (costUsd === null || costUsd === undefined) return "—";
|
|
123
|
-
if (costUsd === 0) return "free";
|
|
179
|
+
if (costUsd === 0) return billingMode === "subscription" ? "subbed" : "free";
|
|
124
180
|
const amount = costUsd < 0.005 ? "<$0.01" : `$${costUsd.toFixed(2)}`;
|
|
125
181
|
return estimate ? `~${amount}` : amount;
|
|
126
182
|
}
|
|
@@ -148,11 +204,12 @@ export function runCostText(run: {
|
|
|
148
204
|
costUsd: number | null;
|
|
149
205
|
effectiveCostUsd?: number | null;
|
|
150
206
|
pricingEstimate?: boolean;
|
|
207
|
+
billingMode?: BillingMode | null;
|
|
151
208
|
}): string | null {
|
|
152
209
|
if (run.effectiveCostUsd === undefined) {
|
|
153
210
|
return run.costUsd !== null && run.costUsd > 0 ? `$${run.costUsd.toFixed(2)}` : null;
|
|
154
211
|
}
|
|
155
|
-
return formatEffectiveCost(run.effectiveCostUsd, run.pricingEstimate);
|
|
212
|
+
return formatEffectiveCost(run.effectiveCostUsd, run.pricingEstimate, run.billingMode);
|
|
156
213
|
}
|
|
157
214
|
|
|
158
215
|
/**
|
|
@@ -407,6 +407,11 @@ export function eventsFor(state: ActivityState, spanId: string): ActivitySpanEve
|
|
|
407
407
|
return state.events[spanId] ?? EMPTY_EVENTS;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/** Payload event types the tool expander owns — every OTHER type belongs to
|
|
411
|
+
* the narrative stream (`narrativeEventsFor`) so nothing recorded is
|
|
412
|
+
* rendered nowhere. */
|
|
413
|
+
const TOOL_PAYLOAD_TYPES = new Set(["tool_input", "tool_output"]);
|
|
414
|
+
|
|
410
415
|
/**
|
|
411
416
|
* The recorded input/output payload events of a tool span (AE7). Returns a
|
|
412
417
|
* fresh array per call — subscribe through `useShallow` (like `childSpans`).
|
|
@@ -414,12 +419,40 @@ export function eventsFor(state: ActivityState, spanId: string): ActivitySpanEve
|
|
|
414
419
|
export function payloadEventsFor(state: ActivityState, spanId: string): ActivitySpanEvent[] {
|
|
415
420
|
const events = state.events[spanId];
|
|
416
421
|
if (!events) return EMPTY_EVENTS;
|
|
417
|
-
const payloads = events.filter(
|
|
418
|
-
(e) => e.eventType === "tool_input" || e.eventType === "tool_output"
|
|
419
|
-
);
|
|
422
|
+
const payloads = events.filter((e) => TOOL_PAYLOAD_TYPES.has(e.eventType));
|
|
420
423
|
return payloads.length > 0 ? payloads : EMPTY_EVENTS;
|
|
421
424
|
}
|
|
422
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Everything recorded against a span that is NOT a tool input/output payload:
|
|
428
|
+
* transcript excerpts, job output, and any span-sink event type a producer
|
|
429
|
+
* invents. Rendered as the span's narrative so an unknown type degrades to a
|
|
430
|
+
* labelled block rather than to invisibility.
|
|
431
|
+
*/
|
|
432
|
+
export function narrativeEventsFor(
|
|
433
|
+
state: ActivityState,
|
|
434
|
+
spanId: string
|
|
435
|
+
): ActivitySpanEvent[] {
|
|
436
|
+
const events = state.events[spanId];
|
|
437
|
+
if (!events) return EMPTY_EVENTS;
|
|
438
|
+
const rest = events.filter((e) => !TOOL_PAYLOAD_TYPES.has(e.eventType));
|
|
439
|
+
return rest.length > 0 ? rest : EMPTY_EVENTS;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Every event recorded under a run, ordered by time — the run detail's
|
|
443
|
+
* narrative stream and the raw-trace dump both read through this. */
|
|
444
|
+
export function runEvents(state: ActivityState, runId: string): ActivitySpanEvent[] {
|
|
445
|
+
const byId = state.spans[runId];
|
|
446
|
+
if (!byId) return EMPTY_EVENTS;
|
|
447
|
+
const out: ActivitySpanEvent[] = [];
|
|
448
|
+
for (const spanId of Object.keys(byId)) {
|
|
449
|
+
const events = state.events[spanId];
|
|
450
|
+
if (events) out.push(...events);
|
|
451
|
+
}
|
|
452
|
+
if (out.length === 0) return EMPTY_EVENTS;
|
|
453
|
+
return out.sort((a, b) => a.ts - b.ts || a.eventIndex - b.eventIndex);
|
|
454
|
+
}
|
|
455
|
+
|
|
423
456
|
/** The span behind one tool call (span ids ARE toolUseIds), if streamed. */
|
|
424
457
|
export function spanForTool(state: ActivityState, toolUseId: string): ActivitySpan | null {
|
|
425
458
|
const runId = state.spanRun[toolUseId];
|