Generative UI lets the agent render React components directly in the chat. Instead of responding with text, the agent can produce charts, interactive widgets, visualizations, and custom UI.
Hook-based generative UI in this project is registered in apps/app/src/hooks/use-generative-ui-examples.tsx. Free-form sandboxed widgets use the runtime-level Open Generative UI rail (see below), wired up in apps/app/src/app/layout.tsx.
| Hook | Purpose |
|---|---|
useComponent |
Register a named React component the agent can render with parameters |
useFrontendTool |
Register a tool the agent can call that runs in the browser |
useRenderTool |
Custom renderer for a specific backend tool |
useDefaultRenderTool |
Fallback renderer for any tool without a custom renderer |
useHumanInTheLoop |
Interactive component that pauses the agent for user input |
All hooks are imported from @copilotkit/react-core/v2. Sandboxed streaming widgets are not hook-registered — they ride the runtime's openGenerativeUI option and the provider's renderActivityMessages prop.
Register a React component with a name, description, Zod schema, and render function. The agent decides when to use it based on the description.
import { z } from "zod";
import { useComponent } from "@copilotkit/react-core/v2";
const PieChartProps = z.object({
title: z.string(),
description: z.string(),
data: z.array(z.object({
label: z.string(),
value: z.number(),
})),
});
useComponent({
name: "pieChart",
description: "Displays data as a pie chart.",
parameters: PieChartProps,
render: PieChart, // your React component
});Free-form HTML/SVG widgets use CopilotKit's canonical Open Generative UI rail instead of a custom component. The agent streams a whole UI — styles, markup, and behavior — and the frontend renders it live in a sandboxed iframe.
How the pieces connect:
- Runtime — the CopilotKit API route enables the rail with
openGenerativeUI: true(apps/app/src/lib/copilotkit-runtime-options.ts). This injects thegenerateSandboxedUitool for the agent. - Tool — the agent calls
generateSandboxedUiwith parameters streamed in a fixed order:initialHeight→placeholderMessages→css→html→jsFunctions→jsExpressions. CSS arrives first so the UI never renders unstyled; expressions arrive last so the user watches each behavior take effect. - Middleware — the runtime's
OpenGenerativeUIMiddlewareturns the streaming tool call intoopen-generative-uiactivity events. - Renderer — the demo registers a custom activity renderer (
apps/app/src/components/generative-ui/open-generative-ui/) via the provider'srenderActivityMessagesprop. While html streams it morphs each update into a preview iframe with Idiomorph (no flicker); once html completes it boots a websandbox iframe with the shared design-system CSS and CDN importmap injected, runsjsFunctionsthen eachjsExpression, and continuously autosizes from a ResizeObserver inside the frame. - Sandbox bridge — generated UI calls back into the host through Zod-validated sandbox functions (
apps/app/src/lib/sandbox/sandbox-functions.ts):await Websandbox.connection.remote.sendPrompt({ text })andawait Websandbox.connection.remote.openLink({ url }).
// app/layout.tsx
const renderActivityMessages = [OPEN_GEN_UI_ACTIVITY_RENDERER];
const openGenerativeUI = {
sandboxFunctions: [...SANDBOX_FUNCTIONS], // sendPrompt, openLink
designSkill: OPEN_GEN_UI_DESIGN_SKILL, // teaches the agent the design system
};
<CopilotKit
runtimeUrl="/api/copilotkit"
renderActivityMessages={renderActivityMessages}
openGenerativeUI={openGenerativeUI}
>The iframe environment includes:
ES Module Libraries (importmap pre-injected; use <script type="module"> with bare imports in html, or await import(...) inside an async function in jsFunctions — the js channels run as classic scripts, so top-level await is not allowed):
three— 3D graphics (import * as THREE from "three")gsap— Animation (import gsap from "gsap")d3— Data visualization (import * as d3 from "d3")chart.js/auto— Charts
CSS Variables for theming (light/dark mode):
var(--color-background-primary)
var(--color-background-secondary)
var(--color-text-primary)
var(--color-text-secondary)
var(--color-border-primary)
var(--font-sans)
var(--border-radius-md)Pre-built SVG Classes:
.c-purple, .c-teal, .c-coral, .c-pink, .c-gray, .c-blue, .c-green, .c-amber, .c-red
Register a tool that the agent can call but that executes in the browser:
import { useFrontendTool } from "@copilotkit/react-core/v2";
useFrontendTool({
name: "toggleTheme",
description: "Toggle the app between light and dark mode.",
parameters: z.object({}),
handler: async () => {
setTheme(theme === "dark" ? "light" : "dark");
},
}, [theme, setTheme]);The agent sees this as a callable tool. When it invokes toggleTheme, the handler runs in the browser.
Override how a specific backend tool is displayed in the chat:
import { useRenderTool } from "@copilotkit/react-core/v2";
useRenderTool({
name: "plan_visualization",
parameters: z.object({
approach: z.string(),
technology: z.string(),
key_elements: z.array(z.string()),
}),
render: ({ status, parameters }) => (
<PlanCard
status={status}
approach={parameters.approach}
technology={parameters.technology}
keyElements={parameters.key_elements}
/>
),
});The status parameter is "executing" while the tool runs and "complete" when done.
Handle any tool that doesn't have a custom renderer:
import { useDefaultRenderTool } from "@copilotkit/react-core/v2";
useDefaultRenderTool({
render: ({ name, status, parameters }) => {
if (ignoredTools.includes(name)) return <></>;
return <ToolReasoning name={name} status={status} args={parameters} />;
},
});Call all hooks inside a custom hook, then invoke it in your page:
// hooks/use-generative-ui-examples.tsx
export const useGenerativeUIExamples = () => {
useFrontendTool({ ... });
useComponent({ ... });
useRenderTool({ ... });
useDefaultRenderTool({ ... });
useHumanInTheLoop({ ... });
};
// app/page.tsx
export default function HomePage() {
useGenerativeUIExamples();
// ...
}- Agent Tools — Create the backend tools that drive generative UI
- Human in the Loop — Pause the agent for user decisions