Vercel AI SDK
Drop Prompt Area into a Vercel AI SDK chatbot. PromptArea owns the input; useChat owns the stream. They meet at one line: sendMessage({ text: segmentsToPlainText(segments) }).
The AI SDK gives you transport, streaming, and message state for free. Prompt Area gives you a rich composer — mentions, slash commands, tags, inline markdown, and attachments — that most chat starters skip. Because Prompt Area is just a controlled Segment[] value, it sits cleanly between the two: you keep the AI SDK's runtime and add a real input on top.
- One integration seam. Flatten segments with
segmentsToPlainText()and hand them tosendMessage. No bespoke serialization. - Status-aware controls. The AI SDK's
statusdrives the action bar — the send button becomes a stop button while a response streams. - Structured metadata. Read typed chips with
getChipsByTrigger()and ship them in the requestbodynext to the prompt. - Provider-agnostic. Swap
@ai-sdk/anthropicfor any other provider package — the component code is identical.
Install
Add the AI SDK alongside Prompt Area. This example streams from Claude, so it uses the Anthropic provider — swap in another provider package (@ai-sdk/openai, @ai-sdk/google, …) if you prefer.
npm install ai @ai-sdk/react @ai-sdk/anthropic zodThe Anthropic provider reads ANTHROPIC_API_KEY from the environment. Set it in .env.local before calling the route.
Example
Streaming chat with useChat
Type a message and submit. The demo simulates the AI SDK stream so it runs without a backend; the Code tab shows the real useChat + Claude wiring. The send button becomes a stop button while a response streams.
useChat() for a small simulated hook so it can run in the browser with no API key. Everything else — the onSubmit wiring, segmentsToPlainText(), the status-aware send/stop button, and rendering message.parts — is identical to the production code in the Code tab.How it works
On submit, flatten the segments to a string with segmentsToPlainText() and pass it to sendMessage. Mentions, commands, and tags resolve to their display text, so a model-friendly prompt falls out for free; useChat appends it to messages and POSTs to your route.
The AI SDK's status (ready, submitted, streaming, error) drives the action bar: while a response streams, the send button becomes a stop button wired to stop(). Guard handleSubmit on both isSegmentsEmpty(segments) and the streaming state so a press during generation is a no-op rather than a second request.
On the server, convertToModelMessages() turns the UI message history into the provider format, streamText() calls the model, and toUIMessageStreamResponse() streams it back in the shape useChat expects. Render each assistant reply by mapping over message.parts and picking the text parts.
Send structured context
Flattening to text is enough for a plain chatbot, but the whole point of chips is keeping structure. Read them back with getChipsByTrigger() and attach them to the request body — the AI SDK merges body into the POST, so your route receives them next to messages.
import { getChipsByTrigger, segmentsToPlainText } from '@/components/segment-helpers'
function handleSubmit() {
const mentions = getChipsByTrigger(segments, '@')
const commands = getChipsByTrigger(segments, '/')
sendMessage(
{ text: segmentsToPlainText(segments) },
{
// Anything in `body` is merged into the POST to /api/chat,
// so typed chips travel alongside the prompt — no string parsing.
body: {
mentions: mentions.map((c) => c.value),
command: commands[0]?.value,
model: 'claude-opus-4-8',
},
},
)
setSegments([])
}Then validate the body with a Zod schema in your route handler and shape the call from the parsed, allowlisted values:
import { anthropic } from '@ai-sdk/anthropic'
import { convertToModelMessages, streamText, type UIMessage } from 'ai'
import { z } from 'zod'
// Allowlist everything you accept — the request body is untrusted input.
const bodySchema = z.object({
messages: z.array(z.custom<UIMessage>()),
model: z.enum(['claude-opus-4-8', 'claude-haiku-4-5']).default('claude-opus-4-8'),
command: z.enum(['summarize', 'translate']).optional(),
mentions: z.array(z.string().max(64)).max(10).default([]),
})
export async function POST(req: Request) {
const parsed = bodySchema.safeParse(await req.json())
if (!parsed.success) {
return Response.json({ error: parsed.error.issues }, { status: 400 })
}
const { messages, model, command, mentions } = parsed.data
const system = [
command
? `The user invoked the "${command}" command. Follow its conventions.`
: 'You are a helpful assistant.',
mentions.length ? `People referenced: ${mentions.join(', ')}.` : '',
]
.filter(Boolean)
.join(' ')
const result = streamText({
model: anthropic(model),
system,
messages: convertToModelMessages(messages),
})
return result.toUIMessageStreamResponse()
}body is untrusted input. Parse it with a Zod schema and safeParse on the server — allowlist model IDs and command values with z.enum() and bound array sizes rather than passing anything straight through to the model.Attachments
Prompt Area handles the attachment UI — pasted screenshots and picked files render as thumbnails with loading and remove states. The AI SDK accepts files on the same call: hand the underlying File objects to sendMessage({ text, files }). See Attachments for the image and file props.
Next steps
- Action Bar — the toolbar that hosts the send/stop button and any extra controls.
- DX Helpers —
usePromptAreaState(), trigger presets, andgetChipsByTrigger(). - Chat Prompt Layout — a scrollable message list with a pinned composer to wrap this example.
- Hooks & Helpers — full signatures for the helpers used here.