@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.
Files changed (92) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +4 -1
  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.shared.d.ts +8 -0
  6. package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
  7. package/dist/JSONGenerativeUI.shared.js.map +1 -1
  8. package/dist/actionRegistry.d.ts +41 -0
  9. package/dist/actionRegistry.d.ts.map +1 -0
  10. package/dist/actionRegistry.js +41 -0
  11. package/dist/actionRegistry.js.map +1 -0
  12. package/dist/buildPresentParameters.js +10 -1
  13. package/dist/buildPresentParameters.js.map +1 -1
  14. package/dist/index.d.ts +6 -3
  15. package/dist/index.js +5 -2
  16. package/dist/index.server.d.ts +12 -0
  17. package/dist/index.server.js +10 -0
  18. package/dist/index.shared.d.ts +11 -0
  19. package/dist/index.shared.js +9 -0
  20. package/dist/ir.d.ts +115 -0
  21. package/dist/ir.d.ts.map +1 -0
  22. package/dist/ir.js +149 -0
  23. package/dist/ir.js.map +1 -0
  24. package/dist/renderGenerativeUI.d.ts +5 -4
  25. package/dist/renderGenerativeUI.d.ts.map +1 -1
  26. package/dist/renderGenerativeUI.js +18 -45
  27. package/dist/renderGenerativeUI.js.map +1 -1
  28. package/dist/types.d.ts +34 -30
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/vocabulary/alert.d.ts +34 -0
  31. package/dist/vocabulary/alert.d.ts.map +1 -0
  32. package/dist/vocabulary/alert.js +49 -0
  33. package/dist/vocabulary/alert.js.map +1 -0
  34. package/dist/vocabulary/data.d.ts +53 -0
  35. package/dist/vocabulary/data.d.ts.map +1 -0
  36. package/dist/vocabulary/data.js +63 -0
  37. package/dist/vocabulary/data.js.map +1 -0
  38. package/dist/vocabulary/fact.d.ts +20 -0
  39. package/dist/vocabulary/fact.d.ts.map +1 -0
  40. package/dist/vocabulary/fact.js +24 -0
  41. package/dist/vocabulary/fact.js.map +1 -0
  42. package/dist/vocabulary/index.d.ts +16 -0
  43. package/dist/vocabulary/index.d.ts.map +1 -0
  44. package/dist/vocabulary/index.js +30 -0
  45. package/dist/vocabulary/index.js.map +1 -0
  46. package/dist/vocabulary/interactive.d.ts +81 -0
  47. package/dist/vocabulary/interactive.d.ts.map +1 -0
  48. package/dist/vocabulary/interactive.js +123 -0
  49. package/dist/vocabulary/interactive.js.map +1 -0
  50. package/dist/vocabulary/layout.d.ts +78 -0
  51. package/dist/vocabulary/layout.d.ts.map +1 -0
  52. package/dist/vocabulary/layout.js +72 -0
  53. package/dist/vocabulary/layout.js.map +1 -0
  54. package/dist/vocabulary/media.d.ts +34 -0
  55. package/dist/vocabulary/media.d.ts.map +1 -0
  56. package/dist/vocabulary/media.js +32 -0
  57. package/dist/vocabulary/media.js.map +1 -0
  58. package/dist/vocabulary/text.d.ts +74 -0
  59. package/dist/vocabulary/text.d.ts.map +1 -0
  60. package/dist/vocabulary/text.js +48 -0
  61. package/dist/vocabulary/text.js.map +1 -0
  62. package/package.json +13 -13
  63. package/src/JSONGenerativeUI.client.tsx +10 -2
  64. package/src/JSONGenerativeUI.shared.ts +8 -0
  65. package/src/actionRegistry.test.ts +90 -0
  66. package/src/actionRegistry.ts +96 -0
  67. package/src/buildPresentParameters.ts +23 -7
  68. package/src/index.server.ts +2 -0
  69. package/src/index.shared.ts +58 -0
  70. package/src/index.ts +2 -26
  71. package/src/ir.test.ts +203 -0
  72. package/src/ir.ts +253 -0
  73. package/src/renderGenerativeUI.test.tsx +53 -0
  74. package/src/renderGenerativeUI.tsx +49 -82
  75. package/src/types.ts +57 -42
  76. package/src/vocabulary/alert.test.tsx +66 -0
  77. package/src/vocabulary/alert.tsx +47 -0
  78. package/src/vocabulary/data.test.tsx +67 -0
  79. package/src/vocabulary/data.tsx +86 -0
  80. package/src/vocabulary/fact.test.tsx +15 -0
  81. package/src/vocabulary/fact.tsx +22 -0
  82. package/src/vocabulary/index.test.ts +56 -0
  83. package/src/vocabulary/index.ts +27 -0
  84. package/src/vocabulary/interactive.dispatch.test.tsx +110 -0
  85. package/src/vocabulary/interactive.test.tsx +108 -0
  86. package/src/vocabulary/interactive.tsx +165 -0
  87. package/src/vocabulary/layout.test.tsx +61 -0
  88. package/src/vocabulary/layout.tsx +76 -0
  89. package/src/vocabulary/media.test.tsx +53 -0
  90. package/src/vocabulary/media.tsx +37 -0
  91. package/src/vocabulary/text.test.tsx +73 -0
  92. package/src/vocabulary/text.tsx +59 -0
