Sorry not compatible with mobile devices
Newton Raul
Vercel AI SDK
This section covers what the SDK gives you out of the box, and why it was the right choice for building an agentic system like Athena.
Creating your Next.js Application
Throughout this documentation we use the App Router the modern Next.js router built on React Server Components, Suspense, and Server Functions. It is the recommended way to build Next.js applications and is what Athena is built on. If you are familiar with the Pages Router, note that the App Router works differently and the two should not be mixed.
To create a new Next.js application run the following command:
pnpm create next-app@latest my-ai-app
During setup make sure to select Yes when prompted for App Router. The default setup also enables TypeScript and Tailwind CSS which is what we use throughout this project.
Navigate to the newly created directory:
cd my-ai-app
For a full walkthrough of the installation and project structure, refer to the Official Next.js installation guide.
Install Dependencies
Install the core packages needed to build AI-powered features in your Next.js app:
pnpm add ai @ai-sdk/react zod
Configure your AI Gateway API key
Create a .env.local file at the root of your project and add your AI provider
API key. In this documentation we use Google Gemini as our provider:
touch .env.local
Add your AI api key to your .env.local file.
You can get your Gemini API key from the Google AI Studio.
Make sure to add .env.local to your .gitignore so your API key is never committed to version control.
Create a Route Handler
Create a route handler, app/api/chat/route.ts and add the following code:
import { streamText, UIMessage, convertToModelMessages } from 'ai';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "google/gemini-3-pro-preview",
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}How this Works
This is a Next.js API route it's the server side of your AI chat. When your frontend sends a message, it hits this endpoint, the AI processes it, and streams the response back in real time.
Breaking it down line by line:
- Receiving the messages
const { messages }: { messages: UIMessage[] } = await req.json();When the user sends a message from the frontend, it arrives here as a list of all the messages in the conversation so far not just the latest one. This is how the AI knows the full context of the conversation.
- Converting the messages
messages: await convertToModelMessages(messages);The frontend sends messages in a UI friendly format. convertToModelMessages
transforms them into the format the AI model actually understands before
sending them over.
- Streaming the response
const result = streamText({ model: 'google/gemini-3-pro-preview', ...});Instead of waiting for the AI to finish generating the full response before
sending it back, streamText sends each word back to the frontend as soon
as it is generated just like you see in ChatGPT where the text appears
word by word.
- Sending it back
return result.toUIMessageStreamResponse(); This packages the stream into a format the frontend can read and display in real time.
So the full flow is:
- Frontend posts to this route
- AI generates a response word by word
- Frontend displays it as it arrives.
Setting up the UI
With the API route ready, the frontend side is straightforward. The Vercel AI
SDK gives you a useChat hook that handles everything sending messages,
receiving the streamed response, and managing the conversation history
Please install library Lucide React to continue with the UI setup:
pnpm add lucide-react
We have a pre built UI boilerplate to facilitate the learning process. Just paste this code into your /app/page.tsx
'use client'
import { useChat } from '@ai-sdk/react'
import { CopyIcon, ForwardIcon, Loader2Icon, RefreshCcw, StopCircleIcon } from 'lucide-react'
import { useState } from 'react'
export default function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, status, stop, regenerate } = useChat()
const isEmpty = messages.length === 0
return (
<div className='flex flex-col h-screen max-w-2xl mx-auto'>
<div className='flex-1 overflow-y-auto px-4 py-6'>
{isEmpty ? (
<div className='h-full flex flex-col items-center justify-center gap-2 text-center'>
<p className='text-2xl font-semibold'>What can I help with?</p>
<p className='text-sm text-zinc-500'>Ask me anything to get started.</p>
</div>
) : (
<div className='flex flex-col gap-4'>
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`group relative px-4 py-2.5 rounded-2xl text-sm leading-relaxed max-w-[80%] whitespace-pre-wrap ${
msg.role === 'user'
? 'bg-black text-white rounded-br-sm'
: 'bg-zinc-100 text-zinc-800 border border-zinc-200 rounded-bl-sm'
}`}
>
{msg.parts.map((part, i) =>
part.type === 'text' ? <span key={i}>{part.text}</span> : null
)}
<div className='absolute left-0 pt-1 bottom-0 translate-y-full flex items-center opacity-0 group-hover:opacity-100 transition-opacity duration-300'>
<button className='p-1 text-zinc-400 hover:text-zinc-600'>
<CopyIcon size={14} />
</button>
<button
onClick={() => regenerate({ messageId: msg.id })}
className='p-1 text-zinc-400 hover:text-zinc-600'
>
<RefreshCcw size={14} />
</button>
</div>
</div>
</div>
))}
{status === 'submitted' && (
<div className='flex justify-start'>
<div className='px-4 py-2.5 rounded-2xl rounded-bl-sm bg-zinc-100 border border-zinc-200 text-sm text-zinc-500 flex items-center gap-2'>
<Loader2Icon size={14} className='animate-spin' />
Thinking…
</div>
</div>
)}
</div>
)}
</div>
<form
onSubmit={(e) => {
e.preventDefault()
sendMessage({ text: input })
setInput('')
}}
className='border-t border-zinc-200 p-4'
>
<div className='border border-zinc-200 rounded-xl bg-white flex flex-col'>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder='Message Athena…'
rows={1}
className='w-full resize-none p-3 text-sm focus:outline-none min-h-11 max-h-40 leading-relaxed'
/>
<div className='flex justify-end p-2'>
<button
type='submit'
onClick={() => { if (status === 'streaming') stop() }}
disabled={!input.trim() || status === 'submitted' || status === 'streaming'}
className='bg-black text-white text-sm px-4 py-2 rounded-lg hover:bg-zinc-800 transition-colors disabled:opacity-40 flex items-center gap-2'
>
{status === 'submitted' ? (
<><ForwardIcon size={14} /> Sending...</>
) : status === 'streaming' ? (
<><StopCircleIcon size={14} /> Stop</>
) : (
<><ForwardIcon size={14} /> Send</>
)}
</button>
</div>
</div>
</form>
</div>
)
}With all this you should have a basic chatbot setup !!