Generative UI

Render rich React components from AI tool results — per-tool custom renderers or AI-driven built-in components

Instead of showing raw JSON or plain text from tool calls, render interactive UI directly inside the chat — from your own branded React components per tool, to fully AI-generated dashboards, charts, and layouts running in a sandboxed iframe.


Two Approaches

toolRenderersuseGenerativeUI (experimental)
What it doesYour React component renders per tool resultAI writes full HTML + Tailwind + Chart.js, runs in a sandboxed iframe — or picks a typed renderer (table, stat, card, chart)
Who decides the UIYou — one renderer per toolThe AI — generates or selects based on the data
SetupPass toolRenderers to <CopilotChat>One useGenerativeUI() call + backend generativeUITool()
Best forDomain-specific, branded componentsDashboards, charts, tables, any data layout you haven't pre-built
CustomizationFull controlOverride any built-in renderer

Approach 1 — toolRenderers

Map tool names to React components. Each component receives the tool's args and result as props.

Basic example

import { CopilotChat } from "@yourgpt/copilot-sdk/ui";

function WeatherCard({ data, status }) {
  if (status === "executing") {
    return <div className="animate-pulse">Loading weather...</div>;
  }
  return (
    <div className="p-4 rounded-lg bg-gradient-to-br from-blue-400 to-blue-600 text-white">
      <h3 className="text-xl font-bold">{data.city}</h3>
      <p className="text-4xl">{data.temp}°F</p>
      <p>{data.conditions}</p>
    </div>
  );
}

<CopilotChat
  toolRenderers={{
    get_weather: WeatherCard,
    get_chart: ChartCard,
    search_products: ProductGrid,
  }}
/>

ToolRendererProps

Every renderer receives these props:

interface ToolRendererProps {
  status: "pending" | "executing" | "completed" | "error" | "failed" | "rejected";
  args: Record<string, unknown>;   // arguments passed to the tool
  data?: unknown;                  // result (when completed)
  error?: string;                  // error message (when failed)
  executionId: string;
  toolName: string;
}

Handling all states

