@theoremjs/react 0.3.4-next.604.ge4d5866 → 0.3.4-next.608.gd007c52

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/react",
3
- "version": "0.3.4-next.604.ge4d5866",
3
+ "version": "0.3.4-next.608.gd007c52",
4
4
  "description": "React bindings for Theorem: headless hooks + transport, an Astryx chat UI, and a Web-standard server handler.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,8 +60,8 @@
60
60
  "@stylexjs/stylex": "0.19.0",
61
61
  "react": "^19.0.0",
62
62
  "react-dom": "^19.0.0",
63
- "@theoremjs/agents": "^0.3.4-next.604.ge4d5866",
64
- "@theoremjs/web": "^0.3.4-next.604.ge4d5866"
63
+ "@theoremjs/agents": "^0.3.4-next.608.gd007c52",
64
+ "@theoremjs/web": "^0.3.4-next.608.gd007c52"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@astryxdesign/core": {
package/ui/CardWall.d.ts CHANGED
@@ -2,7 +2,10 @@ import { type ReactNode } from 'react';
2
2
  /**
3
3
  * Cards in as many columns as fit, each as tall as its content. Each card,
4
4
  * in order, goes under the shortest column so far, so short cards never
5
- * leave a hole beside a tall one.
5
+ * leave a hole beside a tall one. A card that renders nothing takes no place.
6
+ *
7
+ * invariant: Cards stay in one container in source order and are only moved,
8
+ * so a reflow never remounts a card and focus and tab order follow the source.
6
9
  */
