Next.js Chat Example
This example builds a support chat interface using Next.js App Router with HTTP/SSE transport. This is the recommended pattern for most web applications.
What You're Building
A chat interface that:
- Creates sessions server-side
- Streams AI responses in real-time
- Handles tool calls on your server
- Shows typing status during streaming
Architecture
Prerequisites
- Next.js 14+ with App Router
- Octavus account with API key
- An agent configured in Octavus
Step 1: Install Dependencies
npm install @octavus/server-sdk @octavus/reactStep 2: Configure Environment
# .env.local
OCTAVUS_API_URL=https://octavus.ai
OCTAVUS_API_KEY=your-api-keyStep 3: Create the Octavus Client
// lib/octavus.ts
import { OctavusClient } from '@octavus/server-sdk';
export const octavus = new OctavusClient({
baseUrl: process.env.OCTAVUS_API_URL!,
apiKey: process.env.OCTAVUS_API_KEY!,
});Step 4: Create Upload URLs Endpoint (Optional)
If your agent supports file uploads (images, documents), create an endpoint to get presigned URLs:
// app/api/upload-urls/route.ts
import { NextResponse } from 'next/server';
import { octavus } from '@/lib/octavus';
export async function POST(request: Request) {
const { sessionId, files } = await request.json();
// Get presigned URLs from Octavus
const result = await octavus.files.getUploadUrls(sessionId, files);
return NextResponse.json(result);
}Step 5: Create Session Endpoint
Sessions hold conversation state. Create one when the user opens the chat:
// app/api/sessions/route.ts
import { NextResponse } from 'next/server';
import { octavus } from '@/lib/octavus';
export async function POST(request: Request) {
const { agentId, input } = await request.json();
// Create a new session with initial input variables
const sessionId = await octavus.agentSessions.create(agentId, input);
return NextResponse.json({ sessionId });
}Protocol Note: The input object contains variables defined in your agent's protocol. For example, if your agent has COMPANY_NAME as an input variable:
const sessionId = await octavus.agentSessions.create(agentId, {
COMPANY_NAME: 'Acme Corp',
USER_ID: user.id,
});Step 6: Create Trigger Endpoint
Triggers execute agent actions. The user-message trigger is the most common:
// app/api/trigger/route.ts
import { toSSEStream } from '@octavus/server-sdk';
import { octavus } from '@/lib/octavus';
export async function POST(request: Request) {
const body = await request.json();
const { sessionId, ...payload } = body;
// Attach to the session with tool handlers
const session = octavus.agentSessions.attach(sessionId, {
tools: {
// Server-side tool handlers run on YOUR server, not Octavus
'get-user-account': async (args) => {
const userId = args.userId as string;
// Fetch from your database
const user = await db.users.findUnique({ where: { id: userId } });
return {
name: user.name,
email: user.email,
plan: user.plan,
};
},
'create-support-ticket': async (args) => {
const ticket = await db.tickets.create({
data: {
summary: args.summary as string,
priority: args.priority as string,
},
});
return {
ticketId: ticket.id,
estimatedResponse: '24 hours',
};
},
// Tools without handlers here are forwarded to the client
// See Client Tools docs for handling on frontend
},
});
// execute() handles both triggers and client tool continuations
const events = session.execute(payload, { signal: request.signal });
return new Response(toSSEStream(events), {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no',
},
});
}Protocol Note: Tool names and arguments are defined in your agent's protocol YAML. The tool handlers here must match those definitions. Tools without server handlers are forwarded to the client.
Step 7: Build the Chat Component
// components/Chat.tsx
'use client';
import { useState, useMemo } from 'react';
import { useOctavusChat, createHttpTransport } from '@octavus/react';
interface ChatProps {
sessionId: string;
}
export function Chat({ sessionId }: ChatProps) {
const [inputValue, setInputValue] = useState('');
// Create transport - memoized on sessionId
const transport = useMemo(
() =>
createHttpTransport({
request: (payload, options) =>
fetch('/api/trigger', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, ...payload }),
signal: options?.signal,
}),
}),
[sessionId],
);
const { messages, status, send } = useOctavusChat({ transport });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!inputValue.trim() || status === 'streaming') return;
const message = inputValue.trim();
setInputValue('');
// Send triggers the 'user-message' action
// The third argument adds the user message to the UI
await send('user-message', { USER_MESSAGE: message }, { userMessage: { content: message } });
};
return (
<div className="flex flex-col h-screen">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg) => (
<div key={msg.id} className={msg.role === 'user' ? 'text-right' : 'text-left'}>
<div
className={`inline-block p-3 rounded-lg ${
msg.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-100'
}`}
>
{msg.parts.map((part, i) => {
if (part.type === 'text') {
return <p key={i}>{part.text}</p>;
}
if (part.type === 'tool-call') {
return (
<div key={i} className="text-sm opacity-70">
Using {part.toolName}...
</div>
);
}
return null;
})}
</div>
</div>
))}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex gap-2">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Type a message..."
className="flex-1 px-4 py-2 border rounded-lg"
disabled={status === 'streaming'}
/>
<button
type="submit"
disabled={status === 'streaming'}
className="px-4 py-2 bg-blue-500 text-white rounded-lg"
>
{status === 'streaming' ? 'Sending...' : 'Send'}
</button>
</div>
</form>
</div>
);
}Step 8: Create the Page
// app/chat/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { Chat } from '@/components/Chat';
const AGENT_ID = 'your-agent-id'; // From Octavus dashboard
export default function ChatPage() {
const [sessionId, setSessionId] = useState<string | null>(null);
useEffect(() => {
// Create session on mount
fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: AGENT_ID,
input: {
COMPANY_NAME: 'Acme Corp',
},
}),
})
.then((res) => res.json())
.then((data) => setSessionId(data.sessionId));
}, []);
if (!sessionId) {
return <div className="p-8">Loading...</div>;
}
return <Chat sessionId={sessionId} />;
}Protocol Integration
Your agent's protocol defines the triggers and tools. Here's how the code maps to protocol:
Triggers
# In your agent's protocol.yaml
triggers:
user-message:
description: User sends a chat message
input:
USER_MESSAGE:
type: string
description: The user's messageThe send() call maps directly:
await send(
'user-message', // trigger name
{ USER_MESSAGE: message }, // trigger inputs
{ userMessage: { content: message } },
);Tools
# In your agent's protocol.yaml
tools:
get-user-account:
description: Fetch user account details
parameters:
userId:
type: string
description: The user ID to look upTool handlers receive the parameters as args:
'get-user-account': async (args) => {
const userId = args.userId as string;
// ...
}Next Steps
- Protocol Overview - Define agent behavior
- Messages - Rich message rendering
- Streaming - Advanced streaming UI