@assistant-ui/react-generative-ui 0.0.5 → 0.0.7
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/dist/JSONGenerativeUI.client.d.ts +4 -1
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +9 -2
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/JSONGenerativeUI.shared.d.ts +8 -0
- package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.js.map +1 -1
- package/dist/actionRegistry.d.ts +41 -0
- package/dist/actionRegistry.d.ts.map +1 -0
- package/dist/actionRegistry.js +41 -0
- package/dist/actionRegistry.js.map +1 -0
- package/dist/buildPresentParameters.js +10 -1
- package/dist/buildPresentParameters.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/index.server.d.ts +12 -0
- package/dist/index.server.js +10 -0
- package/dist/index.shared.d.ts +11 -0
- package/dist/index.shared.js +9 -0
- package/dist/ir.d.ts +115 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +149 -0
- package/dist/ir.js.map +1 -0
- package/dist/renderGenerativeUI.d.ts +5 -4
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/renderGenerativeUI.js +18 -45
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/types.d.ts +34 -30
- package/dist/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +34 -0
- package/dist/vocabulary/alert.d.ts.map +1 -0
- package/dist/vocabulary/alert.js +49 -0
- package/dist/vocabulary/alert.js.map +1 -0
- package/dist/vocabulary/data.d.ts +53 -0
- package/dist/vocabulary/data.d.ts.map +1 -0
- package/dist/vocabulary/data.js +63 -0
- package/dist/vocabulary/data.js.map +1 -0
- package/dist/vocabulary/fact.d.ts +20 -0
- package/dist/vocabulary/fact.d.ts.map +1 -0
- package/dist/vocabulary/fact.js +24 -0
- package/dist/vocabulary/fact.js.map +1 -0
- package/dist/vocabulary/index.d.ts +16 -0
- package/dist/vocabulary/index.d.ts.map +1 -0
- package/dist/vocabulary/index.js +30 -0
- package/dist/vocabulary/index.js.map +1 -0
- package/dist/vocabulary/interactive.d.ts +81 -0
- package/dist/vocabulary/interactive.d.ts.map +1 -0
- package/dist/vocabulary/interactive.js +123 -0
- package/dist/vocabulary/interactive.js.map +1 -0
- package/dist/vocabulary/layout.d.ts +78 -0
- package/dist/vocabulary/layout.d.ts.map +1 -0
- package/dist/vocabulary/layout.js +72 -0
- package/dist/vocabulary/layout.js.map +1 -0
- package/dist/vocabulary/media.d.ts +34 -0
- package/dist/vocabulary/media.d.ts.map +1 -0
- package/dist/vocabulary/media.js +32 -0
- package/dist/vocabulary/media.js.map +1 -0
- package/dist/vocabulary/text.d.ts +74 -0
- package/dist/vocabulary/text.d.ts.map +1 -0
- package/dist/vocabulary/text.js +48 -0
- package/dist/vocabulary/text.js.map +1 -0
- package/package.json +13 -13
- package/src/JSONGenerativeUI.client.tsx +10 -2
- package/src/JSONGenerativeUI.shared.ts +8 -0
- package/src/actionRegistry.test.ts +90 -0
- package/src/actionRegistry.ts +96 -0
- package/src/buildPresentParameters.ts +23 -7
- package/src/index.server.ts +2 -0
- package/src/index.shared.ts +58 -0
- package/src/index.ts +2 -26
- package/src/ir.test.ts +203 -0
- package/src/ir.ts +253 -0
- package/src/renderGenerativeUI.test.tsx +53 -0
- package/src/renderGenerativeUI.tsx +49 -82
- package/src/types.ts +57 -42
- package/src/vocabulary/alert.test.tsx +66 -0
- package/src/vocabulary/alert.tsx +47 -0
- package/src/vocabulary/data.test.tsx +67 -0
- package/src/vocabulary/data.tsx +86 -0
- package/src/vocabulary/fact.test.tsx +15 -0
- package/src/vocabulary/fact.tsx +22 -0
- package/src/vocabulary/index.test.ts +56 -0
- package/src/vocabulary/index.ts +27 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +110 -0
- package/src/vocabulary/interactive.test.tsx +108 -0
- package/src/vocabulary/interactive.tsx +165 -0
- package/src/vocabulary/layout.test.tsx +61 -0
- package/src/vocabulary/layout.tsx +76 -0
- package/src/vocabulary/media.test.tsx +53 -0
- package/src/vocabulary/media.tsx +37 -0
- package/src/vocabulary/text.test.tsx +73 -0
- package/src/vocabulary/text.tsx +59 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/vocabulary/text.d.ts
|
|
4
|
+
declare const textVocabulary: {
|
|
5
|
+
Header: {
|
|
6
|
+
description: string;
|
|
7
|
+
properties: z.ZodObject<{
|
|
8
|
+
text: z.ZodString;
|
|
9
|
+
size: z.ZodOptional<z.ZodEnum<{
|
|
10
|
+
sm: "sm";
|
|
11
|
+
md: "md";
|
|
12
|
+
lg: "lg";
|
|
13
|
+
xl: "xl";
|
|
14
|
+
"2xl": "2xl";
|
|
15
|
+
"3xl": "3xl";
|
|
16
|
+
}>>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
render: ({
|
|
19
|
+
text,
|
|
20
|
+
size,
|
|
21
|
+
children
|
|
22
|
+
}: any) => import("react").JSX.Element;
|
|
23
|
+
};
|
|
24
|
+
Text: {
|
|
25
|
+
description: string;
|
|
26
|
+
properties: z.ZodObject<{
|
|
27
|
+
value: z.ZodString;
|
|
28
|
+
size: z.ZodOptional<z.ZodEnum<{
|
|
29
|
+
sm: "sm";
|
|
30
|
+
md: "md";
|
|
31
|
+
lg: "lg";
|
|
32
|
+
xl: "xl";
|
|
33
|
+
"2xl": "2xl";
|
|
34
|
+
"3xl": "3xl";
|
|
35
|
+
}>>;
|
|
36
|
+
weight: z.ZodOptional<z.ZodEnum<{
|
|
37
|
+
bold: "bold";
|
|
38
|
+
normal: "normal";
|
|
39
|
+
medium: "medium";
|
|
40
|
+
semibold: "semibold";
|
|
41
|
+
}>>;
|
|
42
|
+
color: z.ZodOptional<z.ZodEnum<{
|
|
43
|
+
emphasis: "emphasis";
|
|
44
|
+
secondary: "secondary";
|
|
45
|
+
"alpha-70": "alpha-70";
|
|
46
|
+
white: "white";
|
|
47
|
+
"white-70": "white-70";
|
|
48
|
+
"white-50": "white-50";
|
|
49
|
+
}>>;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
streamProperties: true;
|
|
52
|
+
render: ({
|
|
53
|
+
value,
|
|
54
|
+
size,
|
|
55
|
+
weight,
|
|
56
|
+
color,
|
|
57
|
+
children
|
|
58
|
+
}: any) => import("react").JSX.Element;
|
|
59
|
+
};
|
|
60
|
+
Caption: {
|
|
61
|
+
description: string;
|
|
62
|
+
properties: z.ZodObject<{
|
|
63
|
+
value: z.ZodString;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
streamProperties: true;
|
|
66
|
+
render: ({
|
|
67
|
+
value,
|
|
68
|
+
children
|
|
69
|
+
}: any) => import("react").JSX.Element;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
export { textVocabulary };
|
|
74
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","names":[],"sources":["../../src/vocabulary/text.tsx"],"mappings":";;;cAIa,cAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { COLORS, TEXT_SIZES, WEIGHTS } from "../ir.js";
|
|
2
|
+
import { jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/vocabulary/text.tsx
|
|
5
|
+
const textVocabulary = {
|
|
6
|
+
Header: {
|
|
7
|
+
description: "A section heading. Renders as a bold title.",
|
|
8
|
+
properties: z.object({
|
|
9
|
+
text: z.string().describe("The heading text."),
|
|
10
|
+
size: z.enum(TEXT_SIZES).optional().describe("Heading size; defaults to `lg`.")
|
|
11
|
+
}),
|
|
12
|
+
render: ({ text, size, children }) => /* @__PURE__ */ jsxs("h2", {
|
|
13
|
+
"data-aui": "header",
|
|
14
|
+
"data-aui-size": size ?? "lg",
|
|
15
|
+
children: [text, children]
|
|
16
|
+
})
|
|
17
|
+
},
|
|
18
|
+
Text: {
|
|
19
|
+
description: "A run of text. Use for paragraphs, labels, or inline copy.",
|
|
20
|
+
properties: z.object({
|
|
21
|
+
value: z.string().describe("The text content."),
|
|
22
|
+
size: z.enum(TEXT_SIZES).optional().describe("Text size; defaults to `md`."),
|
|
23
|
+
weight: z.enum(WEIGHTS).optional().describe("Font weight."),
|
|
24
|
+
color: z.enum(COLORS).optional().describe("Foreground color token.")
|
|
25
|
+
}),
|
|
26
|
+
streamProperties: true,
|
|
27
|
+
render: ({ value, size, weight, color, children }) => /* @__PURE__ */ jsxs("span", {
|
|
28
|
+
"data-aui": "text",
|
|
29
|
+
"data-aui-size": size ?? "md",
|
|
30
|
+
"data-aui-weight": weight,
|
|
31
|
+
"data-aui-color": color,
|
|
32
|
+
children: [value, children]
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
Caption: {
|
|
36
|
+
description: "Secondary, de-emphasized text under a primary element.",
|
|
37
|
+
properties: z.object({ value: z.string().describe("The caption text.") }),
|
|
38
|
+
streamProperties: true,
|
|
39
|
+
render: ({ value, children }) => /* @__PURE__ */ jsxs("p", {
|
|
40
|
+
"data-aui": "caption",
|
|
41
|
+
children: [value, children]
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { textVocabulary };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../src/vocabulary/text.tsx"],"sourcesContent":["import { z } from \"zod\";\nimport type { GenerativeUILibrary } from \"../types\";\nimport { COLORS, TEXT_SIZES, WEIGHTS } from \"../ir\";\n\nexport const textVocabulary = {\n Header: {\n description: \"A section heading. Renders as a bold title.\",\n properties: z.object({\n text: z.string().describe(\"The heading text.\"),\n size: z\n .enum(TEXT_SIZES)\n .optional()\n .describe(\"Heading size; defaults to `lg`.\"),\n }),\n render: ({ text, size, children }) => (\n <h2 data-aui=\"header\" data-aui-size={size ?? \"lg\"}>\n {text}\n {children}\n </h2>\n ),\n },\n Text: {\n description: \"A run of text. Use for paragraphs, labels, or inline copy.\",\n properties: z.object({\n value: z.string().describe(\"The text content.\"),\n size: z\n .enum(TEXT_SIZES)\n .optional()\n .describe(\"Text size; defaults to `md`.\"),\n weight: z.enum(WEIGHTS).optional().describe(\"Font weight.\"),\n color: z.enum(COLORS).optional().describe(\"Foreground color token.\"),\n }),\n streamProperties: true,\n render: ({ value, size, weight, color, children }) => (\n <span\n data-aui=\"text\"\n data-aui-size={size ?? \"md\"}\n data-aui-weight={weight}\n data-aui-color={color}\n >\n {value}\n {children}\n </span>\n ),\n },\n Caption: {\n description: \"Secondary, de-emphasized text under a primary element.\",\n properties: z.object({\n value: z.string().describe(\"The caption text.\"),\n }),\n streamProperties: true,\n render: ({ value, children }) => (\n <p data-aui=\"caption\">\n {value}\n {children}\n </p>\n ),\n },\n} satisfies GenerativeUILibrary;\n"],"mappings":";;;;AAIA,MAAa,iBAAiB;CAC5B,QAAQ;EACN,aAAa;EACb,YAAY,EAAE,OAAO;GACnB,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS,mBAAmB;GAC7C,MAAM,EACH,KAAK,UAAU,CAAC,CAChB,SAAS,CAAC,CACV,SAAS,iCAAiC;EAC/C,CAAC;EACD,SAAS,EAAE,MAAM,MAAM,eACrB,qBAAC,MAAD;GAAI,YAAS;GAAS,iBAAe,QAAQ;aAA7C,CACG,MACA,QACC;;CAER;CACA,MAAM;EACJ,aAAa;EACb,YAAY,EAAE,OAAO;GACnB,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS,mBAAmB;GAC9C,MAAM,EACH,KAAK,UAAU,CAAC,CAChB,SAAS,CAAC,CACV,SAAS,8BAA8B;GAC1C,QAAQ,EAAE,KAAK,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,cAAc;GAC1D,OAAO,EAAE,KAAK,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,yBAAyB;EACrE,CAAC;EACD,kBAAkB;EAClB,SAAS,EAAE,OAAO,MAAM,QAAQ,OAAO,eACrC,qBAAC,QAAD;GACE,YAAS;GACT,iBAAe,QAAQ;GACvB,mBAAiB;GACjB,kBAAgB;aAJlB,CAMG,OACA,QACG;;CAEV;CACA,SAAS;EACP,aAAa;EACb,YAAY,EAAE,OAAO,EACnB,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS,mBAAmB,EAChD,CAAC;EACD,kBAAkB;EAClB,SAAS,EAAE,OAAO,eAChB,qBAAC,KAAD;GAAG,YAAS;aAAZ,CACG,OACA,QACA;;CAEP;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-generative-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Generative UI components for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"generative-ui",
|
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"types": "./dist/index.d.ts",
|
|
19
|
-
"default": "./dist/index.js"
|
|
20
|
-
},
|
|
21
|
-
"./internal-json": {
|
|
22
18
|
"react-server": {
|
|
23
|
-
"types": "./dist/
|
|
24
|
-
"default": "./dist/
|
|
19
|
+
"types": "./dist/index.server.d.ts",
|
|
20
|
+
"default": "./dist/index.server.js"
|
|
25
21
|
},
|
|
26
22
|
"default": {
|
|
27
|
-
"types": "./dist/
|
|
28
|
-
"default": "./dist/
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
29
25
|
}
|
|
26
|
+
},
|
|
27
|
+
"./ir": {
|
|
28
|
+
"types": "./dist/ir.d.ts",
|
|
29
|
+
"default": "./dist/ir.js"
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"main": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"sideEffects": false,
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"assistant-stream": "^0.3.
|
|
41
|
+
"assistant-stream": "^0.3.25"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@assistant-ui/react": "^0.14.14",
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"@types/react": "^19.2.17",
|
|
57
57
|
"react": "^19.2.7",
|
|
58
58
|
"react-dom": "^19.2.7",
|
|
59
|
-
"vitest": "^4.1.
|
|
59
|
+
"vitest": "^4.1.9",
|
|
60
60
|
"zod": "^4.4.3",
|
|
61
|
-
"@assistant-ui/react": "0.14.
|
|
62
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
61
|
+
"@assistant-ui/react": "0.14.26",
|
|
62
|
+
"@assistant-ui/x-buildutils": "0.0.17"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type PresentToolOptions,
|
|
10
10
|
type PromptUserTool,
|
|
11
11
|
} from "./JSONGenerativeUI.shared";
|
|
12
|
+
import { type ActionRegistry } from "./actionRegistry";
|
|
12
13
|
import { renderGenerativeUI } from "./renderGenerativeUI";
|
|
13
14
|
import type { GenerativeUILibrary, GenerativeUIStatus } from "./types";
|
|
14
15
|
|
|
@@ -27,15 +28,19 @@ function uiStatus(status: { type: string }): GenerativeUIStatus {
|
|
|
27
28
|
* `present` is a frontend tool that renders the model's `{ $type, ...props }`
|
|
28
29
|
* tree against the library and resolves immediately. `prompt_user` is a
|
|
29
30
|
* human-in-the-loop tool: the model pauses and the rendered UI supplies the
|
|
30
|
-
* result. Both draw the tree the same way, so they share one `render`.
|
|
31
|
+
* result. Both draw the tree the same way, so they share one `render`. The
|
|
32
|
+
* `actions` registry (if provided) is threaded into the render context so
|
|
33
|
+
* interactive components can fire `$action` through `$dispatch`.
|
|
31
34
|
*/
|
|
32
35
|
export class JSONGenerativeUI {
|
|
33
36
|
private readonly library: GenerativeUILibrary;
|
|
34
37
|
private readonly parameters: PresentParameters;
|
|
38
|
+
private readonly actions: ActionRegistry | undefined;
|
|
35
39
|
|
|
36
40
|
constructor(options: JSONGenerativeUIOptions) {
|
|
37
41
|
this.library = options.library;
|
|
38
42
|
this.parameters = buildPresentParameters(options.library);
|
|
43
|
+
this.actions = options.actions;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
private readonly render = ({
|
|
@@ -45,7 +50,10 @@ export class JSONGenerativeUI {
|
|
|
45
50
|
args: unknown;
|
|
46
51
|
status: { type: string };
|
|
47
52
|
}): ReactNode =>
|
|
48
|
-
renderGenerativeUI(args, this.library, {
|
|
53
|
+
renderGenerativeUI(args, this.library, {
|
|
54
|
+
status: uiStatus(status),
|
|
55
|
+
...(this.actions ? { dispatch: this.actions.dispatch } : {}),
|
|
56
|
+
});
|
|
49
57
|
|
|
50
58
|
present(options?: PresentToolOptions): PresentTool {
|
|
51
59
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@assistant-ui/react";
|
|
2
2
|
import type { JSONSchema7 } from "json-schema";
|
|
3
|
+
import type { ActionRegistry } from "./actionRegistry";
|
|
3
4
|
import type { GenerativeUILibrary } from "./types";
|
|
4
5
|
|
|
5
6
|
/** Options for {@link JSONGenerativeUI}. */
|
|
@@ -10,6 +11,13 @@ export type JSONGenerativeUIOptions = {
|
|
|
10
11
|
* `"use generative"` build can split each `render` from its `properties`.
|
|
11
12
|
*/
|
|
12
13
|
library: GenerativeUILibrary;
|
|
14
|
+
/**
|
|
15
|
+
* Host-provided map from `$action.type` to its handler. Interactive
|
|
16
|
+
* components (`Button`/`Select`/`Input`/`DatePicker`) call `$dispatch($action)`
|
|
17
|
+
* on their event, which resolves through this registry. Omit it for a
|
|
18
|
+
* read-only render where model-emitted actions degrade to a no-op.
|
|
19
|
+
*/
|
|
20
|
+
actions?: ActionRegistry;
|
|
13
21
|
};
|
|
14
22
|
|
|
15
23
|
/** Options for {@link JSONGenerativeUI.present}. */
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
|
7
|
+
* the model emitted it. For interactive components (`Select`/`Input`/
|
|
8
|
+
* `DatePicker`) the user's runtime input is merged in under the reserved
|
|
9
|
+
* `$input` key, so the handler sees both the model payload and what the user
|
|
10
|
+
* did; a model-supplied `value` field is never clobbered.
|
|
11
|
+
*/
|
|
12
|
+
readonly payload: Action;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a single `$action.type`. Fire-and-forget actions return `void` or a
|
|
17
|
+
* promise that resolves to it; human-in-the-loop actions return a value the
|
|
18
|
+
* runtime uses to resume the run (see IR doc Open question #2 — the resume
|
|
19
|
+
* value is left as `unknown` and documented per-action). The return value is
|
|
20
|
+
* handed back to `dispatch`'s caller as `unknown`.
|
|
21
|
+
*/
|
|
22
|
+
export type ActionHandler = (
|
|
23
|
+
ctx: ActionDispatchContext,
|
|
24
|
+
) => unknown | Promise<unknown>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The host-provided map from `$action.type` to {@link ActionHandler}. This is
|
|
28
|
+
* the single dispatch target for `$action` on web, shared by the model
|
|
29
|
+
* `present` renderer, the synthetic `present` produced from A2A/A2UI, and the
|
|
30
|
+
* decoded `block_actions` from Slack. Construct it with
|
|
31
|
+
* {@link createActionRegistry} and pass it to `JSONGenerativeUI`.
|
|
32
|
+
*/
|
|
33
|
+
export type ActionRegistry = {
|
|
34
|
+
dispatch(action: Action): unknown;
|
|
35
|
+
has(type: string): boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function createActionRegistry(
|
|
39
|
+
handlers: Readonly<Record<string, ActionHandler>>,
|
|
40
|
+
): ActionRegistry {
|
|
41
|
+
const map = new Map(Object.entries(handlers));
|
|
42
|
+
return {
|
|
43
|
+
dispatch(action) {
|
|
44
|
+
if (typeof action?.type !== "string") {
|
|
45
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.warn(
|
|
48
|
+
"[@assistant-ui/react-generative-ui] Skipping malformed action; " +
|
|
49
|
+
"`$action.type` must be a string. " +
|
|
50
|
+
`Received ${formatReceivedType(action?.type)}. Update the emitted ` +
|
|
51
|
+
"`$action` payload.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handler = map.get(action.type);
|
|
58
|
+
if (!handler) {
|
|
59
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
60
|
+
const actionTypes = [...map.keys()];
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.warn(
|
|
63
|
+
`[@assistant-ui/react-generative-ui] Action "${action.type}" has ` +
|
|
64
|
+
`no registered handler. ${formatRegisteredActions(actionTypes)} ` +
|
|
65
|
+
"Register it with createActionRegistry(...) or update the emitted " +
|
|
66
|
+
"`$action.type`.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return handler({ payload: action });
|
|
72
|
+
},
|
|
73
|
+
has(type) {
|
|
74
|
+
return map.has(type);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A no-op registry used when no handlers are provided. Dispatch resolves to
|
|
80
|
+
* `undefined` and logs a warning in dev for unknown types, so a model-emitted
|
|
81
|
+
* action with no handler degrades to "does nothing" rather than throwing. */
|
|
82
|
+
export const emptyActionRegistry: ActionRegistry = createActionRegistry({});
|
|
83
|
+
|
|
84
|
+
function formatRegisteredActions(actionTypes: string[]) {
|
|
85
|
+
if (actionTypes.length === 0) return "No actions are registered.";
|
|
86
|
+
|
|
87
|
+
return `Registered actions: ${actionTypes
|
|
88
|
+
.map((type) => `"${type}"`)
|
|
89
|
+
.join(", ")}.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatReceivedType(value: unknown) {
|
|
93
|
+
if (value === null) return "null";
|
|
94
|
+
if (Array.isArray(value)) return "array";
|
|
95
|
+
return typeof value;
|
|
96
|
+
}
|
|
@@ -29,6 +29,7 @@ export function buildPresentParameters(
|
|
|
29
29
|
// first component's schema wins — props are an advisory hint here, not a
|
|
30
30
|
// strict per-component contract.
|
|
31
31
|
const props: Record<string, JSONSchema7Definition> = {};
|
|
32
|
+
const propOwners = new Map<string, string[]>();
|
|
32
33
|
for (const name of names) {
|
|
33
34
|
const propsSchema = toJSONSchema(library[name]!.properties);
|
|
34
35
|
if (propsSchema.type !== "object") {
|
|
@@ -41,14 +42,20 @@ export function buildPresentParameters(
|
|
|
41
42
|
if (key === TYPE_KEY || key === "children") continue;
|
|
42
43
|
if (!(key in props)) {
|
|
43
44
|
props[key] = schema;
|
|
44
|
-
} else if (process.env["NODE_ENV"] !== "production") {
|
|
45
|
-
// eslint-disable-next-line no-console
|
|
46
|
-
console.warn(
|
|
47
|
-
`[@assistant-ui/react-generative-ui] Prop "${key}" is declared by more ` +
|
|
48
|
-
"than one component; the first component's schema is kept and the rest " +
|
|
49
|
-
"are ignored. Rename or align the type to avoid an ambiguous schema.",
|
|
50
|
-
);
|
|
51
45
|
}
|
|
46
|
+
propOwners.set(key, [...(propOwners.get(key) ?? []), name]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
51
|
+
for (const [key, owners] of propOwners) {
|
|
52
|
+
if (owners.length < 2) continue;
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.warn(
|
|
55
|
+
`[@assistant-ui/react-generative-ui] Prop "${key}" is declared by ` +
|
|
56
|
+
`${formatComponentList(owners)}; keeping "${owners[0]}"'s schema. ` +
|
|
57
|
+
"Rename or align the prop type to avoid an ambiguous schema.",
|
|
58
|
+
);
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -88,3 +95,12 @@ export function buildPresentParameters(
|
|
|
88
95
|
$defs: { node, children },
|
|
89
96
|
};
|
|
90
97
|
}
|
|
98
|
+
|
|
99
|
+
function formatComponentList(names: string[]) {
|
|
100
|
+
if (names.length <= 2) return names.map((name) => `"${name}"`).join(" and ");
|
|
101
|
+
|
|
102
|
+
return `${names
|
|
103
|
+
.slice(0, -1)
|
|
104
|
+
.map((name) => `"${name}"`)
|
|
105
|
+
.join(", ")}, and "${names[names.length - 1]}"`;
|
|
106
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export { renderGenerativeUI } from "./renderGenerativeUI";
|
|
2
|
+
export { generativeUIToJSX } from "./generativeUIToJSX";
|
|
3
|
+
export { buildPresentParameters } from "./buildPresentParameters";
|
|
4
|
+
export { defineGenerativeComponents } from "./defineGenerativeComponents";
|
|
5
|
+
export { TYPE_KEY } from "./constants";
|
|
6
|
+
export { normalizeUINode, normalizeSpec } from "./ir";
|
|
7
|
+
export {
|
|
8
|
+
TEXT_SIZES,
|
|
9
|
+
IMAGE_SIZE_TOKENS,
|
|
10
|
+
WEIGHTS,
|
|
11
|
+
COLORS,
|
|
12
|
+
ALIGNS,
|
|
13
|
+
JUSTIFIES,
|
|
14
|
+
BUTTON_STYLES,
|
|
15
|
+
ALERT_TONES,
|
|
16
|
+
} from "./ir";
|
|
17
|
+
export { defaultGenerativeUILibrary } from "./vocabulary";
|
|
18
|
+
export { createActionRegistry, emptyActionRegistry } from "./actionRegistry";
|
|
19
|
+
export type {
|
|
20
|
+
ActionHandler,
|
|
21
|
+
ActionRegistry,
|
|
22
|
+
ActionDispatchContext,
|
|
23
|
+
} from "./actionRegistry";
|
|
24
|
+
export type {
|
|
25
|
+
JSONGenerativeUIOptions,
|
|
26
|
+
PresentTool,
|
|
27
|
+
PresentToolOptions,
|
|
28
|
+
PromptUserTool,
|
|
29
|
+
} from "./JSONGenerativeUI.shared";
|
|
30
|
+
export type {
|
|
31
|
+
GenerativeUILibrary,
|
|
32
|
+
GenerativeUIComponent,
|
|
33
|
+
GenerativeUIElement,
|
|
34
|
+
GenerativeUIProps,
|
|
35
|
+
GenerativeUINode,
|
|
36
|
+
GenerativeUIStatus,
|
|
37
|
+
GenerativeUIRenderContext,
|
|
38
|
+
GenerativeUIAction,
|
|
39
|
+
GenerativeUIDispatch,
|
|
40
|
+
} from "./types";
|
|
41
|
+
export type {
|
|
42
|
+
UINode,
|
|
43
|
+
UIElement,
|
|
44
|
+
UIChildren,
|
|
45
|
+
LegacyComponentNode,
|
|
46
|
+
Action,
|
|
47
|
+
UISpec,
|
|
48
|
+
NormalizedUINode,
|
|
49
|
+
NormalizedUIElement,
|
|
50
|
+
TextSize,
|
|
51
|
+
ImageSize,
|
|
52
|
+
Weight,
|
|
53
|
+
Color,
|
|
54
|
+
Align,
|
|
55
|
+
Justify,
|
|
56
|
+
ButtonStyle,
|
|
57
|
+
AlertTone,
|
|
58
|
+
} from "./ir";
|
package/src/index.ts
CHANGED
|
@@ -1,26 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// under the `react-server` condition and the client variant (adds render/execute)
|
|
4
|
-
// otherwise. Everything else here is isomorphic, so this single entry serves
|
|
5
|
-
// both conditions — no per-build index barrel needed.
|
|
6
|
-
export { JSONGenerativeUI } from "@assistant-ui/react-generative-ui/internal-json";
|
|
7
|
-
export { renderGenerativeUI } from "./renderGenerativeUI";
|
|
8
|
-
export { generativeUIToJSX } from "./generativeUIToJSX";
|
|
9
|
-
export { buildPresentParameters } from "./buildPresentParameters";
|
|
10
|
-
export { defineGenerativeComponents } from "./defineGenerativeComponents";
|
|
11
|
-
export { TYPE_KEY } from "./constants";
|
|
12
|
-
export type {
|
|
13
|
-
JSONGenerativeUIOptions,
|
|
14
|
-
PresentTool,
|
|
15
|
-
PresentToolOptions,
|
|
16
|
-
PromptUserTool,
|
|
17
|
-
} from "./JSONGenerativeUI.shared";
|
|
18
|
-
export type {
|
|
19
|
-
GenerativeUILibrary,
|
|
20
|
-
GenerativeUIComponent,
|
|
21
|
-
GenerativeUIElement,
|
|
22
|
-
GenerativeUIProps,
|
|
23
|
-
GenerativeUINode,
|
|
24
|
-
GenerativeUIStatus,
|
|
25
|
-
GenerativeUIRenderContext,
|
|
26
|
-
} from "./types";
|
|
1
|
+
export { JSONGenerativeUI } from "./JSONGenerativeUI.client";
|
|
2
|
+
export * from "./index.shared";
|