Streaming UI
@cloudflare/kumo

概述

Kumo catalog 模块支持从 JSON 结构渲染 UI,专为 AI 生成的界面而设计。它为基于 JSON 的 UI 树提供运行时校验、数据绑定、条件渲染和操作处理。

从你的代码库派生的 Schema

与需要单独维护 schema 定义的做法不同,Kumo 会从你组件的真实 TypeScript 类型中自动派生校验 schema。当你更新组件的 props 时,校验 schema 会通过组件注册表 codegen 流程自动更新。无需手动同步 — 你的 schema 始终与组件保持一致。

Schema 在 @cloudflare/kumo/ai/schemas 中从组件 TypeScript 类型自动生成。 修改组件 props 后,运行 pnpm codegen:registry 重新生成。

工作原理

catalog 模块使用一条流水线,从你的 TypeScript 源码中提取组件元数据:

Component TSXTypeScript TypesCodegen ScriptZod Schemas

ai/schemas.ts 中生成的 schema 包括:

  • 每个组件的 props schema(例如 ButtonPropsSchema
  • variant props 的枚举值
  • UI 元素和树结构 schema
  • 动态值、可见性和操作 schema

安装

import {
  createKumoCatalog,
  initCatalog,
  resolveProps,
  evaluateVisibility,
} from "@cloudflare/kumo/catalog";

创建 Catalog

创建一个 catalog 实例,用自动生成的 schema 校验 AI 生成的 JSON:

import { createKumoCatalog, initCatalog } from "@cloudflare/kumo/catalog";

// Create a catalog with optional actions
const catalog = createKumoCatalog({
  actions: {
    submit_form: { description: "Submit the current form" },
    delete_item: { description: "Delete the selected item" },
  },
});

// Initialize schemas (required before sync validation)
await initCatalog(catalog);

// Validate AI-generated JSON
const result = catalog.validateTree(aiGeneratedJson);
if (result.success) {
  // Render the validated tree
  renderTree(result.data);
}

UI 树格式

UI 树采用扁平结构,专门为 LLM 生成和流式传输优化。元素通过 key 相互引用,而不是嵌套,从而可以在元素流式到达时进行渐进式渲染。

{
  "root": "card-1",
  "elements": {
    "card-1": {
      "key": "card-1",
      "type": "Surface",
      "props": { "className": "p-4" },
      "children": ["heading-1", "text-1", "button-1"]
    },
    "heading-1": {
      "key": "heading-1",
      "type": "Text",
      "props": {
        "variant": "heading2",
        "children": "Welcome"
      },
      "parentKey": "card-1"
    },
    "text-1": {
      "key": "text-1",
      "type": "Text",
      "props": {
        "children": { "path": "/user/name" }
      },
      "parentKey": "card-1"
    },
    "button-1": {
      "key": "button-1",
      "type": "Button",
      "props": {
        "variant": "primary",
        "children": "Get Started"
      },
      "parentKey": "card-1",
      "action": {
        "name": "submit_form"
      }
    }
  }
}

为什么用扁平结构?

  • 元素一到达即可渲染(流式传输)
  • 无需深层遍历即可轻松更新
  • 序列化/反序列化简单
  • 天然契合 LLM 逐个 token 生成的方式

动态值(数据绑定)

Props 可以使用 JSON Pointer 路径引用数据模型中的值。这让 AI 可以声明数据绑定,由你的应用在渲染时解析。

import { resolveProps, resolveDynamicValue } from "@cloudflare/kumo/catalog";

// Data model backing the UI
const dataModel = {
  user: {
    name: "Alice",
    isAdmin: true,
  },
  items: [
    { id: 1, title: "First Item" },
    { id: 2, title: "Second Item" },
  ],
};

// AI-generated props with dynamic references
const props = {
  children: { path: "/user/name" },
  disabled: false,
};

// Resolve all dynamic values
const resolved = resolveProps(props, dataModel);
// { children: "Alice", disabled: false }

// Or resolve individual values
const name = resolveDynamicValue({ path: "/user/name" }, dataModel);
// "Alice"

可见性条件

元素可以根据数据值、身份验证状态或复杂逻辑表达式进行条件渲染。

import {
  evaluateVisibility,
  createVisibilityContext,
} from "@cloudflare/kumo/catalog";

const ctx = createVisibilityContext(
  // Data model
  { user: { isAdmin: true, role: "editor" } },
  // Auth state
  { isSignedIn: true },
);

// Simple boolean
evaluateVisibility(true, ctx); // true

// Path check (truthy test)
evaluateVisibility({ path: "/user/isAdmin" }, ctx); // true

// Auth check
evaluateVisibility({ auth: "signedIn" }, ctx); // true
evaluateVisibility({ auth: "signedOut" }, ctx); // false

// Equality check
evaluateVisibility(
  {
    eq: [{ path: "/user/role" }, "editor"],
  },
  ctx,
); // true

// Complex logic
evaluateVisibility(
  {
    and: [
      { path: "/user/isAdmin" },
      { auth: "signedIn" },
      { gt: [{ path: "/items/length" }, 0] },
    ],
  },
  ctx,
);

可用操作符

OperatorDescription
path对数据路径做真值检查
auth”signedIn” 或 “signedOut”
eq / neq相等 / 不相等比较
gt / gte大于 / 大于或等于
lt / lte小于 / 小于或等于
and / or / not布尔逻辑组合

操作

元素可以声明由你的应用处理的操作。AI 描述意图,你的处理器执行业务逻辑。

// In your UI tree element
{
  "key": "delete-btn",
  "type": "Button",
  "props": {
    "variant": "destructive",
    "children": "Delete"
  },
  "action": {
    "name": "delete_item",
    "params": {
      "itemId": { "path": "/selected/id" }
    },
    "confirm": {
      "title": "Delete Item",
      "message": "Are you sure you want to delete this item?",
      "variant": "danger",
      "confirmLabel": "Delete",
      "cancelLabel": "Cancel"
    },
    "onSuccess": {
      "set": { "/selected": null }
    }
  }
}

// Register actions when creating the catalog
const catalog = createKumoCatalog({
  actions: {
    delete_item: {
      description: "Delete an item by ID",
      params: {
        itemId: { type: "string", description: "Item ID to delete" }
      }
    }
  }
});

校验

catalog 使用从组件 TypeScript 类型派生的自动生成 Zod schema 来校验 AI 生成的 JSON。

// Validate a complete tree
const result = catalog.validateTree(aiJson);

if (result.success) {
  console.log("Valid tree:", result.data);
} else {
  console.error("Validation errors:", result.error);
  // [{ message: "Invalid enum value", path: ["elements", "btn-1", "props", "variant"] }]
}

// Validate a single element
const elementResult = catalog.validateElement({
  key: "btn-1",
  type: "Button",
  props: { variant: "primary" },
});

// Check available components
catalog.hasComponent("Button"); // true
catalog.hasComponent("Foobar"); // false

// List all component names
console.log(catalog.componentNames);
// ["Badge", "Banner", "Button", ...]

AI 提示生成

生成向 AI 模型描述 catalog 的提示词:

const prompt = catalog.generatePrompt();

// Returns markdown describing:
// - Available components
// - Available actions (if any)
// - Output format (UITree schema)
// - Dynamic value syntax

// Use in your LLM prompt
const systemPrompt = `
You are a UI generation assistant.

${catalog.generatePrompt()}

Generate UI based on the user's request.
`;

类型导出

所有类型都已导出,便于 TypeScript 集成:

import type {
  // Core types
  UIElement,
  UITree,
  DynamicValue,
  DynamicString,
  DynamicNumber,
  DynamicBoolean,

  // Visibility
  VisibilityCondition,
  LogicExpression,

  // Actions
  Action,
  ActionConfirm,
  ActionHandler,
  ActionHandlers,
  ActionDefinition,

  // Auth & Data
  AuthState,
  DataModel,

  // Catalog
  KumoCatalog,
  CatalogConfig,
  ValidationResult,
} from "@cloudflare/kumo/catalog";

完整示例

一个展示 catalog 创建、校验和渲染的完整示例:

import {
  createKumoCatalog,
  initCatalog,
  resolveProps,
  evaluateVisibility,
  createVisibilityContext,
} from "@cloudflare/kumo/catalog";
import { Button, Text, Surface } from "@cloudflare/kumo";

// 1. Create and initialize catalog
const catalog = createKumoCatalog({
  actions: {
    greet: { description: "Show a greeting" },
  },
});
await initCatalog(catalog);

// 2. Validate AI-generated JSON
const aiJson = {
  root: "container",
  elements: {
    container: {
      key: "container",
      type: "Surface",
      props: { className: "p-4 space-y-4" },
      children: ["greeting", "action-btn"],
    },
    greeting: {
      key: "greeting",
      type: "Text",
      props: {
        variant: "heading2",
        children: { path: "/user/name" },
      },
      parentKey: "container",
      visible: { auth: "signedIn" },
    },
    "action-btn": {
      key: "action-btn",
      type: "Button",
      props: {
        variant: "primary",
        children: "Say Hello",
      },
      parentKey: "container",
      action: { name: "greet" },
    },
  },
};

const result = catalog.validateTree(aiJson);
if (!result.success) {
  throw new Error("Invalid UI tree");
}

// 3. Set up rendering context
const dataModel = {
  user: { name: "Alice", preferences: { theme: "dark" } },
};
const visibilityCtx = createVisibilityContext(dataModel, { isSignedIn: true });

// 4. Render function
function renderElement(element, elements) {
  // Check visibility
  if (!evaluateVisibility(element.visible, visibilityCtx)) {
    return null;
  }

  // Resolve dynamic props
  const props = resolveProps(element.props, dataModel);

  // Render children
  const children = element.children?.map((key) =>
    renderElement(elements[key], elements),
  );

  // Map to components
  const Component = { Surface, Text, Button }[element.type];
  return <Component {...props}>{children}</Component>;
}

// 5. Render the tree
const tree = result.data;
const ui = renderElement(tree.elements[tree.root], tree.elements);

核心优势

自动生成的 Schema — 校验 schema 直接来自组件 TypeScript 类型,无需单独维护 schema 定义。

始终保持同步 — 更新组件 props 时,schema 会通过组件注册表 codegen 流程自动更新。

适合流式传输 — 扁平树结构让 LLM 响应逐个 token 流式返回时也能渐进式渲染。

类型安全 — 完整的 TypeScript 支持,导出了 UIElement、UITree、DynamicValue 等类型。