@theoremjs/react 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -2
  3. package/client/attachment-hover-preview.d.ts +11 -0
  4. package/client/attachment-hover-preview.js +36 -0
  5. package/client/audio-level.d.ts +5 -0
  6. package/client/audio-level.js +24 -0
  7. package/client/composer-attachments.d.ts +30 -0
  8. package/client/composer-attachments.js +33 -0
  9. package/client/composer-drawer.d.ts +19 -0
  10. package/client/composer-drawer.js +15 -0
  11. package/client/composer-hints.d.ts +25 -0
  12. package/client/composer-hints.js +20 -0
  13. package/client/composer-primary.d.ts +16 -0
  14. package/client/composer-primary.js +25 -0
  15. package/client/decision-transport.d.ts +48 -0
  16. package/client/decision-transport.js +102 -0
  17. package/client/decode-composer-draft.d.ts +11 -0
  18. package/client/decode-composer-draft.js +12 -0
  19. package/client/encode-composer-draft.d.ts +9 -0
  20. package/client/encode-composer-draft.js +34 -0
  21. package/client/encode-files.d.ts +18 -0
  22. package/client/encode-files.js +54 -0
  23. package/client/failure.d.ts +32 -0
  24. package/client/failure.js +34 -0
  25. package/client/generation-selection.d.ts +15 -0
  26. package/client/generation-selection.js +24 -0
  27. package/client/host-call.d.ts +51 -0
  28. package/client/host-call.js +42 -0
  29. package/client/host-transport.d.ts +68 -0
  30. package/client/host-transport.js +74 -0
  31. package/client/image-output.d.ts +6 -0
  32. package/client/image-output.js +13 -0
  33. package/client/index.d.ts +20 -0
  34. package/client/index.js +18 -0
  35. package/client/ink-waveform.d.ts +17 -0
  36. package/client/ink-waveform.js +69 -0
  37. package/client/live/apply-live-turn-tool-event.d.ts +14 -0
  38. package/client/live/apply-live-turn-tool-event.js +39 -0
  39. package/client/live/live-captions.d.ts +23 -0
  40. package/client/live/live-captions.js +70 -0
  41. package/client/live/live-mic-forward.d.ts +29 -0
  42. package/client/live/live-mic-forward.js +37 -0
  43. package/client/live/live-state.d.ts +11 -0
  44. package/client/live/live-state.js +20 -0
  45. package/client/live/live-tool.d.ts +11 -0
  46. package/client/live/live-tool.js +1 -0
  47. package/client/live/live-video.d.ts +10 -0
  48. package/client/live/live-video.js +71 -0
  49. package/client/live/run-live-tool-call.d.ts +17 -0
  50. package/client/live/run-live-tool-call.js +32 -0
  51. package/client/live-client.d.ts +141 -0
  52. package/client/live-client.js +638 -0
  53. package/client/live-errors.d.ts +2 -0
  54. package/client/live-errors.js +9 -0
  55. package/client/live-messages.d.ts +110 -0
  56. package/client/live-messages.js +67 -0
  57. package/client/mic-capture.d.ts +4 -0
  58. package/client/mic-capture.js +6 -0
  59. package/client/oauth-popup.d.ts +26 -0
  60. package/client/oauth-popup.js +30 -0
  61. package/client/pcm-downsample.d.ts +4 -0
  62. package/client/pcm-downsample.js +45 -0
  63. package/client/run-commit.d.ts +61 -0
  64. package/client/run-commit.js +124 -0
  65. package/client/run-session.d.ts +80 -0
  66. package/client/run-session.js +215 -0
  67. package/client/schema-fields.d.ts +63 -0
  68. package/client/schema-fields.js +178 -0
  69. package/client/shaped-data.d.ts +118 -0
  70. package/client/shaped-data.js +314 -0
  71. package/client/source-citations.d.ts +13 -0
  72. package/client/source-citations.js +56 -0
  73. package/client/state-fields.d.ts +16 -0
  74. package/client/state-fields.js +44 -0
  75. package/client/tool-result.d.ts +78 -0
  76. package/client/tool-result.js +275 -0
  77. package/client/tool-resume.d.ts +33 -0
  78. package/client/tool-resume.js +11 -0
  79. package/client/trace-feed.d.ts +13 -0
  80. package/client/trace-feed.js +18 -0
  81. package/client/trace-story.d.ts +163 -0
  82. package/client/trace-story.js +416 -0
  83. package/client/trace-view.d.ts +130 -0
  84. package/client/trace-view.js +293 -0
  85. package/client/transcript-groups.d.ts +114 -0
  86. package/client/transcript-groups.js +167 -0
  87. package/client/transcript-scroll.d.ts +7 -0
  88. package/client/transcript-scroll.js +6 -0
  89. package/client/transport.d.ts +176 -0
  90. package/client/transport.js +256 -0
  91. package/client/turn-client.d.ts +35 -0
  92. package/client/turn-client.js +93 -0
  93. package/client/voice-label.d.ts +2 -0
  94. package/client/voice-label.js +15 -0
  95. package/client/voice-recorder.d.ts +27 -0
  96. package/client/voice-recorder.js +165 -0
  97. package/client/walk-away.d.ts +34 -0
  98. package/client/walk-away.js +48 -0
  99. package/client/wire-line.d.ts +56 -0
  100. package/client/wire-line.js +90 -0
  101. package/client/worklets/mic-capture.js +16 -0
  102. package/components/InkWaveform.d.ts +10 -0
  103. package/components/InkWaveform.js +108 -0
  104. package/components/live/use-live-runner-controls.d.ts +48 -0
  105. package/components/live/use-live-runner-controls.js +100 -0
  106. package/components/live/use-live-runner-model.d.ts +45 -0
  107. package/components/live/use-live-runner-model.js +175 -0
  108. package/components/live/use-live-runner-ui.d.ts +65 -0
  109. package/components/live/use-live-runner-ui.js +159 -0
  110. package/components/live/use-live-session-client.d.ts +41 -0
  111. package/components/live/use-live-session-client.js +123 -0
  112. package/components/use-composer-voice.d.ts +28 -0
  113. package/components/use-composer-voice.js +90 -0
  114. package/components/use-editor-selection.d.ts +3 -0
  115. package/components/use-editor-selection.js +16 -0
  116. package/components/use-voice-playback.d.ts +16 -0
  117. package/components/use-voice-playback.js +68 -0
  118. package/hooks/use-described.d.ts +8 -0
  119. package/hooks/use-described.js +23 -0
  120. package/hooks/use-theorem-chat-actions.d.ts +75 -0
  121. package/hooks/use-theorem-chat-actions.js +265 -0
  122. package/hooks/use-theorem-chat-state.d.ts +66 -0
  123. package/hooks/use-theorem-chat-state.js +118 -0
  124. package/hooks/use-theorem-chat.d.ts +63 -0
  125. package/hooks/use-theorem-chat.js +340 -0
  126. package/hooks/use-theorem-decision.d.ts +23 -0
  127. package/hooks/use-theorem-decision.js +75 -0
  128. package/hooks/use-theorem-host.d.ts +44 -0
  129. package/hooks/use-theorem-host.js +117 -0
  130. package/hooks/use-theorem-interface.d.ts +19 -0
  131. package/hooks/use-theorem-interface.js +25 -0
  132. package/hooks/use-trace-records.d.ts +4 -0
  133. package/hooks/use-trace-records.js +7 -0
  134. package/index.d.ts +14 -0
  135. package/index.js +14 -0
  136. package/live.d.ts +8 -0
  137. package/live.js +10 -0
  138. package/package.json +73 -4
  139. package/server/credential-store.d.ts +24 -0
  140. package/server/credential-store.js +14 -0
  141. package/server/decision-handler.d.ts +34 -0
  142. package/server/decision-handler.js +55 -0
  143. package/server/handler.d.ts +114 -0
  144. package/server/handler.js +568 -0
  145. package/server/json-response.d.ts +2 -0
  146. package/server/json-response.js +7 -0
  147. package/server/mod.d.ts +16 -0
  148. package/server/mod.js +15 -0
  149. package/server/request-check.d.ts +16 -0
  150. package/server/request-check.js +34 -0
  151. package/server/session-store.d.ts +74 -0
  152. package/server/session-store.js +49 -0
  153. package/server/steer-inbox.d.ts +35 -0
  154. package/server/steer-inbox.js +52 -0
  155. package/server/walk-away.d.ts +22 -0
  156. package/server/walk-away.js +41 -0
  157. package/styles/ink-controls.css +134 -0
  158. package/ui/ChatComposerBar.d.ts +39 -0
  159. package/ui/ChatComposerBar.js +251 -0
  160. package/ui/ChatTranscript.d.ts +30 -0
  161. package/ui/ChatTranscript.js +474 -0
  162. package/ui/ConsoleFrame.d.ts +47 -0
  163. package/ui/ConsoleFrame.js +46 -0
  164. package/ui/DecisionAnswers.d.ts +15 -0
  165. package/ui/DecisionAnswers.js +102 -0
  166. package/ui/LiveRunner.d.ts +28 -0
  167. package/ui/LiveRunner.js +193 -0
  168. package/ui/SchemaFields.d.ts +10 -0
  169. package/ui/SchemaFields.js +122 -0
  170. package/ui/ShapedData.d.ts +11 -0
  171. package/ui/ShapedData.js +151 -0
  172. package/ui/SidePanel.d.ts +57 -0
  173. package/ui/SidePanel.js +50 -0
  174. package/ui/StateFields.d.ts +36 -0
  175. package/ui/StateFields.js +96 -0
  176. package/ui/TheoremChat.d.ts +71 -0
  177. package/ui/TheoremChat.js +146 -0
  178. package/ui/TheoremDecision.d.ts +38 -0
  179. package/ui/TheoremDecision.js +87 -0
  180. package/ui/TheoremHost.d.ts +38 -0
  181. package/ui/TheoremHost.js +150 -0
  182. package/ui/ToolGateCard.d.ts +28 -0
  183. package/ui/ToolGateCard.js +100 -0
  184. package/ui/ToolResult.d.ts +10 -0
  185. package/ui/ToolResult.js +60 -0
  186. package/ui/ToolResultCharts.d.ts +5 -0
  187. package/ui/ToolResultCharts.js +86 -0
  188. package/ui/TraceCharts.d.ts +13 -0
  189. package/ui/TraceCharts.js +235 -0
  190. package/ui/TraceGuardrails.d.ts +10 -0
  191. package/ui/TraceGuardrails.js +56 -0
  192. package/ui/TraceInspector.d.ts +6 -0
  193. package/ui/TraceInspector.js +189 -0
  194. package/ui/TraceInspectorPanel.d.ts +24 -0
  195. package/ui/TraceInspectorPanel.js +57 -0
  196. package/ui/TraceOverview.d.ts +34 -0
  197. package/ui/TraceOverview.js +96 -0
  198. package/ui/TraceSpanDetail.d.ts +10 -0
  199. package/ui/TraceSpanDetail.js +188 -0
  200. package/ui/TraceStory.d.ts +20 -0
  201. package/ui/TraceStory.js +107 -0
  202. package/ui/TraceValues.d.ts +36 -0
  203. package/ui/TraceValues.js +113 -0
  204. package/ui/TraceWaterfall.d.ts +13 -0
  205. package/ui/TraceWaterfall.js +132 -0
  206. package/ui/VoiceNote.d.ts +12 -0
  207. package/ui/VoiceNote.js +28 -0
  208. package/ui/built/theme.css +774 -0
  209. package/ui/built/theorem.d.ts +13 -0
  210. package/ui/built/theorem.js +631 -0
  211. package/ui/built/theorem.variants.d.ts +16 -0
  212. package/ui/disclosure-motion.d.ts +6 -0
  213. package/ui/disclosure-motion.js +163 -0
  214. package/ui/icons.d.ts +4 -0
  215. package/ui/icons.js +37 -0
  216. package/ui/index.d.ts +25 -0
  217. package/ui/index.js +29 -0
  218. package/ui/labels-provider.d.ts +16 -0
  219. package/ui/labels-provider.js +61 -0
  220. package/ui/labels.d.ts +1153 -0
  221. package/ui/labels.js +603 -0
  222. package/ui/theme.d.ts +10 -0
  223. package/ui/theme.js +13 -0
  224. package/ui/theorem-theme.d.ts +10 -0
  225. package/ui/theorem-theme.js +83 -0
  226. package/ui/trace-palette.d.ts +24 -0
  227. package/ui/trace-palette.js +24 -0
  228. package/ui/trace-patterns.d.ts +32 -0
  229. package/ui/trace-patterns.js +44 -0
  230. package/ui/transcript-copy-text.d.ts +4 -0
  231. package/ui/transcript-copy-text.js +39 -0
