@theoremjs/react 0.3.4-next.614.g49b2422 → 0.3.4-next.618.g0612421
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/TraceInspector.js +38 -11
- package/ui/TraceInspectorPanel.js +3 -34
- package/ui/TraceOverview.d.ts +3 -2
- package/ui/TraceOverview.js +3 -3
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.618.g0612421",
|
|
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.618.g0612421",
|
|
64
|
+
"@theoremjs/web": "^0.3.4-next.618.g0612421"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"@astryxdesign/core": {
|
package/ui/TraceInspector.js
CHANGED
|
@@ -3,11 +3,12 @@ import { Badge } from '@astryxdesign/core/Badge';
|
|
|
3
3
|
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
|
4
4
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
5
5
|
import { Icon } from '@astryxdesign/core/Icon';
|
|
6
|
+
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
6
7
|
import { Item } from '@astryxdesign/core/Item';
|
|
7
8
|
import { PowerSearch, } from '@astryxdesign/core/PowerSearch';
|
|
8
9
|
import { Text } from '@astryxdesign/core/Text';
|
|
9
10
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
10
|
-
import { IconActivity } from '@tabler/icons-react';
|
|
11
|
+
import { IconActivity, IconDownload } from '@tabler/icons-react';
|
|
11
12
|
import { useMemo, useState } from 'react';
|
|
12
13
|
import { isCallTrace, traceGuardrails, traceLatency, traceStory, traceTurnRow, traceTurns, } from "../client/trace-story.js";
|
|
13
14
|
import { TRACE_SEARCH_OPERATORS, TRACE_TEXT_FIELD, traceMatchIds, traceSearchFields, traceSpans, traceTotals, traceTree, } from "../client/trace-view.js";
|
|
@@ -112,7 +113,7 @@ function TurnRow({ turn, index, isCall, onOpen, }) {
|
|
|
112
113
|
t(isCall ? '@theorem.panel.trace.charts.hostCall' : '@theorem.panel.trace.charts.turn', { index: index + 1 }) }), description: turnRowNote(row), endContent: _jsx(TurnRowTotals, { turn: turn }), align: "start", density: "compact", onClick: onOpen }));
|
|
113
114
|
}
|
|
114
115
|
/** Every turn at once: the conversation's totals, each turn's time and tokens, and the turns themselves. */
|
|
115
|
-
function ConversationOverview({ turns, onPick, }) {
|
|
116
|
+
function ConversationOverview({ turns, onPick, end, }) {
|
|
116
117
|
const { t } = useTraceFormat();
|
|
117
118
|
const nodes = useMemo(() => turns.map((turn) => turn.node), [turns]);
|
|
118
119
|
// why: Time between turns is the person's, not the agent's: the duration sums the turns.
|
|
@@ -122,12 +123,12 @@ function ConversationOverview({ turns, onPick, }) {
|
|
|
122
123
|
}), [nodes, turns]);
|
|
123
124
|
const tools = useMemo(() => traceSpans(nodes).filter((node) => node.meta.type === 'tool').length, [nodes]);
|
|
124
125
|
const isCalls = isCallTrace(turns);
|
|
125
|
-
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
|
-
|
|
126
|
+
return (_jsxs(VStack, { gap: 5, padding: 4, children: [_jsxs(HStack, { gap: 1, align: "center", justify: "between", children: [_jsx(Text, { weight: "semibold", children: t(isCalls ? '@theorem.panel.trace.calls.count' : '@theorem.panel.trace.conversation', {
|
|
127
|
+
count: turns.length,
|
|
128
|
+
}) }), end] }), _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
129
|
}
|
|
129
130
|
/** One turn at a glance, then its story, then its timeline under the search. */
|
|
130
|
-
function TurnOverview({ turns, index, onPick, onBack, selectedId, onSelect, }) {
|
|
131
|
+
function TurnOverview({ turns, index, onPick, onBack, selectedId, onSelect, end, }) {
|
|
131
132
|
const { t } = useTraceFormat();
|
|
132
133
|
const [filters, setFilters] = useState([]);
|
|
133
134
|
const turn = turns[index];
|
|
@@ -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, end: end }), _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
145
|
}
|
|
145
146
|
function EmptyTrace() {
|
|
146
147
|
const { t } = useTraceFormat();
|
|
@@ -171,10 +172,36 @@ function SpanPane({ open, turnStartMs, isWide, onBack, onOpen, }) {
|
|
|
171
172
|
return (_jsx(VStack, { padding: 4, children: _jsx(TraceSpanDetail, { node: open, turnStartMs: turnStartMs, isClose: isWide, onBack: onBack, onOpen: onOpen }) }));
|
|
172
173
|
}
|
|
173
174
|
/** The whole conversation, or the open turn with a way back when there are others. */
|
|
174
|
-
function Overview({ turns, index, pick, selectedId, select, }) {
|
|
175
|
+
function Overview({ turns, index, pick, selectedId, select, end, }) {
|
|
175
176
|
if (index === null)
|
|
176
|
-
return _jsx(ConversationOverview, { turns: turns, onPick: pick });
|
|
177
|
-
return (_jsx(TurnOverview, { turns: turns, index: index, onPick: pick, onBack: turns.length > 1 ? () => pick(null) : undefined, selectedId: selectedId, onSelect: select }));
|
|
177
|
+
return _jsx(ConversationOverview, { turns: turns, onPick: pick, end: end });
|
|
178
|
+
return (_jsx(TurnOverview, { turns: turns, index: index, onPick: pick, onBack: turns.length > 1 ? () => pick(null) : undefined, selectedId: selectedId, onSelect: select, end: end }));
|
|
179
|
+
}
|
|
180
|
+
/** The agent the records ran, which names their file. */
|
|
181
|
+
function tracedAgent(records) {
|
|
182
|
+
for (const record of records) {
|
|
183
|
+
for (const span of record.spans) {
|
|
184
|
+
const name = span.attributes['gen_ai.agent.name'];
|
|
185
|
+
if (typeof name === 'string' && name)
|
|
186
|
+
return name;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return 'trace';
|
|
190
|
+
}
|
|
191
|
+
/** Saves the records on screen as `<agent>-<time>.trace.json`, the file the studio imports. */
|
|
192
|
+
function TraceDownload({ records }) {
|
|
193
|
+
const { t } = useTraceFormat();
|
|
194
|
+
const label = t('@theorem.panel.trace.download');
|
|
195
|
+
return (_jsx(IconButton, { label: label, variant: "ghost", size: "sm", tooltip: label, icon: _jsx(Icon, { icon: IconDownload }), onClick: () => {
|
|
196
|
+
const stamp = new Date().toISOString().replaceAll(':', '-').slice(0, 19);
|
|
197
|
+
const file = new Blob([JSON.stringify(records, null, 2)], { type: 'application/json' });
|
|
198
|
+
const url = URL.createObjectURL(file);
|
|
199
|
+
const link = document.createElement('a');
|
|
200
|
+
link.href = url;
|
|
201
|
+
link.download = `${tracedAgent(records)}-${stamp}.trace.json`;
|
|
202
|
+
link.click();
|
|
203
|
+
URL.revokeObjectURL(url);
|
|
204
|
+
} }));
|
|
178
205
|
}
|
|
179
206
|
/**
|
|
180
207
|
* The panel's content. Narrow, the turn fills it and a span opens in its
|
|
@@ -187,7 +214,7 @@ function TraceInspectorBody({ records, isWide, }) {
|
|
|
187
214
|
const { index, root, open, select, close, pick } = useOpenSpan(turns);
|
|
188
215
|
if (turns.length === 0)
|
|
189
216
|
return _jsx(EmptyTrace, {});
|
|
190
|
-
const overview = (_jsx(Overview, { turns: turns, index: index, pick: pick, selectedId: open?.id, select: select }));
|
|
217
|
+
const overview = (_jsx(Overview, { turns: turns, index: index, pick: pick, selectedId: open?.id, select: select, end: _jsx(TraceDownload, { records: records }) }));
|
|
191
218
|
return (_jsx(PaneLayout, { isWide: isWide, label: t('@theorem.panel.trace.name'), detailLabel: open?.meta.label ?? '', lead: _jsx(TracePatterns, {}), overview: overview, detail: open && (_jsx(SpanPane, { open: open, turnStartMs: root?.startMs ?? 0, isWide: isWide, onBack: close, onOpen: select })) }));
|
|
192
219
|
}
|
|
193
220
|
/** The inspector over the records a feed has delivered; the panel loads this module when it first opens. */
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Icon } from '@astryxdesign/core/Icon';
|
|
3
|
-
import {
|
|
4
|
-
import { IconActivity, IconDownload } from '@tabler/icons-react';
|
|
3
|
+
import { IconActivity } from '@tabler/icons-react';
|
|
5
4
|
import { cloneElement, createContext, isValidElement, lazy, Suspense, use, useCallback, useId, useRef, useState, } from 'react';
|
|
6
|
-
import { useTraceRecords } from "../hooks/use-trace-records.js";
|
|
7
5
|
import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
|
|
8
6
|
import { InPlace, isSidePanelWide, RaisedPane, SidePanel, SidePanelHeader, SidePanelToggle, useSidePanel, useWidth, } from "./SidePanel.js";
|
|
9
7
|
// why: The inspector and its charts are fetched when the trace first opens, not with the chat.
|
|
@@ -45,9 +43,9 @@ export function useTraceInspector(iface, traces, open) {
|
|
|
45
43
|
const view = isOpen ? openTrace(placement === 'panel', id, traces, toggle) : null;
|
|
46
44
|
return { toggle, view, labels, offered: true };
|
|
47
45
|
}
|
|
48
|
-
/** The open trace: filling the host's docked panel, or as its own panel;
|
|
46
|
+
/** The open trace: filling the host's docked panel, or as its own panel; the toggle on top. */
|
|
49
47
|
function openTrace(docked, id, traces, toggle) {
|
|
50
|
-
const header = (
|
|
48
|
+
const header = toggle ? _jsx(SidePanelHeader, { floating: true, children: toggle }) : null;
|
|
51
49
|
if (docked)
|
|
52
50
|
return _jsx(TraceFill, { id: id, header: header, traces: traces });
|
|
53
51
|
return _jsx(TraceView, { id: id, header: header, traces: traces });
|
|
@@ -57,35 +55,6 @@ function TraceFill({ id, header, traces, }) {
|
|
|
57
55
|
const { ref, width } = useWidth();
|
|
58
56
|
return (_jsxs("div", { id: id, ref: ref, style: { height: '100%', position: 'relative' }, children: [header, _jsx(Suspense, { fallback: null, children: _jsx(TraceFeedBody, { traces: traces, isWide: isSidePanelWide(width) }) })] }));
|
|
59
57
|
}
|
|
60
|
-
/** The agent the records ran, which names their file. */
|
|
61
|
-
function tracedAgent(records) {
|
|
62
|
-
for (const record of records) {
|
|
63
|
-
for (const span of record.spans) {
|
|
64
|
-
const name = span.attributes['gen_ai.agent.name'];
|
|
65
|
-
if (typeof name === 'string' && name)
|
|
66
|
-
return name;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return 'trace';
|
|
70
|
-
}
|
|
71
|
-
/** Saves the records on screen as `<agent>-<time>.trace.json`, the file the studio imports. */
|
|
72
|
-
function TraceDownload({ traces }) {
|
|
73
|
-
const t = useLabels();
|
|
74
|
-
const records = useTraceRecords(traces);
|
|
75
|
-
if (records.length === 0)
|
|
76
|
-
return null;
|
|
77
|
-
const label = t('@theorem.panel.trace.download');
|
|
78
|
-
return (_jsx(IconButton, { label: label, variant: "ghost", tooltip: label, icon: _jsx(Icon, { icon: IconDownload, size: "sm" }), onClick: () => {
|
|
79
|
-
const stamp = new Date().toISOString().replaceAll(':', '-').slice(0, 19);
|
|
80
|
-
const file = new Blob([JSON.stringify(records, null, 2)], { type: 'application/json' });
|
|
81
|
-
const url = URL.createObjectURL(file);
|
|
82
|
-
const link = document.createElement('a');
|
|
83
|
-
link.href = url;
|
|
84
|
-
link.download = `${tracedAgent(records)}-${stamp}.trace.json`;
|
|
85
|
-
link.click();
|
|
86
|
-
URL.revokeObjectURL(url);
|
|
87
|
-
} }));
|
|
88
|
-
}
|
|
89
58
|
/** The surface, with the trace docked at its inline end. A host-driven trace stays in place. */
|
|
90
59
|
function TraceDock({ inspector, children }) {
|
|
91
60
|
const layoutRef = useRef(null);
|
package/ui/TraceOverview.d.ts
CHANGED
|
@@ -15,13 +15,14 @@ export declare function ChartFrame({ children }: {
|
|
|
15
15
|
export declare function ChartTip({ children }: {
|
|
16
16
|
children: ReactNode;
|
|
17
17
|
}): import("react").JSX.Element;
|
|
18
|
-
/** Which turn is open: its name,
|
|
19
|
-
export declare function TurnPicker({ turns, index, onChange, onBack, }: {
|
|
18
|
+
/** Which turn is open: its name, the previous and next turn, then `end`. */
|
|
19
|
+
export declare function TurnPicker({ turns, index, onChange, onBack, end, }: {
|
|
20
20
|
turns: readonly TraceTurn[];
|
|
21
21
|
index: number;
|
|
22
22
|
onChange: (index: number) => void;
|
|
23
23
|
/** Leaves the turn for the whole conversation; absent when there is only the one turn. */
|
|
24
24
|
onBack?: (() => void) | undefined;
|
|
25
|
+
end?: ReactNode;
|
|
25
26
|
}): import("react").JSX.Element | null;
|
|
26
27
|
/** A turn's guardrail figures alone, as {@link TurnStats} lays them out. */
|
|
27
28
|
export declare function GuardrailStats({ latency }: {
|
package/ui/TraceOverview.js
CHANGED
|
@@ -42,8 +42,8 @@ export function ChartTip({ children }) {
|
|
|
42
42
|
pointerEvents: 'none',
|
|
43
43
|
}, children: children }));
|
|
44
44
|
}
|
|
45
|
-
/** Which turn is open: its name,
|
|
46
|
-
export function TurnPicker({ turns, index, onChange, onBack, }) {
|
|
45
|
+
/** Which turn is open: its name, the previous and next turn, then `end`. */
|
|
46
|
+
export function TurnPicker({ turns, index, onChange, onBack, end, }) {
|
|
47
47
|
const { t } = useTraceFormat();
|
|
48
48
|
const turn = turns[index];
|
|
49
49
|
if (!turn)
|
|
@@ -53,7 +53,7 @@ export function TurnPicker({ turns, index, onChange, onBack, }) {
|
|
|
53
53
|
label: turn.node.meta.label,
|
|
54
54
|
index: index + 1,
|
|
55
55
|
count: turns.length,
|
|
56
|
-
}) }), turn.node.meta.subject ? (_jsx(Text, { color: "secondary", maxLines: 1, children: turn.node.meta.subject })) : null] }), _jsxs(HStack, { gap: 0, children: [_jsx(IconButton, { label: t(isCalls ? '@theorem.panel.trace.calls.previous' : '@theorem.panel.trace.previous'), icon: _jsx(Icon, { icon: IconChevronLeft }), variant: "ghost", size: "sm", isDisabled: index === 0, onClick: () => onChange(index - 1) }), _jsx(IconButton, { label: t(isCalls ? '@theorem.panel.trace.calls.next' : '@theorem.panel.trace.next'), icon: _jsx(Icon, { icon: IconChevronRight }), variant: "ghost", size: "sm", isDisabled: index === turns.length - 1, onClick: () => onChange(index + 1) })] })] })] }));
|
|
56
|
+
}) }), turn.node.meta.subject ? (_jsx(Text, { color: "secondary", maxLines: 1, children: turn.node.meta.subject })) : null] }), _jsxs(HStack, { gap: 0, children: [_jsx(IconButton, { label: t(isCalls ? '@theorem.panel.trace.calls.previous' : '@theorem.panel.trace.previous'), icon: _jsx(Icon, { icon: IconChevronLeft }), variant: "ghost", size: "sm", isDisabled: index === 0, onClick: () => onChange(index - 1) }), _jsx(IconButton, { label: t(isCalls ? '@theorem.panel.trace.calls.next' : '@theorem.panel.trace.next'), icon: _jsx(Icon, { icon: IconChevronRight }), variant: "ghost", size: "sm", isDisabled: index === turns.length - 1, onClick: () => onChange(index + 1) }), end] })] })] }));
|
|
57
57
|
}
|
|
58
58
|
function Stat({ label, value, detail, }) {
|
|
59
59
|
return (_jsx(Card, { padding: 3, variant: "muted", style: { background: 'var(--color-background-surface)' }, children: _jsxs(VStack, { gap: 1, children: [_jsx(Text, { type: "supporting", color: "secondary", children: label }), _jsx(Text, { type: "large", weight: "semibold", hasTabularNumbers: true, children: value }), _jsx(VStack, { gap: 0, children: [detail]
|