@assistant-ui/react-generative-ui 0.0.21 → 0.0.22

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 (189) hide show
  1. package/LICENSE +1 -1
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +30 -13
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/RadioGroupScope.d.ts +6 -0
  6. package/dist/RadioGroupScope.d.ts.map +1 -0
  7. package/dist/RadioGroupScope.js +25 -0
  8. package/dist/RadioGroupScope.js.map +1 -0
  9. package/dist/a2ui/BindingContext.d.ts +11 -0
  10. package/dist/a2ui/BindingContext.d.ts.map +1 -0
  11. package/dist/a2ui/BindingContext.js +12 -0
  12. package/dist/a2ui/BindingContext.js.map +1 -0
  13. package/dist/a2ui/PresentRenderer.d.ts +11 -0
  14. package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
  15. package/dist/a2ui/PresentRenderer.js +39 -0
  16. package/dist/a2ui/PresentRenderer.js.map +1 -0
  17. package/dist/a2ui/convert.d.ts +6 -0
  18. package/dist/a2ui/convert.d.ts.map +1 -1
  19. package/dist/a2ui/convert.js +344 -83
  20. package/dist/a2ui/convert.js.map +1 -1
  21. package/dist/a2ui/dataModel.d.ts +9 -0
  22. package/dist/a2ui/dataModel.d.ts.map +1 -0
  23. package/dist/a2ui/dataModel.js +44 -0
  24. package/dist/a2ui/dataModel.js.map +1 -0
  25. package/dist/a2ui/reducer.d.ts +5 -0
  26. package/dist/a2ui/reducer.d.ts.map +1 -1
  27. package/dist/a2ui/reducer.js +3 -3
  28. package/dist/a2ui/reducer.js.map +1 -1
  29. package/dist/a2ui/types.d.ts +8 -2
  30. package/dist/a2ui/types.d.ts.map +1 -1
  31. package/dist/a2ui/types.js.map +1 -1
  32. package/dist/a2ui/valueFunctions.d.ts +16 -0
  33. package/dist/a2ui/valueFunctions.d.ts.map +1 -0
  34. package/dist/a2ui/valueFunctions.js +332 -0
  35. package/dist/a2ui/valueFunctions.js.map +1 -0
  36. package/dist/actionRegistry.d.ts +1 -1
  37. package/dist/actionRegistry.js.map +1 -1
  38. package/dist/answeredValues.d.ts +16 -0
  39. package/dist/answeredValues.d.ts.map +1 -0
  40. package/dist/answeredValues.js +37 -0
  41. package/dist/answeredValues.js.map +1 -0
  42. package/dist/constants.d.ts +2 -0
  43. package/dist/constants.d.ts.map +1 -1
  44. package/dist/constants.js +3 -1
  45. package/dist/constants.js.map +1 -1
  46. package/dist/fieldReferences.d.ts +9 -0
  47. package/dist/fieldReferences.d.ts.map +1 -0
  48. package/dist/fieldReferences.js +53 -0
  49. package/dist/fieldReferences.js.map +1 -0
  50. package/dist/generativeUIToJSX.js +3 -3
  51. package/dist/generativeUIToJSX.js.map +1 -1
  52. package/dist/index.js +2 -2
  53. package/dist/index.server.js +2 -2
  54. package/dist/index.shared.js +2 -2
  55. package/dist/ir.d.ts +1 -1
  56. package/dist/ir.js.map +1 -1
  57. package/dist/renderGenerativeUI.d.ts.map +1 -1
  58. package/dist/renderGenerativeUI.js +12 -1
  59. package/dist/renderGenerativeUI.js.map +1 -1
  60. package/dist/slack/constants.d.ts +2 -0
  61. package/dist/slack/constants.d.ts.map +1 -1
  62. package/dist/slack/constants.js +3 -1
  63. package/dist/slack/constants.js.map +1 -1
  64. package/dist/slack/decodeBlockAction.d.ts +3 -1
  65. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  66. package/dist/slack/decodeBlockAction.js +58 -7
  67. package/dist/slack/decodeBlockAction.js.map +1 -1
  68. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  69. package/dist/slack/fromSlackBlocks.js +20 -3
  70. package/dist/slack/fromSlackBlocks.js.map +1 -1
  71. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  72. package/dist/slack/toSlackBlocks.js +187 -19
  73. package/dist/slack/toSlackBlocks.js.map +1 -1
  74. package/dist/slack/types.d.ts +4 -0
  75. package/dist/slack/types.d.ts.map +1 -1
  76. package/dist/teams/constants.d.ts +1 -1
  77. package/dist/teams/constants.d.ts.map +1 -1
  78. package/dist/teams/constants.js +1 -1
  79. package/dist/teams/constants.js.map +1 -1
  80. package/dist/teams/decodeSubmitData.d.ts +4 -0
  81. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  82. package/dist/teams/decodeSubmitData.js +6 -1
  83. package/dist/teams/decodeSubmitData.js.map +1 -1
  84. package/dist/teams/toAdaptiveCard.d.ts +5 -4
  85. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  86. package/dist/teams/toAdaptiveCard.js +56 -17
  87. package/dist/teams/toAdaptiveCard.js.map +1 -1
  88. package/dist/teams/types.d.ts +4 -1
  89. package/dist/teams/types.d.ts.map +1 -1
  90. package/dist/vocabulary/alert.d.ts +9 -2
  91. package/dist/vocabulary/alert.d.ts.map +1 -1
  92. package/dist/vocabulary/alert.js +116 -19
  93. package/dist/vocabulary/alert.js.map +1 -1
  94. package/dist/vocabulary/collectFormValues.d.ts +2 -1
  95. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  96. package/dist/vocabulary/collectFormValues.js +10 -3
  97. package/dist/vocabulary/collectFormValues.js.map +1 -1
  98. package/dist/vocabulary/data.d.ts +22 -5
  99. package/dist/vocabulary/data.d.ts.map +1 -1
  100. package/dist/vocabulary/data.js +278 -83
  101. package/dist/vocabulary/data.js.map +1 -1
  102. package/dist/vocabulary/dispatch.d.ts +3 -2
  103. package/dist/vocabulary/dispatch.d.ts.map +1 -1
  104. package/dist/vocabulary/dispatch.js +33 -4
  105. package/dist/vocabulary/dispatch.js.map +1 -1
  106. package/dist/vocabulary/fact.d.ts +8 -1
  107. package/dist/vocabulary/fact.d.ts.map +1 -1
  108. package/dist/vocabulary/fact.js +37 -11
  109. package/dist/vocabulary/fact.js.map +1 -1
  110. package/dist/vocabulary/form.d.ts.map +1 -1
  111. package/dist/vocabulary/form.js +3 -3
  112. package/dist/vocabulary/form.js.map +1 -1
  113. package/dist/vocabulary/formatValue.d.ts +7 -0
  114. package/dist/vocabulary/formatValue.d.ts.map +1 -0
  115. package/dist/vocabulary/formatValue.js +76 -0
  116. package/dist/vocabulary/formatValue.js.map +1 -0
  117. package/dist/vocabulary/icon.d.ts +1 -1
  118. package/dist/vocabulary/interactive.d.ts +137 -8
  119. package/dist/vocabulary/interactive.d.ts.map +1 -1
  120. package/dist/vocabulary/interactive.js +404 -103
  121. package/dist/vocabulary/interactive.js.map +1 -1
  122. package/dist/vocabulary/layout.d.ts.map +1 -1
  123. package/dist/vocabulary/layout.js +4 -4
  124. package/dist/vocabulary/layout.js.map +1 -1
  125. package/dist/vocabulary/list.js +2 -2
  126. package/dist/vocabulary/list.js.map +1 -1
  127. package/dist/vocabulary/text.d.ts +1 -1
  128. package/package.json +6 -3
  129. package/src/JSONGenerativeUI.client.tsx +84 -40
  130. package/src/JSONGenerativeUI.test.tsx +964 -68
  131. package/src/RadioGroupScope.server.test.tsx +24 -0
  132. package/src/RadioGroupScope.tsx +34 -0
  133. package/src/a2ui/BindingContext.ts +26 -0
  134. package/src/a2ui/PresentRenderer.tsx +83 -0
  135. package/src/a2ui/convert.test.ts +1795 -179
  136. package/src/a2ui/convert.ts +629 -132
  137. package/src/a2ui/dataModel.ts +69 -0
  138. package/src/a2ui/reducer.test.ts +35 -0
  139. package/src/a2ui/reducer.ts +5 -4
  140. package/src/a2ui/types.ts +9 -2
  141. package/src/a2ui/valueFunctions.test.ts +271 -0
  142. package/src/a2ui/valueFunctions.ts +523 -0
  143. package/src/actionRegistry.ts +1 -1
  144. package/src/answeredValues.test.ts +33 -0
  145. package/src/answeredValues.tsx +70 -0
  146. package/src/constants.ts +4 -0
  147. package/src/fieldReferences.test.ts +66 -0
  148. package/src/fieldReferences.ts +83 -0
  149. package/src/generativeUIToJSX.test.ts +18 -0
  150. package/src/generativeUIToJSX.ts +4 -6
  151. package/src/index.server.test.ts +106 -0
  152. package/src/ir.ts +1 -1
  153. package/src/renderGenerativeUI.test.tsx +14 -5
  154. package/src/renderGenerativeUI.tsx +30 -1
  155. package/src/slack/a2uiSlack.test.ts +198 -0
  156. package/src/slack/constants.ts +3 -0
  157. package/src/slack/decodeBlockAction.test.ts +98 -0
  158. package/src/slack/decodeBlockAction.ts +110 -17
  159. package/src/slack/fromSlackBlocks.test.ts +222 -3
  160. package/src/slack/fromSlackBlocks.ts +53 -2
  161. package/src/slack/toSlackBlocks.test.ts +682 -3
  162. package/src/slack/toSlackBlocks.ts +374 -23
  163. package/src/slack/types.ts +4 -0
  164. package/src/teams/constants.ts +1 -1
  165. package/src/teams/decodeSubmitData.test.ts +33 -0
  166. package/src/teams/decodeSubmitData.ts +9 -1
  167. package/src/teams/toAdaptiveCard.test.ts +219 -0
  168. package/src/teams/toAdaptiveCard.ts +93 -19
  169. package/src/teams/types.ts +4 -1
  170. package/src/vocabulary/alert.test.tsx +288 -7
  171. package/src/vocabulary/alert.tsx +168 -26
  172. package/src/vocabulary/collectFormValues.test.ts +28 -1
  173. package/src/vocabulary/collectFormValues.ts +25 -5
  174. package/src/vocabulary/data.test.tsx +248 -6
  175. package/src/vocabulary/data.tsx +398 -100
  176. package/src/vocabulary/dispatch.ts +63 -2
  177. package/src/vocabulary/fact.test.tsx +67 -0
  178. package/src/vocabulary/fact.tsx +40 -9
  179. package/src/vocabulary/form.tsx +7 -2
  180. package/src/vocabulary/formatValue.test.ts +72 -0
  181. package/src/vocabulary/formatValue.ts +127 -0
  182. package/src/vocabulary/index.test.ts +2 -0
  183. package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
  184. package/src/vocabulary/interactive.dom.test.tsx +1699 -0
  185. package/src/vocabulary/interactive.test.tsx +60 -4
  186. package/src/vocabulary/interactive.tsx +770 -151
  187. package/src/vocabulary/layout.test.tsx +6 -4
  188. package/src/vocabulary/layout.tsx +13 -2
  189. package/src/vocabulary/list.tsx +2 -2
