@assistant-ui/react-generative-ui 0.0.6 → 0.0.8

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.
Files changed (201) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +4 -2
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +9 -2
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/JSONGenerativeUI.server.d.ts +0 -1
  6. package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
  7. package/dist/JSONGenerativeUI.shared.d.ts +8 -1
  8. package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
  9. package/dist/JSONGenerativeUI.shared.js.map +1 -1
  10. package/dist/actionRegistry.d.ts +32 -0
  11. package/dist/actionRegistry.d.ts.map +1 -0
  12. package/dist/actionRegistry.js +41 -0
  13. package/dist/actionRegistry.js.map +1 -0
  14. package/dist/buildPresentParameters.d.ts +5 -5
  15. package/dist/buildPresentParameters.d.ts.map +1 -1
  16. package/dist/buildPresentParameters.js +20 -6
  17. package/dist/buildPresentParameters.js.map +1 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/defineGenerativeComponents.d.ts +0 -1
  20. package/dist/defineGenerativeComponents.d.ts.map +1 -1
  21. package/dist/generativeUIToJSX.d.ts +15 -10
  22. package/dist/generativeUIToJSX.d.ts.map +1 -1
  23. package/dist/generativeUIToJSX.js +47 -17
  24. package/dist/generativeUIToJSX.js.map +1 -1
  25. package/dist/index.d.ts +8 -4
  26. package/dist/index.js +5 -2
  27. package/dist/index.server.d.ts +13 -0
  28. package/dist/index.server.js +10 -0
  29. package/dist/index.shared.d.ts +11 -0
  30. package/dist/index.shared.js +9 -0
  31. package/dist/ir.d.ts +115 -0
  32. package/dist/ir.d.ts.map +1 -0
  33. package/dist/ir.js +175 -0
  34. package/dist/ir.js.map +1 -0
  35. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
  36. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
  37. package/dist/renderGenerativeUI.d.ts +5 -5
  38. package/dist/renderGenerativeUI.d.ts.map +1 -1
  39. package/dist/renderGenerativeUI.js +18 -45
  40. package/dist/renderGenerativeUI.js.map +1 -1
  41. package/dist/slack/constants.d.ts +111 -0
  42. package/dist/slack/constants.d.ts.map +1 -0
  43. package/dist/slack/constants.js +168 -0
  44. package/dist/slack/constants.js.map +1 -0
  45. package/dist/slack/decodeBlockAction.d.ts +11 -0
  46. package/dist/slack/decodeBlockAction.d.ts.map +1 -0
  47. package/dist/slack/decodeBlockAction.js +40 -0
  48. package/dist/slack/decodeBlockAction.js.map +1 -0
  49. package/dist/slack/fromSlackBlocks.d.ts +30 -0
  50. package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
  51. package/dist/slack/fromSlackBlocks.js +447 -0
  52. package/dist/slack/fromSlackBlocks.js.map +1 -0
  53. package/dist/slack/toSlackBlocks.d.ts +7 -0
  54. package/dist/slack/toSlackBlocks.d.ts.map +1 -0
  55. package/dist/slack/toSlackBlocks.js +649 -0
  56. package/dist/slack/toSlackBlocks.js.map +1 -0
  57. package/dist/slack/types.d.ts +178 -0
  58. package/dist/slack/types.d.ts.map +1 -0
  59. package/dist/slack/types.js +0 -0
  60. package/dist/slack.d.ts +5 -0
  61. package/dist/slack.js +4 -0
  62. package/dist/teams/boundSpec.d.ts +20 -0
  63. package/dist/teams/boundSpec.d.ts.map +1 -0
  64. package/dist/teams/boundSpec.js +67 -0
  65. package/dist/teams/boundSpec.js.map +1 -0
  66. package/dist/teams/constants.d.ts +57 -0
  67. package/dist/teams/constants.d.ts.map +1 -0
  68. package/dist/teams/constants.js +92 -0
  69. package/dist/teams/constants.js.map +1 -0
  70. package/dist/teams/decodeSubmitData.d.ts +25 -0
  71. package/dist/teams/decodeSubmitData.d.ts.map +1 -0
  72. package/dist/teams/decodeSubmitData.js +44 -0
  73. package/dist/teams/decodeSubmitData.js.map +1 -0
  74. package/dist/teams/toAdaptiveCard.d.ts +39 -0
  75. package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
  76. package/dist/teams/toAdaptiveCard.js +496 -0
  77. package/dist/teams/toAdaptiveCard.js.map +1 -0
  78. package/dist/teams/toTeamsAttachments.d.ts +13 -0
  79. package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
  80. package/dist/teams/toTeamsAttachments.js +95 -0
  81. package/dist/teams/toTeamsAttachments.js.map +1 -0
  82. package/dist/teams/types.d.ts +196 -0
  83. package/dist/teams/types.d.ts.map +1 -0
  84. package/dist/teams/types.js +0 -0
  85. package/dist/teams.d.ts +5 -0
  86. package/dist/teams.js +4 -0
  87. package/dist/types.d.ts +41 -34
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/vocabulary/alert.d.ts +28 -0
  90. package/dist/vocabulary/alert.d.ts.map +1 -0
  91. package/dist/vocabulary/alert.js +58 -0
  92. package/dist/vocabulary/alert.js.map +1 -0
  93. package/dist/vocabulary/collectFormValues.d.ts +23 -0
  94. package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
  95. package/dist/vocabulary/collectFormValues.js +32 -0
  96. package/dist/vocabulary/collectFormValues.js.map +1 -0
  97. package/dist/vocabulary/data.d.ts +52 -0
  98. package/dist/vocabulary/data.d.ts.map +1 -0
  99. package/dist/vocabulary/data.js +294 -0
  100. package/dist/vocabulary/data.js.map +1 -0
  101. package/dist/vocabulary/dispatch.d.ts +11 -0
  102. package/dist/vocabulary/dispatch.d.ts.map +1 -0
  103. package/dist/vocabulary/dispatch.js +21 -0
  104. package/dist/vocabulary/dispatch.js.map +1 -0
  105. package/dist/vocabulary/fact.d.ts +15 -0
  106. package/dist/vocabulary/fact.d.ts.map +1 -0
  107. package/dist/vocabulary/fact.js +24 -0
  108. package/dist/vocabulary/fact.js.map +1 -0
  109. package/dist/vocabulary/form.d.ts +14 -0
  110. package/dist/vocabulary/form.d.ts.map +1 -0
  111. package/dist/vocabulary/form.js +23 -0
  112. package/dist/vocabulary/form.js.map +1 -0
  113. package/dist/vocabulary/icon.d.ts +44 -0
  114. package/dist/vocabulary/icon.d.ts.map +1 -0
  115. package/dist/vocabulary/icon.js +272 -0
  116. package/dist/vocabulary/icon.js.map +1 -0
  117. package/dist/vocabulary/index.d.ts +15 -0
  118. package/dist/vocabulary/index.d.ts.map +1 -0
  119. package/dist/vocabulary/index.js +36 -0
  120. package/dist/vocabulary/index.js.map +1 -0
  121. package/dist/vocabulary/interactive.d.ts +93 -0
  122. package/dist/vocabulary/interactive.d.ts.map +1 -0
  123. package/dist/vocabulary/interactive.js +172 -0
  124. package/dist/vocabulary/interactive.js.map +1 -0
  125. package/dist/vocabulary/layout.d.ts +82 -0
  126. package/dist/vocabulary/layout.d.ts.map +1 -0
  127. package/dist/vocabulary/layout.js +136 -0
  128. package/dist/vocabulary/layout.js.map +1 -0
  129. package/dist/vocabulary/list.d.ts +17 -0
  130. package/dist/vocabulary/list.d.ts.map +1 -0
  131. package/dist/vocabulary/list.js +48 -0
  132. package/dist/vocabulary/list.js.map +1 -0
  133. package/dist/vocabulary/media.d.ts +28 -0
  134. package/dist/vocabulary/media.d.ts.map +1 -0
  135. package/dist/vocabulary/media.js +42 -0
  136. package/dist/vocabulary/media.js.map +1 -0
  137. package/dist/vocabulary/text.d.ts +60 -0
  138. package/dist/vocabulary/text.d.ts.map +1 -0
  139. package/dist/vocabulary/text.js +48 -0
  140. package/dist/vocabulary/text.js.map +1 -0
  141. package/package.json +21 -13
  142. package/src/JSONGenerativeUI.client.tsx +10 -2
  143. package/src/JSONGenerativeUI.shared.ts +8 -0
  144. package/src/actionRegistry.test.ts +90 -0
  145. package/src/actionRegistry.ts +88 -0
  146. package/src/buildPresentParameters.ts +38 -16
  147. package/src/generativeUIToJSX.test.ts +186 -0
  148. package/src/generativeUIToJSX.ts +112 -21
  149. package/src/index.server.ts +2 -0
  150. package/src/index.shared.ts +61 -0
  151. package/src/index.ts +2 -26
  152. package/src/ir.test.ts +203 -0
  153. package/src/ir.ts +279 -0
  154. package/src/renderGenerativeUI.test.tsx +69 -2
  155. package/src/renderGenerativeUI.tsx +49 -82
  156. package/src/slack/constants.ts +217 -0
  157. package/src/slack/decodeBlockAction.test.ts +135 -0
  158. package/src/slack/decodeBlockAction.ts +66 -0
  159. package/src/slack/fromSlackBlocks.test.ts +1145 -0
  160. package/src/slack/fromSlackBlocks.ts +709 -0
  161. package/src/slack/toSlackBlocks.test.ts +1936 -0
  162. package/src/slack/toSlackBlocks.ts +1319 -0
  163. package/src/slack/types.ts +230 -0
  164. package/src/slack.ts +37 -0
  165. package/src/teams/boundSpec.ts +102 -0
  166. package/src/teams/constants.ts +127 -0
  167. package/src/teams/decodeSubmitData.test.ts +148 -0
  168. package/src/teams/decodeSubmitData.ts +57 -0
  169. package/src/teams/toAdaptiveCard.test.ts +1410 -0
  170. package/src/teams/toAdaptiveCard.ts +762 -0
  171. package/src/teams/toTeamsAttachments.test.ts +29 -0
  172. package/src/teams/toTeamsAttachments.ts +137 -0
  173. package/src/teams/types.ts +237 -0
  174. package/src/teams.ts +34 -0
  175. package/src/types.ts +57 -42
  176. package/src/vocabulary/alert.test.tsx +136 -0
  177. package/src/vocabulary/alert.tsx +66 -0
  178. package/src/vocabulary/collectFormValues.test.ts +127 -0
  179. package/src/vocabulary/collectFormValues.ts +58 -0
  180. package/src/vocabulary/data.test.tsx +567 -0
  181. package/src/vocabulary/data.tsx +494 -0
  182. package/src/vocabulary/dispatch.ts +22 -0
  183. package/src/vocabulary/fact.test.tsx +15 -0
  184. package/src/vocabulary/fact.tsx +22 -0
  185. package/src/vocabulary/form.test.tsx +112 -0
  186. package/src/vocabulary/form.tsx +34 -0
  187. package/src/vocabulary/icon.test.tsx +64 -0
  188. package/src/vocabulary/icon.tsx +174 -0
  189. package/src/vocabulary/index.test.ts +63 -0
  190. package/src/vocabulary/index.ts +33 -0
  191. package/src/vocabulary/interactive.dispatch.test.tsx +352 -0
  192. package/src/vocabulary/interactive.test.tsx +248 -0
  193. package/src/vocabulary/interactive.tsx +269 -0
  194. package/src/vocabulary/layout.test.tsx +289 -0
  195. package/src/vocabulary/layout.tsx +229 -0
  196. package/src/vocabulary/list.test.tsx +195 -0
  197. package/src/vocabulary/list.tsx +48 -0
  198. package/src/vocabulary/media.test.tsx +85 -0
  199. package/src/vocabulary/media.tsx +56 -0
  200. package/src/vocabulary/text.test.tsx +73 -0
  201. package/src/vocabulary/text.tsx +59 -0
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ //#region src/vocabulary/text.d.ts
3
+ declare const textVocabulary: {
4
+ Header: {
5
+ description: string;
6
+ properties: z.ZodObject<{
7
+ text: z.ZodString;
8
+ size: z.ZodOptional<z.ZodEnum<{
9
+ sm: "sm";
10
+ md: "md";
11
+ lg: "lg";
12
+ xl: "xl";
13
+ "2xl": "2xl";
14
+ "3xl": "3xl";
15
+ }>>;
16
+ }, z.core.$strip>;
17
+ render: ({ text, size, children }: any) => import("react").JSX.Element;
18
+ };
19
+ Text: {
20
+ description: string;
21
+ properties: z.ZodObject<{
22
+ value: z.ZodString;
23
+ size: z.ZodOptional<z.ZodEnum<{
24
+ sm: "sm";
25
+ md: "md";
26
+ lg: "lg";
27
+ xl: "xl";
28
+ "2xl": "2xl";
29
+ "3xl": "3xl";
30
+ }>>;
31
+ weight: z.ZodOptional<z.ZodEnum<{
32
+ bold: "bold";
33
+ normal: "normal";
34
+ medium: "medium";
35
+ semibold: "semibold";
36
+ }>>;
37
+ color: z.ZodOptional<z.ZodEnum<{
38
+ emphasis: "emphasis";
39
+ secondary: "secondary";
40
+ "alpha-70": "alpha-70";
41
+ white: "white";
42
+ "white-70": "white-70";
43
+ "white-50": "white-50";
44
+ }>>;
45
+ }, z.core.$strip>;
46
+ streamProperties: true;
47
+ render: ({ value, size, weight, color, children }: any) => import("react").JSX.Element;
48
+ };
49
+ Caption: {
50
+ description: string;
51
+ properties: z.ZodObject<{
52
+ value: z.ZodString;
53
+ }, z.core.$strip>;
54
+ streamProperties: true;
55
+ render: ({ value, children }: any) => import("react").JSX.Element;
56
+ };
57
+ };
58
+ //#endregion
59
+ export { textVocabulary };
60
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.d.ts","names":[],"sources":["../../src/vocabulary/text.tsx"],"mappings":";;cAIa"}
@@ -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.6",
3
+ "version": "0.0.8",
4
4
  "description": "Generative UI components for assistant-ui",
