@assistant-ui/react-generative-ui 0.0.2
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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/JSONGenerativeUI.client.d.ts +23 -0
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -0
- package/dist/JSONGenerativeUI.client.js +48 -0
- package/dist/JSONGenerativeUI.client.js.map +1 -0
- package/dist/JSONGenerativeUI.server.d.ts +25 -0
- package/dist/JSONGenerativeUI.server.d.ts.map +1 -0
- package/dist/JSONGenerativeUI.server.js +32 -0
- package/dist/JSONGenerativeUI.server.js.map +1 -0
- package/dist/JSONGenerativeUI.shared.d.ts +53 -0
- package/dist/JSONGenerativeUI.shared.d.ts.map +1 -0
- package/dist/JSONGenerativeUI.shared.js +31 -0
- package/dist/JSONGenerativeUI.shared.js.map +1 -0
- package/dist/buildPresentParameters.d.ts +24 -0
- package/dist/buildPresentParameters.d.ts.map +1 -0
- package/dist/buildPresentParameters.js +68 -0
- package/dist/buildPresentParameters.js.map +1 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/defineGenerativeComponents.d.ts +33 -0
- package/dist/defineGenerativeComponents.d.ts.map +1 -0
- package/dist/defineGenerativeComponents.js +34 -0
- package/dist/defineGenerativeComponents.js.map +1 -0
- package/dist/generativeUIToJSX.d.ts +16 -0
- package/dist/generativeUIToJSX.d.ts.map +1 -0
- package/dist/generativeUIToJSX.js +46 -0
- package/dist/generativeUIToJSX.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +141 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -0
- package/dist/renderGenerativeUI.d.ts +17 -0
- package/dist/renderGenerativeUI.d.ts.map +1 -0
- package/dist/renderGenerativeUI.js +85 -0
- package/dist/renderGenerativeUI.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/package.json +82 -0
- package/src/JSONGenerativeUI.client.tsx +66 -0
- package/src/JSONGenerativeUI.server.tsx +39 -0
- package/src/JSONGenerativeUI.shared.ts +74 -0
- package/src/JSONGenerativeUI.test.tsx +89 -0
- package/src/buildPresentParameters.ts +90 -0
- package/src/constants.ts +10 -0
- package/src/defineGenerativeComponents.ts +38 -0
- package/src/generativeUIToJSX.test.ts +64 -0
- package/src/generativeUIToJSX.ts +63 -0
- package/src/index.ts +26 -0
- package/src/renderGenerativeUI.test.tsx +204 -0
- package/src/renderGenerativeUI.tsx +135 -0
- package/src/types.ts +104 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { renderGenerativeUI } from "./renderGenerativeUI";
|
|
5
|
+
import { buildPresentParameters } from "./buildPresentParameters";
|
|
6
|
+
import type { GenerativeUILibrary } from "./types";
|
|
7
|
+
|
|
8
|
+
const library: GenerativeUILibrary = {
|
|
9
|
+
Card: {
|
|
10
|
+
description: "A card container.",
|
|
11
|
+
properties: z.object({ title: z.string() }),
|
|
12
|
+
render: ({ title, children }: any) => (
|
|
13
|
+
<section data-title={title}>{children}</section>
|
|
14
|
+
),
|
|
15
|
+
},
|
|
16
|
+
Text: {
|
|
17
|
+
description: "A run of text.",
|
|
18
|
+
properties: z.object({ tone: z.enum(["muted", "normal"]).optional() }),
|
|
19
|
+
render: ({ tone, children }: any) => <p data-tone={tone}>{children}</p>,
|
|
20
|
+
},
|
|
21
|
+
Button: {
|
|
22
|
+
description: "A button with its own `type` prop.",
|
|
23
|
+
properties: z.object({ type: z.enum(["button", "submit"]) }),
|
|
24
|
+
render: ({ type, children }: any) => (
|
|
25
|
+
<button type={type}>{children}</button>
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
Live: {
|
|
29
|
+
description: "Renders from partial props while streaming.",
|
|
30
|
+
properties: z.object({ label: z.string() }),
|
|
31
|
+
streamProperties: true,
|
|
32
|
+
render: (props) => (
|
|
33
|
+
<span data-status={props.$status}>{props.label ?? "…"}</span>
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("renderGenerativeUI", () => {
|
|
39
|
+
it("renders a component and passes its props", () => {
|
|
40
|
+
const html = renderToStaticMarkup(
|
|
41
|
+
<>{renderGenerativeUI({ $type: "Text", tone: "muted" }, library)}</>,
|
|
42
|
+
);
|
|
43
|
+
expect(html).toBe('<p data-tone="muted"></p>');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders children recursively", () => {
|
|
47
|
+
const html = renderToStaticMarkup(
|
|
48
|
+
<>
|
|
49
|
+
{renderGenerativeUI(
|
|
50
|
+
{
|
|
51
|
+
$type: "Card",
|
|
52
|
+
title: "Hello",
|
|
53
|
+
children: [
|
|
54
|
+
{ $type: "Text", children: "first" },
|
|
55
|
+
{ $type: "Text", tone: "muted", children: "second" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
library,
|
|
59
|
+
)}
|
|
60
|
+
</>,
|
|
61
|
+
);
|
|
62
|
+
expect(html).toBe(
|
|
63
|
+
'<section data-title="Hello"><p>first</p><p data-tone="muted">second</p></section>',
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("passes a component's own `type` prop through without collision", () => {
|
|
68
|
+
const html = renderToStaticMarkup(
|
|
69
|
+
<>
|
|
70
|
+
{renderGenerativeUI(
|
|
71
|
+
{ $type: "Button", type: "submit", children: "Go" },
|
|
72
|
+
library,
|
|
73
|
+
)}
|
|
74
|
+
</>,
|
|
75
|
+
);
|
|
76
|
+
expect(html).toBe('<button type="submit">Go</button>');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("renders a string child directly", () => {
|
|
80
|
+
const html = renderToStaticMarkup(
|
|
81
|
+
<>{renderGenerativeUI({ $type: "Card", children: "plain" }, library)}</>,
|
|
82
|
+
);
|
|
83
|
+
expect(html).toBe("<section><p></p></section>".replace("<p></p>", "plain"));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("renders nothing for an unknown component", () => {
|
|
87
|
+
const html = renderToStaticMarkup(
|
|
88
|
+
<>{renderGenerativeUI({ $type: "Missing" }, library)}</>,
|
|
89
|
+
);
|
|
90
|
+
expect(html).toBe("");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("renders nothing for a node without a resolved type", () => {
|
|
94
|
+
const html = renderToStaticMarkup(<>{renderGenerativeUI({}, library)}</>);
|
|
95
|
+
expect(html).toBe("");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("bounds deeply nested trees instead of overflowing the stack", () => {
|
|
99
|
+
let node: any = { $type: "Text", children: "deep" };
|
|
100
|
+
for (let i = 0; i < 5000; i++) node = { $type: "Card", children: node };
|
|
101
|
+
expect(() =>
|
|
102
|
+
renderToStaticMarkup(<>{renderGenerativeUI(node, library)}</>),
|
|
103
|
+
).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("passes the status to render and tolerates partial props while streaming", () => {
|
|
107
|
+
const html = renderToStaticMarkup(
|
|
108
|
+
<>
|
|
109
|
+
{renderGenerativeUI({ $type: "Live" }, library, {
|
|
110
|
+
status: "streaming",
|
|
111
|
+
})}
|
|
112
|
+
</>,
|
|
113
|
+
);
|
|
114
|
+
expect(html).toBe('<span data-status="streaming">…</span>');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("gates opt-out components until their props are complete", () => {
|
|
118
|
+
const streaming = renderToStaticMarkup(
|
|
119
|
+
<>
|
|
120
|
+
{renderGenerativeUI({ $type: "Card", title: "x" }, library, {
|
|
121
|
+
status: "streaming",
|
|
122
|
+
})}
|
|
123
|
+
</>,
|
|
124
|
+
);
|
|
125
|
+
expect(streaming).toBe("");
|
|
126
|
+
|
|
127
|
+
const done = renderToStaticMarkup(
|
|
128
|
+
<>
|
|
129
|
+
{renderGenerativeUI({ $type: "Card", title: "x" }, library, {
|
|
130
|
+
status: "done",
|
|
131
|
+
})}
|
|
132
|
+
</>,
|
|
133
|
+
);
|
|
134
|
+
expect(done).toBe('<section data-title="x"></section>');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("buildPresentParameters", () => {
|
|
139
|
+
it("produces a flat object schema with no top-level oneOf/anyOf", () => {
|
|
140
|
+
const schema = buildPresentParameters(library) as any;
|
|
141
|
+
|
|
142
|
+
expect(schema.type).toBe("object");
|
|
143
|
+
expect(schema.required).toEqual(["$type"]);
|
|
144
|
+
// Tool/function-call schemas reject these at the top level.
|
|
145
|
+
expect(schema.oneOf).toBeUndefined();
|
|
146
|
+
expect(schema.anyOf).toBeUndefined();
|
|
147
|
+
|
|
148
|
+
expect(schema.properties.$type.enum).toEqual([
|
|
149
|
+
"Card",
|
|
150
|
+
"Text",
|
|
151
|
+
"Button",
|
|
152
|
+
"Live",
|
|
153
|
+
]);
|
|
154
|
+
// each component's description rides along on the $type enum.
|
|
155
|
+
expect(schema.properties.$type.description).toContain("Card");
|
|
156
|
+
expect(schema.properties.children.$ref).toBe("#/$defs/children");
|
|
157
|
+
|
|
158
|
+
// every component's props are merged into the one flat property bag.
|
|
159
|
+
expect(schema.properties.title).toBeDefined(); // Card
|
|
160
|
+
expect(schema.properties.tone).toBeDefined(); // Text
|
|
161
|
+
expect(schema.properties.type).toBeDefined(); // Button's own `type` prop
|
|
162
|
+
expect(schema.properties.label).toBeDefined(); // Live
|
|
163
|
+
|
|
164
|
+
// children recurses back into a node.
|
|
165
|
+
expect(schema.$defs.children.anyOf).toContainEqual({
|
|
166
|
+
$ref: "#/$defs/node",
|
|
167
|
+
});
|
|
168
|
+
expect(schema.$defs.node.type).toBe("object");
|
|
169
|
+
expect(schema.$defs.node.oneOf).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("drops author-declared `$type`/`children` and keeps the discriminator", () => {
|
|
173
|
+
const schema = buildPresentParameters({
|
|
174
|
+
Reserved: {
|
|
175
|
+
description: "Declares reserved keys that must not leak through.",
|
|
176
|
+
properties: z.object({
|
|
177
|
+
$type: z.number(),
|
|
178
|
+
children: z.number(),
|
|
179
|
+
label: z.string(),
|
|
180
|
+
}),
|
|
181
|
+
render: () => null,
|
|
182
|
+
},
|
|
183
|
+
}) as any;
|
|
184
|
+
|
|
185
|
+
// The discriminator is the framework enum, not the author's `$type`; the
|
|
186
|
+
// author's `children` is dropped (the root `children` $ref owns that slot).
|
|
187
|
+
expect(schema.properties.$type.enum).toEqual(["Reserved"]);
|
|
188
|
+
expect(schema.properties.children.$ref).toBe("#/$defs/children");
|
|
189
|
+
expect(schema.properties.label).toBeDefined();
|
|
190
|
+
expect(schema.required).toEqual(["$type"]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("throws when a component's properties is not an object schema", () => {
|
|
194
|
+
expect(() =>
|
|
195
|
+
buildPresentParameters({
|
|
196
|
+
Bad: {
|
|
197
|
+
description: "Non-object props.",
|
|
198
|
+
properties: z.string() as never,
|
|
199
|
+
render: () => null,
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
).toThrow(/must be an object schema/);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Fragment, type ReactNode } from "react";
|
|
2
|
+
import { TYPE_KEY } from "./constants";
|
|
3
|
+
import type {
|
|
4
|
+
GenerativeUIElement,
|
|
5
|
+
GenerativeUILibrary,
|
|
6
|
+
GenerativeUINode,
|
|
7
|
+
GenerativeUIRenderContext,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CONTEXT: GenerativeUIRenderContext = { status: "done" };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Renders a generative-ui tree against a {@link GenerativeUILibrary}.
|
|
14
|
+
*
|
|
15
|
+
* The model emits each node as a flat object `{ $type, ...props }`. We first
|
|
16
|
+
* normalize that wire form into React-shaped elements (`{ type, props }`), then
|
|
17
|
+
* render: each `type` is looked up in the library and its `props` are passed
|
|
18
|
+
* to the component's `render(props, context)`, with `children` rendered
|
|
19
|
+
* recursively so components can nest.
|
|
20
|
+
*/
|
|
21
|
+
export function renderGenerativeUI(
|
|
22
|
+
node: unknown,
|
|
23
|
+
library: GenerativeUILibrary,
|
|
24
|
+
context: GenerativeUIRenderContext = DEFAULT_CONTEXT,
|
|
25
|
+
): ReactNode {
|
|
26
|
+
return renderNode(normalizeNode(node), library, context);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The deepest tree we normalize. The input comes from the model, so a runaway
|
|
31
|
+
* or adversarial response could nest arbitrarily deep and overflow the stack;
|
|
32
|
+
* past this depth we stop (far beyond any real UI). Bounding normalization
|
|
33
|
+
* bounds rendering too, since it only walks the normalized tree.
|
|
34
|
+
*/
|
|
35
|
+
const MAX_DEPTH = 64;
|
|
36
|
+
|
|
37
|
+
/** Converts the flat wire form into a normalized {@link GenerativeUINode}. */
|
|
38
|
+
function normalizeNode(node: unknown, depth = 0): GenerativeUINode {
|
|
39
|
+
if (depth > MAX_DEPTH) return null;
|
|
40
|
+
if (node == null || typeof node === "boolean") return null;
|
|
41
|
+
if (typeof node === "string" || typeof node === "number") return node;
|
|
42
|
+
if (Array.isArray(node))
|
|
43
|
+
return node.map((child) => normalizeNode(child, depth + 1));
|
|
44
|
+
if (typeof node !== "object") return null;
|
|
45
|
+
|
|
46
|
+
const { [TYPE_KEY]: type, ...props } = node as Record<string, unknown>;
|
|
47
|
+
// Args stream in incrementally; a node whose `$type` has not arrived yet is
|
|
48
|
+
// not an error, it just isn't renderable.
|
|
49
|
+
if (typeof type !== "string") return null;
|
|
50
|
+
|
|
51
|
+
if ("children" in props) {
|
|
52
|
+
props["children"] = normalizeNode(props["children"], depth + 1);
|
|
53
|
+
}
|
|
54
|
+
return { type, props } as GenerativeUIElement;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderNode(
|
|
58
|
+
node: GenerativeUINode,
|
|
59
|
+
library: GenerativeUILibrary,
|
|
60
|
+
context: GenerativeUIRenderContext,
|
|
61
|
+
): ReactNode {
|
|
62
|
+
if (node == null || typeof node === "boolean") return null;
|
|
63
|
+
if (typeof node === "string" || typeof node === "number") return node;
|
|
64
|
+
if (Array.isArray(node)) {
|
|
65
|
+
// The wire format has no per-node id, so the key is positional. Pairing the
|
|
66
|
+
// index with the node's kind means that when the model splices or reorders
|
|
67
|
+
// `children` and the kind at an index changes, the key changes and React
|
|
68
|
+
// remounts instead of handing a streaming node's hook state to a different
|
|
69
|
+
// component.
|
|
70
|
+
return node.map((child, index) => (
|
|
71
|
+
<Fragment key={`${index}:${nodeKind(child)}`}>
|
|
72
|
+
{renderNode(child, library, context)}
|
|
73
|
+
</Fragment>
|
|
74
|
+
));
|
|
75
|
+
}
|
|
76
|
+
return renderElement(node, library, context);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderElement(
|
|
80
|
+
element: GenerativeUIElement,
|
|
81
|
+
library: GenerativeUILibrary,
|
|
82
|
+
context: GenerativeUIRenderContext,
|
|
83
|
+
): ReactNode {
|
|
84
|
+
const entry = library[element.type];
|
|
85
|
+
if (!entry) {
|
|
86
|
+
reportUnknownComponent(element.type, Object.keys(library));
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Components that opt out of prop streaming wait until their props are
|
|
91
|
+
// complete rather than rendering from a partial parse.
|
|
92
|
+
if (!entry.streamProperties && context.status === "streaming") return null;
|
|
93
|
+
|
|
94
|
+
// Inject the framework props last so the model can never override them.
|
|
95
|
+
const { children, ...rest } = element.props;
|
|
96
|
+
const props: Record<string, unknown> = { ...rest, $status: context.status };
|
|
97
|
+
if (children !== undefined) {
|
|
98
|
+
props["children"] = renderNode(children, library, context);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return <GenerativeUIComponentRenderer render={entry.render} props={props} />;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Mounts a single node's `render` on its own fiber so the function may use
|
|
106
|
+
* hooks and hold state independently of its siblings and parent.
|
|
107
|
+
*/
|
|
108
|
+
function GenerativeUIComponentRenderer({
|
|
109
|
+
render,
|
|
110
|
+
props,
|
|
111
|
+
}: {
|
|
112
|
+
render: (props: any) => ReactNode;
|
|
113
|
+
props: Record<string, unknown>;
|
|
114
|
+
}): ReactNode {
|
|
115
|
+
return render(props);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A coarse kind tag for a child, used in its list key so a node changing kind
|
|
119
|
+
* at a given index forces a remount rather than a wrong-fiber reuse. */
|
|
120
|
+
function nodeKind(node: GenerativeUINode): string {
|
|
121
|
+
if (node == null || typeof node === "boolean") return "";
|
|
122
|
+
if (typeof node === "string" || typeof node === "number") return "#text";
|
|
123
|
+
if (Array.isArray(node)) return "#array";
|
|
124
|
+
return node.type;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function reportUnknownComponent(type: string, available: string[]): void {
|
|
128
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
129
|
+
// eslint-disable-next-line no-console
|
|
130
|
+
console.error(
|
|
131
|
+
`[@assistant-ui/react-generative-ui] Unknown component "${type}". ` +
|
|
132
|
+
`Available components: ${available.join(", ") || "(none)"}.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
|
|
4
|
+
/** Whether a node's props are still streaming in or have fully arrived. */
|
|
5
|
+
export type GenerativeUIStatus = "streaming" | "done";
|
|
6
|
+
|
|
7
|
+
/** The render context threaded through {@link renderGenerativeUI}. */
|
|
8
|
+
export type GenerativeUIRenderContext = {
|
|
9
|
+
/** Whether the tool call's arguments are still streaming or are complete. */
|
|
10
|
+
status: GenerativeUIStatus;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props a component's `render` receives: its model props, rendered `children`,
|
|
15
|
+
* and the injected `$status`. `$status` is the discriminant — when it is
|
|
16
|
+
* `"done"`, `P` is complete; while `"streaming"`, `P` is partial. It is named
|
|
17
|
+
* `$status` (not `status`) so it never collides with a real `status` prop, the
|
|
18
|
+
* same reservation as `$type`.
|
|
19
|
+
*/
|
|
20
|
+
type StreamingRenderProps<P> =
|
|
21
|
+
| (Partial<P> & { children?: ReactNode; $status: "streaming" })
|
|
22
|
+
| (P & { children?: ReactNode; $status: "done" });
|
|
23
|
+
|
|
24
|
+
/** Props for a component that only renders once complete — `$status` is always `"done"`. */
|
|
25
|
+
type StaticRenderProps<P> = P & { children?: ReactNode; $status: "done" };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A component the model is allowed to render, with the schema for its props.
|
|
29
|
+
*
|
|
30
|
+
* Components opt into prop streaming with `streamProperties`: they render as
|
|
31
|
+
* props arrive, so `render` sees `Partial<P>` while `$status` is `"streaming"`
|
|
32
|
+
* and the full `P` once it is `"done"`. By default a component opts out and is
|
|
33
|
+
* only rendered once its props are complete.
|
|
34
|
+
*
|
|
35
|
+
* `render` is a function of `props`, not a React component. Direct hook use is
|
|
36
|
+
* fine (each node is mounted on its own fiber), but a given node must keep its
|
|
37
|
+
* `type` across renders for hook identity to be stable.
|
|
38
|
+
*/
|
|
39
|
+
export type GenerativeUIComponent<P = any> =
|
|
40
|
+
| {
|
|
41
|
+
/** Natural-language description shown to the model when selecting the component. */
|
|
42
|
+
description: string;
|
|
43
|
+
/** Schema for the props the model must provide. Drives the tool parameters. */
|
|
44
|
+
properties: ZodType<P>;
|
|
45
|
+
/**
|
|
46
|
+
* Render from partially-streamed props. Widened to `boolean | undefined`
|
|
47
|
+
* so a non-literal value (e.g. a variable) still resolves to this branch
|
|
48
|
+
* cleanly rather than matching neither; the strict `false | undefined`
|
|
49
|
+
* branch below is the only one that promises complete props.
|
|
50
|
+
*/
|
|
51
|
+
streamProperties: boolean | undefined;
|
|
52
|
+
render: (props: StreamingRenderProps<P>) => ReactNode;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
description: string;
|
|
56
|
+
properties: ZodType<P>;
|
|
57
|
+
/** Render only once props are complete (the default). */
|
|
58
|
+
streamProperties?: false | undefined;
|
|
59
|
+
render: (props: StaticRenderProps<P>) => ReactNode;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The consumer-provided allowlist of components the model is permitted to
|
|
64
|
+
* render. Keys are the `type` values referenced in the generative-ui tree
|
|
65
|
+
* (e.g. `"Card"`, `"Button"`); values describe each component.
|
|
66
|
+
*
|
|
67
|
+
* This registry is the security boundary — any `type` not present is rejected.
|
|
68
|
+
*/
|
|
69
|
+
export type GenerativeUILibrary = Record<string, GenerativeUIComponent>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A component invocation — mirrors React's `ReactElement`.
|
|
73
|
+
*
|
|
74
|
+
* `type` selects a component from the {@link GenerativeUILibrary}; `props` are
|
|
75
|
+
* passed to it. The `children` prop is special: it is itself a renderable
|
|
76
|
+
* {@link GenerativeUINode}, drawn as generative UI rather than passed as data.
|
|
77
|
+
*
|
|
78
|
+
* On the wire the model emits the flattened form `{ $type, ...props }`, where
|
|
79
|
+
* `$type` names the component so a real `type` prop never collides. This is the
|
|
80
|
+
* normalized shape the renderer works with, the same way React normalizes
|
|
81
|
+
* `createElement` arguments into an element.
|
|
82
|
+
*/
|
|
83
|
+
export type GenerativeUIElement = {
|
|
84
|
+
type: string;
|
|
85
|
+
props: GenerativeUIProps;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Props passed to a component — mirrors a React component's props. */
|
|
89
|
+
export type GenerativeUIProps = {
|
|
90
|
+
children?: GenerativeUINode;
|
|
91
|
+
} & Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Anything renderable as generative UI — mirrors React's `ReactNode`: an
|
|
95
|
+
* element, primitive text, or a list of nodes.
|
|
96
|
+
*/
|
|
97
|
+
export type GenerativeUINode =
|
|
98
|
+
| GenerativeUIElement
|
|
99
|
+
| string
|
|
100
|
+
| number
|
|
101
|
+
| boolean
|
|
102
|
+
| null
|
|
103
|
+
| undefined
|
|
104
|
+
| GenerativeUINode[];
|