package/dist/ir.js ADDED
@@ -0,0 +1,149 @@
1
+ import { TYPE_KEY } from "./constants.js";
2
+ //#region src/ir.ts
3
+ /**
4
+ * The generative-ui intermediate representation: a model-emitted, cross-platform
5
+ * UI tree. React-free, so converters and non-web runtimes consume it without
6
+ * pulling React. The flat `$type` shape is the canonical form; the legacy
7
+ * `component` shape is accepted as a backward-compatible alias.
8
+ *
9
+ * Reserved keys are partitioned off from component props so the component prop
10
+ * namespace stays fully free:
11
+ *
12
+ * - `$`-prefixed keys are framework-reserved (`$type`, `$key`, `$action`,
13
+ * `$status`). Components never declare `$`-prefixed props, so a component
14
+ * can use `type`, `status`, `variant`, etc. as ordinary props without
15
+ * colliding with the framework.
16
+ * - `children` is additionally reserved (the JSX convention).
17
+ * - every other key is an inline prop passed straight to the component.
18
+ */
19
+ const TEXT_SIZES = [
20
+ "sm",
21
+ "md",
22
+ "lg",
23
+ "xl",
24
+ "2xl",
25
+ "3xl"
26
+ ];
27
+ const IMAGE_SIZE_TOKENS = [
28
+ "sm",
29
+ "md",
30
+ "lg"
31
+ ];
32
+ const WEIGHTS = [
33
+ "normal",
34
+ "medium",
35
+ "semibold",
36
+ "bold"
37
+ ];
38
+ const COLORS = [
39
+ "emphasis",
40
+ "secondary",
41
+ "alpha-70",
42
+ "white",
43
+ "white-70",
44
+ "white-50"
45
+ ];
46
+ const ALIGNS = [
47
+ "start",
48
+ "center",
49
+ "end"
50
+ ];
51
+ const JUSTIFIES = [
52
+ "start",
53
+ "center",
54
+ "end",
55
+ "between"
56
+ ];
57
+ const BUTTON_STYLES = [
58
+ "primary",
59
+ "secondary",
60
+ "outline",
61
+ "ghost",
62
+ "danger"
63
+ ];
64
+ const ALERT_TONES = [
65
+ "info",
66
+ "success",
67
+ "warning",
68
+ "danger"
69
+ ];
70
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
71
+ const isLegacyNode = (node) => typeof node["component"] === "string";
72
+ const isTypeNode = (node) => typeof node[TYPE_KEY] === "string";
73
+ /** Bounds recursion so a runaway or adversarial model response cannot overflow
74
+ * the stack; past this depth (far beyond any real UI) we stop. */
75
+ const MAX_DEPTH = 64;
76
+ /**
77
+ * Steps a streaming partial path down into `key`. `partialPath` is the remaining
78
+ * segment of the parse meta's partial path relative to `node` (`undefined` once
79
+ * the walk leaves the partial frontier, i.e. everything below is complete).
80
+ */
81
+ function descend(partialPath, key) {
82
+ return partialPath?.[0] === key ? partialPath.slice(1) : void 0;
83
+ }
84
+ /**
85
+ * Normalizes a generative-ui input to {@link NormalizedUINode}. The flat
86
+ * `$type` shape and the legacy `component` shape both map to the same canonical
87
+ * element, with reserved keys (`$type`, `$key`, `$action`, `children`) stripped
88
+ * from the prop bag. A node that carries neither a `$type` nor a `component`
89
+ * string is not renderable and resolves to `null` rather than throwing, so a
90
+ * partially-streamed or malformed node degrades to "render nothing".
91
+ *
92
+ * `partialPath` carries streaming state from the tool-args parse meta: a node
93
+ * whose `$type` is still mid-arrival is held back (resolves to `null`) until it
94
+ * completes, and the path is threaded into `children` so a nested streaming
95
+ * node is held back while completed siblings render. Omit it for a
96
+ * non-streaming (converter) normalize.
97
+ */
98
+ function normalizeUINode(node, partialPath, depth = 0) {
99
+ if (depth > MAX_DEPTH) return null;
100
+ if (node == null || typeof node === "boolean") return null;
101
+ if (typeof node === "string" || typeof node === "number") return node;
102
+ if (Array.isArray(node)) return node.map((child, index) => normalizeUINode(child, descend(partialPath, String(index)), depth + 1));
103
+ if (!isRecord(node)) return null;
104
+ if (isTypeNode(node)) {
105
+ if (partialPath?.length === 1 && partialPath[0] === "$type") return null;
106
+ const { [TYPE_KEY]: type, $key, $action, children, ...rest } = node;
107
+ return {
108
+ type,
109
+ props: stripReservedProps(rest),
110
+ children: normalizeChildren(children, partialPath, depth),
111
+ key: $key,
112
+ action: $action
113
+ };
114
+ }
115
+ if (isLegacyNode(node)) {
116
+ const props = stripReservedProps(node.props ?? {});
117
+ return {
118
+ type: node.component,
119
+ props,
120
+ children: normalizeChildren(node.children, partialPath, depth),
121
+ key: node.key
122
+ };
123
+ }
124
+ return null;
125
+ }
126
+ function stripReservedProps(props) {
127
+ let out;
128
+ for (const key of Object.keys(props)) if (key.startsWith("$")) {
129
+ out ??= { ...props };
130
+ delete out[key];
131
+ }
132
+ return out ?? props;
133
+ }
134
+ function normalizeChildren(children, partialPath, depth) {
135
+ if (children === void 0) return void 0;
136
+ return normalizeUINode(children, descend(partialPath, "children"), depth + 1);
137
+ }
138
+ /**
139
+ * Normalizes the root of a {@link UISpec}, preserving whether the root was a
140
+ * single node or a list.
141
+ */
142
+ function normalizeSpec(spec) {
143
+ if (Array.isArray(spec)) return { root: spec.map((node) => normalizeUINode(node)) };
144
+ return { root: normalizeUINode(spec) };
145
+ }
146
+ //#endregion
147
+ export { ALERT_TONES, ALIGNS, BUTTON_STYLES, COLORS, IMAGE_SIZE_TOKENS, JUSTIFIES, TEXT_SIZES, WEIGHTS, normalizeSpec, normalizeUINode };
148
+
149
+ //# sourceMappingURL=ir.js.map
package/dist/ir.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ir.js","names":[],"sources":["../src/ir.ts"],"sourcesContent":["/**\n * The generative-ui intermediate representation: a model-emitted, cross-platform\n * UI tree. React-free, so converters and non-web runtimes consume it without\n * pulling React. The flat `$type` shape is the canonical form; the legacy\n * `component` shape is accepted as a backward-compatible alias.\n *\n * Reserved keys are partitioned off from component props so the component prop\n * namespace stays fully free:\n *\n * - `$`-prefixed keys are framework-reserved (`$type`, `$key`, `$action`,\n * `$status`). Components never declare `$`-prefixed props, so a component\n * can use `type`, `status`, `variant`, etc. as ordinary props without\n * colliding with the framework.\n * - `children` is additionally reserved (the JSX convention).\n * - every other key is an inline prop passed straight to the component.\n */\n\nimport { TYPE_KEY } from \"./constants\";\n\nexport const TEXT_SIZES = [\"sm\", \"md\", \"lg\", \"xl\", \"2xl\", \"3xl\"] as const;\nexport type TextSize = (typeof TEXT_SIZES)[number];\n\nexport const IMAGE_SIZE_TOKENS = [\"sm\", \"md\", \"lg\"] as const;\nexport type ImageSize = (typeof IMAGE_SIZE_TOKENS)[number] | number;\n\nexport const WEIGHTS = [\"normal\", \"medium\", \"semibold\", \"bold\"] as const;\nexport type Weight = (typeof WEIGHTS)[number];\n\nexport const COLORS = [\n \"emphasis\",\n \"secondary\",\n \"alpha-70\",\n \"white\",\n \"white-70\",\n \"white-50\",\n] as const;\nexport type Color = (typeof COLORS)[number];\n\nexport const ALIGNS = [\"start\", \"center\", \"end\"] as const;\nexport type Align = (typeof ALIGNS)[number];\n\nexport const JUSTIFIES = [\"start\", \"center\", \"end\", \"between\"] as const;\nexport type Justify = (typeof JUSTIFIES)[number];\n\nexport const BUTTON_STYLES = [\n \"primary\",\n \"secondary\",\n \"outline\",\n \"ghost\",\n \"danger\",\n] as const;\nexport type ButtonStyle = (typeof BUTTON_STYLES)[number];\n\nexport const ALERT_TONES = [\"info\", \"success\", \"warning\", \"danger\"] as const;\n/** Maps to ChatKit `alert` severity levels. */\nexport type AlertTone = (typeof ALERT_TONES)[number];\n\n/**\n * Behavior payload carried by an interactive node. `type` is resolved by the\n * host's action registry, not the renderer; keeping behavior as data keeps the\n * tree serializable so the same node renders on web and converts to a native\n * action id on Slack.\n */\nexport interface Action {\n readonly type: string;\n readonly [payload: string]: unknown;\n}\n\n/**\n * Anything renderable as generative UI, as the model emits it. The renderer\n * also accepts `number`, `boolean`, `null`, `undefined`, and arrays at the\n * input boundary (numbers render as text, falsy/boolean as nothing, arrays as\n * lists); {@link normalizeUINode} accepts that full range.\n */\nexport type UINode = string | number | UIElement | LegacyComponentNode;\n\nexport type UIChildren = UINode | readonly UINode[];\n\n/** The flat node shape: inline props keep the tree compact and natural for a\n * model to emit, instead of a nested `{ type, props }` bag. Reserved keys are\n * stripped before props reach the component (see the module header). */\nexport interface UIElement {\n readonly $type: string;\n readonly $key?: string | number;\n readonly children?: UIChildren;\n readonly $action?: Action;\n readonly [prop: string]: unknown;\n}\n\n/**\n * The legacy node shape: a `component` name plus a nested `props` bag. Kept\n * for backward compatibility. New code authors the flat {@link UIElement}\n * shape instead.\n */\nexport interface LegacyComponentNode {\n readonly component: string;\n readonly props?: Record<string, unknown>;\n readonly children?: UIChildren;\n readonly key?: string;\n}\n\nexport type UISpec = UINode | readonly UINode[];\n\n/**\n * A node normalized to a single canonical shape: a `type` string, an inline\n * `props` bag, recursive `children`, an optional `key`, and an optional\n * `action`. Renderers and platform converters consume this form, so they never\n * branch on whether the model emitted the flat `$type` shape or the legacy\n * `component` shape, and they never see the reserved `$`-prefixed keys leak\n * into component props.\n */\nexport interface NormalizedUIElement {\n readonly type: string;\n readonly props: Readonly<Record<string, unknown>>;\n readonly children?: NormalizedUINode | undefined;\n readonly key?: string | number | undefined;\n readonly action?: Action | undefined;\n}\n\nexport type NormalizedUINode =\n | string\n | number\n | readonly NormalizedUINode[]\n | NormalizedUIElement\n | null;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\ntype LegacyNodeRecord = { component: string } & Record<string, unknown>;\n\nconst isLegacyNode = (\n node: Record<string, unknown>,\n): node is LegacyNodeRecord => typeof node[\"component\"] === \"string\";\n\nconst isTypeNode = (node: Record<string, unknown>): node is UIElement =>\n typeof node[TYPE_KEY] === \"string\";\n\n/** Bounds recursion so a runaway or adversarial model response cannot overflow\n * the stack; past this depth (far beyond any real UI) we stop. */\nconst MAX_DEPTH = 64;\n\n/**\n * Steps a streaming partial path down into `key`. `partialPath` is the remaining\n * segment of the parse meta's partial path relative to `node` (`undefined` once\n * the walk leaves the partial frontier, i.e. everything below is complete).\n */\nfunction descend(\n partialPath: readonly string[] | undefined,\n key: string,\n): readonly string[] | undefined {\n return partialPath?.[0] === key ? partialPath.slice(1) : undefined;\n}\n\n/**\n * Normalizes a generative-ui input to {@link NormalizedUINode}. The flat\n * `$type` shape and the legacy `component` shape both map to the same canonical\n * element, with reserved keys (`$type`, `$key`, `$action`, `children`) stripped\n * from the prop bag. A node that carries neither a `$type` nor a `component`\n * string is not renderable and resolves to `null` rather than throwing, so a\n * partially-streamed or malformed node degrades to \"render nothing\".\n *\n * `partialPath` carries streaming state from the tool-args parse meta: a node\n * whose `$type` is still mid-arrival is held back (resolves to `null`) until it\n * completes, and the path is threaded into `children` so a nested streaming\n * node is held back while completed siblings render. Omit it for a\n * non-streaming (converter) normalize.\n */\nexport function normalizeUINode(\n node: unknown,\n partialPath?: readonly string[] | undefined,\n depth = 0,\n): NormalizedUINode {\n if (depth > MAX_DEPTH) return null;\n if (node == null || typeof node === \"boolean\") return null;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node))\n return node.map((child, index) =>\n normalizeUINode(child, descend(partialPath, String(index)), depth + 1),\n );\n if (!isRecord(node)) return null;\n\n // The flat `$type` shape is the canonical form; detect it first so a flat\n // node that happens to use `component` as an ordinary prop is not swallowed\n // by the legacy `component`-shape branch.\n if (isTypeNode(node)) {\n if (partialPath?.length === 1 && partialPath[0] === TYPE_KEY) return null;\n const { [TYPE_KEY]: type, $key, $action, children, ...rest } = node;\n // The `$`-prefixed namespace is framework-reserved (see the module header),\n // so any model-supplied `$`-prefixed key is stripped from the prop bag the\n // component sees. `$type`/`$key`/`$action` are pulled above; sweep the rest\n // (e.g. a stray `$status`) so it never leaks to converters or components.\n const props = stripReservedProps(rest);\n return {\n type,\n props,\n children: normalizeChildren(children, partialPath, depth),\n key: $key,\n action: $action,\n };\n }\n\n if (isLegacyNode(node)) {\n const props = stripReservedProps(\n (node.props ?? {}) as Record<string, unknown>,\n );\n return {\n type: node.component,\n props,\n children: normalizeChildren(node.children, partialPath, depth),\n key: node.key as string | undefined,\n };\n }\n\n return null;\n}\n\nfunction stripReservedProps(\n props: Record<string, unknown>,\n): Record<string, unknown> {\n let out: Record<string, unknown> | undefined;\n for (const key of Object.keys(props)) {\n if (key.startsWith(\"$\")) {\n out ??= { ...props };\n delete out[key];\n }\n }\n return out ?? props;\n}\n\nfunction normalizeChildren(\n children: unknown,\n partialPath: readonly string[] | undefined,\n depth: number,\n): NormalizedUINode | undefined {\n if (children === undefined) return undefined;\n return normalizeUINode(children, descend(partialPath, \"children\"), depth + 1);\n}\n\n/**\n * Normalizes the root of a {@link UISpec}, preserving whether the root was a\n * single node or a list.\n */\nexport function normalizeSpec(spec: UISpec): {\n readonly root: NormalizedUINode | readonly NormalizedUINode[];\n} {\n if (Array.isArray(spec)) {\n return {\n root: (spec as readonly UINode[]).map((node) => normalizeUINode(node)),\n };\n }\n return { root: normalizeUINode(spec) };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,MAAa,aAAa;CAAC;CAAM;CAAM;CAAM;CAAM;CAAO;AAAK;AAG/D,MAAa,oBAAoB;CAAC;CAAM;CAAM;AAAI;AAGlD,MAAa,UAAU;CAAC;CAAU;CAAU;CAAY;AAAM;AAG9D,MAAa,SAAS;CACpB;CACA;CACA;CACA;CACA;CACA;AACF;AAGA,MAAa,SAAS;CAAC;CAAS;CAAU;AAAK;AAG/C,MAAa,YAAY;CAAC;CAAS;CAAU;CAAO;AAAS;AAG7D,MAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;AACF;AAGA,MAAa,cAAc;CAAC;CAAQ;CAAW;CAAW;AAAQ;AAyElE,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAIrE,MAAM,gBACJ,SAC6B,OAAO,KAAK,iBAAiB;AAE5D,MAAM,cAAc,SAClB,OAAO,KAAK,cAAc;;;AAI5B,MAAM,YAAY;;;;;;AAOlB,SAAS,QACP,aACA,KAC+B;CAC/B,OAAO,cAAc,OAAO,MAAM,YAAY,MAAM,CAAC,IAAI,KAAA;AAC3D;;;;;;;;;;;;;;;AAgBA,SAAgB,gBACd,MACA,aACA,QAAQ,GACU;CAClB,IAAI,QAAQ,WAAW,OAAO;CAC9B,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GACpB,OAAO,KAAK,KAAK,OAAO,UACtB,gBAAgB,OAAO,QAAQ,aAAa,OAAO,KAAK,CAAC,GAAG,QAAQ,CAAC,CACvE;CACF,IAAI,CAAC,SAAS,IAAI,GAAG,OAAO;CAK5B,IAAI,WAAW,IAAI,GAAG;EACpB,IAAI,aAAa,WAAW,KAAK,YAAY,OAAA,SAAiB,OAAO;EACrE,MAAM,GAAG,WAAW,MAAM,MAAM,SAAS,UAAU,GAAG,SAAS;EAM/D,OAAO;GACL;GACA,OAHY,mBAAmB,IAG3B;GACJ,UAAU,kBAAkB,UAAU,aAAa,KAAK;GACxD,KAAK;GACL,QAAQ;EACV;CACF;CAEA,IAAI,aAAa,IAAI,GAAG;EACtB,MAAM,QAAQ,mBACX,KAAK,SAAS,CAAC,CAClB;EACA,OAAO;GACL,MAAM,KAAK;GACX;GACA,UAAU,kBAAkB,KAAK,UAAU,aAAa,KAAK;GAC7D,KAAK,KAAK;EACZ;CACF;CAEA,OAAO;AACT;AAEA,SAAS,mBACP,OACyB;CACzB,IAAI;CACJ,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GACjC,IAAI,IAAI,WAAW,GAAG,GAAG;EACvB,QAAQ,EAAE,GAAG,MAAM;EACnB,OAAO,IAAI;CACb;CAEF,OAAO,OAAO;AAChB;AAEA,SAAS,kBACP,UACA,aACA,OAC8B;CAC9B,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;CACnC,OAAO,gBAAgB,UAAU,QAAQ,aAAa,UAAU,GAAG,QAAQ,CAAC;AAC9E;;;;;AAMA,SAAgB,cAAc,MAE5B;CACA,IAAI,MAAM,QAAQ,IAAI,GACpB,OAAO,EACL,MAAO,KAA2B,KAAK,SAAS,gBAAgB,IAAI,CAAC,EACvE;CAEF,OAAO,EAAE,MAAM,gBAAgB,IAAI,EAAE;AACvC"}
@@ -6,10 +6,11 @@ import { ReactNode } from "react";
6
6
  * Renders a generative-ui tree against a {@link GenerativeUILibrary}.
