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