@assistant-ui/react-generative-ui 0.0.23 → 0.1.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 (338) hide show
  1. package/README.md +2 -0
  2. package/dist/a2ui.d.ts +1 -4
  3. package/dist/a2ui.d.ts.map +1 -1
  4. package/dist/a2ui.js +1 -4
  5. package/dist/index.d.ts +1 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -10
  8. package/dist/index.server.d.ts +1 -2
  9. package/dist/index.server.d.ts.map +1 -1
  10. package/dist/index.server.js +1 -10
  11. package/dist/ir.d.ts +1 -111
  12. package/dist/ir.d.ts.map +1 -1
  13. package/dist/ir.js +1 -175
  14. package/dist/slack.d.ts +1 -4
  15. package/dist/slack.d.ts.map +1 -1
  16. package/dist/slack.js +1 -4
  17. package/dist/teams.d.ts +1 -4
  18. package/dist/teams.d.ts.map +1 -1
  19. package/dist/teams.js +1 -4
  20. package/package.json +5 -5
  21. package/src/a2ui.ts +1 -22
  22. package/src/index.server.ts +1 -2
  23. package/src/index.test.ts +75 -0
  24. package/src/index.ts +1 -2
  25. package/src/ir.ts +1 -279
  26. package/src/slack.ts +1 -39
  27. package/src/teams.ts +1 -35
  28. package/dist/JSONGenerativeUI.client.d.ts +0 -28
  29. package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
  30. package/dist/JSONGenerativeUI.client.js +0 -101
  31. package/dist/JSONGenerativeUI.client.js.map +0 -1
  32. package/dist/JSONGenerativeUI.server.d.ts +0 -21
  33. package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
  34. package/dist/JSONGenerativeUI.server.js +0 -32
  35. package/dist/JSONGenerativeUI.server.js.map +0 -1
  36. package/dist/JSONGenerativeUI.shared.d.ts +0 -63
  37. package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
  38. package/dist/JSONGenerativeUI.shared.js +0 -31
  39. package/dist/JSONGenerativeUI.shared.js.map +0 -1
  40. package/dist/RadioGroupScope.d.ts +0 -6
  41. package/dist/RadioGroupScope.d.ts.map +0 -1
  42. package/dist/RadioGroupScope.js +0 -25
  43. package/dist/RadioGroupScope.js.map +0 -1
  44. package/dist/a2ui/BindingContext.d.ts +0 -11
  45. package/dist/a2ui/BindingContext.d.ts.map +0 -1
  46. package/dist/a2ui/BindingContext.js +0 -12
  47. package/dist/a2ui/BindingContext.js.map +0 -1
  48. package/dist/a2ui/PresentRenderer.d.ts +0 -11
  49. package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
  50. package/dist/a2ui/PresentRenderer.js +0 -39
  51. package/dist/a2ui/PresentRenderer.js.map +0 -1
  52. package/dist/a2ui/convert.d.ts +0 -15
  53. package/dist/a2ui/convert.d.ts.map +0 -1
  54. package/dist/a2ui/convert.js +0 -728
  55. package/dist/a2ui/convert.js.map +0 -1
  56. package/dist/a2ui/dataModel.d.ts +0 -9
  57. package/dist/a2ui/dataModel.d.ts.map +0 -1
  58. package/dist/a2ui/dataModel.js +0 -44
  59. package/dist/a2ui/dataModel.js.map +0 -1
  60. package/dist/a2ui/reducer.d.ts +0 -8
  61. package/dist/a2ui/reducer.d.ts.map +0 -1
  62. package/dist/a2ui/reducer.js +0 -242
  63. package/dist/a2ui/reducer.js.map +0 -1
  64. package/dist/a2ui/snapshot.d.ts +0 -3
  65. package/dist/a2ui/snapshot.d.ts.map +0 -1
  66. package/dist/a2ui/snapshot.js +0 -33
  67. package/dist/a2ui/snapshot.js.map +0 -1
  68. package/dist/a2ui/types.d.ts +0 -88
  69. package/dist/a2ui/types.d.ts.map +0 -1
  70. package/dist/a2ui/types.js +0 -6
  71. package/dist/a2ui/types.js.map +0 -1
  72. package/dist/a2ui/valueFunctions.d.ts +0 -16
  73. package/dist/a2ui/valueFunctions.d.ts.map +0 -1
  74. package/dist/a2ui/valueFunctions.js +0 -332
  75. package/dist/a2ui/valueFunctions.js.map +0 -1
  76. package/dist/actionRegistry.d.ts +0 -29
  77. package/dist/actionRegistry.d.ts.map +0 -1
  78. package/dist/actionRegistry.js +0 -41
  79. package/dist/actionRegistry.js.map +0 -1
  80. package/dist/answeredValues.d.ts +0 -16
  81. package/dist/answeredValues.d.ts.map +0 -1
  82. package/dist/answeredValues.js +0 -37
  83. package/dist/answeredValues.js.map +0 -1
  84. package/dist/buildPresentParameters.d.ts +0 -21
  85. package/dist/buildPresentParameters.d.ts.map +0 -1
  86. package/dist/buildPresentParameters.js +0 -95
  87. package/dist/buildPresentParameters.js.map +0 -1
  88. package/dist/constants.d.ts +0 -15
  89. package/dist/constants.d.ts.map +0 -1
  90. package/dist/constants.js +0 -19
  91. package/dist/constants.js.map +0 -1
  92. package/dist/convert/boundSpec.d.ts +0 -48
  93. package/dist/convert/boundSpec.d.ts.map +0 -1
  94. package/dist/convert/boundSpec.js +0 -108
  95. package/dist/convert/boundSpec.js.map +0 -1
  96. package/dist/convert/copyBounded.d.ts +0 -25
  97. package/dist/convert/copyBounded.d.ts.map +0 -1
  98. package/dist/convert/copyBounded.js +0 -38
  99. package/dist/convert/copyBounded.js.map +0 -1
  100. package/dist/convert/isElement.d.ts +0 -3
  101. package/dist/convert/isElement.d.ts.map +0 -1
  102. package/dist/convert/isElement.js +0 -6
  103. package/dist/convert/isElement.js.map +0 -1
  104. package/dist/convert/takeRun.d.ts +0 -6
  105. package/dist/convert/takeRun.d.ts.map +0 -1
  106. package/dist/convert/takeRun.js +0 -20
  107. package/dist/convert/takeRun.js.map +0 -1
  108. package/dist/defineGenerativeComponents.d.ts +0 -29
  109. package/dist/defineGenerativeComponents.d.ts.map +0 -1
  110. package/dist/defineGenerativeComponents.js +0 -34
  111. package/dist/defineGenerativeComponents.js.map +0 -1
  112. package/dist/fieldReferences.d.ts +0 -9
  113. package/dist/fieldReferences.d.ts.map +0 -1
  114. package/dist/fieldReferences.js +0 -53
  115. package/dist/fieldReferences.js.map +0 -1
  116. package/dist/generativeUIToJSX.d.ts +0 -18
  117. package/dist/generativeUIToJSX.d.ts.map +0 -1
  118. package/dist/generativeUIToJSX.js +0 -76
  119. package/dist/generativeUIToJSX.js.map +0 -1
  120. package/dist/index.shared.d.ts +0 -15
  121. package/dist/index.shared.d.ts.map +0 -1
  122. package/dist/index.shared.js +0 -9
  123. package/dist/ir.js.map +0 -1
  124. package/dist/renderGenerativeUI.d.ts +0 -14
  125. package/dist/renderGenerativeUI.d.ts.map +0 -1
  126. package/dist/renderGenerativeUI.js +0 -83
  127. package/dist/renderGenerativeUI.js.map +0 -1
  128. package/dist/scopeSchema.d.ts +0 -6
  129. package/dist/scopeSchema.d.ts.map +0 -1
  130. package/dist/scopeSchema.js +0 -56
  131. package/dist/scopeSchema.js.map +0 -1
  132. package/dist/slack/constants.d.ts +0 -93
  133. package/dist/slack/constants.d.ts.map +0 -1
  134. package/dist/slack/constants.js +0 -146
  135. package/dist/slack/constants.js.map +0 -1
  136. package/dist/slack/decodeBlockAction.d.ts +0 -10
  137. package/dist/slack/decodeBlockAction.d.ts.map +0 -1
  138. package/dist/slack/decodeBlockAction.js +0 -129
  139. package/dist/slack/decodeBlockAction.js.map +0 -1
  140. package/dist/slack/fromSlackBlocks.d.ts +0 -27
  141. package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
  142. package/dist/slack/fromSlackBlocks.js +0 -483
  143. package/dist/slack/fromSlackBlocks.js.map +0 -1
  144. package/dist/slack/toSlackBlocks.d.ts +0 -4
  145. package/dist/slack/toSlackBlocks.d.ts.map +0 -1
  146. package/dist/slack/toSlackBlocks.js +0 -901
  147. package/dist/slack/toSlackBlocks.js.map +0 -1
  148. package/dist/slack/types.d.ts +0 -204
  149. package/dist/slack/types.d.ts.map +0 -1
  150. package/dist/slack/types.js +0 -0
  151. package/dist/teams/constants.d.ts +0 -34
  152. package/dist/teams/constants.d.ts.map +0 -1
  153. package/dist/teams/constants.js +0 -68
  154. package/dist/teams/constants.js.map +0 -1
  155. package/dist/teams/decodeSubmitData.d.ts +0 -26
  156. package/dist/teams/decodeSubmitData.d.ts.map +0 -1
  157. package/dist/teams/decodeSubmitData.js +0 -70
  158. package/dist/teams/decodeSubmitData.js.map +0 -1
  159. package/dist/teams/temporalId.d.ts +0 -5
  160. package/dist/teams/temporalId.d.ts.map +0 -1
  161. package/dist/teams/temporalId.js +0 -28
  162. package/dist/teams/temporalId.js.map +0 -1
  163. package/dist/teams/toAdaptiveCard.d.ts +0 -45
  164. package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
  165. package/dist/teams/toAdaptiveCard.js +0 -642
  166. package/dist/teams/toAdaptiveCard.js.map +0 -1
  167. package/dist/teams/toTeamsAttachments.d.ts +0 -10
  168. package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
  169. package/dist/teams/toTeamsAttachments.js +0 -99
  170. package/dist/teams/toTeamsAttachments.js.map +0 -1
  171. package/dist/teams/types.d.ts +0 -224
  172. package/dist/teams/types.d.ts.map +0 -1
  173. package/dist/teams/types.js +0 -0
  174. package/dist/temporal.d.ts +0 -33
  175. package/dist/temporal.d.ts.map +0 -1
  176. package/dist/temporal.js +0 -152
  177. package/dist/temporal.js.map +0 -1
  178. package/dist/types.d.ts +0 -99
  179. package/dist/types.d.ts.map +0 -1
  180. package/dist/types.js +0 -0
  181. package/dist/vocabulary/alert.d.ts +0 -32
  182. package/dist/vocabulary/alert.d.ts.map +0 -1
  183. package/dist/vocabulary/alert.js +0 -160
  184. package/dist/vocabulary/alert.js.map +0 -1
  185. package/dist/vocabulary/collectFormValues.d.ts +0 -23
  186. package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
  187. package/dist/vocabulary/collectFormValues.js +0 -44
  188. package/dist/vocabulary/collectFormValues.js.map +0 -1
  189. package/dist/vocabulary/data.d.ts +0 -66
  190. package/dist/vocabulary/data.d.ts.map +0 -1
  191. package/dist/vocabulary/data.js +0 -503
  192. package/dist/vocabulary/data.js.map +0 -1
  193. package/dist/vocabulary/dispatch.d.ts +0 -9
  194. package/dist/vocabulary/dispatch.d.ts.map +0 -1
  195. package/dist/vocabulary/dispatch.js +0 -50
  196. package/dist/vocabulary/dispatch.js.map +0 -1
  197. package/dist/vocabulary/fact.d.ts +0 -19
  198. package/dist/vocabulary/fact.d.ts.map +0 -1
  199. package/dist/vocabulary/fact.js +0 -51
  200. package/dist/vocabulary/fact.js.map +0 -1
  201. package/dist/vocabulary/form.d.ts +0 -11
  202. package/dist/vocabulary/form.d.ts.map +0 -1
  203. package/dist/vocabulary/form.js +0 -23
  204. package/dist/vocabulary/form.js.map +0 -1
  205. package/dist/vocabulary/formatValue.d.ts +0 -7
  206. package/dist/vocabulary/formatValue.d.ts.map +0 -1
  207. package/dist/vocabulary/formatValue.js +0 -76
  208. package/dist/vocabulary/formatValue.js.map +0 -1
  209. package/dist/vocabulary/icon.d.ts +0 -41
  210. package/dist/vocabulary/icon.d.ts.map +0 -1
  211. package/dist/vocabulary/icon.js +0 -272
  212. package/dist/vocabulary/icon.js.map +0 -1
  213. package/dist/vocabulary/index.d.ts +0 -12
  214. package/dist/vocabulary/index.d.ts.map +0 -1
  215. package/dist/vocabulary/index.js +0 -36
  216. package/dist/vocabulary/index.js.map +0 -1
  217. package/dist/vocabulary/interactive.d.ts +0 -230
  218. package/dist/vocabulary/interactive.d.ts.map +0 -1
  219. package/dist/vocabulary/interactive.js +0 -530
  220. package/dist/vocabulary/interactive.js.map +0 -1
  221. package/dist/vocabulary/layout.d.ts +0 -79
  222. package/dist/vocabulary/layout.d.ts.map +0 -1
  223. package/dist/vocabulary/layout.js +0 -139
  224. package/dist/vocabulary/layout.js.map +0 -1
  225. package/dist/vocabulary/list.d.ts +0 -14
  226. package/dist/vocabulary/list.d.ts.map +0 -1
  227. package/dist/vocabulary/list.js +0 -50
  228. package/dist/vocabulary/list.js.map +0 -1
  229. package/dist/vocabulary/media.d.ts +0 -25
  230. package/dist/vocabulary/media.d.ts.map +0 -1
  231. package/dist/vocabulary/media.js +0 -42
  232. package/dist/vocabulary/media.js.map +0 -1
  233. package/dist/vocabulary/text.d.ts +0 -57
  234. package/dist/vocabulary/text.d.ts.map +0 -1
  235. package/dist/vocabulary/text.js +0 -49
  236. package/dist/vocabulary/text.js.map +0 -1
  237. package/dist/vocabulary/toTextContent.d.ts +0 -2
  238. package/dist/vocabulary/toTextContent.d.ts.map +0 -1
  239. package/dist/vocabulary/toTextContent.js +0 -6
  240. package/dist/vocabulary/toTextContent.js.map +0 -1
  241. package/src/JSONGenerativeUI.client.tsx +0 -168
  242. package/src/JSONGenerativeUI.server.tsx +0 -39
  243. package/src/JSONGenerativeUI.shared.ts +0 -90
  244. package/src/JSONGenerativeUI.test.tsx +0 -1298
  245. package/src/RadioGroupScope.server.test.tsx +0 -24
  246. package/src/RadioGroupScope.tsx +0 -34
  247. package/src/a2ui/BindingContext.ts +0 -26
  248. package/src/a2ui/PresentRenderer.tsx +0 -83
  249. package/src/a2ui/convert.test.ts +0 -2923
  250. package/src/a2ui/convert.ts +0 -1207
  251. package/src/a2ui/dataModel.ts +0 -69
  252. package/src/a2ui/reducer.test.ts +0 -422
  253. package/src/a2ui/reducer.ts +0 -363
  254. package/src/a2ui/snapshot.test.ts +0 -52
  255. package/src/a2ui/snapshot.ts +0 -52
  256. package/src/a2ui/types.ts +0 -114
  257. package/src/a2ui/valueFunctions.test.ts +0 -271
  258. package/src/a2ui/valueFunctions.ts +0 -523
  259. package/src/actionRegistry.test.ts +0 -90
  260. package/src/actionRegistry.ts +0 -88
  261. package/src/answeredValues.test.ts +0 -33
  262. package/src/answeredValues.tsx +0 -70
  263. package/src/buildPresentParameters.test.ts +0 -246
  264. package/src/buildPresentParameters.ts +0 -130
  265. package/src/constants.ts +0 -17
  266. package/src/convert/boundSpec.test.ts +0 -192
  267. package/src/convert/boundSpec.ts +0 -134
  268. package/src/convert/copyBounded.test.ts +0 -131
  269. package/src/convert/copyBounded.ts +0 -38
  270. package/src/convert/isElement.ts +0 -6
  271. package/src/convert/takeRun.test.ts +0 -64
  272. package/src/convert/takeRun.ts +0 -19
  273. package/src/defineGenerativeComponents.ts +0 -38
  274. package/src/fieldReferences.test.ts +0 -66
  275. package/src/fieldReferences.ts +0 -83
  276. package/src/generativeUIToJSX.test.ts +0 -268
  277. package/src/generativeUIToJSX.ts +0 -152
  278. package/src/index.server.test.ts +0 -106
  279. package/src/index.shared.ts +0 -61
  280. package/src/ir.test.ts +0 -203
  281. package/src/renderGenerativeUI.test.tsx +0 -373
  282. package/src/renderGenerativeUI.tsx +0 -165
  283. package/src/scopeSchema.test.ts +0 -72
  284. package/src/scopeSchema.ts +0 -71
  285. package/src/slack/a2uiSlack.test.ts +0 -198
  286. package/src/slack/constants.ts +0 -194
  287. package/src/slack/datePicker.test.ts +0 -612
  288. package/src/slack/decodeBlockAction.test.ts +0 -233
  289. package/src/slack/decodeBlockAction.ts +0 -232
  290. package/src/slack/fromSlackBlocks.test.ts +0 -1465
  291. package/src/slack/fromSlackBlocks.ts +0 -789
  292. package/src/slack/toSlackBlocks.test.ts +0 -3065
  293. package/src/slack/toSlackBlocks.ts +0 -1818
  294. package/src/slack/types.ts +0 -264
  295. package/src/teams/constants.ts +0 -93
  296. package/src/teams/decodeSubmitData.test.ts +0 -397
  297. package/src/teams/decodeSubmitData.ts +0 -110
  298. package/src/teams/temporal.test.ts +0 -766
  299. package/src/teams/temporalId.ts +0 -56
  300. package/src/teams/toAdaptiveCard.test.ts +0 -1897
  301. package/src/teams/toAdaptiveCard.ts +0 -1057
  302. package/src/teams/toTeamsAttachments.test.ts +0 -74
  303. package/src/teams/toTeamsAttachments.ts +0 -133
  304. package/src/teams/types.ts +0 -266
  305. package/src/temporal.test.ts +0 -319
  306. package/src/temporal.ts +0 -259
  307. package/src/types.ts +0 -119
  308. package/src/vocabulary/alert.test.tsx +0 -427
  309. package/src/vocabulary/alert.tsx +0 -217
  310. package/src/vocabulary/collectFormValues.test.ts +0 -389
  311. package/src/vocabulary/collectFormValues.ts +0 -116
  312. package/src/vocabulary/data.test.tsx +0 -858
  313. package/src/vocabulary/data.tsx +0 -819
  314. package/src/vocabulary/datepicker.dom.test.tsx +0 -1148
  315. package/src/vocabulary/dispatch.ts +0 -83
  316. package/src/vocabulary/fact.test.tsx +0 -94
  317. package/src/vocabulary/fact.tsx +0 -54
  318. package/src/vocabulary/form.test.tsx +0 -113
  319. package/src/vocabulary/form.tsx +0 -39
  320. package/src/vocabulary/formatValue.test.ts +0 -72
  321. package/src/vocabulary/formatValue.ts +0 -127
  322. package/src/vocabulary/icon.test.tsx +0 -64
  323. package/src/vocabulary/icon.tsx +0 -174
  324. package/src/vocabulary/index.test.ts +0 -65
  325. package/src/vocabulary/index.ts +0 -33
  326. package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
  327. package/src/vocabulary/interactive.dom.test.tsx +0 -1747
  328. package/src/vocabulary/interactive.test.tsx +0 -377
  329. package/src/vocabulary/interactive.tsx +0 -1048
  330. package/src/vocabulary/layout.test.tsx +0 -323
  331. package/src/vocabulary/layout.tsx +0 -247
  332. package/src/vocabulary/list.test.tsx +0 -222
  333. package/src/vocabulary/list.tsx +0 -50
  334. package/src/vocabulary/media.test.tsx +0 -85
  335. package/src/vocabulary/media.tsx +0 -56
  336. package/src/vocabulary/text.test.tsx +0 -102
  337. package/src/vocabulary/text.tsx +0 -60
  338. package/src/vocabulary/toTextContent.ts +0 -2