7
7
  *
8
8
  * The model emits each node as a flat object `{ $type, ...props }`. We first
9
- * normalize that wire form into React-shaped elements (`{ type, props }`), then
10
- * render: each `type` is looked up in the library and its `props` are passed
11
- * to the component's `render(props, context)`, with `children` rendered
12
- * recursively so components can nest.
9
+ * normalize that wire form into the canonical {@link NormalizedUINode} (with
10
+ * `children` lifted to a reserved top-level key), then render: each `type` is
11
+ * looked up in the library and its `props` are passed to the component's
12
+ * `render(props, context)`, with `children` rendered recursively so components
13
+ * can nest.
13
14
  */
14
15
  declare function renderGenerativeUI(node: unknown, library: GenerativeUILibrary, context?: GenerativeUIRenderContext): ReactNode;
15
16
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"renderGenerativeUI.d.ts","names":[],"sources":["../src/renderGenerativeUI.tsx"],"mappings":";;;;;;AAqBA;;;;;;;iBAAgB,kBAAA,CACd,IAAA,WACA,OAAA,EAAS,mBAAA,EACT,OAAA,GAAS,yBAAA,GACR,SAAA"}
1
+ {"version":3,"file":"renderGenerativeUI.d.ts","names":[],"sources":["../src/renderGenerativeUI.tsx"],"mappings":";;;;;;AAwBA;;;;;;;;iBAAgB,kBAAA,CACd,IAAA,WACA,OAAA,EAAS,mBAAA,EACT,OAAA,GAAS,yBAAA,GACR,SAAA"}
@@ -1,60 +1,29 @@
1
- import { TYPE_KEY } from "./constants.js";
1
+ import { normalizeUINode } from "./ir.js";
2
2
  import { getPartialJsonObjectMeta } from "assistant-stream/utils";
