@theoremjs/react 0.0.0-stage → 0.3.1

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 (320) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -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-images.d.ts +76 -0
  16. package/client/decision-images.js +139 -0
  17. package/client/decision-transport.d.ts +70 -0
  18. package/client/decision-transport.js +175 -0
  19. package/client/decode-composer-draft.d.ts +11 -0
  20. package/client/decode-composer-draft.js +12 -0
  21. package/client/embedding-input.d.ts +84 -0
  22. package/client/embedding-input.js +194 -0
  23. package/client/embedding-space.d.ts +82 -0
  24. package/client/embedding-space.js +389 -0
  25. package/client/embedding-transport.d.ts +40 -0
  26. package/client/embedding-transport.js +71 -0
  27. package/client/encode-composer-draft.d.ts +9 -0
  28. package/client/encode-composer-draft.js +34 -0
  29. package/client/encode-files.d.ts +18 -0
  30. package/client/encode-files.js +54 -0
  31. package/client/failure.d.ts +29 -0
  32. package/client/failure.js +34 -0
  33. package/client/generation-selection.d.ts +15 -0
  34. package/client/generation-selection.js +24 -0
  35. package/client/host-call.d.ts +51 -0
  36. package/client/host-call.js +47 -0
  37. package/client/host-transport.d.ts +70 -0
  38. package/client/host-transport.js +75 -0
  39. package/client/image-output.d.ts +6 -0
  40. package/client/image-output.js +13 -0
  41. package/client/index.d.ts +28 -0
  42. package/client/index.js +22 -0
  43. package/client/ink-waveform.d.ts +17 -0
  44. package/client/ink-waveform.js +68 -0
  45. package/client/live/apply-live-turn-tool-event.d.ts +14 -0
  46. package/client/live/apply-live-turn-tool-event.js +39 -0
  47. package/client/live/live-captions.d.ts +53 -0
  48. package/client/live/live-captions.js +174 -0
  49. package/client/live/live-mic-forward.d.ts +30 -0
  50. package/client/live/live-mic-forward.js +46 -0
  51. package/client/live/live-page-tool.d.ts +34 -0
  52. package/client/live/live-page-tool.js +11 -0
  53. package/client/live/live-state.d.ts +13 -0
  54. package/client/live/live-state.js +22 -0
  55. package/client/live/live-tool.d.ts +11 -0
  56. package/client/live/live-tool.js +1 -0
  57. package/client/live/live-video.d.ts +10 -0
  58. package/client/live/live-video.js +71 -0
  59. package/client/live/run-live-tool-call.d.ts +22 -0
  60. package/client/live/run-live-tool-call.js +70 -0
  61. package/client/live/use-live-caption-log.d.ts +17 -0
  62. package/client/live/use-live-caption-log.js +51 -0
  63. package/client/live-client.d.ts +189 -0
  64. package/client/live-client.js +829 -0
  65. package/client/live-errors.d.ts +2 -0
  66. package/client/live-errors.js +9 -0
  67. package/client/live-messages.d.ts +148 -0
  68. package/client/live-messages.js +100 -0
  69. package/client/mic-capture.d.ts +4 -0
  70. package/client/mic-capture.js +6 -0
  71. package/client/oauth-popup.d.ts +26 -0
  72. package/client/oauth-popup.js +30 -0
  73. package/client/page-dispatch.d.ts +15 -0
  74. package/client/page-dispatch.js +63 -0
  75. package/client/pcm-downsample.d.ts +4 -0
  76. package/client/pcm-downsample.js +45 -0
  77. package/client/run-commit.d.ts +34 -0
  78. package/client/run-commit.js +49 -0
  79. package/client/run-session.d.ts +79 -0
  80. package/client/run-session.js +207 -0
  81. package/client/schema-fields.d.ts +63 -0
  82. package/client/schema-fields.js +199 -0
  83. package/client/shaped-data.d.ts +118 -0
  84. package/client/shaped-data.js +343 -0
  85. package/client/source-citations.d.ts +13 -0
  86. package/client/source-citations.js +56 -0
  87. package/client/space-geometry.d.ts +39 -0
  88. package/client/space-geometry.js +183 -0
  89. package/client/state-fields.d.ts +16 -0
  90. package/client/state-fields.js +44 -0
  91. package/client/tool-result.d.ts +78 -0
  92. package/client/tool-result.js +293 -0
  93. package/client/tool-resume.d.ts +50 -0
  94. package/client/tool-resume.js +14 -0
  95. package/client/trace-feed.d.ts +13 -0
  96. package/client/trace-feed.js +18 -0
  97. package/client/trace-story.d.ts +163 -0
  98. package/client/trace-story.js +440 -0
  99. package/client/trace-view.d.ts +130 -0
  100. package/client/trace-view.js +333 -0
  101. package/client/transcript-groups.d.ts +140 -0
  102. package/client/transcript-groups.js +217 -0
  103. package/client/transcript-schema.d.ts +10 -0
  104. package/client/transcript-schema.js +120 -0
  105. package/client/transcript-scroll.d.ts +7 -0
  106. package/client/transcript-scroll.js +6 -0
  107. package/client/transport.d.ts +264 -0
  108. package/client/transport.js +330 -0
  109. package/client/turn-client.d.ts +44 -0
  110. package/client/turn-client.js +88 -0
  111. package/client/voice-label.d.ts +2 -0
  112. package/client/voice-label.js +15 -0
  113. package/client/voice-recorder.d.ts +27 -0
  114. package/client/voice-recorder.js +162 -0
  115. package/client/wire-line.d.ts +56 -0
  116. package/client/wire-line.js +90 -0
  117. package/client/worklets/mic-capture.js +16 -0
  118. package/components/InkWaveform.d.ts +21 -0
  119. package/components/InkWaveform.js +131 -0
  120. package/components/live/use-live-runner-controls.d.ts +47 -0
  121. package/components/live/use-live-runner-controls.js +96 -0
  122. package/components/live/use-live-runner-model.d.ts +49 -0
  123. package/components/live/use-live-runner-model.js +190 -0
  124. package/components/live/use-live-runner-ui.d.ts +67 -0
  125. package/components/live/use-live-runner-ui.js +167 -0
  126. package/components/live/use-live-session-client.d.ts +41 -0
  127. package/components/live/use-live-session-client.js +126 -0
  128. package/components/use-composer-voice.d.ts +28 -0
  129. package/components/use-composer-voice.js +90 -0
  130. package/components/use-editor-selection.d.ts +3 -0
  131. package/components/use-editor-selection.js +19 -0
  132. package/components/use-voice-playback.d.ts +17 -0
  133. package/components/use-voice-playback.js +68 -0
  134. package/hooks/use-described.d.ts +12 -0
  135. package/hooks/use-described.js +25 -0
  136. package/hooks/use-embedding-index.d.ts +27 -0
  137. package/hooks/use-embedding-index.js +43 -0
  138. package/hooks/use-one-call.d.ts +31 -0
  139. package/hooks/use-one-call.js +76 -0
  140. package/hooks/use-page-tool-warnings.d.ts +6 -0
  141. package/hooks/use-page-tool-warnings.js +25 -0
  142. package/hooks/use-second-ticker.d.ts +2 -0
  143. package/hooks/use-second-ticker.js +13 -0
  144. package/hooks/use-theorem-chat-actions.d.ts +79 -0
  145. package/hooks/use-theorem-chat-actions.js +287 -0
  146. package/hooks/use-theorem-chat-state.d.ts +66 -0
  147. package/hooks/use-theorem-chat-state.js +122 -0
  148. package/hooks/use-theorem-chat.d.ts +97 -0
  149. package/hooks/use-theorem-chat.js +528 -0
  150. package/hooks/use-theorem-decision.d.ts +18 -0
  151. package/hooks/use-theorem-decision.js +12 -0
  152. package/hooks/use-theorem-embedding.d.ts +18 -0
  153. package/hooks/use-theorem-embedding.js +12 -0
  154. package/hooks/use-theorem-host.d.ts +45 -0
  155. package/hooks/use-theorem-host.js +135 -0
  156. package/hooks/use-theorem-interface.d.ts +22 -0
  157. package/hooks/use-theorem-interface.js +15 -0
  158. package/hooks/use-trace-records.d.ts +4 -0
  159. package/hooks/use-trace-records.js +7 -0
  160. package/index.d.ts +17 -0
  161. package/index.js +17 -0
  162. package/live.d.ts +9 -0
  163. package/live.js +10 -0
  164. package/package.json +74 -4
  165. package/server/checkpoint-ownership.d.ts +6 -0
  166. package/server/checkpoint-ownership.js +42 -0
  167. package/server/conversation-store.d.ts +92 -0
  168. package/server/conversation-store.js +131 -0
  169. package/server/conversation.d.ts +193 -0
  170. package/server/conversation.js +552 -0
  171. package/server/credential-store.d.ts +24 -0
  172. package/server/credential-store.js +14 -0
  173. package/server/decision-handler.d.ts +42 -0
  174. package/server/decision-handler.js +46 -0
  175. package/server/embedding-handler.d.ts +33 -0
  176. package/server/embedding-handler.js +45 -0
  177. package/server/handler.d.ts +152 -0
  178. package/server/handler.js +771 -0
  179. package/server/json-response.d.ts +2 -0
  180. package/server/json-response.js +7 -0
  181. package/server/mod.d.ts +25 -0
  182. package/server/mod.js +23 -0
  183. package/server/page-dispatch.d.ts +22 -0
  184. package/server/page-dispatch.js +127 -0
  185. package/server/request-body.d.ts +2 -0
  186. package/server/request-body.js +42 -0
  187. package/server/request-check.d.ts +21 -0
  188. package/server/request-check.js +49 -0
  189. package/server/session-authority.d.ts +8 -0
  190. package/server/session-authority.js +78 -0
  191. package/server/session-boundary.d.ts +26 -0
  192. package/server/session-boundary.js +64 -0
  193. package/server/session-identity.d.ts +16 -0
  194. package/server/session-identity.js +41 -0
  195. package/server/session-limits.d.ts +12 -0
  196. package/server/session-limits.js +59 -0
  197. package/server/session-store.d.ts +123 -0
  198. package/server/session-store.js +264 -0
  199. package/server/single-action.d.ts +21 -0
  200. package/server/single-action.js +30 -0
  201. package/server/steer-inbox.d.ts +35 -0
  202. package/server/steer-inbox.js +52 -0
  203. package/server/tool-operations.d.ts +6 -0
  204. package/server/tool-operations.js +132 -0
  205. package/server/turn-input.d.ts +10 -0
  206. package/server/turn-input.js +29 -0
  207. package/styles/ink-controls.css +134 -0
  208. package/ui/CardWall.d.ts +10 -0
  209. package/ui/CardWall.js +61 -0
  210. package/ui/ChatComposerBar.d.ts +62 -0
  211. package/ui/ChatComposerBar.js +306 -0
  212. package/ui/ChatTranscript.d.ts +43 -0
  213. package/ui/ChatTranscript.js +527 -0
  214. package/ui/ConsoleFrame.d.ts +104 -0
  215. package/ui/ConsoleFrame.js +146 -0
  216. package/ui/DecisionAnswers.d.ts +53 -0
  217. package/ui/DecisionAnswers.js +233 -0
  218. package/ui/DecisionImages.d.ts +16 -0
  219. package/ui/DecisionImages.js +46 -0
  220. package/ui/EmbeddingDocuments.d.ts +78 -0
  221. package/ui/EmbeddingDocuments.js +278 -0
  222. package/ui/EmbeddingQuery.d.ts +27 -0
  223. package/ui/EmbeddingQuery.js +95 -0
  224. package/ui/EmbeddingResults.d.ts +41 -0
  225. package/ui/EmbeddingResults.js +165 -0
  226. package/ui/EmbeddingSpace.d.ts +26 -0
  227. package/ui/EmbeddingSpace.js +106 -0
  228. package/ui/LiveCaptionsPanel.d.ts +22 -0
  229. package/ui/LiveCaptionsPanel.js +27 -0
  230. package/ui/LiveRunner.d.ts +30 -0
  231. package/ui/LiveRunner.js +180 -0
  232. package/ui/PaneState.d.ts +33 -0
  233. package/ui/PaneState.js +50 -0
  234. package/ui/SchemaFields.d.ts +10 -0
  235. package/ui/SchemaFields.js +150 -0
  236. package/ui/ShapedData.d.ts +11 -0
  237. package/ui/ShapedData.js +196 -0
  238. package/ui/SidePanel.d.ts +126 -0
  239. package/ui/SidePanel.js +123 -0
  240. package/ui/StateFields.d.ts +44 -0
  241. package/ui/StateFields.js +139 -0
  242. package/ui/TheoremChat.d.ts +122 -0
  243. package/ui/TheoremChat.js +134 -0
  244. package/ui/TheoremDecision.d.ts +30 -0
  245. package/ui/TheoremDecision.js +130 -0
  246. package/ui/TheoremEmbedding.d.ts +27 -0
  247. package/ui/TheoremEmbedding.js +122 -0
  248. package/ui/TheoremHost.d.ts +55 -0
  249. package/ui/TheoremHost.js +240 -0
  250. package/ui/ToolGateCard.d.ts +32 -0
  251. package/ui/ToolGateCard.js +107 -0
  252. package/ui/ToolResult.d.ts +10 -0
  253. package/ui/ToolResult.js +62 -0
  254. package/ui/ToolResultCharts.d.ts +5 -0
  255. package/ui/ToolResultCharts.js +103 -0
  256. package/ui/TraceCharts.d.ts +13 -0
  257. package/ui/TraceCharts.js +266 -0
  258. package/ui/TraceGuardrails.d.ts +11 -0
  259. package/ui/TraceGuardrails.js +74 -0
  260. package/ui/TraceInspector.d.ts +16 -0
  261. package/ui/TraceInspector.js +206 -0
  262. package/ui/TraceInspectorPanel.d.ts +46 -0
  263. package/ui/TraceInspectorPanel.js +95 -0
  264. package/ui/TraceOverview.d.ts +38 -0
  265. package/ui/TraceOverview.js +126 -0
  266. package/ui/TraceSpanDetail.d.ts +10 -0
  267. package/ui/TraceSpanDetail.js +190 -0
  268. package/ui/TraceStory.d.ts +20 -0
  269. package/ui/TraceStory.js +108 -0
  270. package/ui/TraceValues.d.ts +36 -0
  271. package/ui/TraceValues.js +123 -0
  272. package/ui/TraceWaterfall.d.ts +13 -0
  273. package/ui/TraceWaterfall.js +135 -0
  274. package/ui/VoiceNote.d.ts +12 -0
  275. package/ui/VoiceNote.js +28 -0
  276. package/ui/approval-heading.d.ts +4 -0
  277. package/ui/approval-heading.js +9 -0
  278. package/ui/arrive.d.ts +18 -0
  279. package/ui/arrive.js +46 -0
  280. package/ui/built/theme.css +792 -0
  281. package/ui/built/theme.css.d.ts +9 -0
  282. package/ui/built/theorem.d.ts +13 -0
  283. package/ui/built/theorem.js +660 -0
  284. package/ui/built/theorem.variants.d.ts +22 -0
  285. package/ui/code-language.d.ts +12 -0
  286. package/ui/code-language.js +50 -0
  287. package/ui/disclosure-motion.d.ts +6 -0
  288. package/ui/disclosure-motion.js +207 -0
  289. package/ui/embedding-space-gl.d.ts +37 -0
  290. package/ui/embedding-space-gl.js +663 -0
  291. package/ui/embedding-space-view.d.ts +8 -0
  292. package/ui/embedding-space-view.js +8 -0
  293. package/ui/glide.d.ts +7 -0
  294. package/ui/glide.js +44 -0
  295. package/ui/icons.d.ts +4 -0
  296. package/ui/icons.js +38 -0
  297. package/ui/index.d.ts +36 -0
  298. package/ui/index.js +39 -0
  299. package/ui/labels-provider.d.ts +16 -0
  300. package/ui/labels-provider.js +64 -0
  301. package/ui/labels.d.ts +1442 -0
  302. package/ui/labels.js +1443 -0
  303. package/ui/markdown-plugins.d.ts +9 -0
  304. package/ui/markdown-plugins.js +13 -0
  305. package/ui/mcp-icon.d.ts +6 -0
  306. package/ui/mcp-icon.js +8 -0
  307. package/ui/row-keys.d.ts +19 -0
  308. package/ui/row-keys.js +33 -0
  309. package/ui/theme.d.ts +10 -0
  310. package/ui/theme.js +13 -0
  311. package/ui/theorem-theme.d.ts +10 -0
  312. package/ui/theorem-theme.js +105 -0
  313. package/ui/trace-palette.d.ts +10 -0
  314. package/ui/trace-palette.js +10 -0
  315. package/ui/trace-patterns.d.ts +40 -0
  316. package/ui/trace-patterns.js +57 -0
  317. package/ui/transcript-copy-text.d.ts +4 -0
  318. package/ui/transcript-copy-text.js +40 -0
  319. package/ui/wall-columns.d.ts +8 -0
  320. package/ui/wall-columns.js +19 -0