@@ -1,9 +1,13 @@
1
1
  import { ICON_NAMES } from "../ir.js";
2
2
  import { A2UI_SURFACE_ID } from "./types.js";
3
+ import "./reducer.js";
4
+ import { decodePointer, resolvePointer } from "./dataModel.js";
5
+ import { evaluateA2uiValueFunction } from "./valueFunctions.js";
3
6
  //#region src/a2ui/convert.ts
4
7
  const DEPTH_CAP = 32;
5
8
  const TEMPLATE_ITEM_CAP = 100;
6
9
  const NODE_BUDGET = 5e3;
10
+ const EVALUATION_BUDGET = 2e4;
7
11
  const SUPPORTED_COMPONENTS = /* @__PURE__ */ new Set([
8
12
  "Text",
9
13
  "Image",
@@ -17,7 +21,13 @@ const SUPPORTED_COMPONENTS = /* @__PURE__ */ new Set([
17
21
  "TextField",
18
22
  "CheckBox",
19
23
  "ChoicePicker",
20
- "DateTimeInput"
24
+ "DateTimeInput",
25
+ "Slider"
26
+ ]);
27
+ const CHILD_LIST_COMPONENTS = /* @__PURE__ */ new Set([
28
+ "Row",
29
+ "Column",
30
+ "List"
21
31
  ]);
22
32
  const ICON_NAME_SET = new Set(ICON_NAMES);
23
33
  const ICON_SIZE_SET = /* @__PURE__ */ new Set([
@@ -40,44 +50,85 @@ const setOwnProperty = (target, key, value) => {
40
50
  });
41
51
  else target[key] = value;
42
52
  };
53
+ const FUNCTION_CALL_KEYS = /* @__PURE__ */ new Set([
54
+ "call",
55
+ "args",
56
+ "returnType",
57
+ "catalogId"
58
+ ]);
59
+ const isFunctionCall = (value) => isPlainObject(value) && typeof value["call"] === "string" && (value["args"] === void 0 || isPlainObject(value["args"])) && Object.keys(value).every((key) => FUNCTION_CALL_KEYS.has(key));
43
60
  const isBinding = (value) => isPlainObject(value) && Object.keys(value).length === 1 && typeof value["path"] === "string";
44
- const isTemplateChildren = (value) => isPlainObject(value) && Object.keys(value).length === 1 && isPlainObject(value["template"]) && Object.keys(value["template"]).length === 2 && typeof value["template"]["componentId"] === "string" && typeof value["template"]["path"] === "string";
45
- const decodePointer = (path) => {
46
- if (path === "" || path === "/") return [];
47
- if (!path.startsWith("/")) return void 0;
48
- return path.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
61
+ const childTemplateOf = (children) => {
62
+ const template = isPlainObject(children) && Object.keys(children).length === 1 && isPlainObject(children["template"]) ? children["template"] : children;
63
+ return isPlainObject(template) && Object.keys(template).length === 2 && typeof template["componentId"] === "string" && typeof template["path"] === "string" ? {
64
+ componentId: template["componentId"],
65
+ path: template["path"]
66
+ } : void 0;
49
67
  };
50
- const resolvePointer = (source, path) => {
51
- const segments = decodePointer(path);
52
- if (!segments) return void 0;
53
- let current = source;
54
- for (const segment of segments) {
55
- if (Array.isArray(current)) {
56
- if (!/^(0|[1-9]\d*)$/.test(segment)) return void 0;
57
- current = current[Number(segment)];
58
- continue;
59
- }
60
- if (!isRecord(current) || !Object.hasOwn(current, segment)) return;
61
- current = current[segment];
68
+ const bindingPath = (value) => isBinding(value) ? value.path : void 0;
69
+ const pointerIn = (scope, path) => (path.startsWith("/") ? "" : scope.path) + decodePointer(path).map((segment) => `/${segment.replaceAll("~", "~0").replaceAll("/", "~1")}`).join("");
70
+ const setIn = (value, segments, leaf) => {
71
+ const [head, ...rest] = segments;
72
+ if (head === void 0) return leaf;
73
+ if (/^(0|[1-9]\d*)$/.test(head) && (Array.isArray(value) || value === void 0 || value === null)) {
74
+ const list = Array.isArray(value) ? value : [];
75
+ const index = Number(head);
76
+ if (index >= list.length && index > 1e4) return value;
77
+ const copy = [...list];
78
+ copy[index] = setIn(copy[index], rest, leaf);
79
+ return copy;
62
80
  }
63
- return current;
81
+ const copy = isRecord(value) ? { ...value } : {};
82
+ setOwnProperty(copy, head, setIn(Object.hasOwn(copy, head) ? copy[head] : void 0, rest, leaf));
83
+ return copy;
64
84
  };
65
- const bindingPath = (value) => isBinding(value) ? value.path : void 0;
66
- const lastPointerSegment = (path) => {
67
- if (!path) return void 0;
68
- return decodePointer(path)?.at(-1);
85
+ const withFieldReferences = (value, pointer, fields) => {
86
+ if (fields.has(pointer)) return fields.get(pointer);
87
+ let result = value;
88
+ for (const [name, field] of fields) if (name.startsWith(`${pointer}/`)) result = setIn(result, decodePointer(name.slice(pointer.length)), field);
89
+ return result;
69
90
  };
70
- const materialize = (value, source) => {
71
- if (isBinding(value)) return resolvePointer(source, value.path);
72
- if (Array.isArray(value)) return value.map((entry) => materialize(entry, source)).filter((entry) => entry !== void 0);
73
- if (!isPlainObject(value)) return value;
91
+ const materializeEntries = (value, source, context, evaluate, depth, positional = false) => {
74
92
  const result = {};
75
93
  for (const [key, entry] of Object.entries(value)) {
76
- const resolved = materialize(entry, source);
94
+ const resolved = key === "functionCall" && isFunctionCall(entry) ? {
95
+ ...entry,
96
+ ...entry.args !== void 0 ? { args: materializeEntries(entry.args, source, context, evaluate, depth, true) } : {}
97
+ } : materialize(entry, source, context, evaluate, depth, positional);
77
98
  if (resolved !== void 0) setOwnProperty(result, key, resolved);
78
99
  }
79
100
  return result;
80
101
  };
102
+ function materialize(value, source, context, evaluate = true, depth = 0, positional = false) {
103
+ if (isBinding(value)) {
104
+ const pointer = pointerIn({
105
+ data: source,
106
+ path: context.scopePath
107
+ }, value.path);
108
+ const resolved = resolvePointer(context.surface.dataModel, pointer);
109
+ context.dependencies?.set(pointer, resolved);
110
+ return resolved;
111
+ }
112
+ if (Array.isArray(value)) {
113
+ const entries = value.map((entry) => materialize(entry, source, context, evaluate, depth, positional));
114
+ return positional ? entries : entries.filter((entry) => entry !== void 0);
115
+ }
116
+ if (!isPlainObject(value)) return value;
117
+ if (!evaluate || !isFunctionCall(value)) return materializeEntries(value, source, context, evaluate, depth, positional);
118
+ if (!spendEvaluation(context)) return void 0;
119
+ if (depth >= DEPTH_CAP) {
120
+ if (!context.functionDepthWarned) {
121
+ context.warnings.push(`A2UI function nesting cap of ${DEPTH_CAP} was reached.`);
122
+ context.functionDepthWarned = true;
123
+ }
124
+ return;
125
+ }
126
+ return evaluateA2uiValueFunction(value.call, materializeEntries(value.args ?? {}, source, context, true, depth + 1, true), {
127
+ resolve: (part) => spendEvaluation(context) ? materialize(part, source, context, true, depth + 1) : void 0,
128
+ warn: (message) => context.warnings.push(message),
129
+ templates: context.templates
130
+ });
131
+ }
81
132
  const firstDefined = (props, keys) => {
82
133
  for (const key of keys) if (props[key] !== void 0) return props[key];
83
134
  };
@@ -86,6 +137,74 @@ const stringProp = (props, keys) => {
86
137
  return typeof value === "string" ? value : void 0;
87
138
  };
88
139
  const sourceSurfaceId = (surface) => surface[A2UI_SURFACE_ID] ?? "";
140
+ const BOUND_KEYS = [
141
+ "text",
142
+ "value",
143
+ "binding"
144
+ ];
145
+ const INPUT_COMPONENTS = /* @__PURE__ */ new Set([
146
+ "TextField",
147
+ "CheckBox",
148
+ "ChoicePicker",
149
+ "DateTimeInput",
150
+ "Slider"
151
+ ]);
152
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
153
+ const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
154
+ const decimalPlaces = (value) => {
155
+ const [mantissa = "", exponent = "0"] = String(value).split("e");
156
+ return Math.max(0, (mantissa.split(".")[1]?.length ?? 0) - Number(exponent));
157
+ };
158
+ const sliderStep = (min, max, value, steps) => {
159
+ if (max <= min) return void 0;
160
+ if (typeof steps === "number" && Number.isInteger(steps) && steps >= 1) return (max - min) / steps;
161
+ const precision = Math.max(decimalPlaces(min), value === void 0 ? 0 : decimalPlaces(value));
162
+ return Math.min(10 ** Math.floor(Math.log10((max - min) / 100)), 10 ** -precision);
163
+ };
164
+ const recordBindings = (node, props, mapped, scope, context) => {
165
+ const component = node["component"];
166
+ const name = mapped["name"];
167
+ const boundKey = BOUND_KEYS.find((key) => node[key] !== void 0);
168
+ const value = boundKey === void 0 ? void 0 : props[boundKey];
169
+ if (INPUT_COMPONENTS.has(String(component)) && typeof name === "string") {
170
+ context.textFields.set(name, (!context.textFields.has(name) || context.bindings !== void 0) && (component === "TextField" || component === "DateTimeInput"));
171
+ const field = (fallback) => fallback === void 0 ? { $field: name } : {
172
+ $field: name,
173
+ fallback
174
+ };
175
+ const listValued = component === "ChoicePicker" && mapped.$type !== "CheckboxGroup" && typeof value !== "string";
176
+ context.inputFields.set(name, listValued ? [field(Array.isArray(value) ? value[0] : void 0)] : field(value));
177
+ }
178
+ const action = mapped.$action;
179
+ const raw = node["action"];
180
+ if (!action || !isRecord(raw)) return;
181
+ const functionCall = action.type === "a2ui:functionCall";
182
+ const event = isRecord(raw["event"]) ? raw["event"] : raw;
183
+ const userMessage = event["userMessage"];
184
+ if (!functionCall && isBinding(userMessage)) context.boundUserMessages.push({
185
+ action,
186
+ pointer: pointerIn(scope, userMessage.path)
187
+ });
188
+ const rawEntries = functionCall ? isRecord(raw["functionCall"]) ? raw["functionCall"]["args"] : void 0 : event["context"];
189
+ const target = functionCall ? action["args"] : action["context"];
190
+ if (!isRecord(rawEntries) || !isRecord(target)) return;
191
+ for (const [key, entry] of Object.entries(rawEntries)) if (isBinding(entry)) context.boundActionEntries.push({
192
+ target,
193
+ key,
194
+ pointer: pointerIn(scope, entry.path)
195
+ });
196
+ };
197
+ const spendEvaluation = (context) => {
198
+ if (context.evaluations < EVALUATION_BUDGET) {
199
+ context.evaluations++;
200
+ return true;
201
+ }
202
+ if (!context.evaluationBudgetWarned) {
203
+ context.warnings.push(`A2UI function evaluation budget of ${EVALUATION_BUDGET} was reached.`);
204
+ context.evaluationBudgetWarned = true;
205
+ }
206
+ return false;
207
+ };
89
208
  const reserveNode = (context) => {
90
209
  if (context.emittedNodes >= NODE_BUDGET) {
91
210
  if (!context.budgetWarned) {
@@ -105,14 +224,14 @@ const childReferences = (node) => {
105
224
  if (node["component"] === "Tabs" && Array.isArray(node["tabs"])) for (const tab of node["tabs"]) references.push(isRecord(tab) ? tab["child"] : void 0);
106
225
  return references;
107
226
  };
108
- const childrenOf = (node, dataSource, context, depth, visited) => {
227
+ const childrenOf = (node, scope, context, depth, visited) => {
109
228
  const result = [];
110
229
  for (const childId of childReferences(node)) {
111
230
  if (typeof childId !== "string") {
112
231
  context.warnings.push(`Component "${String(node["id"] ?? "")}" has a malformed child reference.`);
113
232
  continue;
114
233
  }
115
- const child = convertComponent(childId, dataSource, context, depth + 1, visited);
234
+ const child = convertComponent(childId, scope, context, depth + 1, visited);
116
235
  if (child) result.push(child);
117
236
  }
118
237
  return result;
@@ -127,17 +246,35 @@ const textLabel = (node, children, context) => {
127
246
  };
128
247
  const mappedAction = (node, props, context) => {
129
248
  const action = props["action"];
130
- const event = isRecord(action) && isRecord(action["event"]) ? action["event"] : action;
131
- const actionName = typeof event === "string" ? event : isRecord(event) && typeof event["name"] === "string" ? event["name"] : void 0;
132
- if (!actionName) return void 0;
133
- const actionContext = isRecord(event) ? event["context"] : void 0;
134
- return {
135
- type: "a2ui:action",
136
- name: actionName,
249
+ if (action === void 0) return void 0;
250
+ const source = {
137
251
  surfaceId: context.surfaceId,
138
- sourceComponentId: typeof node["id"] === "string" ? node["id"] : "",
139
- ...actionContext !== void 0 ? { context: actionContext } : {}
252
+ sourceComponentId: typeof node["id"] === "string" ? node["id"] : ""
140
253
  };
254
+ const event = isRecord(action) && isRecord(action["event"]) ? action["event"] : action;
255
+ const actionName = typeof event === "string" ? event : isRecord(event) && typeof event["name"] === "string" ? event["name"] : void 0;
256
+ if (actionName) {
257
+ const actionContext = isRecord(event) ? event["context"] : void 0;
258
+ const userMessage = isRecord(event) ? event["userMessage"] : void 0;
259
+ return {
260
+ type: "a2ui:action",
261
+ name: actionName,
262
+ ...source,
263
+ ...typeof userMessage === "string" ? { userMessage } : {},
264
+ ...actionContext !== void 0 ? { context: actionContext } : {}
265
+ };
266
+ }
267
+ const functionCall = isRecord(action) ? action["functionCall"] : void 0;
268
+ if (isRecord(functionCall) && typeof functionCall["call"] === "string") {
269
+ const args = functionCall["args"];
270
+ return {
271
+ type: "a2ui:functionCall",
272
+ call: functionCall["call"],
273
+ ...source,
274
+ ...isRecord(args) ? { args } : {}
275
+ };
276
+ }
277
+ context.warnings.push(`Component "${String(node["id"] ?? "")}" has a malformed action.`);
141
278
  };
142
279
  const choiceOptions = (value) => {
143
280
  if (!Array.isArray(value)) return [];
@@ -148,7 +285,7 @@ const choiceOptions = (value) => {
148
285
  });
149
286
  return options;
150
287
  };
151
- const mappedProps = (node, props, context) => {
288
+ const mappedProps = (node, props, context, scope) => {
152
289
  const component = node["component"];
153
290
  if (component === "Text") {
154
291
  const text = stringProp(props, ["text", "value"]);
@@ -239,35 +376,37 @@ const mappedProps = (node, props, context) => {
239
376
  };
240
377
  if (component === "Button") {
241
378
  const label = stringProp(props, ["label", "text"]);
242
- const buttonStyle = props["buttonStyle"];
379
+ const variantStyle = props["variant"] === "primary" ? "primary" : props["variant"] === "borderless" ? "ghost" : void 0;
380
+ const buttonStyle = typeof props["buttonStyle"] === "string" ? props["buttonStyle"] : variantStyle;
243
381
  const block = props["block"];
244
382
  const submit = props["submit"];
245
383
  const action = mappedAction(node, props, context);
246
384
  return {
247
385
  $type: "Button",
248
386
  ...label !== void 0 ? { label } : {},
249
- ...typeof buttonStyle === "string" ? { buttonStyle } : {},
387
+ ...buttonStyle !== void 0 ? { buttonStyle } : {},
250
388
  ...typeof block === "boolean" ? { block } : {},
251
389
  ...typeof submit === "boolean" ? { submit } : {},
252
390
  ...action ? { $action: action } : {}
253
391
  };
254
392
  }
255
- const binding = firstDefined(node, [
256
- "text",
257
- "value",
258
- "binding"
259
- ]);
260
- const name = lastPointerSegment(bindingPath(binding));
393
+ const bound = bindingPath(firstDefined(node, BOUND_KEYS));
394
+ const name = bound === void 0 ? void 0 : pointerIn(scope, bound);
261
395
  const label = stringProp(props, ["label"]);
262
396
  if (component === "TextField") {
263
397
  const placeholder = stringProp(props, ["placeholder"]);
264
- const multiline = typeof props["multiline"] === "boolean" ? props["multiline"] : props["textFieldType"] === "longText" ? true : void 0;
398
+ const inputType = props["variant"] === "obscured" ? "password" : props["variant"] === "number" ? "number" : void 0;
399
+ const multiline = typeof props["multiline"] === "boolean" ? props["multiline"] : props["variant"] === "longText" || props["textFieldType"] === "longText" ? true : void 0;
400
+ const initial = firstDefined(props, ["value", "text"]);
401
+ const defaultValue = typeof initial === "string" ? initial : typeof initial === "number" && Number.isFinite(initial) ? String(initial) : void 0;
265
402
  return {
266
403
  $type: "Input",
404
+ ...inputType !== void 0 ? { inputType } : {},
267
405
  ...placeholder !== void 0 ? { placeholder } : {},
268
406
  ...multiline !== void 0 ? { multiline } : {},
269
407
  ...label !== void 0 ? { label } : {},
270
- ...name !== void 0 ? { name } : {}
408
+ ...name !== void 0 ? { name } : {},
409
+ ...defaultValue !== void 0 ? { defaultValue } : {}
271
410
  };
272
411
  }
273
412
  if (component === "CheckBox") {
@@ -279,28 +418,51 @@ const mappedProps = (node, props, context) => {
279
418
  ...typeof defaultChecked === "boolean" ? { defaultChecked } : {}
280
419
  };
281
420
  }
421
+ if (component === "Slider") {
422
+ const min = finiteNumber(props["min"]) ?? 0;
423
+ const max = finiteNumber(props["max"]);
424
+ if (max === void 0 || max < min) return void 0;
425
+ const value = finiteNumber(props["value"]);
426
+ const step = sliderStep(min, max, name === void 0 ? value : finiteNumber(resolvePointer(context.stepModel, name)), props["steps"]);
427
+ return {
428
+ $type: "Slider",
429
+ min,
430
+ max,
431
+ ...step !== void 0 ? { step } : {},
432
+ ...value !== void 0 ? { defaultValue: value } : {},
433
+ ...label !== void 0 ? { label } : {},
434
+ ...name !== void 0 ? { name } : {}
435
+ };
436
+ }
282
437
  if (component === "ChoicePicker") {
283
438
  const options = choiceOptions(props["options"]);
284
439
  const value = props["value"];
285
- const defaultValue = typeof value === "string" ? value : Array.isArray(value) && typeof value[0] === "string" ? value[0] : void 0;
286
- if (props["variant"] === "radio" || props["variant"] === "single" || props["variant"] === "singleSelection" || props["displayStyle"] === "radio" || props["variant"] === "mutuallyExclusive" && props["displayStyle"] !== "chips") return {
287
- $type: "RadioGroup",
440
+ const selected = typeof value === "string" ? [value] : Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
441
+ if (props["variant"] === "multipleSelection") return {
442
+ $type: "CheckboxGroup",
288
443
  options,
289
444
  ...label !== void 0 ? { label } : {},
290
445
  ...name !== void 0 ? { name } : {},
291
- ...defaultValue !== void 0 ? { defaultValue } : {}
446
+ ...selected.length > 0 ? { defaultValue: selected } : {}
292
447
  };
293
- const placeholder = stringProp(props, ["placeholder"]);
448
+ const [defaultValue] = selected;
294
449
  return {
295
- $type: "Select",
450
+ $type: "RadioGroup",
296
451
  options,
297
- ...placeholder !== void 0 ? { placeholder } : {},
298
452
  ...label !== void 0 ? { label } : {},
299
- ...name !== void 0 ? { name } : {}
453
+ ...name !== void 0 ? { name } : {},
454
+ ...defaultValue !== void 0 ? { defaultValue } : {}
300
455
  };
301
456
  }
302
457
  if (component === "DateTimeInput") {
303
458
  const value = stringProp(props, ["value"]);
459
+ const shapeValue = name === void 0 ? value : resolvePointer(context.stepModel, name);
460
+ if (props["enableTime"] === true || typeof shapeValue === "string" && shapeValue !== "" && !DATE_PATTERN.test(shapeValue)) return {
461
+ $type: "Input",
462
+ ...value !== void 0 ? { defaultValue: value } : {},
463
+ ...label !== void 0 ? { label } : {},
464
+ ...name !== void 0 ? { name } : {}
465
+ };
304
466
  const min = stringProp(props, ["min"]);
305
467
  const max = stringProp(props, ["max"]);
306
468
  return {
@@ -313,11 +475,12 @@ const mappedProps = (node, props, context) => {
313
475
  };
314
476
  }
315
477
  };
316
- const convertTemplate = (node, templateChildren, dataSource, context, depth, visited, retained, mappedContainer) => {
478
+ const convertTemplate = (node, template, scope, context, depth, visited, converted) => {
317
479
  if (!reserveNode(context)) return null;
318
- const horizontalList = node["component"] === "List" && materialize(node["direction"], dataSource) === "horizontal";
319
- const container = mappedContainer ?? retained ?? { $type: horizontalList ? "Row" : "ListView" };
320
- const list = resolvePointer(dataSource, templateChildren.template.path);
480
+ const container = converted ?? { $type: "ListView" };
481
+ const itemized = container.$type === "ListView";
482
+ const list = resolvePointer(context.surface.dataModel, pointerIn(scope, template.path));
483
+ const listPointer = pointerIn(scope, template.path);
321
484
  if (!Array.isArray(list)) {
322
485
  context.warnings.push(`Template on component "${String(node["id"] ?? "")}" did not resolve to a list.`);
323
486
  return {
@@ -332,21 +495,23 @@ const convertTemplate = (node, templateChildren, dataSource, context, depth, vis
332
495
  }
333
496
  const children = [];
334
497
  for (let index = 0; index < itemCount; index++) {
335
- if (!retained && !horizontalList && !reserveNode(context)) break;
336
- const child = convertComponent(templateChildren.template.componentId, list[index], context, depth + 1, visited);
337
- if (retained || horizontalList) {
338
- if (child) children.push(child);
339
- } else children.push({
498
+ if (itemized && !reserveNode(context)) break;
499
+ const child = convertComponent(template.componentId, {
500
+ data: list[index],
501
+ path: `${listPointer}/${index}`
502
+ }, context, depth + 1, visited);
503
+ if (itemized) children.push({
340
504
  $type: "ListViewItem",
341
505
  ...child ? { children: child } : {}
342
506
  });
507
+ else if (child) children.push(child);
343
508
  }
344
509
  return {
345
510
  ...container,
346
511
  children
347
512
  };
348
513
  };
349
- function convertComponent(componentId, dataSource, context, depth, visited) {
514
+ function convertComponent(componentId, scope, context, depth, visited) {
350
515
  if (depth >= DEPTH_CAP) {
351
516
  if (!context.depthWarned) {
352
517
  context.warnings.push(`A2UI depth cap of ${DEPTH_CAP} was reached.`);
@@ -370,19 +535,61 @@ function convertComponent(componentId, dataSource, context, depth, visited) {
370
535
  context.warnings.push(`Unknown A2UI component "${String(component ?? "")}" was skipped.`);
371
536
  return null;
372
537
  }
373
- const templateChildren = node["children"];
374
- const hasTemplate = isTemplateChildren(templateChildren);
375
- if (!SUPPORTED_COMPONENTS.has(component) && !context.keepUnknownComponents && !hasTemplate) {
538
+ const template = childTemplateOf(node["children"]);
539
+ if (!SUPPORTED_COMPONENTS.has(component) && !context.keepUnknownComponents && !template) {
376
540
  context.warnings.push(`Unknown A2UI component "${component}" was skipped.`);
377
541
  return null;
378
542
  }
379
- const props = {};
380
- for (const [key, value] of Object.entries(node)) {
381
- if (key === "id" || key === "component" || key === "children" || key === "child") continue;
382
- const resolved = materialize(value, dataSource);
383
- if (resolved !== void 0) setOwnProperty(props, key, resolved);
543
+ const cacheKey = context.cache ? JSON.stringify([
544
+ componentId,
545
+ scope.path,
546
+ depth
547
+ ]) : "";
548
+ let cached = context.cache?.get(cacheKey);
549
+ if (cached && (context.evaluations + cached.evaluations > EVALUATION_BUDGET || [...cached.dependencies].some(([path, value]) => !Object.is(resolvePointer(context.surface.dataModel, path), value)))) cached = void 0;
550
+ let props;
551
+ let mapped;
552
+ if (cached) {
553
+ props = cached.props;
554
+ mapped = cached.mapped;
555
+ context.evaluations += cached.evaluations;
556
+ } else {
557
+ props = {};
558
+ const dependencies = /* @__PURE__ */ new Map();
559
+ const evaluations = context.evaluations;
560
+ context.dependencies = dependencies;
561
+ context.scopePath = scope.path;
562
+ for (const [key, value] of Object.entries(node)) {
563
+ if (key === "id" || key === "component" || key === "children" || key === "child") continue;
564
+ const resolved = materialize(value, scope.data, context, key !== "checks");
565
+ if (resolved !== void 0) setOwnProperty(props, key, resolved);
566
+ }
567
+ context.dependencies = void 0;
568
+ mapped = mappedProps(node, props, context, scope);
569
+ if (context.cache && context.warnings.length === 0 && !mapped?.$action) {
570
+ cached = {
571
+ props,
572
+ mapped,
573
+ dependencies,
574
+ evaluations: context.evaluations - evaluations
575
+ };
576
+ context.cache.set(cacheKey, cached);
577
+ }
578
+ }
579
+ if (mapped && context.bindings && typeof mapped["name"] === "string" && INPUT_COMPONENTS.has(component)) {
580
+ const field = mapped.$type === "Checkbox" ? "checked" : "value";
581
+ const defaultField = mapped.$type === "Checkbox" ? "defaultChecked" : "defaultValue";
582
+ const fallback = mapped.$type === "Checkbox" ? false : mapped.$type === "CheckboxGroup" ? [] : mapped.$type === "Slider" ? mapped["min"] : "";
583
+ const { [defaultField]: initialValue, ...rest } = mapped;
584
+ mapped = {
585
+ ...rest,
586
+ [field]: initialValue ?? mapped[field] ?? fallback
587
+ };
588
+ context.bindings.set(mapped["name"], {
589
+ value: mapped[field],
590
+ arrayValue: component === "ChoicePicker" && mapped.$type !== "CheckboxGroup" && typeof props["value"] !== "string"
591
+ });
384
592
  }
385
- const mapped = mappedProps(node, props, context);
386
593
  if (!mapped && SUPPORTED_COMPONENTS.has(component)) {
387
594
  context.warnings.push(`A2UI component "${component}" could not be mapped and was skipped.`);
388
595
  return null;
@@ -391,11 +598,12 @@ function convertComponent(componentId, dataSource, context, depth, visited) {
391
598
  $type: component,
392
599
  ...Object.fromEntries(Object.entries(props).filter(([key]) => !key.startsWith("$")))
393
600
  } : void 0;
394
- if (hasTemplate) return convertTemplate(node, templateChildren, dataSource, context, depth, visited, retained, component === "List" ? mapped : void 0);
601
+ if (template) return convertTemplate(node, template, scope, context, depth, visited, CHILD_LIST_COMPONENTS.has(component) ? mapped : retained);
395
602
  if (!reserveNode(context)) return null;
396
603
  const converted = mapped ?? retained;
397
604
  if (!converted) return null;
398
- const children = childrenOf(node, dataSource, context, depth, visited);
605
+ recordBindings(node, props, converted, scope, context);
606
+ const children = childrenOf(node, scope, context, depth, visited);
399
607
  if (mapped?.$type === "Button" && mapped["label"] === void 0) {
400
608
  const label = textLabel(node, children, context);
401
609
  if (label !== void 0) return {
@@ -407,15 +615,39 @@ function convertComponent(componentId, dataSource, context, depth, visited) {
407
615
  $type: "ListViewItem",
408
616
  children: child
409
617
  })) : children;
410
- return {
618
+ if (cached?.result && cached.children?.length === listChildren.length && cached.children.every((child, index) => child === listChildren[index])) return cached.result;
619
+ const result = {
411
620
  ...converted,
412
621
  ...listChildren.length > 0 ? { children: listChildren } : {}
413
622
  };
623
+ if (cached) {
624
+ cached.children = listChildren;
625
+ cached.result = result;
626
+ }
627
+ return result;
414
628
  } finally {
415
629
  visited.delete(componentId);
416
630
  }
417
631
  }
418
632
  function convertSurfaceToUISpec(surface, options = {}) {
633
+ return convertSurface(surface, options.keepUnknownComponents === true);
634
+ }
635
+ const createLiveSurfaceConverter = (surface) => {
636
+ const cache = /* @__PURE__ */ new Map();
637
+ const templates = /* @__PURE__ */ new Map();
638
+ return (dataModel) => {
639
+ const bindings = /* @__PURE__ */ new Map();
640
+ return {
641
+ ...convertSurface({
642
+ ...surface,
643
+ dataModel,
644
+ [A2UI_SURFACE_ID]: sourceSurfaceId(surface)
645
+ }, false, bindings, cache, templates, surface.dataModel),
646
+ bindings
647
+ };
648
+ };
649
+ };
650
+ function convertSurface(surface, keepUnknownComponents, bindings, cache, templates = /* @__PURE__ */ new Map(), stepModel = surface.dataModel) {
419
651
  const warnings = [];
420
652
  if (!surface.components.has("root")) return {
421
653
  spec: null,
@@ -423,17 +655,46 @@ function convertSurfaceToUISpec(surface, options = {}) {
423
655
  };
424
656
  const context = {
425
657
  surface,
658
+ stepModel,
426
659
  surfaceId: sourceSurfaceId(surface),
427
660
  warnings,
428
661
  emittedNodes: 0,
429
662
  depthWarned: false,
430
663
  budgetWarned: false,
431
664
  templateCapWarned: false,
432
- keepUnknownComponents: options.keepUnknownComponents === true
665
+ evaluations: 0,
666
+ evaluationBudgetWarned: false,
667
+ functionDepthWarned: false,
668
+ templates,
669
+ inputFields: /* @__PURE__ */ new Map(),
670
+ textFields: /* @__PURE__ */ new Map(),
671
+ boundActionEntries: [],
672
+ boundUserMessages: [],
673
+ keepUnknownComponents,
674
+ bindings,
675
+ cache,
676
+ dependencies: void 0,
677
+ scopePath: ""
433
678
  };
434
679
  try {
680
+ const spec = convertComponent("root", {
681
+ data: surface.dataModel,
682
+ path: ""
683
+ }, context, 0, /* @__PURE__ */ new Set());
684
+ for (const { target, key, pointer } of context.boundActionEntries) {
685
+ const value = withFieldReferences(target[key], pointer, context.inputFields);
686
+ if (value !== void 0) setOwnProperty(target, key, value);
687
+ }
688
+ for (const { action, pointer } of context.boundUserMessages) {
689
+ if (!context.textFields.get(pointer)) continue;
690
+ const fallback = action["userMessage"];
691
+ setOwnProperty(action, "userMessage", typeof fallback === "string" ? {
692
+ $field: pointer,
693
+ fallback
694
+ } : { $field: pointer });
695
+ }
435
696
  return {
436
- spec: convertComponent("root", surface.dataModel, context, 0, /* @__PURE__ */ new Set()),
697
+ spec,
437
698
  warnings
438
699
  };
439
700
  } catch {
@@ -445,6 +706,6 @@ function convertSurfaceToUISpec(surface, options = {}) {
445
706
  }
446
707
  }
447
708
  //#endregion
448
- export { convertSurfaceToUISpec };
709
+ export { convertSurfaceToUISpec, createLiveSurfaceConverter };
449
710
 
450
711
  //# sourceMappingURL=convert.js.map