3
3
  import { Fragment } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/renderGenerativeUI.tsx
6
6
  const DEFAULT_CONTEXT = { status: "done" };
7
+ const isElement = (node) => typeof node === "object" && node !== null && !Array.isArray(node);
7
8
  /**
8
9
  * Renders a generative-ui tree against a {@link GenerativeUILibrary}.
9
10
  *
10
11
  * The model emits each node as a flat object `{ $type, ...props }`. We first
11
- * normalize that wire form into React-shaped elements (`{ type, props }`), then
12
- * render: each `type` is looked up in the library and its `props` are passed
13
- * to the component's `render(props, context)`, with `children` rendered
14
- * recursively so components can nest.
12
+ * normalize that wire form into the canonical {@link NormalizedUINode} (with
13
+ * `children` lifted to a reserved top-level key), then render: each `type` is
14
+ * looked up in the library and its `props` are passed to the component's
15
+ * `render(props, context)`, with `children` rendered recursively so components
16
+ * can nest.
15
17
  */
16
18
  function renderGenerativeUI(node, library, context = DEFAULT_CONTEXT) {
17
19
  const meta = getPartialJsonObjectMeta(node);
18
- return renderNode(normalizeNode(node, meta?.state === "partial" ? meta.partialPath : void 0), library, context);
19
- }
20
- /**
21
- * The deepest tree we normalize. The input comes from the model, so a runaway
22
- * or adversarial response could nest arbitrarily deep and overflow the stack;
23
- * past this depth we stop (far beyond any real UI). Bounding normalization
24
- * bounds rendering too, since it only walks the normalized tree.
25
- */
26
- const MAX_DEPTH = 64;
27
- /**
28
- * Converts the flat wire form into a normalized {@link GenerativeUINode}.
29
- *
30
- * `partialPath` is the remaining segment of the parse meta's partial path
31
- * relative to `node` (`undefined` once the walk leaves the partial frontier,
32
- * i.e. everything below is complete).
33
- */
34
- function normalizeNode(node, partialPath, depth = 0) {
35
- if (depth > MAX_DEPTH) return null;
36
- if (node == null || typeof node === "boolean") return null;
37
- if (typeof node === "string" || typeof node === "number") return node;
38
- if (Array.isArray(node)) return node.map((child, index) => normalizeNode(child, descend(partialPath, String(index)), depth + 1));
39
- if (typeof node !== "object") return null;
40
- const { [TYPE_KEY]: type, ...props } = node;
41
- if (typeof type !== "string") return null;
42
- if (partialPath?.length === 1 && partialPath[0] === "$type") return null;
43
- if ("children" in props) props["children"] = normalizeNode(props["children"], descend(partialPath, "children"), depth + 1);
44
- return {
45
- type,
46
- props
47
- };
48
- }
49
- /** Steps the partial path down into `key`; siblings of the path are complete. */
50
- function descend(partialPath, key) {
51
- return partialPath?.[0] === key ? partialPath.slice(1) : void 0;
20
+ return renderNode(normalizeUINode(node, meta?.state === "partial" ? meta.partialPath : void 0), library, context);
52
21
  }
53
22
  function renderNode(node, library, context) {
54
23
  if (node == null || typeof node === "boolean") return null;
55
24
  if (typeof node === "string" || typeof node === "number") return node;
56
- if (Array.isArray(node)) return node.map((child, index) => /* @__PURE__ */ jsx(Fragment, { children: renderNode(child, library, context) }, `${index}:${nodeKind(child)}`));
57
- return renderElement(node, library, context);
25
+ if (Array.isArray(node)) return node.map((child, index) => /* @__PURE__ */ jsx(Fragment, { children: renderNode(child, library, context) }, nodeKey(child, index)));
26
+ return isElement(node) ? renderElement(node, library, context) : null;
58
27
  }
