Generative UI 让 AI 根据自然语言提示生成完整的用户界面。AI 输出不再是在聊天气泡中渲染的文本响应,AI 输出本身就是 UI:表单、卡片、仪表板等。开发者定义哪些组件可用(即“catalog”),AI 将它们组合成有效的 UI 树。 这个模式使用 Generative UI 框架 json-render 定义组件 catalog、用 AI 生成 spec,并在 React、Vue、Svelte 和 Angular 中安全渲染。

工作原理

  1. 定义 catalog:声明 AI 可以使用哪些组件,并为 props 提供类型
  2. 提示 AI:用自然语言描述你想要的 UI
  3. AI 生成 spec:描述组件树的 JSON 文档
  4. 安全渲染:json-render 的 Renderer 使用你的组件渲染 spec
Catalog 充当护栏:AI 只能使用你定义的组件,并且 props 必须与你的 schema 匹配。输出始终可预测且安全。

定义组件 catalog

Catalog 描述 AI 允许使用的每个组件。每个组件都有一个用于 props 的 Zod schema,以及一段供 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: "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: {},
});
保持 catalog 聚焦。只包含该用例需要的组件。较小的 catalog 通常比包罗万象的做法产生更好的结果。

构建组件 registry

Registry 将每个 catalog 组件映射到实际的渲染实现。使用 defineRegistry 在 catalog 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>
    ),
    Stack: ({ props, children }) => (
      <div className={`stack stack-${props.direction ?? "vertical"} gap-${props.gap ?? "md"}`}>
        {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 spec。用代理的 assistant ID 设置 useStream,然后从 AI 消息的 tool_calls 中提取 spec:
import { useStream } from "@langchain/react";
import { AIMessage } from "langchain";

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)
}

渐进式流式传输和渲染

在流式传输过程中,spec 会逐步构建。元素会逐个到达,最初可能缺少 typeprops。只筛选完整元素,并向 Renderer 传入 loading={true},让它静默跳过尚未到达的子节点。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 && (
      <JSONUIProvider registry={registry}>
        <Renderer spec={spec} registry={registry} loading={stream.isLoading} />
      </JSONUIProvider>
    )}
  </>
);
需要使用 JSONUIProvider 设置 json-render 的内部 context providers(state、visibility、validation、actions)。Renderer 组件必须渲染在它内部。

Spec 格式

AI 代理会生成扁平 JSON spec,其中 root 键指向根元素,elements map 包含所有组件:
{
  "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 引用它的子元素,TextInputButton 等叶子元素的 children 数组为空。

最佳实践

  • 使用描述性组件说明:AI 使用这些说明理解何时使用每个组件。清晰说明会带来更好的 UI 生成效果。
  • 渲染前验证:由于流式传输会交付部分数据,在传给 Renderer 前始终检查元素是否有有效的 type 和非空 props
  • 面向流式传输设计:流式传输期间传入 loading={true},让 Renderer 优雅处理尚未到达的子节点。用户会看到 UI 实时构建,而不是等待完整响应。
  • 使用设计 token 设置样式:使用 CSS 自定义属性,让渲染出的组件自动适配浅色和深色主题。
  • 用 JSONUIProvider 包裹Renderer 必须位于 JSONUIProvider 内部,才能访问 json-render 用于 state、visibility 和 actions 的内部 context。