7
10
  export declare function CardWall({ minWidth, children }: {
8
11
  minWidth: number;
package/ui/CardWall.js CHANGED
@@ -1,23 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { HStack } from '@astryxdesign/core/HStack';
3
- import { StackItem } from '@astryxdesign/core/Stack';
4
2
  import { VStack } from '@astryxdesign/core/VStack';
5
- import { Children, useLayoutEffect, useRef, useState } from 'react';
6
- import { columnCount, wallColumns } from "./wall-columns.js";
7
- /** The wall's width and gap, and each card's height by its place in `children`. */
8
- function measureWall(wall, stacks, placed, cards) {
9
- const heights = Array.from({ length: cards });
10
- placed.forEach((column, at) => {
11
- const shown = stacks[at]?.children;
12
- column.forEach((card, row) => {
13
- const element = shown?.[row];
14
- if (element instanceof HTMLElement)
15
- heights[card] = element.offsetHeight;
16
- });
17
- });
18
- const gap = Number.parseFloat(getComputedStyle(wall).columnGap) || 0;
19
- return { width: wall.clientWidth, gap, heights };
20
- }
3
+ import { Children, isValidElement, useLayoutEffect, useRef, useState } from 'react';
4
+ import { columnCount, wallPlaces } from "./wall-columns.js";
21
5
  function sameWall(a, b) {
22
6
  return (a.width === b.width &&
23
7
  a.gap === b.gap &&
@@ -27,35 +11,66 @@ function sameWall(a, b) {
27
11
  /**
28
12
  * Cards in as many columns as fit, each as tall as its content. Each card,
29
13
  * in order, goes under the shortest column so far, so short cards never
30
- * leave a hole beside a tall one.
14
+ * leave a hole beside a tall one. A card that renders nothing takes no place.
15
+ *
16
+ * invariant: Cards stay in one container in source order and are only moved,
17
+ * so a reflow never remounts a card and focus and tab order follow the source.
31
18
  */
32
19
  export function CardWall({ minWidth, children }) {
33
20
  const cards = Children.toArray(children);
21
+ const keyOf = (card, at) => isValidElement(card) && card.key !== null ? card.key : String(at);
34
22
  const wallRef = useRef(null);
35
- const stacks = useRef([]);
23
+ const slots = useRef([]);
24
+ const observer = useRef(null);
36
25
  const [wall, setWall] = useState({ width: 0, gap: 0, heights: [] });
37
- const placed = wallColumns(cards.map((_, at) => wall.heights[at]), columnCount(wall.width, minWidth, wall.gap), wall.gap);
38
- const placedRef = useRef(placed);
39
- placedRef.current = placed;
40
- const layout = placed.map((column) => column.join(' ')).join('|');
26
+ const count = columnCount(wall.width, minWidth, wall.gap);
27
+ const columnWidth = (wall.width - wall.gap * (count - 1)) / count;
28
+ const { places, height } = wallPlaces(cards.map((_, at) => wall.heights[at]), count, wall.gap);
29
+ // why: Until the wall is measured, cards stack in flow so the first paint has their heights.
30
+ const isPlaced = wall.width > 0;
31
+ slots.current.length = cards.length;
41
32
  useLayoutEffect(() => {
42
33
  const node = wallRef.current;
43
- if (!node || !layout)
34
+ if (!node)
44
35
  return;
45
36
  const measure = () => {
46
- const next = measureWall(node, stacks.current, placedRef.current, cards.length);
37
+ const next = {
38
+ width: node.clientWidth,
39
+ gap: Number.parseFloat(getComputedStyle(node).rowGap) || 0,
40
+ heights: slots.current.map((slot) => slot?.offsetHeight),
41
+ };
47
42
  setWall((shown) => (sameWall(shown, next) ? shown : next));
48
43
  };
49
44
  measure();
50
- const observer = new ResizeObserver(measure);
51
- observer.observe(node);
52
- for (const stack of stacks.current) {
53
- for (const card of stack?.children ?? [])
54
- observer.observe(card);
55
- }
56
- return () => observer.disconnect();
57
- }, [layout, cards.length]);
58
- return (_jsx(HStack, { ref: wallRef, gap: 3, vAlign: "start", children: placed.map((column, at) => (_jsx(StackItem, { size: "fill", children: _jsx(VStack, { ref: (node) => {
59
- stacks.current[at] = node;
60
- }, gap: 3, children: column.map((card) => cards[card]) }) }, String(at)))) }));
45
+ const watch = new ResizeObserver(measure);
46
+ watch.observe(node);
47
+ for (const slot of slots.current)
48
+ if (slot)
49
+ watch.observe(slot);
50
+ observer.current = watch;
51
+ return () => {
52
+ watch.disconnect();
53
+ observer.current = null;
54
+ };
55
+ }, []);
56
+ return (_jsx(VStack, { ref: wallRef, gap: 3, style: isPlaced ? { position: 'relative', height } : undefined, children: cards.map((card, at) => {
57
+ const place = places[at];
58
+ return (_jsx("div", { ref: (slot) => {
59
+ slots.current[at] = slot;
60
+ // why: A card that mounts later is measured once it is watched.
61
+ if (slot)
62
+ observer.current?.observe(slot);
63
+ return () => {
64
+ if (slot)
65
+ observer.current?.unobserve(slot);
66
+ };
67
+ }, style: isPlaced && place
68
+ ? {
69
+ position: 'absolute',
70
+ top: place.top,
71
+ left: place.column * (columnWidth + wall.gap),
72
+ width: columnWidth,
73
+ }
74
+ : undefined, children: card }, keyOf(card, at)));
75
+ }) }));
61
76
  }
package/ui/ToolResult.js CHANGED
@@ -1,7 +1,6 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { AspectRatio } from '@astryxdesign/core/AspectRatio';
3
3
  import { ClickableCard } from '@astryxdesign/core/ClickableCard';
4
- import { Grid } from '@astryxdesign/core/Grid';
5
4
  import { useLocale } from '@astryxdesign/core/i18n';
6
5
  import { Lightbox } from '@astryxdesign/core/Lightbox';
7
6
  import { Skeleton } from '@astryxdesign/core/Skeleton';
@@ -9,43 +8,34 @@ import { VStack } from '@astryxdesign/core/VStack';
9
8
  import { hasLead, layoutResult, returnedMedia, withUnit, } from '@theoremjs/web/client';
10
9
  import { lazy, Suspense, useMemo, useState } from 'react';
11
10
  import { Arrive } from "./arrive.js";
11
+ import { CardWall } from "./CardWall.js";
12
12
  import { useLabels } from "./labels-provider.js";
13
13
  import { keyedByContent } from "./row-keys.js";
14
14
  import { ShapedData } from "./ShapedData.js";
15
15
  import { ChartCard } from "./trace-patterns.js";
16
16
  import { VoiceNote } from "./VoiceNote.js";
17
- const ToolResultCharts = lazy(() => import("./ToolResultCharts.js").then((module) => ({ default: module.ToolResultCharts })));
18
- /** Narrowest an image card gets. */
19
- const IMAGE_CARD_PX = 200;
17
+ const ToolResultChart = lazy(() => import("./ToolResultCharts.js").then((module) => ({ default: module.ToolResultChart })));
20
18
  /** Height a chart card holds while its module loads. */
21
19
  const CHART_CARD_PX = 236;
22
- function Images({ images }) {
23
- const [open, setOpen] = useState(null);
24
- return (_jsxs(_Fragment, { children: [_jsx(Grid, { columns: { minWidth: IMAGE_CARD_PX, repeat: 'fill' }, gap: 3, children: images.map((image, index) => (_jsx(Arrive, { children: _jsx(ClickableCard, { label: image.alt || 'Open image', padding: 0, onClick: () => setOpen(index), children: _jsx(AspectRatio, { ratio: 4 / 3, fit: "cover", children: _jsx("img", { src: image.src, alt: image.alt, loading: "lazy" }) }) }) }, image.src))) }), _jsx(Lightbox, { isOpen: open !== null, onOpenChange: (isOpen) => {
25
- if (!isOpen)
26
- setOpen(null);
27
- }, media: images.map((image) => ({ src: image.src, alt: image.alt })), index: open ?? 0, onIndexChange: setOpen })] }));
28
- }
29
- function Figures({ figures }) {
30
- const locale = useLocale();
31
- const decimal = useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }), [locale]);
32
- if (figures.length === 0)
33
- return null;
34
- return (_jsx(Grid, { columns: { minWidth: 160, repeat: 'fill' }, gap: 3, children: figures.map((figure) => (_jsx(Arrive, { children: _jsx(ChartCard, { title: figure.label, value: withUnit(decimal.format(figure.value), figure.unit) }) }, figure.key))) }));
35
- }
36
- /** Charts load with their module; cards hold their place until then. */
37
- function Charts({ charts }) {
38
- if (charts.length === 0)
39
- return null;
40
- return (_jsx(Suspense, { fallback: _jsx(Grid, { columns: { minWidth: 260, repeat: 'fill' }, gap: 3, children: charts.map((chart) => (_jsx(Arrive, { children: _jsx(Skeleton, { height: CHART_CARD_PX }) }, chart.key))) }), children: _jsx(ToolResultCharts, { charts: charts }) }));
41
- }
42
- /** What answers the call, laid out from the output: its figures as stat cards, its series as charts, the images, and audio it returned. */
20
+ /** Narrowest a card in the result's wall gets: wide enough for a chart's axis and bars. */
21
+ const CARD_PX = 260;
22
+ /**
23
+ * What answers the call, laid out from the output in one wall: its figures as
24
+ * stat cards, its series as charts, the images it returned; then its audio.
25
+ */
43
26
  function ResultLead({ layout, parts = [], }) {
44
27
  const t = useLabels();
28
+ const locale = useLocale();
29
+ const decimal = useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }), [locale]);
45
30
  const media = useMemo(() => returnedMedia(layout, parts, t('@theorem.host.image')), [layout, parts, t]);
