@assistant-ui/react-generative-ui 0.0.20 → 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 +369 -87
  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 +1982 -145
  136. package/src/a2ui/convert.ts +677 -137
  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) {
@@ -97,33 +216,65 @@ const reserveNode = (context) => {
97
216
  context.emittedNodes++;
98
217
  return true;
99
218
  };
100
- const childrenOf = (node, dataSource, context, depth, visited) => {
219
+ const childReferences = (node) => {
101
220
  const children = node["children"];
102
- if (!Array.isArray(children)) return [];
221
+ const references = Array.isArray(children) ? [...children] : [];
222
+ if (node["child"] !== void 0) references.push(node["child"]);
223
+ if (node["component"] === "Modal") references.push(node["trigger"], node["content"]);
224
+ if (node["component"] === "Tabs" && Array.isArray(node["tabs"])) for (const tab of node["tabs"]) references.push(isRecord(tab) ? tab["child"] : void 0);
225
+ return references;
226
+ };
227
+ const childrenOf = (node, scope, context, depth, visited) => {
103
228
  const result = [];
104
- for (const childId of children) {
229
+ for (const childId of childReferences(node)) {
105
230
  if (typeof childId !== "string") {
106
231
  context.warnings.push(`Component "${String(node["id"] ?? "")}" has a malformed child reference.`);
107
232
  continue;
108
233
  }
109
- const child = convertComponent(childId, dataSource, context, depth + 1, visited);
234
+ const child = convertComponent(childId, scope, context, depth + 1, visited);
110
235
  if (child) result.push(child);
111
236
  }
112
237
  return result;
113
238
  };
114
- const mappedAction = (node, props, dataSource, context) => {
239
+ const textLabel = (node, children, context) => {
240
+ const references = childReferences(node);
241
+ const [reference] = references;
242
+ const [child] = children;
243
+ if (references.length !== 1 || children.length !== 1 || typeof reference !== "string" || !child || context.surface.components.get(reference)?.["component"] !== "Text") return;
244
+ const text = child.$type === "Header" ? child["text"] : child["value"];
245
+ return typeof text === "string" ? text : void 0;
246
+ };
247
+ const mappedAction = (node, props, context) => {
115
248
  const action = props["action"];
116
- const actionName = typeof action === "string" ? action : isRecord(action) && typeof action["name"] === "string" ? action["name"] : void 0;
117
- if (!actionName) return void 0;
118
- const rawContext = isRecord(action) ? action["context"] : void 0;
119
- const resolvedContext = rawContext === void 0 ? void 0 : materialize(rawContext, dataSource);
120
- return {
121
- type: "a2ui:action",
122
- name: actionName,
249
+ if (action === void 0) return void 0;
250
+ const source = {
123
251
  surfaceId: context.surfaceId,
124
- sourceComponentId: typeof node["id"] === "string" ? node["id"] : "",
125
- ...resolvedContext !== void 0 ? { context: resolvedContext } : {}
252
+ sourceComponentId: typeof node["id"] === "string" ? node["id"] : ""
126
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.`);
127
278
  };
128
279
  const choiceOptions = (value) => {
129
280
  if (!Array.isArray(value)) return [];
@@ -134,7 +285,7 @@ const choiceOptions = (value) => {
134
285
  });
135
286
  return options;
136
287
  };
137
- const mappedProps = (node, props, dataSource, context) => {
288
+ const mappedProps = (node, props, context, scope) => {
138
289
  const component = node["component"];
139
290
  if (component === "Text") {
140
291
  const text = stringProp(props, ["text", "value"]);
@@ -225,35 +376,37 @@ const mappedProps = (node, props, dataSource, context) => {
225
376
  };
226
377
  if (component === "Button") {
227
378
  const label = stringProp(props, ["label", "text"]);
228
- 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;
229
381
  const block = props["block"];
230
382
  const submit = props["submit"];
231
- const action = mappedAction(node, props, dataSource, context);
383
+ const action = mappedAction(node, props, context);
232
384
  return {
233
385
  $type: "Button",
234
386
  ...label !== void 0 ? { label } : {},
235
- ...typeof buttonStyle === "string" ? { buttonStyle } : {},
387
+ ...buttonStyle !== void 0 ? { buttonStyle } : {},
236
388
  ...typeof block === "boolean" ? { block } : {},
237
389
  ...typeof submit === "boolean" ? { submit } : {},
238
390
  ...action ? { $action: action } : {}
239
391
  };
240
392
  }
241
- const binding = firstDefined(node, [
242
- "text",
243
- "value",
244
- "binding"
245
- ]);
246
- const name = lastPointerSegment(bindingPath(binding));
393
+ const bound = bindingPath(firstDefined(node, BOUND_KEYS));
394
+ const name = bound === void 0 ? void 0 : pointerIn(scope, bound);
247
395
  const label = stringProp(props, ["label"]);
248
396
  if (component === "TextField") {
249
397
  const placeholder = stringProp(props, ["placeholder"]);
250
- 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;
251
402
  return {
252
403
  $type: "Input",
404
+ ...inputType !== void 0 ? { inputType } : {},
253
405
  ...placeholder !== void 0 ? { placeholder } : {},
254
406
  ...multiline !== void 0 ? { multiline } : {},
255
407
  ...label !== void 0 ? { label } : {},
256
- ...name !== void 0 ? { name } : {}
408
+ ...name !== void 0 ? { name } : {},
409
+ ...defaultValue !== void 0 ? { defaultValue } : {}
257
410
  };
258
411
  }
259
412
  if (component === "CheckBox") {
@@ -265,28 +418,51 @@ const mappedProps = (node, props, dataSource, context) => {
265
418
  ...typeof defaultChecked === "boolean" ? { defaultChecked } : {}
266
419
  };
267
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
+ }
268
437
  if (component === "ChoicePicker") {
269
438
  const options = choiceOptions(props["options"]);
270
439
  const value = props["value"];
271
- const defaultValue = typeof value === "string" ? value : Array.isArray(value) && typeof value[0] === "string" ? value[0] : void 0;
272
- if (props["variant"] === "radio" || props["variant"] === "single" || props["variant"] === "singleSelection" || props["displayStyle"] === "radio" || props["variant"] === "mutuallyExclusive" && props["displayStyle"] !== "chips") return {
273
- $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",
274
443
  options,
275
444
  ...label !== void 0 ? { label } : {},
276
445
  ...name !== void 0 ? { name } : {},
277
- ...defaultValue !== void 0 ? { defaultValue } : {}
446
+ ...selected.length > 0 ? { defaultValue: selected } : {}
278
447
  };
279
- const placeholder = stringProp(props, ["placeholder"]);
448
+ const [defaultValue] = selected;
280
449
  return {
281
- $type: "Select",
450
+ $type: "RadioGroup",
282
451
  options,
283
- ...placeholder !== void 0 ? { placeholder } : {},
284
452
  ...label !== void 0 ? { label } : {},
285
- ...name !== void 0 ? { name } : {}
453
+ ...name !== void 0 ? { name } : {},
454
+ ...defaultValue !== void 0 ? { defaultValue } : {}
286
455
  };
287
456
  }
288
457
  if (component === "DateTimeInput") {
289
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
+ };
290
466
  const min = stringProp(props, ["min"]);
291
467
  const max = stringProp(props, ["max"]);
292
468
  return {
@@ -299,11 +475,12 @@ const mappedProps = (node, props, dataSource, context) => {
299
475
  };
300
476
  }
301
477
  };
302
- const convertTemplate = (node, templateChildren, dataSource, context, depth, visited, retained, mappedContainer) => {
478
+ const convertTemplate = (node, template, scope, context, depth, visited, converted) => {
303
479
  if (!reserveNode(context)) return null;
304
- const horizontalList = node["component"] === "List" && materialize(node["direction"], dataSource) === "horizontal";
305
- const container = mappedContainer ?? retained ?? { $type: horizontalList ? "Row" : "ListView" };
306
- 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);
307
484
  if (!Array.isArray(list)) {
308
485
  context.warnings.push(`Template on component "${String(node["id"] ?? "")}" did not resolve to a list.`);
309
486
  return {
@@ -318,21 +495,23 @@ const convertTemplate = (node, templateChildren, dataSource, context, depth, vis
318
495
  }
319
496
  const children = [];
320
497
  for (let index = 0; index < itemCount; index++) {
321
- if (!retained && !horizontalList && !reserveNode(context)) break;
322
- const child = convertComponent(templateChildren.template.componentId, list[index], context, depth + 1, visited);
323
- if (retained || horizontalList) {
324
- if (child) children.push(child);
325
- } 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({
326
504
  $type: "ListViewItem",
327
505
  ...child ? { children: child } : {}
328
506
  });
507
+ else if (child) children.push(child);
329
508
  }
330
509
  return {
331
510
  ...container,
332
511
  children
333
512
  };
334
513
  };
335
- function convertComponent(componentId, dataSource, context, depth, visited) {
514
+ function convertComponent(componentId, scope, context, depth, visited) {
336
515
  if (depth >= DEPTH_CAP) {
337
516
  if (!context.depthWarned) {
338
517
  context.warnings.push(`A2UI depth cap of ${DEPTH_CAP} was reached.`);
@@ -356,19 +535,61 @@ function convertComponent(componentId, dataSource, context, depth, visited) {
356
535
  context.warnings.push(`Unknown A2UI component "${String(component ?? "")}" was skipped.`);
357
536
  return null;
358
537
  }
359
- const templateChildren = node["children"];
360
- const hasTemplate = isTemplateChildren(templateChildren);
361
- if (!SUPPORTED_COMPONENTS.has(component) && !context.keepUnknownComponents && !hasTemplate) {
538
+ const template = childTemplateOf(node["children"]);
539
+ if (!SUPPORTED_COMPONENTS.has(component) && !context.keepUnknownComponents && !template) {
362
540
  context.warnings.push(`Unknown A2UI component "${component}" was skipped.`);
363
541
  return null;
364
542
  }
365
- const props = {};
366
- for (const [key, value] of Object.entries(node)) {
367
- if (key === "id" || key === "component" || key === "children") continue;
368
- const resolved = materialize(value, dataSource);
369
- 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
+ });
370
592
  }
371
- const mapped = mappedProps(node, props, dataSource, context);
372
593
  if (!mapped && SUPPORTED_COMPONENTS.has(component)) {
373
594
  context.warnings.push(`A2UI component "${component}" could not be mapped and was skipped.`);
374
595
  return null;
@@ -377,24 +598,56 @@ function convertComponent(componentId, dataSource, context, depth, visited) {
377
598
  $type: component,
378
599
  ...Object.fromEntries(Object.entries(props).filter(([key]) => !key.startsWith("$")))
379
600
  } : void 0;
380
- 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);
381
602
  if (!reserveNode(context)) return null;
382
603
  const converted = mapped ?? retained;
383
604
  if (!converted) return null;
384
- const children = childrenOf(node, dataSource, context, depth, visited);
605
+ recordBindings(node, props, converted, scope, context);
606
+ const children = childrenOf(node, scope, context, depth, visited);
607
+ if (mapped?.$type === "Button" && mapped["label"] === void 0) {
608
+ const label = textLabel(node, children, context);
609
+ if (label !== void 0) return {
610
+ ...mapped,
611
+ label
612
+ };
613
+ }
385
614
  const listChildren = component === "List" && mapped?.$type === "ListView" ? children.map((child) => ({
386
615
  $type: "ListViewItem",
387
616
  children: child
388
617
  })) : children;
389
- return {
618
+ if (cached?.result && cached.children?.length === listChildren.length && cached.children.every((child, index) => child === listChildren[index])) return cached.result;
619
+ const result = {
390
620
  ...converted,
391
621
  ...listChildren.length > 0 ? { children: listChildren } : {}
392
622
  };
623
+ if (cached) {
624
+ cached.children = listChildren;
625
+ cached.result = result;
626
+ }
627
+ return result;
393
628
  } finally {
394
629
  visited.delete(componentId);
395
630
  }
396
631
  }
397
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) {
398
651
  const warnings = [];
399
652
  if (!surface.components.has("root")) return {
400
653
  spec: null,
@@ -402,17 +655,46 @@ function convertSurfaceToUISpec(surface, options = {}) {
402
655
  };
403
656
  const context = {
404
657
  surface,
658
+ stepModel,
405
659
  surfaceId: sourceSurfaceId(surface),
406
660
  warnings,
407
661
  emittedNodes: 0,
408
662
  depthWarned: false,
409
663
  budgetWarned: false,
410
664
  templateCapWarned: false,
411
- 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: ""
412
678
  };
413
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
+ }
414
696
  return {
415
- spec: convertComponent("root", surface.dataModel, context, 0, /* @__PURE__ */ new Set()),
697
+ spec,
416
698
  warnings
417
699
  };
418
700
  } catch {
@@ -424,6 +706,6 @@ function convertSurfaceToUISpec(surface, options = {}) {
424
706
  }
425
707
  }
426
708
  //#endregion
427
- export { convertSurfaceToUISpec };
709
+ export { convertSurfaceToUISpec, createLiveSurfaceConverter };
428
710
 
429
711
  //# sourceMappingURL=convert.js.map