5
5
  "keywords": [
6
6
  "generative-ui",
@@ -15,18 +15,26 @@
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/JSONGenerativeUI.server.d.ts",
24
- "default": "./dist/JSONGenerativeUI.server.js"
19
+ "types": "./dist/index.server.d.ts",
20
+ "default": "./dist/index.server.js"
25
21
  },
26
22
  "default": {
27
- "types": "./dist/JSONGenerativeUI.client.d.ts",
28
- "default": "./dist/JSONGenerativeUI.client.js"
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
+ },
31
+ "./slack": {
32
+ "types": "./dist/slack.d.ts",
33
+ "default": "./dist/slack.js"
34
+ },
35
+ "./teams": {
36
+ "types": "./dist/teams.d.ts",
37
+ "default": "./dist/teams.js"
30
38
  }
31
39
  },
32
40
  "main": "./dist/index.js",
@@ -38,7 +46,7 @@
38
46
  ],
39
47
  "sideEffects": false,
40
48
  "dependencies": {
41
- "assistant-stream": "^0.3.24"
49
+ "assistant-stream": "^0.3.26"
42
50
  },
43
51
  "peerDependencies": {
44
52
  "@assistant-ui/react": "^0.14.14",
@@ -56,10 +64,10 @@
56
64
  "@types/react": "^19.2.17",
57
65
  "react": "^19.2.7",
58
66
  "react-dom": "^19.2.7",
59
- "vitest": "^4.1.9",
67
+ "vitest": "^4.1.10",
60
68
  "zod": "^4.4.3",
61
- "@assistant-ui/react": "0.14.24",
62
- "@assistant-ui/x-buildutils": "0.0.16"
69
+ "@assistant-ui/react": "0.14.27",
70
+ "@assistant-ui/x-buildutils": "0.0.18"
63
71
  },