31
+ const [open, setOpen] = useState(null);
46
32
  if (!hasLead(layout, media))
47
33
  return null;
48
- return (_jsxs(VStack, { gap: 3, children: [_jsx(Figures, { figures: layout.figures }), _jsx(Charts, { charts: layout.charts }), media.images.length > 0 ? _jsx(Images, { images: media.images }) : null, keyedByContent(media.audio, (clip) => clip.src).map(({ item: clip, key }) => (_jsx(Arrive, { children: _jsx(VoiceNote, { src: clip.src, mimeType: clip.mimeType }) }, key)))] }));
34
+ const { images, audio } = media;
35
+ return (_jsxs(VStack, { gap: 3, children: [_jsxs(CardWall, { minWidth: CARD_PX, children: [layout.figures.map((figure) => (_jsx(Arrive, { children: _jsx(ChartCard, { title: figure.label, value: withUnit(decimal.format(figure.value), figure.unit) }) }, `figure:${figure.key}`))), layout.charts.map((chart) => (_jsx(Arrive, { children: _jsx(Suspense, { fallback: _jsx(Skeleton, { height: CHART_CARD_PX }), children: _jsx(ToolResultChart, { chart: chart }) }) }, `chart:${chart.key}`))), images.map((image, index) => (_jsx(Arrive, { children: _jsx(ClickableCard, { label: image.alt || 'Open image', padding: 0, onClick: () => setOpen(index), children: _jsx(AspectRatio, { ratio: 4 / 3, fit: "cover", children: _jsx("img", { src: image.src, alt: image.alt, loading: "lazy" }) }) }) }, `image:${image.src}`)))] }), images.length > 0 ? (_jsx(Lightbox, { isOpen: open !== null, onOpenChange: (isOpen) => {
36
+ if (!isOpen)
37
+ setOpen(null);
38
+ }, media: images.map((image) => ({ src: image.src, alt: image.alt })), index: open ?? 0, onIndexChange: setOpen })) : null, keyedByContent(audio, (clip) => clip.src).map(({ item: clip, key }) => (_jsx(Arrive, { children: _jsx(VoiceNote, { src: clip.src, mimeType: clip.mimeType }) }, key)))] }));
49
39
  }