@@ -1,1207 +0,0 @@
1
- import { ICON_NAMES, type UIElement } from "../ir";
2
- import { classifyTemporal } from "../temporal";
3
- import { A2UI_SURFACE_ID, type A2uiSurfaceState } from "./types";
4
- import {
5
- evaluateA2uiValueFunction,
6
- type ExpressionPart,
7
- } from "./valueFunctions";
8
- import { MAX_AUTO_VIVIFY_ARRAY_INDEX } from "./reducer";
9
- import type { A2uiBinding } from "./BindingContext";
10
- import { decodePointer, resolvePointer } from "./dataModel";
11
-
12
- const DEPTH_CAP = 32;
13
- const TEMPLATE_ITEM_CAP = 100;
14
- const NODE_BUDGET = 5000;
15
- const EVALUATION_BUDGET = 20_000;
16
-
17
- const SUPPORTED_COMPONENTS = new Set([
18
- "Text",
19
- "Image",
20
- "Icon",
21
- "Row",
22
- "Column",
23
- "List",
24
- "Card",
25
- "Divider",
26
- "Button",
27
- "TextField",
28
- "CheckBox",
29
- "ChoicePicker",
30
- "DateTimeInput",
31
- "Slider",
32
- ]);
33
-
34
- const CHILD_LIST_COMPONENTS: ReadonlySet<string> = new Set([
35
- "Row",
36
- "Column",
37
- "List",
38
- ]);
39
-
40
- const ICON_NAME_SET: ReadonlySet<string> = new Set(ICON_NAMES);
41
- const ICON_SIZE_SET: ReadonlySet<string> = new Set(["sm", "md", "lg"]);
42
-
43
- const isRecord = (value: unknown): value is Record<string, unknown> =>
44
- typeof value === "object" && value !== null && !Array.isArray(value);
45
-
46
- const isPlainObject = (value: unknown): value is Record<string, unknown> => {
47
- if (!isRecord(value)) return false;
48
- const prototype = Object.getPrototypeOf(value);
49
- return prototype === Object.prototype || prototype === null;
50
- };
51
-
52
- const setOwnProperty = (
53
- target: Record<string, unknown>,
54
- key: string,
55
- value: unknown,
56
- ) => {
57
- if (key === "__proto__") {
58
- Object.defineProperty(target, key, {
59
- value,
60
- enumerable: true,
61
- configurable: true,
62
- writable: true,
63
- });
64
- } else {
65
- target[key] = value;
66
- }
67
- };
68
-
69
- const FUNCTION_CALL_KEYS: ReadonlySet<string> = new Set([
70
- "call",
71
- "args",
72
- "returnType",
73
- "catalogId",
74
- ]);
75
-
76
- const isFunctionCall = (
77
- value: unknown,
78
- ): value is {
79
- readonly call: string;
80
- readonly args?: Record<string, unknown>;
81
- } =>
82
- isPlainObject(value) &&
83
- typeof value["call"] === "string" &&
84
- (value["args"] === undefined || isPlainObject(value["args"])) &&
85
- Object.keys(value).every((key) => FUNCTION_CALL_KEYS.has(key));
86
-
87
- const isBinding = (value: unknown): value is { readonly path: string } =>
88
- isPlainObject(value) &&
89
- Object.keys(value).length === 1 &&
90
- typeof value["path"] === "string";
91
-
92
- type ChildTemplate = { readonly componentId: string; readonly path: string };
93
-
94
- const childTemplateOf = (children: unknown): ChildTemplate | undefined => {
95
- const template =
96
- isPlainObject(children) &&
97
- Object.keys(children).length === 1 &&
98
- isPlainObject(children["template"])
99
- ? children["template"]
100
- : children;
101
- return isPlainObject(template) &&
102
- Object.keys(template).length === 2 &&
103
- typeof template["componentId"] === "string" &&
104
- typeof template["path"] === "string"
105
- ? { componentId: template["componentId"], path: template["path"] }
106
- : undefined;
107
- };
108
-
109
- const bindingPath = (value: unknown): string | undefined =>
110
- isBinding(value) ? value.path : undefined;
111
-
112
- type Scope = { readonly data: unknown; readonly path: string };
113
-
114
- const pointerIn = (scope: Scope, path: string): string =>
115
- (path.startsWith("/") ? "" : scope.path) +
116
- decodePointer(path)
117
- .map((segment) => `/${segment.replaceAll("~", "~0").replaceAll("/", "~1")}`)
118
- .join("");
119
-
120
- const setIn = (
121
- value: unknown,
122
- segments: readonly string[],
123
- leaf: unknown,
124
- ): unknown => {
125
- const [head, ...rest] = segments;
126
- if (head === undefined) return leaf;
127
- if (
128
- /^(0|[1-9]\d*)$/.test(head) &&
129
- (Array.isArray(value) || value === undefined || value === null)
130
- ) {
131
- const list = Array.isArray(value) ? value : [];
132
- const index = Number(head);
133
- if (index >= list.length && index > MAX_AUTO_VIVIFY_ARRAY_INDEX) {
134
- return value;
135
- }
136
- const copy = [...list];
137
- copy[index] = setIn(copy[index], rest, leaf);
138
- return copy;
139
- }
140
- const copy: Record<string, unknown> = isRecord(value) ? { ...value } : {};
141
- setOwnProperty(
142
- copy,
143
- head,
144
- setIn(Object.hasOwn(copy, head) ? copy[head] : undefined, rest, leaf),
145
- );
146
- return copy;
147
- };
148
-
149
- const withFieldReferences = (
150
- value: unknown,
151
- pointer: string,
152
- fields: ReadonlyMap<string, unknown>,
153
- ): unknown => {
154
- if (fields.has(pointer)) return fields.get(pointer);
155
- let result = value;
156
- for (const [name, field] of fields) {
157
- if (name.startsWith(`${pointer}/`)) {
158
- result = setIn(result, decodePointer(name.slice(pointer.length)), field);
159
- }
160
- }
161
- return result;
162
- };
163
-
164
- const materializeEntries = (
165
- value: Record<string, unknown>,
166
- source: unknown,
167
- context: ConversionContext,
168
- evaluate: boolean,
169
- depth: number,
170
- positional = false,
171
- ): Record<string, unknown> => {
172
- const result: Record<string, unknown> = {};
173
- for (const [key, entry] of Object.entries(value)) {
174
- // An action's functionCall runs when the button fires, so only its arguments resolve here.
175
- const resolved =
176
- key === "functionCall" && isFunctionCall(entry)
177
- ? {
178
- ...entry,
179
- ...(entry.args !== undefined
180
- ? {
181
- args: materializeEntries(
182
- entry.args,
183
- source,
184
- context,
185
- evaluate,
186
- depth,
187
- true,
188
- ),
189
- }
190
- : {}),
191
- }
192
- : materialize(entry, source, context, evaluate, depth, positional);
193
- if (resolved !== undefined) {
194
- setOwnProperty(result, key, resolved);
195
- }
196
- }
197
- return result;
198
- };
199
-
200
- function materialize(
201
- value: unknown,
202
- source: unknown,
203
- context: ConversionContext,
204
- evaluate = true,
205
- depth = 0,
206
- positional = false,
207
- ): unknown {
208
- if (isBinding(value)) {
209
- const pointer = pointerIn(
210
- { data: source, path: context.scopePath },
211
- value.path,
212
- );
213
- const resolved = resolvePointer(context.surface.dataModel, pointer);
214
- context.dependencies?.set(pointer, resolved);
215
- return resolved;
216
- }
217
- if (Array.isArray(value)) {
218
- const entries = value.map((entry) =>
219
- materialize(entry, source, context, evaluate, depth, positional),
220
- );
221
- return positional
222
- ? entries
223
- : entries.filter((entry) => entry !== undefined);
224
- }
225
- if (!isPlainObject(value)) return value;
226
- if (!evaluate || !isFunctionCall(value)) {
227
- return materializeEntries(
228
- value,
229
- source,
230
- context,
231
- evaluate,
232
- depth,
233
- positional,
234
- );
235
- }
236
- if (!spendEvaluation(context)) return undefined;
237
- if (depth >= DEPTH_CAP) {
238
- if (!context.functionDepthWarned) {
239
- context.warnings.push(
240
- `A2UI function nesting cap of ${DEPTH_CAP} was reached.`,
241
- );
242
- context.functionDepthWarned = true;
243
- }
244
- return undefined;
245
- }
246
- return evaluateA2uiValueFunction(
247
- value.call,
248
- materializeEntries(
249
- value.args ?? {},
250
- source,
251
- context,
252
- true,
253
- depth + 1,
254
- true,
255
- ),
256
- {
257
- resolve: (part) =>
258
- spendEvaluation(context)
259
- ? materialize(part, source, context, true, depth + 1)
260
- : undefined,
261
- warn: (message) => context.warnings.push(message),
262
- templates: context.templates,
263
- },
264
- );
265
- }
266
-
267
- const firstDefined = (
268
- props: Record<string, unknown>,
269
- keys: readonly string[],
270
- ): unknown => {
271
- for (const key of keys) {
272
- if (props[key] !== undefined) return props[key];
273
- }
274
- return undefined;
275
- };
276
-
277
- const stringProp = (
278
- props: Record<string, unknown>,
279
- keys: readonly string[],
280
- ): string | undefined => {
281
- const value = firstDefined(props, keys);
282
- return typeof value === "string" ? value : undefined;
283
- };
284
-
285
- const sourceSurfaceId = (surface: A2uiSurfaceState): string =>
286
- (surface as A2uiSurfaceState & { [A2UI_SURFACE_ID]?: string })[
287
- A2UI_SURFACE_ID
288
- ] ?? "";
289
-
290
- type ConversionContext = {
291
- readonly surface: A2uiSurfaceState;
292
- readonly stepModel: unknown;
293
- readonly surfaceId: string;
294
- readonly warnings: string[];
295
- emittedNodes: number;
296
- depthWarned: boolean;
297
- budgetWarned: boolean;
298
- templateCapWarned: boolean;
299
- evaluations: number;
300
- evaluationBudgetWarned: boolean;
301
- functionDepthWarned: boolean;
302
- readonly templates: Map<string, ExpressionPart[] | null>;
303
- readonly inputFields: Map<string, unknown>;
304
- readonly textFields: Map<string, boolean>;
305
- readonly boundActionEntries: {
306
- readonly target: Record<string, unknown>;
307
- readonly key: string;
308
- readonly pointer: string;
309
- }[];
310
- readonly boundUserMessages: {
311
- readonly action: Record<string, unknown>;
312
- readonly pointer: string;
313
- }[];
314
- readonly keepUnknownComponents: boolean;
315
- readonly bindings: Map<string, A2uiBinding> | undefined;
316
- readonly cache: Map<string, CachedComponent> | undefined;
317
- dependencies: Map<string, unknown> | undefined;
318
- scopePath: string;
319
- };
320
-
321
- type CachedComponent = {
322
- readonly props: Record<string, unknown>;
323
- readonly mapped: UIElement | undefined;
324
- readonly dependencies: ReadonlyMap<string, unknown>;
325
- readonly evaluations: number;
326
- children?: readonly UIElement[];
327
- result?: UIElement;
328
- };
329
-
330
- const BOUND_KEYS = ["text", "value", "binding"] as const;
331
-
332
- const INPUT_COMPONENTS: ReadonlySet<string> = new Set([
333
- "TextField",
334
- "CheckBox",
335
- "ChoicePicker",
336
- "DateTimeInput",
337
- "Slider",
338
- ]);
339
-
340
- type DateInputType = "date" | "datetime" | "time";
341
-
342
- const dateInputType = (value: string): DateInputType | undefined => {
343
- const kind = classifyTemporal(value).kind;
344
- if (kind === "date" || kind === "time") return kind;
345
- return kind === "floating" || kind === "instant" ? "datetime" : undefined;
346
- };
347
-
348
- const finiteNumber = (value: unknown): number | undefined =>
349
- typeof value === "number" && Number.isFinite(value) ? value : undefined;
350
-
351
- const decimalPlaces = (value: number): number => {
352
- const [mantissa = "", exponent = "0"] = String(value).split("e");
353
- return Math.max(0, (mantissa.split(".")[1]?.length ?? 0) - Number(exponent));
354
- };
355
-
356
- // A spec slider without `steps` is continuous, so its step leaves at least a hundred divisions and keeps every bound value on the grid.
357
- const sliderStep = (
358
- min: number,
359
- max: number,
360
- value: number | undefined,
361
- steps: unknown,
362
- ): number | undefined => {
363
- if (max <= min) return undefined;
364
- if (typeof steps === "number" && Number.isInteger(steps) && steps >= 1) {
365
- return (max - min) / steps;
366
- }
367
- const precision = Math.max(
368
- decimalPlaces(min),
369
- value === undefined ? 0 : decimalPlaces(value),
370
- );
371
- return Math.min(
372
- 10 ** Math.floor(Math.log10((max - min) / 100)),
373
- 10 ** -precision,
374
- );
375
- };
376
-
377
- const recordBindings = (
378
- node: Record<string, unknown>,
379
- props: Record<string, unknown>,
380
- mapped: UIElement,
381
- scope: Scope,
382
- context: ConversionContext,
383
- ) => {
384
- const component = node["component"];
385
- const name = mapped["name"];
386
- const boundKey = BOUND_KEYS.find((key) => node[key] !== undefined);
387
- const value = boundKey === undefined ? undefined : props[boundKey];
388
- if (INPUT_COMPONENTS.has(String(component)) && typeof name === "string") {
389
- context.textFields.set(
390
- name,
391
- (!context.textFields.has(name) || context.bindings !== undefined) &&
392
- (component === "TextField" || component === "DateTimeInput"),
393
- );
394
- const field = (fallback: unknown) =>
395
- fallback === undefined ? { $field: name } : { $field: name, fallback };
396
- // A single-choice picker collects one string, while the spec binds it to a string list.
397
- const listValued =
398
- component === "ChoicePicker" &&
399
- mapped.$type !== "CheckboxGroup" &&
400
- typeof value !== "string";
401
- context.inputFields.set(
402
- name,
403
- listValued
404
- ? [field(Array.isArray(value) ? value[0] : undefined)]
405
- : field(value),
406
- );
407
- }
408
- const action = mapped.$action;
409
- const raw = node["action"];
410
- if (!action || !isRecord(raw)) return;
411
- const functionCall = action.type === "a2ui:functionCall";
412
- const event = isRecord(raw["event"]) ? raw["event"] : raw;
413
- const userMessage = event["userMessage"];
414
- if (!functionCall && isBinding(userMessage)) {
415
- context.boundUserMessages.push({
416
- action,
417
- pointer: pointerIn(scope, userMessage.path),
418
- });
419
- }
420
- const rawEntries = functionCall
421
- ? isRecord(raw["functionCall"])
422
- ? raw["functionCall"]["args"]
423
- : undefined
424
- : event["context"];
425
- const target = functionCall ? action["args"] : action["context"];
426
- if (!isRecord(rawEntries) || !isRecord(target)) return;
427
- for (const [key, entry] of Object.entries(rawEntries)) {
428
- if (isBinding(entry)) {
429
- context.boundActionEntries.push({
430
- target,
431
- key,
432
- pointer: pointerIn(scope, entry.path),
433
- });
434
- }
435
- }
436
- };
437
-
438
- const spendEvaluation = (context: ConversionContext): boolean => {
439
- if (context.evaluations < EVALUATION_BUDGET) {
440
- context.evaluations++;
441
- return true;
442
- }
443
- if (!context.evaluationBudgetWarned) {
444
- context.warnings.push(
445
- `A2UI function evaluation budget of ${EVALUATION_BUDGET} was reached.`,
446
- );
447
- context.evaluationBudgetWarned = true;
448
- }
449
- return false;
450
- };
451
-
452
- const reserveNode = (context: ConversionContext): boolean => {
453
- if (context.emittedNodes >= NODE_BUDGET) {
454
- if (!context.budgetWarned) {
455
- context.warnings.push(`A2UI node budget of ${NODE_BUDGET} was reached.`);
456
- context.budgetWarned = true;
457
- }
458
- return false;
459
- }
460
- context.emittedNodes++;
461
- return true;
462
- };
463
-
464
- const childReferences = (node: Record<string, unknown>): unknown[] => {
465
- const children = node["children"];
466
- const references: unknown[] = Array.isArray(children) ? [...children] : [];
467
- if (node["child"] !== undefined) references.push(node["child"]);
468
- if (node["component"] === "Modal") {
469
- references.push(node["trigger"], node["content"]);
470
- }
471
- if (node["component"] === "Tabs" && Array.isArray(node["tabs"])) {
472
- for (const tab of node["tabs"]) {
473
- references.push(isRecord(tab) ? tab["child"] : undefined);
474
- }
475
- }
476
- return references;
477
- };
478
-
479
- const childrenOf = (
480
- node: Record<string, unknown>,
481
- scope: Scope,
482
- context: ConversionContext,
483
- depth: number,
484
- visited: Set<string>,
485
- ): UIElement[] => {
486
- const result: UIElement[] = [];
487
- for (const childId of childReferences(node)) {
488
- if (typeof childId !== "string") {
489
- context.warnings.push(
490
- `Component "${String(node["id"] ?? "")}" has a malformed child reference.`,
491
- );
492
- continue;
493
- }
494
- const child = convertComponent(childId, scope, context, depth + 1, visited);
495
- if (child) result.push(child);
496
- }
497
- return result;
498
- };
499
-
500
- const textLabel = (
501
- node: Record<string, unknown>,
502
- children: readonly UIElement[],
503
- context: ConversionContext,
504
- ): string | undefined => {
505
- const references = childReferences(node);
506
- const [reference] = references;
507
- const [child] = children;
508
- if (
509
- references.length !== 1 ||
510
- children.length !== 1 ||
511
- typeof reference !== "string" ||
512
- !child ||
513
- context.surface.components.get(reference)?.["component"] !== "Text"
514
- ) {
515
- return undefined;
516
- }
517
- const text = child.$type === "Header" ? child["text"] : child["value"];
518
- return typeof text === "string" ? text : undefined;
519
- };
520
-
521
- const mappedAction = (
522
- node: Record<string, unknown>,
523
- props: Record<string, unknown>,
524
- context: ConversionContext,
525
- ): UIElement["$action"] | undefined => {
526
- const action = props["action"];
527
- if (action === undefined) return undefined;
528
- const source = {
529
- surfaceId: context.surfaceId,
530
- sourceComponentId: typeof node["id"] === "string" ? node["id"] : "",
531
- };
532
- const event =
533
- isRecord(action) && isRecord(action["event"]) ? action["event"] : action;
534
- const actionName =
535
- typeof event === "string"
536
- ? event
537
- : isRecord(event) && typeof event["name"] === "string"
538
- ? event["name"]
539
- : undefined;
540
- if (actionName) {
541
- const actionContext = isRecord(event) ? event["context"] : undefined;
542
- const userMessage = isRecord(event) ? event["userMessage"] : undefined;
543
- return {
544
- type: "a2ui:action",
545
- name: actionName,
546
- ...source,
547
- ...(typeof userMessage === "string" ? { userMessage } : {}),
548
- ...(actionContext !== undefined ? { context: actionContext } : {}),
549
- };
550
- }
551
- const functionCall = isRecord(action) ? action["functionCall"] : undefined;
552
- if (isRecord(functionCall) && typeof functionCall["call"] === "string") {
553
- const args = functionCall["args"];
554
- return {
555
- type: "a2ui:functionCall",
556
- call: functionCall["call"],
557
- ...source,
558
- ...(isRecord(args) ? { args } : {}),
559
- };
560
- }
561
- context.warnings.push(
562
- `Component "${String(node["id"] ?? "")}" has a malformed action.`,
563
- );
564
- return undefined;
565
- };
566
-
567
- const choiceOptions = (value: unknown): { label: string; value: string }[] => {
568
- if (!Array.isArray(value)) return [];
569
- const options: { label: string; value: string }[] = [];
570
- for (const option of value) {
571
- if (
572
- isRecord(option) &&
573
- typeof option["label"] === "string" &&
574
- typeof option["value"] === "string"
575
- ) {
576
- options.push({ label: option["label"], value: option["value"] });
577
- }
578
- }
579
- return options;
580
- };
581
-
582
- const mappedProps = (
583
- node: Record<string, unknown>,
584
- props: Record<string, unknown>,
585
- context: ConversionContext,
586
- scope: Scope,
587
- ): UIElement | undefined => {
588
- const component = node["component"];
589
-
590
- if (component === "Text") {
591
- const text = stringProp(props, ["text", "value"]);
592
- const variant = props["variant"];
593
- if (typeof variant === "string" && /^h[1-6]$/.test(variant)) {
594
- return {
595
- $type: "Header",
596
- ...(text !== undefined ? { text } : {}),
597
- };
598
- }
599
- if (variant === "caption") {
600
- return {
601
- $type: "Caption",
602
- ...(text !== undefined ? { value: text } : {}),
603
- };
604
- }
605
- return {
606
- $type: "Markdown",
607
- ...(text !== undefined ? { value: text } : {}),
608
- };
609
- }
610
-
611
- if (component === "Image") {
612
- const src = stringProp(props, ["src", "url"]);
613
- const alt = stringProp(props, ["alt", "altText", "description"]);
614
- const size = props["size"];
615
- const round = props["round"];
616
- return {
617
- $type: "Image",
618
- ...(src !== undefined ? { src } : {}),
619
- ...(alt !== undefined ? { alt } : {}),
620
- ...(typeof size === "string" || typeof size === "number" ? { size } : {}),
621
- ...(typeof round === "boolean" ? { round } : {}),
622
- };
623
- }
624
-
625
- if (component === "Icon") {
626
- const name = stringProp(props, ["name"]);
627
- if (!name || !ICON_NAME_SET.has(name)) return undefined;
628
- const size = props["size"];
629
- return {
630
- $type: "Icon",
631
- name,
632
- ...(typeof size === "string" && ICON_SIZE_SET.has(size) ? { size } : {}),
633
- };
634
- }
635
-
636
- if (component === "Row") {
637
- const gap = props["gap"];
638
- const align = firstDefined(props, ["align", "alignment"]);
639
- const justify = firstDefined(props, ["justify", "distribution"]);
640
- return {
641
- $type: "Row",
642
- ...(typeof gap === "number" ? { gap } : {}),
643
- ...(typeof align === "string" ? { align } : {}),
644
- ...(typeof justify === "string" ? { justify } : {}),
645
- };
646
- }
647
-
648
- if (component === "Column") {
649
- const gap = props["gap"];
650
- const align = firstDefined(props, ["align", "alignment"]);
651
- return {
652
- $type: "Col",
653
- ...(typeof gap === "number" ? { gap } : {}),
654
- ...(typeof align === "string" ? { align } : {}),
655
- };
656
- }
657
-
658
- if (component === "List") {
659
- const direction = props["direction"];
660
- const align = props["align"];
661
- if (direction === "horizontal") {
662
- return {
663
- $type: "Row",
664
- ...(typeof align === "string" ? { align } : {}),
665
- };
666
- }
667
- return { $type: "ListView" };
668
- }
669
-
670
- if (component === "Card") {
671
- const title = props["title"];
672
- const padding = props["padding"];
673
- const background = props["background"];
674
- return {
675
- $type: "Card",
676
- ...(typeof title === "string" ? { title } : {}),
677
- ...(typeof padding === "number" ? { padding } : {}),
678
- ...(typeof background === "string" ? { background } : {}),
679
- };
680
- }
681
-
682
- if (component === "Divider") {
683
- return {
684
- $type: "Divider",
685
- ...(typeof props["flush"] === "boolean" ? { flush: props["flush"] } : {}),
686
- };
687
- }
688
-
689
- if (component === "Button") {
690
- const label = stringProp(props, ["label", "text"]);
691
- const variantStyle =
692
- props["variant"] === "primary"
693
- ? "primary"
694
- : props["variant"] === "borderless"
695
- ? "ghost"
696
- : undefined;
697
- const buttonStyle =
698
- typeof props["buttonStyle"] === "string"
699
- ? props["buttonStyle"]
700
- : variantStyle;
701
- const block = props["block"];
702
- const submit = props["submit"];
703
- const action = mappedAction(node, props, context);
704
- return {
705
- $type: "Button",
706
- ...(label !== undefined ? { label } : {}),
707
- ...(buttonStyle !== undefined ? { buttonStyle } : {}),
708
- ...(typeof block === "boolean" ? { block } : {}),
709
- ...(typeof submit === "boolean" ? { submit } : {}),
710
- ...(action ? { $action: action } : {}),
711
- };
712
- }
713
-
714
- const bound = bindingPath(firstDefined(node, BOUND_KEYS));
715
- const name = bound === undefined ? undefined : pointerIn(scope, bound);
716
- const label = stringProp(props, ["label"]);
717
-
718
- if (component === "TextField") {
719
- const placeholder = stringProp(props, ["placeholder"]);
720
- const inputType =
721
- props["variant"] === "obscured"
722
- ? "password"
723
- : props["variant"] === "number"
724
- ? "number"
725
- : undefined;
726
- const multiline =
727
- typeof props["multiline"] === "boolean"
728
- ? props["multiline"]
729
- : props["variant"] === "longText" ||
730
- props["textFieldType"] === "longText"
731
- ? true
732
- : undefined;
733
- const initial = firstDefined(props, ["value", "text"]);
734
- const defaultValue =
735
- typeof initial === "string"
736
- ? initial
737
- : typeof initial === "number" && Number.isFinite(initial)
738
- ? String(initial)
739
- : undefined;
740
- return {
741
- $type: "Input",
742
- ...(inputType !== undefined ? { inputType } : {}),
743
- ...(placeholder !== undefined ? { placeholder } : {}),
744
- ...(multiline !== undefined ? { multiline } : {}),
745
- ...(label !== undefined ? { label } : {}),
746
- ...(name !== undefined ? { name } : {}),
747
- ...(defaultValue !== undefined ? { defaultValue } : {}),
748
- };
749
- }
750
-
751
- if (component === "CheckBox") {
752
- const defaultChecked = firstDefined(props, ["defaultChecked", "value"]);
753
- return {
754
- $type: "Checkbox",
755
- ...(label !== undefined ? { label } : {}),
756
- ...(name !== undefined ? { name } : {}),
757
- ...(typeof defaultChecked === "boolean" ? { defaultChecked } : {}),
758
- };
759
- }
760
-
761
- if (component === "Slider") {
762
- const min = finiteNumber(props["min"]) ?? 0;
763
- const max = finiteNumber(props["max"]);
764
- if (max === undefined || max < min) return undefined;
765
- const value = finiteNumber(props["value"]);
766
- const step = sliderStep(
767
- min,
768
- max,
769
- name === undefined
770
- ? value
771
- : finiteNumber(resolvePointer(context.stepModel, name)),
772
- props["steps"],
773
- );
774
- return {
775
- $type: "Slider",
776
- min,
777
- max,
778
- ...(step !== undefined ? { step } : {}),
779
- ...(value !== undefined ? { defaultValue: value } : {}),
780
- ...(label !== undefined ? { label } : {}),
781
- ...(name !== undefined ? { name } : {}),
782
- };
783
- }
784
-
785
- if (component === "ChoicePicker") {
786
- const options = choiceOptions(props["options"]);
787
- const value = props["value"];
788
- const selected =
789
- typeof value === "string"
790
- ? [value]
791
- : Array.isArray(value)
792
- ? value.filter((entry): entry is string => typeof entry === "string")
793
- : [];
794
- if (props["variant"] === "multipleSelection") {
795
- return {
796
- $type: "CheckboxGroup",
797
- options,
798
- ...(label !== undefined ? { label } : {}),
799
- ...(name !== undefined ? { name } : {}),
800
- ...(selected.length > 0 ? { defaultValue: selected } : {}),
801
- };
802
- }
803
- const [defaultValue] = selected;
804
- return {
805
- $type: "RadioGroup",
806
- options,
807
- ...(label !== undefined ? { label } : {}),
808
- ...(name !== undefined ? { name } : {}),
809
- ...(defaultValue !== undefined ? { defaultValue } : {}),
810
- };
811
- }
812
-
813
- if (component === "DateTimeInput") {
814
- const value = stringProp(props, ["value"]);
815
- const shapeValue =
816
- name === undefined ? value : resolvePointer(context.stepModel, name);
817
- const shapeType =
818
- typeof shapeValue === "string" ? dateInputType(shapeValue) : undefined;
819
- let inputType: DateInputType = shapeType ?? "date";
820
- if ("enableDate" in props || "enableTime" in props) {
821
- inputType =
822
- props["enableTime"] === true
823
- ? props["enableDate"] === true
824
- ? "datetime"
825
- : "time"
826
- : "date";
827
- }
828
- if (
829
- typeof shapeValue === "string" &&
830
- shapeValue !== "" &&
831
- shapeType !== inputType
832
- ) {
833
- return {
834
- $type: "Input",
835
- ...(value !== undefined ? { defaultValue: value } : {}),
836
- ...(label !== undefined ? { label } : {}),
837
- ...(name !== undefined ? { name } : {}),
838
- };
839
- }
840
- // The spec types a bound as a date, a time or a date-time whatever the
841
- // enabled flags are, so a date bound on a datetime control is the range of
842
- // that whole local day rather than a mismatch.
843
- const bound = (edge: "min" | "max"): string | undefined => {
844
- const boundValue = stringProp(props, [edge]);
845
- if (boundValue === undefined) return undefined;
846
- const boundType = dateInputType(boundValue);
847
- if (boundType === inputType) return boundValue;
848
- if (inputType === "datetime" && boundType === "date") {
849
- return `${boundValue}T${edge === "min" ? "00:00" : "23:59:59.999"}`;
850
- }
851
- context.warnings.push(
852
- `A2UI DateTimeInput "${edge}" of "${boundValue}" is not a ${inputType} value and was dropped.`,
853
- );
854
- return undefined;
855
- };
856
- const min = bound("min");
857
- const max = bound("max");
858
- return {
859
- $type: "DatePicker",
860
- inputType,
861
- ...(value !== undefined ? { value } : {}),
862
- ...(min !== undefined ? { min } : {}),
863
- ...(max !== undefined ? { max } : {}),
864
- ...(label !== undefined ? { label } : {}),
865
- ...(name !== undefined ? { name } : {}),
866
- };
867
- }
868
-
869
- return undefined;
870
- };
871
-
872
- const convertTemplate = (
873
- node: Record<string, unknown>,
874
- template: ChildTemplate,
875
- scope: Scope,
876
- context: ConversionContext,
877
- depth: number,
878
- visited: Set<string>,
879
- converted: UIElement | undefined,
880
- ): UIElement | null => {
881
- if (!reserveNode(context)) return null;
882
- const container = converted ?? { $type: "ListView" };
883
- const itemized = container.$type === "ListView";
884
- const list = resolvePointer(
885
- context.surface.dataModel,
886
- pointerIn(scope, template.path),
887
- );
888
- const listPointer = pointerIn(scope, template.path);
889
- if (!Array.isArray(list)) {
890
- context.warnings.push(
891
- `Template on component "${String(node["id"] ?? "")}" did not resolve to a list.`,
892
- );
893
- return { ...container, children: [] };
894
- }
895
- const itemCount = Math.min(list.length, TEMPLATE_ITEM_CAP);
896
- if (list.length > TEMPLATE_ITEM_CAP && !context.templateCapWarned) {
897
- context.warnings.push(
898
- `A2UI template expansion was capped at ${TEMPLATE_ITEM_CAP} items.`,
899
- );
900
- context.templateCapWarned = true;
901
- }
902
- const children: UIElement[] = [];
903
- for (let index = 0; index < itemCount; index++) {
904
- if (itemized && !reserveNode(context)) break;
905
- const child = convertComponent(
906
- template.componentId,
907
- { data: list[index], path: `${listPointer}/${index}` },
908
- context,
909
- depth + 1,
910
- visited,
911
- );
912
- if (itemized) {
913
- children.push({
914
- $type: "ListViewItem",
915
- ...(child ? { children: child } : {}),
916
- });
917
- } else if (child) {
918
- children.push(child);
919
- }
920
- }
921
- return { ...container, children };
922
- };
923
-
924
- function convertComponent(
925
- componentId: string,
926
- scope: Scope,
927
- context: ConversionContext,
928
- depth: number,
929
- visited: Set<string>,
930
- ): UIElement | null {
931
- if (depth >= DEPTH_CAP) {
932
- if (!context.depthWarned) {
933
- context.warnings.push(`A2UI depth cap of ${DEPTH_CAP} was reached.`);
934
- context.depthWarned = true;
935
- }
936
- return null;
937
- }
938
- if (visited.has(componentId)) {
939
- context.warnings.push(`A2UI component cycle detected at "${componentId}".`);
940
- return null;
941
- }
942
- const node = context.surface.components.get(componentId);
943
- if (!node) {
944
- context.warnings.push(`A2UI component "${componentId}" was not found.`);
945
- return null;
946
- }
947
- const component = node["component"];
948
-
949
- visited.add(componentId);
950
- try {
951
- if (typeof component !== "string") {
952
- context.warnings.push(
953
- `Unknown A2UI component "${String(component ?? "")}" was skipped.`,
954
- );
955
- return null;
956
- }
957
- const template = childTemplateOf(node["children"]);
958
- if (
959
- !SUPPORTED_COMPONENTS.has(component) &&
960
- !context.keepUnknownComponents &&
961
- !template
962
- ) {
963
- context.warnings.push(
964
- `Unknown A2UI component "${component}" was skipped.`,
965
- );
966
- return null;
967
- }
968
- const cacheKey = context.cache
969
- ? JSON.stringify([componentId, scope.path, depth])
970
- : "";
971
- let cached = context.cache?.get(cacheKey);
972
- if (
973
- cached &&
974
- (context.evaluations + cached.evaluations > EVALUATION_BUDGET ||
975
- [...cached.dependencies].some(
976
- ([path, value]) =>
977
- !Object.is(resolvePointer(context.surface.dataModel, path), value),
978
- ))
979
- )
980
- cached = undefined;
981
- let props: Record<string, unknown>;
982
- let mapped: UIElement | undefined;
983
- if (cached) {
984
- props = cached.props;
985
- mapped = cached.mapped;
986
- context.evaluations += cached.evaluations;
987
- } else {
988
- props = {};
989
- const dependencies = new Map<string, unknown>();
990
- const evaluations = context.evaluations;
991
- context.dependencies = dependencies;
992
- context.scopePath = scope.path;
993
- for (const [key, value] of Object.entries(node)) {
994
- if (
995
- key === "id" ||
996
- key === "component" ||
997
- key === "children" ||
998
- key === "child"
999
- )
1000
- continue;
1001
- const resolved = materialize(
1002
- value,
1003
- scope.data,
1004
- context,
1005
- key !== "checks",
1006
- );
1007
- if (resolved !== undefined) setOwnProperty(props, key, resolved);
1008
- }
1009
- context.dependencies = undefined;
1010
- mapped = mappedProps(node, props, context, scope);
1011
- if (context.cache && context.warnings.length === 0 && !mapped?.$action) {
1012
- cached = {
1013
- props,
1014
- mapped,
1015
- dependencies,
1016
- evaluations: context.evaluations - evaluations,
1017
- };
1018
- context.cache.set(cacheKey, cached);
1019
- }
1020
- }
1021
- if (
1022
- mapped &&
1023
- context.bindings &&
1024
- typeof mapped["name"] === "string" &&
1025
- INPUT_COMPONENTS.has(component)
1026
- ) {
1027
- const field = mapped.$type === "Checkbox" ? "checked" : "value";
1028
- const defaultField =
1029
- mapped.$type === "Checkbox" ? "defaultChecked" : "defaultValue";
1030
- const fallback =
1031
- mapped.$type === "Checkbox"
1032
- ? false
1033
- : mapped.$type === "CheckboxGroup"
1034
- ? []
1035
- : mapped.$type === "Slider"
1036
- ? mapped["min"]
1037
- : "";
1038
- const { [defaultField]: initialValue, ...rest } = mapped;
1039
- mapped = { ...rest, [field]: initialValue ?? mapped[field] ?? fallback };
1040
- context.bindings.set(mapped["name"] as string, {
1041
- value: mapped[field],
1042
- arrayValue:
1043
- component === "ChoicePicker" &&
1044
- mapped.$type !== "CheckboxGroup" &&
1045
- typeof props["value"] !== "string",
1046
- });
1047
- }
1048
-
1049
- if (!mapped && SUPPORTED_COMPONENTS.has(component)) {
1050
- context.warnings.push(
1051
- `A2UI component "${component}" could not be mapped and was skipped.`,
1052
- );
1053
- return null;
1054
- }
1055
- const retained =
1056
- !mapped && context.keepUnknownComponents
1057
- ? {
1058
- $type: component,
1059
- ...Object.fromEntries(
1060
- Object.entries(props).filter(([key]) => !key.startsWith("$")),
1061
- ),
1062
- }
1063
- : undefined;
1064
- if (template) {
1065
- return convertTemplate(
1066
- node,
1067
- template,
1068
- scope,
1069
- context,
1070
- depth,
1071
- visited,
1072
- CHILD_LIST_COMPONENTS.has(component) ? mapped : retained,
1073
- );
1074
- }
1075
- if (!reserveNode(context)) return null;
1076
- const converted = mapped ?? retained;
1077
- if (!converted) return null;
1078
- recordBindings(node, props, converted, scope, context);
1079
- const children = childrenOf(node, scope, context, depth, visited);
1080
- if (mapped?.$type === "Button" && mapped["label"] === undefined) {
1081
- const label = textLabel(node, children, context);
1082
- if (label !== undefined) return { ...mapped, label };
1083
- }
1084
- const listChildren =
1085
- component === "List" && mapped?.$type === "ListView"
1086
- ? children.map((child) => ({ $type: "ListViewItem", children: child }))
1087
- : children;
1088
- if (
1089
- cached?.result &&
1090
- cached.children?.length === listChildren.length &&
1091
- cached.children.every((child, index) => child === listChildren[index])
1092
- )
1093
- return cached.result;
1094
- const result = {
1095
- ...converted,
1096
- ...(listChildren.length > 0 ? { children: listChildren } : {}),
1097
- };
1098
- if (cached) {
1099
- cached.children = listChildren;
1100
- cached.result = result;
1101
- }
1102
- return result;
1103
- } finally {
1104
- visited.delete(componentId);
1105
- }
1106
- }
1107
-
1108
- export function convertSurfaceToUISpec(
1109
- surface: A2uiSurfaceState,
1110
- options: {
1111
- readonly keepUnknownComponents?: boolean;
1112
- } = {},
1113
- ): {
1114
- spec: UIElement | null;
1115
- warnings: string[];
1116
- } {
1117
- return convertSurface(surface, options.keepUnknownComponents === true);
1118
- }
1119
-
1120
- export const createLiveSurfaceConverter = (surface: A2uiSurfaceState) => {
1121
- const cache = new Map<string, CachedComponent>();
1122
- const templates = new Map<string, ExpressionPart[] | null>();
1123
- return (dataModel: unknown) => {
1124
- const bindings = new Map<string, A2uiBinding>();
1125
- const converted = convertSurface(
1126
- { ...surface, dataModel, [A2UI_SURFACE_ID]: sourceSurfaceId(surface) },
1127
- false,
1128
- bindings,
1129
- cache,
1130
- templates,
1131
- surface.dataModel,
1132
- );
1133
- return { ...converted, bindings };
1134
- };
1135
- };
1136
-
1137
- function convertSurface(
1138
- surface: A2uiSurfaceState & { [A2UI_SURFACE_ID]?: string },
1139
- keepUnknownComponents: boolean,
1140
- bindings?: Map<string, A2uiBinding>,
1141
- cache?: Map<string, CachedComponent>,
1142
- templates = new Map<string, ExpressionPart[] | null>(),
1143
- stepModel: unknown = surface.dataModel,
1144
- ): { spec: UIElement | null; warnings: string[] } {
1145
- const warnings: string[] = [];
1146
- if (!surface.components.has("root")) {
1147
- return {
1148
- spec: null,
1149
- warnings: ['A2UI root component "root" was not found.'],
1150
- };
1151
- }
1152
- const context: ConversionContext = {
1153
- surface,
1154
- stepModel,
1155
- surfaceId: sourceSurfaceId(surface),
1156
- warnings,
1157
- emittedNodes: 0,
1158
- depthWarned: false,
1159
- budgetWarned: false,
1160
- templateCapWarned: false,
1161
- evaluations: 0,
1162
- evaluationBudgetWarned: false,
1163
- functionDepthWarned: false,
1164
- templates,
1165
- inputFields: new Map(),
1166
- textFields: new Map(),
1167
- boundActionEntries: [],
1168
- boundUserMessages: [],
1169
- keepUnknownComponents,
1170
- bindings,
1171
- cache,
1172
- dependencies: undefined,
1173
- scopePath: "",
1174
- };
1175
- try {
1176
- const spec = convertComponent(
1177
- "root",
1178
- { data: surface.dataModel, path: "" },
1179
- context,
1180
- 0,
1181
- new Set(),
1182
- );
1183
- for (const { target, key, pointer } of context.boundActionEntries) {
1184
- const value = withFieldReferences(
1185
- target[key],
1186
- pointer,
1187
- context.inputFields,
1188
- );
1189
- if (value !== undefined) setOwnProperty(target, key, value);
1190
- }
1191
- for (const { action, pointer } of context.boundUserMessages) {
1192
- if (!context.textFields.get(pointer)) continue;
1193
- const fallback = action["userMessage"];
1194
- setOwnProperty(
1195
- action,
1196
- "userMessage",
1197
- typeof fallback === "string"
1198
- ? { $field: pointer, fallback }
1199
- : { $field: pointer },
1200
- );
1201
- }
1202
- return { spec, warnings };
1203
- } catch {
1204
- warnings.push("A2UI surface conversion encountered malformed input.");
1205
- return { spec: null, warnings };
1206
- }
1207
- }