@theoremjs/react 0.3.4-next.616.g595707c → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/react",
3
- "version": "0.3.4-next.616.g595707c",
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.616.g595707c",
64
- "@theoremjs/web": "^0.3.4-next.616.g595707c"
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": {
@@ -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
- 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))) }) })] })] }));
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 { IconButton } from '@astryxdesign/core/IconButton';
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; Download, then the toggle, on top. */
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 = (_jsxs(SidePanelHeader, { floating: true, children: [_jsx(TraceDownload, { traces: traces }), toggle] }));
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);
@@ -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, and the previous and next turn. */
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 }: {
@@ -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, and the previous and next turn. */
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]