@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
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
const MAX_TEMPLATE_LENGTH = 10_000;
|
|
2
|
-
const MAX_TEMPLATE_PARTS = 1_000;
|
|
3
|
-
const MAX_EXPRESSION_DEPTH = 32;
|
|
4
|
-
|
|
5
|
-
export type ExpressionPart =
|
|
6
|
-
| string
|
|
7
|
-
| number
|
|
8
|
-
| boolean
|
|
9
|
-
| null
|
|
10
|
-
| { readonly path: string }
|
|
11
|
-
| { readonly call: string; readonly args: Record<string, ExpressionPart> };
|
|
12
|
-
|
|
13
|
-
export type ValueFunctionContext = {
|
|
14
|
-
readonly resolve: (value: ExpressionPart) => unknown;
|
|
15
|
-
readonly warn: (message: string) => void;
|
|
16
|
-
readonly templates: Map<string, ExpressionPart[] | null>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export class ExpressionSyntaxError extends Error {}
|
|
20
|
-
|
|
21
|
-
const FUNCTION_NAME = /^[A-Za-z_@]\w*$/;
|
|
22
|
-
|
|
23
|
-
const isSpace = (character: string) => /\s/.test(character);
|
|
24
|
-
|
|
25
|
-
const isPathCharacter = (character: string) =>
|
|
26
|
-
!isSpace(character) && !"{}(),:'\"$".includes(character);
|
|
27
|
-
|
|
28
|
-
const isDigit = (character: string | undefined) =>
|
|
29
|
-
character !== undefined && character >= "0" && character <= "9";
|
|
30
|
-
|
|
31
|
-
class ExpressionReader {
|
|
32
|
-
private readonly text: string;
|
|
33
|
-
position: number;
|
|
34
|
-
|
|
35
|
-
constructor(text: string, position: number) {
|
|
36
|
-
this.text = text;
|
|
37
|
-
this.position = position;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
expression(depth: number): ExpressionPart {
|
|
41
|
-
if (depth > MAX_EXPRESSION_DEPTH) this.fail("it nests too deeply");
|
|
42
|
-
this.skipSpace();
|
|
43
|
-
const character = this.text[this.position];
|
|
44
|
-
if (this.text.startsWith("${", this.position)) {
|
|
45
|
-
this.position += 2;
|
|
46
|
-
this.skipSpace();
|
|
47
|
-
if (this.text[this.position] === "}") {
|
|
48
|
-
this.position++;
|
|
49
|
-
return "";
|
|
50
|
-
}
|
|
51
|
-
const value = this.expression(depth + 1);
|
|
52
|
-
this.skipSpace();
|
|
53
|
-
if (this.text[this.position] !== "}") {
|
|
54
|
-
this.fail(
|
|
55
|
-
this.position >= this.text.length
|
|
56
|
-
? "an interpolation is not closed"
|
|
57
|
-
: "characters follow a complete expression",
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
this.position++;
|
|
61
|
-
return value;
|
|
62
|
-
}
|
|
63
|
-
if (character === "'" || character === '"') return this.string(character);
|
|
64
|
-
if (
|
|
65
|
-
isDigit(character) ||
|
|
66
|
-
(character === "-" && isDigit(this.text[this.position + 1]))
|
|
67
|
-
) {
|
|
68
|
-
return this.number();
|
|
69
|
-
}
|
|
70
|
-
const word = this.word();
|
|
71
|
-
if (word === "") this.fail("an expression is missing");
|
|
72
|
-
const end = this.position;
|
|
73
|
-
this.skipSpace();
|
|
74
|
-
if (this.text[this.position] === "(") {
|
|
75
|
-
if (!FUNCTION_NAME.test(word)) this.fail(`"${word}" is not a function`);
|
|
76
|
-
this.position++;
|
|
77
|
-
return { call: word, args: this.arguments(depth) };
|
|
78
|
-
}
|
|
79
|
-
this.position = end;
|
|
80
|
-
if (word === "true") return true;
|
|
81
|
-
if (word === "false") return false;
|
|
82
|
-
if (word === "null") return null;
|
|
83
|
-
return { path: word };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private arguments(depth: number): Record<string, ExpressionPart> {
|
|
87
|
-
const args: Record<string, ExpressionPart> = {};
|
|
88
|
-
this.skipSpace();
|
|
89
|
-
if (this.text[this.position] === ")") {
|
|
90
|
-
this.position++;
|
|
91
|
-
return args;
|
|
92
|
-
}
|
|
93
|
-
for (;;) {
|
|
94
|
-
this.skipSpace();
|
|
95
|
-
const name = this.word();
|
|
96
|
-
if (!FUNCTION_NAME.test(name)) this.fail("an argument name is missing");
|
|
97
|
-
this.skipSpace();
|
|
98
|
-
if (this.text[this.position] !== ":") {
|
|
99
|
-
this.fail(`argument "${name}" has no colon`);
|
|
100
|
-
}
|
|
101
|
-
this.position++;
|
|
102
|
-
Object.defineProperty(args, name, {
|
|
103
|
-
value: this.expression(depth + 1),
|
|
104
|
-
enumerable: true,
|
|
105
|
-
configurable: true,
|
|
106
|
-
writable: true,
|
|
107
|
-
});
|
|
108
|
-
this.skipSpace();
|
|
109
|
-
const separator = this.text[this.position++];
|
|
110
|
-
if (separator === ")") return args;
|
|
111
|
-
if (separator !== ",") {
|
|
112
|
-
this.fail(
|
|
113
|
-
separator === undefined
|
|
114
|
-
? "a function call is not closed"
|
|
115
|
-
: "arguments are not separated by commas",
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
this.skipSpace();
|
|
119
|
-
if (this.text[this.position] === ")") {
|
|
120
|
-
this.position++;
|
|
121
|
-
return args;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private string(quote: string): string {
|
|
127
|
-
let value = "";
|
|
128
|
-
this.position++;
|
|
129
|
-
for (;;) {
|
|
130
|
-
const character = this.text[this.position++];
|
|
131
|
-
if (character === undefined) this.fail("a string is not closed");
|
|
132
|
-
if (character === quote) return value;
|
|
133
|
-
if (character !== "\\") {
|
|
134
|
-
value += character;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const escaped = this.text[this.position++];
|
|
138
|
-
if (escaped === undefined) this.fail("a string is not closed");
|
|
139
|
-
value +=
|
|
140
|
-
escaped === "n"
|
|
141
|
-
? "\n"
|
|
142
|
-
: escaped === "t"
|
|
143
|
-
? "\t"
|
|
144
|
-
: escaped === "r"
|
|
145
|
-
? "\r"
|
|
146
|
-
: escaped;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private number(): number {
|
|
151
|
-
const start = this.position;
|
|
152
|
-
if (this.text[this.position] === "-") this.position++;
|
|
153
|
-
while (isDigit(this.text[this.position])) this.position++;
|
|
154
|
-
if (this.text[this.position] === ".") {
|
|
155
|
-
this.position++;
|
|
156
|
-
while (isDigit(this.text[this.position])) this.position++;
|
|
157
|
-
}
|
|
158
|
-
const next = this.text[this.position];
|
|
159
|
-
if (next !== undefined && isPathCharacter(next)) {
|
|
160
|
-
this.fail("a number is malformed");
|
|
161
|
-
}
|
|
162
|
-
return Number(this.text.slice(start, this.position));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private word(): string {
|
|
166
|
-
const start = this.position;
|
|
167
|
-
while (
|
|
168
|
-
this.position < this.text.length &&
|
|
169
|
-
isPathCharacter(this.text[this.position]!)
|
|
170
|
-
) {
|
|
171
|
-
this.position++;
|
|
172
|
-
}
|
|
173
|
-
return this.text.slice(start, this.position);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private skipSpace() {
|
|
177
|
-
while (
|
|
178
|
-
this.position < this.text.length &&
|
|
179
|
-
isSpace(this.text[this.position]!)
|
|
180
|
-
) {
|
|
181
|
-
this.position++;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private fail(reason: string): never {
|
|
186
|
-
throw new ExpressionSyntaxError(reason);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export const parseExpressionTemplate = (template: string): ExpressionPart[] => {
|
|
191
|
-
if (template.length > MAX_TEMPLATE_LENGTH) {
|
|
192
|
-
throw new ExpressionSyntaxError(
|
|
193
|
-
`it is longer than ${MAX_TEMPLATE_LENGTH} characters`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
const parts: ExpressionPart[] = [];
|
|
197
|
-
const push = (part: ExpressionPart) => {
|
|
198
|
-
if (parts.length >= MAX_TEMPLATE_PARTS) {
|
|
199
|
-
throw new ExpressionSyntaxError(
|
|
200
|
-
`it has more than ${MAX_TEMPLATE_PARTS} parts`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
parts.push(part);
|
|
204
|
-
};
|
|
205
|
-
let literal = "";
|
|
206
|
-
let index = 0;
|
|
207
|
-
while (index < template.length) {
|
|
208
|
-
if (template.startsWith("\\${", index)) {
|
|
209
|
-
literal += "${";
|
|
210
|
-
index += 3;
|
|
211
|
-
} else if (template.startsWith("${", index)) {
|
|
212
|
-
if (literal) push(literal);
|
|
213
|
-
literal = "";
|
|
214
|
-
const reader = new ExpressionReader(template, index);
|
|
215
|
-
push(reader.expression(0));
|
|
216
|
-
index = reader.position;
|
|
217
|
-
} else {
|
|
218
|
-
literal += template[index];
|
|
219
|
-
index++;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (literal) push(literal);
|
|
223
|
-
return parts;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const isMissing = (value: unknown) =>
|
|
227
|
-
value === undefined || value === null || value === "";
|
|
228
|
-
|
|
229
|
-
const toNumber = (value: unknown): number | undefined => {
|
|
230
|
-
const number =
|
|
231
|
-
typeof value === "number"
|
|
232
|
-
? value
|
|
233
|
-
: typeof value === "string" && value.trim() !== ""
|
|
234
|
-
? Number(value)
|
|
235
|
-
: Number.NaN;
|
|
236
|
-
return Number.isFinite(number) ? number : undefined;
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
240
|
-
|
|
241
|
-
const toDate = (value: unknown): Date | undefined => {
|
|
242
|
-
if (typeof value === "number") return new Date(value);
|
|
243
|
-
if (typeof value !== "string") return undefined;
|
|
244
|
-
const dateOnly = DATE_ONLY.exec(value);
|
|
245
|
-
if (!dateOnly) return new Date(value);
|
|
246
|
-
const [year, month, day] = dateOnly.slice(1).map(Number) as [
|
|
247
|
-
number,
|
|
248
|
-
number,
|
|
249
|
-
number,
|
|
250
|
-
];
|
|
251
|
-
const date = new Date(0);
|
|
252
|
-
date.setFullYear(year, month - 1, day);
|
|
253
|
-
date.setHours(0, 0, 0, 0);
|
|
254
|
-
return date.getMonth() === month - 1 && date.getDate() === day
|
|
255
|
-
? date
|
|
256
|
-
: undefined;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const toText = (value: unknown): string => {
|
|
260
|
-
if (value === undefined || value === null) return "";
|
|
261
|
-
if (typeof value !== "object") return String(value);
|
|
262
|
-
try {
|
|
263
|
-
return JSON.stringify(value) ?? "";
|
|
264
|
-
} catch {
|
|
265
|
-
return "";
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const cached = <T>(cache: Map<string, T>, key: string, create: () => T): T => {
|
|
270
|
-
let value = cache.get(key);
|
|
271
|
-
if (value === undefined) {
|
|
272
|
-
value = create();
|
|
273
|
-
if (cache.size >= 64) cache.clear();
|
|
274
|
-
cache.set(key, value);
|
|
275
|
-
}
|
|
276
|
-
return value;
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const numberFormats = new Map<string, Intl.NumberFormat>();
|
|
280
|
-
const dateFormats = new Map<string, Intl.DateTimeFormat>();
|
|
281
|
-
const pluralRules = new Map<string, Intl.PluralRules>();
|
|
282
|
-
|
|
283
|
-
const invalidArguments = (name: string, context: ValueFunctionContext) => {
|
|
284
|
-
context.warn(`A2UI function "${name}" received invalid arguments.`);
|
|
285
|
-
return undefined;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const formatAmount = (
|
|
289
|
-
name: string,
|
|
290
|
-
args: Record<string, unknown>,
|
|
291
|
-
context: ValueFunctionContext,
|
|
292
|
-
currency?: string,
|
|
293
|
-
): unknown => {
|
|
294
|
-
if (isMissing(args["value"])) return "";
|
|
295
|
-
const value = toNumber(args["value"]);
|
|
296
|
-
const decimals =
|
|
297
|
-
args["decimals"] === undefined ? undefined : toNumber(args["decimals"]);
|
|
298
|
-
if (
|
|
299
|
-
value === undefined ||
|
|
300
|
-
(args["decimals"] !== undefined &&
|
|
301
|
-
(decimals === undefined ||
|
|
302
|
-
!Number.isInteger(decimals) ||
|
|
303
|
-
decimals < 0 ||
|
|
304
|
-
decimals > 20))
|
|
305
|
-
) {
|
|
306
|
-
return invalidArguments(name, context);
|
|
307
|
-
}
|
|
308
|
-
const options: Intl.NumberFormatOptions = {
|
|
309
|
-
useGrouping: args["grouping"] !== false,
|
|
310
|
-
...(currency !== undefined ? { style: "currency", currency } : {}),
|
|
311
|
-
...(decimals !== undefined
|
|
312
|
-
? { minimumFractionDigits: decimals, maximumFractionDigits: decimals }
|
|
313
|
-
: {}),
|
|
314
|
-
};
|
|
315
|
-
try {
|
|
316
|
-
return cached(
|
|
317
|
-
numberFormats,
|
|
318
|
-
JSON.stringify(options),
|
|
319
|
-
() => new Intl.NumberFormat(undefined, options),
|
|
320
|
-
).format(value);
|
|
321
|
-
} catch {
|
|
322
|
-
return invalidArguments(name, context);
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const dateName = (
|
|
327
|
-
date: Date,
|
|
328
|
-
options: Intl.DateTimeFormatOptions,
|
|
329
|
-
type: Intl.DateTimeFormatPartTypes,
|
|
330
|
-
): string | undefined => {
|
|
331
|
-
try {
|
|
332
|
-
return cached(
|
|
333
|
-
dateFormats,
|
|
334
|
-
JSON.stringify(options),
|
|
335
|
-
() => new Intl.DateTimeFormat(undefined, options),
|
|
336
|
-
)
|
|
337
|
-
.formatToParts(date)
|
|
338
|
-
.find((part) => part.type === type)?.value;
|
|
339
|
-
} catch {
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const pad = (value: number, width: number) =>
|
|
345
|
-
String(value).padStart(width, "0");
|
|
346
|
-
|
|
347
|
-
const dateField = (
|
|
348
|
-
date: Date,
|
|
349
|
-
letter: string,
|
|
350
|
-
width: number,
|
|
351
|
-
): string | undefined => {
|
|
352
|
-
switch (letter) {
|
|
353
|
-
case "y":
|
|
354
|
-
return width === 2
|
|
355
|
-
? pad(date.getFullYear() % 100, 2)
|
|
356
|
-
: pad(date.getFullYear(), width);
|
|
357
|
-
case "M":
|
|
358
|
-
return width <= 2
|
|
359
|
-
? pad(date.getMonth() + 1, width)
|
|
360
|
-
: dateName(
|
|
361
|
-
date,
|
|
362
|
-
{ month: width === 3 ? "short" : width === 4 ? "long" : "narrow" },
|
|
363
|
-
"month",
|
|
364
|
-
);
|
|
365
|
-
case "d":
|
|
366
|
-
return pad(date.getDate(), width);
|
|
367
|
-
case "E":
|
|
368
|
-
return dateName(
|
|
369
|
-
date,
|
|
370
|
-
{ weekday: width <= 3 ? "short" : width === 4 ? "long" : "narrow" },
|
|
371
|
-
"weekday",
|
|
372
|
-
);
|
|
373
|
-
case "h":
|
|
374
|
-
return pad(date.getHours() % 12 || 12, width);
|
|
375
|
-
case "H":
|
|
376
|
-
return pad(date.getHours(), width);
|
|
377
|
-
case "m":
|
|
378
|
-
return pad(date.getMinutes(), width);
|
|
379
|
-
case "s":
|
|
380
|
-
return pad(date.getSeconds(), width);
|
|
381
|
-
case "a":
|
|
382
|
-
return (
|
|
383
|
-
dateName(date, { hour: "numeric", hour12: true }, "dayPeriod") ??
|
|
384
|
-
(date.getHours() < 12 ? "AM" : "PM")
|
|
385
|
-
);
|
|
386
|
-
default:
|
|
387
|
-
return undefined;
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
const formatDatePattern = (date: Date, pattern: string): string | undefined => {
|
|
392
|
-
let result = "";
|
|
393
|
-
let index = 0;
|
|
394
|
-
while (index < pattern.length) {
|
|
395
|
-
const character = pattern[index]!;
|
|
396
|
-
if (character === "'") {
|
|
397
|
-
index++;
|
|
398
|
-
if (pattern[index] === "'") {
|
|
399
|
-
result += "'";
|
|
400
|
-
index++;
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
while (index < pattern.length) {
|
|
404
|
-
if (pattern[index] !== "'") {
|
|
405
|
-
result += pattern[index++];
|
|
406
|
-
} else if (pattern[index + 1] === "'") {
|
|
407
|
-
result += "'";
|
|
408
|
-
index += 2;
|
|
409
|
-
} else {
|
|
410
|
-
index++;
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
} else if (/[A-Za-z]/.test(character)) {
|
|
415
|
-
let end = index + 1;
|
|
416
|
-
while (pattern[end] === character) end++;
|
|
417
|
-
const field = dateField(date, character, end - index);
|
|
418
|
-
if (field === undefined) return undefined;
|
|
419
|
-
result += field;
|
|
420
|
-
index = end;
|
|
421
|
-
} else {
|
|
422
|
-
result += character;
|
|
423
|
-
index++;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return result;
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
const pluralCategory = (value: number): string => {
|
|
430
|
-
try {
|
|
431
|
-
return cached(pluralRules, "", () => new Intl.PluralRules()).select(value);
|
|
432
|
-
} catch {
|
|
433
|
-
return value === 1 ? "one" : "other";
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
type ValueFunction = (
|
|
438
|
-
args: Record<string, unknown>,
|
|
439
|
-
context: ValueFunctionContext,
|
|
440
|
-
) => unknown;
|
|
441
|
-
|
|
442
|
-
const VALUE_FUNCTIONS: Readonly<Record<string, ValueFunction>> = {
|
|
443
|
-
formatString: (args, context) => {
|
|
444
|
-
const template = args["value"];
|
|
445
|
-
if (template === undefined || template === null) return "";
|
|
446
|
-
if (typeof template !== "string") {
|
|
447
|
-
return invalidArguments("formatString", context);
|
|
448
|
-
}
|
|
449
|
-
let parts = context.templates.get(template);
|
|
450
|
-
if (parts === undefined) {
|
|
451
|
-
try {
|
|
452
|
-
parts = parseExpressionTemplate(template);
|
|
453
|
-
} catch (error) {
|
|
454
|
-
if (!(error instanceof ExpressionSyntaxError)) throw error;
|
|
455
|
-
context.warn(
|
|
456
|
-
`A2UI formatString template is malformed: ${error.message}.`,
|
|
457
|
-
);
|
|
458
|
-
parts = null;
|
|
459
|
-
}
|
|
460
|
-
context.templates.set(template, parts);
|
|
461
|
-
}
|
|
462
|
-
if (parts === null) return undefined;
|
|
463
|
-
return parts
|
|
464
|
-
.map((part) =>
|
|
465
|
-
toText(
|
|
466
|
-
typeof part === "object" && part !== null
|
|
467
|
-
? context.resolve(part)
|
|
468
|
-
: part,
|
|
469
|
-
),
|
|
470
|
-
)
|
|
471
|
-
.join("");
|
|
472
|
-
},
|
|
473
|
-
formatNumber: (args, context) => formatAmount("formatNumber", args, context),
|
|
474
|
-
formatCurrency: (args, context) =>
|
|
475
|
-
typeof args["currency"] === "string"
|
|
476
|
-
? formatAmount("formatCurrency", args, context, args["currency"])
|
|
477
|
-
: invalidArguments("formatCurrency", context),
|
|
478
|
-
formatDate: (args, context) => {
|
|
479
|
-
const value = args["value"];
|
|
480
|
-
if (isMissing(value)) return "";
|
|
481
|
-
const format = args["format"];
|
|
482
|
-
const date = toDate(value);
|
|
483
|
-
if (typeof format !== "string" || !date || Number.isNaN(date.getTime())) {
|
|
484
|
-
return invalidArguments("formatDate", context);
|
|
485
|
-
}
|
|
486
|
-
const formatted = formatDatePattern(date, format);
|
|
487
|
-
if (formatted !== undefined) return formatted;
|
|
488
|
-
context.warn(
|
|
489
|
-
`A2UI function "formatDate" does not support the pattern "${format}", so it returned an ISO date.`,
|
|
490
|
-
);
|
|
491
|
-
return date.toISOString();
|
|
492
|
-
},
|
|
493
|
-
pluralize: (args, context) => {
|
|
494
|
-
if (isMissing(args["value"])) return "";
|
|
495
|
-
const value = toNumber(args["value"]);
|
|
496
|
-
if (value === undefined || typeof args["other"] !== "string") {
|
|
497
|
-
return invalidArguments("pluralize", context);
|
|
498
|
-
}
|
|
499
|
-
const selected = args[pluralCategory(value)];
|
|
500
|
-
return typeof selected === "string" ? selected : args["other"];
|
|
501
|
-
},
|
|
502
|
-
and: (args, context) =>
|
|
503
|
-
Array.isArray(args["values"])
|
|
504
|
-
? args["values"].every(Boolean)
|
|
505
|
-
: invalidArguments("and", context),
|
|
506
|
-
or: (args, context) =>
|
|
507
|
-
Array.isArray(args["values"])
|
|
508
|
-
? args["values"].some(Boolean)
|
|
509
|
-
: invalidArguments("or", context),
|
|
510
|
-
not: (args) => !args["value"],
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
export const evaluateA2uiValueFunction = (
|
|
514
|
-
name: string,
|
|
515
|
-
args: Record<string, unknown>,
|
|
516
|
-
context: ValueFunctionContext,
|
|
517
|
-
): unknown => {
|
|
518
|
-
if (!Object.hasOwn(VALUE_FUNCTIONS, name)) {
|
|
519
|
-
context.warn(`A2UI function "${name}" is not supported and was skipped.`);
|
|
520
|
-
return undefined;
|
|
521
|
-
}
|
|
522
|
-
return VALUE_FUNCTIONS[name]!(args, context);
|
|
523
|
-
};
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { createActionRegistry, emptyActionRegistry } from "./actionRegistry";
|
|
3
|
-
|
|
4
|
-
describe("createActionRegistry", () => {
|
|
5
|
-
it("dispatch resolves a registered action type and calls its handler with the payload", () => {
|
|
6
|
-
const handler = vi.fn(() => "ok");
|
|
7
|
-
const registry = createActionRegistry({ purchase: handler });
|
|
8
|
-
|
|
9
|
-
const result = registry.dispatch({
|
|
10
|
-
type: "purchase",
|
|
11
|
-
itemId: "sku-1",
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
expect(handler).toHaveBeenCalledWith({
|
|
15
|
-
payload: { type: "purchase", itemId: "sku-1" },
|
|
16
|
-
});
|
|
17
|
-
expect(result).toBe("ok");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("dispatch returns the handler's resolved promise value for async handlers", async () => {
|
|
21
|
-
const registry = createActionRegistry({
|
|
22
|
-
fetch: async () => 42,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const result = await registry.dispatch({ type: "fetch" });
|
|
26
|
-
expect(result).toBe(42);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("dispatch is a no-op (returns undefined) for an unknown action type", () => {
|
|
30
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
31
|
-
try {
|
|
32
|
-
const registry = createActionRegistry({
|
|
33
|
-
purchase: () => undefined,
|
|
34
|
-
refund: () => undefined,
|
|
35
|
-
});
|
|
36
|
-
expect(registry.dispatch({ type: "unknown" })).toBeUndefined();
|
|
37
|
-
expect(warn).toHaveBeenCalledTimes(1);
|
|
38
|
-
expect(warn).toHaveBeenCalledWith(
|
|
39
|
-
'[@assistant-ui/react-generative-ui] Action "unknown" has no registered handler. Registered actions: "purchase", "refund". Register it with createActionRegistry(...) or update the emitted `$action.type`.',
|
|
40
|
-
);
|
|
41
|
-
} finally {
|
|
42
|
-
warn.mockRestore();
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("dispatch is a no-op for malformed non-string action types", () => {
|
|
47
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
48
|
-
try {
|
|
49
|
-
const handler = vi.fn();
|
|
50
|
-
const registry = createActionRegistry({ purchase: handler });
|
|
51
|
-
|
|
52
|
-
expect(registry.dispatch({ type: 123 } as never)).toBeUndefined();
|
|
53
|
-
|
|
54
|
-
expect(handler).not.toHaveBeenCalled();
|
|
55
|
-
expect(warn).toHaveBeenCalledTimes(1);
|
|
56
|
-
expect(warn).toHaveBeenCalledWith(
|
|
57
|
-
"[@assistant-ui/react-generative-ui] Skipping malformed action; `$action.type` must be a string. Received number. Update the emitted `$action` payload.",
|
|
58
|
-
);
|
|
59
|
-
} finally {
|
|
60
|
-
warn.mockRestore();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("has reports whether a handler is registered", () => {
|
|
65
|
-
const registry = createActionRegistry({ a: () => undefined });
|
|
66
|
-
expect(registry.has("a")).toBe(true);
|
|
67
|
-
expect(registry.has("b")).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("emptyActionRegistry", () => {
|
|
72
|
-
it("dispatch is a no-op for any type and emits the dev warning", () => {
|
|
73
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
74
|
-
try {
|
|
75
|
-
expect(
|
|
76
|
-
emptyActionRegistry.dispatch({ type: "anything" }),
|
|
77
|
-
).toBeUndefined();
|
|
78
|
-
expect(warn).toHaveBeenCalledTimes(1);
|
|
79
|
-
expect(warn).toHaveBeenCalledWith(
|
|
80
|
-
'[@assistant-ui/react-generative-ui] Action "anything" has no registered handler. No actions are registered. Register it with createActionRegistry(...) or update the emitted `$action.type`.',
|
|
81
|
-
);
|
|
82
|
-
} finally {
|
|
83
|
-
warn.mockRestore();
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("has is false for any type", () => {
|
|
88
|
-
expect(emptyActionRegistry.has("anything")).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
});
|
package/src/actionRegistry.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { Action } from "./ir";
|
|
2
|
-
|
|
3
|
-
/** Context handed to an {@link ActionHandler} when an `$action` fires. */
|
|
4
|
-
export type ActionDispatchContext = {
|
|
5
|
-
/**
|
|
6
|
-
* The action payload. For fire-and-forget actions this is the `$action` as the model emitted it. For an interactive component the user's runtime input is merged in under the reserved `$input` key: a single value for a standalone control's dispatch, or an object keyed by each field's `name` for a `Form` or a `Card` with `asForm` set. A model-supplied `value` field is never clobbered.
|
|
7
|
-
*/
|
|
8
|
-
readonly payload: Action;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Resolves a single `$action.type`. Fire-and-forget actions return `void` or a
|
|
13
|
-
* promise that resolves to it. For a `prompt_user` tool whose call has no
|
|
14
|
-
* result yet, a non-`undefined` return value becomes the tool result. Whether
|
|
15
|
-
* that resumes the run depends on the runtime, including how it treats human
|
|
16
|
-
* tools. The return value is handed back to `dispatch`'s caller as `unknown`.
|
|
17
|
-
*/
|
|
18
|
-
export type ActionHandler = (
|
|
19
|
-
ctx: ActionDispatchContext,
|
|
20
|
-
) => unknown | Promise<unknown>;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The host-provided map from `$action.type` to {@link ActionHandler}. This is the single dispatch target for rendered generative UI: every interactive component fires its `$action` here through the injected `$dispatch` (a submit Button defers to its ancestor form's dispatch instead). The payload's `$input` takes one of two shapes: a single value for a standalone control's dispatch (`Select`, `Input`, `DatePicker`, `Checkbox`, `RadioGroup`, or a `CheckboxGroup`'s array of checked values), or an object keyed by field `name` for a `Form` or a `Card` with `asForm` set. When a registry is used in a `JSONGenerativeUI` tool, each dispatched payload, including `$input`, is recorded on the tool call on a best-effort basis. Recording depends on the runtime and interaction log limits, and never delays or prevents dispatch. Construct it with {@link createActionRegistry} and pass it to `JSONGenerativeUI`.
|
|
24
|
-
*/
|
|
25
|
-
export type ActionRegistry = {
|
|
26
|
-
dispatch(action: Action): unknown;
|
|
27
|
-
has(type: string): boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function createActionRegistry(
|
|
31
|
-
handlers: Readonly<Record<string, ActionHandler>>,
|
|
32
|
-
): ActionRegistry {
|
|
33
|
-
const map = new Map(Object.entries(handlers));
|
|
34
|
-
return {
|
|
35
|
-
dispatch(action) {
|
|
36
|
-
if (typeof action?.type !== "string") {
|
|
37
|
-
if (process.env["NODE_ENV"] !== "production") {
|
|
38
|
-
// eslint-disable-next-line no-console
|
|
39
|
-
console.warn(
|
|
40
|
-
"[@assistant-ui/react-generative-ui] Skipping malformed action; " +
|
|
41
|
-
"`$action.type` must be a string. " +
|
|
42
|
-
`Received ${formatReceivedType(action?.type)}. Update the emitted ` +
|
|
43
|
-
"`$action` payload.",
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const handler = map.get(action.type);
|
|
50
|
-
if (!handler) {
|
|
51
|
-
if (process.env["NODE_ENV"] !== "production") {
|
|
52
|
-
const actionTypes = [...map.keys()];
|
|
53
|
-
// eslint-disable-next-line no-console
|
|
54
|
-
console.warn(
|
|
55
|
-
`[@assistant-ui/react-generative-ui] Action "${action.type}" has ` +
|
|
56
|
-
`no registered handler. ${formatRegisteredActions(actionTypes)} ` +
|
|
57
|
-
"Register it with createActionRegistry(...) or update the emitted " +
|
|
58
|
-
"`$action.type`.",
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
return handler({ payload: action });
|
|
64
|
-
},
|
|
65
|
-
has(type) {
|
|
66
|
-
return map.has(type);
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** A no-op registry used when no handlers are provided. Dispatch resolves to
|
|
72
|
-
* `undefined` and logs a warning in dev for unknown types, so a model-emitted
|
|
73
|
-
* action with no handler degrades to "does nothing" rather than throwing. */
|
|
74
|
-
export const emptyActionRegistry: ActionRegistry = createActionRegistry({});
|
|
75
|
-
|
|
76
|
-
function formatRegisteredActions(actionTypes: string[]) {
|
|
77
|
-
if (actionTypes.length === 0) return "No actions are registered.";
|
|
78
|
-
|
|
79
|
-
return `Registered actions: ${actionTypes
|
|
80
|
-
.map((type) => `"${type}"`)
|
|
81
|
-
.join(", ")}.`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function formatReceivedType(value: unknown) {
|
|
85
|
-
if (value === null) return "null";
|
|
86
|
-
if (Array.isArray(value)) return "array";
|
|
87
|
-
return typeof value;
|
|
88
|
-
}
|