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
39 changes: 39 additions & 0 deletions examples/multi-agent-chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
270 changes: 270 additions & 0 deletions examples/multi-agent-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
# Vercel AI Chat Example

A full-stack generative UI chatbot built with the [Vercel AI SDK](https://ai-sdk.dev/) and [OpenUI Lang](https://www.openui.com/docs/openui-lang/overview). Instead of replying with plain text or markdown, the LLM generates structured UI markup that the client renders as interactive React components — cards, tables, charts, forms, and more — in real time as tokens stream in.

Features: multi-step tool calling, persistent conversation threads (localStorage), collapsible sidebar, and automatic light/dark theme support.

<video src="../../docs/public/videos/vercel-ai-chat.mp4"
noControls
playsInline
muted
preload="metadata"
className="w-full rounded-lg m-auto"
autoPlay
loop
/>

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

---

## How It Works

The LLM is prompted with a system prompt that describes a set of UI components — their names, props, and when to use them. Instead of writing prose, the model responds in **OpenUI Lang**: a declarative markup syntax that maps directly to React components. For example:

```
Card([
CardHeader(title="Weather in Tokyo"),
BarChart(data=[...], title="Temperature this week")
])
```

On the client, `<Renderer />` from `@openuidev/react-lang` parses this markup token-by-token as it streams in and renders it as real React components. The user sees UI building up live — not a wall of text.

The Vercel AI SDK handles the streaming transport on both ends:
- **Backend**: `streamText` calls the LLM, executes tools, and streams the response.
- **Frontend**: `useChat` manages message state, accumulates the streamed text, and triggers re-renders.

---

## Architecture

```
┌────────────────────────────────────┐ ┌────────────────────────────────────┐
│ Browser │ HTTP │ Next.js API Route │
│ │ ──────►│ │
│ • useChat manages message state │ │ • Loads system-prompt.txt │
│ • <Renderer /> parses OpenUI Lang │◄────── │ • Calls LLM with streamText │
│ • openuiChatLibrary renders UI │ stream │ • Executes tools server-side │
│ • Sidebar with thread history │ │ • Returns toUIMessageStreamResponse│
└────────────────────────────────────┘ └────────────────────────────────────┘
```

### Request / Response Flow

1. User types a message. `useChat` sends `POST /api/chat` with the full conversation history.
2. The API route reads `system-prompt.txt`, calls `streamText` with the model, system prompt, messages, and tool definitions.
3. If the LLM calls a tool, the Vercel AI SDK executes it server-side and feeds the result back into the model. This loop runs up to 5 steps (`stopWhen: stepCountIs(5)`).
4. The LLM generates a final OpenUI Lang response and it streams back via `toUIMessageStreamResponse()`.
5. `useChat` accumulates the streaming text and passes it to `<AssistantMessage />`.
6. `<AssistantMessage />` feeds the accumulated text into `<Renderer response={text} library={openuiChatLibrary} isStreaming={isStreaming} />`.
7. `<Renderer />` parses the markup progressively and renders live React components as each token arrives.
8. When the stream ends, `isStreaming` is set to `false`. The message is saved to localStorage via `useThreads`.

---

## Project Structure

```
vercel-ai-chat/
├── src/
│ ├── app/
│ │ ├── api/chat/route.ts # Streaming chat endpoint (Vercel AI SDK)
│ │ ├── page.tsx # Main chat page — wires useChat + components
│ │ └── layout.tsx # Root layout with ThemeProvider
│ ├── components/
│ │ ├── assistant-message.tsx # Renders <Renderer /> for each AI reply
│ │ ├── user-message.tsx # Renders user messages
│ │ ├── chat-input.tsx # Textarea input with send / stop buttons
│ │ ├── chat-header.tsx # Top bar with sidebar toggle
│ │ ├── sidebar.tsx # Thread list with new/switch/delete
│ │ ├── sidebar-toggle.tsx # Mobile sidebar toggle button
│ │ ├── conversation-starters.tsx # Suggested prompts on empty chat
│ │ ├── thinking-indicator.tsx # Animated dots while model is thinking
│ │ └── tool-call-indicator.tsx # Shows which tool is running
│ ├── hooks/
│ │ ├── use-system-theme.tsx # Detects system light/dark preference
│ │ └── use-threads.ts # Thread CRUD + localStorage persistence
│ ├── lib/
│ │ ├── tools.ts # Four mock tool definitions (Vercel AI SDK format)
│ │ └── thread-store.ts # Read/write threads to localStorage
│ ├── library.ts # OpenUI library export for prompt generation
│ └── generated/
│ └── system-prompt.txt # Auto-generated — do not edit manually
└── package.json
```

---

## Getting Started

### Prerequisites

- Node.js 18+
- pnpm
- An OpenAI API key

### 1. Install dependencies

```bash
cd examples/vercel-ai-chat
pnpm install
```

### 2. Configure environment

```bash
cp .env.example .env.local
```

Add your key to `.env.local`:

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

### 3. Start the dev server

```bash
pnpm dev
```

This runs `generate:prompt` first (compiles `src/library.ts` → `src/generated/system-prompt.txt`) then starts the Next.js dev server at `http://localhost:3000`.

---

## What's in This Example

### System Prompt Generation

`src/library.ts` re-exports `openuiChatLibrary` (as `library`) and `openuiChatPromptOptions` (as `promptOptions`) from `@openuidev/react-ui/genui-lib`. At dev time, the OpenUI CLI reads this file and generates `src/generated/system-prompt.txt` — a text file containing every component's name, prop schema, description, and usage examples. This is what the LLM receives as the system prompt, teaching it which components exist and how to use them.

Re-run generation any time you change component definitions:

```bash
pnpm generate:prompt
```

### `src/app/api/chat/route.ts` — Backend

The API route uses the Vercel AI SDK's `streamText` with five arguments:

| Argument | Value | Purpose |
| -------- | ----- | ------- |
| `model` | `openai("gpt-5.4")` | The LLM to call |
| `system` | contents of `system-prompt.txt` | Teaches the model OpenUI Lang and the available components |
| `messages` | conversation history from client | The full thread so far |
| `tools` | four tool definitions | Functions the LLM can call mid-generation |
| `stopWhen` | `stepCountIs(5)` | Stops the tool-call loop after 5 steps maximum |

`convertToModelMessages` bridges the Vercel AI SDK's `UIMessage` client format to the `ModelMessage` server format. `toUIMessageStreamResponse()` serializes the stream in the format `useChat` on the client expects.

### `src/components/assistant-message.tsx` — Frontend Renderer

Each assistant message is rendered by `<AssistantMessage />`, which:

1. Finds all `text` parts in the message (filtering out tool-call parts).
2. Concatenates them into `textContent`.
3. Passes it to `<Renderer />`:

```tsx
<Renderer
response={textContent}
library={openuiChatLibrary}
isStreaming={isStreaming}
onAction={handleAction}
/>
```

The `onAction` handler listens for `BuiltinActionType.ContinueConversation` events — fired when the user clicks a follow-up button inside a rendered card — and calls `onSend(event.humanFriendlyMessage)` to feed it back into the conversation.

### `src/hooks/use-threads.ts` — Thread Management

Conversations are organized into threads. Each thread has an `id`, `title` (derived from the first user message), `messages`, and timestamps. Threads are stored in localStorage under the key `vercel-ai-chat-threads`.

The hook exposes:

| Function | Description |
| -------- | ----------- |
| `createThread()` | Creates a new empty thread and makes it active |
| `switchThread(id)` | Loads a previous thread's messages into `useChat` |
| `deleteThread(id)` | Removes a thread; switches to a new one if it was active |
| `persistMessages(messages)` | Saves the current `useChat` messages to localStorage |

### `src/lib/tools.ts` — Mock Tools

All four tools are mock implementations with simulated network delays. They return realistic-looking data so the LLM can generate rich UI responses.

#### `get_weather`

Returns current conditions and a two-day forecast for a city.

- **Input**: `location` (string) — city name
- **Simulated delay**: 800ms
- **Returns**:

| Field | Example |
| ----- | ------- |
| `temperature_celsius` | `22` |
| `temperature_fahrenheit` | `72` |
| `condition` | `"Sunny"` |
| `humidity_percent` | `65` |
| `wind_speed_kmh` | `12` |
| `forecast` | `[{ day, high, low, condition }, ...]` (2 days) |

Hardcoded temperatures for: Tokyo (22°C), San Francisco (18°C), London (14°C), New York (25°C), Paris (19°C), Sydney (27°C), Mumbai (33°C), Berlin (16°C). Any other city gets a random temperature between 5–35°C.

#### `get_stock_price`

Returns current price data for a stock ticker.

- **Input**: `symbol` (string) — e.g. `AAPL`
- **Simulated delay**: 600ms
- **Returns**:

| Field | Example |
| ----- | ------- |
| `price` | `190.12` |
| `change` | `+0.28` |
| `change_percent` | `+0.15%` |
| `volume` | `"42.3M"` |
| `day_high` | `191.50` |
| `day_low` | `188.90` |

Hardcoded prices for: AAPL ($189.84), GOOGL ($141.80), TSLA ($248.42), MSFT ($378.91), AMZN ($178.25), NVDA ($875.28), META ($485.58). Other tickers get a random price.

#### `calculate`

Evaluates a math expression safely.

- **Input**: `expression` (string) — e.g. `"2 * (3 + 4)"` or `"Math.sqrt(144)"`
- **Simulated delay**: 300ms
- **Supports**: `+`, `-`, `*`, `/`, `()`, `%`, `Math.sqrt`, `pow`, `abs`, `ceil`, `floor`, `round`
- **Returns**: `{ expression, result }` or `{ expression, error }` on invalid input

#### `search_web`

Returns mock search results for any query.

- **Input**: `query` (string) — the search term
- **Simulated delay**: 1000ms
- **Returns**: an array of 3 results, each with `title` and `snippet` templated from the query string

---

## Scripts

| Script | Description |
| ------ | ----------- |
| `pnpm dev` | Generate system prompt, then start the Next.js dev server |
| `pnpm generate:prompt` | Recompile `src/library.ts` → `src/generated/system-prompt.txt` |
| `pnpm build` | Build for production |
| `pnpm start` | Start the production server |

---

## Learn More

- [OpenUI Lang overview](https://www.openui.com/docs/openui-lang/overview) — Library, Prompt Generator, Parser, Renderer
- [Vercel AI SDK docs](https://ai-sdk.dev/) — `streamText`, `useChat`, `tool()`
- [`@openuidev/react-lang` package](../../packages/react-lang)
16 changes: 16 additions & 0 deletions examples/multi-agent-chat/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
globalIgnores([
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);

export default eslintConfig;
8 changes: 8 additions & 0 deletions examples/multi-agent-chat/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
turbopack: {},
transpilePackages: ["@openuidev/react-ui", "@openuidev/react-lang"],
};

export default nextConfig;
36 changes: 36 additions & 0 deletions examples/multi-agent-chat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "vercel-ai-chat",
"version": "0.1.0",
"private": true,
"scripts": {
"generate:prompt": "pnpm --filter @openuidev/cli build && pnpm exec openui generate src/library.ts --out src/generated/system-prompt.txt",
"dev": "pnpm generate:prompt && next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@ai-sdk/openai": "^3.0.41",
"@ai-sdk/react": "^3.0.118",
"@openuidev/cli": "workspace:*",
"@openuidev/react-lang": "workspace:*",
"@openuidev/react-ui": "workspace:*",
"ai": "^6.0.116",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"streamdown": "^2.5.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}
7 changes: 7 additions & 0 deletions examples/multi-agent-chat/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};

export default config;
Loading
Loading