@assistant-ui/react-generative-ui 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/JSONGenerativeUI.client.d.ts +4 -2
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +9 -2
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -1
- package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.d.ts +8 -1
- package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.js.map +1 -1
- package/dist/actionRegistry.d.ts +32 -0
- package/dist/actionRegistry.d.ts.map +1 -0
- package/dist/actionRegistry.js +41 -0
- package/dist/actionRegistry.js.map +1 -0
- package/dist/buildPresentParameters.d.ts +5 -5
- package/dist/buildPresentParameters.d.ts.map +1 -1
- package/dist/buildPresentParameters.js +20 -6
- package/dist/buildPresentParameters.js.map +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/defineGenerativeComponents.d.ts +0 -1
- package/dist/defineGenerativeComponents.d.ts.map +1 -1
- package/dist/generativeUIToJSX.d.ts +15 -10
- package/dist/generativeUIToJSX.d.ts.map +1 -1
- package/dist/generativeUIToJSX.js +47 -17
- package/dist/generativeUIToJSX.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +5 -2
- package/dist/index.server.d.ts +13 -0
- package/dist/index.server.js +10 -0
- package/dist/index.shared.d.ts +11 -0
- package/dist/index.shared.js +9 -0
- package/dist/ir.d.ts +115 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +175 -0
- package/dist/ir.js.map +1 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
- package/dist/renderGenerativeUI.d.ts +5 -5
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/renderGenerativeUI.js +18 -45
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +111 -0
- package/dist/slack/constants.d.ts.map +1 -0
- package/dist/slack/constants.js +168 -0
- package/dist/slack/constants.js.map +1 -0
- package/dist/slack/decodeBlockAction.d.ts +11 -0
- package/dist/slack/decodeBlockAction.d.ts.map +1 -0
- package/dist/slack/decodeBlockAction.js +40 -0
- package/dist/slack/decodeBlockAction.js.map +1 -0
- package/dist/slack/fromSlackBlocks.d.ts +30 -0
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
- package/dist/slack/fromSlackBlocks.js +447 -0
- package/dist/slack/fromSlackBlocks.js.map +1 -0
- package/dist/slack/toSlackBlocks.d.ts +7 -0
- package/dist/slack/toSlackBlocks.d.ts.map +1 -0
- package/dist/slack/toSlackBlocks.js +649 -0
- package/dist/slack/toSlackBlocks.js.map +1 -0
- package/dist/slack/types.d.ts +178 -0
- package/dist/slack/types.d.ts.map +1 -0
- package/dist/slack/types.js +0 -0
- package/dist/slack.d.ts +5 -0
- package/dist/slack.js +4 -0
- package/dist/teams/boundSpec.d.ts +20 -0
- package/dist/teams/boundSpec.d.ts.map +1 -0
- package/dist/teams/boundSpec.js +67 -0
- package/dist/teams/boundSpec.js.map +1 -0
- package/dist/teams/constants.d.ts +57 -0
- package/dist/teams/constants.d.ts.map +1 -0
- package/dist/teams/constants.js +92 -0
- package/dist/teams/constants.js.map +1 -0
- package/dist/teams/decodeSubmitData.d.ts +25 -0
- package/dist/teams/decodeSubmitData.d.ts.map +1 -0
- package/dist/teams/decodeSubmitData.js +44 -0
- package/dist/teams/decodeSubmitData.js.map +1 -0
- package/dist/teams/toAdaptiveCard.d.ts +39 -0
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
- package/dist/teams/toAdaptiveCard.js +496 -0
- package/dist/teams/toAdaptiveCard.js.map +1 -0
- package/dist/teams/toTeamsAttachments.d.ts +13 -0
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
- package/dist/teams/toTeamsAttachments.js +95 -0
- package/dist/teams/toTeamsAttachments.js.map +1 -0
- package/dist/teams/types.d.ts +196 -0
- package/dist/teams/types.d.ts.map +1 -0
- package/dist/teams/types.js +0 -0
- package/dist/teams.d.ts +5 -0
- package/dist/teams.js +4 -0
- package/dist/types.d.ts +41 -34
- package/dist/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +28 -0
- package/dist/vocabulary/alert.d.ts.map +1 -0
- package/dist/vocabulary/alert.js +58 -0
- package/dist/vocabulary/alert.js.map +1 -0
- package/dist/vocabulary/collectFormValues.d.ts +23 -0
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
- package/dist/vocabulary/collectFormValues.js +32 -0
- package/dist/vocabulary/collectFormValues.js.map +1 -0
- package/dist/vocabulary/data.d.ts +52 -0
- package/dist/vocabulary/data.d.ts.map +1 -0
- package/dist/vocabulary/data.js +294 -0
- package/dist/vocabulary/data.js.map +1 -0
- package/dist/vocabulary/dispatch.d.ts +11 -0
- package/dist/vocabulary/dispatch.d.ts.map +1 -0
- package/dist/vocabulary/dispatch.js +21 -0
- package/dist/vocabulary/dispatch.js.map +1 -0
- package/dist/vocabulary/fact.d.ts +15 -0
- package/dist/vocabulary/fact.d.ts.map +1 -0
- package/dist/vocabulary/fact.js +24 -0
- package/dist/vocabulary/fact.js.map +1 -0
- package/dist/vocabulary/form.d.ts +14 -0
- package/dist/vocabulary/form.d.ts.map +1 -0
- package/dist/vocabulary/form.js +23 -0
- package/dist/vocabulary/form.js.map +1 -0
- package/dist/vocabulary/icon.d.ts +44 -0
- package/dist/vocabulary/icon.d.ts.map +1 -0
- package/dist/vocabulary/icon.js +272 -0
- package/dist/vocabulary/icon.js.map +1 -0
- package/dist/vocabulary/index.d.ts +15 -0
- package/dist/vocabulary/index.d.ts.map +1 -0
- package/dist/vocabulary/index.js +36 -0
- package/dist/vocabulary/index.js.map +1 -0
- package/dist/vocabulary/interactive.d.ts +93 -0
- package/dist/vocabulary/interactive.d.ts.map +1 -0
- package/dist/vocabulary/interactive.js +172 -0
- package/dist/vocabulary/interactive.js.map +1 -0
- package/dist/vocabulary/layout.d.ts +82 -0
- package/dist/vocabulary/layout.d.ts.map +1 -0
- package/dist/vocabulary/layout.js +136 -0
- package/dist/vocabulary/layout.js.map +1 -0
- package/dist/vocabulary/list.d.ts +17 -0
- package/dist/vocabulary/list.d.ts.map +1 -0
- package/dist/vocabulary/list.js +48 -0
- package/dist/vocabulary/list.js.map +1 -0
- package/dist/vocabulary/media.d.ts +28 -0
- package/dist/vocabulary/media.d.ts.map +1 -0
- package/dist/vocabulary/media.js +42 -0
- package/dist/vocabulary/media.js.map +1 -0
- package/dist/vocabulary/text.d.ts +60 -0
- package/dist/vocabulary/text.d.ts.map +1 -0
- package/dist/vocabulary/text.js +48 -0
- package/dist/vocabulary/text.js.map +1 -0
- package/package.json +21 -13
- package/src/JSONGenerativeUI.client.tsx +10 -2
- package/src/JSONGenerativeUI.shared.ts +8 -0
- package/src/actionRegistry.test.ts +90 -0
- package/src/actionRegistry.ts +88 -0
- package/src/buildPresentParameters.ts +38 -16
- package/src/generativeUIToJSX.test.ts +186 -0
- package/src/generativeUIToJSX.ts +112 -21
- package/src/index.server.ts +2 -0
- package/src/index.shared.ts +61 -0
- package/src/index.ts +2 -26
- package/src/ir.test.ts +203 -0
- package/src/ir.ts +279 -0
- package/src/renderGenerativeUI.test.tsx +69 -2
- package/src/renderGenerativeUI.tsx +49 -82
- package/src/slack/constants.ts +217 -0
- package/src/slack/decodeBlockAction.test.ts +135 -0
- package/src/slack/decodeBlockAction.ts +66 -0
- package/src/slack/fromSlackBlocks.test.ts +1145 -0
- package/src/slack/fromSlackBlocks.ts +709 -0
- package/src/slack/toSlackBlocks.test.ts +1936 -0
- package/src/slack/toSlackBlocks.ts +1319 -0
- package/src/slack/types.ts +230 -0
- package/src/slack.ts +37 -0
- package/src/teams/boundSpec.ts +102 -0
- package/src/teams/constants.ts +127 -0
- package/src/teams/decodeSubmitData.test.ts +148 -0
- package/src/teams/decodeSubmitData.ts +57 -0
- package/src/teams/toAdaptiveCard.test.ts +1410 -0
- package/src/teams/toAdaptiveCard.ts +762 -0
- package/src/teams/toTeamsAttachments.test.ts +29 -0
- package/src/teams/toTeamsAttachments.ts +137 -0
- package/src/teams/types.ts +237 -0
- package/src/teams.ts +34 -0
- package/src/types.ts +57 -42
- package/src/vocabulary/alert.test.tsx +136 -0
- package/src/vocabulary/alert.tsx +66 -0
- package/src/vocabulary/collectFormValues.test.ts +127 -0
- package/src/vocabulary/collectFormValues.ts +58 -0
- package/src/vocabulary/data.test.tsx +567 -0
- package/src/vocabulary/data.tsx +494 -0
- package/src/vocabulary/dispatch.ts +22 -0
- package/src/vocabulary/fact.test.tsx +15 -0
- package/src/vocabulary/fact.tsx +22 -0
- package/src/vocabulary/form.test.tsx +112 -0
- package/src/vocabulary/form.tsx +34 -0
- package/src/vocabulary/icon.test.tsx +64 -0
- package/src/vocabulary/icon.tsx +174 -0
- package/src/vocabulary/index.test.ts +63 -0
- package/src/vocabulary/index.ts +33 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +352 -0
- package/src/vocabulary/interactive.test.tsx +248 -0
- package/src/vocabulary/interactive.tsx +269 -0
- package/src/vocabulary/layout.test.tsx +289 -0
- package/src/vocabulary/layout.tsx +229 -0
- package/src/vocabulary/list.test.tsx +195 -0
- package/src/vocabulary/list.tsx +48 -0
- package/src/vocabulary/media.test.tsx +85 -0
- package/src/vocabulary/media.tsx +56 -0
- package/src/vocabulary/text.test.tsx +73 -0
- package/src/vocabulary/text.tsx +59 -0
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { generativeUIToJSX } from "./generativeUIToJSX";
|
|
3
3
|
|
|
4
|
+
describe("generativeUIToJSX pretty depth bound", () => {
|
|
5
|
+
it("tolerates children nested in arrays beyond the depth bound", () => {
|
|
6
|
+
let node: unknown = { $type: "Text", value: "leaf" };
|
|
7
|
+
for (let i = 0; i < 100; i++) node = [node];
|
|
8
|
+
expect(() =>
|
|
9
|
+
generativeUIToJSX({ $type: "Card", children: node }, { pretty: true }),
|
|
10
|
+
).not.toThrow();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
4
14
|
describe("generativeUIToJSX", () => {
|
|
5
15
|
it("renders a leaf element as a self-closing tag", () => {
|
|
6
16
|
expect(generativeUIToJSX({ $type: "Weather", id: "5d99d2e9" })).toBe(
|
|
@@ -31,6 +41,15 @@ describe("generativeUIToJSX", () => {
|
|
|
31
41
|
);
|
|
32
42
|
});
|
|
33
43
|
|
|
44
|
+
it("emits a model-provided $key as the JSX key attribute", () => {
|
|
45
|
+
expect(
|
|
46
|
+
generativeUIToJSX({ $type: "Text", $key: "1:Text", children: "hello" }),
|
|
47
|
+
).toBe('<Text key="1:Text">hello</Text>');
|
|
48
|
+
expect(generativeUIToJSX({ $type: "Item", $key: 2 })).toBe(
|
|
49
|
+
"<Item key={2} />",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
34
53
|
it("renders string children between tags", () => {
|
|
35
54
|
expect(
|
|
36
55
|
generativeUIToJSX({ $type: "Text", tone: "muted", children: "hello" }),
|
|
@@ -62,3 +81,170 @@ describe("generativeUIToJSX", () => {
|
|
|
62
81
|
expect(generativeUIToJSX({})).toBe(""); // no $type yet (still streaming)
|
|
63
82
|
});
|
|
64
83
|
});
|
|
84
|
+
|
|
85
|
+
describe("generativeUIToJSX with escape: true", () => {
|
|
86
|
+
it("escapes a string child containing HTML-like tags", () => {
|
|
87
|
+
expect(
|
|
88
|
+
generativeUIToJSX(
|
|
89
|
+
{ $type: "Text", children: "<script>alert(1)</script>" },
|
|
90
|
+
{ escape: true },
|
|
91
|
+
),
|
|
92
|
+
).toBe('<Text>{"<script>alert(1)</script>"}</Text>');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("escapes a string child containing braces", () => {
|
|
96
|
+
expect(
|
|
97
|
+
generativeUIToJSX(
|
|
98
|
+
{ $type: "Text", children: "a {b} c" },
|
|
99
|
+
{ escape: true },
|
|
100
|
+
),
|
|
101
|
+
).toBe('<Text>{"a {b} c"}</Text>');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("escapes a string child containing an ampersand", () => {
|
|
105
|
+
expect(
|
|
106
|
+
generativeUIToJSX({ $type: "Text", children: "Q&A" }, { escape: true }),
|
|
107
|
+
).toBe('<Text>{"Q&A"}</Text>');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("leaves a plain text child verbatim", () => {
|
|
111
|
+
expect(
|
|
112
|
+
generativeUIToJSX(
|
|
113
|
+
{ $type: "Text", children: "hello world" },
|
|
114
|
+
{ escape: true },
|
|
115
|
+
),
|
|
116
|
+
).toBe("<Text>hello world</Text>");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("leaves numbers, arrays, and nested objects unaffected", () => {
|
|
120
|
+
expect(
|
|
121
|
+
generativeUIToJSX(
|
|
122
|
+
{
|
|
123
|
+
$type: "Card",
|
|
124
|
+
children: [
|
|
125
|
+
{ $type: "Text", children: 42 },
|
|
126
|
+
{ $type: "Text", data: { a: 1 }, children: "plain" },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{ escape: true },
|
|
130
|
+
),
|
|
131
|
+
).toBe('<Card><Text>42</Text><Text data={{"a":1}}>plain</Text></Card>');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("generativeUIToJSX with pretty: true", () => {
|
|
136
|
+
const nestedCard = {
|
|
137
|
+
$type: "Card",
|
|
138
|
+
title: "Hi",
|
|
139
|
+
children: [
|
|
140
|
+
{ $type: "Text", children: "a" },
|
|
141
|
+
{
|
|
142
|
+
$type: "Box",
|
|
143
|
+
children: { $type: "Text", tone: "muted", children: "b" },
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
it("keeps default output single-line and byte-identical without pretty", () => {
|
|
149
|
+
expect(generativeUIToJSX(nestedCard)).toBe(
|
|
150
|
+
'<Card title="Hi"><Text>a</Text><Box><Text tone="muted">b</Text></Box></Card>',
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("block-nests elements with two-space grandchild indentation", () => {
|
|
155
|
+
expect(generativeUIToJSX(nestedCard, { pretty: true })).toBe(
|
|
156
|
+
[
|
|
157
|
+
'<Card title="Hi">',
|
|
158
|
+
" <Text>a</Text>",
|
|
159
|
+
" <Box>",
|
|
160
|
+
' <Text tone="muted">b</Text>',
|
|
161
|
+
" </Box>",
|
|
162
|
+
"</Card>",
|
|
163
|
+
].join("\n"),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("keeps elements with only string or number children on one line", () => {
|
|
168
|
+
expect(
|
|
169
|
+
generativeUIToJSX(
|
|
170
|
+
{ $type: "Text", tone: "muted", children: "hello" },
|
|
171
|
+
{ pretty: true },
|
|
172
|
+
),
|
|
173
|
+
).toBe('<Text tone="muted">hello</Text>');
|
|
174
|
+
expect(
|
|
175
|
+
generativeUIToJSX({ $type: "Text", children: 42 }, { pretty: true }),
|
|
176
|
+
).toBe("<Text>42</Text>");
|
|
177
|
+
expect(generativeUIToJSX({ $type: "Divider" }, { pretty: true })).toBe(
|
|
178
|
+
"<Divider />",
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("renders a root-level array with each item on its own line", () => {
|
|
183
|
+
expect(
|
|
184
|
+
generativeUIToJSX(
|
|
185
|
+
[
|
|
186
|
+
{ $type: "Text", children: "a" },
|
|
187
|
+
{ $type: "Text", children: "b" },
|
|
188
|
+
],
|
|
189
|
+
{ pretty: true },
|
|
190
|
+
),
|
|
191
|
+
).toBe("<Text>a</Text>\n<Text>b</Text>");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("puts mixed text and element children each on their own line", () => {
|
|
195
|
+
expect(
|
|
196
|
+
generativeUIToJSX(
|
|
197
|
+
{
|
|
198
|
+
$type: "Card",
|
|
199
|
+
children: ["hello", { $type: "Text", children: "world" }, 7],
|
|
200
|
+
},
|
|
201
|
+
{ pretty: true },
|
|
202
|
+
),
|
|
203
|
+
).toBe(
|
|
204
|
+
["<Card>", " hello", " <Text>world</Text>", " 7", "</Card>"].join(
|
|
205
|
+
"\n",
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("skips non-renderable children without blank lines", () => {
|
|
211
|
+
expect(
|
|
212
|
+
generativeUIToJSX(
|
|
213
|
+
{
|
|
214
|
+
$type: "Card",
|
|
215
|
+
children: [
|
|
216
|
+
null,
|
|
217
|
+
{ $type: "Text", children: "a" },
|
|
218
|
+
false,
|
|
219
|
+
{ $type: "Text", children: "b" },
|
|
220
|
+
true,
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
{ pretty: true },
|
|
224
|
+
),
|
|
225
|
+
).toBe(
|
|
226
|
+
["<Card>", " <Text>a</Text>", " <Text>b</Text>", "</Card>"].join("\n"),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("composes escape with pretty", () => {
|
|
231
|
+
expect(
|
|
232
|
+
generativeUIToJSX(
|
|
233
|
+
{
|
|
234
|
+
$type: "Card",
|
|
235
|
+
children: {
|
|
236
|
+
$type: "Text",
|
|
237
|
+
children: "<script>alert(1)</script>",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{ escape: true, pretty: true },
|
|
241
|
+
),
|
|
242
|
+
).toBe(
|
|
243
|
+
[
|
|
244
|
+
"<Card>",
|
|
245
|
+
' <Text>{"<script>alert(1)</script>"}</Text>',
|
|
246
|
+
"</Card>",
|
|
247
|
+
].join("\n"),
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
package/src/generativeUIToJSX.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { TYPE_KEY } from "./constants";
|
|
2
2
|
|
|
3
|
+
/** Options for {@link generativeUIToJSX}. */
|
|
4
|
+
export interface GenerativeUIToJSXOptions {
|
|
5
|
+
/** Escape string children so the output is safe to paste back into a JSX file. Defaults to `false`. */
|
|
6
|
+
escape?: boolean;
|
|
7
|
+
/** Pretty-print nested element trees with two-space indentation. Defaults to `false`. */
|
|
8
|
+
pretty?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
|
-
* Serializes a generative-UI node to a JSX-like string for display
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `children` render between tags: `<Card title="Hi"><Text>hello</Text></Card>`.
|
|
12
|
+
* Serializes a generative-UI node to a JSX-like string for display: the "view source" of a model-produced tree. The wire form `{ $type: "Weather", id: "x" }` becomes `<Weather id="x" />`, and nested `children` render between tags: `<Card title="Hi"><Text>hello</Text></Card>`. A model-provided `$key` becomes the JSX `key` attribute.
|
|
13
|
+
*
|
|
14
|
+
* By default this is a faithful textual rendering, not a parser: text children are emitted verbatim (not HTML/JSX-escaped), so the result is meant to be shown, not re-parsed. Returns `""` for nodes that aren't renderable (no `$type` yet, `null`, booleans).
|
|
8
15
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `null`, booleans).
|
|
16
|
+
* Pass `{ escape: true }` to make the output safe to paste back into JSX: any string child containing `<`, `>`, `&`, `{`, or `}` is emitted as a JSON-stringified expression (`{JSON.stringify(child)}`) instead of verbatim, while a string child with none of those characters still renders verbatim.
|
|
17
|
+
*
|
|
18
|
+
* Pass `{ pretty: true }` to pretty-print nested element trees with two-space indentation, while pure text children stay on one line.
|
|
13
19
|
*/
|
|
14
|
-
export function generativeUIToJSX(
|
|
15
|
-
|
|
20
|
+
export function generativeUIToJSX(
|
|
21
|
+
node: unknown,
|
|
22
|
+
options?: GenerativeUIToJSXOptions,
|
|
23
|
+
): string {
|
|
24
|
+
return toJSX(node, 0, options?.escape ?? false, options?.pretty ?? false, 0);
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
/**
|
|
@@ -22,30 +31,112 @@ export function generativeUIToJSX(node: unknown): string {
|
|
|
22
31
|
*/
|
|
23
32
|
const MAX_DEPTH = 64;
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
/** Characters that would break JSX if emitted verbatim inside a text child. */
|
|
35
|
+
const UNSAFE_CHILD_CHARS = /[<>&{}]/;
|
|
36
|
+
|
|
37
|
+
function toJSX(
|
|
38
|
+
node: unknown,
|
|
39
|
+
depth: number,
|
|
40
|
+
escape: boolean,
|
|
41
|
+
pretty: boolean,
|
|
42
|
+
indent: number,
|
|
43
|
+
): string {
|
|
26
44
|
if (depth > MAX_DEPTH) return "";
|
|
27
45
|
if (node == null || typeof node === "boolean") return "";
|
|
28
|
-
if (typeof node === "string") return node;
|
|
46
|
+
if (typeof node === "string") return formatChildText(node, escape);
|
|
29
47
|
if (typeof node === "number") return String(node);
|
|
30
|
-
if (Array.isArray(node))
|
|
31
|
-
|
|
48
|
+
if (Array.isArray(node)) {
|
|
49
|
+
if (pretty) {
|
|
50
|
+
return collectBlockLines(node, depth, escape, indent).join("\n");
|
|
51
|
+
}
|
|
52
|
+
return node
|
|
53
|
+
.map((child) => toJSX(child, depth + 1, escape, false, 0))
|
|
54
|
+
.join("");
|
|
55
|
+
}
|
|
32
56
|
if (typeof node !== "object") return "";
|
|
33
57
|
|
|
34
58
|
const {
|
|
35
59
|
[TYPE_KEY]: type,
|
|
60
|
+
$key,
|
|
36
61
|
children,
|
|
37
62
|
...props
|
|
38
63
|
} = node as Record<string, unknown>;
|
|
39
64
|
if (typeof type !== "string") return "";
|
|
40
65
|
|
|
41
|
-
const attrs =
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
66
|
+
const attrs =
|
|
67
|
+
formatAttr("key", $key) +
|
|
68
|
+
Object.entries(props)
|
|
69
|
+
.map(([key, value]) => formatAttr(key, value))
|
|
70
|
+
.join("");
|
|
71
|
+
|
|
72
|
+
if (!pretty) {
|
|
73
|
+
const inner =
|
|
74
|
+
children === undefined
|
|
75
|
+
? ""
|
|
76
|
+
: toJSX(children, depth + 1, escape, false, 0);
|
|
77
|
+
|
|
78
|
+
return inner === ""
|
|
79
|
+
? `<${type}${attrs} />`
|
|
80
|
+
: `<${type}${attrs}>${inner}</${type}>`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pad = " ".repeat(indent);
|
|
84
|
+
|
|
85
|
+
if (children === undefined || !hasElementChild(children)) {
|
|
86
|
+
const inner =
|
|
87
|
+
children === undefined
|
|
88
|
+
? ""
|
|
89
|
+
: toJSX(children, depth + 1, escape, false, 0);
|
|
90
|
+
return inner === ""
|
|
91
|
+
? `${pad}<${type}${attrs} />`
|
|
92
|
+
: `${pad}<${type}${attrs}>${inner}</${type}>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lines = collectBlockLines(children, depth + 1, escape, indent + 1);
|
|
96
|
+
if (lines.length === 0) {
|
|
97
|
+
return `${pad}<${type}${attrs} />`;
|
|
98
|
+
}
|
|
99
|
+
return `${pad}<${type}${attrs}>\n${lines.join("\n")}\n${pad}</${type}>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasElementChild(node: unknown, depth = 0): boolean {
|
|
103
|
+
if (depth > MAX_DEPTH) return false;
|
|
104
|
+
if (Array.isArray(node))
|
|
105
|
+
return node.some((child) => hasElementChild(child, depth + 1));
|
|
106
|
+
return (
|
|
107
|
+
node != null &&
|
|
108
|
+
typeof node === "object" &&
|
|
109
|
+
typeof (node as Record<string, unknown>)[TYPE_KEY] === "string"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectBlockLines(
|
|
114
|
+
node: unknown,
|
|
115
|
+
depth: number,
|
|
116
|
+
escape: boolean,
|
|
117
|
+
indent: number,
|
|
118
|
+
): string[] {
|
|
119
|
+
if (depth > MAX_DEPTH) return [];
|
|
120
|
+
if (node == null || typeof node === "boolean") return [];
|
|
121
|
+
if (Array.isArray(node)) {
|
|
122
|
+
return node.flatMap((child) =>
|
|
123
|
+
collectBlockLines(child, depth + 1, escape, indent),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (typeof node === "string") {
|
|
127
|
+
return [`${" ".repeat(indent)}${formatChildText(node, escape)}`];
|
|
128
|
+
}
|
|
129
|
+
if (typeof node === "number") {
|
|
130
|
+
return [`${" ".repeat(indent)}${String(node)}`];
|
|
131
|
+
}
|
|
132
|
+
const rendered = toJSX(node, depth, escape, true, indent);
|
|
133
|
+
return rendered === "" ? [] : [rendered];
|
|
134
|
+
}
|
|
45
135
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
136
|
+
function formatChildText(value: string, escape: boolean): string {
|
|
137
|
+
return escape && UNSAFE_CHILD_CHARS.test(value)
|
|
138
|
+
? `{${JSON.stringify(value)}}`
|
|
139
|
+
: value;
|
|
49
140
|
}
|
|
50
141
|
|
|
51
142
|
/** Formats one prop as a JSX attribute (`id="x"`, `count={3}`, `open`, …). */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export { renderGenerativeUI } from "./renderGenerativeUI";
|
|
2
|
+
export { generativeUIToJSX } from "./generativeUIToJSX";
|
|
3
|
+
export type { GenerativeUIToJSXOptions } from "./generativeUIToJSX";
|
|
4
|
+
export { buildPresentParameters } from "./buildPresentParameters";
|
|
5
|
+
export { defineGenerativeComponents } from "./defineGenerativeComponents";
|
|
6
|
+
export { TYPE_KEY } from "./constants";
|
|
7
|
+
export { normalizeUINode, normalizeSpec } from "./ir";
|
|
8
|
+
export {
|
|
9
|
+
TEXT_SIZES,
|
|
10
|
+
IMAGE_SIZE_TOKENS,
|
|
11
|
+
WEIGHTS,
|
|
12
|
+
COLORS,
|
|
13
|
+
ALIGNS,
|
|
14
|
+
JUSTIFIES,
|
|
15
|
+
BUTTON_STYLES,
|
|
16
|
+
ALERT_TONES,
|
|
17
|
+
ICON_NAMES,
|
|
18
|
+
} from "./ir";
|
|
19
|
+
export { defaultGenerativeUILibrary } from "./vocabulary";
|
|
20
|
+
export { createActionRegistry, emptyActionRegistry } from "./actionRegistry";
|
|
21
|
+
export type {
|
|
22
|
+
ActionHandler,
|
|
23
|
+
ActionRegistry,
|
|
24
|
+
ActionDispatchContext,
|
|
25
|
+
} from "./actionRegistry";
|
|
26
|
+
export type {
|
|
27
|
+
JSONGenerativeUIOptions,
|
|
28
|
+
PresentTool,
|
|
29
|
+
PresentToolOptions,
|
|
30
|
+
PromptUserTool,
|
|
31
|
+
} from "./JSONGenerativeUI.shared";
|
|
32
|
+
export type {
|
|
33
|
+
GenerativeUILibrary,
|
|
34
|
+
GenerativeUIComponent,
|
|
35
|
+
GenerativeUIElement,
|
|
36
|
+
GenerativeUIProps,
|
|
37
|
+
GenerativeUINode,
|
|
38
|
+
GenerativeUIStatus,
|
|
39
|
+
GenerativeUIRenderContext,
|
|
40
|
+
GenerativeUIAction,
|
|
41
|
+
GenerativeUIDispatch,
|
|
42
|
+
} from "./types";
|
|
43
|
+
export type {
|
|
44
|
+
UINode,
|
|
45
|
+
UIElement,
|
|
46
|
+
UIChildren,
|
|
47
|
+
LegacyComponentNode,
|
|
48
|
+
Action,
|
|
49
|
+
UISpec,
|
|
50
|
+
NormalizedUINode,
|
|
51
|
+
NormalizedUIElement,
|
|
52
|
+
TextSize,
|
|
53
|
+
ImageSize,
|
|
54
|
+
Weight,
|
|
55
|
+
Color,
|
|
56
|
+
Align,
|
|
57
|
+
Justify,
|
|
58
|
+
ButtonStyle,
|
|
59
|
+
AlertTone,
|
|
60
|
+
IconName,
|
|
61
|
+
} from "./ir";
|
package/src/index.ts
CHANGED
|
@@ -1,26 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// under the `react-server` condition and the client variant (adds render/execute)
|
|
4
|
-
// otherwise. Everything else here is isomorphic, so this single entry serves
|
|
5
|
-
// both conditions — no per-build index barrel needed.
|
|
6
|
-
export { JSONGenerativeUI } from "@assistant-ui/react-generative-ui/internal-json";
|
|
7
|
-
export { renderGenerativeUI } from "./renderGenerativeUI";
|
|
8
|
-
export { generativeUIToJSX } from "./generativeUIToJSX";
|
|
9
|
-
export { buildPresentParameters } from "./buildPresentParameters";
|
|
10
|
-
export { defineGenerativeComponents } from "./defineGenerativeComponents";
|
|
11
|
-
export { TYPE_KEY } from "./constants";
|
|
12
|
-
export type {
|
|
13
|
-
JSONGenerativeUIOptions,
|
|
14
|
-
PresentTool,
|
|
15
|
-
PresentToolOptions,
|
|
16
|
-
PromptUserTool,
|
|
17
|
-
} from "./JSONGenerativeUI.shared";
|
|
18
|
-
export type {
|
|
19
|
-
GenerativeUILibrary,
|
|
20
|
-
GenerativeUIComponent,
|
|
21
|
-
GenerativeUIElement,
|
|
22
|
-
GenerativeUIProps,
|
|
23
|
-
GenerativeUINode,
|
|
24
|
-
GenerativeUIStatus,
|
|
25
|
-
GenerativeUIRenderContext,
|
|
26
|
-
} from "./types";
|
|
1
|
+
export { JSONGenerativeUI } from "./JSONGenerativeUI.client";
|
|
2
|
+
export * from "./index.shared";
|
package/src/ir.test.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
normalizeSpec,
|
|
4
|
+
normalizeUINode,
|
|
5
|
+
type NormalizedUIElement,
|
|
6
|
+
type NormalizedUINode,
|
|
7
|
+
} from "./ir";
|
|
8
|
+
|
|
9
|
+
/** Narrows a normalized node to its element form for property assertions. */
|
|
10
|
+
const asElement = (node: NormalizedUINode): NormalizedUIElement => {
|
|
11
|
+
if (node === null || typeof node === "string" || typeof node === "number") {
|
|
12
|
+
throw new Error(`expected an element, got ${node as unknown as string}`);
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(node)) {
|
|
15
|
+
throw new Error("expected an element, got an array");
|
|
16
|
+
}
|
|
17
|
+
return node as NormalizedUIElement;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("normalizeUINode", () => {
|
|
21
|
+
describe("text leaf", () => {
|
|
22
|
+
it("passes a string through", () => {
|
|
23
|
+
expect(normalizeUINode("hello")).toBe("hello");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("legacy component shape", () => {
|
|
28
|
+
it("maps component + nested props to the canonical element", () => {
|
|
29
|
+
const node = asElement(
|
|
30
|
+
normalizeUINode({
|
|
31
|
+
component: "Card",
|
|
32
|
+
props: { padding: 4 },
|
|
33
|
+
children: ["inside"],
|
|
34
|
+
key: "c1",
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(node).toEqual({
|
|
39
|
+
type: "Card",
|
|
40
|
+
props: { padding: 4 },
|
|
41
|
+
children: ["inside"],
|
|
42
|
+
key: "c1",
|
|
43
|
+
action: undefined,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defaults missing props to an empty bag and carries no action", () => {
|
|
48
|
+
const node = asElement(normalizeUINode({ component: "Divider" }));
|
|
49
|
+
expect(node.props).toEqual({});
|
|
50
|
+
expect(node.children).toBeUndefined();
|
|
51
|
+
expect(node.action).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("flat $type shape", () => {
|
|
56
|
+
it("keeps inline props and strips reserved keys", () => {
|
|
57
|
+
const node = asElement(
|
|
58
|
+
normalizeUINode({
|
|
59
|
+
$type: "Text",
|
|
60
|
+
value: "hi",
|
|
61
|
+
size: "md",
|
|
62
|
+
weight: "semibold",
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(node.type).toBe("Text");
|
|
67
|
+
expect(node.props).toEqual({
|
|
68
|
+
value: "hi",
|
|
69
|
+
size: "md",
|
|
70
|
+
weight: "semibold",
|
|
71
|
+
});
|
|
72
|
+
expect(node.children).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("lets a component use `type` as an ordinary prop (no collision)", () => {
|
|
76
|
+
const node = asElement(
|
|
77
|
+
normalizeUINode({
|
|
78
|
+
$type: "Button",
|
|
79
|
+
type: "submit",
|
|
80
|
+
label: "Go",
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(node.type).toBe("Button");
|
|
85
|
+
expect(node.props).toEqual({ type: "submit", label: "Go" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("routes a flat `$type` node with a `component` prop to the flat branch, not the legacy one", () => {
|
|
89
|
+
const node = asElement(
|
|
90
|
+
normalizeUINode({
|
|
91
|
+
$type: "Schema",
|
|
92
|
+
component: "display-name",
|
|
93
|
+
label: "Go",
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(node.type).toBe("Schema");
|
|
98
|
+
expect(node.props).toEqual({ component: "display-name", label: "Go" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("threads $action and $key through as action/key", () => {
|
|
102
|
+
const node = asElement(
|
|
103
|
+
normalizeUINode({
|
|
104
|
+
$type: "Button",
|
|
105
|
+
$key: "buy",
|
|
106
|
+
label: "Purchase",
|
|
107
|
+
$action: { type: "purchase", itemId: "sku-1" },
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(node.key).toBe("buy");
|
|
112
|
+
expect(node.action).toEqual({ type: "purchase", itemId: "sku-1" });
|
|
113
|
+
expect(node.props).toEqual({ label: "Purchase" });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("strips the whole $-prefixed namespace from props (not just the named keys)", () => {
|
|
117
|
+
const node = asElement(
|
|
118
|
+
normalizeUINode({
|
|
119
|
+
$type: "Card",
|
|
120
|
+
$status: "done",
|
|
121
|
+
$custom: 1,
|
|
122
|
+
title: "hi",
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(node.props).toEqual({ title: "hi" });
|
|
127
|
+
expect("$status" in node.props).toBe(false);
|
|
128
|
+
expect("$custom" in node.props).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("nesting", () => {
|
|
133
|
+
it("recurses through children of both shapes", () => {
|
|
134
|
+
const node = asElement(
|
|
135
|
+
normalizeUINode({
|
|
136
|
+
$type: "Col",
|
|
137
|
+
children: [
|
|
138
|
+
{ component: "Text", props: { value: "a" } },
|
|
139
|
+
{ $type: "Text", value: "b" },
|
|
140
|
+
"leaf",
|
|
141
|
+
],
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(node.children).toEqual([
|
|
146
|
+
{
|
|
147
|
+
type: "Text",
|
|
148
|
+
props: { value: "a" },
|
|
149
|
+
children: undefined,
|
|
150
|
+
key: undefined,
|
|
151
|
+
action: undefined,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
type: "Text",
|
|
155
|
+
props: { value: "b" },
|
|
156
|
+
children: undefined,
|
|
157
|
+
key: undefined,
|
|
158
|
+
action: undefined,
|
|
159
|
+
},
|
|
160
|
+
"leaf",
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("malformed input", () => {
|
|
166
|
+
it("resolves a node without $type or component to null", () => {
|
|
167
|
+
expect(normalizeUINode({ foo: "bar" } as unknown as never)).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("resolves non-record input: null and boolean to null, number to a text leaf", () => {
|
|
171
|
+
expect(normalizeUINode(null as unknown as never)).toBeNull();
|
|
172
|
+
expect(normalizeUINode(true as unknown as never)).toBeNull();
|
|
173
|
+
expect(normalizeUINode(42 as unknown as never)).toBe(42);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("normalizeSpec", () => {
|
|
179
|
+
it("normalizes a single root", () => {
|
|
180
|
+
expect(normalizeSpec({ $type: "Text", value: "x" }).root).toEqual({
|
|
181
|
+
type: "Text",
|
|
182
|
+
props: { value: "x" },
|
|
183
|
+
children: undefined,
|
|
184
|
+
key: undefined,
|
|
185
|
+
action: undefined,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("normalizes a list root", () => {
|
|
190
|
+
const { root } = normalizeSpec([{ $type: "Text", value: "a" }, "b"]);
|
|
191
|
+
expect(Array.isArray(root)).toBe(true);
|
|
192
|
+
expect(root).toEqual([
|
|
193
|
+
{
|
|
194
|
+
type: "Text",
|
|
195
|
+
props: { value: "a" },
|
|
196
|
+
children: undefined,
|
|
197
|
+
key: undefined,
|
|
198
|
+
action: undefined,
|
|
199
|
+
},
|
|
200
|
+
"b",
|
|
201
|
+
]);
|
|
202
|
+
});
|
|
203
|
+
});
|