
Ai Sdk Ui
- 68 installs
- 51 repo stars
- Updated November 25, 2025
- ovachiever/droid-tings
Helps with ai & agent building tasks during AI-assisted development.
About
ai-sdk-ui is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- ai-sdk-ui
- AI & Agent Building
- AI-coding skill
Ai Sdk Ui by the numbers
- 68 all-time installs (skills.sh)
- Ranked #5,755 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ovachiever/droid-tings --skill ai-sdk-uiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 68 |
|---|---|
| repo stars | ★ 51 |
| Last updated | November 25, 2025 |
| Repository | ovachiever/droid-tings ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
AI SDK UI - Frontend React Hooks
Frontend React hooks for AI-powered user interfaces with Vercel AI SDK v5/v6.
Version: AI SDK v5.0.99 (Stable) / v6.0.0-beta.108 (Beta) Framework: React 18+, Next.js 14+ Last Updated: 2025-11-22
---
AI SDK 6 Beta (November 2025)
Status: Beta (stable release planned end of 2025) Latest: ai@6.0.0-beta.108 (Nov 22, 2025) Migration: Minimal breaking changes from v5 → v6
New UI Features in v6 Beta
1. Agent Integration Type-safe messaging with agents using InferAgentUIMessage<typeof agent>:
import { useChat } from '@ai-sdk/react';
import type { InferAgentUIMessage } from 'ai';
import { myAgent } from './agent';
export default function AgentChat() {
const { messages, sendMessage } = useChat<InferAgentUIMessage<typeof myAgent>>({
api: '/api/chat',
});
// messages are now type-checked against agent schema
}2. Tool Approval Workflows (Human-in-the-Loop) Request user confirmation before executing tools:
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function ChatWithApproval() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
api: '/api/chat',
});
const handleApprove = (toolCallId: string) => {
addToolApprovalResponse({
toolCallId,
approved: true, // or false to deny
});
};
return (
<div>
{messages.map(message => (
<div key={message.id}>
{message.toolInvocations?.map(tool => (
tool.state === 'awaiting-approval' && (
<div key={tool.toolCallId}>
<p>Approve tool call: {tool.toolName}?</p>
<button onClick={() => handleApprove(tool.toolCallId)}>
Approve
</button>
<button onClick={() => addToolApprovalResponse({
toolCallId: tool.toolCallId,
approved: false
})}>
Deny
</button>
</div>
)
))}
</div>
))}
</div>
);
}3. Auto-Submit Capability Automatically continue conversation after handling approvals:
import { useChat, lastAssistantMessageIsCompleteWithApprovalResponses } from '@ai-sdk/react';
export default function AutoSubmitChat() {
const { messages, sendMessage } = useChat({
api: '/api/chat',
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
// Automatically resubmit after all approval responses provided
});
}4. Structured Output in Chat Generate structured data alongside tool calling (previously only available in useObject):
import { useChat } from '@ai-sdk/react';
import { z } from 'zod';
const schema = z.object({
summary: z.string(),
sentiment: z.enum(['positive', 'neutral', 'negative']),
});
export default function StructuredChat() {
const { messages, sendMessage } = useChat({
api: '/api/chat',
// Server can now stream structured output with chat messages
});
}---
useChat Hook - v4 → v5 Breaking Changes
CRITICAL: useChat no longer manages input state in v5!
v4 (OLD - DON'T USE):
const { messages, input, handleInputChange, handleSubmit, append } = useChat();
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>v5 (NEW - CORRECT):
const { messages, sendMessage } = useChat();
const [input, setInput] = useState('');
<form onSubmit={(e) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
}}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>Summary of v5 Changes: 1. Input management removed: input, handleInputChange, handleSubmit no longer exist 2. `append()` → `sendMessage()`: New method for sending messages 3. `onResponse` removed: Use onFinish instead 4. `initialMessages` → controlled mode: Use messages prop for full control 5. `maxSteps` removed: Handle on server-side only
See references/use-chat-migration.md for complete migration guide.
---
useAssistant Hook
Interact with OpenAI-compatible assistant APIs with automatic UI state management.
Import:
import { useAssistant } from '@ai-sdk/react';Basic Usage:
'use client';
import { useAssistant } from '@ai-sdk/react';
import { useState, FormEvent } from 'react';
export default function AssistantChat() {
const { messages, sendMessage, isLoading, error } = useAssistant({
api: '/api/assistant',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div>
{messages.map(m => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
/>
</form>
{error && <div>{error.message}</div>}
</div>
);
}Use Cases:
- Building OpenAI Assistant-powered UIs
- Managing assistant threads and runs
- Streaming assistant responses with UI state management
- File search and code interpreter integrations
See official docs for complete API reference: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-assistant
---
Top UI Errors & Solutions
See references/top-ui-errors.md for complete documentation. Quick reference:
1. useChat Failed to Parse Stream
Error: SyntaxError: Unexpected token in JSON at position X
Cause: API route not returning proper stream format.
Solution:
// ✅ CORRECT
return result.toDataStreamResponse();
// ❌ WRONG
return new Response(result.textStream);2. useChat No Response
Cause: API route not streaming correctly.
Solution:
// App Router - use toDataStreamResponse()
export async function POST(req: Request) {
const result = streamText({ /* ... */ });
return result.toDataStreamResponse(); // ✅
}
// Pages Router - use pipeDataStreamToResponse()
export default async function handler(req, res) {
const result = streamText({ /* ... */ });
return result.pipeDataStreamToResponse(res); // ✅
}3. Streaming Not Working When Deployed
Cause: Deployment platform buffering responses.
Solution: Vercel auto-detects streaming. Other platforms may need configuration.
4. Stale Body Values with useChat
Cause: body option captured at first render only.
Solution:
// ❌ WRONG - body captured once
const { userId } = useUser();
const { messages } = useChat({
body: { userId }, // Stale!
});
// ✅ CORRECT - use controlled mode
const { userId } = useUser();
const { messages, sendMessage } = useChat();
sendMessage({
content: input,
data: { userId }, // Fresh on each send
});5. React Maximum Update Depth
Cause: Infinite loop in useEffect.
Solution:
// ❌ WRONG
useEffect(() => {
saveMessages(messages);
}, [messages, saveMessages]); // saveMessages triggers re-render!
// ✅ CORRECT
useEffect(() => {
saveMessages(messages);
}, [messages]); // Only depend on messagesSee references/top-ui-errors.md for 7 more common errors.
---
Streaming Best Practices
Performance
Always use streaming for better UX:
// ✅ GOOD - Streaming (shows tokens as they arrive)
const { messages } = useChat({ api: '/api/chat' });
// ❌ BAD - Non-streaming (user waits for full response)
const response = await fetch('/api/chat', { method: 'POST' });UX Patterns
Show loading states:
{isLoading && <div>AI is typing...</div>}Provide stop button:
{isLoading && <button onClick={stop}>Stop</button>}Auto-scroll to latest message:
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);Disable input while loading:
<input disabled={isLoading} />See references/streaming-patterns.md for comprehensive best practices.
---
When to Use This Skill
Use ai-sdk-ui When:
- Building React chat interfaces
- Implementing AI completions in UI
- Streaming AI responses to frontend
- Building Next.js AI applications
- Handling chat message state
- Displaying tool calls in UI
- Managing file attachments with AI
- Migrating from v4 to v5 (UI hooks)
- Encountering useChat/useCompletion errors
Don't Use When:
- Need backend AI functionality → Use ai-sdk-core instead
- Building non-React frontends (Svelte, Vue) → Check official docs
- Need Generative UI / RSC → See https://ai-sdk.dev/docs/ai-sdk-rsc
- Building native apps → Different SDK required
Related Skills:
- ai-sdk-core - Backend text generation, structured output, tools, agents
- Compose both for full-stack AI applications
---
Package Versions
Stable (v5):
{
"dependencies": {
"ai": "^5.0.99",
"@ai-sdk/react": "^1.0.0",
"@ai-sdk/openai": "^2.0.68",
"react": "^18.2.0",
"zod": "^3.23.8"
}
}Beta (v6):
{
"dependencies": {
"ai": "6.0.0-beta.108",
"@ai-sdk/react": "beta",
"@ai-sdk/openai": "beta"
}
}Version Notes:
- AI SDK v5.0.99 (stable, Nov 2025)
- AI SDK v6.0.0-beta.108 (beta, Nov 22, 2025) - minimal breaking changes
- React 18+ (React 19 supported)
- Next.js 14+ recommended (13.4+ works)
- Zod 3.23.8+ for schema validation
---
Links to Official Documentation
Core UI Hooks:
- AI SDK UI Overview: https://ai-sdk.dev/docs/ai-sdk-ui/overview
- useChat: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot
- useCompletion: https://ai-sdk.dev/docs/ai-sdk-ui/completion
- useObject: https://ai-sdk.dev/docs/ai-sdk-ui/object-generation
Advanced Topics (Link Only):
- Generative UI (RSC): https://ai-sdk.dev/docs/ai-sdk-rsc/overview
- Stream Protocols: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocols
- Message Metadata: https://ai-sdk.dev/docs/ai-sdk-ui/message-metadata
Next.js Integration:
- Next.js App Router: https://ai-sdk.dev/docs/getting-started/nextjs-app-router
- Next.js Pages Router: https://ai-sdk.dev/docs/getting-started/nextjs-pages-router
Migration & Troubleshooting:
- v4→v5 Migration: https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
- Troubleshooting: https://ai-sdk.dev/docs/troubleshooting
- Common Issues: https://ai-sdk.dev/docs/troubleshooting/common-issues
Vercel Deployment:
- Vercel Functions: https://vercel.com/docs/functions
- Streaming on Vercel: https://vercel.com/docs/functions/streaming
---
Templates
This skill includes the following templates in templates/:
1. use-chat-basic.tsx - Basic chat with manual input (v5 pattern) 2. use-chat-tools.tsx - Chat with tool calling UI rendering 3. use-chat-attachments.tsx - File attachments support 4. use-completion-basic.tsx - Basic text completion 5. use-object-streaming.tsx - Streaming structured data 6. nextjs-chat-app-router.tsx - Next.js App Router complete example 7. nextjs-chat-pages-router.tsx - Next.js Pages Router complete example 8. nextjs-api-route.ts - API route for both App and Pages Router 9. message-persistence.tsx - Save/load chat history 10. custom-message-renderer.tsx - Custom message components with markdown 11. package.json - Dependencies template
Reference Documents
See references/ for:
- use-chat-migration.md - Complete v4→v5 migration guide
- streaming-patterns.md - UI streaming best practices
- top-ui-errors.md - 12 common UI errors with solutions
- nextjs-integration.md - Next.js setup patterns
- links-to-official-docs.md - Organized links to official docs
---
Production Tested: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev) Last Updated: 2025-11-22
{
"name": "ai-sdk-ui",
"description": "Build React chat interfaces and AI-powered UIs with Vercel AI SDK v5. Provides useChat, useCompletion, and useObject hooks for streaming responses, managing conversation state, and handling file attachments. Use when: implementing chat interfaces, streaming AI completions, managing message state in Next.js apps, or troubleshooting useChat failed to parse stream or useChat no response errors.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"license": "MIT",
"repository": "https://github.com/jezweb/claude-skills",
"keywords": []
}
AI SDK UI - Frontend React Hooks
Version: AI SDK v5.0.95+ Status: Production-Ready ✅ Framework: React 18+, Next.js 14+ Last Updated: 2025-11-19
---
What This Skill Does
Provides complete implementation patterns for Vercel AI SDK v5 frontend React hooks:
- useChat - Chat interfaces with streaming
- useCompletion - Text completions
- useObject - Streaming structured data
Focus: React UI layer for AI-powered applications.
---
Auto-Trigger Keywords
This skill should be automatically discovered when working with any of the following:
Primary Keywords (Highest Priority)
ai sdk uiuseChat hookuseCompletion hookuseObject hookreact ai chatai chat interfacechat ui reactai sdk reactvercel ai uiai react hooksstreaming ai uireact streaming chatnextjs ai chatnextjs ainext.js chatai chat componentreact ai components
Secondary Keywords (Medium Priority)
nextjs app router ainextjs pages router aichat message statemessage persistence aiai file attachmentsfile upload ai chatstreaming chat reactreal-time ai chattool calling uiai tools reactai completion reacttext completion uistructured data streaminguseObject streamingreact chat appreact ai application
Error-Based Keywords (Trigger on Errors)
useChat failed to parse streamparse stream erroruseChat no responsechat hook no responseunclosed streams aistream not closingstreaming not working deployedvercel streaming issuestreaming not working proxiedproxy bufferingstrange stream output0: characters streamstale body values useChatbody not updatingcustom headers not working useChatreact maximum update depthinfinite loop useChatrepeated assistant messagesduplicate messagesonFinish not calledstream abortedv5 migration useChatuseChat breaking changesinput handleInputChange removedsendMessage v5
Framework Integration Keywords
nextjs ai integrationnext.js ai sdkvite react airemix ai chatvercel ai deployment
Provider Keywords
openai react chatanthropic react chatclaude chat uigpt chat interface
---
Quick Start
npm install ai @ai-sdk/openai5-minute chat interface:
// app/chat/page.tsx
'use client';
import { useChat } from 'ai/react';
import { useState } from 'react';
export default function Chat() {
const { messages, sendMessage, isLoading } = useChat({ api: '/api/chat' });
const [input, setInput] = useState('');
return (
<div>
{messages.map(m => (
<div key={m.id}>{m.role}: {m.content}</div>
))}
<form onSubmit={(e) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
}}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
</div>
);
}// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({ model: openai('gpt-4-turbo'), messages });
return result.toDataStreamResponse();
}---
What's Included
Templates (11)
1. use-chat-basic.tsx - Basic chat with v5 input management 2. use-chat-tools.tsx - Chat with tool calling UI 3. use-chat-attachments.tsx - File attachments support 4. use-completion-basic.tsx - Text completion streaming 5. use-object-streaming.tsx - Structured data streaming 6. nextjs-chat-app-router.tsx - Next.js App Router complete example 7. nextjs-chat-pages-router.tsx - Next.js Pages Router complete example 8. nextjs-api-route.ts - API route for both routers 9. message-persistence.tsx - localStorage persistence 10. custom-message-renderer.tsx - Markdown & code highlighting 11. package.json - Dependencies template
References (5)
1. use-chat-migration.md - Complete v4→v5 migration guide 2. streaming-patterns.md - UI streaming best practices 3. top-ui-errors.md - 12 common UI errors with solutions 4. nextjs-integration.md - Next.js setup patterns 5. links-to-official-docs.md - Official docs organization
Scripts (1)
1. check-versions.sh - Verify package versions
---
Critical v5 Changes
BREAKING: useChat no longer manages input state!
v4 (OLD):
const { input, handleInputChange, handleSubmit } = useChat();
<input value={input} onChange={handleInputChange} />v5 (NEW):
const { sendMessage } = useChat();
const [input, setInput] = useState('');
<input value={input} onChange={(e) => setInput(e.target.value)} />Other changes:
append()→sendMessage()onResponseremoved → useonFinishinitialMessages→ controlled mode withmessagespropmaxStepsremoved (handle server-side)
See references/use-chat-migration.md for complete migration guide.
---
Token Savings
Without skill: ~15,500 tokens (research, trial-and-error, debugging) With skill: ~7,000 tokens (templates, references, examples)
Savings: ~55% (8,500 tokens)
---
Errors Prevented
This skill documents and prevents 12 common UI errors:
1. useChat failed to parse stream 2. useChat no response 3. Unclosed streams 4. Streaming not working when deployed 5. Streaming not working when proxied 6. Strange stream output (0:... characters) 7. Stale body values 8. Custom headers not working 9. React maximum update depth 10. Repeated assistant messages 11. onFinish not called when aborted 12. Type errors with message parts
---
When to Use This Skill
Use ai-sdk-ui when:
- Building React chat interfaces
- Implementing AI completions in UI
- Streaming AI responses to frontend
- Building Next.js AI applications
- Handling chat message state
- Displaying tool calls in UI
- Managing file attachments with AI
- Migrating from v4 to v5
- Encountering useChat/useCompletion errors
Don't use when:
- Need backend AI (use ai-sdk-core instead)
- Building non-React frontends (check official docs)
- Need Generative UI / RSC (advanced topic)
---
Related Skills
- ai-sdk-core - Backend text generation, structured output, tools, agents
- Compose both for full-stack AI applications
---
Package Versions
Required:
ai: ^5.0.76@ai-sdk/openai: ^2.0.53react: ^18.2.0zod: ^3.23.8
Next.js:
next: ^14.0.0react: ^18.2.0react-dom: ^18.2.0
---
Official Documentation
Core UI Hooks:
- AI SDK UI Overview: https://ai-sdk.dev/docs/ai-sdk-ui/overview
- useChat: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot
- useCompletion: https://ai-sdk.dev/docs/ai-sdk-ui/completion
- useObject: https://ai-sdk.dev/docs/ai-sdk-ui/object-generation
Next.js:
- App Router: https://ai-sdk.dev/docs/getting-started/nextjs-app-router
- Pages Router: https://ai-sdk.dev/docs/getting-started/nextjs-pages-router
Migration:
- v4→v5: https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
---
Production Validation
Tested In: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev)
Verified:
- ✅ All 11 templates work copy-paste
- ✅ v5 breaking changes documented
- ✅ 12 common errors prevented
- ✅ Package versions current (2025-11-19)
- ✅ Next.js App Router & Pages Router examples
- ✅ Token savings: 55%
---
Recent Updates (v1.0.1 - 2025-11-19)
- Updated Package Versions: AI SDK 5.0.95, @ai-sdk/anthropic 2.0.45, @ai-sdk/openai 2.0.68, @ai-sdk/google 2.0.38
- Added Metadata: YAML frontmatter now includes version tracking and last_verified date
- Clarified Engine Versions: Added comment explaining minimum supported versions in package.json
- Zod 3.x for Compatibility: Templates use Zod 3.23.8 for maximum compatibility
---
License: MIT
AI SDK UI - Official Documentation Links
Organized links to official AI SDK UI and React hooks documentation.
Last Updated: 2025-10-22
---
AI SDK UI Documentation
Core Hooks
- AI SDK UI Overview: https://ai-sdk.dev/docs/ai-sdk-ui/overview
- useChat: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot
- useCompletion: https://ai-sdk.dev/docs/ai-sdk-ui/completion
- useObject: https://ai-sdk.dev/docs/ai-sdk-ui/object-generation
Advanced Topics (Not Replicated in This Skill)
- Generative UI (RSC): https://ai-sdk.dev/docs/ai-sdk-rsc/overview
- Stream Protocols: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocols
- Message Metadata: https://ai-sdk.dev/docs/ai-sdk-ui/message-metadata
- Custom Transports: https://ai-sdk.dev/docs/ai-sdk-ui/transports
---
Next.js Integration
- Next.js App Router: https://ai-sdk.dev/docs/getting-started/nextjs-app-router
- Next.js Pages Router: https://ai-sdk.dev/docs/getting-started/nextjs-pages-router
- Next.js Documentation: https://nextjs.org/docs
---
Migration & Troubleshooting
- v4 → v5 Migration Guide: https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
- Troubleshooting Guide: https://ai-sdk.dev/docs/troubleshooting
- Common Issues: https://ai-sdk.dev/docs/troubleshooting/common-issues
- All Error Types (28 total): https://ai-sdk.dev/docs/reference/ai-sdk-errors
---
API Reference
- useChat API: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat
- useCompletion API: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-completion
- useObject API: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-object
---
Vercel Deployment
- Vercel Functions: https://vercel.com/docs/functions
- Streaming on Vercel: https://vercel.com/docs/functions/streaming
- Environment Variables: https://vercel.com/docs/projects/environment-variables
- AI SDK 5.0 Release: https://vercel.com/blog/ai-sdk-5
---
GitHub & Community
- GitHub Repository: https://github.com/vercel/ai
- GitHub Issues: https://github.com/vercel/ai/issues
- GitHub Discussions: https://github.com/vercel/ai/discussions
- Discord Community: https://discord.gg/vercel
---
TypeScript & React
- TypeScript Handbook: https://www.typescriptlang.org/docs/
- React Documentation: https://react.dev
---
Complementary Skills
For complete AI SDK coverage, also see:
- ai-sdk-core skill: Backend text generation, structured output, tools, agents
- cloudflare-workers-ai skill: Native Cloudflare Workers AI binding (no multi-provider)
---
Quick Navigation
I want to...
Build a chat interface:
- Docs: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot
- Template:
templates/use-chat-basic.tsx
Stream text completions:
- Docs: https://ai-sdk.dev/docs/ai-sdk-ui/completion
- Template:
templates/use-completion-basic.tsx
Generate structured output:
- Docs: https://ai-sdk.dev/docs/ai-sdk-ui/object-generation
- Template:
templates/use-object-streaming.tsx
Migrate from v4:
- Docs: https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
- Reference:
references/use-chat-migration.md
Fix a UI error:
- Reference:
references/top-ui-errors.md - Docs: https://ai-sdk.dev/docs/reference/ai-sdk-errors
Deploy to production:
- Reference:
references/nextjs-integration.md - Docs: https://vercel.com/docs/functions/streaming
---
Last Updated: 2025-10-22
AI SDK UI - Next.js Integration
Complete guide for integrating AI SDK UI with Next.js.
Last Updated: 2025-10-22
---
App Router (Next.js 13+)
Directory Structure
app/
├── api/
│ └── chat/
│ └── route.ts # API route
├── chat/
│ └── page.tsx # Chat page (Client Component)
└── layout.tsxChat Page (Client Component)
// app/chat/page.tsx
'use client'; // REQUIRED
import { useChat } from 'ai/react';
import { useState } from 'react';
export default function ChatPage() {
const { messages, sendMessage, isLoading } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div>
{messages.map(m => <div key={m.id}>{m.content}</div>)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
</div>
);
}API Route
// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
});
return result.toDataStreamResponse(); // App Router method
}---
Pages Router (Next.js 12 and earlier)
Directory Structure
pages/
├── api/
│ └── chat.ts # API route
└── chat.tsx # Chat pageChat Page
// pages/chat.tsx
import { useChat } from 'ai/react';
import { useState } from 'react';
export default function ChatPage() {
const { messages, sendMessage, isLoading } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div>
{messages.map(m => <div key={m.id}>{m.content}</div>)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
</div>
);
}API Route
// pages/api/chat.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { messages } = req.body;
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
});
return result.pipeDataStreamToResponse(res); // Pages Router method
}---
Key Differences
| Feature | App Router | Pages Router |
|---|---|---|
| Route Handler | app/api/chat/route.ts | pages/api/chat.ts |
| Stream Method | toDataStreamResponse() | pipeDataStreamToResponse() |
| Client Directive | Requires 'use client' | Not required |
| Server Components | Supported | Not supported |
---
Environment Variables
.env.local
# Required
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_GENERATIVE_AI_API_KEY=...
# Optional
NODE_ENV=developmentAccessing in API Routes
// App Router
export async function POST(req: Request) {
const apiKey = process.env.OPENAI_API_KEY;
// ...
}
// Pages Router
export default async function handler(req, res) {
const apiKey = process.env.OPENAI_API_KEY;
// ...
}---
Deployment to Vercel
1. Add Environment Variables
In Vercel Dashboard: 1. Go to Settings → Environment Variables 2. Add OPENAI_API_KEY, ANTHROPIC_API_KEY, etc. 3. Select environments (Production, Preview, Development)
2. Deploy
npm run build
vercel deployVercel auto-detects streaming and configures appropriately.
3. Verify Streaming
Check response headers:
Transfer-Encoding: chunkedX-Vercel-Streaming: true
Docs: https://vercel.com/docs/functions/streaming
---
Common Issues
Issue: "useChat is not defined"
Cause: Not importing from correct package.
Fix:
import { useChat } from 'ai/react'; // ✅ Correct
import { useChat } from 'ai'; // ❌ WrongIssue: "Cannot use 'use client' directive"
Cause: Using 'use client' in Pages Router.
Fix: Remove 'use client' - only needed in App Router.
Issue: "API route returns 405 Method Not Allowed"
Cause: Using GET instead of POST.
Fix: Ensure API route exports POST function (App Router) or checks req.method === 'POST' (Pages Router).
---
Official Documentation
- App Router: https://ai-sdk.dev/docs/getting-started/nextjs-app-router
- Pages Router: https://ai-sdk.dev/docs/getting-started/nextjs-pages-router
- Next.js Docs: https://nextjs.org/docs
---
Last Updated: 2025-10-22
AI SDK UI - Streaming Best Practices
UI patterns and best practices for streaming AI responses.
Last Updated: 2025-10-22
---
Performance
Always Use Streaming for Long-Form Content
// ✅ GOOD: Streaming provides better perceived performance
const { messages } = useChat({ api: '/api/chat' });
// ❌ BAD: Blocking - user waits for entire response
const response = await fetch('/api/chat', { method: 'POST' });Why?
- Users see tokens as they arrive
- Perceived performance is much faster
- Users can start reading before response completes
- Can stop generation early
---
UX Patterns
1. Show Loading States
const { messages, isLoading } = useChat();
{isLoading && (
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-200" />
</div>
)}2. Provide Stop Button
const { isLoading, stop } = useChat();
{isLoading && (
<button onClick={stop} className="bg-red-500 text-white px-4 py-2 rounded">
Stop Generation
</button>
)}3. Auto-Scroll to Latest Message
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
<div ref={messagesEndRef} />4. Disable Input While Loading
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading} // Prevent new messages while generating
className="disabled:bg-gray-100"
/>5. Handle Empty States
{messages.length === 0 ? (
<div className="text-center">
<h2>Start a conversation</h2>
<p>Ask me anything!</p>
</div>
) : (
// Messages list
)}---
Error Handling
1. Display Errors to Users
const { error } = useChat();
{error && (
<div className="p-4 bg-red-50 text-red-700 rounded">
<strong>Error:</strong> {error.message}
</div>
)}2. Provide Retry Functionality
const { error, reload } = useChat();
{error && (
<div className="flex items-center justify-between p-4 bg-red-50">
<span>{error.message}</span>
<button onClick={reload} className="px-3 py-1 border rounded">
Retry
</button>
</div>
)}3. Handle Network Failures Gracefully
useChat({
onError: (error) => {
console.error('Chat error:', error);
// Log to monitoring service (Sentry, etc.)
// Show user-friendly message
},
});4. Log Errors for Debugging
useChat({
onError: (error) => {
const errorLog = {
timestamp: new Date().toISOString(),
message: error.message,
url: window.location.href,
};
console.error('AI SDK Error:', errorLog);
// Send to Sentry/Datadog/etc.
},
});---
Message Rendering
1. Support Markdown
Use react-markdown for rich content:
import ReactMarkdown from 'react-markdown';
{messages.map(m => (
<ReactMarkdown>{m.content}</ReactMarkdown>
))}2. Handle Code Blocks
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
<ReactMarkdown
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter language={match[1]}>
{String(children)}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{message.content}
</ReactMarkdown>3. Display Tool Calls Visually
{message.toolInvocations?.map((tool, idx) => (
<div key={idx} className="bg-blue-50 border border-blue-200 p-3 rounded">
<div className="font-semibold">Tool: {tool.toolName}</div>
<div className="text-sm">Args: {JSON.stringify(tool.args)}</div>
{tool.result && (
<div className="text-sm">Result: {JSON.stringify(tool.result)}</div>
)}
</div>
))}4. Show Timestamps
<div className="text-xs text-gray-500">
{new Date(message.createdAt).toLocaleTimeString()}
</div>5. Group Messages by Role
{messages.reduce((groups, message, idx) => {
const prevMessage = messages[idx - 1];
const showRole = !prevMessage || prevMessage.role !== message.role;
return [
...groups,
<div key={message.id}>
{showRole && <div className="font-bold">{message.role}</div>}
<div>{message.content}</div>
</div>
];
}, [])}---
State Management
1. Persist Chat History
const chatId = 'chat-123';
const { messages } = useChat({
id: chatId,
initialMessages: loadFromLocalStorage(chatId),
});
useEffect(() => {
saveToLocalStorage(chatId, messages);
}, [messages, chatId]);2. Clear Chat Functionality
const { setMessages } = useChat();
const clearChat = () => {
if (confirm('Clear chat history?')) {
setMessages([]);
}
};3. Export/Import Conversations
const exportChat = () => {
const json = JSON.stringify(messages, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chat-${Date.now()}.json`;
a.click();
};
const importChat = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const imported = JSON.parse(e.target?.result as string);
setMessages(imported);
};
reader.readAsText(file);
};4. Handle Multiple Chats (Routing)
// Use URL params for chat ID
const searchParams = useSearchParams();
const chatId = searchParams.get('chatId') || 'default';
const { messages } = useChat({
id: chatId,
initialMessages: loadMessages(chatId),
});
// Navigation
<Link href={`/chat?chatId=${newChatId}`}>New Chat</Link>---
Advanced Patterns
1. Debounced Input for Completions
import { useDebouncedCallback } from 'use-debounce';
const { complete } = useCompletion();
const debouncedComplete = useDebouncedCallback((value) => {
complete(value);
}, 500);
<input onChange={(e) => debouncedComplete(e.target.value)} />2. Optimistic Updates
const { messages, sendMessage } = useChat();
const optimisticSend = (content: string) => {
// Add user message immediately
const tempMessage = {
id: `temp-${Date.now()}`,
role: 'user',
content,
};
setMessages([...messages, tempMessage]);
// Send to server
sendMessage({ content });
};3. Custom Message Formatting
const formatMessage = (content: string) => {
// Replace @mentions
content = content.replace(/@(\w+)/g, '<span class="mention">@$1</span>');
// Replace URLs
content = content.replace(
/(https?:\/\/[^\s]+)/g,
'<a href="$1" target="_blank">$1</a>'
);
return content;
};
<div dangerouslySetInnerHTML={{ __html: formatMessage(message.content) }} />4. Typing Indicators
const [isTyping, setIsTyping] = useState(false);
useChat({
onFinish: () => setIsTyping(false),
});
const handleSend = (content: string) => {
setIsTyping(true);
sendMessage({ content });
};
{isTyping && <div className="text-gray-500 italic">AI is typing...</div>}---
Performance Optimization
1. Virtualize Long Message Lists
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={messages.length}
itemSize={100}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{messages[index].content}
</div>
)}
</FixedSizeList>2. Lazy Load Message History
const [page, setPage] = useState(1);
const messagesPerPage = 50;
const visibleMessages = messages.slice(
(page - 1) * messagesPerPage,
page * messagesPerPage
);3. Memoize Message Rendering
import { memo } from 'react';
const MessageComponent = memo(({ message }: { message: Message }) => {
return <div>{message.content}</div>;
});
{messages.map(m => <MessageComponent key={m.id} message={m} />)}---
Official Documentation
- AI SDK UI Overview: https://ai-sdk.dev/docs/ai-sdk-ui/overview
- Streaming Protocols: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocols
- Message Metadata: https://ai-sdk.dev/docs/ai-sdk-ui/message-metadata
---
Last Updated: 2025-10-22
AI SDK UI - Top 12 Errors & Solutions
Common AI SDK UI errors with actionable solutions.
Last Updated: 2025-10-22
---
1. useChat Failed to Parse Stream
Error: SyntaxError: Unexpected token in JSON at position X
Cause: API route not returning proper stream format.
Solution:
// ✅ CORRECT (App Router)
export async function POST(req: Request) {
const result = streamText({ /* ... */ });
return result.toDataStreamResponse(); // Correct method
}
// ✅ CORRECT (Pages Router)
export default async function handler(req, res) {
const result = streamText({ /* ... */ });
return result.pipeDataStreamToResponse(res); // Correct method
}
// ❌ WRONG
return new Response(result.textStream); // Missing stream protocol---
2. useChat No Response
Cause: API route not streaming correctly or wrong method.
Solution:
// Check 1: Are you using the right method?
// App Router: toDataStreamResponse()
// Pages Router: pipeDataStreamToResponse()
// Check 2: Is your API route returning a Response?
export async function POST(req: Request) {
const result = streamText({ model: openai('gpt-4'), messages });
return result.toDataStreamResponse(); // Must return this!
}
// Check 3: Check network tab - is the request completing?
// If status is 200 but no data: likely streaming issue---
3. Unclosed Streams
Cause: Stream not properly closed in API.
Solution:
// ✅ GOOD: SDK handles closing automatically
export async function POST(req: Request) {
const result = streamText({ model: openai('gpt-4'), messages });
return result.toDataStreamResponse();
}
// ❌ BAD: Manual stream handling (error-prone)
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// ...must manually close!
controller.close();
}
});GitHub Issue: #4123
---
4. Streaming Not Working When Deployed
Cause: Deployment platform buffering responses.
Solution:
- Vercel: Auto-detects streaming (no config needed)
- Netlify: Ensure Edge Functions enabled
- Cloudflare Workers: Use
toDataStreamResponse() - Other platforms: Check for response buffering settings
// Vercel - works out of the box
export async function POST(req: Request) {
const result = streamText({ /* ... */ });
return result.toDataStreamResponse();
}Docs: https://vercel.com/docs/functions/streaming
---
5. Streaming Not Working When Proxied
Cause: Proxy (nginx, Cloudflare, etc.) buffering responses.
Solution:
Nginx:
location /api/ {
proxy_pass http://localhost:3000;
proxy_buffering off; # Disable buffering
proxy_cache off;
}Cloudflare: Disable "Auto Minify" in dashboard
---
6. Strange Stream Output (0:... characters)
Error: Seeing raw stream protocol like 0:"Hello" in browser.
Cause: Not using correct hook or consuming stream directly.
Solution:
// ✅ CORRECT: Use useChat hook
const { messages } = useChat({ api: '/api/chat' });
// ❌ WRONG: Consuming stream directly
const response = await fetch('/api/chat');
const reader = response.body.getReader(); // Don't do this!---
7. Stale Body Values with useChat
Cause: body captured at first render only.
Solution:
// ❌ BAD: body captured once
const { userId } = useUser();
const { messages } = useChat({
body: { userId }, // Stale! Won't update if userId changes
});
// ✅ GOOD: Use data in sendMessage
const { userId } = useUser();
const { messages, sendMessage } = useChat();
sendMessage({
content: input,
data: { userId }, // Fresh value on each send
});---
8. Custom Headers Not Working with useChat
Cause: Headers not passed correctly.
Solution:
// ✅ CORRECT
const { messages } = useChat({
headers: {
'Authorization': `Bearer ${token}`,
'X-Custom-Header': 'value',
},
});
// OR use fetch options
const { messages } = useChat({
fetch: (url, options) => {
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});
},
});---
9. React Maximum Update Depth
Error: Maximum update depth exceeded
Cause: Infinite loop in useEffect.
Solution:
// ❌ BAD: Infinite loop
const saveMessages = (messages) => { /* ... */ };
useEffect(() => {
saveMessages(messages);
}, [messages, saveMessages]); // saveMessages changes every render!
// ✅ GOOD: Only depend on messages
useEffect(() => {
localStorage.setItem('messages', JSON.stringify(messages));
}, [messages]); // saveMessages not needed in deps---
10. Repeated Assistant Messages
Cause: Duplicate message handling or multiple sendMessage calls.
Solution:
// ❌ BAD: Calling sendMessage multiple times
const handleSubmit = (e) => {
e.preventDefault();
sendMessage({ content: input });
sendMessage({ content: input }); // Duplicate!
};
// ✅ GOOD: Single call
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return; // Guard
sendMessage({ content: input });
setInput('');
};---
11. onFinish Not Called When Stream Aborted
Cause: Stream abort doesn't trigger onFinish callback.
Solution:
const { stop } = useChat({
onFinish: (message) => {
console.log('Finished:', message);
},
});
// Handle abort separately
const handleStop = () => {
stop();
console.log('Stream aborted by user');
// Do cleanup here
};---
12. Type Error with Message Parts (v5)
Error: Property 'parts' does not exist on type 'Message'
Cause: v5 changed message structure for tool calls.
Solution:
// ✅ CORRECT (v5)
messages.map(message => {
// Use content for simple messages
if (message.content) {
return <div>{message.content}</div>;
}
// Use toolInvocations for tool calls
if (message.toolInvocations) {
return message.toolInvocations.map(tool => (
<div key={tool.toolCallId}>
Tool: {tool.toolName}
</div>
));
}
});
// ❌ WRONG (v4 style)
message.toolCalls // Doesn't exist in v5---
For More Errors
See complete error reference (28 total types): https://ai-sdk.dev/docs/reference/ai-sdk-errors
---
Last Updated: 2025-10-22
useChat v4 → v5 Migration Guide
Complete guide to migrating from AI SDK v4 to v5 for UI hooks.
Last Updated: 2025-10-22 Applies to: AI SDK v5.0+
---
Critical Breaking Change
BREAKING: useChat no longer manages input state!
In v4, useChat provided input, handleInputChange, and handleSubmit. In v5, you must manage input state manually using useState.
---
Quick Migration Checklist
- [ ] Replace
input,handleInputChange,handleSubmitwith manual state - [ ] Change
append()tosendMessage() - [ ] Replace
onResponsewithonFinish - [ ] Move
initialMessagesto controlled mode withmessagesprop - [ ] Remove
maxSteps(handle server-side) - [ ] Update message rendering for parts structure (if using tools)
---
1. Input State Management (CRITICAL)
v4 (OLD)
import { useChat } from 'ai/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: '/api/chat',
});
return (
<div>
{messages.map(m => <div key={m.id}>{m.content}</div>)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
</div>
);
}v5 (NEW)
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function Chat() {
const { messages, sendMessage } = useChat({
api: '/api/chat',
});
// Manual input state
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div>
{messages.map(m => <div key={m.id}>{m.content}</div>)}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</form>
</div>
);
}Why?
- More control over input handling
- Easier to add features like debouncing, validation, etc.
- Consistent with React patterns
---
2. append() → sendMessage()
v4 (OLD)
const { append } = useChat();
// Append a message
append({
role: 'user',
content: 'Hello',
});v5 (NEW)
const { sendMessage } = useChat();
// Send a message (role is assumed to be 'user')
sendMessage({
content: 'Hello',
});
// With attachments
sendMessage({
content: 'Analyze this image',
experimental_attachments: [
{ name: 'image.png', contentType: 'image/png', url: 'blob:...' },
],
});Why?
- Clearer API:
sendMessageis more intuitive thanappend - Supports attachments natively
- Role is always 'user' (no need to specify)
---
3. onResponse → onFinish
v4 (OLD)
const { messages } = useChat({
onResponse: (response) => {
console.log('Response received:', response);
},
});v5 (NEW)
const { messages } = useChat({
onFinish: (message, options) => {
console.log('Response finished:', message);
console.log('Finish reason:', options.finishReason);
console.log('Usage:', options.usage);
},
});Why?
onResponsefired too early (when response started)onFinishfires when response is complete- Provides more context (usage, finish reason)
---
4. initialMessages → Controlled Mode
v4 (OLD)
const { messages } = useChat({
initialMessages: [
{ role: 'system', content: 'You are a helpful assistant.' },
],
});v5 (NEW - Option 1: Uncontrolled)
const { messages } = useChat({
// Use initialMessages for read-only initialization
initialMessages: [
{ role: 'system', content: 'You are a helpful assistant.' },
],
});v5 (NEW - Option 2: Controlled)
const [messages, setMessages] = useState([
{ role: 'system', content: 'You are a helpful assistant.' },
]);
const { sendMessage } = useChat({
messages, // Pass messages for controlled mode
onUpdate: ({ messages }) => {
setMessages(messages); // Sync state
},
});Why?
- Clearer distinction between controlled and uncontrolled
- Easier to persist messages to database
---
5. maxSteps Removed
v4 (OLD)
const { messages } = useChat({
maxSteps: 5, // Limit agent steps
});v5 (NEW)
Handle maxSteps (or stopWhen) on the server-side only:
// app/api/chat/route.ts
import { streamText, stopWhen } from 'ai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4'),
messages,
maxSteps: 5, // Handle on server
});
return result.toDataStreamResponse();
}Why?
- Server has more control over costs
- Prevents client-side bypass
- Consistent with v5 architecture
---
6. Message Structure (for Tools)
v4 (OLD)
// Simple message structure
{
id: '1',
role: 'assistant',
content: 'The weather is sunny',
toolCalls: [...] // Tool calls as separate property
}v5 (NEW)
// Parts-based structure
{
id: '1',
role: 'assistant',
content: 'The weather is sunny', // Still exists for simple messages
parts: [
{ type: 'text', content: 'The weather is' },
{ type: 'tool-call', toolName: 'getWeather', args: { location: 'SF' } },
{ type: 'tool-result', toolName: 'getWeather', result: { temp: 72 } },
{ type: 'text', content: 'sunny' },
]
}Rendering v5 Messages:
messages.map(message => {
// For simple text messages, use content
if (message.content) {
return <div>{message.content}</div>;
}
// For tool calls, use toolInvocations
if (message.toolInvocations) {
return message.toolInvocations.map(tool => (
<div key={tool.toolCallId}>
Tool: {tool.toolName}
Args: {JSON.stringify(tool.args)}
Result: {JSON.stringify(tool.result)}
</div>
));
}
});---
7. Other Removed/Changed Properties
Removed in v5
input- Use manualuseStatehandleInputChange- UseonChange={(e) => setInput(e.target.value)}handleSubmit- Use custom submit handleronResponse- UseonFinishinstead
Renamed in v5
append()→sendMessage()initialMessages→ Still exists, but usemessagesprop for controlled mode
Added in v5
sendMessage()- New way to send messagesexperimental_attachments- File attachments supporttoolInvocations- Simplified tool call rendering
---
Common Migration Patterns
Pattern 1: Basic Chat
v4:
const { messages, input, handleInputChange, handleSubmit } = useChat();
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>v5:
const { messages, sendMessage } = useChat();
const [input, setInput] = useState('');
<form onSubmit={(e) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
}}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>Pattern 2: With Initial Messages
v4:
const { messages } = useChat({
initialMessages: loadFromStorage(),
});v5:
const { messages } = useChat({
initialMessages: loadFromStorage(), // Still works
});Pattern 3: With Response Callback
v4:
useChat({
onResponse: (res) => console.log('Started'),
});v5:
useChat({
onFinish: (msg, opts) => {
console.log('Finished');
console.log('Tokens:', opts.usage.totalTokens);
},
});---
Migration Troubleshooting
Error: "input is undefined"
Cause: You're using v5 but trying to access input from useChat.
Fix: Add manual input state:
const [input, setInput] = useState('');Error: "append is not a function"
Cause: append() was renamed to sendMessage() in v5.
Fix: Replace all instances of append() with sendMessage().
Error: "handleSubmit is undefined"
Cause: v5 doesn't provide handleSubmit.
Fix: Create custom submit handler:
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};Warning: "onResponse is deprecated"
Cause: v5 removed onResponse.
Fix: Use onFinish instead.
---
Official Migration Resources
- v5 Migration Guide: https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
- useChat API Reference: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat
- v5 Release Notes: https://vercel.com/blog/ai-sdk-5
---
Last Updated: 2025-10-22
#!/bin/bash
# Check installed AI SDK UI package versions against latest
# Usage: ./scripts/check-versions.sh
echo "==================================="
echo " AI SDK UI - Version Checker"
echo "==================================="
echo ""
packages=(
"ai"
"@ai-sdk/openai"
"@ai-sdk/anthropic"
"@ai-sdk/google"
"react"
"react-dom"
"next"
"zod"
)
echo "Checking package versions..."
echo ""
for package in "${packages[@]}"; do
echo "📦 $package"
# Get installed version
installed=$(npm list "$package" --depth=0 2>/dev/null | grep "$package" | awk -F@ '{print $NF}')
if [ -z "$installed" ]; then
echo " ❌ Not installed"
else
echo " ✅ Installed: $installed"
fi
# Get latest version
latest=$(npm view "$package" version 2>/dev/null)
if [ -z "$latest" ]; then
echo " ⚠️ Could not fetch latest version"
else
echo " 📌 Latest: $latest"
# Compare versions
if [ "$installed" = "$latest" ]; then
echo " ✨ Up to date!"
elif [ -n "$installed" ]; then
echo " ⬆️ Update available"
fi
fi
echo ""
done
echo "==================================="
echo " Recommended Versions (AI SDK v5)"
echo "==================================="
echo ""
echo "ai: ^5.0.76"
echo "@ai-sdk/openai: ^2.0.53"
echo "@ai-sdk/anthropic: ^2.0.0"
echo "@ai-sdk/google: ^2.0.0"
echo "react: ^18.2.0"
echo "react-dom: ^18.2.0"
echo "next: ^14.0.0"
echo "zod: ^3.23.8"
echo ""
echo "To update all packages:"
echo "npm install ai@latest @ai-sdk/openai@latest @ai-sdk/anthropic@latest @ai-sdk/google@latest react@latest react-dom@latest next@latest zod@latest"
echo ""
/**
* AI SDK UI - Custom Message Renderer
*
* Demonstrates:
* - Markdown rendering (react-markdown)
* - Code syntax highlighting (react-syntax-highlighter)
* - Custom message components
* - Copy code button
* - Timestamp display
* - User avatars
*
* Dependencies:
* npm install react-markdown react-syntax-highlighter
* npm install --save-dev @types/react-syntax-highlighter
*
* Usage:
* 1. Install dependencies
* 2. Copy this component
* 3. Use <MessageRenderer message={message} /> in your chat
*/
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
import type { Message } from 'ai';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
// Custom message renderer component
function MessageRenderer({ message }: { message: Message }) {
const [copied, setCopied] = useState(false);
const copyCode = (code: string) => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[75%] rounded-lg p-4 ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-white border shadow-sm'
}`}
>
{/* Avatar & name */}
<div className="flex items-center space-x-2 mb-2">
<div
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
message.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700'
}`}
>
{message.role === 'user' ? 'U' : 'AI'}
</div>
<span className="text-xs font-semibold">
{message.role === 'user' ? 'You' : 'Assistant'}
</span>
</div>
{/* Message content with markdown */}
<div
className={`prose prose-sm ${
message.role === 'user' ? 'prose-invert' : ''
} max-w-none`}
>
<ReactMarkdown
components={{
// Custom code block renderer
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const codeString = String(children).replace(/\n$/, '');
return !inline && match ? (
<div className="relative group">
<SyntaxHighlighter
style={oneDark}
language={match[1]}
PreTag="div"
className="rounded-lg"
{...props}
>
{codeString}
</SyntaxHighlighter>
<button
onClick={() => copyCode(codeString)}
className="absolute top-2 right-2 px-2 py-1 text-xs bg-gray-700 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity"
>
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
) : (
<code
className={`${
message.role === 'user'
? 'bg-blue-600'
: 'bg-gray-100'
} px-1 rounded`}
{...props}
>
{children}
</code>
);
},
}}
>
{message.content}
</ReactMarkdown>
</div>
{/* Timestamp */}
<div
className={`text-xs mt-2 ${
message.role === 'user' ? 'text-blue-100' : 'text-gray-500'
}`}
>
{new Date(message.createdAt || Date.now()).toLocaleTimeString()}
</div>
</div>
</div>
);
}
// Main chat component
export default function ChatWithCustomRenderer() {
const { messages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ content: input });
setInput('');
};
return (
<div className="flex flex-col h-screen max-w-4xl mx-auto">
{/* Header */}
<div className="p-4 border-b bg-white">
<h1 className="text-2xl font-bold">Custom Message Renderer</h1>
<p className="text-sm text-gray-600">
With markdown, syntax highlighting, and copy buttons
</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 bg-gray-50 space-y-4">
{messages.length === 0 && (
<div className="flex items-center justify-center h-full text-center">
<div>
<div className="text-6xl mb-4">✨</div>
<h2 className="text-xl font-semibold text-gray-700">
Try asking for code examples
</h2>
<p className="text-gray-500 mt-2">
Messages will render with markdown and syntax highlighting
</p>
<div className="mt-4 space-y-2">
{[
'Write a Python function to sort a list',
'Explain React hooks with code examples',
'Show me a TypeScript interface example',
].map((suggestion, idx) => (
<button
key={idx}
onClick={() => setInput(suggestion)}
className="block w-full p-2 text-left border rounded hover:bg-white"
>
{suggestion}
</button>
))}
</div>
</div>
</div>
)}
{messages.map((message) => (
<MessageRenderer key={message.id} message={message} />
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white border p-3 rounded-lg">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" />
</div>
</div>
</div>
)}
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200 text-red-700">
<strong>Error:</strong> {error.message}
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t bg-white">
<div className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask for code examples..."
disabled={isLoading}
className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-6 py-3 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Send
</button>
</div>
</form>
</div>
);
}
// ============================================================================
// Simpler Version (without react-markdown)
// ============================================================================
/*
// Simple markdown parsing without external dependencies
function SimpleMarkdownRenderer({ content }: { content: string }) {
// Basic markdown parsing
const parseMarkdown = (text: string) => {
// Code blocks
text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => {
return `<pre><code class="language-${lang || 'text'}">${code}</code></pre>`;
});
// Inline code
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
// Bold
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// Italic
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// Line breaks
text = text.replace(/\n/g, '<br/>');
return text;
};
return (
<div dangerouslySetInnerHTML={{ __html: parseMarkdown(content) }} />
);
}
*/
/**
* AI SDK UI - Message Persistence
*
* Demonstrates:
* - Saving chat history to localStorage
* - Loading previous conversations
* - Multiple chat sessions
* - Clear history functionality
*
* Features:
* - Auto-save on message changes
* - Persistent chat IDs
* - Load on mount
* - Clear/delete chats
*
* Usage:
* 1. Copy this component
* 2. Customize storage mechanism (localStorage, database, etc.)
* 3. Add chat history UI if needed
*/
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent, useEffect } from 'react';
import type { Message } from 'ai';
// Storage key prefix
const STORAGE_KEY_PREFIX = 'ai-chat-';
// Helper functions for localStorage
const saveMessages = (chatId: string, messages: Message[]) => {
try {
localStorage.setItem(
`${STORAGE_KEY_PREFIX}${chatId}`,
JSON.stringify(messages)
);
} catch (error) {
console.error('Failed to save messages:', error);
}
};
const loadMessages = (chatId: string): Message[] => {
try {
const stored = localStorage.getItem(`${STORAGE_KEY_PREFIX}${chatId}`);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Failed to load messages:', error);
return [];
}
};
const clearMessages = (chatId: string) => {
try {
localStorage.removeItem(`${STORAGE_KEY_PREFIX}${chatId}`);
} catch (error) {
console.error('Failed to clear messages:', error);
}
};
const listChats = (): string[] => {
const chats: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith(STORAGE_KEY_PREFIX)) {
chats.push(key.replace(STORAGE_KEY_PREFIX, ''));
}
}
return chats;
};
export default function PersistentChat() {
// Generate or use existing chat ID
const [chatId, setChatId] = useState<string>('');
const [isLoaded, setIsLoaded] = useState(false);
// Initialize chat ID
useEffect(() => {
// Try to load from URL params or generate new
const params = new URLSearchParams(window.location.search);
const urlChatId = params.get('chatId');
if (urlChatId) {
setChatId(urlChatId);
} else {
// Generate new chat ID
const newChatId = `chat-${Date.now()}`;
setChatId(newChatId);
// Update URL
const url = new URL(window.location.href);
url.searchParams.set('chatId', newChatId);
window.history.replaceState({}, '', url.toString());
}
setIsLoaded(true);
}, []);
const { messages, setMessages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
id: chatId,
initialMessages: isLoaded ? loadMessages(chatId) : [],
});
const [input, setInput] = useState('');
// Save messages whenever they change
useEffect(() => {
if (chatId && messages.length > 0) {
saveMessages(chatId, messages);
}
}, [messages, chatId]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ content: input });
setInput('');
};
const handleClearChat = () => {
if (confirm('Are you sure you want to clear this chat?')) {
clearMessages(chatId);
setMessages([]);
}
};
const handleNewChat = () => {
const newChatId = `chat-${Date.now()}`;
setChatId(newChatId);
setMessages([]);
// Update URL
const url = new URL(window.location.href);
url.searchParams.set('chatId', newChatId);
window.history.pushState({}, '', url.toString());
};
if (!isLoaded) {
return <div className="flex items-center justify-center h-screen">Loading...</div>;
}
return (
<div className="flex flex-col h-screen max-w-4xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b bg-white">
<div>
<h1 className="text-2xl font-bold">Persistent Chat</h1>
<p className="text-sm text-gray-600">
Chat ID: <code className="bg-gray-100 px-1 rounded">{chatId}</code>
</p>
</div>
<div className="flex space-x-2">
<button
onClick={handleNewChat}
className="px-3 py-1 text-sm border rounded hover:bg-gray-50"
>
New Chat
</button>
{messages.length > 0 && (
<button
onClick={handleClearChat}
className="px-3 py-1 text-sm border border-red-300 text-red-600 rounded hover:bg-red-50"
>
Clear
</button>
)}
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 bg-gray-50">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full text-center">
<div>
<div className="text-6xl mb-4">💾</div>
<h2 className="text-xl font-semibold text-gray-700">
Your conversation is saved
</h2>
<p className="text-gray-500 mt-2">
All messages are automatically saved to localStorage
</p>
</div>
</div>
) : (
<div className="space-y-4 max-w-3xl mx-auto">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-white border shadow-sm'
}`}
>
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white border p-3 rounded-lg shadow-sm">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" />
</div>
</div>
</div>
)}
</div>
)}
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200 text-red-700">
<strong>Error:</strong> {error.message}
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t bg-white">
<div className="max-w-4xl mx-auto">
<div className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-6 py-3 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-600"
>
Send
</button>
</div>
<div className="mt-2 text-xs text-gray-500 text-center">
{messages.length > 0 && (
<>Last saved: {new Date().toLocaleTimeString()}</>
)}
</div>
</div>
</form>
</div>
);
}
// ============================================================================
// Database Persistence Example (Supabase)
// ============================================================================
/*
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const saveMessagesToDB = async (chatId: string, messages: Message[]) => {
const { error } = await supabase
.from('chat_messages')
.upsert({ chat_id: chatId, messages, updated_at: new Date() });
if (error) console.error('Save error:', error);
};
const loadMessagesFromDB = async (chatId: string): Promise<Message[]> => {
const { data, error } = await supabase
.from('chat_messages')
.select('messages')
.eq('chat_id', chatId)
.single();
if (error) {
console.error('Load error:', error);
return [];
}
return data?.messages || [];
};
*/
/**
* Next.js API Routes for useChat
*
* Shows both App Router and Pages Router patterns.
*
* Key Difference:
* - App Router: Use toDataStreamResponse()
* - Pages Router: Use pipeDataStreamToResponse()
*
* This file includes both patterns for reference.
*/
// ============================================================================
// APP ROUTER (Next.js 13+)
// ============================================================================
// Location: app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
system: 'You are a helpful AI assistant.',
maxOutputTokens: 1000,
temperature: 0.7,
});
// App Router: Use toDataStreamResponse()
return result.toDataStreamResponse();
}
// ============================================================================
// PAGES ROUTER (Next.js 12 and earlier)
// ============================================================================
// Location: pages/api/chat.ts
/*
import type { NextApiRequest, NextApiResponse } from 'next';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { messages } = req.body;
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
system: 'You are a helpful AI assistant.',
});
// Pages Router: Use pipeDataStreamToResponse()
return result.pipeDataStreamToResponse(res);
}
*/
// ============================================================================
// WITH ANTHROPIC (Claude)
// ============================================================================
/*
import { anthropic } from '@ai-sdk/anthropic';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic('claude-3-5-sonnet-20241022'),
messages,
});
return result.toDataStreamResponse();
}
*/
// ============================================================================
// WITH GOOGLE (Gemini)
// ============================================================================
/*
import { google } from '@ai-sdk/google';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: google('gemini-1.5-pro'),
messages,
});
return result.toDataStreamResponse();
}
*/
// ============================================================================
// WITH CLOUDFLARE WORKERS AI
// ============================================================================
/*
// Requires: workers-ai-provider
import { createWorkersAI } from 'workers-ai-provider';
// For Cloudflare Workers (not Next.js):
export default {
async fetch(request, env) {
const { messages } = await request.json();
const workersai = createWorkersAI({ binding: env.AI });
const result = streamText({
model: workersai('@cf/meta/llama-3.1-8b-instruct'),
messages,
});
return result.toDataStreamResponse();
},
};
*/
// ============================================================================
// WITH ERROR HANDLING
// ============================================================================
/*
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
try {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
});
return result.toDataStreamResponse();
} catch (error) {
console.error('API error:', error);
return new Response(
JSON.stringify({
error: 'An error occurred while processing your request.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
*/
// ============================================================================
// WITH TOOLS
// ============================================================================
/*
import { streamText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
tools: {
getWeather: tool({
description: 'Get the current weather for a location',
inputSchema: z.object({
location: z.string().describe('The city name'),
}),
execute: async ({ location }) => {
// Simulated weather API call
return {
location,
temperature: 72,
condition: 'sunny',
};
},
}),
},
});
return result.toDataStreamResponse();
}
*/
// ============================================================================
// FOR useCompletion
// ============================================================================
// Location: app/api/completion/route.ts
/*
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: openai('gpt-3.5-turbo'),
prompt,
maxOutputTokens: 500,
});
return result.toDataStreamResponse();
}
*/
// ============================================================================
// FOR useObject
// ============================================================================
// Location: app/api/recipe/route.ts
/*
import { streamObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamObject({
model: openai('gpt-4'),
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.string()),
instructions: z.array(z.string()),
}),
}),
prompt: `Generate a recipe for ${prompt}`,
});
return result.toTextStreamResponse();
}
*/
/**
* Next.js App Router - Complete Chat Example
*
* Complete production-ready chat interface for Next.js App Router.
*
* Features:
* - v5 useChat with manual input management
* - Auto-scroll to bottom
* - Loading states & error handling
* - Stop generation button
* - Responsive design
* - Keyboard shortcuts (Enter to send, Cmd+K to clear)
*
* Directory structure:
* app/
* ├── chat/
* │ └── page.tsx (this file)
* └── api/
* └── chat/
* └── route.ts (see nextjs-api-route.ts)
*
* Usage:
* 1. Copy to app/chat/page.tsx
* 2. Create API route (see nextjs-api-route.ts)
* 3. Navigate to /chat
*/
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent, useRef, useEffect } from 'react';
export default function ChatPage() {
const { messages, sendMessage, isLoading, error, stop, reload } = useChat({
api: '/api/chat',
onError: (error) => {
console.error('Chat error:', error);
},
});
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Focus input on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
sendMessage({ content: input });
setInput('');
};
// Keyboard shortcuts
const handleKeyDown = (e: React.KeyboardEvent) => {
// Cmd+K or Ctrl+K to clear (focus input)
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
inputRef.current?.focus();
}
};
return (
<div className="flex flex-col h-screen max-w-4xl mx-auto" onKeyDown={handleKeyDown}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b bg-white">
<div>
<h1 className="text-2xl font-bold">AI Assistant</h1>
<p className="text-sm text-gray-600">
{messages.length > 0
? `${messages.length} message${messages.length === 1 ? '' : 's'}`
: 'Start a conversation'}
</p>
</div>
{messages.length > 0 && !isLoading && (
<button
onClick={() => window.location.reload()}
className="px-3 py-1 text-sm border rounded hover:bg-gray-50"
>
New Chat
</button>
)}
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 bg-gray-50">
{messages.length === 0 ? (
// Empty state
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-4">
<div className="text-4xl">💬</div>
<div>
<h2 className="text-xl font-semibold text-gray-900">
Start a conversation
</h2>
<p className="text-gray-600 mt-2">
Ask me anything or try one of these:
</p>
</div>
<div className="grid gap-2 max-w-md">
{[
'Explain quantum computing',
'Write a haiku about coding',
'Plan a trip to Tokyo',
].map((suggestion, idx) => (
<button
key={idx}
onClick={() => setInput(suggestion)}
className="p-3 text-left border rounded-lg hover:bg-white hover:shadow-sm transition-all"
>
{suggestion}
</button>
))}
</div>
</div>
</div>
) : (
// Messages list
<div className="space-y-4 max-w-3xl mx-auto">
{messages.map((message, idx) => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[75%] rounded-lg p-4 ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-white border shadow-sm'
}`}
>
{/* Role label (only for assistant on first message) */}
{message.role === 'assistant' && idx === 1 && (
<div className="text-xs font-semibold text-gray-500 mb-2">
AI Assistant
</div>
)}
{/* Message content */}
<div className="whitespace-pre-wrap break-words">
{message.content}
</div>
</div>
</div>
))}
{/* Loading indicator */}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white border rounded-lg p-4 shadow-sm">
<div className="flex items-center space-x-2">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" />
</div>
<span className="text-sm text-gray-600">Thinking...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
)}
</div>
{/* Error banner */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200">
<div className="flex items-center justify-between max-w-4xl mx-auto">
<div className="flex items-center space-x-2 text-red-700">
<span className="text-xl">⚠️</span>
<div>
<div className="font-semibold">Error</div>
<div className="text-sm">{error.message}</div>
</div>
</div>
<button
onClick={reload}
className="px-3 py-1 text-sm border border-red-300 rounded hover:bg-red-100"
>
Retry
</button>
</div>
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t bg-white">
<div className="max-w-4xl mx-auto">
<div className="flex space-x-2">
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 font-medium"
>
Stop
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-6 py-3 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-600 font-medium"
>
Send
</button>
)}
</div>
<div className="mt-2 text-xs text-gray-500 text-center">
Press Enter to send • Cmd+K to focus input
</div>
</div>
</form>
</div>
);
}
/**
* Next.js Pages Router - Complete Chat Example
*
* Complete production-ready chat interface for Next.js Pages Router.
*
* Features:
* - v5 useChat with manual input management
* - Auto-scroll to bottom
* - Loading states & error handling
* - Stop generation button
* - Responsive design
*
* Directory structure:
* pages/
* ├── chat.tsx (this file)
* └── api/
* └── chat.ts (see nextjs-api-route.ts)
*
* Usage:
* 1. Copy to pages/chat.tsx
* 2. Create API route at pages/api/chat.ts (see nextjs-api-route.ts)
* 3. Navigate to /chat
*/
import { useChat } from 'ai/react';
import { useState, FormEvent, useRef, useEffect } from 'react';
import Head from 'next/head';
export default function ChatPage() {
const { messages, sendMessage, isLoading, error, stop } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
sendMessage({ content: input });
setInput('');
};
return (
<>
<Head>
<title>AI Chat</title>
<meta name="description" content="Chat with AI" />
</Head>
<div className="flex flex-col h-screen max-w-3xl mx-auto">
{/* Header */}
<div className="p-4 border-b bg-white">
<h1 className="text-2xl font-bold">AI Chat</h1>
<p className="text-sm text-gray-600">
Powered by AI SDK v5 (Pages Router)
</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 bg-gray-50">
{messages.length === 0 ? (
// Empty state
<div className="flex items-center justify-center h-full text-center">
<div>
<div className="text-6xl mb-4">💬</div>
<h2 className="text-xl font-semibold text-gray-700">
Start a conversation
</h2>
<p className="text-gray-500 mt-2">
Type a message below to begin
</p>
</div>
</div>
) : (
// Messages list
<div className="space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-white border shadow-sm'
}`}
>
<div className="whitespace-pre-wrap">{message.content}</div>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white border p-3 rounded-lg shadow-sm">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" />
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
)}
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200">
<div className="text-red-700">
<strong>Error:</strong> {error.message}
</div>
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t bg-white">
<div className="flex space-x-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
Stop
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-6 py-3 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-600"
>
Send
</button>
)}
</div>
</form>
</div>
</>
);
}
{
"name": "ai-sdk-ui-app",
"version": "0.1.0",
"private": true,
"description": "AI SDK UI application with React hooks for chat, completion, and streaming",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit"
},
"dependencies": {
"ai": "^5.0.95",
"@ai-sdk/openai": "^2.0.68",
"@ai-sdk/anthropic": "^2.0.45",
"@ai-sdk/google": "^2.0.38",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^14.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.3.3",
"tailwindcss": "^4.0.0",
"@tailwindcss/vite": "^4.0.0",
"eslint": "^8.0.0",
"eslint-config-next": "^14.0.0"
},
"optionalDependencies": {
"react-markdown": "^9.0.0",
"react-syntax-highlighter": "^15.5.0",
"@types/react-syntax-highlighter": "^15.5.0",
"workers-ai-provider": "^2.0.0"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
},
"packageManager": "npm@10.0.0",
"comment": "Engine versions specify minimum supported versions for compatibility"
}
/**
* AI SDK UI - Chat with File Attachments
*
* Demonstrates:
* - File upload with experimental_attachments
* - Image preview
* - Multiple file support
* - Sending files with messages
*
* Requires:
* - API route that handles multimodal inputs (GPT-4 Vision, Claude 3.5, etc.)
* - experimental_attachments feature (v5)
*
* Usage:
* 1. Set up API route with vision model
* 2. Copy this component
* 3. Customize file handling as needed
*/
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatWithAttachments() {
const { messages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const [files, setFiles] = useState<FileList | null>(null);
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
// Handle file selection
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = e.target.files;
setFiles(selectedFiles);
if (selectedFiles) {
// Create preview URLs
const urls = Array.from(selectedFiles).map((file) =>
URL.createObjectURL(file)
);
setPreviewUrls(urls);
} else {
setPreviewUrls([]);
}
};
// Handle form submission
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim() && !files) return;
sendMessage({
content: input || 'Please analyze these images',
experimental_attachments: files
? Array.from(files).map((file) => ({
name: file.name,
contentType: file.type,
url: URL.createObjectURL(file),
}))
: undefined,
});
// Clean up
setInput('');
setFiles(null);
previewUrls.forEach((url) => URL.revokeObjectURL(url));
setPreviewUrls([]);
};
// Remove file
const removeFile = (index: number) => {
if (!files) return;
const newFiles = Array.from(files).filter((_, i) => i !== index);
const dataTransfer = new DataTransfer();
newFiles.forEach((file) => dataTransfer.items.add(file));
setFiles(dataTransfer.files);
// Update preview URLs
URL.revokeObjectURL(previewUrls[index]);
setPreviewUrls(previewUrls.filter((_, i) => i !== index));
};
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto">
{/* Header */}
<div className="p-4 border-b">
<h1 className="text-2xl font-bold">AI Chat with File Attachments</h1>
<p className="text-sm text-gray-600">
Upload images and ask questions about them
</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<div key={message.id} className="space-y-2">
{/* Text content */}
<div
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-900'
}`}
>
{message.content}
</div>
</div>
{/* Attachments */}
{message.experimental_attachments &&
message.experimental_attachments.length > 0 && (
<div
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div className="grid grid-cols-2 gap-2 max-w-[70%]">
{message.experimental_attachments.map(
(attachment, idx) => (
<div key={idx} className="relative">
{attachment.contentType?.startsWith('image/') ? (
<img
src={attachment.url}
alt={attachment.name}
className="rounded-lg max-h-40 object-cover"
/>
) : (
<div className="p-2 bg-gray-100 rounded-lg text-sm">
{attachment.name}
</div>
)}
</div>
)
)}
</div>
</div>
)}
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-200 p-3 rounded-lg">Processing...</div>
</div>
)}
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200 text-red-700">
<strong>Error:</strong> {error.message}
</div>
)}
{/* File preview */}
{previewUrls.length > 0 && (
<div className="p-4 border-t bg-gray-50">
<p className="text-sm text-gray-700 mb-2">
Selected files ({previewUrls.length}):
</p>
<div className="grid grid-cols-4 gap-2">
{previewUrls.map((url, idx) => (
<div key={idx} className="relative">
<img
src={url}
alt={`Preview ${idx + 1}`}
className="rounded-lg h-20 w-full object-cover"
/>
<button
type="button"
onClick={() => removeFile(idx)}
className="absolute top-1 right-1 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs hover:bg-red-600"
>
×
</button>
</div>
))}
</div>
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="space-y-2">
{/* File input */}
<label className="flex items-center space-x-2 cursor-pointer">
<div className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300">
📎 Attach Files
</div>
<input
type="file"
multiple
accept="image/*"
onChange={handleFileChange}
className="hidden"
/>
{files && <span className="text-sm text-gray-600">{files.length} file(s)</span>}
</label>
{/* Text input */}
<div className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask a question about the images..."
disabled={isLoading}
className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading || (!input.trim() && !files)}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Send
</button>
</div>
</div>
</form>
</div>
);
}
/**
* AI SDK UI - Basic Chat Component (v5)
*
* Demonstrates:
* - useChat hook with v5 manual input management
* - Streaming chat messages
* - Loading states
* - Error handling
* - Auto-scroll to latest message
*
* CRITICAL v5 Change: useChat NO LONGER manages input state!
* You must manually manage input with useState.
*
* Usage:
* 1. Copy this component to your app
* 2. Create API route (see nextjs-api-route.ts)
* 3. Customize styling as needed
*/
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent, useRef, useEffect } from 'react';
export default function ChatBasic() {
// useChat hook - v5 style
const { messages, sendMessage, isLoading, error, stop } = useChat({
api: '/api/chat',
});
// Manual input management (v5 requires this!)
const [input, setInput] = useState('');
// Auto-scroll to bottom
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Handle form submission
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
// v5: Use sendMessage instead of append
sendMessage({ content: input });
setInput('');
};
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto">
{/* Header */}
<div className="p-4 border-b">
<h1 className="text-2xl font-bold">AI Chat</h1>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-900'
}`}
>
{message.content}
</div>
</div>
))}
{/* Loading indicator */}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-200 p-3 rounded-lg">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-200" />
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Error message */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200 text-red-700">
<strong>Error:</strong> {error.message}
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
Stop
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-600"
>
Send
</button>
)}
</div>
</form>
</div>
);
}
/**
* AI SDK UI - Chat with Tool Calling
*
* Demonstrates:
* - Displaying tool calls in UI
* - Rendering tool arguments and results
* - Handling multi-step tool invocations
* - Visual distinction between messages and tool calls
*
* Requires:
* - API route with tools configured (see ai-sdk-core skill)
* - Backend using `tool()` helper
*
* Usage:
* 1. Set up API route with tools
* 2. Copy this component
* 3. Customize tool rendering as needed
*/
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatWithTools() {
const { messages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ content: input });
setInput('');
};
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto">
{/* Header */}
<div className="p-4 border-b">
<h1 className="text-2xl font-bold">AI Chat with Tools</h1>
<p className="text-sm text-gray-600">
Ask about weather, calculations, or search queries
</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<div key={message.id} className="space-y-2">
{/* Text content */}
{message.content && (
<div
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-900'
}`}
>
{message.content}
</div>
</div>
)}
{/* Tool invocations */}
{message.toolInvocations && message.toolInvocations.length > 0 && (
<div className="flex justify-start">
<div className="max-w-[85%] space-y-2">
{message.toolInvocations.map((tool, idx) => (
<div
key={idx}
className="border border-blue-200 bg-blue-50 p-3 rounded-lg"
>
{/* Tool name */}
<div className="flex items-center space-x-2 mb-2">
<div className="w-2 h-2 bg-blue-500 rounded-full" />
<span className="font-semibold text-blue-900">
Tool: {tool.toolName}
</span>
</div>
{/* Tool state */}
{tool.state === 'call' && (
<div className="text-sm text-blue-700">
<strong>Calling with:</strong>
<pre className="mt-1 p-2 bg-white rounded text-xs overflow-x-auto">
{JSON.stringify(tool.args, null, 2)}
</pre>
</div>
)}
{tool.state === 'result' && (
<div className="text-sm text-blue-700">
<strong>Arguments:</strong>
<pre className="mt-1 p-2 bg-white rounded text-xs overflow-x-auto">
{JSON.stringify(tool.args, null, 2)}
</pre>
<strong className="block mt-2">Result:</strong>
<pre className="mt-1 p-2 bg-white rounded text-xs overflow-x-auto">
{JSON.stringify(tool.result, null, 2)}
</pre>
</div>
)}
{tool.state === 'partial-call' && (
<div className="text-sm text-blue-600 italic">
Preparing arguments...
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-200 p-3 rounded-lg">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-200" />
</div>
</div>
</div>
)}
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200 text-red-700">
<strong>Error:</strong> {error.message}
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Try: 'What's the weather in San Francisco?'"
disabled={isLoading}
className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Send
</button>
</div>
</form>
</div>
);
}
/**
* AI SDK UI - Basic Text Completion
*
* Demonstrates:
* - useCompletion hook for text generation
* - Streaming text completion
* - Loading states
* - Stop generation
* - Clear completion
*
* Use cases:
* - Text generation (blog posts, summaries, etc.)
* - Content expansion
* - Writing assistance
*
* Usage:
* 1. Copy this component to your app
* 2. Create /api/completion route (see references)
* 3. Customize UI as needed
*/
'use client';
import { useCompletion } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function CompletionBasic() {
const {
completion,
complete,
isLoading,
error,
stop,
setCompletion,
} = useCompletion({
api: '/api/completion',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
complete(input);
setInput('');
};
const handleClear = () => {
setCompletion('');
};
return (
<div className="max-w-3xl mx-auto p-4 space-y-6">
{/* Header */}
<div>
<h1 className="text-3xl font-bold">AI Text Completion</h1>
<p className="text-gray-600 mt-2">
Enter a prompt to generate text with AI
</p>
</div>
{/* Input form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="prompt"
className="block text-sm font-medium text-gray-700 mb-2"
>
Prompt
</label>
<textarea
id="prompt"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Write a blog post about..."
rows={4}
disabled={isLoading}
className="w-full p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
</div>
<div className="flex space-x-2">
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
Stop Generation
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-6 py-2 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-600"
>
Generate
</button>
)}
{completion && (
<button
type="button"
onClick={handleClear}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600"
>
Clear
</button>
)}
</div>
</form>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
<strong>Error:</strong> {error.message}
</div>
)}
{/* Completion output */}
{(completion || isLoading) && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Generated Text</h2>
{isLoading && (
<div className="flex items-center space-x-2 text-sm text-gray-600">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce delay-200" />
<span>Generating...</span>
</div>
)}
</div>
<div className="p-4 bg-gray-50 border rounded-lg whitespace-pre-wrap">
{completion || 'Waiting for response...'}
</div>
{!isLoading && completion && (
<div className="text-sm text-gray-600">
{completion.split(/\s+/).length} words, {completion.length}{' '}
characters
</div>
)}
</div>
)}
{/* Example prompts */}
{!completion && !isLoading && (
<div className="space-y-2">
<h3 className="font-semibold">Example prompts:</h3>
<div className="space-y-2">
{[
'Write a blog post about the future of AI',
'Explain quantum computing in simple terms',
'Create a recipe for chocolate chip cookies',
'Write a product description for wireless headphones',
].map((example, idx) => (
<button
key={idx}
onClick={() => setInput(example)}
className="block w-full text-left p-2 border rounded hover:bg-gray-50"
>
{example}
</button>
))}
</div>
</div>
)}
</div>
);
}
/**
* AI SDK UI - Streaming Structured Data
*
* Demonstrates:
* - useObject hook for streaming structured data
* - Partial object updates (live as schema fields fill in)
* - Zod schema validation
* - Loading states
* - Error handling
*
* Use cases:
* - Forms generation
* - Recipe creation
* - Product specs
* - Structured content generation
*
* Usage:
* 1. Copy this component
* 2. Create /api/object route with streamObject
* 3. Define Zod schema matching your needs
*/
'use client';
import { useObject } from 'ai/react';
import { z } from 'zod';
import { FormEvent, useState } from 'react';
// Define the schema for the object
const recipeSchema = z.object({
recipe: z.object({
name: z.string().describe('Recipe name'),
description: z.string().describe('Short description'),
prepTime: z.number().describe('Preparation time in minutes'),
cookTime: z.number().describe('Cooking time in minutes'),
servings: z.number().describe('Number of servings'),
difficulty: z.enum(['easy', 'medium', 'hard']),
ingredients: z.array(
z.object({
item: z.string(),
amount: z.string(),
})
),
instructions: z.array(z.string()),
}),
});
export default function ObjectStreaming() {
const { object, submit, isLoading, error, stop } = useObject({
api: '/api/recipe',
schema: recipeSchema,
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
submit(input);
setInput('');
};
return (
<div className="max-w-4xl mx-auto p-4 space-y-6">
{/* Header */}
<div>
<h1 className="text-3xl font-bold">AI Recipe Generator</h1>
<p className="text-gray-600 mt-2">
Streaming structured data with live updates
</p>
</div>
{/* Input form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="dish"
className="block text-sm font-medium text-gray-700 mb-2"
>
What would you like to cook?
</label>
<input
id="dish"
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="e.g., 'chocolate chip cookies' or 'thai green curry'"
disabled={isLoading}
className="w-full p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
</div>
<div className="flex space-x-2">
{isLoading ? (
<button
type="button"
onClick={stop}
className="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
>
Stop
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="px-6 py-2 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-600"
>
Generate Recipe
</button>
)}
</div>
</form>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
<strong>Error:</strong> {error.message}
</div>
)}
{/* Generated recipe */}
{(object?.recipe || isLoading) && (
<div className="border rounded-lg p-6 space-y-6 bg-white shadow-sm">
{/* Recipe header */}
<div className="border-b pb-4">
<h2 className="text-2xl font-bold">
{object?.recipe?.name || (
<span className="text-gray-400 italic">
{isLoading ? 'Generating name...' : 'Recipe name'}
</span>
)}
</h2>
{object?.recipe?.description && (
<p className="text-gray-600 mt-2">{object.recipe.description}</p>
)}
</div>
{/* Recipe meta */}
<div className="grid grid-cols-4 gap-4 text-sm">
<div>
<div className="font-semibold text-gray-700">Prep Time</div>
<div>
{object?.recipe?.prepTime ? (
`${object.recipe.prepTime} min`
) : (
<span className="text-gray-400">...</span>
)}
</div>
</div>
<div>
<div className="font-semibold text-gray-700">Cook Time</div>
<div>
{object?.recipe?.cookTime ? (
`${object.recipe.cookTime} min`
) : (
<span className="text-gray-400">...</span>
)}
</div>
</div>
<div>
<div className="font-semibold text-gray-700">Servings</div>
<div>
{object?.recipe?.servings || (
<span className="text-gray-400">...</span>
)}
</div>
</div>
<div>
<div className="font-semibold text-gray-700">Difficulty</div>
<div className="capitalize">
{object?.recipe?.difficulty || (
<span className="text-gray-400">...</span>
)}
</div>
</div>
</div>
{/* Ingredients */}
<div>
<h3 className="text-xl font-semibold mb-3">Ingredients</h3>
{object?.recipe?.ingredients &&
object.recipe.ingredients.length > 0 ? (
<ul className="space-y-2">
{object.recipe.ingredients.map((ingredient, idx) => (
<li key={idx} className="flex items-start">
<span className="text-blue-500 mr-2">•</span>
<span>
{ingredient.amount} {ingredient.item}
</span>
</li>
))}
</ul>
) : (
<p className="text-gray-400 italic">
{isLoading ? 'Loading ingredients...' : 'No ingredients yet'}
</p>
)}
</div>
{/* Instructions */}
<div>
<h3 className="text-xl font-semibold mb-3">Instructions</h3>
{object?.recipe?.instructions &&
object.recipe.instructions.length > 0 ? (
<ol className="space-y-3">
{object.recipe.instructions.map((step, idx) => (
<li key={idx} className="flex items-start">
<span className="flex-shrink-0 w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm mr-3 mt-0.5">
{idx + 1}
</span>
<span>{step}</span>
</li>
))}
</ol>
) : (
<p className="text-gray-400 italic">
{isLoading ? 'Loading instructions...' : 'No instructions yet'}
</p>
)}
</div>
{/* Loading indicator */}
{isLoading && (
<div className="flex items-center justify-center space-x-2 text-blue-600 py-4">
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce delay-200" />
<span>Generating recipe...</span>
</div>
)}
</div>
)}
{/* Example prompts */}
{!object && !isLoading && (
<div className="space-y-2">
<h3 className="font-semibold">Try these:</h3>
<div className="grid grid-cols-2 gap-2">
{[
'Chocolate chip cookies',
'Thai green curry',
'Classic margarita pizza',
'Banana bread',
].map((example, idx) => (
<button
key={idx}
onClick={() => setInput(example)}
className="text-left p-3 border rounded-lg hover:bg-gray-50"
>
{example}
</button>
))}
</div>
</div>
)}
</div>
);
}