@assistant-ui/react-generative-ui 0.0.3 → 0.0.4
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderGenerativeUI.d.ts","names":[],"sources":["../src/renderGenerativeUI.tsx"],"mappings":";;;;;;
|
|
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,4 +1,5 @@
|
|
|
1
1
|
import { TYPE_KEY } from "./constants.js";
|
|
2
|
+
import { getPartialJsonObjectMeta } from "assistant-stream/utils";
|
|
2
3
|
import { Fragment } from "react";
|
|
3
4
|
import { jsx } from "react/jsx-runtime";
|
|
4
5
|
//#region src/renderGenerativeUI.tsx
|
|
@@ -13,7 +14,8 @@ const DEFAULT_CONTEXT = { status: "done" };
|
|
|
13
14
|
* recursively so components can nest.
|
|
14
15
|
*/
|
|
15
16
|
function renderGenerativeUI(node, library, context = DEFAULT_CONTEXT) {
|
|
16
|
-
|
|
17
|
+
const meta = getPartialJsonObjectMeta(node);
|
|
18
|
+
return renderNode(normalizeNode(node, meta?.state === "partial" ? meta.partialPath : void 0), library, context);
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* The deepest tree we normalize. The input comes from the model, so a runaway
|
|
@@ -22,21 +24,32 @@ function renderGenerativeUI(node, library, context = DEFAULT_CONTEXT) {
|
|
|
22
24
|
* bounds rendering too, since it only walks the normalized tree.
|
|
23
25
|
*/
|
|
24
26
|
const MAX_DEPTH = 64;
|
|
25
|
-
/**
|
|
26
|
-
|
|
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) {
|
|
27
35
|
if (depth > MAX_DEPTH) return null;
|
|
28
36
|
if (node == null || typeof node === "boolean") return null;
|
|
29
37
|
if (typeof node === "string" || typeof node === "number") return node;
|
|
30
|
-
if (Array.isArray(node)) return node.map((child) => normalizeNode(child, depth + 1));
|
|
38
|
+
if (Array.isArray(node)) return node.map((child, index) => normalizeNode(child, descend(partialPath, String(index)), depth + 1));
|
|
31
39
|
if (typeof node !== "object") return null;
|
|
32
40
|
const { [TYPE_KEY]: type, ...props } = node;
|
|
33
41
|
if (typeof type !== "string") return null;
|
|
34
|
-
if (
|
|
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);
|
|
35
44
|
return {
|
|
36
45
|
type,
|
|
37
46
|
props
|
|
38
47
|
};
|
|
39
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;
|
|
52
|
+
}
|
|
40
53
|
function renderNode(node, library, context) {
|
|
41
54
|
if (node == null || typeof node === "boolean") return null;
|
|
42
55
|
if (typeof node === "string" || typeof node === "number") return node;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderGenerativeUI.js","names":[],"sources":["../src/renderGenerativeUI.tsx"],"sourcesContent":["import { 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 return renderNode(normalizeNode(node), 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
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-generative-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Generative UI components for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"generative-ui",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"react-dom": "^19.2.7",
|
|
59
59
|
"vitest": "^4.1.8",
|
|
60
60
|
"zod": "^4.4.3",
|
|
61
|
-
"@assistant-ui/react": "0.14.
|
|
61
|
+
"@assistant-ui/react": "0.14.18",
|
|
62
62
|
"@assistant-ui/x-buildutils": "0.0.12"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { parsePartialJsonObject } from "assistant-stream/utils";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { renderGenerativeUI } from "./renderGenerativeUI";
|
|
5
6
|
import { buildPresentParameters } from "./buildPresentParameters";
|
|
@@ -90,6 +91,41 @@ describe("renderGenerativeUI", () => {
|
|
|
90
91
|
expect(html).toBe("");
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
it("holds back a node whose `$type` is still streaming", () => {
|
|
95
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
96
|
+
try {
|
|
97
|
+
for (const argsText of ['{"$type": "', '{"$type": "Tex']) {
|
|
98
|
+
const html = renderToStaticMarkup(
|
|
99
|
+
<>
|
|
100
|
+
{renderGenerativeUI(parsePartialJsonObject(argsText), library, {
|
|
101
|
+
status: "streaming",
|
|
102
|
+
})}
|
|
103
|
+
</>,
|
|
104
|
+
);
|
|
105
|
+
expect(html).toBe("");
|
|
106
|
+
}
|
|
107
|
+
expect(error).not.toHaveBeenCalled();
|
|
108
|
+
} finally {
|
|
109
|
+
error.mockRestore();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("renders completed siblings while a nested `$type` streams", () => {
|
|
114
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
115
|
+
try {
|
|
116
|
+
const args = parsePartialJsonObject(
|
|
117
|
+
'{"$type": "Live", "label": "hi", "children": [{"$type": "Liv',
|
|
118
|
+
);
|
|
119
|
+
const html = renderToStaticMarkup(
|
|
120
|
+
<>{renderGenerativeUI(args, library, { status: "streaming" })}</>,
|
|
121
|
+
);
|
|
122
|
+
expect(html).toBe('<span data-status="streaming">hi</span>');
|
|
123
|
+
expect(error).not.toHaveBeenCalled();
|
|
124
|
+
} finally {
|
|
125
|
+
error.mockRestore();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
93
129
|
it("renders nothing for a node without a resolved type", () => {
|
|
94
130
|
const html = renderToStaticMarkup(<>{renderGenerativeUI({}, library)}</>);
|
|
95
131
|
expect(html).toBe("");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPartialJsonObjectMeta } from "assistant-stream/utils";
|
|
1
2
|
import { Fragment, type ReactNode } from "react";
|
|
2
3
|
import { TYPE_KEY } from "./constants";
|
|
3
4
|
import type {
|
|
@@ -23,7 +24,12 @@ export function renderGenerativeUI(
|
|
|
23
24
|
library: GenerativeUILibrary,
|
|
24
25
|
context: GenerativeUIRenderContext = DEFAULT_CONTEXT,
|
|
25
26
|
): ReactNode {
|
|
26
|
-
|
|
27
|
+
// Tool args are parsed incrementally, and the parse meta records which path
|
|
28
|
+
// is still mid-arrival, so normalization can hold back a node whose `$type`
|
|
29
|
+
// string has not finished streaming.
|
|
30
|
+
const meta = getPartialJsonObjectMeta(node as Record<symbol, unknown>);
|
|
31
|
+
const partialPath = meta?.state === "partial" ? meta.partialPath : undefined;
|
|
32
|
+
return renderNode(normalizeNode(node, partialPath), library, context);
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -34,26 +40,52 @@ export function renderGenerativeUI(
|
|
|
34
40
|
*/
|
|
35
41
|
const MAX_DEPTH = 64;
|
|
36
42
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Converts the flat wire form into a normalized {@link GenerativeUINode}.
|
|
45
|
+
*
|
|
46
|
+
* `partialPath` is the remaining segment of the parse meta's partial path
|
|
47
|
+
* relative to `node` (`undefined` once the walk leaves the partial frontier,
|
|
48
|
+
* i.e. everything below is complete).
|
|
49
|
+
*/
|
|
50
|
+
function normalizeNode(
|
|
51
|
+
node: unknown,
|
|
52
|
+
partialPath: readonly string[] | undefined,
|
|
53
|
+
depth = 0,
|
|
54
|
+
): GenerativeUINode {
|
|
39
55
|
if (depth > MAX_DEPTH) return null;
|
|
40
56
|
if (node == null || typeof node === "boolean") return null;
|
|
41
57
|
if (typeof node === "string" || typeof node === "number") return node;
|
|
42
58
|
if (Array.isArray(node))
|
|
43
|
-
return node.map((child) =>
|
|
59
|
+
return node.map((child, index) =>
|
|
60
|
+
normalizeNode(child, descend(partialPath, String(index)), depth + 1),
|
|
61
|
+
);
|
|
44
62
|
if (typeof node !== "object") return null;
|
|
45
63
|
|
|
46
64
|
const { [TYPE_KEY]: type, ...props } = node as Record<string, unknown>;
|
|
47
|
-
// Args stream in incrementally; a node whose `$type` has not arrived yet
|
|
48
|
-
// not an error, it just
|
|
65
|
+
// Args stream in incrementally; a node whose `$type` has not arrived yet
|
|
66
|
+
// (or whose `$type` string is still mid-arrival) is not an error, it just
|
|
67
|
+
// isn't renderable.
|
|
49
68
|
if (typeof type !== "string") return null;
|
|
69
|
+
if (partialPath?.length === 1 && partialPath[0] === TYPE_KEY) return null;
|
|
50
70
|
|
|
51
71
|
if ("children" in props) {
|
|
52
|
-
props["children"] = normalizeNode(
|
|
72
|
+
props["children"] = normalizeNode(
|
|
73
|
+
props["children"],
|
|
74
|
+
descend(partialPath, "children"),
|
|
75
|
+
depth + 1,
|
|
76
|
+
);
|
|
53
77
|
}
|
|
54
78
|
return { type, props } as GenerativeUIElement;
|
|
55
79
|
}
|
|
56
80
|
|
|
81
|
+
/** Steps the partial path down into `key`; siblings of the path are complete. */
|
|
82
|
+
function descend(
|
|
83
|
+
partialPath: readonly string[] | undefined,
|
|
84
|
+
key: string,
|
|
85
|
+
): readonly string[] | undefined {
|
|
86
|
+
return partialPath?.[0] === key ? partialPath.slice(1) : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
function renderNode(
|
|
58
90
|
node: GenerativeUINode,
|
|
59
91
|
library: GenerativeUILibrary,
|