@@ -0,0 +1,102 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Grid } from '@astryxdesign/core/Grid';
3
+ import { HStack } from '@astryxdesign/core/HStack';
4
+ import { Text } from '@astryxdesign/core/Text';
5
+ import { VStack } from '@astryxdesign/core/VStack';
6
+ import { useLocale } from '@astryxdesign/core/i18n';
7
+ import { useMemo } from 'react';
8
+ import { workDuration } from "./labels.js";
9
+ import { ChartCard, TRACE_RADIUS, traceFill } from "./trace-patterns.js";
10
+ import { useLabels } from "./labels-provider.js";
11
+ const EASE = 'var(--duration-medium) var(--ease-standard)';
12
+ /** Height of a share bar, as the trace draws one. */
13
+ const SHARE_PX = 12;
14
+ function useNumbers() {
15
+ const locale = useLocale();
16
+ return useMemo(() => ({
17
+ percent: new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 0 }),
18
+ decimal: new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }),
19
+ usd: new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD', maximumSignificantDigits: 2 }),
20
+ }), [locale]);
21
+ }
22
+ /** The pick in the trace's clear series, the rest in its shade. */
23
+ function answerFill(isLead) {
24
+ return traceFill(isLead ? 'clear' : 'shade2');
25
+ }
26
+ /** The option's share as a bar, drawn like the trace's. */
27
+ function Share({ value, isLead }) {
28
+ return (_jsx("div", { style: {
29
+ height: SHARE_PX,
30
+ width: `${String(Math.max(value, 0.01) * 100)}%`,
31
+ borderRadius: TRACE_RADIUS,
32
+ background: answerFill(isLead),
33
+ transition: `width ${EASE}`,
34
+ } }));
35
+ }
36
+ function ChoiceAnswer({ title, answer }) {
37
+ const t = useLabels();
38
+ const n = useNumbers();
39
+ const options = Object.entries(answer.probabilities).sort(([, a], [, b]) => b - a);
40
+ return (_jsx(ChartCard, { title: title, value: answer.choice, note: t('@theorem.decision.confidence', { percent: n.percent.format(answer.confidence) }), children: _jsx(VStack, { gap: 2, role: "list", children: options.map(([label, p]) => (_jsxs(VStack, { gap: 1, role: "listitem", "aria-label": t('@theorem.decision.option', { label, percent: n.percent.format(p) }), children: [_jsxs(HStack, { gap: 2, hAlign: "between", "aria-hidden": true, children: [_jsx(Text, { type: "supporting", color: "secondary", maxLines: 1, children: label }), _jsx(Text, { type: "supporting", hasTabularNumbers: true, children: n.percent.format(p) })] }), _jsx(Share, { value: p, isLead: label === answer.choice })] }, label))) }) }));
41
+ }
42
+ const SCALE_HEIGHT = 56;
43
+ /** A level's short name: the words before its colon or dash ("Low: reversible…" → "Low"), when there are few. */
44
+ function levelName(label) {
45
+ const head = label.split(/[:—–]\s/)[0]?.trim() ?? '';
46
+ return head && head.length <= 32 && head !== label ? head : label;
47
+ }
48
+ /** Each level's probability as a column, with a mark where the score falls between them. */
49
+ function ScoreAnswer({ title, answer, labels }) {
50
+ const t = useLabels();
51
+ const n = useNumbers();
52
+ const levels = labels.length;
53
+ const nearest = Math.min(levels - 1, Math.max(0, Math.round(answer.score)));
54
+ return (_jsx(ChartCard, { title: title, value: labels[nearest] === undefined ? n.decimal.format(answer.score) : levelName(labels[nearest]), note: [
55
+ t('@theorem.decision.score_of', { score: n.decimal.format(answer.score), max: n.decimal.format(levels - 1) }),
56
+ t('@theorem.decision.confidence', { percent: n.percent.format(answer.confidence) }),
57
+ ].join(' · '), children: _jsxs(VStack, { gap: 1, children: [_jsxs("div", { "aria-hidden": true, style: { position: 'relative', display: 'flex', alignItems: 'flex-end', gap: 4, height: SCALE_HEIGHT }, children: [labels.map((label, level) => (_jsx("div", { style: {
58
+ flex: 1,
59
+ height: `${String(Math.max(0.04, answer.probabilities[String(level)] ?? 0) * 100)}%`,
60
+ borderRadius: TRACE_RADIUS,
61
+ background: answerFill(level === nearest),
62
+ transition: `height ${EASE}`,
63
+ } }, `${String(level)}:${label}`))), _jsx("div", { style: {
64
+ position: 'absolute',
65
+ insetBlock: -4,
66
+ left: `${String(((answer.score + 0.5) / levels) * 100)}%`,
67
+ width: 0,
68
+ borderInlineStart: '1.5px dashed var(--color-text-secondary)',
69
+ transition: `left ${EASE}`,
70
+ } })] }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: labels.map((label, level) => (_jsx("div", { style: { flex: 1, minWidth: 0 }, title: label, children: _jsx(Text, { type: "supporting", color: "secondary", maxLines: 1, children: levelName(label) }) }, `${String(level)}:${label}`))) }), _jsx("span", { style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clipPath: 'inset(50%)' }, children: labels.map((label, level) => t('@theorem.decision.option', { label, percent: n.percent.format(answer.probabilities[String(level)] ?? 0) })).join('; ') })] }) }));
71
+ }
72
+ function NumberAnswer({ title, answer }) {
73
+ const n = useNumbers();
74
+ return _jsx(ChartCard, { title: title, value: n.decimal.format(answer.noul) });
75
+ }
76
+ /** One question's answer in the trace's chart card, titled by the question and its kind. */
77
+ function AnswerCard({ question, answer }) {
78
+ const t = useLabels();
79
+ const title = `${question.id} · ${t(`@theorem.decision.type.${question.type}`)}`;
80
+ if (answer?.type === 'choice')
81
+ return _jsx(ChoiceAnswer, { title: title, answer: answer });
82
+ if (answer?.type === 'score')
83
+ return _jsx(ScoreAnswer, { title: title, answer: answer, labels: question.labels });
84
+ if (answer?.type === 'noul')
85
+ return _jsx(NumberAnswer, { title: title, answer: answer });
86
+ return null;
87
+ }
88
+ /**
89
+ * A decision's answers, one card per question in the order the profile asks
90
+ * them, and one quiet line for the model, time, tokens, and cost.
91
+ */
92
+ export function TheoremDecisionAnswers({ iface, result, elapsedMs, isStale }) {
93
+ const t = useLabels();
94
+ const n = useNumbers();
95
+ const meta = [
96
+ result.model,
97
+ elapsedMs == null ? null : workDuration(t, elapsedMs),
98
+ result.usage ? t('@theorem.decision.tokens', { count: result.usage.inputTokens }) : null,
99
+ result.usage?.costUsd === undefined ? null : n.usd.format(result.usage.costUsd),
100
+ ].filter(Boolean);
101
+ return (_jsxs(VStack, { gap: 3, style: { opacity: isStale ? 0.5 : 1, transition: `opacity ${EASE}` }, "aria-busy": isStale, children: [_jsx(Grid, { columns: { minWidth: 260, repeat: 'fill' }, gap: 3, children: iface.questions.map((question) => (_jsx(AnswerCard, { question: question, answer: result.answers[question.id] }, question.id))) }), _jsx(Text, { type: "supporting", color: "secondary", hasTabularNumbers: true, children: meta.join(' · ') })] }));
102
+ }
@@ -0,0 +1,28 @@
1
+ import type { DefinedTheme } from '@astryxdesign/core/theme';
2
+ import type { LiveProfileInterface } from '@theoremjs/agents/interface';
3
+ import type { LiveConnection } from '../client/live-client.ts';
4
+ import { type TheoremLabels } from './labels.ts';
5
+ export type LiveRunnerProps = {
6
+ iface: LiveProfileInterface;
7
+ /** What the call opens: a profile the host registered, or an open message for the relay. */
8
+ connection: () => LiveConnection | Promise<LiveConnection>;
9
+ /** Astryx theme, as on `TheoremChat`. Omit to inherit the host's `<Theme>` or use `theoremTheme`. */
10
+ theme?: DefinedTheme;
11
+ mode?: 'system' | 'light' | 'dark';
12
+ /** Replacement lines by locale, as on `TheoremChat`. */
13
+ labels?: TheoremLabels;
14
+ /**
15
+ * Show the trace in place of the call, from the host's own control; the
16
+ * built-in trace toggle then hides. Omit to keep the toggle. Needs a profile
17
+ * that records traces.
18
+ */
19
+ trace?: boolean;
20
+ };
21
+ /**
22
+ * Live (voice / video) runner on Astryx. Before a call: a landing like the
23
+ * chat's, offering a voice or video call. In a call: the agent tag and state
24
+ * over the ink waveform, captions in a right-hand chat column, call controls
25
+ * in a toolbar, and tool gates in a dialog. Offers the trace inspector when
26
+ * the profile records traces.
27
+ */
28
+ export declare function LiveRunner({ theme, mode, labels, ...props }: LiveRunnerProps): import("react").JSX.Element;
@@ -0,0 +1,193 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { AspectRatio } from '@astryxdesign/core/AspectRatio';
3
+ import { Banner } from '@astryxdesign/core/Banner';
4
+ import { Button } from '@astryxdesign/core/Button';
5
+ import { Center } from '@astryxdesign/core/Center';
6
+ import { ChatComposer, ChatComposerInput, ChatLayout, ChatMessage, ChatMessageBubble, ChatMessageList, ChatSystemMessage, ChatSendButton, } from '@astryxdesign/core/Chat';
7
+ import { Dialog } from '@astryxdesign/core/Dialog';
8
+ import { EmptyState } from '@astryxdesign/core/EmptyState';
9
+ import { HStack } from '@astryxdesign/core/HStack';
10
+ import { Icon } from '@astryxdesign/core/Icon';
11
+ import { IconButton } from '@astryxdesign/core/IconButton';
12
+ import { Layout, LayoutContent } from '@astryxdesign/core/Layout';
13
+ import { StackItem } from '@astryxdesign/core/Stack';
14
+ import { Text } from '@astryxdesign/core/Text';
15
+ import { ToggleButton } from '@astryxdesign/core/ToggleButton';
16
+ import { Toolbar } from '@astryxdesign/core/Toolbar';
17
+ import { VStack } from '@astryxdesign/core/VStack';
18
+ import { IconCameraRotate, IconMessage, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneOff, IconSubtitles, IconVideo, IconVideoOff, } from '@tabler/icons-react';
19
+ import { useEffect, useLayoutEffect, useRef } from 'react';
20
+ import { InkWaveform } from "../components/InkWaveform.js";
21
+ import { useLiveRunnerModel } from "../components/live/use-live-runner-model.js";
22
+ import { NO_FOCUS_RING } from "./ChatComposerBar.js";
23
+ import { liveStateLabel } from "./labels.js";
24
+ import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
25
+ import { SidePanel, SidePanelHeader, SidePanelToggle, useSidePanel } from "./SidePanel.js";
26
+ import { ApprovalCard, AuthChallengeCard } from "./ToolGateCard.js";
27
+ import { DEFAULT_CHAT_MAX_WIDTH } from "./TheoremChat.js";
28
+ import { TheoremThemeProvider } from "./theme.js";
29
+ import { useTraceInspector, WithTrace } from "./TraceInspectorPanel.js";
30
+ /** The waveform's box: it rests on the stage floor rather than filling the stage. */
31
+ const WAVE_MAX_WIDTH = 720;
32
+ const WAVE_HEIGHT = 320;
33
+ /**
34
+ * Live (voice / video) runner on Astryx. Before a call: a landing like the
35
+ * chat's, offering a voice or video call. In a call: the agent tag and state
36
+ * over the ink waveform, captions in a right-hand chat column, call controls
37
+ * in a toolbar, and tool gates in a dialog. Offers the trace inspector when
38
+ * the profile records traces.
39
+ */
40
+ export function LiveRunner({ theme, mode, labels, ...props }) {
41
+ return (_jsx(TheoremThemeProvider, { theme: theme, mode: mode, children: _jsx(TheoremLabelsProvider, { labels: labels, children: _jsx(LiveRunnerBody, { ...props }) }) }));
42
+ }
43
+ function LiveRunnerBody({ iface, connection, trace }) {
44
+ const t = useLabels();
45
+ const model = useLiveRunnerModel(iface, connection);
46
+ // Both panels size against the whole live layout, so a third means the same for each.
47
+ const layoutRef = useRef(null);
48
+ const inspector = useTraceInspector(iface, model.traces, trace);
49
+ const captions = useSidePanel(layoutRef, true);
50
+ const captionLabels = {
51
+ name: t('@theorem.panel.captions.name'),
52
+ show: t('@theorem.panel.captions.show'),
53
+ hide: t('@theorem.panel.captions.hide'),
54
+ resize: t('@theorem.panel.captions.resize'),
55
+ };
56
+ const captionsToggle = (_jsx(SidePanelToggle, { labels: captionLabels, icon: _jsx(Icon, { icon: IconMessage }), panelId: captions.id, open: captions.open, onToggle: captions.toggle }));
57
+ if (!model.callStarted) {
58
+ return (_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", header: _jsx(SidePanelHeader, { children: inspector.toggle }), content: _jsx(LayoutContent, { padding: 0, children: _jsx(LiveLanding, { model: model }) }) }) }));
59
+ }
60
+ return (_jsxs(_Fragment, { children: [_jsx(WithTrace, { inspector: inspector, children: _jsx(Layout, { ref: layoutRef, height: "fill", header: _jsxs(SidePanelHeader, { children: [captionsToggle, inspector.toggle] }), content: _jsx(LayoutContent, { padding: 0, children: _jsx(Layout, { height: "fill", end: _jsx(SidePanel, { id: captions.id, labels: captionLabels, resizable: captions.resizable, open: captions.open, padding: 0, children: _jsx(LiveCaptions, { model: model }) }), content: _jsx(LayoutContent, { padding: 0, children: _jsx(VStack, { height: "100%", paddingInline: 3, paddingBlockEnd: 3, children: _jsx(LiveStage, { model: model }) }) }) }) }) }) }), _jsx(LiveToolGateDialog, { prompt: model.gatePrompt, onResolve: model.resolveGateDecision })] }));
61
+ }
62
+ /**
63
+ * Before a call: the chat landing's greeting, with call buttons where the
64
+ * composer would be. Video starts camera and mic together.
65
+ */
66
+ function LiveLanding({ model }) {
67
+ const t = useLabels();
68
+ return (_jsx(VStack, { minHeight: "100%", vAlign: "center", gap: 8, paddingInline: 3, children: _jsx(Center, { axis: "horizontal", width: "100%", children: _jsxs(VStack, { width: "100%", maxWidth: DEFAULT_CHAT_MAX_WIDTH, gap: 8, children: [_jsxs(VStack, { gap: 1, children: [_jsx(Text, { type: "large", as: "h2", children: t('@theorem.agent.handle', { handle: model.handle }) }), _jsx(Text, { type: "display-2", as: "h1", children: t('@theorem.live.greeting') })] }), _jsxs(HStack, { gap: 2, wrap: "wrap", children: [_jsx(Button, { label: t(model.voiceAvailable ? '@theorem.live.start_voice_call' : '@theorem.live.start_call'), variant: "primary", icon: _jsx(Icon, { icon: IconPhone }), onClick: () => model.startCall({ video: false }) }), model.videoAvailable ? (_jsx(Button, { label: t('@theorem.live.start_video_call'), variant: "secondary", icon: _jsx(Icon, { icon: IconVideo }), onClick: () => model.startCall({ video: true }) })) : null] })] }) }) }));
69
+ }
70
+ /**
71
+ * The agent tag and state over the resting waveform, then the call controls,
72
+ * all in one column the waveform's width.
73
+ */
74
+ function LiveStage({ model }) {
75
+ const t = useLabels();
76
+ const ref = useEnterMotion();
77
+ return (_jsx(VStack, { ref: ref, height: "100%", hAlign: "center", children: _jsxs(VStack, { width: "100%", maxWidth: WAVE_MAX_WIDTH, height: "100%", gap: 3, children: [_jsxs(VStack, { gap: 0.5, children: [_jsx(Text, { type: "label", color: "secondary", children: t('@theorem.agent.handle', { handle: model.handle }) }), _jsx(Text, { size: "sm", color: "secondary", children: liveStateLabel(t, model.liveState, model.activeTool) })] }), model.failure ? _jsx(Banner, { status: "error", title: model.failure.error }) : null, model.sessionEnded ? _jsx(Banner, { status: "info", title: model.sessionEnded }) : null, _jsx(StackItem, { size: "fill", children: _jsx(VStack, { height: "100%", vAlign: "end", children: _jsx(VStack, { width: "100%", height: WAVE_HEIGHT, style: { color: 'var(--color-icon-primary)' }, "aria-hidden": "true", children: _jsx(InkWaveform, { status: model.status, inputLevel: model.inputLevel, outputLevel: model.outputLevel, toolActive: model.toolActive, variant: "hero" }) }) }) }), _jsx(LiveControls, { model: model })] }) }));
78
+ }
79
+ /**
80
+ * Eases the call view in from the landing: Astryx has no enter-transition
81
+ * helper, so this animates with its `--duration-medium` / `--ease-standard`.
82
+ */
83
+ function useEnterMotion() {
84
+ const ref = useRef(null);
85
+ useLayoutEffect(() => {
86
+ const el = ref.current;
87
+ if (!el || globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
88
+ return;
89
+ const tokens = getComputedStyle(el);
90
+ const duration = Number.parseFloat(tokens.getPropertyValue('--duration-medium')) || 410;
91
+ el.animate([
92
+ { opacity: 0, transform: 'translateY(8px)' },
93
+ { opacity: 1, transform: 'none' },
94
+ ], { duration, easing: tokens.getPropertyValue('--ease-standard').trim() || 'ease-out' });
95
+ }, []);
96
+ return ref;
97
+ }
98
+ /**
99
+ * Mic, camera, text and call controls, centred under the stage. Icons show
100
+ * what a click does, as End call does: a slash means "turn this off".
101
+ */
102
+ function LiveControls({ model }) {
103
+ const t = useLabels();
104
+ const inactive = !model.sessionActive;
105
+ const controls = [];
106
+ if (model.voiceAvailable) {
107
+ controls.push(_jsx(ToggleButton, { label: t('@theorem.live.microphone'), tooltip: t(model.isMuted ? '@theorem.live.unmute' : '@theorem.live.mute'), isIconOnly: true, isPressed: !model.isMuted, isDisabled: inactive, icon: _jsx(Icon, { icon: IconMicrophone }), pressedIcon: _jsx(Icon, { icon: IconMicrophoneOff }), onPressedChange: model.handleToggleMic }, "mic"));
108
+ }
109
+ if (model.videoAvailable) {
110
+ controls.push(_jsx(ToggleButton, { label: t('@theorem.live.camera'), tooltip: t(model.isVideoOn ? '@theorem.live.camera_off' : '@theorem.live.camera_on'), isIconOnly: true, isPressed: model.isVideoOn, isDisabled: inactive, icon: _jsx(Icon, { icon: IconVideo }), pressedIcon: _jsx(Icon, { icon: IconVideoOff }), onPressedChange: () => {
111
+ void model.handleToggleVideo();
112
+ } }, "video"));
113
+ if (model.isVideoOn) {
114
+ controls.push(_jsx(IconButton, { label: t('@theorem.live.flip_camera'), tooltip: t('@theorem.live.flip_camera'), variant: "ghost", isDisabled: inactive, icon: _jsx(Icon, { icon: IconCameraRotate }), onClick: () => {
115
+ void model.handleFlipCamera();
116
+ } }, "flip"));
117
+ }
118
+ }
119
+ controls.push(model.canRestart ? (_jsx(IconButton, { label: t('@theorem.live.start_call'), tooltip: t('@theorem.live.start_call'), variant: "primary", icon: _jsx(Icon, { icon: IconPhone }), onClick: () => {
120
+ void model.handleRestart();
121
+ } }, "call")) : (_jsx(IconButton, { label: t('@theorem.live.end_call'), tooltip: t('@theorem.live.end_call'), variant: "destructive", isDisabled: inactive, icon: _jsx(Icon, { icon: IconPhoneOff }), onClick: model.handleEnd }, "call")));
122
+ return (_jsx(Toolbar, { label: t('@theorem.live.controls'), dividers: ['top'], centerContent: _jsx(HStack, { gap: 2, vAlign: "center", children: controls }) }));
123
+ }
124
+ /**
125
+ * Captions as a chat column: committed turns, then the interim lines still
126
+ * being heard, and the text composer in its dock when the profile takes text.
127
+ */
128
+ function LiveCaptions({ model }) {
129
+ const t = useLabels();
130
+ const { interimAgent } = model.captions;
131
+ const agentName = t('@theorem.agent.handle', { handle: model.handle });
132
+ const composer = model.textAvailable ? (_jsx(ChatComposer, { style: NO_FOCUS_RING, value: model.textDraft, onChange: model.setTextDraft, onSubmit: model.handleSendText, isDisabled: !model.sessionActive, placeholder: t('@theorem.composer.placeholder', { handle: model.handle }), input: _jsx(ChatComposerInput, {}), sendButton: _jsx(ChatSendButton, {}) })) : null;
133
+ const messages = [
134
+ ...model.pastCalls.flatMap((turns, call) => [
135
+ ...turns.map((turn) => captionMessage(`${String(call)}:${turn.id}`, turn.role, turn.text, agentName)),
136
+ _jsx(ChatSystemMessage, { variant: "divider", children: t('@theorem.live.new_session') }, `session-${String(call + 1)}`),
137
+ ]),
138
+ ...captionMessages(model.captions, agentName),
139
+ ];
140
+ // The empty state goes in ChatLayout's slot, which centres it; the list's
141
+ // own slot sits under its bottom-align spacer. ChatLayout flexes to fill the
142
+ // stack below any video preview.
143
+ return (_jsxs(VStack, { height: "100%", children: [model.videoPreview ? (_jsx(LiveVideoPreview, { video: model.videoPreview, facingMode: model.videoFacingMode })) : null, _jsx(ChatLayout, { composer: composer, emptyState: _jsx(EmptyState, { icon: _jsx(Icon, { icon: IconSubtitles, size: "lg", color: "secondary" }), title: t('@theorem.panel.captions.empty.title'), description: t('@theorem.panel.captions.empty.description'), isCompact: true }), children: messages.length > 0 ? (_jsx(ChatMessageList, { isStreaming: interimAgent !== '', children: messages })) : null })] }));
144
+ }
145
+ /** The current call's committed turns, then the lines still being heard. */
146
+ function captionMessages({ turns, interimUser, interimAgent }, agentName) {
147
+ const messages = turns.map((turn) => captionMessage(turn.id, turn.role, turn.text, agentName));
148
+ if (interimUser)
149
+ messages.push(captionMessage('interim-user', 'user', interimUser, agentName));
150
+ if (interimAgent)
151
+ messages.push(captionMessage('interim-agent', 'agent', interimAgent, agentName));
152
+ return messages;
153
+ }
154
+ function captionMessage(key, role, text, agentName) {
155
+ return role === 'user' ? (_jsx(ChatMessage, { sender: "user", children: _jsx(ChatMessageBubble, { children: text }) }, key)) : (_jsx(ChatMessage, { sender: "assistant", name: agentName, children: _jsx(Text, { children: text }) }, key));
156
+ }
157
+ /**
158
+ * The camera preview, at the top of the captions panel. The capture element comes
159
+ * from the session client, so it is mounted imperatively; it sits one level
160
+ * below AspectRatio, so it carries its own fill and mirror styles.
161
+ */
162
+ function LiveVideoPreview({ video, facingMode }) {
163
+ const t = useLabels();
164
+ const hostRef = useRef(null);
165
+ useEffect(() => {
166
+ const host = hostRef.current;
167
+ if (!host)
168
+ return;
169
+ Object.assign(video.style, { width: '100%', height: '100%', objectFit: 'cover', display: 'block' });
170
+ host.replaceChildren(video);
171
+ return () => {
172
+ if (video.parentElement === host)
173
+ host.removeChild(video);
174
+ };
175
+ }, [video]);
176
+ useEffect(() => {
177
+ video.style.transform = facingMode === 'user' ? 'scaleX(-1)' : '';
178
+ }, [video, facingMode]);
179
+ return (_jsx(AspectRatio, { ratio: 4 / 3, fit: "cover", children: _jsx("div", { ref: hostRef, "aria-label": t('@theorem.live.camera_preview'), role: "img" }) }));
180
+ }
181
+ /** Tool approvals and credential prompts, as a dialog that must be answered. */
182
+ function LiveToolGateDialog({ prompt, onResolve, }) {
183
+ return (_jsx(Dialog, { isOpen: prompt !== null, purpose: "required", width: 480,
184
+ // "required" disables dismissal; should one slip through, it denies.
185
+ onOpenChange: (open) => {
186
+ if (!open)
187
+ onResolve({ action: 'deny' });
188
+ }, children: prompt ? (prompt.gate.kind === 'auth' ? (_jsx(AuthChallengeCard, { gate: prompt.gate, toolName: prompt.gate.tool, onAuthenticated: (secret) => {
189
+ onResolve({ action: 'auth', secret });
190
+ } })) : (_jsx(ApprovalCard, { gate: prompt.gate, toolName: prompt.gate.tool, input: prompt.input, onDecision: (action) => {
191
+ onResolve({ action });
192
+ } }))) : null }));
193
+ }
@@ -0,0 +1,10 @@
1
+ import { type JsonSchema } from '../client/schema-fields.ts';
2
+ import { type Edit as StateEdit } from './StateFields.tsx';
3
+ /**
4
+ * A tool's request as a form drawn from its input schema: each field the
5
+ * input its type asks for, labelled by its title or key and described by its
6
+ * description. Optional fields left empty aren't sent.
7
+ */
8
+ export declare function SchemaFields({ schema, value, onChange }: StateEdit<Record<string, unknown>> & {
9
+ schema: JsonSchema;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CheckboxList, CheckboxListItem } from '@astryxdesign/core/CheckboxList';
3
+ import { DateInput } from '@astryxdesign/core/DateInput';
4
+ import { Grid } from '@astryxdesign/core/Grid';
5
+ import { HStack } from '@astryxdesign/core/HStack';
6
+ import { NumberInput } from '@astryxdesign/core/NumberInput';
7
+ import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
8
+ import { Selector } from '@astryxdesign/core/Selector';
9
+ import { Slider } from '@astryxdesign/core/Slider';
10
+ import { StackItem } from '@astryxdesign/core/Stack';
11
+ import { Switch } from '@astryxdesign/core/Switch';
12
+ import { TextArea } from '@astryxdesign/core/TextArea';
13
+ import { TextInput } from '@astryxdesign/core/TextInput';
14
+ import { Tokenizer } from '@astryxdesign/core/Tokenizer';
15
+ import { VStack } from '@astryxdesign/core/VStack';
16
+ import { useState } from 'react';
17
+ import { blankRow, schemaFields, withField } from "../client/schema-fields.js";
18
+ import { isRow, json } from "../client/shaped-data.js";
19
+ import { useLabels } from "./labels-provider.js";
20
+ import { AddRow, Group, Labelled, RemoveRow } from "./StateFields.js";
21
+ const NO_SUGGESTIONS = { search: () => [], bootstrap: () => [] };
22
+ const CODE_FONT = { '--font-family-body': 'var(--font-family-code)' };
23
+ /** A field whose input carries no label of its own, labelled as one. */
24
+ function Described({ field, children }) {
25
+ return (_jsx(Labelled, { label: field.label, isRequired: field.isRequired, description: field.description, children: children }));
26
+ }
27
+ /** A field the schema doesn't shape (no type, a union, a free object), as its JSON. */
28
+ function JsonField({ field, value, onChange }) {
29
+ const t = useLabels();
30
+ const [text, setText] = useState(() => (value === undefined ? '' : json(value)));
31
+ const [isValid, setValid] = useState(true);
32
+ return (_jsx(TextArea, { label: field.label, description: field.description, isRequired: field.isRequired, size: "sm", rows: 3, value: text, hasSpellCheck: false, status: isValid ? undefined : { type: 'error', message: t('@theorem.decision.invalid_json') }, style: CODE_FONT, onChange: (next) => {
33
+ setText(next);
34
+ if (next.trim() === '') {
35
+ setValid(true);
36
+ onChange(undefined);
37
+ return;
38
+ }
39
+ try {
40
+ onChange(JSON.parse(next));
41
+ setValid(true);
42
+ }
43
+ catch {
44
+ setValid(false);
45
+ }
46
+ } }));
47
+ }
48
+ /** A list of objects: each row its own fields, removable, and one more at the end. */
49
+ function RowsField({ field, item, value, onChange }) {
50
+ const rows = Array.isArray(value) ? value : [];
51
+ return (_jsx(Group, { label: field.label, isRequired: field.isRequired, description: field.description, children: _jsxs(VStack, { gap: 3, children: [rows.map((row, index) => (_jsxs(HStack, { gap: 2, vAlign: "start", children: [_jsx(StackItem, { size: "fill", children: _jsx(ObjectFields, { schema: item, value: isRow(row) ? row : {}, onChange: (next) => onChange(rows.map((old, at) => (at === index ? next : old))) }) }), _jsx(RemoveRow, { index: index, onRemove: () => onChange(rows.filter((_, at) => at !== index)) })] }, index))), _jsx(AddRow, { field: field.label, onAdd: () => onChange([...rows, blankRow(item)]) })] }) }));
52
+ }
53
+ function common(field) {
54
+ return { label: field.label, description: field.description, isRequired: field.isRequired, size: 'sm' };
55
+ }
56
+ /** One of a few options as segments; more as a menu. */
57
+ function ChoiceField({ field, options, isMenu, value, onChange }) {
58
+ if (isMenu) {
59
+ return _jsx(Selector, { ...common(field), options: options, value: typeof value === 'string' ? value : undefined, hasSearch: options.length > 10, onChange: onChange });
60
+ }
61
+ return (_jsx(Described, { field: field, children: _jsx(HStack, { children: _jsx(SegmentedControl, { label: field.label, size: "sm", value: typeof value === 'string' ? value : '', onChange: onChange, children: options.map((option) => (_jsx(SegmentedControlItem, { value: option, label: option }, option))) }) }) }));
62
+ }
63
+ function TextField({ field, isLong, type, value, onChange }) {
64
+ const text = typeof value === 'string' ? value : '';
65
+ return isLong ? _jsx(TextArea, { ...common(field), rows: 3, value: text, onChange: onChange }) : _jsx(TextInput, { ...common(field), type: type, value: text, onChange: onChange });
66
+ }
67
+ /** A list of words or numbers as tokens. */
68
+ function WordsField({ field, value, onChange }) {
69
+ const t = useLabels();
70
+ const words = Array.isArray(value) ? value : [];
71
+ return (_jsx(Tokenizer, { ...common(field), value: words.map((word, index) => ({ id: `${String(index)}:${String(word)}`, label: String(word) })), searchSource: NO_SUGGESTIONS, hasCreate: true, debounceMs: 0, placeholder: t('@theorem.decision.add_value'), onChange: (items) =>
72
+ // Numbers stay numbers when every word reads as one and the list held numbers.
73
+ onChange(items.map((item) => (typeof words[0] === 'number' && Number.isFinite(Number(item.label)) ? Number(item.label) : item.label))) }));
74
+ }
75
+ /** Each control's input, as the schema asks for it. */
76
+ const CONTROLS = {
77
+ choice: ({ field, control, value, onChange }) => (_jsx(ChoiceField, { field: field, options: control.options, isMenu: control.isMenu, value: value, onChange: onChange })),
78
+ range: ({ field, control, value, onChange }) => (_jsx(Slider, { label: field.label, description: field.description, isRequired: field.isRequired, min: control.min, max: control.max, step: 1, value: typeof value === 'number' ? value : control.min, valueDisplay: "text", onChange: onChange })),
79
+ number: ({ field, control, value, onChange }) => (_jsx(NumberInput, { ...common(field), value: typeof value === 'number' ? value : undefined, min: control.min, max: control.max, isIntegerOnly: control.isInteger, isWheelEnabled: false, onChange: (next) => onChange(next ?? undefined) })),
80
+ switch: ({ field, value, onChange }) => _jsx(Switch, { label: field.label, description: field.description, value: value === true, onChange: onChange }),
81
+ date: ({ field, value, onChange }) => (_jsx(DateInput, { ...common(field), value: typeof value === 'string' ? value : undefined, onChange: onChange })),
82
+ text: ({ field, control, value, onChange }) => _jsx(TextField, { field: field, isLong: control.isLong, type: control.type, value: value, onChange: onChange }),
83
+ choices: ({ field, control, value, onChange }) => (_jsx(CheckboxList, { label: field.label, description: field.description, value: Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [], onChange: onChange, children: control.options.map((option) => (_jsx(CheckboxListItem, { label: option, value: option }, option))) })),
84
+ words: ({ field, value, onChange }) => _jsx(WordsField, { field: field, value: value, onChange: onChange }),
85
+ rows: ({ field, control, value, onChange }) => _jsx(RowsField, { field: field, item: control.item, value: value, onChange: onChange }),
86
+ group: ({ field, control, value, onChange }) => (_jsx(Group, { label: field.label, isRequired: field.isRequired, description: field.description, children: _jsx(ObjectFields, { schema: control.schema, value: isRow(value) ? value : {}, onChange: onChange }) })),
87
+ json: ({ field, value, onChange }) => _jsx(JsonField, { field: field, value: value, onChange: onChange }),
88
+ };
89
+ function Field({ field, value, onChange }) {
90
+ const control = CONTROLS[field.control.kind];
91
+ return control({ field, control: field.control, value, onChange });
92
+ }
93
+ /** Room a short field takes: numbers, dates, menus and one-line text sit side by side. */
94
+ const SHORT_FIELD_PX = 200;
95
+ /** Segments whose labels run this long in all still fit a short field's width. */
96
+ const SHORT_SEGMENT_CHARS = 18;
97
+ /** A field that reads in a column's width; the rest take the whole row. */
98
+ function isShort({ control }) {
99
+ switch (control.kind) {
100
+ case 'number':
101
+ case 'switch':
102
+ case 'date':
103
+ return true;
104
+ case 'choice':
105
+ return control.isMenu || control.options.join('').length <= SHORT_SEGMENT_CHARS;
106
+ case 'text':
107
+ return !control.isLong;
108
+ default:
109
+ return false;
110
+ }
111
+ }
112
+ function ObjectFields({ schema, value, onChange }) {
113
+ return (_jsx(Grid, { columns: { minWidth: SHORT_FIELD_PX, repeat: 'fill' }, gap: 3, children: schemaFields(schema).map((field) => (_jsx("div", { style: isShort(field) ? { minWidth: 0, alignSelf: 'end' } : { gridColumn: '1 / -1', minWidth: 0 }, children: _jsx(Field, { field: field, value: value[field.key], onChange: (next) => onChange(withField(value, field.key, next)) }) }, field.key))) }));
114
+ }
115
+ /**
116
+ * A tool's request as a form drawn from its input schema: each field the
117
+ * input its type asks for, labelled by its title or key and described by its
118
+ * description. Optional fields left empty aren't sent.
119
+ */
120
+ export function SchemaFields({ schema, value, onChange }) {
121
+ return _jsx(ObjectFields, { schema: schema, value: value, onChange: onChange });
122
+ }
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from 'react';
2
+ export type ShapedDataProps = {
3
+ value: unknown;
4
+ title?: string;
5
+ /** Drawn above the data in its data view (a tool result's figures and charts); a render that throws falls back to the JSON with it. */
6
+ lead?: ReactNode;
7
+ /** Top-level fields the lead already shows: the data view leaves them out, the JSON keeps them. */
8
+ ledKeys?: readonly string[];
9
+ };
10
+ /** A tool call's input, output or failure, or a structured reply, with a switch to its JSON. */
11
+ export declare function ShapedData({ value: raw, title, lead, ledKeys }: ShapedDataProps): import("react").JSX.Element;
@@ -0,0 +1,151 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CodeBlock } from '@astryxdesign/core/CodeBlock';
3
+ import { Collapsible, CollapsibleGroup } from '@astryxdesign/core/Collapsible';
4
+ import { HStack } from '@astryxdesign/core/HStack';
5
+ import { useLocale } from '@astryxdesign/core/i18n';
6
+ import { Link } from '@astryxdesign/core/Link';
7
+ import { List, ListItem } from '@astryxdesign/core/List';
8
+ import { Markdown } from '@astryxdesign/core/Markdown';
9
+ import { MetadataList, MetadataListItem } from '@astryxdesign/core/MetadataList';
10
+ import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
11
+ import { Table } from '@astryxdesign/core/Table';
12
+ import { Text } from '@astryxdesign/core/Text';
13
+ import { Token } from '@astryxdesign/core/Token';
14
+ import { VStack } from '@astryxdesign/core/VStack';
15
+ import { Component, useMemo, useState } from 'react';
16
+ import { fieldLabel, fieldReading, isPlain, isProse, isRow, json, MAX_ROWS, plainReading, rowHeading, shapeOf, splitFields, unpacked, withUnit, } from "../client/shaped-data.js";
17
+ import { useLabels } from "./labels-provider.js";
18
+ /** Each way a plain value reads, drawn. */
19
+ const READINGS = {
20
+ none: () => _jsx(Text, { color: "secondary", children: "\u2014" }),
21
+ boolean: ({ value }, _, t) => t(value ? '@theorem.data.yes' : '@theorem.data.no'),
22
+ number: ({ value, unit }, formats) => withUnit((Number.isInteger(value) ? formats.integer : formats.decimal).format(value), unit),
23
+ link: ({ href }) => (_jsx(Link, { href: href, target: "_blank", rel: "noreferrer", children: href })),
24
+ date: ({ date }, formats) => formats.date.format(date),
25
+ 'date-time': ({ date }, formats) => formats.dateTime.format(date),
26
+ // Prose (an answer, a summary) is usually Markdown; headings start small, under the section's own.
27
+ text: ({ text }) => isProse(text) ? (_jsx(Markdown, { density: "compact", headingLevelStart: 4, children: text })) : (_jsx("span", { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: text })),
28
+ };
29
+ function Plain({ value, unit }) {
30
+ const t = useLabels();
31
+ const locale = useLocale();
32
+ const formats = useMemo(() => ({
33
+ // No grouping: ids and years read as written. Six significant digits: a timing reads 1.40369.
34
+ integer: new Intl.NumberFormat(locale, { useGrouping: false }),
35
+ decimal: new Intl.NumberFormat(locale, { useGrouping: false, maximumSignificantDigits: 6 }),
36
+ dateTime: new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }),
37
+ // A bare date is a calendar day, parsed at UTC midnight: read it in UTC, or it slips a day west of Greenwich.
38
+ date: new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone: 'UTC' }),
39
+ }), [locale]);
40
+ const reading = plainReading(value, unit);
41
+ return READINGS[reading.kind](reading, formats, t);
42
+ }
43
+ /** JSON past this many characters isn't laid out or highlighted in full: the page stays responsive however big the payload. */
44
+ const LARGE_JSON_CHARS = 200_000;
45
+ function Json({ text }) {
46
+ const code = text.length > LARGE_JSON_CHARS ? `${text.slice(0, LARGE_JSON_CHARS)}\n…` : text;
47
+ return _jsx(CodeBlock, { code: code, language: "json", hasLanguageLabel: false, size: "sm", width: "100%" });
48
+ }
49
+ /** Past MAX_ROWS, a note that the rest are in the JSON view. */
50
+ function More({ total }) {
51
+ const t = useLabels();
52
+ if (total <= MAX_ROWS)
53
+ return null;
54
+ return (_jsx(Text, { type: "supporting", color: "secondary", children: t('@theorem.data.more', { count: String(total - MAX_ROWS) }) }));
55
+ }
56
+ function DataTable({ columns, rows, units }) {
57
+ const tableColumns = columns.map((key, index) => {
58
+ const { label, unit } = fieldLabel(key, units);
59
+ return {
60
+ key: `c${String(index)}`,
61
+ header: unit ? `${label} (${unit})` : label,
62
+ renderCell: (row) => _jsx(Plain, { value: row.cells[key] }),
63
+ };
64
+ });
65
+ const data = rows.slice(0, MAX_ROWS).map((cells, index) => ({ id: String(index), cells }));
66
+ return (_jsxs(VStack, { gap: 1, children: [_jsx(Table, { data: data, columns: tableColumns, idKey: "id", density: "compact", textOverflow: "truncate" }), _jsx(More, { total: rows.length })] }));
67
+ }
68
+ /** A list row's extra fields and tags, at its end. */
69
+ function ListRowEnd({ row }) {
70
+ if (row.tags.length + row.extras.length === 0)
71
+ return undefined;
72
+ return (_jsxs(HStack, { gap: 2, vAlign: "center", children: [row.extras.map(([key, value]) => {
73
+ const { label, unit } = fieldReading(key, value);
74
+ return (_jsxs(Text, { type: "supporting", color: "secondary", children: [label, " ", _jsx(Plain, { value: value, unit: unit })] }, key));
75
+ }), row.tags.map((tag) => (_jsx(Token, { label: tag, size: "sm" }, tag)))] }));
76
+ }
77
+ function DataList({ rows }) {
78
+ return (_jsxs(VStack, { gap: 1, children: [_jsx(List, { hasDividers: true, density: "compact", children: rows.slice(0, MAX_ROWS).map((row, index) => (_jsx(ListItem
79
+ // biome-ignore lint/suspicious/noArrayIndexKey: rows have no identity of their own
80
+ , { label: row.title, description: row.description, href: row.href, target: row.href ? '_blank' : undefined, rel: row.href ? 'noreferrer' : undefined, endContent: _jsx(ListRowEnd, { row: row }) }, index))) }), _jsx(More, { total: rows.length })] }));
81
+ }
82
+ function Rows({ items, depth }) {
83
+ const t = useLabels();
84
+ return (_jsxs(VStack, { gap: 1, children: [_jsx(CollapsibleGroup, { type: "multiple", hasDividers: true, density: "compact", children: items.slice(0, MAX_ROWS).map((item, index) => {
85
+ const heading = rowHeading(item);
86
+ return (_jsx(Collapsible
87
+ // biome-ignore lint/suspicious/noArrayIndexKey: rows have no identity of their own
88
+ , { value: String(index), trigger: _jsxs(HStack, { gap: 2, vAlign: "center", wrap: "wrap", children: [_jsx(Text, { type: "body", children: heading.title ?? t('@theorem.data.item', { index: String(index + 1) }) }), heading.tags.map((tag) => (_jsx(Token, { label: tag, size: "sm" }, tag)))] }), children: _jsx(Node, { value: heading.rest, depth: depth + 1 }) }, index));
89
+ }) }), _jsx(More, { total: items.length })] }));
90
+ }
91
+ function Fields({ row, depth, units }) {
92
+ const { plain, sections } = splitFields(row);
93
+ const fields = plain.length > 0 && (_jsx(MetadataList, { label: { position: 'start', width: '40%' }, children: plain.map(([key, value]) => {
94
+ const { label, unit } = fieldReading(key, value, units);
95
+ return (_jsx(MetadataListItem, { label: label, children: Array.isArray(unpacked(value)) ? _jsx(Node, { value: value, depth: depth + 1 }) : _jsx(Plain, { value: value, unit: unit }) }, key));
96
+ }) }));
97
+ return (_jsxs(VStack, { gap: 2, children: [depth > 0 && fields, sections.length > 0 && (_jsx(CollapsibleGroup, { type: "multiple", hasDividers: true, density: "compact",
98
+ // The top level's first section starts open: it's usually the payload. The rest wait to be asked for.
99
+ defaultValue: depth === 0 ? sections.slice(0, 1).map((shown) => shown.key) : [], children: sections.map((shown) => (_jsx(Collapsible, { value: shown.key, trigger: _jsx(Text, { type: "label", children: shown.title }), children: _jsx(VStack, { paddingInlineStart: 3, children: _jsx(Node, { value: shown.value, depth: depth + 1, units: shown.units }) }) }, shown.key))) })), depth === 0 && fields] }));
100
+ }
101
+ /** Each shape a value takes, drawn. */
102
+ const SHAPES = {
103
+ json: (_, { value }) => _jsx(Json, { text: json(value) }),
104
+ none: (_, { t }) => _jsx(Text, { color: "secondary", children: t('@theorem.data.none') }),
105
+ plain: (_, { value }) => (_jsx(Text, { type: "body", children: _jsx(Plain, { value: value }) })),
106
+ tokens: ({ items }) => (_jsxs(VStack, { gap: 1, children: [_jsx(HStack, { gap: 1, wrap: "wrap", children: items.slice(0, MAX_ROWS).map((item, index) => (_jsx(Token, { label: item === null ? '—' : String(item), size: "sm" }, index))) }), _jsx(More, { total: items.length })] })),
107
+ list: ({ rows }) => _jsx(DataList, { rows: rows }),
108
+ table: ({ columns, rows }, { units }) => _jsx(DataTable, { columns: columns, rows: rows, units: units }),
109
+ rows: ({ items }, { depth }) => _jsx(Rows, { items: items, depth: depth }),
110
+ fields: ({ row }, { depth, units }) => _jsx(Fields, { row: row, depth: depth, units: units }),
111
+ };
112
+ function Node({ value: raw, depth, units }) {
113
+ const t = useLabels();
114
+ const value = unpacked(raw);
115
+ const shape = shapeOf(value, depth);
116
+ return SHAPES[shape.kind](shape, { value, depth, units, t });
117
+ }
118
+ /** A render the rules didn't foresee throws into the JSON, never into the transcript. */
119
+ class ShapeBoundary extends Component {
120
+ state = { failed: false };
121
+ static getDerivedStateFromError() {
122
+ return { failed: true };
123
+ }
124
+ render() {
125
+ return this.state.failed ? this.props.fallback : this.props.children;
126
+ }
127
+ }
128
+ /** A tool call's input, output or failure, or a structured reply, with a switch to its JSON. */
129
+ export function ShapedData({ value: raw, title, lead, ledKeys = [] }) {
130
+ const t = useLabels();
131
+ const locale = useLocale();
132
+ const [view, setView] = useState('data');
133
+ const value = unpacked(raw);
134
+ // A plain value reads the same either way; only structure gets the switch.
135
+ const structured = !isPlain(value);
136
+ const text = useMemo(() => (structured ? json(value) : ''), [structured, value]);
137
+ // What the lead hasn't shown; nothing when it showed every field.
138
+ const rest = useMemo(() => {
139
+ if (!ledKeys.length || !isRow(value))
140
+ return value;
141
+ const left = Object.entries(value).filter(([key]) => !ledKeys.includes(key));
142
+ return left.length ? Object.fromEntries(left) : undefined;
143
+ }, [value, ledKeys]);
144
+ if (text.length > LARGE_JSON_CHARS) {
145
+ const size = new Intl.NumberFormat(locale, { style: 'unit', unit: 'kilobyte', maximumFractionDigits: 0 }).format(LARGE_JSON_CHARS / 1000);
146
+ return (_jsxs(VStack, { gap: 2, children: [_jsx(Text, { type: "supporting", color: "secondary", children: title }), _jsx(Text, { type: "supporting", color: "secondary", children: t('@theorem.data.large', { size }) }), _jsx(Json, { text: text })] }));
147
+ }
148
+ return (_jsxs(VStack, { gap: 2, children: [_jsxs(HStack, { gap: 2, vAlign: "center", hAlign: "between", children: [_jsx(Text, { type: "supporting", color: "secondary", children: title }), structured && (_jsxs(SegmentedControl, { label: t('@theorem.data.view'), size: "sm", value: view, onChange: (next) => {
149
+ setView(next === 'json' ? 'json' : 'data');
150
+ }, children: [_jsx(SegmentedControlItem, { value: "data", label: t('@theorem.data.shaped') }), _jsx(SegmentedControlItem, { value: "json", label: t('@theorem.data.json') })] }))] }), view === 'json' && structured ? (_jsx(Json, { text: text })) : (_jsx(ShapeBoundary, { fallback: _jsx(Json, { text: text }), children: _jsxs(VStack, { gap: 3, children: [lead, rest === undefined ? null : _jsx(Node, { value: rest, depth: 0 })] }) }))] }));
151
+ }