@@ -0,0 +1,29 @@
1
+ /** The page's context as the client's and the host's as the server's; nothing when neither gave any. */
2
+ function turnContext(client, server) {
3
+ const context = {
4
+ ...(client === undefined ? {} : { client }),
5
+ ...(server === undefined ? {} : { server }),
6
+ };
7
+ return Object.keys(context).length > 0 ? context : undefined;
8
+ }
9
+ /** A turn input off the wire as the kernel takes it: the page's context is the client's. */
10
+ export function kernelTurnInput(input, server) {
11
+ const { context: client, ...rest } = input;
12
+ const context = turnContext(client, server);
13
+ return context ? { ...rest, context } : rest;
14
+ }
15
+ /**
16
+ * What a live call's first message asks of the session, as `runSession` takes
17
+ * it: the page's context is the client's, and `server` is the host's own.
18
+ */
19
+ export function liveSessionOpen(open, server) {
20
+ const context = turnContext(open.context, server);
21
+ return {
22
+ ...(open.history ? { history: open.history } : {}),
23
+ ...(open.slots ? { slots: open.slots } : {}),
24
+ ...(context ? { context } : {}),
25
+ ...(open.resume
26
+ ? { providerState: open.resume.providerState, awayMs: open.resume.awayMs }
27
+ : {}),
28
+ };
29
+ }
@@ -0,0 +1,134 @@
1
+ /* Shared ink waveform + borderless controls (live shell and turn voice composer). */
2
+
3
+ :root {
4
+ --ink-ease: cubic-bezier(0.22, 1, 0.36, 1);
5
+ --ink-duration: 0.3s;
6
+ }
7
+
8
+ .ink-state {
9
+ margin: 0;
10
+ font-size: 0.75rem;
11
+ font-weight: 700;
12
+ letter-spacing: 0.06em;
13
+ text-transform: lowercase;
14
+ color: var(--color-ink);
15
+ }
16
+
17
+ .ink-wave-slot {
18
+ flex: 1 1 auto;
19
+ display: flex;
20
+ align-items: flex-end;
21
+ justify-content: center;
22
+ min-height: min(52vh, 22rem);
23
+ margin-bottom: 1.5rem;
24
+ padding: 0 0.5rem;
25
+ }
26
+
27
+ .ink-wave {
28
+ display: block;
29
+ width: min(100%, 36rem);
30
+ height: min(52vh, 22rem);
31
+ color: var(--color-ink);
32
+ }
33
+
34
+ .ink-wave--hero {
35
+ width: min(100%, 56rem);
36
+ height: 100%;
37
+ min-height: 40dvh;
38
+ max-height: 80dvh;
39
+ }
40
+
41
+ .ink-wave__bar {
42
+ stroke: currentcolor;
43
+ stroke-linecap: square;
44
+ }
45
+
46
+ .ink-divider {
47
+ flex-shrink: 0;
48
+ width: 100%;
49
+ height: 0;
50
+ margin: 0 0 1.5rem;
51
+ border: none;
52
+ border-top: 1px solid rgba(0, 0, 0, 0.14);
53
+ }
54
+
55
+ .ink-controls {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: space-between;
59
+ gap: 1rem;
60
+ flex-shrink: 0;
61
+ }
62
+
63
+ .ink-controls__left,
64
+ .ink-controls__right {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 0.65rem;
68
+ }
69
+
70
+ .ink-control {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ width: 2.5rem;
75
+ height: 2.5rem;
76
+ padding: 0;
77
+ color: var(--color-ink);
78
+ background: transparent;
79
+ border: none;
80
+ cursor: pointer;
81
+ opacity: 0.55;
82
+ transition: opacity var(--ink-duration) var(--ink-ease);
83
+ }
84
+
85
+ .ink-control:not(:disabled):hover {
86
+ opacity: 1;
87
+ }
88
+
89
+ .ink-control:disabled {
90
+ opacity: 0.22;
91
+ cursor: default;
92
+ }
93
+
94
+ .ink-control--active {
95
+ opacity: 0.9;
96
+ }
97
+
98
+ .ink-tooltip {
99
+ position: relative;
100
+ display: inline-flex;
101
+ }
102
+
103
+ .ink-tooltip__label {
104
+ position: absolute;
105
+ left: 50%;
106
+ z-index: 6;
107
+ padding: 0.28rem 0.45rem;
108
+ font-family: var(--font-mono, ui-monospace, monospace);
109
+ font-size: 0.62rem;
110
+ font-weight: 700;
111
+ letter-spacing: 0.08em;
112
+ line-height: 1.2;
113
+ text-transform: lowercase;
114
+ white-space: nowrap;
115
+ color: var(--color-paper, #f4f1ea);
116
+ background: var(--color-ink, #111);
117
+ border-radius: 0;
118
+ pointer-events: none;
119
+ transform: translateX(-50%);
120
+ }
121
+
122
+ .ink-tooltip__label--top {
123
+ bottom: calc(100% + 0.45rem);
124
+ }
125
+
126
+ .ink-tooltip__label--bottom {
127
+ top: calc(100% + 0.45rem);
128
+ }
129
+
130
+ @media (prefers-reduced-motion: reduce) {
131
+ .ink-control {
132
+ transition: none;
133
+ }
134
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Cards in as many columns as fit, each as tall as its content. Each card,
4
+ * in order, goes under the shortest column so far, so short cards never
5
+ * leave a hole beside a tall one.
6
+ */
7
+ export declare function CardWall({ minWidth, children }: {
8
+ minWidth: number;
9
+ children: ReactNode;
10
+ }): import("react").JSX.Element;
package/ui/CardWall.js ADDED
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { HStack } from '@astryxdesign/core/HStack';
3
+ import { StackItem } from '@astryxdesign/core/Stack';
4
+ import { VStack } from '@astryxdesign/core/VStack';
5
+ import { Children, useLayoutEffect, useRef, useState } from 'react';
6
+ import { columnCount, wallColumns } from "./wall-columns.js";
7
+ /** The wall's width and gap, and each card's height by its place in `children`. */
8
+ function measureWall(wall, stacks, placed, cards) {
9
+ const heights = Array.from({ length: cards });
10
+ placed.forEach((column, at) => {
11
+ const shown = stacks[at]?.children;
12
+ column.forEach((card, row) => {
13
+ const element = shown?.[row];
14
+ if (element instanceof HTMLElement)
15
+ heights[card] = element.offsetHeight;
16
+ });
17
+ });
18
+ const gap = Number.parseFloat(getComputedStyle(wall).columnGap) || 0;
19
+ return { width: wall.clientWidth, gap, heights };
20
+ }
21
+ function sameWall(a, b) {
22
+ return (a.width === b.width &&
23
+ a.gap === b.gap &&
24
+ a.heights.length === b.heights.length &&
25
+ a.heights.every((height, at) => height === b.heights[at]));
26
+ }
27
+ /**
28
+ * Cards in as many columns as fit, each as tall as its content. Each card,
29
+ * in order, goes under the shortest column so far, so short cards never
30
+ * leave a hole beside a tall one.
31
+ */
32
+ export function CardWall({ minWidth, children }) {
33
+ const cards = Children.toArray(children);
34
+ const wallRef = useRef(null);
35
+ const stacks = useRef([]);
36
+ const [wall, setWall] = useState({ width: 0, gap: 0, heights: [] });
37
+ const placed = wallColumns(cards.map((_, at) => wall.heights[at]), columnCount(wall.width, minWidth, wall.gap), wall.gap);
38
+ const placedRef = useRef(placed);
39
+ placedRef.current = placed;
40
+ const layout = placed.map((column) => column.join(' ')).join('|');
41
+ useLayoutEffect(() => {
42
+ const node = wallRef.current;
43
+ if (!node || !layout)
44
+ return;
45
+ const measure = () => {
46
+ const next = measureWall(node, stacks.current, placedRef.current, cards.length);
47
+ setWall((shown) => (sameWall(shown, next) ? shown : next));
48
+ };
49
+ measure();
50
+ const observer = new ResizeObserver(measure);
51
+ observer.observe(node);
52
+ for (const stack of stacks.current) {
53
+ for (const card of stack?.children ?? [])
54
+ observer.observe(card);
55
+ }
56
+ return () => observer.disconnect();
57
+ }, [layout, cards.length]);
58
+ return (_jsx(HStack, { ref: wallRef, gap: 3, vAlign: "start", children: placed.map((column, at) => (_jsx(StackItem, { size: "fill", children: _jsx(VStack, { ref: (node) => {
59
+ stacks.current[at] = node;
60
+ }, gap: 3, children: column.map((card) => cards[card]) }) }, String(at)))) }));
61
+ }
@@ -0,0 +1,62 @@
1
+ import { type ChatComposerInputHandle } from '@astryxdesign/core/Chat';
2
+ import { type AttachmentValidationIssue, type ComposerMenuAction, type ComposerPendingMessage, type ComposerProfileInterface, type ComposerRunPhase, type LiveProfileInterface } from '@theoremjs/agents/interface';
3
+ import type { ClientFailure } from '../client/failure.ts';
4
+ /** A slot the profile declares: its allowed values, the one chosen, and how to choose another. */
5
+ /** The effort selector's icon. The studio's Agents list shows the same one. */
6
+ export declare const EFFORT_ICON: import("react").ForwardRefExoticComponent<import("@tabler/icons-react").IconProps & import("react").RefAttributes<SVGSVGElement>>;
7
+ export type SlotChoices = {
8
+ declared: Readonly<Record<string, readonly string[]>>;
9
+ values: Readonly<Record<string, string>>;
10
+ onChange: (name: string, value: string) => void;
11
+ };
12
+ export type ChatComposerBarProps = {
13
+ iface: ComposerProfileInterface;
14
+ draftText: string;
15
+ pendingFiles: readonly File[];
16
+ pendingVoice: readonly File[];
17
+ pendingMessages: readonly ComposerPendingMessage[];
18
+ /** Files the last send refused, one per reason; worded with the profile's labels. */
19
+ issues: readonly AttachmentValidationIssue[];
20
+ phase: ComposerRunPhase;
21
+ failure: ClientFailure | null;
22
+ selectedModel?: string;
23
+ selectedEffort?: string;
24
+ placeholder?: string;
25
+ /** Imperative handle for the editor (focus, insert). */
26
+ inputRef?: React.Ref<ChatComposerInputHandle>;
27
+ onDraftTextChange: (text: string) => void;
28
+ onFilesSelected: (files: File[]) => void;
29
+ onAttachmentRemove: (index: number) => void;
30
+ onVoiceStaged: (file: File) => void;
31
+ onVoiceClear: () => void;
32
+ onSubmit: () => void;
33
+ onStop: () => void;
34
+ onMenuAction: (action: ComposerMenuAction) => void;
35
+ onGenerationChange: (next: {
36
+ modelId: string;
37
+ effort?: string;
38
+ }) => void;
39
+ /** The profile's `inputs.slots` with the value chosen for each; one selector per slot, beside the model's. */
40
+ slotChoices?: SlotChoices;
41
+ onPendingMove: (id: string, direction: 'up' | 'down') => void;
42
+ onPendingRemove: (id: string) => void;
43
+ onPendingQueue: (id: string) => void;
44
+ onPendingRestore: (id: string) => void;
45
+ onPendingSendNow: (id: string) => void;
46
+ };
47
+ /** A Live call's text line: it is sent as typed, so there is nothing to attach, queue, or stop. */
48
+ export type LiveComposerBarProps = Omit<Pick<ChatComposerBarProps, 'draftText' | 'placeholder' | 'inputRef' | 'onDraftTextChange' | 'onSubmit'>, 'onSubmit'> & {
49
+ /** The typed line. The button reads the field, so this is the text, not the click. */
50
+ onSubmit: (text: string) => void;
51
+ /** The live profile, when the host has one. The placeholder uses its handle. */
52
+ iface?: LiveProfileInterface;
53
+ /** Profile handle, when the host has no interface object. Wins over `iface`. */
54
+ handle?: string;
55
+ /** Off while no call is connected. */
56
+ isDisabled: boolean;
57
+ };
58
+ /**
59
+ * Astryx composer wired to Theorem's send / stop / queue / steer / stash matrix.
60
+ * For a Live profile it is the text field and send button alone.
61
+ */
62
+ export declare function ChatComposerBar(props: ChatComposerBarProps | LiveComposerBarProps): import("react").JSX.Element;
@@ -0,0 +1,306 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Badge } from '@astryxdesign/core/Badge';
3
+ import { Button } from '@astryxdesign/core/Button';
4
+ import { ChatComposer, ChatComposerDrawer, ChatComposerInput, ChatSendButton, } from '@astryxdesign/core/Chat';
5
+ import { DropdownMenu } from '@astryxdesign/core/DropdownMenu';
6
+ import { HStack } from '@astryxdesign/core/HStack';
7
+ import { IconButton } from '@astryxdesign/core/IconButton';
8
+ import { Kbd } from '@astryxdesign/core/Kbd';
9
+ import { Selector } from '@astryxdesign/core/Selector';
10
+ import { StackItem } from '@astryxdesign/core/Stack';
11
+ import { Text } from '@astryxdesign/core/Text';
12
+ import { Thumbnail } from '@astryxdesign/core/Thumbnail';
13
+ import { Token } from '@astryxdesign/core/Token';
14
+ import { VStack } from '@astryxdesign/core/VStack';
15
+ import { IconAdjustmentsHorizontal, IconArrowDown, IconArrowUp, IconBrain, IconCornerDownLeft, IconInputAi, IconMicrophone, IconPaperclip, IconPencil, IconPlayerRecordFilled, IconSend, IconStack2, IconX, } from '@tabler/icons-react';
16
+ import { attachmentIssueText, labelText } from '@theoremjs/agents';
17
+ import { composerPendingPreview, effortSelectEnabled, interfaceEffortOptions, interfaceModelOptions, modelSelectEnabled, } from '@theoremjs/agents/interface';
18
+ import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
19
+ import { stageComposerFiles } from "../client/composer-attachments.js";
20
+ import { composerDrawerSummary } from "../client/composer-drawer.js";
21
+ import { isStashShortcut, resolveComposerHint, STASH_SHORTCUT, } from "../client/composer-hints.js";
22
+ import { composerActionState } from "../client/composer-primary.js";
23
+ import { useComposerVoice } from "../components/use-composer-voice.js";
24
+ import { useEditorSelection } from "../components/use-editor-selection.js";
25
+ import { composerDrawerLabel } from "./labels.js";
26
+ import { TheoremLabelsProvider, useLabels } from "./labels-provider.js";
27
+ import { VoiceNote } from "./VoiceNote.js";
28
+ const NO_FOCUS_RING = { '--focus-outline-width': '0px' };
29
+ /** A slot the profile declares: its allowed values, the one chosen, and how to choose another. */
30
+ /** The effort selector's icon. The studio's Agents list shows the same one. */
31
+ export const EFFORT_ICON = IconBrain;
32
+ const isImage = (file) => file.type.startsWith('image/');
33
+ const anyFile = () => true;
34
+ /** Object URLs for the files `keep` (a module-level predicate) accepts, revoked when the files change. */
35
+ function useObjectUrls(files, keep) {
36
+ const urls = useMemo(() => files.map((file) => (keep(file) ? URL.createObjectURL(file) : undefined)), [files, keep]);
37
+ useEffect(() => () => {
38
+ for (const url of urls)
39
+ if (url)
40
+ URL.revokeObjectURL(url);
41
+ }, [urls]);
42
+ return urls;
43
+ }
44
+ function PendingRow(props) {
45
+ const { message } = props;
46
+ const t = useLabels();
47
+ const icon = (Glyph) => _jsx(Glyph, { size: 14 });
48
+ const edit = t('@theorem.composer.pending.edit');
49
+ const queue = t('@theorem.composer.pending.queue_action');
50
+ const sendNow = t('@theorem.composer.pending.send_now');
51
+ const moveUp = t('@theorem.composer.pending.move_up');
52
+ const moveDown = t('@theorem.composer.pending.move_down');
53
+ const remove = t('@theorem.composer.pending.remove');
54
+ return (_jsxs(HStack, { gap: 1, align: "center", width: "100%", children: [_jsx(Badge, { variant: message.kind === 'steer' ? 'info' : 'neutral', label: t(`@theorem.composer.pending.${message.kind}`) }), _jsx(StackItem, { size: "fill", children: _jsx(Text, { size: "sm", maxLines: 1, hasTruncateTooltip: true, children: composerPendingPreview(message) }) }), _jsxs(HStack, { gap: 0.5, children: [_jsx(IconButton, { label: edit, tooltip: edit, size: "sm", variant: "ghost", icon: icon(IconPencil), onClick: () => props.onRestore(message.id) }), message.kind === 'stash' ? (_jsx(IconButton, { label: queue, tooltip: queue, size: "sm", variant: "ghost", icon: icon(IconCornerDownLeft), onClick: () => props.onQueue(message.id) })) : null, _jsx(IconButton, { label: sendNow, tooltip: sendNow, size: "sm", variant: "ghost", icon: icon(IconSend), onClick: () => props.onSendNow(message.id) }), _jsx(IconButton, { label: moveUp, tooltip: moveUp, size: "sm", variant: "ghost", icon: icon(IconArrowUp), onClick: () => props.onMove(message.id, 'up') }), _jsx(IconButton, { label: moveDown, tooltip: moveDown, size: "sm", variant: "ghost", icon: icon(IconArrowDown), onClick: () => props.onMove(message.id, 'down') }), _jsx(IconButton, { label: remove, tooltip: remove, size: "sm", variant: "ghost", icon: icon(IconX), onClick: () => props.onRemove(message.id) })] })] }));
55
+ }
56
+ /** The model and effort choices this profile offers; efforts only for a chosen model. */
57
+ function generationOptions(iface, selectedModel) {
58
+ const models = modelSelectEnabled(iface) ? interfaceModelOptions(iface) : [];
59
+ const efforts = selectedModel && effortSelectEnabled(iface, selectedModel)
60
+ ? interfaceEffortOptions(iface, selectedModel)
61
+ : [];
62
+ return { models, efforts };
63
+ }
64
+ function ModelSelector(props) {
65
+ if (props.models.length === 0)
66
+ return null;
67
+ return (_jsx(Selector, { label: props.label, isLabelHidden: true, size: "sm", variant: "ghost", startIcon: _jsx(IconInputAi, { size: 14 }), placement: "above", isDisabled: props.isDisabled, value: props.value ?? '', options: props.models.map((m) => ({
68
+ value: m.id,
69
+ label: m.id,
70
+ description: m.label !== m.id ? m.label : undefined,
71
+ })), onChange: props.onChange }));
72
+ }
73
+ function EffortSelector(props) {
74
+ if (props.efforts.length === 0)
75
+ return null;
76
+ return (_jsx(Selector, { label: props.label, isLabelHidden: true, size: "sm", variant: "ghost", startIcon: _jsx(EFFORT_ICON, { size: 14 }), placement: "above", isDisabled: props.isDisabled, value: props.value ?? '', options: props.efforts.map((e) => ({ value: e.alias, label: e.alias, description: e.level })), onChange: props.onChange }));
77
+ }
78
+ function SlotSelectors(props) {
79
+ if (!props.choices)
80
+ return null;
81
+ const { declared, values, onChange } = props.choices;
82
+ return Object.entries(declared).map(([name, allowed]) => (_jsx(Selector, { label: name, isLabelHidden: true, size: "sm", variant: "ghost", startIcon: _jsx(IconAdjustmentsHorizontal, { size: 14 }), placement: "above", isDisabled: props.isDisabled, value: values[name] ?? '', options: allowed.map((value) => ({ value, label: value })), onChange: (value) => onChange(name, value) }, name)));
83
+ }
84
+ function GenerationSelect(props) {
85
+ const { models, efforts } = generationOptions(props.iface, props.selectedModel);
86
+ const slotCount = Object.keys(props.slotChoices?.declared ?? {}).length;
87
+ if (models.length + efforts.length + slotCount === 0)
88
+ return null;
89
+ return (_jsxs(HStack, { gap: 1, children: [_jsx(ModelSelector, { label: labelText('composer.model', {}, props.iface.labels), models: models, value: props.selectedModel, isDisabled: props.isDisabled, onChange: (modelId) => props.onChange({ modelId }) }), _jsx(EffortSelector, { label: labelText('composer.effort', {}, props.iface.labels), efforts: efforts, value: props.selectedEffort, isDisabled: props.isDisabled, onChange: (effort) => {
90
+ if (props.selectedModel)
91
+ props.onChange({ modelId: props.selectedModel, effort });
92
+ } }), _jsx(SlotSelectors, { choices: props.slotChoices, isDisabled: props.isDisabled })] }));
93
+ }
94
+ function composerStatus(args) {
95
+ if (args.failure)
96
+ return { type: 'error', message: args.failure.error };
97
+ const warning = [
98
+ ...args.issues.map((issue) => attachmentIssueText(issue, args.labels)),
99
+ args.voiceFailure?.error,
100
+ ]
101
+ .filter(Boolean)
102
+ .join(' ');
103
+ return warning ? { type: 'warning', message: warning } : undefined;
104
+ }
105
+ /** Pending messages, then two uniform rows: 64px tiles (images, voice notes), then file tokens. */
106
+ function PendingDrawer(props) {
107
+ const t = useLabels();
108
+ return (_jsx(ChatComposerDrawer, { count: props.summary.count, label: composerDrawerLabel(t, props.summary), children: _jsxs(VStack, { gap: 2, width: "100%", children: [props.messages.map((message) => (_jsx(PendingRow, { message: message, ...props.pendingActions }, message.id))), props.imageFiles.length + props.voiceFiles.length > 0 ? (_jsxs(HStack, { gap: 2, wrap: "wrap", vAlign: "center", children: [props.imageFiles.map(({ file, index, preview }) => (_jsx(Thumbnail, { src: preview, alt: file.name, label: file.name, onRemove: () => props.onAttachmentRemove(index) }, `${file.name}:${String(index)}`))), props.voiceFiles.map((file, index) => (_jsx(VoiceNote, { src: props.voiceUrls[index] ?? '', mimeType: file.type || undefined, onRemove: props.onVoiceRemove }, `voice:${file.name}`)))] })) : null, props.otherFiles.length > 0 ? (_jsx(HStack, { gap: 2, wrap: "wrap", children: props.otherFiles.map(({ file, index }) => (_jsx(Token, { label: file.name, onRemove: () => props.onAttachmentRemove(index) }, `${file.name}:${String(index)}`))) })) : null] }) }));
109
+ }
110
+ /** A hidden file input behind the paperclip button. */
111
+ function AttachFilesButton({ accept, onFiles, }) {
112
+ const t = useLabels();
113
+ const label = t('@theorem.composer.attach');
114
+ const fileInputRef = useRef(null);
115
+ function handleChange(event) {
116
+ const input = event.currentTarget;
117
+ const incoming = input.files ? [...input.files] : [];
118
+ input.value = '';
119
+ if (incoming.length > 0)
120
+ onFiles(incoming);
121
+ }
122
+ return (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, hidden: true, accept: accept, onChange: handleChange }), _jsx(IconButton, { label: label, tooltip: label, size: "sm", variant: "ghost", icon: _jsx(IconPaperclip, { size: 16 }), onClick: () => fileInputRef.current?.click() })] }));
123
+ }
124
+ function SendMenu({ actions, onAction, }) {
125
+ const t = useLabels();
126
+ if (actions.length === 0)
127
+ return null;
128
+ return (_jsx(DropdownMenu, { button: {
129
+ label: t('@theorem.composer.send_options'),
130
+ isIconOnly: true,
131
+ icon: _jsx(IconStack2, { size: 16 }),
132
+ variant: 'ghost',
133
+ }, hasChevron: false, placement: "above", items: actions.map((action) => ({
134
+ id: action,
135
+ label: t(`@theorem.composer.menu.${action}`),
136
+ description: t(`@theorem.composer.menu.${action}.description`),
137
+ ...(action === 'stash' ? { endContent: _jsx(Kbd, { keys: STASH_SHORTCUT }) } : {}),
138
+ onClick: () => onAction(action),
139
+ })) }));
140
+ }
141
+ function RecordButton({ recording, onToggle }) {
142
+ const t = useLabels();
143
+ const label = t(recording ? '@theorem.composer.stop_recording' : '@theorem.composer.record');
144
+ return (_jsx(IconButton, { label: label, tooltip: label, size: "md", variant: recording ? 'destructive' : 'ghost', icon: recording ? _jsx(IconPlayerRecordFilled, { size: 16 }) : _jsx(IconMicrophone, { size: 16 }), onClick: onToggle }));
145
+ }
146
+ /** Astryx's slot for contextual info (header, right side). */
147
+ function ComposerHint({ hint, onAction }) {
148
+ const t = useLabels();
149
+ return (_jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(Text, { size: "sm", color: "secondary", children: t(`@theorem.composer.hint.${hint.id}.message`) }), _jsx(Button, { label: t(`@theorem.composer.hint.${hint.id}.action`), size: "sm", variant: "ghost", onClick: onAction }), _jsx(Kbd, { keys: hint.shortcut })] }));
150
+ }
151
+ /** Staged files split into image tiles and file tokens, with object URLs for previews and voice notes. */
152
+ function useStagedFiles(pendingFiles, pendingVoice) {
153
+ const previews = useObjectUrls(pendingFiles, isImage);
154
+ const voiceUrls = useObjectUrls(pendingVoice, anyFile);
155
+ const staged = pendingFiles.map((file, index) => ({
156
+ file,
157
+ index,
158
+ preview: previews[index],
159
+ }));
160
+ return {
161
+ imageFiles: staged.filter((entry) => entry.preview !== undefined),
162
+ otherFiles: staged.filter((entry) => entry.preview === undefined),
163
+ voiceUrls,
164
+ };
165
+ }
166
+ /** Stages picked files beside the ones already waiting, and keeps what could not be staged. */
167
+ function useFileStaging(pendingFiles, maxFiles, props) {
168
+ const [stagingIssues, setStagingIssues] = useState([]);
169
+ function stageFiles(incoming) {
170
+ const staged = stageComposerFiles({
171
+ existing: pendingFiles,
172
+ incoming,
173
+ maxFiles,
174
+ voiceCount: props.pendingVoice.length,
175
+ });
176
+ const added = staged.files.slice(pendingFiles.length);
177
+ if (added.length > 0)
178
+ props.onFilesSelected(added);
179
+ setStagingIssues(staged.issues);
180
+ }
181
+ return { stageFiles, stagingIssues };
182
+ }
183
+ /** The record button, when the profile takes voice. */
184
+ function ComposerRecordButton({ hasVoice, voice, }) {
185
+ if (!hasVoice)
186
+ return null;
187
+ return _jsx(RecordButton, { recording: voice.recording, onToggle: () => void voice.toggleRecording() });
188
+ }
189
+ /** The drawer of messages and files waiting to send; absent while nothing waits. */
190
+ function composerDrawer(props, stagedFiles, onVoiceRemove) {
191
+ const summary = composerDrawerSummary({
192
+ pendingMessages: props.pendingMessages,
193
+ attachmentCount: props.pendingFiles.length + props.pendingVoice.length,
194
+ });
195
+ if (!summary)
196
+ return undefined;
197
+ return (_jsx(PendingDrawer, { summary: summary, messages: props.pendingMessages, ...stagedFiles, voiceFiles: props.pendingVoice, pendingActions: {
198
+ onMove: props.onPendingMove,
199
+ onRemove: props.onPendingRemove,
200
+ onQueue: props.onPendingQueue,
201
+ onRestore: props.onPendingRestore,
202
+ onSendNow: props.onPendingSendNow,
203
+ }, onAttachmentRemove: props.onAttachmentRemove, onVoiceRemove: onVoiceRemove }));
204
+ }
205
+ /** The text field, with Seance's stash shortcut while it has focus; `onStash` is absent when there is nothing to stash. */
206
+ function ComposerTextInput({ editorRef, handleRef, onStash, }) {
207
+ return (_jsx(ChatComposerInput, { ref: editorRef, handleRef: handleRef, onKeyDown: (event) => {
208
+ if (!isStashShortcut(event))
209
+ return;
210
+ event.preventDefault();
211
+ onStash?.();
212
+ } }));
213
+ }
214
+ function isLiveComposer(props) {
215
+ return 'isDisabled' in props;
216
+ }
217
+ /**
218
+ * Astryx composer wired to Theorem's send / stop / queue / steer / stash matrix.
219
+ * For a Live profile it is the text field and send button alone.
220
+ */
221
+ export function ChatComposerBar(props) {
222
+ return (_jsx(TheoremLabelsProvider, { children: isLiveComposer(props) ? (_jsx(LiveComposerBarBody, { ...props })) : (_jsx(ChatComposerBarBody, { ...props })) }));
223
+ }
224
+ function LiveComposerBarBody(props) {
225
+ const t = useLabels();
226
+ const editorRef = useRef(null);
227
+ const setEditor = useCallback((node) => {
228
+ editorRef.current = node;
229
+ const outer = props.inputRef;
230
+ if (typeof outer === 'function')
231
+ outer(node);
232
+ else if (outer)
233
+ outer.current = node;
234
+ }, [props.inputRef]);
235
+ // why: The button used to send only the React draft. Typing lives in the field first, so an
236
+ // empty draft left the button dead. Read the field.
237
+ const submitTyped = () => {
238
+ const text = (editorRef.current?.getValue() ?? props.draftText).trim();
239
+ if (!text || props.isDisabled)
240
+ return;
241
+ props.onSubmit(text);
242
+ props.onDraftTextChange('');
243
+ };
244
+ return (_jsx(ChatComposer, { style: NO_FOCUS_RING, value: props.draftText, onChange: props.onDraftTextChange, onSubmit: props.onSubmit, isDisabled: props.isDisabled, placeholder: props.placeholder ??
245
+ t('@theorem.composer.placeholder', {
246
+ handle: props.handle ?? props.iface?.identity.handle ?? '',
247
+ }), input: _jsx(ChatComposerInput, { handleRef: setEditor }), sendButton: _jsx(ChatSendButton, { isDisabled: props.isDisabled, onSend: submitTyped }) }));
248
+ }
249
+ function ChatComposerBarBody(props) {
250
+ const { iface, phase } = props;
251
+ const t = useLabels();
252
+ const inputs = iface.inputs;
253
+ const pendingFiles = useMemo(() => [...props.pendingFiles], [props.pendingFiles]);
254
+ const voice = useComposerVoice({
255
+ inputs,
256
+ labels: iface.labels,
257
+ pendingFiles,
258
+ onVoiceStaged: props.onVoiceStaged,
259
+ onVoiceClear: props.onVoiceClear,
260
+ });
261
+ const stagedFiles = useStagedFiles(props.pendingFiles, props.pendingVoice);
262
+ useEffect(() => () => voice.disposeRecorder(), [voice.disposeRecorder]);
263
+ const { primary, menuActions, primaryDisabled } = composerActionState({
264
+ iface,
265
+ phase,
266
+ draftText: props.draftText,
267
+ pendingFiles: props.pendingFiles,
268
+ pendingVoice: props.pendingVoice,
269
+ recording: voice.recording,
270
+ });
271
+ const canStash = menuActions.includes('stash');
272
+ const editorRef = useRef(null);
273
+ const hint = resolveComposerHint({
274
+ draftText: props.draftText,
275
+ selectedText: useEditorSelection(editorRef),
276
+ canStash,
277
+ });
278
+ function runPrimary() {
279
+ if (primaryDisabled)
280
+ return;
281
+ if (primary === 'stop')
282
+ props.onStop();
283
+ else
284
+ props.onSubmit();
285
+ }
286
+ const { stageFiles, stagingIssues } = useFileStaging(pendingFiles, inputs.maxFiles, props);
287
+ return (_jsx(ChatComposer
288
+ // why: No keyboard focus ring on the composer (product choice): zero Astryx's
289
+ // focus-outline token for this subtree. The editor still shows its caret.
290
+ , {
291
+ // why: No keyboard focus ring on the composer (product choice): zero Astryx's
292
+ // focus-outline token for this subtree. The editor still shows its caret.
293
+ style: NO_FOCUS_RING, value: props.draftText, onChange: props.onDraftTextChange, onSubmit: runPrimary, onStop: props.onStop, isStopShown: primary === 'stop', placeholder: voice.recording
294
+ ? t('@theorem.composer.listening')
295
+ : (props.placeholder ??
296
+ t('@theorem.composer.placeholder', { handle: iface.identity.handle })),
297
+ // why: A profile that takes no text has no text field (false, not null: null gets Astryx's default one).
298
+ input: inputs.text && (_jsx(ComposerTextInput, { editorRef: editorRef, handleRef: props.inputRef, onStash: canStash ? () => props.onMenuAction('stash') : undefined })), drawer: composerDrawer(props, stagedFiles, () => voice.discardRecordingOrVoice()), headerActions: inputs.attachments ? (_jsx(AttachFilesButton, { accept: inputs.attachments.acceptAttr, onFiles: stageFiles })) : undefined, headerContext: hint ? _jsx(ComposerHint, { hint: hint, onAction: () => props.onMenuAction('stash') }) : undefined, footerActions: _jsx(GenerationSelect, { iface: iface, slotChoices: props.slotChoices, selectedModel: props.selectedModel, selectedEffort: props.selectedEffort, isDisabled: phase === 'streaming', onChange: props.onGenerationChange }),
299
+ // why: Astryx: sendActions render to the left of the send button, at size="md".
300
+ sendActions: _jsxs(_Fragment, { children: [_jsx(SendMenu, { actions: menuActions, onAction: props.onMenuAction }), _jsx(ComposerRecordButton, { hasVoice: Boolean(inputs.voice), voice: voice })] }), sendButton: _jsx(ChatSendButton, { isStopShown: primary === 'stop', isDisabled: primaryDisabled, onSend: runPrimary, onStop: props.onStop }), status: composerStatus({
301
+ failure: props.failure,
302
+ issues: [...props.issues, ...stagingIssues],
303
+ voiceFailure: voice.failure,
304
+ labels: iface.labels,
305
+ }) }));
306
+ }
@@ -0,0 +1,43 @@
1
+ import { type ChatMessageStatus } from '@astryxdesign/core/Chat';
2
+ import type { LabelOverrides } from '@theoremjs/agents';
3
+ import { type TranscriptBlock } from '@theoremjs/agents/interface';
4
+ import { type ReactNode } from 'react';
5
+ import type { AnsweringGate } from '../client/tool-resume.ts';
6
+ import { type ToolDecision } from './ToolGateCard.tsx';
7
+ export type ChatTranscriptProps = {
8
+ blocks: readonly TranscriptBlock[];
9
+ /** Assistant display name (profile handle). */
10
+ handle: string;
11
+ streaming?: boolean;
12
+ /** How far the latest message has got; shown on it unless its turn failed. */
13
+ delivery?: Exclude<ChatMessageStatus, 'error'> | null;
14
+ onToolDecision?: (index: number, action: ToolDecision) => void;
15
+ /** The answer on its way to a gate (`useTheoremChat().answering`). */
16
+ answering?: AnsweringGate | null;
17
+ /** Signed in at a gate: `secret` is a key the user typed; after an OAuth callback there is none. */
18
+ onAuthenticated?: (index: number, secret?: string) => void;
19
+ emptyState?: ReactNode;
20
+ /**
21
+ * Labelled dividers, keyed by block id: each is drawn above the turn that
22
+ * starts at its block, and that block always starts a turn of its own.
23
+ */
24
+ dividers?: Readonly<Record<string, string>>;
25
+ /**
26
+ * Set for image profiles: generated images show large, framed to their own
27
+ * aspect ratio, with a matching skeleton while one generates.
28
+ */
29
+ imageOutput?: ImageOutput;
30
+ /**
31
+ * Show what was used, as tokens and cost: each reply's total under it, and
32
+ * on an agent tool's call row what the agent it ran used. Off by default.
33
+ */
34
+ usage?: boolean;
35
+ /** The profile's labels (`iface.labels`): a stopped turn says why in its words. */
36
+ labels?: LabelOverrides;
37
+ };
38
+ /** Image-profile display: `ratio` is the profile's pinned aspect ratio, if any. */
39
+ export type ImageOutput = {
40
+ ratio?: number;
41
+ };
42
+ /** Theorem transcript blocks rendered as Astryx chat messages. */
43
+ export declare function ChatTranscript(props: ChatTranscriptProps): import("react").JSX.Element;