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
toolRenderers | useGenerativeUI (experimental) | |
|---|---|---|
| What it does | Your React component renders per tool result | AI writes full HTML + Tailwind + Chart.js, runs in a sandboxed iframe — or picks a typed renderer (table, stat, card, chart) |
| Who decides the UI | You — one renderer per tool | The AI — generates or selects based on the data |
| Setup | Pass toolRenderers to <CopilotChat> | One useGenerativeUI() call + backend generativeUITool() |
| Best for | Domain-specific, branded components | Dashboards, charts, tables, any data layout you haven't pre-built |
| Customization | Full control | Override 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
| Type | When the AI uses it | Renderer |
|---|---|---|
html | Dashboards, custom layouts, anything freeform | HtmlRenderer — sandboxed <iframe> with Tailwind CDN + Chart.js CDN pre-loaded |
table | Structured rows — comparisons, records, lists | TableRenderer |
stat | KPI metrics, numbers with change deltas | StatRenderer |
card | Entity summaries — profiles, products, key-value info | CardRenderer |
chart | Bar, line, pie, area, scatter graphs | Requires 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" });