50
40
  /**
51
41
  * A tool's output under one Response heading, led by what answers it (figures,
@@ -1,5 +1,5 @@
1
1
  import { type ResultChart } from '@theoremjs/web/client';
2
- /** Each chart in its card, side by side where the column is wide enough. */
3
- export declare function ToolResultCharts({ charts }: {
4
- charts: readonly ResultChart[];
2
+ /** One chart in its card: a series over time as lines, one over names as bars. */
3
+ export declare function ToolResultChart({ chart }: {
4
+ chart: ResultChart;
5
5
  }): import("react").JSX.Element;
@@ -6,8 +6,6 @@ import { VStack } from '@astryxdesign/core/VStack';
6
6
  import { timeFormats, withUnit } from '@theoremjs/web/client';
7
7
  import { useMemo } from 'react';
8
8
  import { Bar, BarChart, Cell, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts';
9
- import { Arrive } from "./arrive.js";
10
- import { CardWall } from "./CardWall.js";
11
9
  import { ChartFrame, ChartTip, FIRST_WIDTH_PX } from "./TraceOverview.js";
12
10
  import { ChartCard, TRACE_GAP, TRACE_RADIUS, TraceSwatch, traceFill, } from "./trace-patterns.js";
13
11
  /**
@@ -96,7 +94,7 @@ function CategoryCard({ chart }) {
96
94
  formatter: (value) => formats.number(Number(value), chart.series.unit),
97
95
  }, children: rows.map((row, index) => (_jsx(Cell, { fill: traceFill(index === 0 ? 'clear' : 'shade2') }, row.id))) })] }) }) }) }));
98
96
  }
99
- /** Each chart in its card, side by side where the column is wide enough. */
100
- export function ToolResultCharts({ charts }) {
101
- return (_jsx(CardWall, { minWidth: 260, children: charts.map((chart) => (_jsx(Arrive, { children: chart.kind === 'time' ? _jsx(TimeCard, { chart: chart }) : _jsx(CategoryCard, { chart: chart }) }, chart.key))) }));
97
+ /** One chart in its card: a series over time as lines, one over names as bars. */
98
+ export function ToolResultChart({ chart }) {
99
+ return chart.kind === 'time' ? _jsx(TimeCard, { chart: chart }) : _jsx(CategoryCard, { chart: chart });
102
100
  }
@@ -1,13 +1,24 @@
1
- import { type TraceTimeSplit, type TraceTurn } from '../client/trace-story.ts';
2
- import { type TraceNode } from '../client/trace-view.ts';
3
- /** Where a turn's time, tokens, and slowest steps went. A bar opens its span. */
4
- export declare function TraceCharts({ root, split, onSelect, }: {
1
+ import { type TraceLatency, type TraceTimeSplit, type TraceTurn } from '../client/trace-story.ts';
2
+ import { type TraceNode, type TraceTotals } from '../client/trace-view.ts';
3
+ /**
4
+ * A turn's figures, then where its time, tokens, and slowest steps went, in
5
+ * one wall. A bar opens its span.
6
+ */
7
+ export declare function TraceCharts({ root, split, totals, tools, latency, onSelect, }: {
5
8
  root: TraceNode;
6
9
  split: TraceTimeSplit;
10
+ totals: TraceTotals;
11
+ tools: number;
12
+ latency?: TraceLatency;
7
13
  onSelect: (node: TraceNode) => void;
8
14
  }): import("react").JSX.Element;
9
- /** The whole conversation: each turn's time and tokens. A column opens its turn. */
10
- export declare function ConversationCharts({ turns, onPick, }: {
15
+ /**
16
+ * The whole conversation: its figures, then each turn's time and tokens, in
17
+ * one wall. A column opens its turn.
18
+ */
19
+ export declare function ConversationCharts({ turns, totals, tools, onPick, }: {
11
20
  turns: readonly TraceTurn[];
21
+ totals: TraceTotals;
22
+ tools: number;
12
23
  onPick: (index: number) => void;
13
24
  }): import("react").JSX.Element;
package/ui/TraceCharts.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Grid } from '@astryxdesign/core/Grid';
3
2
  import { HStack } from '@astryxdesign/core/HStack';
4
3
  import { Text } from '@astryxdesign/core/Text';
5
4
  import { VStack } from '@astryxdesign/core/VStack';
@@ -7,7 +6,8 @@ import { useMemo } from 'react';
7
6
  import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts';
8
7
  import { isCallTrace, traceActor, } from "../client/trace-story.js";
9
8
  import { traceSpans } from "../client/trace-view.js";
10
- import { ChartFrame, ChartTip, FIRST_WIDTH_PX } from "./TraceOverview.js";
9
+ import { CardWall } from "./CardWall.js";
10
+ import { ChartFrame, ChartTip, FIRST_WIDTH_PX, TRACE_CARD_PX, turnStats, } from "./TraceOverview.js";
11
11
  import { useTraceFormat } from "./TraceValues.js";
12
12
  import { ACTOR_FILL, ChartCard, TRACE_GAP, TRACE_RADIUS, TraceSwatch, traceFill, } from "./trace-patterns.js";
13
13
  /** Height of each column chart's plot, in pixels. */
@@ -206,11 +206,14 @@ function SlowestCard({ root, onSelect }) {
206
206
  formatter: (value) => format.duration(Number(value)),
207
207
  }, children: rows.map((row) => (_jsx(Cell, { fill: traceFill(ACTOR_FILL[traceActor(row.node)]) }, row.id))) })] }) }) }) }));
