@theoremjs/react 0.3.4-next.606.g8936f75 → 0.3.4-next.610.g6ddf39d
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 +3 -3
- package/ui/CardWall.d.ts +4 -1
- package/ui/CardWall.js +58 -37
- package/ui/ToolResult.js +17 -27
- package/ui/ToolResultCharts.d.ts +3 -3
- package/ui/ToolResultCharts.js +3 -5
- package/ui/TraceCharts.d.ts +17 -6
- package/ui/TraceCharts.js +17 -10
- package/ui/TraceInspector.js +8 -4
- package/ui/TraceOverview.d.ts +12 -7
- package/ui/TraceOverview.js +18 -12
- package/ui/wall-columns.d.ts +14 -4
- package/ui/wall-columns.js +13 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoremjs/react",
|
|
3
|
-
"version": "0.3.4-next.
|
|
3
|
+
"version": "0.3.4-next.610.g6ddf39d",
|
|
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.
|
|
64
|
-
"@theoremjs/web": "^0.3.4-next.
|
|
63
|
+
"@theoremjs/agents": "^0.3.4-next.610.g6ddf39d",
|
|
64
|
+
"@theoremjs/web": "^0.3.4-next.610.g6ddf39d"
|
|
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,
|
|
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,72 @@ 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
|
|
23
|
+
const slots = useRef([]);
|
|
24
|
+
const observer = useRef(null);
|
|
25
|
+
const slotRefs = useRef([]);
|
|
26
|
+
// why: One callback per place, so a re-render does not detach and watch every slot again.
|
|
27
|
+
const slotRef = (at) => {
|
|
28
|
+
slotRefs.current[at] ??= (slot) => {
|
|
29
|
+
slots.current[at] = slot;
|
|
30
|
+
// why: A card that mounts later is measured once it is watched.
|
|
31
|
+
if (slot)
|
|
32
|
+
observer.current?.observe(slot);
|
|
33
|
+
return () => {
|
|
34
|
+
if (slot)
|
|
35
|
+
observer.current?.unobserve(slot);
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
return slotRefs.current[at];
|
|
39
|
+
};
|
|
36
40
|
const [wall, setWall] = useState({ width: 0, gap: 0, heights: [] });
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
const count = columnCount(wall.width, minWidth, wall.gap);
|
|
42
|
+
const columnWidth = (wall.width - wall.gap * (count - 1)) / count;
|
|
43
|
+
const { places, height } = wallPlaces(cards.map((_, at) => wall.heights[at]), count, wall.gap);
|
|
44
|
+
// why: Until the wall is measured, cards stack in flow so the first paint has their heights.
|
|
45
|
+
const isPlaced = wall.width > 0;
|
|
46
|
+
slots.current.length = cards.length;
|
|
41
47
|
useLayoutEffect(() => {
|
|
42
48
|
const node = wallRef.current;
|
|
43
|
-
if (!node
|
|
49
|
+
if (!node)
|
|
44
50
|
return;
|
|
45
51
|
const measure = () => {
|
|
46
|
-
const next =
|
|
52
|
+
const next = {
|
|
53
|
+
width: node.clientWidth,
|
|
54
|
+
gap: Number.parseFloat(getComputedStyle(node).rowGap) || 0,
|
|
55
|
+
heights: slots.current.map((slot) => slot?.offsetHeight),
|
|
56
|
+
};
|
|
47
57
|
setWall((shown) => (sameWall(shown, next) ? shown : next));
|
|
48
58
|
};
|
|
49
59
|
measure();
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
for (const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return () =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
const watch = new ResizeObserver(measure);
|
|
61
|
+
watch.observe(node);
|
|
62
|
+
for (const slot of slots.current)
|
|
63
|
+
if (slot)
|
|
64
|
+
watch.observe(slot);
|
|
65
|
+
observer.current = watch;
|
|
66
|
+
return () => {
|
|
67
|
+
watch.disconnect();
|
|
68
|
+
observer.current = null;
|
|
69
|
+
};
|
|
70
|
+
}, []);
|
|
71
|
+
return (_jsx(VStack, { ref: wallRef, gap: 3, style: isPlaced ? { position: 'relative', height } : undefined, children: cards.map((card, at) => {
|
|
72
|
+
const place = places[at];
|
|
73
|
+
return (_jsx("div", { ref: slotRef(at), style: isPlaced && place
|
|
74
|
+
? {
|
|
75
|
+
position: 'absolute',
|
|
76
|
+
top: place.top,
|
|
77
|
+
left: place.column * (columnWidth + wall.gap),
|
|
78
|
+
width: columnWidth,
|
|
79
|
+
}
|
|
80
|
+
: undefined, children: card }, keyOf(card, at)));
|
|
81
|
+
}) }));
|
|
61
82
|
}
|
package/ui/ToolResult.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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(Suspense, { fallback: _jsx(Arrive, { children: _jsx(Skeleton, { height: CHART_CARD_PX }) }), children: _jsx(Arrive, { 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,
|
package/ui/ToolResultCharts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ResultChart } from '@theoremjs/web/client';
|
|
2
|
-
/**
|
|
3
|
-
export declare function
|
|
4
|
-
|
|
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;
|
package/ui/ToolResultCharts.js
CHANGED
|
@@ -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
|
-
/**
|
|
100
|
-
export function
|
|
101
|
-
return
|
|
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
|
}
|
package/ui/TraceCharts.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
4
|
-
|
|
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
|
-
/**
|
|
10
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
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(
|
|
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
|
-
/**
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
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
|
}
|
package/ui/TraceInspector.js
CHANGED
|
@@ -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 {
|
|
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
|
-
}) }),
|
|
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 }),
|
|
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
|
-
|
|
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
|
}
|
package/ui/TraceOverview.d.ts
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
|
39
|
+
export declare function turnStats(format: TraceFormat, { totals, tools, latency }: {
|
|
35
40
|
totals: TraceTotals;
|
|
36
41
|
tools: number;
|
|
37
42
|
latency?: TraceLatency;
|
|
38
|
-
}):
|
|
43
|
+
}): ReactNode[];
|
package/ui/TraceOverview.js
CHANGED
|
@@ -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
|
-
/**
|
|
113
|
-
export
|
|
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
|
-
|
|
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
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
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
|
}
|
package/ui/wall-columns.d.ts
CHANGED
|
@@ -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,
|
|
5
|
-
* cards' heights and the gap under each
|
|
6
|
-
* no height, so unmeasured cards are dealt out in
|
|
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
|
|
15
|
+
export declare function wallPlaces(heights: readonly (number | undefined)[], count: number, gap: number): {
|
|
16
|
+
places: WallPlace[];
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
package/ui/wall-columns.js
CHANGED
|
@@ -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,
|
|
7
|
-
* cards' heights and the gap under each
|
|
8
|
-
* no height, so unmeasured cards are dealt out in
|
|
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
|
|
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.
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
21
|
+
return { places, height: Math.max(0, Math.max(...tall) - gap) };
|
|
19
22
|
}
|