Streaming UI
@cloudflare/kumo

Overview

The audt.ai catalog module enables rendering UI from JSON structures, designed specifically for AI-generated interfaces. It provides runtime validation, data binding, conditional rendering, and action handling for JSON-based UI trees.

Schemas Derived from Your Codebase

Unlike approaches that require maintaining separate schema definitions, audt.ai automatically derives validation schemas from your actual component TypeScript types. When you update a component’s props, the validation schemas update automatically via the component registry codegen process. No manual synchronization required — your schemas are always in sync with your components.

Schemas are auto-generated in @cloudflare/kumo/ai/schemas from component TypeScript types. Run pnpm codegen:registry after modifying component props to regenerate.

How It Works

The catalog module uses a pipeline that extracts component metadata from your TypeScript source code:

Component TSXTypeScript TypesCodegen ScriptZod Schemas

The generated schemas in ai/schemas.ts include:

  • Props schemas for each component (e.g., ButtonPropsSchema)
  • Enum values for variant props
  • UI element and tree structure schemas
  • Dynamic value, visibility, and action schemas

Installation

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

Creating a Catalog

Create a catalog instance that validates AI-generated JSON against the auto-generated schemas:

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 Tree Format

The UI tree uses a flat structure optimized for LLM generation and streaming. Elements reference each other by key rather than nesting, enabling progressive rendering as elements stream in.

{
  "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"
      }
    }
  }
}

Why a flat structure?

  • Elements can be rendered as soon as they arrive (streaming)
  • Easy updates without deep tree traversal
  • Simple serialization/deserialization
  • Natural fit for how LLMs generate token-by-token

Dynamic Values (Data Binding)

Props can reference values from a data model using JSON Pointer paths. This allows the AI to declare data bindings that your application resolves at render time.

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"

Visibility Conditions

Elements can be conditionally rendered based on data values, authentication state, or complex logic expressions.

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,
);

Available Operators

OperatorDescription
pathTruthy check on data path
auth”signedIn” or “signedOut”
eq / neqEquality / inequality comparison
gt / gteGreater than / greater than or equal
lt / lteLess than / less than or equal
and / or / notBoolean logic combinators

Actions

Elements can declare actions that your application handles. The AI describes the intent, and your handlers execute the logic.

// 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" }
      }
    }
  }
});

Validation

The catalog validates AI-generated JSON against auto-generated Zod schemas derived from component TypeScript types.

// 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 Prompt Generation

Generate prompts describing the catalog for AI models:

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.
`;

Type Exports

All types are exported for TypeScript integration:

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";

Full Example

A complete example showing catalog creation, validation, and rendering:

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

Key Benefits

Auto-Generated Schemas — Validation schemas are derived directly from component TypeScript types. No separate schema definitions to maintain.

Always in Sync — When you update component props, the schemas update automatically via the component registry codegen process.

Streaming-Friendly — Flat tree structure enables progressive rendering as LLM responses stream in token-by-token.

Type Safety — Full TypeScript support with exported types for UIElement, UITree, DynamicValue, and more.