208
208
  }
209
- /** Where a turn's time, tokens, and slowest steps went. A bar opens its span. */
210
- export function TraceCharts({ root, split, onSelect, }) {
211
- const { t } = useTraceFormat();
209
+ /**
210
+ * A turn's figures, then where its time, tokens, and slowest steps went, in
211
+ * one wall. A bar opens its span.
212
+ */
213
+ export function TraceCharts({ root, split, totals, tools, latency, onSelect, }) {
214
+ const format = useTraceFormat();
212
215
  const callRows = useCallRows(root, onSelect);
213
- return (_jsxs(Grid, { columns: { minWidth: 260, repeat: 'fit' }, gap: 3, children: [_jsx(TimeCard, { split: split }), _jsx(TokensCard, { title: t('@theorem.panel.trace.charts.calls'), rows: callRows }), _jsx(SlowestCard, { root: root, onSelect: onSelect })] }));
216
+ return (_jsxs(CardWall, { minWidth: TRACE_CARD_PX, children: [turnStats(format, { totals, tools, latency }), _jsx(TimeCard, { split: split }, "time"), _jsx(TokensCard, { title: format.t('@theorem.panel.trace.charts.calls'), rows: callRows }, "call-tokens"), _jsx(SlowestCard, { root: root, onSelect: onSelect }, "slowest")] }));
214
217
  }
215
218
  function TurnTimeTip({ active, payload }) {
216
219
  const format = useTraceFormat();
@@ -237,9 +240,13 @@ function TurnTimeCard({ rows, isCalls }) {
237
240
  })
238
241
  : undefined, children: [_jsx(Columns, { rows: rows, keys: SPLIT_KEYS, fills: SPLIT_FILL, tip: (props) => _jsx(TurnTimeTip, { ...props }) }), _jsx(SplitLegend, { split: sums, total: total })] }));
239
242
  }
240
- /** The whole conversation: each turn's time and tokens. A column opens its turn. */
241
- export function ConversationCharts({ turns, onPick, }) {
242
- const { t } = useTraceFormat();
243
+ /**
244
+ * The whole conversation: its figures, then each turn's time and tokens, in
245
+ * one wall. A column opens its turn.
246
+ */
247
+ export function ConversationCharts({ turns, totals, tools, onPick, }) {
248
+ const format = useTraceFormat();
249
+ const { t } = format;
243
250
  const isCalls = isCallTrace(turns);
244
251
  const column = isCalls
245
252
  ? '@theorem.panel.trace.charts.hostCall'
@@ -262,5 +269,5 @@ export function ConversationCharts({ turns, onPick, }) {
262
269
  }),
263
270
  open: () => onPick(position),
264
271
  })), [turns, onPick, t, column]);
265
- return (_jsxs(Grid, { columns: { minWidth: 260, repeat: 'fit' }, gap: 3, children: [_jsx(TurnTimeCard, { rows: time, isCalls: isCalls }), _jsx(TokensCard, { title: t('@theorem.panel.trace.charts.turnTokens'), rows: tokens })] }));
272
+ return (_jsxs(CardWall, { minWidth: TRACE_CARD_PX, children: [turnStats(format, { totals, tools }), _jsx(TurnTimeCard, { rows: time, isCalls: isCalls }, "turn-time"), _jsx(TokensCard, { title: t('@theorem.panel.trace.charts.turnTokens'), rows: tokens }, "turn-tokens")] }));
266
273
  }
