Claude
Skills
Sign in
Back

ai-generative-ui

Included with Lifetime
$97 forever

Data-driven generative UI — tool results render as rich React components in chat instead of raw JSON. Uses a registry pattern with _ui field, not createStreamableUI(). Use this skill when the user says "generative ui", "rich tool cards", "custom tool rendering", or "tool components".

Design

What this skill does


# AI Generative UI

Experience layer that renders tool results as interactive React components inside the chat stream instead of raw JSON. Uses a data-driven approach: tool `execute` functions return a `_ui` field that names a registered component, and the client-side renderer looks it up in a registry.

This does **not** use `createStreamableUI()` (that is an older RSC pattern incompatible with the current architecture). Instead, tool results are plain data objects with a `_ui` type hint that the client uses to select the appropriate React component.

## Prerequisites

- Next.js app with `src/` directory and App Router
- `ai-core` skill installed (provides `getModel()`)
- `ai-chat` skill installed (provides chat UI, route pipeline, message renderer)
- `ai-tools` skill installed (provides tool calling framework and `tool-invocation` rendering)

## Installation

No additional packages required. Uses `ai`, `zod`, and `@ai-sdk/react` already installed by prerequisite skills.

## What Gets Created

```
src/
├── lib/
│   └── ai/
│       └── ui-registry.ts             # Component registry — maps tool _ui values to React components
└── components/
    └── ai/
        └── gen-ui/
            ├── weather-card.tsx        # Example: weather display with temperature + conditions
            ├── data-card.tsx           # Example: structured key-value display card
            └── confirmation.tsx        # Example: interactive yes/no confirmation card
```

## What Gets Modified

```
src/
├── app/
│   └── api/
│       └── ai/
│           └── chat/
│               └── route.ts           # Tool execute functions return _ui field
└── components/
    └── ai/
        └── message.tsx                # Check tool-result for _ui field, render registered component
```

## Comment Slots

- **message.tsx**: `// [ai-generative-ui]: check for _ui field` — checks tool results for `_ui` field and renders registered component
- **message.tsx**: `// [ai-generative-ui]: import gen-ui components to trigger registration` — side-effect imports that register components

## Setup Steps

### Step 1: Create `src/lib/ai/ui-registry.ts`

```typescript
import { type ComponentType } from "react";

/**
 * Registry mapping `_ui` field values from tool results to React components.
 *
 * When a tool's `execute` function returns `{ ...data, _ui: "WeatherCard" }`,
 * the message renderer looks up "WeatherCard" in this registry and renders
 * the matched component with the tool result as the `data` prop.
 *
 * If no component is found, the renderer falls back to the default JSON
 * tool result card from ai-tools.
 */

type GenUIProps<T = Record<string, unknown>> = {
  data: T;
};

type GenUIComponent = ComponentType<GenUIProps>;

const registry = new Map<string, GenUIComponent>();

/**
 * Register a component for a given _ui key.
 * Call this at module scope in your component files.
 */
export function registerUIComponent(
  key: string,
  component: GenUIComponent
): void {
  registry.set(key, component);
}

/**
 * Look up a component by _ui key.
 * Returns undefined if no component is registered.
 */
export function getUIComponent(
  key: string
): GenUIComponent | undefined {
  return registry.get(key);
}

/**
 * Check if a tool result has a _ui field that maps to a registered component.
 */
export function hasUIComponent(result: unknown): result is {
  _ui: string;
  [key: string]: unknown;
} {
  return (
    typeof result === "object" &&
    result !== null &&
    "_ui" in result &&
    typeof (result as Record<string, unknown>)._ui === "string" &&
    registry.has((result as Record<string, unknown>)._ui as string)
  );
}

export type { GenUIProps, GenUIComponent };
```

### Step 2: Create `src/components/ai/gen-ui/weather-card.tsx`

```tsx
"use client";

import { memo } from "react";
import { registerUIComponent, type GenUIProps } from "@/lib/ai/ui-registry";

type WeatherData = {
  location: string;
  temperature: number;
  unit: string;
  conditions: string;
  humidity: number;
  windSpeed: number;
  windUnit: string;
  feelsLike: number;
  _ui: string;
};

function getWeatherIcon(conditions: string): string {
  const lower = conditions.toLowerCase();
  if (lower.includes("sun") || lower.includes("clear")) return "sun";
  if (lower.includes("cloud") && lower.includes("part"))
    return "cloud-sun";
  if (lower.includes("cloud")) return "cloud";
  if (lower.includes("rain") || lower.includes("drizzle"))
    return "cloud-rain";
  if (lower.includes("snow")) return "snowflake";
  if (lower.includes("thunder") || lower.includes("storm"))
    return "cloud-lightning";
  if (lower.includes("fog") || lower.includes("mist")) return "cloud-fog";
  return "thermometer";
}

function WeatherIcon({ conditions }: { conditions: string }) {
  const icon = getWeatherIcon(conditions);

  const icons: Record<string, React.ReactNode> = {
    sun: (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="32"
        height="32"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
        className="text-yellow-500"
      >
        <circle cx="12" cy="12" r="4" />
        <path d="M12 2v2" />
        <path d="M12 20v2" />
        <path d="m4.93 4.93 1.41 1.41" />
        <path d="m17.66 17.66 1.41 1.41" />
        <path d="M2 12h2" />
        <path d="M20 12h2" />
        <path d="m6.34 17.66-1.41 1.41" />
        <path d="m19.07 4.93-1.41 1.41" />
      </svg>
    ),
    cloud: (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="32"
        height="32"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
        className="text-gray-400"
      >
        <path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
      </svg>
    ),
    "cloud-rain": (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="32"
        height="32"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
        className="text-blue-400"
      >
        <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" />
        <path d="M16 14v6" />
        <path d="M8 14v6" />
        <path d="M12 16v6" />
      </svg>
    ),
    thermometer: (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="32"
        height="32"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
        className="text-orange-400"
      >
        <path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" />
      </svg>
    ),
  };

  return <>{icons[icon] ?? icons.thermometer}</>;
}

const WeatherCard = memo(function WeatherCard({ data }: GenUIProps<WeatherData>) {
  return (
    <div className="overflow-hidden rounded-xl border bg-gradient-to-br from-blue-50 to-sky-50 dark:from-blue-950 dark:to-sky-950">
      <div className="p-4">
        {/* Location + Icon row */}
        <div className="flex items-start justify-between">
          <div>
            <p className="text-sm font-medium text-muted-foreground">
              {data.location}
            </p>
            <p className="mt-1 text-3xl font-bold tracking-tight">
              {Math.round(data.temperature)}{data.unit === "celsius" ? "\u00B0C" : "\u00B0F"}
            </p>
          </div>
          <WeatherIcon conditions={data.conditions} />
        </div>

        {/* Conditions */}
        <p className="mt-1 text-sm capitalize text-muted-foreground">
          {data.conditions}
        </p>

        {/* Details row */}
        <div className="mt-4 grid grid-cols-3 gap-3 border-t pt-3">
          <div>
            <p className="text-xs text-muted-foreground">Feels like</p>
            <p className="te
Files: 1
Size: 23.4 KB
Complexity: 33/100
Category: Design

Related in Design