Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions docs/content/docs/openui-lang/examples/shadcn-chat.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
---
title: Shadcn Chat
description: A generative UI chat app that streams OpenUI Lang and renders it as shadcn/ui components.
---

A full-stack example that streams [OpenUI Lang](/docs/openui-lang/overview) into a Next.js chat interface using `@openuidev/react-ui`'s [FullScreen](/docs/chat/fullscreen) layout, then renders every response as rich [shadcn/ui](https://ui.shadcn.com/) components via a custom `shadcnChatLibrary`.

[View source on GitHub →](https://github.com/thesysdev/crayon/tree/main/examples/shadcn-chat)

<div className="bg-[rgba(0,0,0,0.05)] dark:bg-gray-900 rounded-2xl h-[500px] flex p-2">
<video
src="/videos/shadcn-demo-chat.mp4"
noControls
playsInline
muted
preload="metadata"
className="w-full rounded-lg m-auto"
autoPlay
loop
/>
</div>

The example includes:

- A **Next.js frontend** with a FullScreen chat layout that adapts to the system light/dark theme
- A **Next.js API route** that calls OpenAI with streaming and server-side tool execution
- A **shadcn-genui component library** with 40+ components covering content, charts, forms, tables, buttons, layout, overlays, and more

## Architecture

```
Browser (FullScreen) -- POST /api/chat --> Next.js route --> OpenAI
<-- SSE stream -- (OpenUI Lang + tool calls)
```

The client sends a conversation to `/api/chat`. The API route loads a generated `system-prompt.txt`, forwards the messages to the LLM with streaming and tool definitions, and returns SSE events. On the client side, `openAIAdapter()` parses the SSE stream, and `shadcnChatLibrary` maps each OpenUI Lang node to a shadcn/ui component that renders progressively as tokens arrive.

The API route also supports **server-side tool execution**. When the model invokes a tool (weather, stock price, calculator, or web search), the route runs it and feeds the result back into the completion loop before streaming the final UI response.

## Project layout

```
examples/shadcn-chat/
|- src/app/ # Next.js app (layout, page, API route)
|- src/hooks/ # Theme detection and context
|- src/components/ui/ # Base shadcn/ui primitives
|- src/lib/shadcn-genui/ # Generative UI component library (40+ components)
|- src/generated/ # Generated system prompt
```

## Run the example

Run these commands from `examples/shadcn-chat`.

1. Install dependencies:

```bash
cd examples/shadcn-chat
pnpm install
```

2. Create a `.env.local` file with your API key:

```bash
OPENAI_API_KEY=sk-...
```

3. Start the dev server:

```bash
pnpm dev
```

This automatically generates the system prompt from the library definition before starting Next.js.

## What to expect

Open the app and try one of the built-in conversation starters:

- "Startup dashboard" — analytics dashboard with charts, tables, and progress bars
- "Travel planner" — calendar, accordion destinations, and a preferences form
- "Market watch" — fetches live stock prices and renders a comparison table with charts
- "Event RSVP" — a rich form with every input type
- "Chart showcase" — six chart types (Bar, Line, Area, Pie, Radar, Scatter) in tabbed view

Instead of rendering plain markdown, the assistant response appears progressively as shadcn/ui components — cards, charts, forms, tables, badges, and more — all styled with the shadcn/ui design system and responsive to your system theme.

## Key files

### `src/hooks/use-system-theme.tsx` — theme detection

A `ThemeProvider` wraps the app at the root layout. It detects the system color scheme via `prefers-color-scheme`, sets a `data-theme` attribute on `<body>`, and exposes the resolved `"light"` or `"dark"` mode through a `useTheme()` hook. CSS variables in `globals.css` switch via the `[data-theme="dark"]` selector, and Tailwind's dark variant is configured to match.

### `src/app/page.tsx` — FullScreen chat setup

The page calls `useTheme()` to get the current mode and passes it to the `FullScreen` layout:

```tsx
<FullScreen
processMessage={async ({ messages, abortController }) => {
return fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: openAIMessageFormat.toApi(messages),
}),
signal: abortController.signal,
});
}}
streamProtocol={openAIAdapter()}
componentLibrary={shadcnChatLibrary}
agentName="shadcn/ui Chat"
theme={{ mode }}
conversationStarters={{
variant: "short",
options: [
{ displayText: "Startup dashboard", prompt: "Build a startup analytics dashboard ..." },
{ displayText: "Market watch", prompt: "Fetch stock prices for AAPL, NVDA ..." },
// ...
],
}}
/>
```

See [Chat FullScreen Layout](/docs/chat/fullscreen) and [Connecting to a Backend](/docs/chat/connecting) for the full API.

### `src/lib/shadcn-genui/index.tsx` — component library

Defines the complete generative UI library using `createLibrary()` from `@openuidev/react-lang`. The library declares a `Card` root component, 13 component groups, 12 prompt examples, and additional rules that guide the LLM:

```tsx
export const shadcnChatLibrary = createLibrary({
root: "Card",
componentGroups: shadcnComponentGroups,
components: [
ChatCard,
CardHeader,
TextContent,
MarkDownRenderer,
Alert /* ... */,
Table,
Col,
BarChartCondensed,
LineChartCondensed /* ... */,
Form,
FormControl,
Input,
Select /* ... */,
Button,
Buttons,
Tabs,
Accordion,
Carousel,
// ... 40+ components total
],
});
```

The component groups organize components by category and include notes that become part of the generated system prompt:

| Group | Components |
| ---------------- | ------------------------------------------------------------------------------------------------------------- |
| Content | CardHeader, TextContent, MarkDownRenderer, Alert, Badge, Avatar, CodeBlock, Image, Progress, Separator |
| Tables | Table, Col |
| Charts (2D) | BarChart, LineChart, AreaChart, RadarChart, Series |
| Charts (1D) | PieChart, RadialChart, Slice |
| Charts (Scatter) | ScatterChart, ScatterSeries, Point |
| Forms | Form, FormControl, Label, Input, TextArea, Select, DatePicker, Slider, CheckBoxGroup, RadioGroup, SwitchGroup |
| Buttons | Button, Buttons |
| Follow-ups | FollowUpBlock, FollowUpItem |
| Layout | Tabs, Accordion, Carousel |
| Data Display | TagBlock, Tag |
| Typography | Heading, Blockquote, InlineCode |
| Calendar | CalendarBlock |
| Navigation | PaginationBlock |
| Overlays | DialogBlock, AlertDialogBlock, DrawerBlock |

See [Defining Components](/docs/openui-lang/defining-components) for the full `defineComponent` API.

### `src/app/api/chat/route.ts` — SSE streaming with tool execution

The API route reads the generated system prompt, calls the LLM with `runTools()` for server-side tool execution, and streams the response as SSE:

```ts
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const MODEL = "gpt-5.4";

const runner = client.chat.completions.runTools({
model: MODEL,
messages: chatMessages,
tools,
stream: true,
});

runner.on("chunk", (chunk) => {
const delta = chunk.choices?.[0]?.delta;
if (delta?.content) {
enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
});
```

When the model calls a tool, the route executes it and enriches the response with both the request and result before continuing. This keeps the tool loop entirely server-side — the client only sees the final streamed UI.

## Tools

The API route defines four server-side tools that the LLM can invoke during a conversation:

| Tool | Description |
| ----------------- | -------------------------------------------------------------------------------------- |
| `get_weather` | Returns temperature, conditions, humidity, wind, and a 2-day forecast for a given city |
| `get_stock_price` | Returns price, change, volume, and day range for a ticker symbol |
| `calculate` | Evaluates a math expression and returns the result |
| `search_web` | Returns mock search results for a query |

These are demo implementations with simulated data. In a production app, you would replace them with real API calls.

### `src/lib/shadcn-genui/helpers.ts` — chart data builders

Utility functions that normalize chart data from OpenUI Lang element nodes into the format expected by Recharts:

- `buildChartData(labels, series)` — transforms labels and series nodes into row-based chart data for 2D charts (bar, line, area, radar)
- `buildSliceData(slices)` — transforms slice nodes into data for 1D charts (pie, radial)

## System prompt generation

The `pnpm generate:prompt` script inspects the library definition in `src/lib/shadcn-genui/index.tsx` and generates `src/generated/system-prompt.txt`. This prompt tells the LLM about every available component, its props, and the OpenUI Lang syntax. Re-run it whenever you change component definitions, prop schemas, descriptions, or prompt rules.

See [System Prompts](/docs/openui-lang/system-prompts) for how prompt generation works.
3 changes: 2 additions & 1 deletion docs/content/docs/openui-lang/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"specification",
"benchmarks",
"---Examples---",
"examples/react-native"
"examples/react-native",
"examples/shadcn-chat"
]
}
Binary file added docs/public/videos/shadcn-demo-chat.mp4
Binary file not shown.
35 changes: 28 additions & 7 deletions examples/shadcn-chat/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,39 @@ export default function Page() {
variant: "short",
options: [
{
displayText: "Weather in Tokyo",
prompt: "What's the weather like in Tokyo right now?",
displayText: "Startup dashboard",
prompt:
"Build a startup analytics dashboard with tags, Tabs (Revenue BarChart, Growth LineChart, Breakdown PieChart), a key metrics table, a progress bar toward the annual goal, and follow-ups.",
},
{
displayText: "Travel planner",
prompt:
"Design a trip planner with a range CalendarBlock (2 months), an Accordion for 3 destinations (Tokyo, Paris, New York) each with description, tags, and a budget progress bar, then a preferences form with select, slider, and checkboxes. Add follow-ups.",
},
{
displayText: "Market watch",
prompt:
"Fetch stock prices for AAPL, NVDA, GOOGL, and TSLA. Show a market overview with tags, a comparison table, an alert for the biggest mover, and a DrawerBlock with a BarChart comparing all four. Add follow-ups.",
},
{ displayText: "AAPL stock price", prompt: "What's the current Apple stock price?" },
{
displayText: "Contact form",
prompt: "Build me a contact form with name, email, topic, and message fields.",
displayText: "Event RSVP",
prompt:
"Create an event RSVP form for a tech summit with an info alert, and a form containing inputs for name and email, a select for ticket tier, radio group for diet, date picker, slider for group size, checkboxes for sessions, and switches for notifications. Add follow-ups.",
},
{
displayText: "Team standup",
prompt:
"Generate a team standup board with a sprint progress bar, a task table (5 members), a warning alert for blockers, an Accordion (Yesterday, Today, Blockers), and a DialogBlock that opens sprint metrics with a PieChart and summary table. Add follow-ups.",
},
{
displayText: "Recipe card",
prompt:
"Build a recipe card for Spicy Thai Basil Chicken with tags, Tabs (Ingredients table, Instructions accordion with 5 steps, Nutrition donut PieChart + table), a blockquote chef's tip, and buttons with a DialogBlock for the full recipe. Add follow-ups.",
},
{
displayText: "Data table",
displayText: "Chart showcase",
prompt:
"Show me a table of the top 5 programming languages by popularity with year created.",
"Build a 'Global Tech Industry Report 2025' with tags and Tabs containing six chart types: a grouped BarChart (quarterly revenue, 3 series), a LineChart (monthly trends, 2 series), an AreaChart (yearly adoption, 2 series), a donut PieChart (market share, 6 slices), a RadarChart (developer skills, 2 series), and a ScatterChart (funding vs revenue, 2 series with labeled points). Below add a summary table, a RadialChart for industry goals, and follow-ups.",
},
],
}}
Expand Down
48 changes: 29 additions & 19 deletions examples/shadcn-chat/src/lib/shadcn-genui/components/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,43 @@ export const Tabs = defineComponent({
description: "Tabbed content. items: TabItem[]. defaultValue: initially active tab.",
component: ({ props, renderNode }) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const items = (props.items ?? []) as any[];
const rawItems = (props.items ?? []) as any[];

const items = rawItems.filter(
(item) => item?.props?.value != null && item?.props?.trigger != null,
);

const [userSelected, setUserSelected] = React.useState<string | null>(null);

const firstValue = items[0]?.props?.value as string | undefined;
const resolvedDefault = props.defaultValue ?? firstValue;
const preferredDefault = props.defaultValue ?? firstValue;

const [activeTab, setActiveTab] = React.useState<string | undefined>(resolvedDefault);
const userSelectionValid =
userSelected != null && items.some((item) => String(item?.props?.value) === userSelected);
const activeTab = userSelectionValid ? userSelected : (preferredDefault ?? "");

React.useEffect(() => {
if (!activeTab && resolvedDefault) {
setActiveTab(resolvedDefault);
}
}, [activeTab, resolvedDefault]);
if (items.length === 0) return null;

return (
<ShadcnTabs value={activeTab ?? ""} onValueChange={setActiveTab}>
<ShadcnTabs value={activeTab} onValueChange={setUserSelected}>
<TabsList>
{items.map((item, i) => (
<TabsTrigger key={i} value={String(item?.props?.value ?? i)}>
{String(item?.props?.trigger ?? "")}
</TabsTrigger>
))}
{items.map((item) => {
const val = String(item.props.value);
return (
<TabsTrigger key={val} value={val}>
{String(item.props.trigger)}
</TabsTrigger>
);
})}
</TabsList>
{items.map((item, i) => (
<TabsContent key={i} value={String(item?.props?.value ?? i)} className="space-y-3">
{renderNode(item?.props?.content)}
</TabsContent>
))}
{items.map((item) => {
const val = String(item.props.value);
return (
<TabsContent key={val} value={val} className="space-y-3">
{renderNode(item.props.content)}
</TabsContent>
);
})}
</ShadcnTabs>
);
},
Expand Down
Loading