@@ -13,10 +13,11 @@ import { isCallTrace, traceGuardrails, traceLatency, traceStory, traceTurnRow, t
13
13
  import { TRACE_SEARCH_OPERATORS, TRACE_TEXT_FIELD, traceMatchIds, traceSearchFields, traceSpans, traceTotals, traceTree, } from "../client/trace-view.js";
14
14
  import { useTraceRecords } from "../hooks/use-trace-records.js";
15
15
  import { Stream, useArrive } from "./arrive.js";
16
+ import { CardWall } from "./CardWall.js";
16
17
  import { PaneLayout } from "./SidePanel.js";
17
18
  import { ConversationCharts, TraceCharts } from "./TraceCharts.js";
18
19
  import { TraceGuardrails } from "./TraceGuardrails.js";
19
- import { GuardrailStats, TurnPicker, TurnStats } from "./TraceOverview.js";
20
+ import { GuardrailStat, TRACE_CARD_PX, TurnPicker } from "./TraceOverview.js";
20
21
  import { TraceSpanDetail } from "./TraceSpanDetail.js";
21
22
  import { ActorMark, TraceStory } from "./TraceStory.js";
22
23
  import { useTraceFormat } from "./TraceValues.js";
@@ -124,7 +125,7 @@ function ConversationOverview({ turns, onPick, }) {
124
125
  const isCalls = isCallTrace(turns);
125
126
  return (_jsxs(VStack, { gap: 5, padding: 4, children: [_jsx(Text, { weight: "semibold", children: t(isCalls ? '@theorem.panel.trace.calls.count' : '@theorem.panel.trace.conversation', {
126
127
  count: turns.length,
127
- }) }), _jsxs(VStack, { gap: 3, children: [_jsx(TurnStats, { totals: totals, tools: tools }), _jsx(ConversationCharts, { turns: turns, onPick: onPick })] }), _jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t(isCalls ? '@theorem.panel.trace.calls' : '@theorem.panel.trace.turns') }), _jsx(Stream, { children: _jsx(VStack, { gap: 0, children: turns.map((turn, index) => (_jsx(TurnRow, { turn: turn, index: index, isCall: isCalls, onOpen: () => onPick(index) }, turn.node.id))) }) })] })] }));
128
+ }) }), _jsx(ConversationCharts, { turns: turns, totals: totals, tools: tools, onPick: onPick }), _jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t(isCalls ? '@theorem.panel.trace.calls' : '@theorem.panel.trace.turns') }), _jsx(Stream, { children: _jsx(VStack, { gap: 0, children: turns.map((turn, index) => (_jsx(TurnRow, { turn: turn, index: index, isCall: isCalls, onOpen: () => onPick(index) }, turn.node.id))) }) })] })] }));
128
129
  }
129
130
  /** One turn at a glance, then its story, then its timeline under the search. */
130
131
  function TurnOverview({ turns, index, onPick, onBack, selectedId, onSelect, }) {
@@ -140,7 +141,7 @@ function TurnOverview({ turns, index, onPick, onBack, selectedId, onSelect, }) {
140
141
  if (!turn || !root)
141
142
  return null;
142
143
  const tools = traceSpans([root]).filter((node) => node.meta.type === 'tool').length;
143
- return (_jsxs(VStack, { gap: 5, padding: 4, children: [_jsx(TurnPicker, { turns: turns, index: index, onChange: onPick, onBack: onBack }), _jsxs(VStack, { gap: 3, children: [_jsx(TurnStats, { totals: turn.totals, tools: tools, latency: latency }), _jsx(TraceCharts, { root: root, split: turn.split, onSelect: onSelect })] }), _jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t('@theorem.panel.trace.timeline') }), _jsx(PowerSearch, { config: config, filters: filters, onChange: setFilters, label: t('@theorem.panel.trace.search'), isLabelHidden: true, placeholder: t('@theorem.panel.trace.search.placeholder'), resultCount: matches?.size, size: "sm" }), _jsx(TraceWaterfall, { root: root, selectedId: selectedId, matches: matches, onSelect: onSelect }, root.id)] }), story.length > 0 ? (_jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t('@theorem.panel.trace.story') }), _jsx(TraceStory, { steps: story, selectedId: selectedId, matches: matches, onSelect: onSelect })] })) : null, guardrails.length > 0 ? (_jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t('@theorem.panel.trace.guardrails') }), _jsx(TraceGuardrails, { checks: guardrails, onSelect: onSelect })] })) : null] }));
144
+ return (_jsxs(VStack, { gap: 5, padding: 4, children: [_jsx(TurnPicker, { turns: turns, index: index, onChange: onPick, onBack: onBack }), _jsx(TraceCharts, { root: root, split: turn.split, totals: turn.totals, tools: tools, latency: latency, onSelect: onSelect }), _jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t('@theorem.panel.trace.timeline') }), _jsx(PowerSearch, { config: config, filters: filters, onChange: setFilters, label: t('@theorem.panel.trace.search'), isLabelHidden: true, placeholder: t('@theorem.panel.trace.search.placeholder'), resultCount: matches?.size, size: "sm" }), _jsx(TraceWaterfall, { root: root, selectedId: selectedId, matches: matches, onSelect: onSelect }, root.id)] }), story.length > 0 ? (_jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t('@theorem.panel.trace.story') }), _jsx(TraceStory, { steps: story, selectedId: selectedId, matches: matches, onSelect: onSelect })] })) : null, guardrails.length > 0 ? (_jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t('@theorem.panel.trace.guardrails') }), _jsx(TraceGuardrails, { checks: guardrails, onSelect: onSelect })] })) : null] }));
144
145
  }