59
28
  function renderElement(element, library, context) {
60
29
  const entry = library[element.type];
@@ -63,12 +32,13 @@ function renderElement(element, library, context) {
63
32
  return null;
64
33
  }
65
34
  if (!entry.streamProperties && context.status === "streaming") return null;
66
- const { children, ...rest } = element.props;
67
35
  const props = {
68
- ...rest,
36
+ ...element.props,
69
37
  $status: context.status
70
38
  };
71
- if (children !== void 0) props["children"] = renderNode(children, library, context);
39
+ if (context.dispatch !== void 0) props["$dispatch"] = context.dispatch;
40
+ if (element.action !== void 0) props["$action"] = element.action;
41
+ if (element.children !== void 0) props["children"] = renderNode(element.children, library, context);
72
42
  return /* @__PURE__ */ jsx(GenerativeUIComponentRenderer, {
73
43
  render: entry.render,
74
44
  props
@@ -87,7 +57,10 @@ function nodeKind(node) {
87
57
  if (node == null || typeof node === "boolean") return "";
88
58
  if (typeof node === "string" || typeof node === "number") return "#text";
89
59
  if (Array.isArray(node)) return "#array";
90
- return node.type;
60
+ return isElement(node) ? node.type : "";
61
+ }
62
+ function nodeKey(node, index) {
63
+ return isElement(node) && (typeof node.key === "string" || typeof node.key === "number") ? `model:${node.key}` : `${index}:${nodeKind(node)}`;
91
64
  }
92
65
  function reportUnknownComponent(type, available) {
93
66
  if (process.env["NODE_ENV"] !== "production") console.error(`[@assistant-ui/react-generative-ui] Unknown component "${type}". Available components: ${available.join(", ") || "(none)"}.`);
@@ -1 +1 @@
1
- {"version":3,"file":"renderGenerativeUI.js","names":[],"sources":["../src/renderGenerativeUI.tsx"],"sourcesContent":["import { getPartialJsonObjectMeta } from \"assistant-stream/utils\";\nimport { Fragment, type ReactNode } from \"react\";\nimport { TYPE_KEY } from \"./constants\";\nimport type {\n GenerativeUIElement,\n GenerativeUILibrary,\n GenerativeUINode,\n GenerativeUIRenderContext,\n} from \"./types\";\n\nconst DEFAULT_CONTEXT: GenerativeUIRenderContext = { status: \"done\" };\n\n/**\n * Renders a generative-ui tree against a {@link GenerativeUILibrary}.\n *\n * The model emits each node as a flat object `{ $type, ...props }`. We first\n * normalize that wire form into React-shaped elements (`{ type, props }`), then\n * render: each `type` is looked up in the library and its `props` are passed\n * to the component's `render(props, context)`, with `children` rendered\n * recursively so components can nest.\n */\nexport function renderGenerativeUI(\n node: unknown,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext = DEFAULT_CONTEXT,\n): ReactNode {\n // Tool args are parsed incrementally, and the parse meta records which path\n // is still mid-arrival, so normalization can hold back a node whose `$type`\n // string has not finished streaming.\n const meta = getPartialJsonObjectMeta(node as Record<symbol, unknown>);\n const partialPath = meta?.state === \"partial\" ? meta.partialPath : undefined;\n return renderNode(normalizeNode(node, partialPath), library, context);\n}\n\n/**\n * The deepest tree we normalize. The input comes from the model, so a runaway\n * or adversarial response could nest arbitrarily deep and overflow the stack;\n * past this depth we stop (far beyond any real UI). Bounding normalization\n * bounds rendering too, since it only walks the normalized tree.\n */\nconst MAX_DEPTH = 64;\n\n/**\n * Converts the flat wire form into a normalized {@link GenerativeUINode}.\n *\n * `partialPath` is the remaining segment of the parse meta's partial path\n * relative to `node` (`undefined` once the walk leaves the partial frontier,\n * i.e. everything below is complete).\n */\nfunction normalizeNode(\n node: unknown,\n partialPath: readonly string[] | undefined,\n depth = 0,\n): GenerativeUINode {\n if (depth > MAX_DEPTH) return null;\n if (node == null || typeof node === \"boolean\") return null;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node))\n return node.map((child, index) =>\n normalizeNode(child, descend(partialPath, String(index)), depth + 1),\n );\n if (typeof node !== \"object\") return null;\n\n const { [TYPE_KEY]: type, ...props } = node as Record<string, unknown>;\n // Args stream in incrementally; a node whose `$type` has not arrived yet\n // (or whose `$type` string is still mid-arrival) is not an error, it just\n // isn't renderable.\n if (typeof type !== \"string\") return null;\n if (partialPath?.length === 1 && partialPath[0] === TYPE_KEY) return null;\n\n if (\"children\" in props) {\n props[\"children\"] = normalizeNode(\n props[\"children\"],\n descend(partialPath, \"children\"),\n depth + 1,\n );\n }\n return { type, props } as GenerativeUIElement;\n}\n\n/** Steps the partial path down into `key`; siblings of the path are complete. */\nfunction descend(\n partialPath: readonly string[] | undefined,\n key: string,\n): readonly string[] | undefined {\n return partialPath?.[0] === key ? partialPath.slice(1) : undefined;\n}\n\nfunction renderNode(\n node: GenerativeUINode,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n if (node == null || typeof node === \"boolean\") return null;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node)) {\n // The wire format has no per-node id, so the key is positional. Pairing the\n // index with the node's kind means that when the model splices or reorders\n // `children` and the kind at an index changes, the key changes and React\n // remounts instead of handing a streaming node's hook state to a different\n // component.\n return node.map((child, index) => (\n <Fragment key={`${index}:${nodeKind(child)}`}>\n {renderNode(child, library, context)}\n </Fragment>\n ));\n }\n return renderElement(node, library, context);\n}\n\nfunction renderElement(\n element: GenerativeUIElement,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n const entry = library[element.type];\n if (!entry) {\n reportUnknownComponent(element.type, Object.keys(library));\n return null;\n }\n\n // Components that opt out of prop streaming wait until their props are\n // complete rather than rendering from a partial parse.\n if (!entry.streamProperties && context.status === \"streaming\") return null;\n\n // Inject the framework props last so the model can never override them.\n const { children, ...rest } = element.props;\n const props: Record<string, unknown> = { ...rest, $status: context.status };\n if (children !== undefined) {\n props[\"children\"] = renderNode(children, library, context);\n }\n\n return <GenerativeUIComponentRenderer render={entry.render} props={props} />;\n}\n\n/**\n * Mounts a single node's `render` on its own fiber so the function may use\n * hooks and hold state independently of its siblings and parent.\n */\nfunction GenerativeUIComponentRenderer({\n render,\n props,\n}: {\n render: (props: any) => ReactNode;\n props: Record<string, unknown>;\n}): ReactNode {\n return render(props);\n}\n\n/** A coarse kind tag for a child, used in its list key so a node changing kind\n * at a given index forces a remount rather than a wrong-fiber reuse. */\nfunction nodeKind(node: GenerativeUINode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return \"#text\";\n if (Array.isArray(node)) return \"#array\";\n return node.type;\n}\n\nfunction reportUnknownComponent(type: string, available: string[]): void {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.error(\n `[@assistant-ui/react-generative-ui] Unknown component \"${type}\". ` +\n `Available components: ${available.join(\", \") || \"(none)\"}.`,\n );\n }\n}\n"],"mappings":";;;;;AAUA,MAAM,kBAA6C,EAAE,QAAQ,OAAO;;;;;;;;;;AAWpE,SAAgB,mBACd,MACA,SACA,UAAqC,iBAC1B;CAIX,MAAM,OAAO,yBAAyB,IAA+B;CAErE,OAAO,WAAW,cAAc,MADZ,MAAM,UAAU,YAAY,KAAK,cAAc,KAAA,CAClB,GAAG,SAAS,OAAO;AACtE;;;;;;;AAQA,MAAM,YAAY;;;;;;;;AASlB,SAAS,cACP,MACA,aACA,QAAQ,GACU;CAClB,IAAI,QAAQ,WAAW,OAAO;CAC9B,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GACpB,OAAO,KAAK,KAAK,OAAO,UACtB,cAAc,OAAO,QAAQ,aAAa,OAAO,KAAK,CAAC,GAAG,QAAQ,CAAC,CACrE;CACF,IAAI,OAAO,SAAS,UAAU,OAAO;CAErC,MAAM,GAAG,WAAW,MAAM,GAAG,UAAU;CAIvC,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,IAAI,aAAa,WAAW,KAAK,YAAY,OAAA,SAAiB,OAAO;CAErE,IAAI,cAAc,OAChB,MAAM,cAAc,cAClB,MAAM,aACN,QAAQ,aAAa,UAAU,GAC/B,QAAQ,CACV;CAEF,OAAO;EAAE;EAAM;CAAM;AACvB;;AAGA,SAAS,QACP,aACA,KAC+B;CAC/B,OAAO,cAAc,OAAO,MAAM,YAAY,MAAM,CAAC,IAAI,KAAA;AAC3D;AAEA,SAAS,WACP,MACA,SACA,SACW;CACX,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAMpB,OAAO,KAAK,KAAK,OAAO,UACtB,oBAAC,UAAD,EAAA,UACG,WAAW,OAAO,SAAS,OAAO,EAC3B,GAFK,GAAG,MAAM,GAAG,SAAS,KAAK,GAE/B,CACX;CAEH,OAAO,cAAc,MAAM,SAAS,OAAO;AAC7C;AAEA,SAAS,cACP,SACA,SACA,SACW;CACX,MAAM,QAAQ,QAAQ,QAAQ;CAC9B,IAAI,CAAC,OAAO;EACV,uBAAuB,QAAQ,MAAM,OAAO,KAAK,OAAO,CAAC;EACzD,OAAO;CACT;CAIA,IAAI,CAAC,MAAM,oBAAoB,QAAQ,WAAW,aAAa,OAAO;CAGtE,MAAM,EAAE,UAAU,GAAG,SAAS,QAAQ;CACtC,MAAM,QAAiC;EAAE,GAAG;EAAM,SAAS,QAAQ;CAAO;CAC1E,IAAI,aAAa,KAAA,GACf,MAAM,cAAc,WAAW,UAAU,SAAS,OAAO;CAG3D,OAAO,oBAAC,+BAAD;EAA+B,QAAQ,MAAM;EAAe;CAAQ,CAAA;AAC7E;;;;;AAMA,SAAS,8BAA8B,EACrC,QACA,SAIY;CACZ,OAAO,OAAO,KAAK;AACrB;;;AAIA,SAAS,SAAS,MAAgC;CAChD,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;CAChC,OAAO,KAAK;AACd;AAEA,SAAS,uBAAuB,MAAc,WAA2B;CACvE,IAAI,QAAQ,IAAI,gBAAgB,cAE9B,QAAQ,MACN,0DAA0D,KAAK,2BACpC,UAAU,KAAK,IAAI,KAAK,SAAS,EAC9D;AAEJ"}
1
+ {"version":3,"file":"renderGenerativeUI.js","names":[],"sources":["../src/renderGenerativeUI.tsx"],"sourcesContent":["import { getPartialJsonObjectMeta } from \"assistant-stream/utils\";\nimport { Fragment, type ReactNode } from \"react\";\nimport {\n normalizeUINode,\n type NormalizedUIElement,\n type NormalizedUINode,\n} from \"./ir\";\nimport type { GenerativeUILibrary, GenerativeUIRenderContext } from \"./types\";\n\nconst DEFAULT_CONTEXT: GenerativeUIRenderContext = { status: \"done\" };\n\nconst isElement = (node: NormalizedUINode): node is NormalizedUIElement =>\n typeof node === \"object\" && node !== null && !Array.isArray(node);\n\n/**\n * Renders a generative-ui tree against a {@link GenerativeUILibrary}.\n *\n * The model emits each node as a flat object `{ $type, ...props }`. We first\n * normalize that wire form into the canonical {@link NormalizedUINode} (with\n * `children` lifted to a reserved top-level key), then render: each `type` is\n * looked up in the library and its `props` are passed to the component's\n * `render(props, context)`, with `children` rendered recursively so components\n * can nest.\n */\nexport function renderGenerativeUI(\n node: unknown,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext = DEFAULT_CONTEXT,\n): ReactNode {\n // Tool args are parsed incrementally, and the parse meta records which path\n // is still mid-arrival, so normalization can hold back a node whose `$type`\n // string has not finished streaming.\n const meta = getPartialJsonObjectMeta(node as Record<symbol, unknown>);\n const partialPath = meta?.state === \"partial\" ? meta.partialPath : undefined;\n return renderNode(normalizeUINode(node, partialPath), library, context);\n}\n\nfunction renderNode(\n node: NormalizedUINode,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n if (node == null || typeof node === \"boolean\") return null;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node)) {\n // Use a model-provided stable key when present. Otherwise, keep the\n // positional fallback: pairing the index with the node's kind means that\n // when the kind at an index changes, React remounts instead of handing a\n // streaming node's hook state to a different component.\n return node.map((child, index) => (\n <Fragment key={nodeKey(child, index)}>\n {renderNode(child, library, context)}\n </Fragment>\n ));\n }\n return isElement(node) ? renderElement(node, library, context) : null;\n}\n\nfunction renderElement(\n element: NormalizedUIElement,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n const entry = library[element.type];\n if (!entry) {\n reportUnknownComponent(element.type, Object.keys(library));\n return null;\n }\n\n // Components that opt out of prop streaming wait until their props are\n // complete rather than rendering from a partial parse.\n if (!entry.streamProperties && context.status === \"streaming\") return null;\n\n // `children` is a reserved top-level key on the normalized element, not a\n // prop, so it is not in `props`. Inject the framework props last so the model\n // can never override them. `$action` is likewise reserved and stripped from\n // the prop bag during normalization, so it is re-injected here for components\n // that carry behavior (e.g. `Button`).\n const props: Record<string, unknown> = {\n ...element.props,\n $status: context.status,\n };\n if (context.dispatch !== undefined) {\n props[\"$dispatch\"] = context.dispatch;\n }\n if (element.action !== undefined) {\n props[\"$action\"] = element.action;\n }\n if (element.children !== undefined) {\n props[\"children\"] = renderNode(element.children, library, context);\n }\n\n return <GenerativeUIComponentRenderer render={entry.render} props={props} />;\n}\n\n/**\n * Mounts a single node's `render` on its own fiber so the function may use\n * hooks and hold state independently of its siblings and parent.\n */\nfunction GenerativeUIComponentRenderer({\n render,\n props,\n}: {\n render: (props: any) => ReactNode;\n props: Record<string, unknown>;\n}): ReactNode {\n return render(props);\n}\n\n/** A coarse kind tag for a child, used in its list key so a node changing kind\n * at a given index forces a remount rather than a wrong-fiber reuse. */\nfunction nodeKind(node: NormalizedUINode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return \"#text\";\n if (Array.isArray(node)) return \"#array\";\n return isElement(node) ? node.type : \"\";\n}\n\nfunction nodeKey(node: NormalizedUINode, index: number): string {\n return isElement(node) &&\n (typeof node.key === \"string\" || typeof node.key === \"number\")\n ? `model:${node.key}`\n : `${index}:${nodeKind(node)}`;\n}\n\nfunction reportUnknownComponent(type: string, available: string[]): void {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.error(\n `[@assistant-ui/react-generative-ui] Unknown component \"${type}\". ` +\n `Available components: ${available.join(\", \") || \"(none)\"}.`,\n );\n }\n}\n"],"mappings":";;;;;AASA,MAAM,kBAA6C,EAAE,QAAQ,OAAO;AAEpE,MAAM,aAAa,SACjB,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI;;;;;;;;;;;AAYlE,SAAgB,mBACd,MACA,SACA,UAAqC,iBAC1B;CAIX,MAAM,OAAO,yBAAyB,IAA+B;CAErE,OAAO,WAAW,gBAAgB,MADd,MAAM,UAAU,YAAY,KAAK,cAAc,KAAA,CAChB,GAAG,SAAS,OAAO;AACxE;AAEA,SAAS,WACP,MACA,SACA,SACW;CACX,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAKpB,OAAO,KAAK,KAAK,OAAO,UACtB,oBAAC,UAAD,EAAA,UACG,WAAW,OAAO,SAAS,OAAO,EAC3B,GAFK,QAAQ,OAAO,KAAK,CAEzB,CACX;CAEH,OAAO,UAAU,IAAI,IAAI,cAAc,MAAM,SAAS,OAAO,IAAI;AACnE;AAEA,SAAS,cACP,SACA,SACA,SACW;CACX,MAAM,QAAQ,QAAQ,QAAQ;CAC9B,IAAI,CAAC,OAAO;EACV,uBAAuB,QAAQ,MAAM,OAAO,KAAK,OAAO,CAAC;EACzD,OAAO;CACT;CAIA,IAAI,CAAC,MAAM,oBAAoB,QAAQ,WAAW,aAAa,OAAO;CAOtE,MAAM,QAAiC;EACrC,GAAG,QAAQ;EACX,SAAS,QAAQ;CACnB;CACA,IAAI,QAAQ,aAAa,KAAA,GACvB,MAAM,eAAe,QAAQ;CAE/B,IAAI,QAAQ,WAAW,KAAA,GACrB,MAAM,aAAa,QAAQ;CAE7B,IAAI,QAAQ,aAAa,KAAA,GACvB,MAAM,cAAc,WAAW,QAAQ,UAAU,SAAS,OAAO;CAGnE,OAAO,oBAAC,+BAAD;EAA+B,QAAQ,MAAM;EAAe;CAAQ,CAAA;AAC7E;;;;;AAMA,SAAS,8BAA8B,EACrC,QACA,SAIY;CACZ,OAAO,OAAO,KAAK;AACrB;;;AAIA,SAAS,SAAS,MAAgC;CAChD,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;CAChC,OAAO,UAAU,IAAI,IAAI,KAAK,OAAO;AACvC;AAEA,SAAS,QAAQ,MAAwB,OAAuB;CAC9D,OAAO,UAAU,IAAI,MAClB,OAAO,KAAK,QAAQ,YAAY,OAAO,KAAK,QAAQ,YACnD,SAAS,KAAK,QACd,GAAG,MAAM,GAAG,SAAS,IAAI;AAC/B;AAEA,SAAS,uBAAuB,MAAc,WAA2B;CACvE,IAAI,QAAQ,IAAI,gBAAgB,cAE9B,QAAQ,MACN,0DAA0D,KAAK,2BACpC,UAAU,KAAK,IAAI,KAAK,SAAS,EAC9D;AAEJ"}
package/dist/types.d.ts CHANGED
@@ -1,31 +1,60 @@
1
+ import { Action, NormalizedUIElement } from "./ir.js";
1
2
  import { ReactNode } from "react";
2
3
  import { ZodType } from "zod";
3
4
 
4
5
  //#region src/types.d.ts
6
+ /**
7
+ * The canonical generative-ui element, normalized from the flat `$type` shape
8
+ * or the legacy `component` shape (see {@link normalizeUINode}). `children` is
9
+ * a reserved top-level key, not a prop, so it lives here rather than inside
10
+ * `props`. Alias of {@link NormalizedUIElement}; kept under its old name so the
11
+ * package's public surface stays append-only.
12
+ */
13
+ type GenerativeUIElement = NormalizedUIElement;
14
+ type GenerativeUIProps = NormalizedUIElement["props"];
15
+ /**
16
+ * Anything renderable as generative UI — mirrors React's `ReactNode`: an
17
+ * element, primitive text, a list of nodes, or nothing. Widened beyond the
18
+ * model payload (a {@link GenerativeUIElement}) with the renderer's
19
+ * null/boolean/undefined inputs.
20
+ */
21
+ type GenerativeUINode = GenerativeUIElement | string | number | boolean | null | undefined | GenerativeUINode[];
22
+ type GenerativeUIAction = Action;
5
23
  /** Whether a node's props are still streaming in or have fully arrived. */
6
24
  type GenerativeUIStatus = "streaming" | "done";
25
+ /** The dispatcher an interactive component calls to fire its `$action`. */
26
+ type GenerativeUIDispatch = (action: Action) => unknown;
7
27
  /** The render context threaded through {@link renderGenerativeUI}. */
8
28
  type GenerativeUIRenderContext = {
9
29
  /** Whether the tool call's arguments are still streaming or are complete. */status: GenerativeUIStatus;
30
+ dispatch?: GenerativeUIDispatch;
10
31
  };
11
32
  /**
12
33
  * Props a component's `render` receives: its model props, rendered `children`,
13
- * and the injected `$status`. `$status` is the discriminant — when it is
14
- * `"done"`, `P` is complete; while `"streaming"`, `P` is partial. It is named
15
- * `$status` (not `status`) so it never collides with a real `status` prop, the
16
- * same reservation as `$type`.
34
+ * and the injected framework keys. `$status` is the discriminant — when it is
35
+ * `"done"`, `P` is complete; while `"streaming"`, `P` is partial. `$action` is
36
+ * the node's behavior payload, present only on interactive nodes. `$dispatch`
37
+ * fires that payload through the host's {@link ActionRegistry}; it is present
38
+ * only when a registry was wired. All three are `$`-prefixed so they never
39
+ * collide with a real `status`/`action`/`dispatch` prop.
17
40
  */
18
41
  type StreamingRenderProps<P> = (Partial<P> & {
19
42
  children?: ReactNode;
20
43
  $status: "streaming";
44
+ $action?: Action;
45
+ $dispatch?: GenerativeUIDispatch;
21
46
  }) | (P & {
22
47
  children?: ReactNode;
23
48
  $status: "done";
49
+ $action?: Action;
50
+ $dispatch?: GenerativeUIDispatch;
24
51
  });
25
52
  /** Props for a component that only renders once complete — `$status` is always `"done"`. */
26
53
  type StaticRenderProps<P> = P & {
27
54
  children?: ReactNode;
28
55
  $status: "done";
56
+ $action?: Action;
57
+ $dispatch?: GenerativeUIDispatch;
29
58
  };
30
59
  /**
31
60
  * A component the model is allowed to render, with the schema for its props.
@@ -64,31 +93,6 @@ type GenerativeUIComponent<P = any> = {
64
93
  * This registry is the security boundary — any `type` not present is rejected.
65
94
  */
66
95
  type GenerativeUILibrary = Record<string, GenerativeUIComponent>;
67
- /**
68
- * A component invocation — mirrors React's `ReactElement`.
69
- *
70
- * `type` selects a component from the {@link GenerativeUILibrary}; `props` are
71
- * passed to it. The `children` prop is special: it is itself a renderable
72
- * {@link GenerativeUINode}, drawn as generative UI rather than passed as data.
73
- *
74
- * On the wire the model emits the flattened form `{ $type, ...props }`, where
75
- * `$type` names the component so a real `type` prop never collides. This is the
76
- * normalized shape the renderer works with, the same way React normalizes
77
- * `createElement` arguments into an element.
78
- */
79
- type GenerativeUIElement = {
80
- type: string;
81
- props: GenerativeUIProps;
82
- };
83
- /** Props passed to a component — mirrors a React component's props. */
84
- type GenerativeUIProps = {
85
- children?: GenerativeUINode;
86
- } & Record<string, unknown>;
87
- /**
88
- * Anything renderable as generative UI — mirrors React's `ReactNode`: an
89
- * element, primitive text, or a list of nodes.
90
- */
91
- type GenerativeUINode = GenerativeUIElement | string | number | boolean | null | undefined | GenerativeUINode[];
92
96
  //#endregion
93
- export { GenerativeUIComponent, GenerativeUIElement, GenerativeUILibrary, GenerativeUINode, GenerativeUIProps, GenerativeUIRenderContext, GenerativeUIStatus };
97
+ export { GenerativeUIAction, GenerativeUIComponent, GenerativeUIDispatch, GenerativeUIElement, GenerativeUILibrary, GenerativeUINode, GenerativeUIProps, GenerativeUIRenderContext, GenerativeUIStatus };
94
98
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAIY,kBAAA;AAAZ;AAAA,KAGY,yBAAA;+EAEV,MAAA,EAAQ,kBAAkB;AAAA;AAF5B;;;;AAE4B;AAC1B;;AAHF,KAYK,oBAAA,OACA,OAAA,CAAQ,CAAA;EAAO,QAAA,GAAW,SAAA;EAAW,OAAA;AAAA,MACrC,CAAA;EAAM,QAAA,GAAW,SAAA;EAAW,OAAA;AAAA;;KAG5B,iBAAA,MAAuB,CAAA;EAAM,QAAA,GAAW,SAAS;EAAE,OAAA;AAAA;;;;;;;;AAHhB;AAAA;;;;KAiB5B,qBAAA;EAdgB,oFAiBtB,WAAA,UAjBuC;EAmBvC,UAAA,EAAY,OAAA,CAAQ,CAAA;EAnBqC;AAAA;AAc/D;;;;EAYM,gBAAA;EACA,MAAA,GAAS,KAAA,EAAO,oBAAA,CAAqB,CAAA,MAAO,SAAA;AAAA;EAG5C,WAAA;EACA,UAAA,EAAY,OAAA,CAAQ,CAAA,GAAR;EAEZ,gBAAA;EACA,MAAA,GAAS,KAAA,EAAO,iBAAA,CAAkB,CAAA,MAAO,SAAA;AAAA;;;;;;;;KAUnC,mBAAA,GAAsB,MAAM,SAAS,qBAAA;;;;;;;;;;;;;KAcrC,mBAAA;EACV,IAAA;EACA,KAAA,EAAO,iBAAiB;AAAA;AA1B8B;AAAA,KA8B5C,iBAAA;EACV,QAAA,GAAW,gBAAA;AAAA,IACT,MAAM;;AAtB4D;AActE;;KAcY,gBAAA,GACR,mBAAA,kDAMA,gBAAgB"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;AAWA;;;;AAAqD;KAAzC,mBAAA,GAAsB,mBAAmB;AAAA,KAEzC,iBAAA,GAAoB,mBAAmB;;;AAAA;AAQnD;;;KAAY,gBAAA,GACR,mBAAA,kDAMA,gBAAgB;AAAA,KAER,kBAAA,GAAqB,MAAM;AAAvC;AAAA,KAGY,kBAAA;;KAGA,oBAAA,IAAwB,MAAc,EAAN,MAAM;AANX;AAAA,KAS3B,yBAAA;EANkB,6EAQ5B,MAAA,EAAQ,kBAAA;EACR,QAAA,GAAW,oBAAoB;AAAA;AANjC;;;;AAAkD;AAGlD;;;;AAHA,KAkBK,oBAAA,OACA,OAAA,CAAQ,CAAA;EACP,QAAA,GAAW,SAAA;EACX,OAAA;EACA,OAAA,GAAU,MAAA;EACV,SAAA,GAAY,oBAAA;AAAA,MAEb,CAAA;EACC,QAAA,GAAW,SAAA;EACX,OAAA;EACA,OAAA,GAAU,MAAA;EACV,SAAA,GAAY,oBAAA;AAAA;;KAIb,iBAAA,MAAuB,CAAA;EAC1B,QAAA,GAAW,SAAA;EACX,OAAA;EACA,OAAA,GAAU,MAAA;EACV,SAAA,GAAY,oBAAA;AAAA;;;;;;;;;;;;;KAeF,qBAAA;EA1BN,oFA6BA,WAAA,UA5BA;EA8BA,UAAA,EAAY,OAAA,CAAQ,CAAA;EA7BV;;;;AACsB;AAAA;EAmChC,gBAAA;EACA,MAAA,GAAS,KAAA,EAAO,oBAAA,CAAqB,CAAA,MAAO,SAAA;AAAA;EAG5C,WAAA;EACA,UAAA,EAAY,OAAA,CAAQ,CAAA,GAhCZ;EAkCR,gBAAA;EACA,MAAA,GAAS,KAAA,EAAO,iBAAA,CAAkB,CAAA,MAAO,SAAA;AAAA;;;;;;;;KAUnC,mBAAA,GAAsB,MAAM,SAAS,qBAAA"}
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/vocabulary/alert.d.ts
4
+ declare const alertVocabulary: {
5
+ Alert: {
6
+ description: string;
7
+ properties: z.ZodObject<{
8
+ title: z.ZodOptional<z.ZodString>;
9
+ description: z.ZodOptional<z.ZodString>;
10
+ tone: z.ZodOptional<z.ZodEnum<{
11
+ success: "success";
12
+ info: "info";
13
+ warning: "warning";
14
+ danger: "danger";
15
+ }>>;
16
+ }, z.core.$strip>;
17
+ render: ({
18
+ title,
19
+ description,
20
+ tone,
21
+ children
22
+ }: any) => import("react").JSX.Element;
23
+ };
24
+ Carousel: {
25
+ description: string;
26
+ properties: z.ZodObject<{}, z.core.$strip>;
27
+ render: ({
28
+ children
29
+ }: any) => import("react").JSX.Element;
30
+ };
31
+ };
32
+ //#endregion
33
+ export { alertVocabulary };
34
+ //# sourceMappingURL=alert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alert.d.ts","names":[],"sources":["../../src/vocabulary/alert.tsx"],"mappings":";;;cAOa,eAAA"}
@@ -0,0 +1,49 @@
1
+ import { ALERT_TONES } from "../ir.js";
2
+ import { Children } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { z } from "zod";
5
+ //#region src/vocabulary/alert.tsx
6
+ const MAX_CARDS = 10;
7
+ const alertVocabulary = {
8
+ Alert: {
9
+ description: "A highlighted message conveying urgency. `tone` drives the severity.",
10
+ properties: z.object({
11
+ title: z.string().optional().describe("Alert title."),
12
+ description: z.string().optional().describe("Supporting description text."),
13
+ tone: z.enum(ALERT_TONES).optional().describe("Severity tone; defaults to `info`.")
14
+ }),
15
+ render: ({ title, description, tone, children }) => /* @__PURE__ */ jsxs("div", {
16
+ "data-aui": "alert",
17
+ "data-aui-tone": tone ?? "info",
18
+ role: "alert",
19
+ children: [
20
+ title ? /* @__PURE__ */ jsx("header", {
21
+ "data-aui": "alert-title",
22
+ children: title
23
+ }) : null,
24
+ description ? /* @__PURE__ */ jsx("p", {
25
+ "data-aui": "alert-desc",
26
+ children: description
27
+ }) : null,
28
+ children
29
+ ]
30
+ })
31
+ },
32
+ Carousel: {
33
+ description: "A horizontally scrollable group of `Card` children (max 10).",
34
+ properties: z.object({}),
35
+ render: ({ children }) => {
36
+ return /* @__PURE__ */ jsx("div", {
37
+ "data-aui": "carousel",
38
+ children: Children.toArray(children).slice(0, MAX_CARDS).map((card, i) => /* @__PURE__ */ jsx("div", {
39
+ "data-aui": "carousel-slide",
40
+ children: card
41
+ }, i))
42
+ });
43
+ }
44
+ }
45
+ };
46
+ //#endregion
47
+ export { alertVocabulary };
48
+
49
+ //# sourceMappingURL=alert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alert.js","names":[],"sources":["../../src/vocabulary/alert.tsx"],"sourcesContent":["import { Children } from \"react\";\nimport { z } from \"zod\";\nimport type { GenerativeUILibrary } from \"../types\";\nimport { ALERT_TONES } from \"../ir\";\n\nconst MAX_CARDS = 10;\n\nexport const alertVocabulary = {\n Alert: {\n description:\n \"A highlighted message conveying urgency. `tone` drives the severity.\",\n properties: z.object({\n title: z.string().optional().describe(\"Alert title.\"),\n description: z\n .string()\n .optional()\n .describe(\"Supporting description text.\"),\n tone: z\n .enum(ALERT_TONES)\n .optional()\n .describe(\"Severity tone; defaults to `info`.\"),\n }),\n render: ({ title, description, tone, children }) => (\n <div data-aui=\"alert\" data-aui-tone={tone ?? \"info\"} role=\"alert\">\n {title ? <header data-aui=\"alert-title\">{title}</header> : null}\n {description ? <p data-aui=\"alert-desc\">{description}</p> : null}\n {children}\n </div>\n ),\n },\n Carousel: {\n description: \"A horizontally scrollable group of `Card` children (max 10).\",\n properties: z.object({}),\n render: ({ children }) => {\n const cards = Children.toArray(children).slice(0, MAX_CARDS);\n return (\n <div data-aui=\"carousel\">\n {cards.map((card, i) => (\n <div key={i} data-aui=\"carousel-slide\">\n {card}\n </div>\n ))}\n </div>\n );\n },\n },\n} satisfies GenerativeUILibrary;\n"],"mappings":";;;;;AAKA,MAAM,YAAY;AAElB,MAAa,kBAAkB;CAC7B,OAAO;EACL,aACE;EACF,YAAY,EAAE,OAAO;GACnB,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,cAAc;GACpD,aAAa,EACV,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SAAS,8BAA8B;GAC1C,MAAM,EACH,KAAK,WAAW,CAAC,CACjB,SAAS,CAAC,CACV,SAAS,oCAAoC;EAClD,CAAC;EACD,SAAS,EAAE,OAAO,aAAa,MAAM,eACnC,qBAAC,OAAD;GAAK,YAAS;GAAQ,iBAAe,QAAQ;GAAQ,MAAK;aAA1D;IACG,QAAQ,oBAAC,UAAD;KAAQ,YAAS;eAAe;IAAc,CAAA,IAAI;IAC1D,cAAc,oBAAC,KAAD;KAAG,YAAS;eAAc;IAAe,CAAA,IAAI;IAC3D;GACE;;CAET;CACA,UAAU;EACR,aAAa;EACb,YAAY,EAAE,OAAO,CAAC,CAAC;EACvB,SAAS,EAAE,eAAe;GAExB,OACE,oBAAC,OAAD;IAAK,YAAS;cAFF,SAAS,QAAQ,QAAQ,CAAC,CAAC,MAAM,GAAG,SAGzC,CAAC,CAAC,KAAK,MAAM,MAChB,oBAAC,OAAD;KAAa,YAAS;eACnB;IACE,GAFK,CAEL,CACN;GACE,CAAA;EAET;CACF;AACF"}
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/vocabulary/data.d.ts
4
+ declare const dataVocabulary: {
5
+ Table: {
6
+ description: string;
7
+ properties: z.ZodObject<{
8
+ columns: z.ZodOptional<z.ZodArray<z.ZodObject<{
9
+ label: z.ZodString;
10
+ }, z.core.$strip>>>;
11
+ rows: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>>;
12
+ }, z.core.$strip>;
13
+ render: ({
14
+ columns,
15
+ rows,
16
+ children
17
+ }: any) => import("react").JSX.Element;
18
+ };
19
+ Markdown: {
20
+ description: string;
21
+ properties: z.ZodObject<{
22
+ value: z.ZodString;
23
+ }, z.core.$strip>;
24
+ streamProperties: true;
25
+ render: ({
26
+ value,
27
+ children
28
+ }: any) => import("react").JSX.Element;
29
+ };
30
+ Chart: {
31
+ description: string;
32
+ properties: z.ZodObject<{
33
+ variant: z.ZodEnum<{
34
+ bar: "bar";
35
+ line: "line";
36
+ sparkline: "sparkline";
37
+ }>;
38
+ data: z.ZodArray<z.ZodObject<{
39
+ label: z.ZodOptional<z.ZodString>;
40
+ value: z.ZodNumber;
41
+ }, z.core.$strip>>;
42
+ color: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strip>;
44
+ render: ({
45
+ variant,
46
+ data,
47
+ color
48
+ }: any) => import("react").JSX.Element;
49
+ };
50
+ };
51
+ //#endregion
52
+ export { dataVocabulary };
53
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.d.ts","names":[],"sources":["../../src/vocabulary/data.tsx"],"mappings":";;;cAWa,cAAA"}