--- title: Building a basic NextJS app with the Anam SDK tags: [javascript, nextjs, beginner] date: 2025-01-17 authors: [ao-anam] --- The Anam JavaScript SDK lets you add conversational AI personas to web apps. By default, it captures audio from the user's microphone and streams back video of a talking avatar. In this cookbook we'll build a simple Next.js app that does exactly this. The complete code is at [examples/basic-nextjs](https://github.com/anam-org/anam-cookbook/tree/main/examples/basic-nextjs). ## What you'll build A single-page Next.js app with a video player. Users click to start, then talk into their microphone. The persona responds with voice and video. ## Prerequisites - Node.js 18+ - An Anam account ([sign up at lab.anam.ai](https://lab.anam.ai)) - Your API key from the Anam Lab dashboard ## Project setup Create a Next.js app and install the SDK: ```bash pnpm create next-app@latest my-anam-app cd my-anam-app pnpm add @anam-ai/js-sdk ``` Create an `.env.local` file for your API key: ```bash ANAM_API_KEY=your_api_key_here ``` Never expose your API key in client-side code. We'll generate session tokens from a server-side API route. ## Persona configuration The persona config defines how your avatar looks and behaves. Put this in `src/config/persona.ts`: ```typescript // src/config/persona.ts export const personaConfig = { // Avatar - the visual character avatarId: "edf6fdcb-acab-44b8-b974-ded72665ee26", // Voice - how the persona sounds voiceId: "6bfbe25a-979d-40f3-a92b-5394170af54b", // LLM - the AI model powering conversations llmId: "0934d97d-0c3a-4f33-91b0-5e136a0ef466", // System prompt - defines personality and behavior systemPrompt: `You are a friendly AI assistant. Keep your responses concise and conversational.`, }; ``` You can browse avatars and voices at [lab.anam.ai](https://lab.anam.ai) and swap in different IDs. ## Session token API route The Anam client needs a session token. Generate it server-side so your API key stays secret: ```typescript // src/app/api/session-token/route.ts import { NextResponse } from "next/server"; import { personaConfig } from "@/config/persona"; export async function POST() { const apiKey = process.env.ANAM_API_KEY; if (!apiKey) { return NextResponse.json( { error: "ANAM_API_KEY is not configured" }, { status: 500 } ); } try { const response = await fetch("https://api.anam.ai/v1/auth/session-token", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ personaConfig }), }); if (!response.ok) { const error = await response.text(); console.error("Anam API error:", error); return NextResponse.json( { error: "Failed to get session token" }, { status: response.status } ); } const data = await response.json(); return NextResponse.json({ sessionToken: data.sessionToken }); } catch (error) { console.error("Error fetching session token:", error); return NextResponse.json( { error: "Failed to get session token" }, { status: 500 } ); } } ``` ## Building the persona player component This component handles the video stream and connection lifecycle. We'll build it in pieces. Start with imports: ```typescript // src/components/PersonaPlayer.tsx "use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { createClient, AnamEvent, ConnectionClosedCode, } from "@anam-ai/js-sdk"; import type { AnamClient, Message } from "@anam-ai/js-sdk"; type ConnectionState = "idle" | "connecting" | "connected" | "error"; ``` ### Fetching a session token A wrapper to get tokens from our API route: ```typescript async function fetchSessionToken(): Promise { const response = await fetch("/api/session-token", { method: "POST" }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || "Failed to get session token"); } const { sessionToken } = await response.json(); return sessionToken; } ``` ### Setting up event listeners The SDK emits events for connection changes and new messages: - `AnamEvent.CONNECTION_ESTABLISHED` - connected and ready - `AnamEvent.MESSAGE_HISTORY_UPDATED` - new message in the conversation - `AnamEvent.CONNECTION_CLOSED` - connection ended, with a reason code ```typescript function setupEventListeners( client: AnamClient, handlers: { onConnected: () => void; onDisconnected: () => void; onError: (message: string) => void; onMessagesUpdated: (messages: Message[]) => void; } ) { client.addListener(AnamEvent.CONNECTION_ESTABLISHED, handlers.onConnected); client.addListener(AnamEvent.MESSAGE_HISTORY_UPDATED, handlers.onMessagesUpdated); client.addListener(AnamEvent.CONNECTION_CLOSED, (reason, details) => { if (reason !== ConnectionClosedCode.NORMAL) { handlers.onError(details || `Connection closed: ${reason}`); } else { handlers.onDisconnected(); } }); } ``` ### The component and session management The component tracks connection state, errors, and messages. A ref holds the client instance for cleanup: ```typescript export function PersonaPlayer() { const [connectionState, setConnectionState] = useState("idle"); const [error, setError] = useState(null); const [messages, setMessages] = useState([]); const clientRef = useRef(null); const startSession = useCallback(async () => { setConnectionState("connecting"); setError(null); try { const sessionToken = await fetchSessionToken(); const client = createClient(sessionToken); clientRef.current = client; setupEventListeners(client, { onConnected: () => setConnectionState("connected"), onDisconnected: () => setConnectionState("idle"), onError: (message) => { setError(message); setConnectionState("error"); }, onMessagesUpdated: setMessages, }); await client.streamToVideoElement("avatar-video"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to start session"); setConnectionState("error"); } }, []); const stopSession = useCallback(() => { if (clientRef.current) { clientRef.current.stopStreaming(); clientRef.current = null; } setConnectionState("idle"); setMessages([]); }, []); // Clean up on unmount useEffect(() => { return () => { if (clientRef.current) { clientRef.current.stopStreaming(); } }; }, []); ``` `startSession` fetches a token, creates the client, sets up listeners, and calls `streamToVideoElement()` to start streaming. That handles all the WebRTC setup. Once it resolves, the persona greets the user and the microphone is active. ### Rendering the video player The video element ID must match what we passed to `streamToVideoElement()`. The 3:2 aspect ratio matches the 720x480 stream: ```typescript return (
); } ``` Add UI controls for each connection state: ```typescript {connectionState === "idle" && (
)} {connectionState === "connecting" && (
Connecting...
)} {connectionState === "error" && (
{error}
)} {connectionState === "connected" && ( )} ``` Add a conversation history panel showing transcribed speech and responses: ```typescript {connectionState === "connected" && (
{messages.length === 0 ? (

Start speaking to have a conversation...

) : ( messages.map((msg) => (
{msg.role === "user" ? "You" : "Persona"}: {" "} {msg.content}
)) )}
)} ``` ## Adding the component to the page Import and render the component: ```typescript // src/app/page.tsx import { PersonaPlayer } from "@/components/PersonaPlayer"; export default function Home() { return (

Anam Persona Demo

Click to start a conversation. Speak using your microphone.

); } ``` ## Running the app ```bash pnpm dev ``` Open [http://localhost:3000](http://localhost:3000), click "Start conversation", and talk to your persona. Try editing `src/config/persona.ts` to change the system prompt. Swap in different avatar and voice IDs from [Anam Lab](https://lab.anam.ai) to see how they affect the experience.