@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,39 @@
|
|
|
1
|
+
import { buildPresentParameters } from "./buildPresentParameters";
|
|
2
|
+
import {
|
|
3
|
+
presentToolBase,
|
|
4
|
+
promptUserToolBase,
|
|
5
|
+
type JSONGenerativeUIOptions,
|
|
6
|
+
type PresentParameters,
|
|
7
|
+
type PresentTool,
|
|
8
|
+
type PresentToolOptions,
|
|
9
|
+
type PromptUserTool,
|
|
10
|
+
} from "./JSONGenerativeUI.shared";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Server build of {@link JSONGenerativeUI}, resolved through the package's
|
|
14
|
+
* `react-server` export condition.
|
|
15
|
+
*
|
|
16
|
+
* The model only needs each tool's `type`, `description`, and `parameters` on
|
|
17
|
+
* the server, so `present`/`prompt_user` return exactly that — no `execute` and
|
|
18
|
+
* no `render`, keeping the renderer (and React) out of the server graph. The
|
|
19
|
+
* client build adds those back. The two builds share one set of public types, so
|
|
20
|
+
* consumers see a single {@link JSONGenerativeUI} either way.
|
|
21
|
+
*
|
|
22
|
+
* `as` casts here because the tool types require a `render` for frontend/human
|
|
23
|
+
* tools; on the server it is structurally absent and never read.
|
|
24
|
+
*/
|
|
25
|
+
export class JSONGenerativeUI {
|
|
26
|
+
private readonly parameters: PresentParameters;
|
|
27
|
+
|
|
28
|
+
constructor(options: JSONGenerativeUIOptions) {
|
|
29
|
+
this.parameters = buildPresentParameters(options.library);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
present(options?: PresentToolOptions): PresentTool {
|
|
33
|
+
return presentToolBase(this.parameters, options) as PresentTool;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
promptUser(): PromptUserTool {
|
|
37
|
+
return promptUserToolBase(this.parameters) as PromptUserTool;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@assistant-ui/react";
|
|
2
|
+
import type { JSONSchema7 } from "json-schema";
|
|
3
|
+
import type { GenerativeUILibrary } from "./types";
|
|
4
|
+
|
|
5
|
+
/** Options for {@link JSONGenerativeUI}. */
|
|
6
|
+
export type JSONGenerativeUIOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* The components the model is allowed to render, keyed by the `$type` it
|
|
9
|
+
* selects them with. Author it with `defineGenerativeComponents({ ... })` so a
|
|
10
|
+
* `"use generative"` build can split each `render` from its `properties`.
|
|
11
|
+
*/
|
|
12
|
+
library: GenerativeUILibrary;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Options for {@link JSONGenerativeUI.present}. */
|
|
16
|
+
export type PresentToolOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Set `"standalone"` to render the component on its own surface (outside the
|
|
19
|
+
* chain-of-thought trace), e.g. a full-bleed artifact like a card. Omit it for
|
|
20
|
+
* the default inline rendering — there is no `"inline"` value because that is
|
|
21
|
+
* already the default.
|
|
22
|
+
*/
|
|
23
|
+
display?: "standalone";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** The `present` tool, as the model sees it (no client `render`/`execute`). */
|
|
27
|
+
export type PresentTool = ToolDefinition<
|
|
28
|
+
Record<string, unknown>,
|
|
29
|
+
Record<string, never>
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
/** The `prompt_user` tool, as the model sees it (no client `render`). */
|
|
33
|
+
export type PromptUserTool = ToolDefinition<Record<string, unknown>, unknown>;
|
|
34
|
+
|
|
35
|
+
const PRESENT_DESCRIPTION =
|
|
36
|
+
"Present a UI component to the user. Select a component with `$type` and " +
|
|
37
|
+
"provide its props inline; nest components with `children`.";
|
|
38
|
+
|
|
39
|
+
const PROMPT_USER_DESCRIPTION =
|
|
40
|
+
"Present a UI component to the user and wait for their response. Select a " +
|
|
41
|
+
"component with `$type` and provide its props inline; nest components with " +
|
|
42
|
+
"`children`. The user interacts with it and the result is returned to you.";
|
|
43
|
+
|
|
44
|
+
/** The tool `parameters` schema, built once per instance (see `buildPresentParameters`). */
|
|
45
|
+
export type PresentParameters = JSONSchema7;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The schema-only half of the `present` tool, shared by both builds. The server
|
|
49
|
+
* build returns exactly this; the client build adds `execute` and `render`.
|
|
50
|
+
* Takes the already-built `parameters` so it isn't recomputed per tool.
|
|
51
|
+
*/
|
|
52
|
+
export function presentToolBase(
|
|
53
|
+
parameters: PresentParameters,
|
|
54
|
+
options?: PresentToolOptions,
|
|
55
|
+
) {
|
|
56
|
+
return {
|
|
57
|
+
type: "frontend" as const,
|
|
58
|
+
description: PRESENT_DESCRIPTION,
|
|
59
|
+
parameters,
|
|
60
|
+
...(options?.display !== undefined ? { display: options.display } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The schema-only half of the `prompt_user` tool, shared by both builds. The
|
|
66
|
+
* server build returns exactly this; the client build adds `render`.
|
|
67
|
+
*/
|
|
68
|
+
export function promptUserToolBase(parameters: PresentParameters) {
|
|
69
|
+
return {
|
|
70
|
+
type: "human" as const,
|
|
71
|
+
description: PROMPT_USER_DESCRIPTION,
|
|
72
|
+
parameters,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { JSONGenerativeUI as ClientGenUI } from "./JSONGenerativeUI.client";
|
|
5
|
+
import { JSONGenerativeUI as ServerGenUI } from "./JSONGenerativeUI.server";
|
|
6
|
+
import { defineGenerativeComponents } from "./defineGenerativeComponents";
|
|
7
|
+
import type { GenerativeUILibrary } from "./types";
|
|
8
|
+
|
|
9
|
+
const library: GenerativeUILibrary = {
|
|
10
|
+
Card: {
|
|
11
|
+
description: "A card.",
|
|
12
|
+
properties: z.object({ title: z.string() }),
|
|
13
|
+
render: ({ title, children }: any) => (
|
|
14
|
+
<section data-title={title}>{children}</section>
|
|
15
|
+
),
|
|
16
|
+
},
|
|
17
|
+
Button: {
|
|
18
|
+
description: "A button.",
|
|
19
|
+
properties: z.object({ label: z.string() }),
|
|
20
|
+
render: ({ label }: any) => <button>{label}</button>,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const renderTool = (tool: any, args: unknown) =>
|
|
25
|
+
renderToStaticMarkup(
|
|
26
|
+
<>{tool.render({ args, status: { type: "complete" } })}</>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
describe("JSONGenerativeUI — client build", () => {
|
|
30
|
+
const ui = new ClientGenUI({ library });
|
|
31
|
+
|
|
32
|
+
it("present is a frontend tool with matching parameters, render, and execute", () => {
|
|
33
|
+
const tool = ui.present();
|
|
34
|
+
expect(tool.type).toBe("frontend");
|
|
35
|
+
expect(typeof tool.execute).toBe("function");
|
|
36
|
+
expect(typeof tool.render).toBe("function");
|
|
37
|
+
expect(tool.unstable_backendDefault).toEqual({ parameters: true });
|
|
38
|
+
expect((tool.parameters as any).properties.$type.enum).toEqual([
|
|
39
|
+
"Card",
|
|
40
|
+
"Button",
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("present renders the model's tree against the library", () => {
|
|
45
|
+
const html = renderTool(ui.present(), { $type: "Card", title: "Hi" });
|
|
46
|
+
expect(html).toBe('<section data-title="Hi"></section>');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("prompt_user is a human tool that renders the tree (no execute)", () => {
|
|
50
|
+
const tool = ui.promptUser();
|
|
51
|
+
expect(tool.type).toBe("human");
|
|
52
|
+
expect(tool.unstable_backendDefault).toEqual({ parameters: true });
|
|
53
|
+
expect((tool as any).execute).toBeUndefined();
|
|
54
|
+
const html = renderTool(tool, { $type: "Button", label: "ok" });
|
|
55
|
+
expect(html).toBe("<button>ok</button>");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("JSONGenerativeUI — server build", () => {
|
|
60
|
+
const ui = new ServerGenUI({ library });
|
|
61
|
+
|
|
62
|
+
it("present carries only schema (no render/execute) and matches the client schema", () => {
|
|
63
|
+
const tool = ui.present() as any;
|
|
64
|
+
expect(tool.type).toBe("frontend");
|
|
65
|
+
expect(tool.render).toBeUndefined();
|
|
66
|
+
expect(tool.execute).toBeUndefined();
|
|
67
|
+
expect(tool.unstable_backendDefault).toBeUndefined();
|
|
68
|
+
expect(tool.parameters.properties.$type.enum).toEqual(["Card", "Button"]);
|
|
69
|
+
expect(tool.parameters).toEqual(
|
|
70
|
+
new ClientGenUI({ library }).present().parameters,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("prompt_user carries only schema (no render)", () => {
|
|
75
|
+
const tool = ui.promptUser() as any;
|
|
76
|
+
expect(tool.type).toBe("human");
|
|
77
|
+
expect(tool.render).toBeUndefined();
|
|
78
|
+
expect(tool.unstable_backendDefault).toBeUndefined();
|
|
79
|
+
expect(tool.parameters.properties.$type.enum).toEqual(["Card", "Button"]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("defineGenerativeComponents", () => {
|
|
84
|
+
it("throws at runtime — it must be stripped by the compiler, never called", () => {
|
|
85
|
+
expect(() => defineGenerativeComponents({})).toThrow(
|
|
86
|
+
/no runtime implementation/,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { toJSONSchema } from "assistant-stream";
|
|
2
|
+
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
|
|
3
|
+
import { TYPE_KEY } from "./constants";
|
|
4
|
+
import type { GenerativeUILibrary } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds the JSON schema for the `present` tool from a {@link GenerativeUILibrary}.
|
|
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.
|
|
13
|
+
*
|
|
14
|
+
* It is intentionally flat rather than a per-`$type` discriminated union. Tool /
|
|
15
|
+
* function-call schemas (OpenAI and others) require the top-level parameters to
|
|
16
|
+
* be a plain object and reject a top-level `oneOf`/`anyOf`/`enum`. So props can't
|
|
17
|
+
* be refined per `$type` at the root; the model is guided instead by `$type`'s
|
|
18
|
+
* description (which lists each component) and each prop's own description. The
|
|
19
|
+
* renderer validates nothing here — an unknown `$type` or stray prop is handled
|
|
20
|
+
* at render time — so a looser schema only costs the model a hint, not safety.
|
|
21
|
+
*/
|
|
22
|
+
export function buildPresentParameters(
|
|
23
|
+
library: GenerativeUILibrary,
|
|
24
|
+
): JSONSchema7 {
|
|
25
|
+
const names = Object.keys(library);
|
|
26
|
+
|
|
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.
|
|
31
|
+
const props: Record<string, JSONSchema7Definition> = {};
|
|
32
|
+
for (const name of names) {
|
|
33
|
+
const propsSchema = toJSONSchema(library[name]!.properties);
|
|
34
|
+
if (propsSchema.type !== "object") {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[@assistant-ui/react-generative-ui] Component "${name}": ` +
|
|
37
|
+
"`properties` must be an object schema (e.g. `z.object({ ... })`).",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
for (const [key, schema] of Object.entries(propsSchema.properties ?? {})) {
|
|
41
|
+
if (key === TYPE_KEY || key === "children") continue;
|
|
42
|
+
if (!(key in props)) {
|
|
43
|
+
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
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Carry each component's description on the `$type` enum, since there are no
|
|
56
|
+
// per-branch schemas to hang them on anymore.
|
|
57
|
+
const typeDescription =
|
|
58
|
+
names.length > 0
|
|
59
|
+
? `The component to render. ${names
|
|
60
|
+
.map((name) => `"${name}": ${library[name]!.description}`)
|
|
61
|
+
.join("; ")}`
|
|
62
|
+
: "The component to render.";
|
|
63
|
+
|
|
64
|
+
const node: JSONSchema7 = {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
[TYPE_KEY]: { type: "string", enum: names, description: typeDescription },
|
|
68
|
+
...props,
|
|
69
|
+
children: { $ref: "#/$defs/children" },
|
|
70
|
+
},
|
|
71
|
+
required: [TYPE_KEY],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const children: JSONSchema7 = {
|
|
75
|
+
description: "Nested generative UI rendered inside this component.",
|
|
76
|
+
anyOf: [
|
|
77
|
+
{ type: "string" },
|
|
78
|
+
{ $ref: "#/$defs/node" },
|
|
79
|
+
{
|
|
80
|
+
type: "array",
|
|
81
|
+
items: { anyOf: [{ type: "string" }, { $ref: "#/$defs/node" }] },
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
...node,
|
|
88
|
+
$defs: { node, children },
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire key naming the component to render.
|
|
3
|
+
*
|
|
4
|
+
* The model emits nodes as flat objects `{ [TYPE_KEY]: name, ...props }`. We
|
|
5
|
+
* use `$type` rather than `type` so a component's own `type` prop (e.g. a
|
|
6
|
+
* button's `type="submit"`) flows through as an ordinary prop without
|
|
7
|
+
* colliding with the discriminator. It mirrors the `$type` discriminator
|
|
8
|
+
* convention used by JSON polymorphism elsewhere.
|
|
9
|
+
*/
|
|
10
|
+
export const TYPE_KEY = "$type";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { GenerativeUILibrary } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authoring helper for a `"use generative"` generative-UI library — the set of
|
|
5
|
+
* components the model may render. Each component colocates its `properties`
|
|
6
|
+
* schema (kept on every build, drives the tool parameters) with its `render`
|
|
7
|
+
* (kept only on the client). Pass the result to {@link JSONGenerativeUI}:
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* "use generative";
|
|
11
|
+
* const generative = new JSONGenerativeUI({
|
|
12
|
+
* library: defineGenerativeComponents({
|
|
13
|
+
* Card: {
|
|
14
|
+
* description: "A card.",
|
|
15
|
+
* properties: z.object({ title: z.string() }),
|
|
16
|
+
* render: (props) => <Card {...props} />,
|
|
17
|
+
* },
|
|
18
|
+
* }),
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Unlike {@link defineToolkit}, it has **no runtime implementation**. A
|
|
23
|
+
* `"use generative"` compiler unwraps the `defineGenerativeComponents(...)` call
|
|
24
|
+
* per build, dropping each `render` (and its client-only imports) from the server
|
|
25
|
+
* build. Reaching it at runtime means the module wasn't compiled (the directive
|
|
26
|
+
* is missing, or it was used outside a `"use generative"` file), so it throws
|
|
27
|
+
* rather than shipping client `render` code to the server.
|
|
28
|
+
*/
|
|
29
|
+
export function defineGenerativeComponents(
|
|
30
|
+
_library: GenerativeUILibrary,
|
|
31
|
+
): GenerativeUILibrary {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"[assistant-ui] defineGenerativeComponents() has no runtime implementation " +
|
|
34
|
+
"— it is stripped at build time by the use-generative compiler. Reaching " +
|
|
35
|
+
"it means this module was not compiled (e.g. used outside a " +
|
|
36
|
+
'"use generative" file). Add the directive, or do not use it here.',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { generativeUIToJSX } from "./generativeUIToJSX";
|
|
3
|
+
|
|
4
|
+
describe("generativeUIToJSX", () => {
|
|
5
|
+
it("renders a leaf element as a self-closing tag", () => {
|
|
6
|
+
expect(generativeUIToJSX({ $type: "Weather", id: "5d99d2e9" })).toBe(
|
|
7
|
+
'<Weather id="5d99d2e9" />',
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("renders an element with no props", () => {
|
|
12
|
+
expect(generativeUIToJSX({ $type: "Divider" })).toBe("<Divider />");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("formats prop types as JSX attributes", () => {
|
|
16
|
+
expect(
|
|
17
|
+
generativeUIToJSX({
|
|
18
|
+
$type: "Box",
|
|
19
|
+
label: "hi",
|
|
20
|
+
count: 3,
|
|
21
|
+
open: true,
|
|
22
|
+
hidden: false,
|
|
23
|
+
data: { a: 1 },
|
|
24
|
+
}),
|
|
25
|
+
).toBe('<Box label="hi" count={3} open hidden={false} data={{"a":1}} />');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("uses the expression form for strings with quotes", () => {
|
|
29
|
+
expect(generativeUIToJSX({ $type: "Note", text: 'say "hi"' })).toBe(
|
|
30
|
+
'<Note text={"say \\"hi\\""} />',
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders string children between tags", () => {
|
|
35
|
+
expect(
|
|
36
|
+
generativeUIToJSX({ $type: "Text", tone: "muted", children: "hello" }),
|
|
37
|
+
).toBe('<Text tone="muted">hello</Text>');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders nested and arrayed children recursively", () => {
|
|
41
|
+
expect(
|
|
42
|
+
generativeUIToJSX({
|
|
43
|
+
$type: "Card",
|
|
44
|
+
title: "Hi",
|
|
45
|
+
children: [
|
|
46
|
+
{ $type: "Text", children: "a" },
|
|
47
|
+
{ $type: "Text", tone: "muted", children: "b" },
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
).toBe('<Card title="Hi"><Text>a</Text><Text tone="muted">b</Text></Card>');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("bounds deeply nested trees instead of overflowing the stack", () => {
|
|
54
|
+
let node: any = { $type: "Text", children: "deep" };
|
|
55
|
+
for (let i = 0; i < 5000; i++) node = { $type: "Card", children: node };
|
|
56
|
+
expect(() => generativeUIToJSX(node)).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns empty string for non-renderable nodes", () => {
|
|
60
|
+
expect(generativeUIToJSX(null)).toBe("");
|
|
61
|
+
expect(generativeUIToJSX(true)).toBe("");
|
|
62
|
+
expect(generativeUIToJSX({})).toBe(""); // no $type yet (still streaming)
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { TYPE_KEY } from "./constants";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serializes a generative-UI node to a JSX-like string for display — the
|
|
5
|
+
* "view source" of a model-produced tree. The wire form
|
|
6
|
+
* `{ $type: "Weather", id: "x" }` becomes `<Weather id="x" />`, and nested
|
|
7
|
+
* `children` render between tags: `<Card title="Hi"><Text>hello</Text></Card>`.
|
|
8
|
+
*
|
|
9
|
+
* It is a faithful textual rendering, not a parser: text children are emitted
|
|
10
|
+
* verbatim (not HTML/JSX-escaped), so the result is meant to be shown, not
|
|
11
|
+
* re-parsed. Returns `""` for nodes that aren't renderable (no `$type` yet,
|
|
12
|
+
* `null`, booleans).
|
|
13
|
+
*/
|
|
14
|
+
export function generativeUIToJSX(node: unknown): string {
|
|
15
|
+
return toJSX(node, 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The deepest tree we serialize. Input is model-produced, so a runaway/
|
|
20
|
+
* adversarial response could nest arbitrarily deep and overflow the stack — past
|
|
21
|
+
* this depth (far beyond any real UI) we stop. Mirrors the renderer's bound.
|
|
22
|
+
*/
|
|
23
|
+
const MAX_DEPTH = 64;
|
|
24
|
+
|
|
25
|
+
function toJSX(node: unknown, depth: number): string {
|
|
26
|
+
if (depth > MAX_DEPTH) return "";
|
|
27
|
+
if (node == null || typeof node === "boolean") return "";
|
|
28
|
+
if (typeof node === "string") return node;
|
|
29
|
+
if (typeof node === "number") return String(node);
|
|
30
|
+
if (Array.isArray(node))
|
|
31
|
+
return node.map((child) => toJSX(child, depth + 1)).join("");
|
|
32
|
+
if (typeof node !== "object") return "";
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
[TYPE_KEY]: type,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
} = node as Record<string, unknown>;
|
|
39
|
+
if (typeof type !== "string") return "";
|
|
40
|
+
|
|
41
|
+
const attrs = Object.entries(props)
|
|
42
|
+
.map(([key, value]) => formatAttr(key, value))
|
|
43
|
+
.join("");
|
|
44
|
+
const inner = children === undefined ? "" : toJSX(children, depth + 1);
|
|
45
|
+
|
|
46
|
+
return inner === ""
|
|
47
|
+
? `<${type}${attrs} />`
|
|
48
|
+
: `<${type}${attrs}>${inner}</${type}>`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Formats one prop as a JSX attribute (`id="x"`, `count={3}`, `open`, …). */
|
|
52
|
+
function formatAttr(key: string, value: unknown): string {
|
|
53
|
+
if (value === undefined) return "";
|
|
54
|
+
if (value === true) return ` ${key}`;
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
// Plain double-quoted form when safe; expression form when a quote or
|
|
57
|
+
// newline would break the attribute.
|
|
58
|
+
return /["\n]/.test(value)
|
|
59
|
+
? ` ${key}={${JSON.stringify(value)}}`
|
|
60
|
+
: ` ${key}="${value}"`;
|
|
61
|
+
}
|
|
62
|
+
return ` ${key}={${JSON.stringify(value)}}`;
|
|
63
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// `JSONGenerativeUI` is the one export that differs per build: it comes from the
|
|
2
|
+
// `./internal-json` subpath, which resolves to the server variant (schema only)
|
|
3
|
+
// under the `react-server` condition and the client variant (adds render/execute)
|
|
4
|
+
// otherwise. Everything else here is isomorphic, so this single entry serves
|
|
5
|
+
// both conditions — no per-build index barrel needed.
|
|
6
|
+
export { JSONGenerativeUI } from "@assistant-ui/react-generative-ui/internal-json";
|
|
7
|
+
export { renderGenerativeUI } from "./renderGenerativeUI";
|
|
8
|
+
export { generativeUIToJSX } from "./generativeUIToJSX";
|
|
9
|
+
export { buildPresentParameters } from "./buildPresentParameters";
|
|
10
|
+
export { defineGenerativeComponents } from "./defineGenerativeComponents";
|
|
11
|
+
export { TYPE_KEY } from "./constants";
|
|
12
|
+
export type {
|
|
13
|
+
JSONGenerativeUIOptions,
|
|
14
|
+
PresentTool,
|
|
15
|
+
PresentToolOptions,
|
|
16
|
+
PromptUserTool,
|
|
17
|
+
} from "./JSONGenerativeUI.shared";
|
|
18
|
+
export type {
|
|
19
|
+
GenerativeUILibrary,
|
|
20
|
+
GenerativeUIComponent,
|
|
21
|
+
GenerativeUIElement,
|
|
22
|
+
GenerativeUIProps,
|
|
23
|
+
GenerativeUINode,
|
|
24
|
+
GenerativeUIStatus,
|
|
25
|
+
GenerativeUIRenderContext,
|
|
26
|
+
} from "./types";
|