生成式UI允许AI从自然语言提示生成完整的用户界面。AI的输出不再是聊天气泡中的文本响应,而是界面本身:表单、卡片、仪表盘等。开发者定义可用的组件(即“目录”),AI将它们组合成一个有效的UI树。
此模式使用 json-render(生成式UI框架)来定义组件目录、通过AI生成规范,并在 React、Vue、Svelte 和 Angular 中安全地渲染它们。
工作原理
- 定义目录:声明AI可以使用的组件,包含类型化的props
- 提示AI:用自然语言描述你想要的UI
- AI生成规范:一个描述组件树的JSON文档
- 安全渲染:json-render的
Renderer使用你的组件渲染该规范
目录充当护栏:AI只能使用你定义的组件,且props必须符合你的模式。输出始终是可预测且安全的。
定义组件目录
目录描述了AI被允许使用的所有组件。每个组件都有一个用于其props的Zod模式,以及一个AI读取以理解何时使用它的描述:
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";
const catalog = defineCatalog(schema, {
components: {
Card: {
description: "一个可选标题和内边距的卡片容器",
props: z.object({
title: z.string().optional(),
padding: z.enum(["sm", "md", "lg"]).optional(),
}),
},
TextInput: {
description: "一个可选标签和占位符的文本输入字段",
props: z.object({
label: z.string().optional(),
placeholder: z.string().optional(),
type: z.enum(["text", "email", "password", "number", "textarea"]).optional(),
}),
},
Button: {
description: "一个带有标签和样式变体的可点击按钮",
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "ghost", "link"]).optional(),
fullWidth: z.boolean().optional(),
}),
},
},
actions: {},
});
保持目录聚焦。只包含AI在该用例中需要的组件。
较小的目录比大而全的目录能产生更好的结果。
构建组件注册表
注册表将每个目录组件映射到其实际的渲染实现。
使用 defineRegistry 来获得目录props与你的组件函数之间的类型安全绑定:
import { defineRegistry, Renderer, JSONUIProvider } from "@json-render/react";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<div className="card">
{props.title && <h2>{props.title}</h2>}
{children}
</div>
),
TextInput: ({ props }) => (
<div>
{props.label && <label>{props.label}</label>}
<input type={props.type ?? "text"} placeholder={props.placeholder} />
</div>
),
Button: ({ props }) => (
<button className={props.variant ?? "primary"}>
{props.label}
</button>
),
},
});
连接到代理
代理使用结构化输出来返回一个json-render规范。使用你的代理的助手ID设置 useStream,然后从AI消息的 tool_calls 中提取规范:
import { useStream } from "@langchain/react";
import { AIMessage } from "@langchain/core/messages";
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;
// ... 过滤和渲染(参见下面的流式处理部分)
}
流式处理与渐进式渲染
在流式处理期间,规范是逐步构建的。元素一个接一个地到达,最初可能缺少 type 或 props。过滤出完整的元素,并将 loading={true} 传递给 Renderer,这告诉它静默跳过尚未到达的子元素。UI会逐个组件地构建起来:
/*
* 过滤流式规范,只包含具有有效 type/props 的元素,
* 从而在AI响应构建过程中实现渐进式渲染。将
* loading={true} 传递给 Renderer 告诉它静默跳过缺失的子元素。
*/
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 && (
<JSONUIProvider registry={registry}>
<Renderer spec={spec} registry={registry} loading={stream.isLoading} />
</JSONUIProvider>
)}
</>
);
JSONUIProvider 是设置 json-render 内部上下文提供者(状态、可见性、验证、操作)所必需的。Renderer 组件必须在其内部渲染。
规范格式
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 数组。
最佳实践
- 使用描述性的组件描述:AI使用这些描述来理解何时使用每个组件。清晰的描述能带来更好的UI生成。
- 渲染前进行验证:在传递给Renderer之前,始终检查元素是否具有有效的
type 和非空的 props,因为流式处理传递的是部分数据。
- 为流式处理而设计:在流式处理期间传递
loading={true},这样Renderer可以优雅地处理尚未到达的子元素。用户可以实时看到UI的构建过程,而不是等待完整的响应。
- 使用设计令牌进行样式设置:使用CSS自定义属性,使渲染的组件能够自动适应亮色和暗色主题。
- 使用JSONUIProvider包装:
Renderer 必须位于 JSONUIProvider 内部,才能访问json-render用于状态、可见性和操作的内部上下文。
将这些文档连接到 Claude、VSCode 等,通过 MCP 获取实时答案。