64
72
  "publishConfig": {
65
73
  "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, { status: uiStatus(status) });
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,88 @@
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; human-in-the-loop actions return a value the
14
+ * runtime uses to resume the run (see IR doc Open question #2 — the resume
15
+ * value is left as `unknown` and documented per-action). The return value is
16
+ * 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 an object keyed by field `name` for a `Form` or a `Card` with `asForm` set. 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
+ }
@@ -6,10 +6,11 @@ import type { GenerativeUILibrary } from "./types";
6
6
  /**
7
7
  * Builds the JSON schema for the `present` tool from a {@link GenerativeUILibrary}.
8
8
  *
9
- * The model produces a node `{ $type, ...props }` where `$type` selects a
10
- * component and the rest are its props. The schema is a flat object: `$type` is
11
- * an enum of the component names, every component's props are merged into one
12
- * optional bag, and `children` recurses via `$defs` so the tree can nest.
9
+ * The model produces a node `{ $type, $key?, ...props }` where `$type` selects
10
+ * a component, the optional `$key` pins a stable identity for list items that
11
+ * may reorder, and the rest are its props. The schema is a flat object: `$type`
12
+ * is an enum of the component names, every component's props are merged into
13
+ * one optional bag, and `children` recurses via `$defs` so the tree can nest.
13
14
  *
14
15
  * It is intentionally flat rather than a per-`$type` discriminated union. Tool /
15
16
  * function-call schemas (OpenAI and others) require the top-level parameters to
@@ -24,11 +25,12 @@ export function buildPresentParameters(
24
25
  ): JSONSchema7 {
25
26
  const names = Object.keys(library);
26
27
 
27
- // Merge every component's props into one optional bag. `$type`/`children` are
28
- // framework-reserved, so drop any author-declared copies. On a name clash the
29
- // first component's schema wins — props are an advisory hint here, not a
30
- // strict per-component contract.
28
+ // Merge every component's props into one optional bag. `$`-prefixed keys and
29
+ // `children` are framework-reserved (see ir.ts), so drop any author-declared
30
+ // copies. On a name clash the first component's schema wins — props are an
31
+ // advisory hint here, not a strict per-component contract.
31
32
  const props: Record<string, JSONSchema7Definition> = {};
33
+ const propOwners = new Map<string, string[]>();
32
34
  for (const name of names) {
33
35
  const propsSchema = toJSONSchema(library[name]!.properties);
34
36
  if (propsSchema.type !== "object") {
@@ -38,17 +40,23 @@ export function buildPresentParameters(
38
40
  );
39
41
  }
40
42
  for (const [key, schema] of Object.entries(propsSchema.properties ?? {})) {
41
- if (key === TYPE_KEY || key === "children") continue;
43
+ if (key.startsWith("$") || key === "children") continue;
42
44
  if (!(key in props)) {
43
45
  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
46
  }
47
+ propOwners.set(key, [...(propOwners.get(key) ?? []), name]);
48
+ }
49
+ }
50
+
51
+ if (process.env["NODE_ENV"] !== "production") {
52
+ for (const [key, owners] of propOwners) {
53
+ if (owners.length < 2) continue;
54
+ // eslint-disable-next-line no-console
55
+ console.warn(
56
+ `[@assistant-ui/react-generative-ui] Prop "${key}" is declared by ` +
57
+ `${formatComponentList(owners)}; keeping "${owners[0]}"'s schema. ` +
58
+ "Rename or align the prop type to avoid an ambiguous schema.",
59
+ );
52
60
  }
53
61
  }
54
62
 
@@ -65,6 +73,11 @@ export function buildPresentParameters(
65
73
  type: "object",
66
74
  properties: {
67
75
  [TYPE_KEY]: { type: "string", enum: names, description: typeDescription },
76
+ $key: {
77
+ description:
78
+ "Stable identity for this UI node. Use it for list items that may reorder.",
79
+ anyOf: [{ type: "string" }, { type: "number" }],
80
+ },
68
81
  ...props,
69
82
  children: { $ref: "#/$defs/children" },
70
83
  },
@@ -88,3 +101,12 @@ export function buildPresentParameters(
88
101
  $defs: { node, children },
89
102
  };
90
103
  }
104
+
105
+ function formatComponentList(names: string[]) {
106
+ if (names.length <= 2) return names.map((name) => `"${name}"`).join(" and ");
107
+
108
+ return `${names
109
+ .slice(0, -1)
110
+ .map((name) => `"${name}"`)
111
+ .join(", ")}, and "${names[names.length - 1]}"`;
112
+ }