145
146
  function EmptyTrace() {
146
147
  const { t } = useTraceFormat();
@@ -202,5 +203,8 @@ export function TraceGuardrailsBody({ records, head, }) {
202
203
  const { t } = useTraceFormat();
203
204
  const roots = useMemo(() => traceTurns(traceTree(records)).map((turn) => turn.node), [records]);
204
205
  const checks = useMemo(() => roots.flatMap(traceGuardrails), [roots]);
205
- return (_jsxs(VStack, { gap: 5, padding: 4, children: [head, roots.map((root) => (_jsx(GuardrailStats, { latency: traceLatency(root) }, root.id))), checks.length > 0 ? (_jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t('@theorem.panel.trace.guardrails') }), _jsx(TraceGuardrails, { checks: checks })] })) : null] }));
206
+ const timed = useMemo(() => roots
207
+ .map((root) => ({ id: root.id, latency: traceLatency(root) }))
208
+ .filter(({ latency }) => latency.guardrails), [roots]);
209
+ return (_jsxs(VStack, { gap: 5, padding: 4, children: [head, timed.length > 0 ? (_jsx(CardWall, { minWidth: TRACE_CARD_PX, children: timed.map(({ id, latency }) => (_jsx(GuardrailStat, { latency: latency }, id))) })) : null, checks.length > 0 ? (_jsxs(VStack, { gap: 2, children: [_jsx(SectionTitle, { title: t('@theorem.panel.trace.guardrails') }), _jsx(TraceGuardrails, { checks: checks })] })) : null] }));
206
210
  }
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type TraceLatency, type TraceTurn } from '../client/trace-story.ts';
3
3
  import type { TraceTotals } from '../client/trace-view.ts';
4
+ import { useTraceFormat } from './TraceValues.tsx';
4
5
  /** Width a chart draws at before it has measured its column: the panel's narrowest. */
5
6
  export declare const FIRST_WIDTH_PX = 280;
6
7
  /**
@@ -23,16 +24,20 @@ export declare function TurnPicker({ turns, index, onChange, onBack, }: {
23
24
  /** Leaves the turn for the whole conversation; absent when there is only the one turn. */
24
25
  onBack?: (() => void) | undefined;
25
26
  }): import("react").JSX.Element | null;
26
- /** A turn's guardrail figures alone, as {@link TurnStats} lays them out. */
27
- export declare function GuardrailStats({ latency }: {
27
+ export type TraceFormat = ReturnType<typeof useTraceFormat>;
28
+ /** Narrowest a card in a trace overview's wall gets: wide enough for a chart's columns. */
29
+ export declare const TRACE_CARD_PX = 260;
30
+ /** A turn's guardrail figure alone, the card {@link turnStats} gives it. */
31
+ export declare function GuardrailStat({ latency }: {
28
32
  latency: TraceLatency;
29
- }): import("react").JSX.Element | null;
33
+ }): import("react").JSX.Element;
30
34
  /**
31
- * Duration, first text, cost, tokens, steps, and guardrails of the open turn;
32
- * a total the trace did not record is left out.
35
+ * Duration, first text, cost, tokens, steps, and guardrails of the open turn,
36
+ * as cards for its overview's wall; a total the trace did not record renders
37
+ * nothing.
33
38
  */
34
- export declare function TurnStats({ totals, tools, latency, }: {
39
+ export declare function turnStats(format: TraceFormat, { totals, tools, latency }: {
35
40
  totals: TraceTotals;
36
41
  tools: number;
37
42
  latency?: TraceLatency;
38
- }): import("react").JSX.Element;
43
+ }): ReactNode[];
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button } from '@astryxdesign/core/Button';
3
3
  import { Card } from '@astryxdesign/core/Card';
4
- import { Grid } from '@astryxdesign/core/Grid';
5
4
  import { HStack } from '@astryxdesign/core/HStack';
6
5
  import { Icon } from '@astryxdesign/core/Icon';
7
6
  import { IconButton } from '@astryxdesign/core/IconButton';
@@ -61,7 +60,7 @@ function Stat({ label, value, detail, }) {
61
60
  .filter(Boolean)
62
61
  .map((line) => (_jsx(Text, { type: "supporting", color: "secondary", hasTabularNumbers: true, children: line }, String(line)))) })] }) }));
63
62
  }
64
- function FirstTextStat({ latency, format }) {
63
+ function FirstTextStat({ latency, format, }) {
65
64
  if (latency?.firstTextMs === undefined)
66
65
  return null;
67
66
  const { t } = format;
@@ -109,18 +108,25 @@ function GuardrailsStat({ latency, format, }) {
109
108
  flagged: guardrails.flagged,
110
109
  }) }));
111
110
  }
