Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ typings
clean-install.sh
lint-fix.sh

draft/
draft/
39 changes: 39 additions & 0 deletions examples/vercel-ai-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
16 changes: 16 additions & 0 deletions examples/vercel-ai-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;
12 changes: 12 additions & 0 deletions examples/vercel-ai-chat/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { NextConfig } from "next";

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

export default nextConfig;
32 changes: 32 additions & 0 deletions examples/vercel-ai-chat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "vercel-ai-chat",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@ai-sdk/openai": "^3.0.0",
"@openuidev/lang-react": "workspace:*",
"@openuidev/react-headless": "workspace:*",
"@openuidev/react-ui": "workspace:*",
"ai": "^6.0.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"zod": "^3.25.76"
},
"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/vercel-ai-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;
62 changes: 62 additions & 0 deletions examples/vercel-ai-chat/src/app/agent/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";
import "@openuidev/react-ui/components.css";
import "@openuidev/react-ui/styles/index.css";

import { vercelAIAdapter, vercelAIMessageFormat } from "@openuidev/react-headless";
import { FullScreen } from "@openuidev/react-ui";
import { defaultExamples, defaultLibrary } from "@openuidev/react-ui/genui-lib";

const systemPrompt = `You are a helpful AI agent with access to tools. Use them when appropriate.

Available tools:
- get_weather: Get current weather for any city
- get_stock_price: Get stock prices by ticker symbol (e.g. AAPL, GOOGL)
- calculate: Evaluate math expressions
- search_web: Search the web for information

Always use the appropriate tool when the user asks about weather, stocks, math, or needs web information. Present results clearly using markdown and GenUI components.

Your response should be in the following vertical format:
don't stack cards horizontally, always stack them vertically.
don't use any other format.
${defaultLibrary.prompt({ examples: defaultExamples })}`;

export default function AgentPage() {
return (
<div className="h-screen w-screen overflow-hidden">
<FullScreen
processMessage={async ({ messages, abortController }) => {
return fetch("/api/agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: vercelAIMessageFormat.toApi(messages),
systemPrompt,
}),
signal: abortController.signal,
});
}}
streamProtocol={vercelAIAdapter()}
componentLibrary={defaultLibrary}
agentName="Vercel AI Agent"
conversationStarters={{
variant: "short",
options: [
{
displayText: "Weather in Tokyo",
prompt: "What's the weather like in Tokyo right now?",
},
{
displayText: "AAPL stock price",
prompt: "What's the current stock price for AAPL?",
},
{
displayText: "Calculate something",
prompt: "What is (42 * 17) + sqrt(144)?",
},
],
}}
/>
</div>
);
}
159 changes: 159 additions & 0 deletions examples/vercel-ai-chat/src/app/api/agent/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { streamText, tool, stepCountIs } from "ai";
import type { ModelMessage } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

export const maxDuration = 30;

const weatherTool = tool({
description: "Get current weather for a location.",
inputSchema: z.object({
location: z.string().describe("City name"),
}),
execute: async ({ location }) => {
await new Promise((r) => setTimeout(r, 800));
const knownTemps: Record<string, number> = {
tokyo: 22,
"san francisco": 18,
london: 14,
"new york": 25,
paris: 19,
sydney: 27,
mumbai: 33,
berlin: 16,
};
const conditions = [
"Sunny",
"Partly Cloudy",
"Cloudy",
"Light Rain",
"Clear Skies",
];
const temp =
knownTemps[location.toLowerCase()] ?? Math.floor(Math.random() * 30 + 5);
const condition =
conditions[Math.floor(Math.random() * conditions.length)];
return {
location,
temperature_celsius: temp,
temperature_fahrenheit: Math.round(temp * 1.8 + 32),
condition,
humidity_percent: Math.floor(Math.random() * 40 + 40),
wind_speed_kmh: Math.floor(Math.random() * 25 + 5),
forecast: [
{
day: "Tomorrow",
high: temp + 2,
low: temp - 4,
condition: "Partly Cloudy",
},
{
day: "Day After",
high: temp + 1,
low: temp - 3,
condition: "Sunny",
},
],
};
},
});

const stockPriceTool = tool({
description: "Get stock price for a ticker symbol.",
inputSchema: z.object({
symbol: z.string().describe("Ticker symbol, e.g. AAPL"),
}),
execute: async ({ symbol }) => {
await new Promise((r) => setTimeout(r, 600));
const s = symbol.toUpperCase();
const knownPrices: Record<string, number> = {
AAPL: 189.84,
GOOGL: 141.8,
TSLA: 248.42,
MSFT: 378.91,
AMZN: 178.25,
NVDA: 875.28,
META: 485.58,
};
const price = knownPrices[s] ?? Math.floor(Math.random() * 500 + 20);
const change = parseFloat((Math.random() * 8 - 4).toFixed(2));
return {
symbol: s,
price: parseFloat((price + change).toFixed(2)),
change,
change_percent: parseFloat(((change / price) * 100).toFixed(2)),
volume: `${(Math.random() * 50 + 10).toFixed(1)}M`,
day_high: parseFloat((price + Math.abs(change) + 1.5).toFixed(2)),
day_low: parseFloat((price - Math.abs(change) - 1.2).toFixed(2)),
};
},
});

const calculateTool = tool({
description: "Evaluate a math expression.",
inputSchema: z.object({
expression: z.string().describe("Math expression"),
}),
execute: async ({ expression }) => {
await new Promise((r) => setTimeout(r, 300));
try {
const sanitized = expression.replace(
/[^0-9+\-*/().%\s,Math.sqrtpowabsceilfloorround]/g,
"",
);
const result = new Function(`return (${sanitized})`)();
return { expression, result: Number(result) };
} catch {
return { expression, error: "Invalid expression" };
}
},
});

const searchWebTool = tool({
description: "Search the web for information.",
inputSchema: z.object({
query: z.string().describe("Search query"),
}),
execute: async ({ query }) => {
await new Promise((r) => setTimeout(r, 1000));
return {
query,
results: [
{
title: `Top result for "${query}"`,
snippet: `Comprehensive overview of ${query} with the latest information.`,
},
{
title: `${query} - Latest News`,
snippet: `Recent developments and updates related to ${query}.`,
},
{
title: `Understanding ${query}`,
snippet: `An in-depth guide explaining everything about ${query}.`,
},
],
};
},
});

export async function POST(req: Request) {
const { messages, systemPrompt } = (await req.json()) as {
messages: ModelMessage[];
systemPrompt?: string;
};

const result = streamText({
model: openai("gpt-5.2"),
system: systemPrompt,
messages,
tools: {
get_weather: weatherTool,
get_stock_price: stockPriceTool,
calculate: calculateTool,
search_web: searchWebTool,
},
stopWhen: stepCountIs(5),
});

return result.toUIMessageStreamResponse();
}
20 changes: 20 additions & 0 deletions examples/vercel-ai-chat/src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { streamText } from "ai";
import type { ModelMessage } from "ai";
import { openai } from "@ai-sdk/openai";

export const maxDuration = 30;

export async function POST(req: Request) {
const { messages, systemPrompt } = (await req.json()) as {
messages: ModelMessage[];
systemPrompt?: string;
};

const result = streamText({
model: openai("gpt-5.2"),
system: systemPrompt,
messages,
});

return result.toUIMessageStreamResponse();
}
1 change: 1 addition & 0 deletions examples/vercel-ai-chat/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "tailwindcss";
19 changes: 19 additions & 0 deletions examples/vercel-ai-chat/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "Vercel AI Chat",
description: "Generative UI Chat with Vercel AI SDK",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Loading