@assistant-ui/react-generative-ui 0.0.10 → 0.0.12

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.
@@ -0,0 +1,10 @@
1
+ import { UIElement } from "../ir.js";
2
+ import { A2uiSurfaceState } from "./types.js";
3
+ //#region src/a2ui/convert.d.ts
4
+ declare function convertSurfaceToUISpec(surface: A2uiSurfaceState): {
5
+ spec: UIElement | null;
6
+ warnings: string[];
7
+ };
8
+ //#endregion
9
+ export { convertSurfaceToUISpec };
10
+ //# sourceMappingURL=convert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.d.ts","names":[],"sources":["../../src/a2ui/convert.ts"],"mappings":";;;iBAmbgB,uBAAuB,SAAS;EAC9C,MAAM;EACN"}
@@ -0,0 +1,321 @@
1
+ import { A2UI_SURFACE_ID } from "./types.js";
2
+ //#region src/a2ui/convert.ts
3
+ const DEPTH_CAP = 32;
4
+ const TEMPLATE_ITEM_CAP = 100;
5
+ const NODE_BUDGET = 5e3;
6
+ const SUPPORTED_COMPONENTS = /* @__PURE__ */ new Set([
7
+ "Text",
8
+ "Image",
9
+ "Row",
10
+ "Column",
11
+ "Card",
12
+ "Divider",
13
+ "Button",
14
+ "TextField",
15
+ "CheckBox"
16
+ ]);
17
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18
+ const isPlainObject = (value) => {
19
+ if (!isRecord(value)) return false;
20
+ const prototype = Object.getPrototypeOf(value);
21
+ return prototype === Object.prototype || prototype === null;
22
+ };
23
+ const isBinding = (value) => isPlainObject(value) && Object.keys(value).length === 1 && typeof value["path"] === "string";
24
+ 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";
25
+ const decodePointer = (path) => {
26
+ if (path === "" || path === "/") return [];
27
+ if (!path.startsWith("/")) return void 0;
28
+ return path.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
29
+ };
30
+ const resolvePointer = (source, path) => {
31
+ const segments = decodePointer(path);
32
+ if (!segments) return void 0;
33
+ let current = source;
34
+ for (const segment of segments) {
35
+ if (Array.isArray(current)) {
36
+ if (!/^(0|[1-9]\d*)$/.test(segment)) return void 0;
37
+ current = current[Number(segment)];
38
+ continue;
39
+ }
40
+ if (!isRecord(current) || !Object.hasOwn(current, segment)) return;
41
+ current = current[segment];
42
+ }
43
+ return current;
44
+ };
45
+ const bindingPath = (value) => isBinding(value) ? value.path : void 0;
46
+ const lastPointerSegment = (path) => {
47
+ if (!path) return void 0;
48
+ return decodePointer(path)?.at(-1);
49
+ };
50
+ const materialize = (value, source) => {
51
+ if (isBinding(value)) return resolvePointer(source, value.path);
52
+ if (Array.isArray(value)) return value.map((entry) => materialize(entry, source)).filter((entry) => entry !== void 0);
53
+ if (!isPlainObject(value)) return value;
54
+ const result = {};
55
+ for (const [key, entry] of Object.entries(value)) {
56
+ const resolved = materialize(entry, source);
57
+ if (resolved !== void 0) result[key] = resolved;
58
+ }
59
+ return result;
60
+ };
61
+ const firstDefined = (props, keys) => {
62
+ for (const key of keys) if (props[key] !== void 0) return props[key];
63
+ };
64
+ const stringProp = (props, keys) => {
65
+ const value = firstDefined(props, keys);
66
+ return typeof value === "string" ? value : void 0;
67
+ };
68
+ const sourceSurfaceId = (surface) => surface[A2UI_SURFACE_ID] ?? "";
69
+ const reserveNode = (context) => {
70
+ if (context.emittedNodes >= NODE_BUDGET) {
71
+ if (!context.budgetWarned) {
72
+ context.warnings.push(`A2UI node budget of ${NODE_BUDGET} was reached.`);
73
+ context.budgetWarned = true;
74
+ }
75
+ return false;
76
+ }
77
+ context.emittedNodes++;
78
+ return true;
79
+ };
80
+ const childrenOf = (node, dataSource, context, depth, visited) => {
81
+ const children = node["children"];
82
+ if (!Array.isArray(children)) return [];
83
+ const result = [];
84
+ for (const childId of children) {
85
+ if (typeof childId !== "string") {
86
+ context.warnings.push(`Component "${String(node["id"] ?? "")}" has a malformed child reference.`);
87
+ continue;
88
+ }
89
+ const child = convertComponent(childId, dataSource, context, depth + 1, visited);
90
+ if (child) result.push(child);
91
+ }
92
+ return result;
93
+ };
94
+ const mappedAction = (node, props, dataSource, context) => {
95
+ const action = props["action"];
96
+ const actionName = typeof action === "string" ? action : isRecord(action) && typeof action["name"] === "string" ? action["name"] : void 0;
97
+ if (!actionName) return void 0;
98
+ const rawContext = isRecord(action) ? action["context"] : void 0;
99
+ const resolvedContext = rawContext === void 0 ? void 0 : materialize(rawContext, dataSource);
100
+ return {
101
+ type: "a2ui:action",
102
+ name: actionName,
103
+ surfaceId: context.surfaceId,
104
+ sourceComponentId: typeof node["id"] === "string" ? node["id"] : "",
105
+ ...resolvedContext !== void 0 ? { context: resolvedContext } : {}
106
+ };
107
+ };
108
+ const mappedProps = (node, props, dataSource, context) => {
109
+ const component = node["component"];
110
+ if (component === "Text") {
111
+ const text = stringProp(props, ["text", "value"]);
112
+ const variant = props["variant"];
113
+ if (typeof variant === "string" && /^h[1-6]$/.test(variant)) return {
114
+ $type: "Header",
115
+ ...text !== void 0 ? { text } : {}
116
+ };
117
+ if (variant === "caption") return {
118
+ $type: "Caption",
119
+ ...text !== void 0 ? { value: text } : {}
120
+ };
121
+ return {
122
+ $type: "Markdown",
123
+ ...text !== void 0 ? { value: text } : {}
124
+ };
125
+ }
126
+ if (component === "Image") {
127
+ const src = stringProp(props, ["src", "url"]);
128
+ const alt = stringProp(props, ["alt", "altText"]);
129
+ const size = props["size"];
130
+ const round = props["round"];
131
+ return {
132
+ $type: "Image",
133
+ ...src !== void 0 ? { src } : {},
134
+ ...alt !== void 0 ? { alt } : {},
135
+ ...typeof size === "string" || typeof size === "number" ? { size } : {},
136
+ ...typeof round === "boolean" ? { round } : {}
137
+ };
138
+ }
139
+ if (component === "Row") {
140
+ const gap = props["gap"];
141
+ const align = firstDefined(props, ["align", "alignment"]);
142
+ const justify = firstDefined(props, ["justify", "distribution"]);
143
+ return {
144
+ $type: "Row",
145
+ ...typeof gap === "number" ? { gap } : {},
146
+ ...typeof align === "string" ? { align } : {},
147
+ ...typeof justify === "string" ? { justify } : {}
148
+ };
149
+ }
150
+ if (component === "Column") {
151
+ const gap = props["gap"];
152
+ const align = firstDefined(props, ["align", "alignment"]);
153
+ return {
154
+ $type: "Col",
155
+ ...typeof gap === "number" ? { gap } : {},
156
+ ...typeof align === "string" ? { align } : {}
157
+ };
158
+ }
159
+ if (component === "Card") {
160
+ const title = props["title"];
161
+ const padding = props["padding"];
162
+ const background = props["background"];
163
+ return {
164
+ $type: "Card",
165
+ ...typeof title === "string" ? { title } : {},
166
+ ...typeof padding === "number" ? { padding } : {},
167
+ ...typeof background === "string" ? { background } : {}
168
+ };
169
+ }
170
+ if (component === "Divider") return {
171
+ $type: "Divider",
172
+ ...typeof props["flush"] === "boolean" ? { flush: props["flush"] } : {}
173
+ };
174
+ if (component === "Button") {
175
+ const label = stringProp(props, ["label", "text"]);
176
+ const buttonStyle = props["buttonStyle"];
177
+ const block = props["block"];
178
+ const submit = props["submit"];
179
+ const action = mappedAction(node, props, dataSource, context);
180
+ return {
181
+ $type: "Button",
182
+ ...label !== void 0 ? { label } : {},
183
+ ...typeof buttonStyle === "string" ? { buttonStyle } : {},
184
+ ...typeof block === "boolean" ? { block } : {},
185
+ ...typeof submit === "boolean" ? { submit } : {},
186
+ ...action ? { $action: action } : {}
187
+ };
188
+ }
189
+ const binding = firstDefined(node, [
190
+ "text",
191
+ "value",
192
+ "binding"
193
+ ]);
194
+ const name = lastPointerSegment(bindingPath(binding));
195
+ const label = stringProp(props, ["label"]);
196
+ if (component === "TextField") {
197
+ const placeholder = stringProp(props, ["placeholder"]);
198
+ const multiline = typeof props["multiline"] === "boolean" ? props["multiline"] : props["textFieldType"] === "longText" ? true : void 0;
199
+ return {
200
+ $type: "Input",
201
+ ...placeholder !== void 0 ? { placeholder } : {},
202
+ ...multiline !== void 0 ? { multiline } : {},
203
+ ...label !== void 0 ? { label } : {},
204
+ ...name !== void 0 ? { name } : {}
205
+ };
206
+ }
207
+ if (component === "CheckBox") {
208
+ const defaultChecked = firstDefined(props, ["defaultChecked", "value"]);
209
+ return {
210
+ $type: "Checkbox",
211
+ ...label !== void 0 ? { label } : {},
212
+ ...name !== void 0 ? { name } : {},
213
+ ...typeof defaultChecked === "boolean" ? { defaultChecked } : {}
214
+ };
215
+ }
216
+ };
217
+ const convertTemplate = (node, templateChildren, dataSource, context, depth, visited) => {
218
+ if (!reserveNode(context)) return null;
219
+ const list = resolvePointer(dataSource, templateChildren.template.path);
220
+ if (!Array.isArray(list)) {
221
+ context.warnings.push(`Template on component "${String(node["id"] ?? "")}" did not resolve to a list.`);
222
+ return {
223
+ $type: "ListView",
224
+ children: []
225
+ };
226
+ }
227
+ const itemCount = Math.min(list.length, TEMPLATE_ITEM_CAP);
228
+ if (list.length > TEMPLATE_ITEM_CAP && !context.templateCapWarned) {
229
+ context.warnings.push(`A2UI template expansion was capped at ${TEMPLATE_ITEM_CAP} items.`);
230
+ context.templateCapWarned = true;
231
+ }
232
+ const children = [];
233
+ for (let index = 0; index < itemCount; index++) {
234
+ if (!reserveNode(context)) break;
235
+ const child = convertComponent(templateChildren.template.componentId, list[index], context, depth + 1, visited);
236
+ children.push({
237
+ $type: "ListViewItem",
238
+ ...child ? { children: child } : {}
239
+ });
240
+ }
241
+ return {
242
+ $type: "ListView",
243
+ children
244
+ };
245
+ };
246
+ function convertComponent(componentId, dataSource, context, depth, visited) {
247
+ if (depth >= DEPTH_CAP) {
248
+ if (!context.depthWarned) {
249
+ context.warnings.push(`A2UI depth cap of ${DEPTH_CAP} was reached.`);
250
+ context.depthWarned = true;
251
+ }
252
+ return null;
253
+ }
254
+ if (visited.has(componentId)) {
255
+ context.warnings.push(`A2UI component cycle detected at "${componentId}".`);
256
+ return null;
257
+ }
258
+ const node = context.surface.components.get(componentId);
259
+ if (!node) {
260
+ context.warnings.push(`A2UI component "${componentId}" was not found.`);
261
+ return null;
262
+ }
263
+ const component = node["component"];
264
+ visited.add(componentId);
265
+ try {
266
+ const templateChildren = node["children"];
267
+ if (isTemplateChildren(templateChildren)) return convertTemplate(node, templateChildren, dataSource, context, depth, visited);
268
+ if (typeof component !== "string" || !SUPPORTED_COMPONENTS.has(component)) {
269
+ context.warnings.push(`Unknown A2UI component "${String(component ?? "")}" was skipped.`);
270
+ return null;
271
+ }
272
+ if (!reserveNode(context)) return null;
273
+ const props = {};
274
+ for (const [key, value] of Object.entries(node)) {
275
+ if (key === "id" || key === "component" || key === "children") continue;
276
+ const resolved = materialize(value, dataSource);
277
+ if (resolved !== void 0) props[key] = resolved;
278
+ }
279
+ const mapped = mappedProps(node, props, dataSource, context);
280
+ if (!mapped) return null;
281
+ const children = childrenOf(node, dataSource, context, depth, visited);
282
+ return {
283
+ ...mapped,
284
+ ...children.length > 0 ? { children } : {}
285
+ };
286
+ } finally {
287
+ visited.delete(componentId);
288
+ }
289
+ }
290
+ function convertSurfaceToUISpec(surface) {
291
+ const warnings = [];
292
+ if (!surface.components.has("root")) return {
293
+ spec: null,
294
+ warnings: ["A2UI root component \"root\" was not found."]
295
+ };
296
+ const context = {
297
+ surface,
298
+ surfaceId: sourceSurfaceId(surface),
299
+ warnings,
300
+ emittedNodes: 0,
301
+ depthWarned: false,
302
+ budgetWarned: false,
303
+ templateCapWarned: false
304
+ };
305
+ try {
306
+ return {
307
+ spec: convertComponent("root", surface.dataModel, context, 0, /* @__PURE__ */ new Set()),
308
+ warnings
309
+ };
310
+ } catch {
311
+ warnings.push("A2UI surface conversion encountered malformed input.");
312
+ return {
313
+ spec: null,
314
+ warnings
315
+ };
316
+ }
317
+ }
318
+ //#endregion
319
+ export { convertSurfaceToUISpec };
320
+
321
+ //# sourceMappingURL=convert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.js","names":[],"sources":["../../src/a2ui/convert.ts"],"sourcesContent":["import type { UIElement } from \"../ir\";\nimport {\n A2UI_SURFACE_ID,\n type A2uiSurfaceState,\n type A2uiTemplateChildren,\n} from \"./types\";\n\nconst DEPTH_CAP = 32;\nconst TEMPLATE_ITEM_CAP = 100;\nconst NODE_BUDGET = 5000;\n\nconst SUPPORTED_COMPONENTS = new Set([\n \"Text\",\n \"Image\",\n \"Row\",\n \"Column\",\n \"Card\",\n \"Divider\",\n \"Button\",\n \"TextField\",\n \"CheckBox\",\n]);\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> => {\n if (!isRecord(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n};\n\nconst isBinding = (value: unknown): value is { readonly path: string } =>\n isPlainObject(value) &&\n Object.keys(value).length === 1 &&\n typeof value[\"path\"] === \"string\";\n\nconst isTemplateChildren = (value: unknown): value is A2uiTemplateChildren =>\n isPlainObject(value) &&\n Object.keys(value).length === 1 &&\n isPlainObject(value[\"template\"]) &&\n Object.keys(value[\"template\"]).length === 2 &&\n typeof value[\"template\"][\"componentId\"] === \"string\" &&\n typeof value[\"template\"][\"path\"] === \"string\";\n\nconst decodePointer = (path: string): string[] | undefined => {\n if (path === \"\" || path === \"/\") return [];\n if (!path.startsWith(\"/\")) return undefined;\n return path\n .slice(1)\n .split(\"/\")\n .map((segment) => segment.replaceAll(\"~1\", \"/\").replaceAll(\"~0\", \"~\"));\n};\n\nconst resolvePointer = (source: unknown, path: string): unknown => {\n const segments = decodePointer(path);\n if (!segments) return undefined;\n let current = source;\n for (const segment of segments) {\n if (Array.isArray(current)) {\n if (!/^(0|[1-9]\\d*)$/.test(segment)) return undefined;\n current = current[Number(segment)];\n continue;\n }\n if (!isRecord(current) || !Object.hasOwn(current, segment)) {\n return undefined;\n }\n current = current[segment];\n }\n return current;\n};\n\nconst bindingPath = (value: unknown): string | undefined =>\n isBinding(value) ? value.path : undefined;\n\nconst lastPointerSegment = (path: string | undefined): string | undefined => {\n if (!path) return undefined;\n const segments = decodePointer(path);\n return segments?.at(-1);\n};\n\nconst materialize = (value: unknown, source: unknown): unknown => {\n if (isBinding(value)) return resolvePointer(source, value.path);\n if (Array.isArray(value)) {\n return value\n .map((entry) => materialize(entry, source))\n .filter((entry) => entry !== undefined);\n }\n if (!isPlainObject(value)) return value;\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value)) {\n const resolved = materialize(entry, source);\n if (resolved !== undefined) result[key] = resolved;\n }\n return result;\n};\n\nconst firstDefined = (\n props: Record<string, unknown>,\n keys: readonly string[],\n): unknown => {\n for (const key of keys) {\n if (props[key] !== undefined) return props[key];\n }\n return undefined;\n};\n\nconst stringProp = (\n props: Record<string, unknown>,\n keys: readonly string[],\n): string | undefined => {\n const value = firstDefined(props, keys);\n return typeof value === \"string\" ? value : undefined;\n};\n\nconst sourceSurfaceId = (surface: A2uiSurfaceState): string =>\n (surface as A2uiSurfaceState & { [A2UI_SURFACE_ID]?: string })[\n A2UI_SURFACE_ID\n ] ?? \"\";\n\ntype ConversionContext = {\n readonly surface: A2uiSurfaceState;\n readonly surfaceId: string;\n readonly warnings: string[];\n emittedNodes: number;\n depthWarned: boolean;\n budgetWarned: boolean;\n templateCapWarned: boolean;\n};\n\nconst reserveNode = (context: ConversionContext): boolean => {\n if (context.emittedNodes >= NODE_BUDGET) {\n if (!context.budgetWarned) {\n context.warnings.push(`A2UI node budget of ${NODE_BUDGET} was reached.`);\n context.budgetWarned = true;\n }\n return false;\n }\n context.emittedNodes++;\n return true;\n};\n\nconst childrenOf = (\n node: Record<string, unknown>,\n dataSource: unknown,\n context: ConversionContext,\n depth: number,\n visited: Set<string>,\n): UIElement[] => {\n const children = node[\"children\"];\n if (!Array.isArray(children)) return [];\n const result: UIElement[] = [];\n for (const childId of children) {\n if (typeof childId !== \"string\") {\n context.warnings.push(\n `Component \"${String(node[\"id\"] ?? \"\")}\" has a malformed child reference.`,\n );\n continue;\n }\n const child = convertComponent(\n childId,\n dataSource,\n context,\n depth + 1,\n visited,\n );\n if (child) result.push(child);\n }\n return result;\n};\n\nconst mappedAction = (\n node: Record<string, unknown>,\n props: Record<string, unknown>,\n dataSource: unknown,\n context: ConversionContext,\n): UIElement[\"$action\"] | undefined => {\n const action = props[\"action\"];\n const actionName =\n typeof action === \"string\"\n ? action\n : isRecord(action) && typeof action[\"name\"] === \"string\"\n ? action[\"name\"]\n : undefined;\n if (!actionName) return undefined;\n const rawContext = isRecord(action) ? action[\"context\"] : undefined;\n const resolvedContext =\n rawContext === undefined ? undefined : materialize(rawContext, dataSource);\n return {\n type: \"a2ui:action\",\n name: actionName,\n surfaceId: context.surfaceId,\n sourceComponentId: typeof node[\"id\"] === \"string\" ? node[\"id\"] : \"\",\n ...(resolvedContext !== undefined ? { context: resolvedContext } : {}),\n };\n};\n\nconst mappedProps = (\n node: Record<string, unknown>,\n props: Record<string, unknown>,\n dataSource: unknown,\n context: ConversionContext,\n): UIElement | undefined => {\n const component = node[\"component\"];\n\n if (component === \"Text\") {\n const text = stringProp(props, [\"text\", \"value\"]);\n const variant = props[\"variant\"];\n if (typeof variant === \"string\" && /^h[1-6]$/.test(variant)) {\n return {\n $type: \"Header\",\n ...(text !== undefined ? { text } : {}),\n };\n }\n if (variant === \"caption\") {\n return {\n $type: \"Caption\",\n ...(text !== undefined ? { value: text } : {}),\n };\n }\n return {\n $type: \"Markdown\",\n ...(text !== undefined ? { value: text } : {}),\n };\n }\n\n if (component === \"Image\") {\n const src = stringProp(props, [\"src\", \"url\"]);\n const alt = stringProp(props, [\"alt\", \"altText\"]);\n const size = props[\"size\"];\n const round = props[\"round\"];\n return {\n $type: \"Image\",\n ...(src !== undefined ? { src } : {}),\n ...(alt !== undefined ? { alt } : {}),\n ...(typeof size === \"string\" || typeof size === \"number\" ? { size } : {}),\n ...(typeof round === \"boolean\" ? { round } : {}),\n };\n }\n\n if (component === \"Row\") {\n const gap = props[\"gap\"];\n const align = firstDefined(props, [\"align\", \"alignment\"]);\n const justify = firstDefined(props, [\"justify\", \"distribution\"]);\n return {\n $type: \"Row\",\n ...(typeof gap === \"number\" ? { gap } : {}),\n ...(typeof align === \"string\" ? { align } : {}),\n ...(typeof justify === \"string\" ? { justify } : {}),\n };\n }\n\n if (component === \"Column\") {\n const gap = props[\"gap\"];\n const align = firstDefined(props, [\"align\", \"alignment\"]);\n return {\n $type: \"Col\",\n ...(typeof gap === \"number\" ? { gap } : {}),\n ...(typeof align === \"string\" ? { align } : {}),\n };\n }\n\n if (component === \"Card\") {\n const title = props[\"title\"];\n const padding = props[\"padding\"];\n const background = props[\"background\"];\n return {\n $type: \"Card\",\n ...(typeof title === \"string\" ? { title } : {}),\n ...(typeof padding === \"number\" ? { padding } : {}),\n ...(typeof background === \"string\" ? { background } : {}),\n };\n }\n\n if (component === \"Divider\") {\n return {\n $type: \"Divider\",\n ...(typeof props[\"flush\"] === \"boolean\" ? { flush: props[\"flush\"] } : {}),\n };\n }\n\n if (component === \"Button\") {\n const label = stringProp(props, [\"label\", \"text\"]);\n const buttonStyle = props[\"buttonStyle\"];\n const block = props[\"block\"];\n const submit = props[\"submit\"];\n const action = mappedAction(node, props, dataSource, context);\n return {\n $type: \"Button\",\n ...(label !== undefined ? { label } : {}),\n ...(typeof buttonStyle === \"string\" ? { buttonStyle } : {}),\n ...(typeof block === \"boolean\" ? { block } : {}),\n ...(typeof submit === \"boolean\" ? { submit } : {}),\n ...(action ? { $action: action } : {}),\n };\n }\n\n const binding = firstDefined(node, [\"text\", \"value\", \"binding\"]);\n const name = lastPointerSegment(bindingPath(binding));\n const label = stringProp(props, [\"label\"]);\n\n if (component === \"TextField\") {\n const placeholder = stringProp(props, [\"placeholder\"]);\n const multiline =\n typeof props[\"multiline\"] === \"boolean\"\n ? props[\"multiline\"]\n : props[\"textFieldType\"] === \"longText\"\n ? true\n : undefined;\n return {\n $type: \"Input\",\n ...(placeholder !== undefined ? { placeholder } : {}),\n ...(multiline !== undefined ? { multiline } : {}),\n ...(label !== undefined ? { label } : {}),\n ...(name !== undefined ? { name } : {}),\n };\n }\n\n if (component === \"CheckBox\") {\n const defaultChecked = firstDefined(props, [\"defaultChecked\", \"value\"]);\n return {\n $type: \"Checkbox\",\n ...(label !== undefined ? { label } : {}),\n ...(name !== undefined ? { name } : {}),\n ...(typeof defaultChecked === \"boolean\" ? { defaultChecked } : {}),\n };\n }\n\n return undefined;\n};\n\nconst convertTemplate = (\n node: Record<string, unknown>,\n templateChildren: A2uiTemplateChildren,\n dataSource: unknown,\n context: ConversionContext,\n depth: number,\n visited: Set<string>,\n): UIElement | null => {\n if (!reserveNode(context)) return null;\n const list = resolvePointer(dataSource, templateChildren.template.path);\n if (!Array.isArray(list)) {\n context.warnings.push(\n `Template on component \"${String(node[\"id\"] ?? \"\")}\" did not resolve to a list.`,\n );\n return { $type: \"ListView\", children: [] };\n }\n const itemCount = Math.min(list.length, TEMPLATE_ITEM_CAP);\n if (list.length > TEMPLATE_ITEM_CAP && !context.templateCapWarned) {\n context.warnings.push(\n `A2UI template expansion was capped at ${TEMPLATE_ITEM_CAP} items.`,\n );\n context.templateCapWarned = true;\n }\n const children: UIElement[] = [];\n for (let index = 0; index < itemCount; index++) {\n if (!reserveNode(context)) break;\n const child = convertComponent(\n templateChildren.template.componentId,\n list[index],\n context,\n depth + 1,\n visited,\n );\n children.push({\n $type: \"ListViewItem\",\n ...(child ? { children: child } : {}),\n });\n }\n return { $type: \"ListView\", children };\n};\n\nfunction convertComponent(\n componentId: string,\n dataSource: unknown,\n context: ConversionContext,\n depth: number,\n visited: Set<string>,\n): UIElement | null {\n if (depth >= DEPTH_CAP) {\n if (!context.depthWarned) {\n context.warnings.push(`A2UI depth cap of ${DEPTH_CAP} was reached.`);\n context.depthWarned = true;\n }\n return null;\n }\n if (visited.has(componentId)) {\n context.warnings.push(`A2UI component cycle detected at \"${componentId}\".`);\n return null;\n }\n const node = context.surface.components.get(componentId);\n if (!node) {\n context.warnings.push(`A2UI component \"${componentId}\" was not found.`);\n return null;\n }\n const component = node[\"component\"];\n\n visited.add(componentId);\n try {\n const templateChildren = node[\"children\"];\n if (isTemplateChildren(templateChildren)) {\n return convertTemplate(\n node,\n templateChildren,\n dataSource,\n context,\n depth,\n visited,\n );\n }\n if (typeof component !== \"string\" || !SUPPORTED_COMPONENTS.has(component)) {\n context.warnings.push(\n `Unknown A2UI component \"${String(component ?? \"\")}\" was skipped.`,\n );\n return null;\n }\n if (!reserveNode(context)) return null;\n const props: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(node)) {\n if (key === \"id\" || key === \"component\" || key === \"children\") continue;\n const resolved = materialize(value, dataSource);\n if (resolved !== undefined) props[key] = resolved;\n }\n const mapped = mappedProps(node, props, dataSource, context);\n if (!mapped) return null;\n const children = childrenOf(node, dataSource, context, depth, visited);\n return {\n ...mapped,\n ...(children.length > 0 ? { children } : {}),\n };\n } finally {\n visited.delete(componentId);\n }\n}\n\nexport function convertSurfaceToUISpec(surface: A2uiSurfaceState): {\n spec: UIElement | null;\n warnings: string[];\n} {\n const warnings: string[] = [];\n if (!surface.components.has(\"root\")) {\n return {\n spec: null,\n warnings: ['A2UI root component \"root\" was not found.'],\n };\n }\n const context: ConversionContext = {\n surface,\n surfaceId: sourceSurfaceId(surface),\n warnings,\n emittedNodes: 0,\n depthWarned: false,\n budgetWarned: false,\n templateCapWarned: false,\n };\n try {\n const spec = convertComponent(\n \"root\",\n surface.dataModel,\n context,\n 0,\n new Set(),\n );\n return { spec, warnings };\n } catch {\n warnings.push(\"A2UI surface conversion encountered malformed input.\");\n return { spec: null, warnings };\n }\n}\n"],"mappings":";;AAOA,MAAM,YAAY;AAClB,MAAM,oBAAoB;AAC1B,MAAM,cAAc;AAEpB,MAAM,uCAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAErE,MAAM,iBAAiB,UAAqD;CAC1E,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO;CAC7B,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,OAAO,cAAc,OAAO,aAAa,cAAc;AACzD;AAEA,MAAM,aAAa,UACjB,cAAc,KAAK,KACnB,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,KAC9B,OAAO,MAAM,YAAY;AAE3B,MAAM,sBAAsB,UAC1B,cAAc,KAAK,KACnB,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,KAC9B,cAAc,MAAM,WAAW,KAC/B,OAAO,KAAK,MAAM,WAAW,CAAC,CAAC,WAAW,KAC1C,OAAO,MAAM,WAAW,CAAC,mBAAmB,YAC5C,OAAO,MAAM,WAAW,CAAC,YAAY;AAEvC,MAAM,iBAAiB,SAAuC;CAC5D,IAAI,SAAS,MAAM,SAAS,KAAK,OAAO,CAAC;CACzC,IAAI,CAAC,KAAK,WAAW,GAAG,GAAG,OAAO,KAAA;CAClC,OAAO,KACJ,MAAM,CAAC,CAAC,CACR,MAAM,GAAG,CAAC,CACV,KAAK,YAAY,QAAQ,WAAW,MAAM,GAAG,CAAC,CAAC,WAAW,MAAM,GAAG,CAAC;AACzE;AAEA,MAAM,kBAAkB,QAAiB,SAA0B;CACjE,MAAM,WAAW,cAAc,IAAI;CACnC,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,IAAI,UAAU;CACd,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,MAAM,QAAQ,OAAO,GAAG;GAC1B,IAAI,CAAC,iBAAiB,KAAK,OAAO,GAAG,OAAO,KAAA;GAC5C,UAAU,QAAQ,OAAO,OAAO;GAChC;EACF;EACA,IAAI,CAAC,SAAS,OAAO,KAAK,CAAC,OAAO,OAAO,SAAS,OAAO,GACvD;EAEF,UAAU,QAAQ;CACpB;CACA,OAAO;AACT;AAEA,MAAM,eAAe,UACnB,UAAU,KAAK,IAAI,MAAM,OAAO,KAAA;AAElC,MAAM,sBAAsB,SAAiD;CAC3E,IAAI,CAAC,MAAM,OAAO,KAAA;CAElB,OADiB,cAAc,IACjB,CAAC,EAAE,GAAG,EAAE;AACxB;AAEA,MAAM,eAAe,OAAgB,WAA6B;CAChE,IAAI,UAAU,KAAK,GAAG,OAAO,eAAe,QAAQ,MAAM,IAAI;CAC9D,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MACJ,KAAK,UAAU,YAAY,OAAO,MAAM,CAAC,CAAC,CAC1C,QAAQ,UAAU,UAAU,KAAA,CAAS;CAE1C,IAAI,CAAC,cAAc,KAAK,GAAG,OAAO;CAClC,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAAG;EAChD,MAAM,WAAW,YAAY,OAAO,MAAM;EAC1C,IAAI,aAAa,KAAA,GAAW,OAAO,OAAO;CAC5C;CACA,OAAO;AACT;AAEA,MAAM,gBACJ,OACA,SACY;CACZ,KAAK,MAAM,OAAO,MAChB,IAAI,MAAM,SAAS,KAAA,GAAW,OAAO,MAAM;AAG/C;AAEA,MAAM,cACJ,OACA,SACuB;CACvB,MAAM,QAAQ,aAAa,OAAO,IAAI;CACtC,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;AAEA,MAAM,mBAAmB,YACtB,QACC,oBACG;AAYP,MAAM,eAAe,YAAwC;CAC3D,IAAI,QAAQ,gBAAgB,aAAa;EACvC,IAAI,CAAC,QAAQ,cAAc;GACzB,QAAQ,SAAS,KAAK,uBAAuB,YAAY,cAAc;GACvE,QAAQ,eAAe;EACzB;EACA,OAAO;CACT;CACA,QAAQ;CACR,OAAO;AACT;AAEA,MAAM,cACJ,MACA,YACA,SACA,OACA,YACgB;CAChB,MAAM,WAAW,KAAK;CACtB,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG,OAAO,CAAC;CACtC,MAAM,SAAsB,CAAC;CAC7B,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,OAAO,YAAY,UAAU;GAC/B,QAAQ,SAAS,KACf,cAAc,OAAO,KAAK,SAAS,EAAE,EAAE,mCACzC;GACA;EACF;EACA,MAAM,QAAQ,iBACZ,SACA,YACA,SACA,QAAQ,GACR,OACF;EACA,IAAI,OAAO,OAAO,KAAK,KAAK;CAC9B;CACA,OAAO;AACT;AAEA,MAAM,gBACJ,MACA,OACA,YACA,YACqC;CACrC,MAAM,SAAS,MAAM;CACrB,MAAM,aACJ,OAAO,WAAW,WACd,SACA,SAAS,MAAM,KAAK,OAAO,OAAO,YAAY,WAC5C,OAAO,UACP,KAAA;CACR,IAAI,CAAC,YAAY,OAAO,KAAA;CACxB,MAAM,aAAa,SAAS,MAAM,IAAI,OAAO,aAAa,KAAA;CAC1D,MAAM,kBACJ,eAAe,KAAA,IAAY,KAAA,IAAY,YAAY,YAAY,UAAU;CAC3E,OAAO;EACL,MAAM;EACN,MAAM;EACN,WAAW,QAAQ;EACnB,mBAAmB,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;EACjE,GAAI,oBAAoB,KAAA,IAAY,EAAE,SAAS,gBAAgB,IAAI,CAAC;CACtE;AACF;AAEA,MAAM,eACJ,MACA,OACA,YACA,YAC0B;CAC1B,MAAM,YAAY,KAAK;CAEvB,IAAI,cAAc,QAAQ;EACxB,MAAM,OAAO,WAAW,OAAO,CAAC,QAAQ,OAAO,CAAC;EAChD,MAAM,UAAU,MAAM;EACtB,IAAI,OAAO,YAAY,YAAY,WAAW,KAAK,OAAO,GACxD,OAAO;GACL,OAAO;GACP,GAAI,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,CAAC;EACvC;EAEF,IAAI,YAAY,WACd,OAAO;GACL,OAAO;GACP,GAAI,SAAS,KAAA,IAAY,EAAE,OAAO,KAAK,IAAI,CAAC;EAC9C;EAEF,OAAO;GACL,OAAO;GACP,GAAI,SAAS,KAAA,IAAY,EAAE,OAAO,KAAK,IAAI,CAAC;EAC9C;CACF;CAEA,IAAI,cAAc,SAAS;EACzB,MAAM,MAAM,WAAW,OAAO,CAAC,OAAO,KAAK,CAAC;EAC5C,MAAM,MAAM,WAAW,OAAO,CAAC,OAAO,SAAS,CAAC;EAChD,MAAM,OAAO,MAAM;EACnB,MAAM,QAAQ,MAAM;EACpB,OAAO;GACL,OAAO;GACP,GAAI,QAAQ,KAAA,IAAY,EAAE,IAAI,IAAI,CAAC;GACnC,GAAI,QAAQ,KAAA,IAAY,EAAE,IAAI,IAAI,CAAC;GACnC,GAAI,OAAO,SAAS,YAAY,OAAO,SAAS,WAAW,EAAE,KAAK,IAAI,CAAC;GACvE,GAAI,OAAO,UAAU,YAAY,EAAE,MAAM,IAAI,CAAC;EAChD;CACF;CAEA,IAAI,cAAc,OAAO;EACvB,MAAM,MAAM,MAAM;EAClB,MAAM,QAAQ,aAAa,OAAO,CAAC,SAAS,WAAW,CAAC;EACxD,MAAM,UAAU,aAAa,OAAO,CAAC,WAAW,cAAc,CAAC;EAC/D,OAAO;GACL,OAAO;GACP,GAAI,OAAO,QAAQ,WAAW,EAAE,IAAI,IAAI,CAAC;GACzC,GAAI,OAAO,UAAU,WAAW,EAAE,MAAM,IAAI,CAAC;GAC7C,GAAI,OAAO,YAAY,WAAW,EAAE,QAAQ,IAAI,CAAC;EACnD;CACF;CAEA,IAAI,cAAc,UAAU;EAC1B,MAAM,MAAM,MAAM;EAClB,MAAM,QAAQ,aAAa,OAAO,CAAC,SAAS,WAAW,CAAC;EACxD,OAAO;GACL,OAAO;GACP,GAAI,OAAO,QAAQ,WAAW,EAAE,IAAI,IAAI,CAAC;GACzC,GAAI,OAAO,UAAU,WAAW,EAAE,MAAM,IAAI,CAAC;EAC/C;CACF;CAEA,IAAI,cAAc,QAAQ;EACxB,MAAM,QAAQ,MAAM;EACpB,MAAM,UAAU,MAAM;EACtB,MAAM,aAAa,MAAM;EACzB,OAAO;GACL,OAAO;GACP,GAAI,OAAO,UAAU,WAAW,EAAE,MAAM,IAAI,CAAC;GAC7C,GAAI,OAAO,YAAY,WAAW,EAAE,QAAQ,IAAI,CAAC;GACjD,GAAI,OAAO,eAAe,WAAW,EAAE,WAAW,IAAI,CAAC;EACzD;CACF;CAEA,IAAI,cAAc,WAChB,OAAO;EACL,OAAO;EACP,GAAI,OAAO,MAAM,aAAa,YAAY,EAAE,OAAO,MAAM,SAAS,IAAI,CAAC;CACzE;CAGF,IAAI,cAAc,UAAU;EAC1B,MAAM,QAAQ,WAAW,OAAO,CAAC,SAAS,MAAM,CAAC;EACjD,MAAM,cAAc,MAAM;EAC1B,MAAM,QAAQ,MAAM;EACpB,MAAM,SAAS,MAAM;EACrB,MAAM,SAAS,aAAa,MAAM,OAAO,YAAY,OAAO;EAC5D,OAAO;GACL,OAAO;GACP,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;GACvC,GAAI,OAAO,gBAAgB,WAAW,EAAE,YAAY,IAAI,CAAC;GACzD,GAAI,OAAO,UAAU,YAAY,EAAE,MAAM,IAAI,CAAC;GAC9C,GAAI,OAAO,WAAW,YAAY,EAAE,OAAO,IAAI,CAAC;GAChD,GAAI,SAAS,EAAE,SAAS,OAAO,IAAI,CAAC;EACtC;CACF;CAEA,MAAM,UAAU,aAAa,MAAM;EAAC;EAAQ;EAAS;CAAS,CAAC;CAC/D,MAAM,OAAO,mBAAmB,YAAY,OAAO,CAAC;CACpD,MAAM,QAAQ,WAAW,OAAO,CAAC,OAAO,CAAC;CAEzC,IAAI,cAAc,aAAa;EAC7B,MAAM,cAAc,WAAW,OAAO,CAAC,aAAa,CAAC;EACrD,MAAM,YACJ,OAAO,MAAM,iBAAiB,YAC1B,MAAM,eACN,MAAM,qBAAqB,aACzB,OACA,KAAA;EACR,OAAO;GACL,OAAO;GACP,GAAI,gBAAgB,KAAA,IAAY,EAAE,YAAY,IAAI,CAAC;GACnD,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;GAC/C,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;GACvC,GAAI,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,CAAC;EACvC;CACF;CAEA,IAAI,cAAc,YAAY;EAC5B,MAAM,iBAAiB,aAAa,OAAO,CAAC,kBAAkB,OAAO,CAAC;EACtE,OAAO;GACL,OAAO;GACP,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;GACvC,GAAI,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,CAAC;GACrC,GAAI,OAAO,mBAAmB,YAAY,EAAE,eAAe,IAAI,CAAC;EAClE;CACF;AAGF;AAEA,MAAM,mBACJ,MACA,kBACA,YACA,SACA,OACA,YACqB;CACrB,IAAI,CAAC,YAAY,OAAO,GAAG,OAAO;CAClC,MAAM,OAAO,eAAe,YAAY,iBAAiB,SAAS,IAAI;CACtE,IAAI,CAAC,MAAM,QAAQ,IAAI,GAAG;EACxB,QAAQ,SAAS,KACf,0BAA0B,OAAO,KAAK,SAAS,EAAE,EAAE,6BACrD;EACA,OAAO;GAAE,OAAO;GAAY,UAAU,CAAC;EAAE;CAC3C;CACA,MAAM,YAAY,KAAK,IAAI,KAAK,QAAQ,iBAAiB;CACzD,IAAI,KAAK,SAAS,qBAAqB,CAAC,QAAQ,mBAAmB;EACjE,QAAQ,SAAS,KACf,yCAAyC,kBAAkB,QAC7D;EACA,QAAQ,oBAAoB;CAC9B;CACA,MAAM,WAAwB,CAAC;CAC/B,KAAK,IAAI,QAAQ,GAAG,QAAQ,WAAW,SAAS;EAC9C,IAAI,CAAC,YAAY,OAAO,GAAG;EAC3B,MAAM,QAAQ,iBACZ,iBAAiB,SAAS,aAC1B,KAAK,QACL,SACA,QAAQ,GACR,OACF;EACA,SAAS,KAAK;GACZ,OAAO;GACP,GAAI,QAAQ,EAAE,UAAU,MAAM,IAAI,CAAC;EACrC,CAAC;CACH;CACA,OAAO;EAAE,OAAO;EAAY;CAAS;AACvC;AAEA,SAAS,iBACP,aACA,YACA,SACA,OACA,SACkB;CAClB,IAAI,SAAS,WAAW;EACtB,IAAI,CAAC,QAAQ,aAAa;GACxB,QAAQ,SAAS,KAAK,qBAAqB,UAAU,cAAc;GACnE,QAAQ,cAAc;EACxB;EACA,OAAO;CACT;CACA,IAAI,QAAQ,IAAI,WAAW,GAAG;EAC5B,QAAQ,SAAS,KAAK,qCAAqC,YAAY,GAAG;EAC1E,OAAO;CACT;CACA,MAAM,OAAO,QAAQ,QAAQ,WAAW,IAAI,WAAW;CACvD,IAAI,CAAC,MAAM;EACT,QAAQ,SAAS,KAAK,mBAAmB,YAAY,iBAAiB;EACtE,OAAO;CACT;CACA,MAAM,YAAY,KAAK;CAEvB,QAAQ,IAAI,WAAW;CACvB,IAAI;EACF,MAAM,mBAAmB,KAAK;EAC9B,IAAI,mBAAmB,gBAAgB,GACrC,OAAO,gBACL,MACA,kBACA,YACA,SACA,OACA,OACF;EAEF,IAAI,OAAO,cAAc,YAAY,CAAC,qBAAqB,IAAI,SAAS,GAAG;GACzE,QAAQ,SAAS,KACf,2BAA2B,OAAO,aAAa,EAAE,EAAE,eACrD;GACA,OAAO;EACT;EACA,IAAI,CAAC,YAAY,OAAO,GAAG,OAAO;EAClC,MAAM,QAAiC,CAAC;EACxC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAAG;GAC/C,IAAI,QAAQ,QAAQ,QAAQ,eAAe,QAAQ,YAAY;GAC/D,MAAM,WAAW,YAAY,OAAO,UAAU;GAC9C,IAAI,aAAa,KAAA,GAAW,MAAM,OAAO;EAC3C;EACA,MAAM,SAAS,YAAY,MAAM,OAAO,YAAY,OAAO;EAC3D,IAAI,CAAC,QAAQ,OAAO;EACpB,MAAM,WAAW,WAAW,MAAM,YAAY,SAAS,OAAO,OAAO;EACrE,OAAO;GACL,GAAG;GACH,GAAI,SAAS,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;EAC5C;CACF,UAAU;EACR,QAAQ,OAAO,WAAW;CAC5B;AACF;AAEA,SAAgB,uBAAuB,SAGrC;CACA,MAAM,WAAqB,CAAC;CAC5B,IAAI,CAAC,QAAQ,WAAW,IAAI,MAAM,GAChC,OAAO;EACL,MAAM;EACN,UAAU,CAAC,6CAA2C;CACxD;CAEF,MAAM,UAA6B;EACjC;EACA,WAAW,gBAAgB,OAAO;EAClC;EACA,cAAc;EACd,aAAa;EACb,cAAc;EACd,mBAAmB;CACrB;CACA,IAAI;EAQF,OAAO;GAAE,MAPI,iBACX,QACA,QAAQ,WACR,SACA,mBACA,IAAI,IAAI,CAEE;GAAG;EAAS;CAC1B,QAAQ;EACN,SAAS,KAAK,sDAAsD;EACpE,OAAO;GAAE,MAAM;GAAM;EAAS;CAChC;AACF"}
@@ -0,0 +1,6 @@
1
+ import { A2uiOperationResult, A2uiState } from "./types.js";
2
+ //#region src/a2ui/reducer.d.ts
3
+ declare function applyA2uiOperations(state: A2uiState, operations: unknown): A2uiOperationResult;
4
+ //#endregion
5
+ export { applyA2uiOperations };
6
+ //# sourceMappingURL=reducer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reducer.d.ts","names":[],"sources":["../../src/a2ui/reducer.ts"],"mappings":";;iBA4JgB,oBACd,OAAO,WACP,sBACC"}
@@ -0,0 +1,181 @@
1
+ import { A2UI_SURFACE_ID } from "./types.js";
2
+ //#region src/a2ui/reducer.ts
3
+ const OPERATION_KEYS = /* @__PURE__ */ new Set([
4
+ "createSurface",
5
+ "updateComponents",
6
+ "updateDataModel",
7
+ "deleteSurface"
8
+ ]);
9
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10
+ const isVersion = (value) => value === "v0.9" || value === "v1.0";
11
+ const surfaceIdOf = (payload) => {
12
+ const surfaceId = payload["surfaceId"];
13
+ return typeof surfaceId === "string" && surfaceId.length > 0 ? surfaceId : void 0;
14
+ };
15
+ const withSurfaceId = (surface, surfaceId) => {
16
+ Object.defineProperty(surface, A2UI_SURFACE_ID, {
17
+ value: surfaceId,
18
+ enumerable: false
19
+ });
20
+ return surface;
21
+ };
22
+ const getSurfaceId = (surface) => surface[A2UI_SURFACE_ID];
23
+ const cloneSurface = (surface, surfaceId) => withSurfaceId({
24
+ components: new Map(surface.components),
25
+ dataModel: surface.dataModel
26
+ }, getSurfaceId(surface) ?? surfaceId);
27
+ const upsertComponents = (surface, components, warnings, operationIndex) => {
28
+ if (!Array.isArray(components)) {
29
+ warnings.push(`Operation at index ${operationIndex} has malformed components.`);
30
+ return;
31
+ }
32
+ for (let componentIndex = 0; componentIndex < components.length; componentIndex++) {
33
+ const component = components[componentIndex];
34
+ if (!isRecord(component) || typeof component["id"] !== "string" || component["id"].length === 0 || typeof component["component"] !== "string" || component["component"].length === 0) {
35
+ warnings.push(`Component at index ${componentIndex} in operation ${operationIndex} is malformed.`);
36
+ continue;
37
+ }
38
+ surface.components.set(component["id"], { ...component });
39
+ }
40
+ };
41
+ const decodePointer = (path) => {
42
+ if (path === "" || path === "/") return [];
43
+ if (!path.startsWith("/")) return void 0;
44
+ return path.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
45
+ };
46
+ const isArrayIndex = (segment) => segment === "0" || /^[1-9]\d*$/.test(segment);
47
+ const setAtPointer = (model, path, value) => {
48
+ const segments = decodePointer(path);
49
+ if (!segments) return {
50
+ ok: false,
51
+ value: model
52
+ };
53
+ if (segments.length === 0) return {
54
+ ok: true,
55
+ value
56
+ };
57
+ const update = (current, index) => {
58
+ const segment = segments[index];
59
+ const isLast = index === segments.length - 1;
60
+ if (Array.isArray(current)) {
61
+ if (segment !== "-" && !isArrayIndex(segment)) return current;
62
+ const clone = current.slice();
63
+ const targetIndex = segment === "-" ? clone.length : Number(segment);
64
+ clone[targetIndex] = isLast ? value : update(clone[targetIndex], index + 1);
65
+ return clone;
66
+ }
67
+ const clone = isRecord(current) ? { ...current } : {};
68
+ const child = clone[segment];
69
+ clone[segment] = isLast ? value : update(child ?? (isArrayIndex(segments[index + 1] ?? "") || segments[index + 1] === "-" ? [] : {}), index + 1);
70
+ return clone;
71
+ };
72
+ return {
73
+ ok: true,
74
+ value: update(model, 0)
75
+ };
76
+ };
77
+ const dataModelValue = (payload) => {
78
+ for (const key of [
79
+ "contents",
80
+ "value",
81
+ "data"
82
+ ]) if (payload[key] !== void 0) return {
83
+ found: true,
84
+ value: payload[key]
85
+ };
86
+ return {
87
+ found: false,
88
+ value: void 0
89
+ };
90
+ };
91
+ function applyA2uiOperations(state, operations) {
92
+ const nextState = new Map(state);
93
+ const warnings = [];
94
+ if (!Array.isArray(operations)) return {
95
+ state: nextState,
96
+ warnings: ["A2UI operations must be an array."]
97
+ };
98
+ for (let index = 0; index < operations.length; index++) try {
99
+ const entry = operations[index];
100
+ if (!isRecord(entry)) {
101
+ warnings.push(`Operation at index ${index} is malformed.`);
102
+ continue;
103
+ }
104
+ const version = entry["version"];
105
+ if (!isVersion(version)) {
106
+ warnings.push(`Operation at index ${index} has an unsupported version.`);
107
+ continue;
108
+ }
109
+ const operationKeys = Object.keys(entry).filter((key) => key !== "version");
110
+ if (operationKeys.length !== 1) {
111
+ warnings.push(`Operation at index ${index} must contain exactly one operation key.`);
112
+ continue;
113
+ }
114
+ const operationKey = operationKeys[0];
115
+ if (!OPERATION_KEYS.has(operationKey)) {
116
+ warnings.push(`Unknown A2UI operation "${operationKey}" at index ${index}.`);
117
+ continue;
118
+ }
119
+ const payload = entry[operationKey];
120
+ if (!isRecord(payload)) {
121
+ warnings.push(`Operation at index ${index} has a malformed payload.`);
122
+ continue;
123
+ }
124
+ const surfaceId = surfaceIdOf(payload);
125
+ if (!surfaceId) {
126
+ warnings.push(`Operation at index ${index} has no valid surfaceId.`);
127
+ continue;
128
+ }
129
+ if (operationKey === "createSurface") {
130
+ const surface = withSurfaceId({
131
+ components: /* @__PURE__ */ new Map(),
132
+ dataModel: version === "v1.0" && payload["dataModel"] !== void 0 ? payload["dataModel"] : void 0
133
+ }, surfaceId);
134
+ if (version === "v1.0" && payload["components"] !== void 0) upsertComponents(surface, payload["components"], warnings, index);
135
+ nextState.set(surfaceId, surface);
136
+ continue;
137
+ }
138
+ if (operationKey === "deleteSurface") {
139
+ nextState.delete(surfaceId);
140
+ continue;
141
+ }
142
+ const currentSurface = nextState.get(surfaceId);
143
+ if (!currentSurface) {
144
+ warnings.push(`Operation at index ${index} references missing surface "${surfaceId}".`);
145
+ continue;
146
+ }
147
+ const surface = cloneSurface(currentSurface, surfaceId);
148
+ if (operationKey === "updateComponents") {
149
+ upsertComponents(surface, payload["components"], warnings, index);
150
+ nextState.set(surfaceId, surface);
151
+ continue;
152
+ }
153
+ const update = dataModelValue(payload);
154
+ if (!update.found) {
155
+ warnings.push(`Operation at index ${index} has no data model value.`);
156
+ continue;
157
+ }
158
+ const path = payload["path"] ?? "/";
159
+ if (typeof path !== "string") {
160
+ warnings.push(`Operation at index ${index} has a malformed data model path.`);
161
+ continue;
162
+ }
163
+ const result = setAtPointer(surface.dataModel, path, update.value);
164
+ if (!result.ok) {
165
+ warnings.push(`Operation at index ${index} has an invalid JSON Pointer path.`);
166
+ continue;
167
+ }
168
+ surface.dataModel = result.value;
169
+ nextState.set(surfaceId, surface);
170
+ } catch {
171
+ warnings.push(`Operation at index ${index} is malformed.`);
172
+ }
173
+ return {
174
+ state: nextState,
175
+ warnings
176
+ };
177
+ }
178
+ //#endregion
179
+ export { applyA2uiOperations };
180
+
181
+ //# sourceMappingURL=reducer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reducer.js","names":[],"sources":["../../src/a2ui/reducer.ts"],"sourcesContent":["import {\n A2UI_SURFACE_ID,\n type A2uiOperationResult,\n type A2uiState,\n type A2uiSurfaceState,\n} from \"./types\";\n\nconst OPERATION_KEYS = new Set([\n \"createSurface\",\n \"updateComponents\",\n \"updateDataModel\",\n \"deleteSurface\",\n]);\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isVersion = (value: unknown): value is \"v0.9\" | \"v1.0\" =>\n value === \"v0.9\" || value === \"v1.0\";\n\nconst surfaceIdOf = (payload: Record<string, unknown>): string | undefined => {\n const surfaceId = payload[\"surfaceId\"];\n return typeof surfaceId === \"string\" && surfaceId.length > 0\n ? surfaceId\n : undefined;\n};\n\nconst withSurfaceId = (\n surface: A2uiSurfaceState,\n surfaceId: string,\n): A2uiSurfaceState => {\n Object.defineProperty(surface, A2UI_SURFACE_ID, {\n value: surfaceId,\n enumerable: false,\n });\n return surface;\n};\n\nconst getSurfaceId = (surface: A2uiSurfaceState): string | undefined =>\n (surface as A2uiSurfaceState & { [A2UI_SURFACE_ID]?: string })[\n A2UI_SURFACE_ID\n ];\n\nconst cloneSurface = (\n surface: A2uiSurfaceState,\n surfaceId: string,\n): A2uiSurfaceState =>\n withSurfaceId(\n {\n components: new Map(surface.components),\n dataModel: surface.dataModel,\n },\n getSurfaceId(surface) ?? surfaceId,\n );\n\nconst upsertComponents = (\n surface: A2uiSurfaceState,\n components: unknown,\n warnings: string[],\n operationIndex: number,\n): void => {\n if (!Array.isArray(components)) {\n warnings.push(\n `Operation at index ${operationIndex} has malformed components.`,\n );\n return;\n }\n\n for (\n let componentIndex = 0;\n componentIndex < components.length;\n componentIndex++\n ) {\n const component = components[componentIndex];\n if (\n !isRecord(component) ||\n typeof component[\"id\"] !== \"string\" ||\n component[\"id\"].length === 0 ||\n typeof component[\"component\"] !== \"string\" ||\n component[\"component\"].length === 0\n ) {\n warnings.push(\n `Component at index ${componentIndex} in operation ${operationIndex} is malformed.`,\n );\n continue;\n }\n surface.components.set(component[\"id\"], { ...component });\n }\n};\n\nconst decodePointer = (path: string): string[] | undefined => {\n if (path === \"\" || path === \"/\") return [];\n if (!path.startsWith(\"/\")) return undefined;\n return path\n .slice(1)\n .split(\"/\")\n .map((segment) => segment.replaceAll(\"~1\", \"/\").replaceAll(\"~0\", \"~\"));\n};\n\nconst isArrayIndex = (segment: string): boolean =>\n segment === \"0\" || /^[1-9]\\d*$/.test(segment);\n\nconst setAtPointer = (\n model: unknown,\n path: string,\n value: unknown,\n): { readonly ok: boolean; readonly value: unknown } => {\n const segments = decodePointer(path);\n if (!segments) return { ok: false, value: model };\n if (segments.length === 0) return { ok: true, value };\n\n const update = (current: unknown, index: number): unknown => {\n const segment = segments[index]!;\n const isLast = index === segments.length - 1;\n\n if (Array.isArray(current)) {\n if (segment !== \"-\" && !isArrayIndex(segment)) return current;\n const clone = current.slice();\n const targetIndex = segment === \"-\" ? clone.length : Number(segment);\n clone[targetIndex] = isLast\n ? value\n : update(clone[targetIndex], index + 1);\n return clone;\n }\n\n const clone: Record<string, unknown> = isRecord(current)\n ? { ...current }\n : {};\n const child = clone[segment];\n clone[segment] = isLast\n ? value\n : update(\n child ??\n (isArrayIndex(segments[index + 1] ?? \"\") ||\n segments[index + 1] === \"-\"\n ? []\n : {}),\n index + 1,\n );\n return clone;\n };\n\n return { ok: true, value: update(model, 0) };\n};\n\nconst dataModelValue = (\n payload: Record<string, unknown>,\n): { readonly found: boolean; readonly value: unknown } => {\n for (const key of [\"contents\", \"value\", \"data\"] as const) {\n if (payload[key] !== undefined) {\n return { found: true, value: payload[key] };\n }\n }\n return { found: false, value: undefined };\n};\n\nexport function applyA2uiOperations(\n state: A2uiState,\n operations: unknown,\n): A2uiOperationResult {\n const nextState = new Map(state);\n const warnings: string[] = [];\n\n if (!Array.isArray(operations)) {\n return {\n state: nextState,\n warnings: [\"A2UI operations must be an array.\"],\n };\n }\n\n for (let index = 0; index < operations.length; index++) {\n try {\n const entry = operations[index];\n if (!isRecord(entry)) {\n warnings.push(`Operation at index ${index} is malformed.`);\n continue;\n }\n\n const version = entry[\"version\"];\n if (!isVersion(version)) {\n warnings.push(\n `Operation at index ${index} has an unsupported version.`,\n );\n continue;\n }\n\n const operationKeys = Object.keys(entry).filter(\n (key) => key !== \"version\",\n );\n if (operationKeys.length !== 1) {\n warnings.push(\n `Operation at index ${index} must contain exactly one operation key.`,\n );\n continue;\n }\n\n const operationKey = operationKeys[0]!;\n if (!OPERATION_KEYS.has(operationKey)) {\n warnings.push(\n `Unknown A2UI operation \"${operationKey}\" at index ${index}.`,\n );\n continue;\n }\n\n const payload = entry[operationKey];\n if (!isRecord(payload)) {\n warnings.push(`Operation at index ${index} has a malformed payload.`);\n continue;\n }\n\n const surfaceId = surfaceIdOf(payload);\n if (!surfaceId) {\n warnings.push(`Operation at index ${index} has no valid surfaceId.`);\n continue;\n }\n\n if (operationKey === \"createSurface\") {\n const surface = withSurfaceId(\n {\n components: new Map(),\n dataModel:\n version === \"v1.0\" && payload[\"dataModel\"] !== undefined\n ? payload[\"dataModel\"]\n : undefined,\n },\n surfaceId,\n );\n if (version === \"v1.0\" && payload[\"components\"] !== undefined) {\n upsertComponents(surface, payload[\"components\"], warnings, index);\n }\n nextState.set(surfaceId, surface);\n continue;\n }\n\n if (operationKey === \"deleteSurface\") {\n nextState.delete(surfaceId);\n continue;\n }\n\n const currentSurface = nextState.get(surfaceId);\n if (!currentSurface) {\n warnings.push(\n `Operation at index ${index} references missing surface \"${surfaceId}\".`,\n );\n continue;\n }\n const surface = cloneSurface(currentSurface, surfaceId);\n\n if (operationKey === \"updateComponents\") {\n upsertComponents(surface, payload[\"components\"], warnings, index);\n nextState.set(surfaceId, surface);\n continue;\n }\n\n const update = dataModelValue(payload);\n if (!update.found) {\n warnings.push(`Operation at index ${index} has no data model value.`);\n continue;\n }\n const path = payload[\"path\"] ?? \"/\";\n if (typeof path !== \"string\") {\n warnings.push(\n `Operation at index ${index} has a malformed data model path.`,\n );\n continue;\n }\n const result = setAtPointer(surface.dataModel, path, update.value);\n if (!result.ok) {\n warnings.push(\n `Operation at index ${index} has an invalid JSON Pointer path.`,\n );\n continue;\n }\n surface.dataModel = result.value;\n nextState.set(surfaceId, surface);\n } catch {\n warnings.push(`Operation at index ${index} is malformed.`);\n }\n }\n\n return { state: nextState, warnings };\n}\n"],"mappings":"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"}