生成式 UI 让 AI 能够从自然语言生成完整的用户界面 提示词。AI 不再在聊天气泡中渲染文本响应,而是输出 **is** UI:表单、卡片、仪表板等。开发者定义哪些组件 可用("目录"),AI 将它们组合成有效的 UI 树。
此模式使用 json-render,生成式 UI 框架, 来定义组件目录、使用 AI 生成规范,并安全地跨 React、Vue、Svelte 和 Angular 渲染。
工作原理
- **定义目录**:声明 AI 可以使用哪些组件,带有类型化属性
- **提示 AI**:用自然语言描述你想要的 UI
- **AI 生成规范**:描述组件树的 JSON 文档
- **安全渲染**:json-render 的
Renderer使用你的组件渲染规范
目录充当护栏:AI 只能使用你定义的组件, 属性与你的模式匹配。输出始终可预测且安全。
定义组件目录
目录描述了 AI 允许使用的每个组件。每个组件都有一个 Zod 模式用于其属性,以及 AI 阅读以了解何时 使用它的描述:
const catalog = defineCatalog(schema, {
components: {
Card: {
description: "A card container with optional title and padding",
props: z.object({
title: z.string().optional(),
padding: z.enum(["sm", "md", "lg"]).optional(),
}),
},
Stack: {
description: "Layout children vertically or horizontally with consistent spacing",
props: z.object({
direction: z.enum(["vertical", "horizontal"]).optional(),
gap: z.enum(["sm", "md", "lg"]).optional(),
}),
},
TextInput: {
description: "A text input field with optional label and placeholder",
props: z.object({
label: z.string().optional(),
placeholder: z.string().optional(),
type: z.enum(["text", "email", "password", "number", "textarea"]).optional(),
}),
},
Button: {
description: "A clickable button with label and style variants",
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "ghost", "link"]).optional(),
fullWidth: z.boolean().optional(),
}),
},
},
actions: {},
});
构建组件注册表
注册表将每个目录组件映射到其实际渲染实现。 使用 defineRegistry 获取目录属性和 组件函数之间的类型安全绑定:
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
{props.title && <h2>{props.title}</h2>}
{children}
),
Stack: ({ props, children }) => (
{children}
),
TextInput: ({ props }) => (
{props.label && <label>{props.label}</label>}
<input type={props.type ?? "text"} placeholder={props.placeholder} />
),
Button: ({ props }) => (
<button className={props.variant ?? "primary"}>
{props.label}
</button>
),
},
});
<script setup lang="ts">
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) =>
h("div", { class: "card" }, [
props.title ? h("h2", null, props.title) : null,
children,
]),
Stack: ({ props, children }) =>
h("div", { class: `stack stack-${props.direction ?? "vertical"} gap-${props.gap ?? "md"}` }, children),
TextInput: ({ props }) =>
h("div", null, [
props.label ? h("label", null, props.label) : null,
h("input", { type: props.type ?? "text", placeholder: props.placeholder }),
]),
Button: ({ props }) =>
h("button", { class: props.variant ?? "primary" }, props.label),
},
});
</script>
连接到代理
代理使用结构化输出来返回 json-render 规范。设置 useStream 并附上你的代理的助手 ID,然后从 AI 消息中提取规范 tool_calls:
function GenerativeUI() {
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "generative_ui",
});
const aiMessage = stream.messages.find(AIMessage.isInstance);
const rawSpec = aiMessage?.tool_calls?.[0]?.args;
// ... filter and render (see streaming section below)
}
<script setup lang="ts">
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "generative_ui",
});
const aiMessage = computed(() => stream.messages.value.find(AIMessage.isInstance));
const rawSpec = computed(() => aiMessage.value?.tool_calls?.[0]?.args);
</script>
<script lang="ts">
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "generative_ui",
});
const aiMessage = $derived(stream.messages.find((m) => AIMessage.isInstance(m)));
const rawSpec = $derived(aiMessage?.tool_calls?.[0]?.args);
</script>
@Component({
selector: "app-generative-ui",
template: `...`,
})
stream = injectStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "generative_ui",
});
get rawSpec() {
const ai = this.stream.messages().find(AIMessage.isInstance);
return ai?.tool_calls?.[0]?.args;
}
}
流式传输和渐进式渲染
在流式传输期间,规范是逐步构建的。元素一次到达一个, 可能最初缺少 type or props。过滤仅完整的元素 并传递 loading={true} 到 Renderer,它告诉组件静默跳过 尚未到达的子元素。UI 逐个组件构建:
/*
* Filter the streamed spec to only include elements with valid type/props,
* enabling progressive rendering as the AI response builds up. Passing
* loading={true} to the Renderer tells it to skip missing children silently.
*/
const spec = (() => {
if (!rawSpec?.root || !rawSpec?.elements) return null;
const rootEl = rawSpec.elements[rawSpec.root];
if (!rootEl?.type || rootEl?.props == null) return null;
const safeElements = {};
for (const [key, el] of Object.entries(rawSpec.elements)) {
if (el?.type && el?.props != null) {
safeElements[key] = el;
}
}
return { root: rawSpec.root, elements: safeElements };
})();
return (
<>
{spec && (
)}
</>
);
规范格式
AI代理生成一个扁平的JSON规范,包含 root 键指向 根元素和一个 elements 映射,包含所有组件:
{
"root": "login-card",
"elements": {
"login-card": {
"type": "Card",
"props": { "title": "Login" },
"children": ["login-stack"]
},
"login-stack": {
"type": "Stack",
"props": { "direction": "vertical", "gap": "md" },
"children": ["email-input", "password-input", "submit-btn"]
},
"email-input": {
"type": "TextInput",
"props": { "label": "Email", "placeholder": "Enter your email", "type": "email" },
"children": []
},
"password-input": {
"type": "TextInput",
"props": { "label": "Password", "placeholder": "Enter your password", "type": "password" },
"children": []
},
"submit-btn": {
"type": "Button",
"props": { "label": "Sign In", "variant": "primary", "fullWidth": true },
"children": []
}
}
}
每个元素通过ID引用其子元素,像 TextInput 和 Button 具有空的 children arrays.
最佳实践
- **使用描述性组件描述**:AI使用这些来理解何时 使用每个组件。清晰的描述可以带来更好的UI生成。 - **渲染前验证**:始终检查元素是否具有有效的 type 和 non-null props ,然后再传递给渲染器,因为流式传输会传递部分数据。 - **为流式传输设计**:传递 loading={true} 在流式传输期间,以便渲染器 优雅地处理尚未到达的子元素。用户看到UI逐渐构建 实时呈现,而无需等待完整响应。 - **使用设计令牌进行样式设计**:使用CSS自定义属性,使渲染的组件 自动适应浅色和深色主题。 - **使用JSONUIProvider包装**: Renderer 必须位于 JSONUIProvider 中,以访问json-render的内部上下文,包括状态、可见性和操作。