function ChartCard({ status, data, error, args }: ToolRendererProps) {
  if (status === "pending" || status === "executing") {
    return (
      <div className="p-4 border rounded-lg">
        <div className="animate-spin h-6 w-6 border-2 border-primary rounded-full border-t-transparent" />
        <p className="text-sm text-muted-foreground mt-2">
          Generating {args.metric} chart...
        </p>
      </div>
    );
  }

  if (status === "error" || status === "failed") {
    return (
      <div className="p-4 border border-destructive rounded-lg">
        <p className="text-destructive">Failed to load chart</p>
        <p className="text-sm text-muted-foreground">{error}</p>
      </div>
    );
  }

  if (status === "rejected") {
    return (
      <div className="p-4 border rounded-lg bg-muted">
        <p className="text-muted-foreground">Chart request was declined</p>
      </div>
    );
  }

  return (
    <div className="p-4 border rounded-lg">
      <h3 className="font-semibold mb-4">{data.title}</h3>
      <ResponsiveContainer width="100%" height={300}>
        <LineChart data={data.chartData}>
          <XAxis dataKey="date" />
          <YAxis />
          <Line type="monotone" dataKey="value" stroke="#8884d8" />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

Interactive components

Renderers can be fully interactive and call back into the chat:

function ProductCard({ data }: ToolRendererProps) {
  const [quantity, setQuantity] = useState(1);
  const { sendMessage } = useCopilot();

  return (
    <div className="p-4 border rounded-lg">
      <img src={data.image} alt={data.name} />
      <h3>{data.name}</h3>
      <p>${data.price}</p>
      <div className="flex items-center gap-2 mt-4">
        <input
          type="number"
          value={quantity}
          onChange={(e) => setQuantity(Number(e.target.value))}
          className="w-16 border rounded px-2 py-1"
        />
        <button
          onClick={() => sendMessage(`Add ${quantity} of ${data.name} to my cart`)}
          className="btn-primary"
        >
          Add to Cart
        </button>
      </div>
    </div>
  );
}

Control AI response verbosity

Return _aiResponseMode: "brief" from your tool handler to prevent the AI from describing what the UI already shows:

handler: async ({ timeRange }) => {
  const data = await fetchDashboardData(timeRange);
  return {
    success: true,
    data,
    _aiResponseMode: "brief",
    _aiContext: `Dashboard for ${timeRange}`,
  };
},

Use _aiResponseMode: "brief" when your UI component is self-explanatory. The AI gives a short acknowledgment instead of narrating the data.


Approach 2 — AI-Generated UI (Experimental)

@yourgpt/copilot-sdk/experimental — APIs may change without a semver major bump.

The AI calls a single render_ui tool and generates the UI itself. The standout capability is type: "html" — the AI writes full HTML with Tailwind CSS and Chart.js, rendered in a sandboxed iframe. No pre-built component needed. For structured data it can also pick typed renderers (table, stat, card, chart) automatically.

User: "Show Q1 revenue by region"

AI calls: render_ui({ type: "chart", chartType: "bar", labels: ["NA","EU","APAC"], datasets: [...] })

UI renders: [Bar chart]

User: "Build an analytics dashboard"

AI calls: render_ui({ type: "html", html: "<div class='grid grid-cols-3 gap-4'>...</div>", height: "600px" })

UI renders: [Full dashboard in sandboxed iframe with Tailwind + Chart.js]

Setup

Register generativeUITool() in your route. The key becomes the tool name.

import { generativeUITool } from "@yourgpt/copilot-sdk/experimental";
import { streamText } from "@yourgpt/llm-sdk";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = await streamText({
    model: openai("gpt-4o"),
    system: "Use render_ui for any data, charts, or structured results.",
    messages,
    tools: {
      render_ui: generativeUITool(),
    },
  });

  return result.toDataStreamResponse();
}

Call useGenerativeUI() once in your component tree — it registers the renderer automatically.

import { useGenerativeUI } from "@yourgpt/copilot-sdk/experimental";
import { CopilotChat } from "@yourgpt/copilot-sdk/ui";

function App() {
  useGenerativeUI({
    chartRenderer: MyChartComponent, // required for chart type
  });

  return <CopilotChat />;
}

Built-in component types

TypeWhen the AI uses itRenderer
htmlDashboards, custom layouts, anything freeformHtmlRenderer — sandboxed <iframe> with Tailwind CDN + Chart.js CDN pre-loaded
tableStructured rows — comparisons, records, listsTableRenderer
statKPI metrics, numbers with change deltasStatRenderer
cardEntity summaries — profiles, products, key-value infoCardRenderer
chartBar, line, pie, area, scatter graphsRequires chartRenderer prop

html type — AI-generated iframe UI

When the AI uses type: "html", it can write full HTML with Tailwind utility classes and inline Chart.js canvases. The SDK renders it in a sandboxed <iframe> so it's fully isolated from your app.

The iframe automatically loads:

  • Tailwind CSS Play CDN — any utility class works
  • Chart.js — create charts with <canvas> + new Chart(...)
// The AI generates something like this:
render_ui({
  type: "html",
  height: "520px",
  html: `
    <div class="grid grid-cols-3 gap-4 p-6">
      <div class="bg-white rounded-xl border border-gray-200 shadow-sm p-6">
        <p class="text-sm text-gray-500">Monthly Revenue</p>
        <p class="text-3xl font-bold text-gray-900 mt-1">$84,200</p>
        <span class="text-xs bg-green-50 text-green-700 px-2 py-0.5 rounded-full">+12%</span>
      </div>
      <!-- more cards... -->
    </div>
    <canvas id="chart" height="220"></canvas>
    <script>
    new Chart(document.getElementById('chart'), {
      type: 'bar',
      data: { labels: ['Jan','Feb','Mar'], datasets: [{ data: [65,80,84], backgroundColor: '#6366f1' }] }
    });
    </script>
  `
})

Chart renderer

Charts require a user-supplied renderer since the SDK doesn't bundle a charting library:

import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts";
import type { ChartRendererProps } from "@yourgpt/copilot-sdk/experimental";

function MyChart({ payload }: ChartRendererProps) {
  const data = payload.labels.map((label, i) => ({
    name: label,
    ...Object.fromEntries(payload.datasets.map((d) => [d.label, d.data[i]])),
  }));

  return (
    <ResponsiveContainer width="100%" height={300}>
      <BarChart data={data}>
        <XAxis dataKey="name" />
        <YAxis />
        <Tooltip />
        {payload.datasets.map((d) => (
          <Bar key={d.label} dataKey={d.label} fill={d.color ?? "#6366f1"} />
        ))}
      </BarChart>
    </ResponsiveContainer>
  );
}

useGenerativeUI({ chartRenderer: MyChart });

Override built-in renderers

Replace any built-in renderer with your own component:

useGenerativeUI({
  overrideRenderers: {
    card: MyCustomCardRenderer,
    table: MyCustomTableRenderer,
  },
});

Custom tool name

If you use a key other than render_ui in tools, pass the same name to useGenerativeUI:

// Backend
tools: { show_component: generativeUITool() }

// Frontend
useGenerativeUI({ name: "show_component" });

On this page