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