112
- /** A turn's guardrail figures alone, as {@link TurnStats} lays them out. */
113
- export function GuardrailStats({ latency }) {
111
+ /** Narrowest a card in a trace overview's wall gets: wide enough for a chart's columns. */
112
+ export const TRACE_CARD_PX = 260;
113
+ /** A turn's guardrail figure alone, the card {@link turnStats} gives it. */
114
+ export function GuardrailStat({ latency }) {
114
115
  const format = useTraceFormat();
115
- if (!latency.guardrails)
116
- return null;
117
- return (_jsx(Grid, { columns: { minWidth: 120, repeat: 'fit' }, gap: 2, children: _jsx(GuardrailsStat, { latency: latency, format: format }) }));
116
+ return _jsx(GuardrailsStat, { latency: latency, format: format });
118
117
  }
119
118
  /**
120
- * Duration, first text, cost, tokens, steps, and guardrails of the open turn;
121
- * a total the trace did not record is left out.
119
+ * Duration, first text, cost, tokens, steps, and guardrails of the open turn,
120
+ * as cards for its overview's wall; a total the trace did not record renders
121
+ * nothing.
122
122
  */
123
- export function TurnStats({ totals, tools, latency, }) {
124
- const format = useTraceFormat();
125
- return (_jsxs(Grid, { columns: { minWidth: 120, repeat: 'fit' }, gap: 2, children: [_jsx(Stat, { label: TRACE_FIELDS.duration.label, value: format.duration(totals.durationMs) }), _jsx(FirstTextStat, { latency: latency, format: format }), _jsx(CostStat, { totals: totals, format: format }), _jsx(TokensStat, { totals: totals, format: format }), _jsx(StepsStat, { totals: totals, tools: tools, format: format }), _jsx(GuardrailsStat, { latency: latency, format: format })] }));
123
+ export function turnStats(format, { totals, tools, latency }) {
124
+ return [
125
+ _jsx(Stat, { label: TRACE_FIELDS.duration.label, value: format.duration(totals.durationMs) }, "duration"),
126
+ _jsx(FirstTextStat, { latency: latency, format: format }, "first-text"),
127
+ _jsx(CostStat, { totals: totals, format: format }, "cost"),
128
+ _jsx(TokensStat, { totals: totals, format: format }, "tokens"),
129
+ _jsx(StepsStat, { totals: totals, tools: tools, format: format }, "steps"),
130
+ _jsx(GuardrailsStat, { latency: latency, format: format }, "guardrails"),
131
+ ];
126
132
  }
@@ -1,8 +1,18 @@
1
1
  /** How many columns at least `minWidth` wide fit across `width`, `gap` apart. Never fewer than one. */
2
2
  export declare function columnCount(width: number, minWidth: number, gap: number): number;
3
+ /** Where one card sits: its column and its distance from the top of the wall. */
4
+ export type WallPlace = {
5
+ column: number;
6
+ top: number;
7
+ };
3
8
  /**
4
- * Each card, in order, into the column that is shortest so far, by the
5
- * cards' heights and the gap under each. A card not measured yet counts as
6
- * no height, so unmeasured cards are dealt out in turn.
9
+ * Each card, in order, under the column that is shortest so far, by the
10
+ * cards' heights and the gap under each, and how tall the wall then is. A card
11
+ * not measured yet counts as no height, so unmeasured cards are dealt out in
12
+ * turn. A card measured at no height renders nothing: it takes no place and no
13
+ * gap.
7
14
  */
8
- export declare function wallColumns(heights: readonly (number | undefined)[], count: number, gap: number): number[][];
15
+ export declare function wallPlaces(heights: readonly (number | undefined)[], count: number, gap: number): {
16
+ places: WallPlace[];
17
+ height: number;
18
+ };
@@ -3,17 +3,20 @@ export function columnCount(width, minWidth, gap) {
3
3
  return Math.max(1, Math.floor((width + gap) / (minWidth + gap)));
4
4
  }
5
5
  /**
6
- * Each card, in order, into the column that is shortest so far, by the
7
- * cards' heights and the gap under each. A card not measured yet counts as
8
- * no height, so unmeasured cards are dealt out in turn.
6
+ * Each card, in order, under the column that is shortest so far, by the
7
+ * cards' heights and the gap under each, and how tall the wall then is. A card
8
+ * not measured yet counts as no height, so unmeasured cards are dealt out in
9
+ * turn. A card measured at no height renders nothing: it takes no place and no
10
+ * gap.
9
11
  */
10
- export function wallColumns(heights, count, gap) {
11
- const columns = Array.from({ length: count }, () => []);
12
+ export function wallPlaces(heights, count, gap) {
12
13
  const tall = Array.from({ length: count }, () => 0);
13
- heights.forEach((height, card) => {
14
- const at = tall.indexOf(Math.min(...tall));
15
- columns[at]?.push(card);
16
- tall[at] = (tall[at] ?? 0) + (height ?? 0) + gap;
14
+ const places = heights.map((height) => {
15
+ const column = tall.indexOf(Math.min(...tall));
16
+ const top = tall[column] ?? 0;
17
+ if (height !== 0)
18
+ tall[column] = top + (height ?? 0) + gap;
19
+ return { column, top };
17
20
  });
18
- return columns;
21
+ return { places, height: Math.max(0, Math.max(...tall) - gap) };
19
22
  }