From 68ba9f53dc6519bfd54874ee0d434cb4fbbd7bf7 Mon Sep 17 00:00:00 2001 From: Tadas Petra <60107328+tadaspetra@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:23:09 -0500 Subject: [PATCH 1/4] Update example to use latest react sdk --- agents/nextjs/guardrails/PROMPT.md | 9 +- agents/nextjs/guardrails/example/app/page.tsx | 314 ++++++++----- agents/nextjs/guardrails/example/package.json | 4 +- agents/nextjs/quickstart/PROMPT.md | 8 +- agents/nextjs/quickstart/example/app/page.tsx | 423 ++++++++++-------- agents/nextjs/quickstart/example/package.json | 6 +- 6 files changed, 458 insertions(+), 306 deletions(-) diff --git a/agents/nextjs/guardrails/PROMPT.md b/agents/nextjs/guardrails/PROMPT.md index 8ebb199d..f9ede581 100644 --- a/agents/nextjs/guardrails/PROMPT.md +++ b/agents/nextjs/guardrails/PROMPT.md @@ -2,7 +2,7 @@ Before writing any code, invoke the `/agents` skill to learn the correct ElevenL ## 1. `package.json` -- Add `@elevenlabs/react` (with `onGuardrailTriggered` support) and `elevenlabs` SDK dependencies. +- Add `@elevenlabs/react` (with `onGuardrailTriggered` support) and `@elevenlabs/elevenlabs-js` dependencies. ## 2. `app/api/agent/route.ts` @@ -26,9 +26,12 @@ Never expose `ELEVENLABS_API_KEY` to the client. Minimal Next.js voice guardrails demo page. -- Use `@elevenlabs/react` and `useConversation` with `onGuardrailTriggered`. +- Use `ConversationProvider` from `@elevenlabs/react`. +- Use the granular conversation hooks `useConversationControls`, `useConversationStatus`, and `useConversationMode`. +- Register `onGuardrailTriggered` on the provider. - Show a `Create Agent` button and an editable agent-id input. Auto-populate on create; allow pasting a different id to load it instead. -- Start WebRTC sessions with a fresh token from `/api/conversation-token`. Request mic access before starting. +- Start voice sessions with a fresh token from `/api/conversation-token`. Request mic access before starting. +- Rely on the SDK's connection-type inference when starting the session; do not hardcode `connectionType: "webrtc"` unless there is a specific reason. - Show a Start/Stop toggle, connection status, and running conversation transcript (append messages, don't replace). - Surface example prompts for testing the guardrail (e.g., asking about investments or Bitcoin). - If the guardrail triggers, show a persistent status message and append a note to the transcript. diff --git a/agents/nextjs/guardrails/example/app/page.tsx b/agents/nextjs/guardrails/example/app/page.tsx index f4f4376e..2aa637a3 100644 --- a/agents/nextjs/guardrails/example/app/page.tsx +++ b/agents/nextjs/guardrails/example/app/page.tsx @@ -1,6 +1,11 @@ "use client"; -import { useConversation } from "@elevenlabs/react"; +import { + ConversationProvider, + useConversationControls, + useConversationMode, + useConversationStatus, +} from "@elevenlabs/react"; import { useCallback, useEffect, useState } from "react"; type TranscriptRole = "user" | "agent" | "system"; @@ -18,132 +23,81 @@ type ConversationMessage = { event_id?: number; }; -export default function Home() { - const [agentIdInput, setAgentIdInput] = useState(""); - const [lookupStatus, setLookupStatus] = useState< - "idle" | "loading" | "ok" | "error" - >("idle"); - const [lookupError, setLookupError] = useState(null); - - const [createError, setCreateError] = useState(null); - const [creating, setCreating] = useState(false); - - const [transcript, setTranscript] = useState([]); - const [guardrailFired, setGuardrailFired] = useState(false); - const [sessionError, setSessionError] = useState(null); - - const onGuardrailTriggered = useCallback(() => { - setGuardrailFired(true); - setTranscript(prev => [ - ...prev, - { - id: `guardrail-${Date.now()}`, - role: "system", - text: "Guardrail triggered — session ended by policy.", - }, - ]); - }, []); - - const conversation = useConversation({ - onConnect: () => { - setSessionError(null); - }, - onDisconnect: () => { - setSessionError(null); - }, - onError: (error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - setSessionError(message); - }, - onGuardrailTriggered: () => { - onGuardrailTriggered(); - }, - onMessage: (props: ConversationMessage) => { - const { message, source, event_id: eventId } = props; - const role: TranscriptRole = source === "user" ? "user" : "agent"; - setTranscript(prev => { - if (eventId !== undefined) { - const idx = prev.findIndex( - l => l.eventId === eventId && l.role === role - ); - if (idx >= 0) { - const next = [...prev]; - next[idx] = { ...next[idx], text: message }; - return next; - } - } - return [ - ...prev, - { - id: - eventId !== undefined - ? `${role}-${eventId}` - : `${role}-${crypto.randomUUID()}`, - role, - text: message, - eventId, - }, - ]; - }); - }, - }); - - useEffect(() => { - const id = agentIdInput.trim(); - if (!id) { - setLookupStatus("idle"); - setLookupError(null); - return; - } - - setLookupStatus("loading"); - const handle = setTimeout(async () => { - try { - const res = await fetch(`/api/agent?agentId=${encodeURIComponent(id)}`); - const data: { agentId?: string; error?: string } = await res.json(); - if (!res.ok) { - setLookupStatus("error"); - setLookupError(data.error ?? "Could not load agent."); - return; - } - setLookupStatus("ok"); - setLookupError(null); - } catch { - setLookupStatus("error"); - setLookupError("Network error while loading agent."); - } - }, 450); +type GuardrailsPageProps = { + agentIdInput: string; + createError: string | null; + creating: boolean; + guardrailFired: boolean; + lookupError: string | null; + lookupStatus: "idle" | "loading" | "ok" | "error"; + sessionError: string | null; + setAgentIdInput: (value: string) => void; + setCreateError: (value: string | null) => void; + setCreating: (value: boolean) => void; + setGuardrailFired: (value: boolean) => void; + setLookupError: (value: string | null) => void; + setLookupStatus: (value: "idle" | "loading" | "ok" | "error") => void; + setSessionError: (value: string | null) => void; + setTranscript: (value: TranscriptLine[]) => void; + transcript: TranscriptLine[]; +}; - return () => clearTimeout(handle); - }, [agentIdInput]); +function GuardrailsPage({ + agentIdInput, + createError, + creating, + guardrailFired, + lookupError, + lookupStatus, + sessionError, + setAgentIdInput, + setCreateError, + setCreating, + setGuardrailFired, + setLookupError, + setLookupStatus, + setSessionError, + setTranscript, + transcript, +}: GuardrailsPageProps) { + const { startSession, endSession } = useConversationControls(); + const { isSpeaking } = useConversationMode(); + const { status, message } = useConversationStatus(); const trimmedId = agentIdInput.trim(); const canStart = trimmedId.length > 0 && lookupStatus !== "loading" && lookupStatus !== "error"; + const sessionLive = status === "connected" || status === "connecting"; - let statusLabel = "Disconnected"; - if (conversation.status === "connected") { - statusLabel = conversation.isSpeaking ? "Speaking" : "Listening"; - } else if (conversation.status === "connecting") { - statusLabel = "Connecting…"; - } else if (conversation.status === "disconnecting") { - statusLabel = "Disconnecting…"; - } + const statusLabel = + status === "connected" + ? isSpeaking + ? "Speaking" + : "Listening" + : status === "connecting" + ? "Connecting…" + : status === "error" + ? message ?? "Connection error" + : "Disconnected"; - const sessionLive = - conversation.status === "connected" || conversation.status === "connecting"; + function handleAgentIdChange(value: string) { + setAgentIdInput(value); + setLookupError(null); + setLookupStatus(value.trim() ? "loading" : "idle"); + } const startOrStop = async () => { setSessionError(null); if (sessionLive) { - await conversation.endSession(); + endSession(); return; } - const id = agentIdInput.trim(); - if (!id || !canStart) return; + if (!trimmedId || !canStart) { + return; + } setGuardrailFired(false); setTranscript([]); @@ -156,7 +110,7 @@ export default function Home() { try { const res = await fetch( - `/api/conversation-token?agentId=${encodeURIComponent(id)}` + `/api/conversation-token?agentId=${encodeURIComponent(trimmedId)}` ); const data: { token?: string; error?: string } = await res.json(); if (!res.ok || !data.token) { @@ -164,13 +118,13 @@ export default function Home() { return; } - await conversation.startSession({ - connectionType: "webrtc", + await startSession({ conversationToken: data.token, }); - } catch (e) { - const msg = e instanceof Error ? e.message : "Failed to start session."; - setSessionError(msg); + } catch (error) { + const nextMessage = + error instanceof Error ? error.message : "Failed to start session."; + setSessionError(nextMessage); } }; @@ -185,6 +139,8 @@ export default function Home() { return; } setAgentIdInput(data.agentId); + setLookupError(null); + setLookupStatus("ok"); } catch { setCreateError("Network error while creating agent."); } finally { @@ -200,8 +156,8 @@ export default function Home() { Voice agent guardrails

- WebRTC voice session with platform guardrails and a banking-style - custom investment-advice policy. + Voice session with platform guardrails and a banking-style custom + investment-advice policy.

@@ -224,7 +180,7 @@ export default function Home() { className="rounded-md border border-neutral-200 px-3 py-2 text-sm" placeholder="Paste or create an agent id" value={agentIdInput} - onChange={e => setAgentIdInput(e.target.value)} + onChange={event => handleAgentIdChange(event.target.value)} /> @@ -311,3 +267,121 @@ export default function Home() { ); } + +export default function Home() { + const [agentIdInput, setAgentIdInput] = useState(""); + const [lookupStatus, setLookupStatus] = useState< + "idle" | "loading" | "ok" | "error" + >("idle"); + const [lookupError, setLookupError] = useState(null); + + const [createError, setCreateError] = useState(null); + const [creating, setCreating] = useState(false); + + const [transcript, setTranscript] = useState([]); + const [guardrailFired, setGuardrailFired] = useState(false); + const [sessionError, setSessionError] = useState(null); + + useEffect(() => { + const id = agentIdInput.trim(); + if (!id) { + return; + } + const handle = setTimeout(async () => { + try { + const res = await fetch(`/api/agent?agentId=${encodeURIComponent(id)}`); + const data: { agentId?: string; error?: string } = await res.json(); + if (!res.ok) { + setLookupStatus("error"); + setLookupError(data.error ?? "Could not load agent."); + return; + } + setLookupStatus("ok"); + setLookupError(null); + } catch { + setLookupStatus("error"); + setLookupError("Network error while loading agent."); + } + }, 450); + + return () => clearTimeout(handle); + }, [agentIdInput]); + + const handleGuardrailTriggered = useCallback(() => { + setGuardrailFired(true); + setTranscript(prev => [ + ...prev, + { + id: `guardrail-${Date.now()}`, + role: "system", + text: "Guardrail triggered - session ended by policy.", + }, + ]); + }, []); + + const handleMessage = useCallback((props: ConversationMessage) => { + const { message, source, event_id: eventId } = props; + const role: TranscriptRole = source === "user" ? "user" : "agent"; + setTranscript(prev => { + if (eventId !== undefined) { + const idx = prev.findIndex( + line => line.eventId === eventId && line.role === role + ); + if (idx >= 0) { + const next = [...prev]; + next[idx] = { ...next[idx], text: message }; + return next; + } + } + return [ + ...prev, + { + id: + eventId !== undefined + ? `${role}-${eventId}` + : `${role}-${crypto.randomUUID()}`, + role, + text: message, + eventId, + }, + ]; + }); + }, []); + + return ( + { + setSessionError(null); + }} + onDisconnect={() => { + setSessionError(null); + }} + onError={(error: unknown) => { + const nextMessage = + error instanceof Error ? error.message : String(error); + setSessionError(nextMessage); + }} + onGuardrailTriggered={handleGuardrailTriggered} + onMessage={handleMessage} + > + + + ); +} diff --git a/agents/nextjs/guardrails/example/package.json b/agents/nextjs/guardrails/example/package.json index 9c271a9c..f41082a9 100644 --- a/agents/nextjs/guardrails/example/package.json +++ b/agents/nextjs/guardrails/example/package.json @@ -9,8 +9,8 @@ "lint": "eslint" }, "dependencies": { - "@elevenlabs/elevenlabs-js": "^2.40.0", - "@elevenlabs/react": "^0.15.0", + "@elevenlabs/elevenlabs-js": "^2.43.0", + "@elevenlabs/react": "^1.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.575.0", diff --git a/agents/nextjs/quickstart/PROMPT.md b/agents/nextjs/quickstart/PROMPT.md index f21fb689..e65f3481 100644 --- a/agents/nextjs/quickstart/PROMPT.md +++ b/agents/nextjs/quickstart/PROMPT.md @@ -2,7 +2,7 @@ Before writing any code, invoke the `/agents` skill to learn the correct ElevenL ## 1. `package.json` -- Add `@elevenlabs/react` and `elevenlabs` SDK dependencies. +- Add `@elevenlabs/react` and `@elevenlabs/elevenlabs-js` dependencies. ## 2. `app/api/agent/route.ts` @@ -24,8 +24,10 @@ Never expose `ELEVENLABS_API_KEY` to the client. Minimal Next.js voice agent page. -- Use `@elevenlabs/react` and the `useConversation` hook. +- Use `ConversationProvider` from `@elevenlabs/react`. +- Use the granular conversation hooks `useConversationControls` and `useConversationStatus`. - Show a `Create Agent` button and an editable agent-id input. Auto-populate on create; allow pasting a different id to load it instead. -- Start WebRTC sessions with a fresh token from `/api/conversation-token`. Request mic access before starting. +- Start voice sessions with a fresh token from `/api/conversation-token`. Request mic access before starting. +- Rely on the SDK's connection-type inference when starting the session; do not hardcode `connectionType: "webrtc"` unless there is a specific reason. - Show a Start/Stop toggle, connection status, and running conversation transcript (append messages, don't replace). - Handle errors gracefully and allow reconnect. Keep the UI simple and voice-first. diff --git a/agents/nextjs/quickstart/example/app/page.tsx b/agents/nextjs/quickstart/example/app/page.tsx index ad3eaa03..79f2aa70 100644 --- a/agents/nextjs/quickstart/example/app/page.tsx +++ b/agents/nextjs/quickstart/example/app/page.tsx @@ -1,7 +1,17 @@ "use client"; -import { useConversation } from "@elevenlabs/react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + ConversationProvider, + useConversationControls, + useConversationStatus, +} from "@elevenlabs/react"; +import { + type MutableRefObject, + useCallback, + useEffect, + useRef, + useState, +} from "react"; type TranscriptLine = { id: string; @@ -10,6 +20,26 @@ type TranscriptLine = { tentative: boolean; }; +type VoiceAgentPageProps = { + agentIdInput: string; + agentLookupError: string | null; + agentLookupOk: boolean; + createError: string | null; + creating: boolean; + lines: TranscriptLine[]; + sessionError: string | null; + setAgentIdInput: (value: string) => void; + setAgentLookupError: (value: string | null) => void; + setAgentLookupOk: (value: boolean) => void; + setCreateError: (value: string | null) => void; + setCreating: (value: boolean) => void; + setLines: (value: TranscriptLine[]) => void; + setSessionError: (value: string | null) => void; + setStarting: (value: boolean) => void; + starting: boolean; + nextLineId: MutableRefObject; +}; + type ConversationMessage = { source: "user" | "ai"; message: unknown; @@ -51,161 +81,46 @@ function isConversationMessage(value: unknown): value is ConversationMessage { return extractMessageText(value.message) !== null; } -export default function Home() { - const [agentIdInput, setAgentIdInput] = useState(""); - const [agentLookupError, setAgentLookupError] = useState(null); - const [agentLookupOk, setAgentLookupOk] = useState(false); - const [createError, setCreateError] = useState(null); - const [creating, setCreating] = useState(false); - const [sessionError, setSessionError] = useState(null); - const [starting, setStarting] = useState(false); - const [lines, setLines] = useState([]); - - const lookupTimer = useRef | null>(null); - const nextLineId = useRef(0); - - const onMessage = useCallback((event: unknown) => { - if (!isConversationMessage(event)) { - return; - } - - const text = extractMessageText(event.message)?.trim(); - if (!text) { - return; - } - - setLines(prev => { - const role = event.source === "ai" ? "agent" : "user"; - const last = prev[prev.length - 1]; - - if (last?.role === role && last.tentative) { - const copy = [...prev]; - copy[copy.length - 1] = { ...last, text, tentative: false }; - return copy; - } - - // The React SDK emits transcript-level messages, so append turns directly. - if (last && last.role === role && last.text === text) { - return prev; - } - - nextLineId.current += 1; - return [ - ...prev, - { - id: `line-${nextLineId.current}`, - role, - text, - tentative: false, - }, - ]; - }); - }, []); - - const onDebug = useCallback((event: unknown) => { - if ( - !isRecord(event) || - event.type !== "internal_tentative_agent_response" - ) { - return; - } - - const payload = event.tentative_agent_response_internal_event; - if (!isRecord(payload)) { - return; - } - - const text = - typeof payload.tentative_agent_response === "string" - ? payload.tentative_agent_response.trim() - : ""; - - if (!text) { - return; - } - - setLines(prev => { - const last = prev[prev.length - 1]; - if (last?.role === "agent" && last.tentative) { - const copy = [...prev]; - copy[copy.length - 1] = { ...last, text }; - return copy; - } - - nextLineId.current += 1; - return [ - ...prev, - { - id: `line-${nextLineId.current}`, - role: "agent", - text, - tentative: true, - }, - ]; - }); - }, []); - - const conversation = useConversation({ - onMessage, - onDebug, - onError: (e: unknown) => { - setSessionError(e instanceof Error ? e.message : String(e)); - }, - onDisconnect: () => { - setStarting(false); - }, - }); +function VoiceAgentPage({ + agentIdInput, + agentLookupError, + agentLookupOk, + createError, + creating, + lines, + nextLineId, + sessionError, + setAgentIdInput, + setAgentLookupError, + setAgentLookupOk, + setCreateError, + setCreating, + setLines, + setSessionError, + setStarting, + starting, +}: VoiceAgentPageProps) { + const { startSession, endSession } = useConversationControls(); + const { status, message } = useConversationStatus(); const trimmedId = agentIdInput.trim(); const canStart = trimmedId.length > 0 && !starting; - - useEffect(() => { - if (!trimmedId) { - setAgentLookupOk(false); - setAgentLookupError(null); - return; - } - - if (lookupTimer.current) clearTimeout(lookupTimer.current); - lookupTimer.current = setTimeout(async () => { - setAgentLookupError(null); - setAgentLookupOk(false); - try { - const res = await fetch( - `/api/agent?agentId=${encodeURIComponent(trimmedId)}` - ); - const data = await res.json(); - if (!res.ok) { - setAgentLookupError( - typeof data.error === "string" ? data.error : "Agent lookup failed" - ); - return; - } - setAgentLookupOk(true); - } catch { - setAgentLookupError("Network error while loading agent."); - } - }, 450); - - return () => { - if (lookupTimer.current) clearTimeout(lookupTimer.current); - }; - }, [trimmedId]); - - const statusLabel = useMemo(() => { - switch (conversation.status) { - case "connected": - return "Connected"; - case "connecting": - return "Connecting…"; - case "disconnecting": - return "Disconnecting…"; - case "disconnected": - return "Disconnected"; - default: - return conversation.status; - } - }, [conversation.status]); + const sessionActive = status === "connected" || status === "connecting"; + + const statusLabel = + status === "connected" + ? "Connected" + : status === "connecting" + ? "Connecting…" + : status === "error" + ? message ?? "Connection error" + : "Disconnected"; + + function handleAgentIdChange(value: string) { + setAgentIdInput(value); + setAgentLookupError(null); + setAgentLookupOk(false); + } async function handleCreateAgent() { setCreateError(null); @@ -221,8 +136,8 @@ export default function Home() { } const id = data.agentId as string; setAgentIdInput(id); - setAgentLookupOk(true); setAgentLookupError(null); + setAgentLookupOk(true); } catch { setCreateError("Network error while creating agent."); } finally { @@ -233,18 +148,15 @@ export default function Home() { async function handleToggleSession() { setSessionError(null); - if ( - conversation.status === "connected" || - conversation.status === "connecting" || - conversation.status === "disconnecting" - ) { - await conversation.endSession(); + if (sessionActive) { + endSession(); setStarting(false); return; } - const id = agentIdInput.trim(); - if (!id) return; + if (!trimmedId) { + return; + } setStarting(true); nextLineId.current = 0; @@ -260,7 +172,7 @@ export default function Home() { try { const res = await fetch( - `/api/conversation-token?agentId=${encodeURIComponent(id)}` + `/api/conversation-token?agentId=${encodeURIComponent(trimmedId)}` ); const data = await res.json(); if (!res.ok) { @@ -273,22 +185,16 @@ export default function Home() { return; } const token = data.token as string; - await conversation.startSession({ + await startSession({ conversationToken: token, - connectionType: "webrtc", }); - } catch (e) { - setSessionError(e instanceof Error ? e.message : String(e)); + } catch (error) { + setSessionError(error instanceof Error ? error.message : String(error)); } finally { setStarting(false); } } - const sessionActive = - conversation.status === "connected" || - conversation.status === "connecting" || - conversation.status === "disconnecting"; - return (
@@ -297,7 +203,7 @@ export default function Home() { Voice agent

- Talk in real time with an ElevenLabs conversational agent (WebRTC). + Talk in real time with an ElevenLabs conversational agent.

@@ -312,7 +218,7 @@ export default function Home() { className="w-full rounded-md border border-neutral-200 px-3 py-2 text-sm outline-none focus:border-neutral-400" placeholder="Paste or create an agent id" value={agentIdInput} - onChange={e => setAgentIdInput(e.target.value)} + onChange={event => handleAgentIdChange(event.target.value)} /> {agentLookupError ? (

{agentLookupError}

@@ -392,3 +298,170 @@ export default function Home() {
); } + +export default function Home() { + const [agentIdInput, setAgentIdInput] = useState(""); + const [agentLookupError, setAgentLookupError] = useState(null); + const [agentLookupOk, setAgentLookupOk] = useState(false); + const [createError, setCreateError] = useState(null); + const [creating, setCreating] = useState(false); + const [sessionError, setSessionError] = useState(null); + const [starting, setStarting] = useState(false); + const [lines, setLines] = useState([]); + + const lookupTimer = useRef | null>(null); + const nextLineId = useRef(0); + + const trimmedId = agentIdInput.trim(); + + useEffect(() => { + if (lookupTimer.current) { + clearTimeout(lookupTimer.current); + } + + if (!trimmedId) { + return; + } + + lookupTimer.current = setTimeout(async () => { + setAgentLookupError(null); + setAgentLookupOk(false); + try { + const res = await fetch( + `/api/agent?agentId=${encodeURIComponent(trimmedId)}` + ); + const data = await res.json(); + if (!res.ok) { + setAgentLookupError( + typeof data.error === "string" ? data.error : "Agent lookup failed" + ); + return; + } + setAgentLookupOk(true); + } catch { + setAgentLookupError("Network error while loading agent."); + } + }, 450); + + return () => { + if (lookupTimer.current) { + clearTimeout(lookupTimer.current); + } + }; + }, [trimmedId]); + + const handleMessage = useCallback((event: unknown) => { + if (!isConversationMessage(event)) { + return; + } + + const text = extractMessageText(event.message)?.trim(); + if (!text) { + return; + } + + setLines(prev => { + const role = event.source === "ai" ? "agent" : "user"; + const last = prev[prev.length - 1]; + + if (last?.role === role && last.tentative) { + const copy = [...prev]; + copy[copy.length - 1] = { ...last, text, tentative: false }; + return copy; + } + + if (last && last.role === role && last.text === text) { + return prev; + } + + nextLineId.current += 1; + return [ + ...prev, + { + id: `line-${nextLineId.current}`, + role, + text, + tentative: false, + }, + ]; + }); + }, []); + + const handleDebug = useCallback((event: unknown) => { + if ( + !isRecord(event) || + event.type !== "internal_tentative_agent_response" + ) { + return; + } + + const payload = event.tentative_agent_response_internal_event; + if (!isRecord(payload)) { + return; + } + + const text = + typeof payload.tentative_agent_response === "string" + ? payload.tentative_agent_response.trim() + : ""; + + if (!text) { + return; + } + + setLines(prev => { + const last = prev[prev.length - 1]; + if (last?.role === "agent" && last.tentative) { + const copy = [...prev]; + copy[copy.length - 1] = { ...last, text }; + return copy; + } + + nextLineId.current += 1; + return [ + ...prev, + { + id: `line-${nextLineId.current}`, + role: "agent", + text, + tentative: true, + }, + ]; + }); + }, []); + + return ( + setSessionError(null)} + onDebug={handleDebug} + onDisconnect={() => { + setSessionError(null); + setStarting(false); + }} + onError={(error: unknown) => { + setSessionError(error instanceof Error ? error.message : String(error)); + }} + onMessage={handleMessage} + > + + + ); +} diff --git a/agents/nextjs/quickstart/example/package.json b/agents/nextjs/quickstart/example/package.json index 78c61922..64c42744 100644 --- a/agents/nextjs/quickstart/example/package.json +++ b/agents/nextjs/quickstart/example/package.json @@ -1,5 +1,5 @@ { - "name": "realtime-transcription", + "name": "agents-quickstart-demo", "version": "0.1.0", "private": true, "scripts": { @@ -17,8 +17,8 @@ "react": "19.2.3", "react-dom": "19.2.3", "tailwind-merge": "^3.5.0", - "@elevenlabs/react": "^0.14.3", - "@elevenlabs/elevenlabs-js": "^2.40.0" + "@elevenlabs/react": "^1.1.0", + "@elevenlabs/elevenlabs-js": "^2.43.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", From 394e083d19cafcf044b45083208aa454071edeb6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 13 Apr 2026 19:36:18 +0000 Subject: [PATCH 2/4] Fix quickstart agent lookup state reset --- agents/nextjs/quickstart/example/app/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/agents/nextjs/quickstart/example/app/page.tsx b/agents/nextjs/quickstart/example/app/page.tsx index 79f2aa70..d05a7287 100644 --- a/agents/nextjs/quickstart/example/app/page.tsx +++ b/agents/nextjs/quickstart/example/app/page.tsx @@ -118,8 +118,6 @@ function VoiceAgentPage({ function handleAgentIdChange(value: string) { setAgentIdInput(value); - setAgentLookupError(null); - setAgentLookupOk(false); } async function handleCreateAgent() { From aaf796484c9eb9cb41596bba900509d56dbe4a7f Mon Sep 17 00:00:00 2001 From: Tadas Petra <60107328+tadaspetra@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:40:20 -0500 Subject: [PATCH 3/4] lint --- agents/nextjs/guardrails/example/app/page.tsx | 2 +- agents/nextjs/quickstart/example/app/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agents/nextjs/guardrails/example/app/page.tsx b/agents/nextjs/guardrails/example/app/page.tsx index 2aa637a3..179f71ad 100644 --- a/agents/nextjs/guardrails/example/app/page.tsx +++ b/agents/nextjs/guardrails/example/app/page.tsx @@ -79,7 +79,7 @@ function GuardrailsPage({ : status === "connecting" ? "Connecting…" : status === "error" - ? message ?? "Connection error" + ? (message ?? "Connection error") : "Disconnected"; function handleAgentIdChange(value: string) { diff --git a/agents/nextjs/quickstart/example/app/page.tsx b/agents/nextjs/quickstart/example/app/page.tsx index d05a7287..abef8cd7 100644 --- a/agents/nextjs/quickstart/example/app/page.tsx +++ b/agents/nextjs/quickstart/example/app/page.tsx @@ -113,7 +113,7 @@ function VoiceAgentPage({ : status === "connecting" ? "Connecting…" : status === "error" - ? message ?? "Connection error" + ? (message ?? "Connection error") : "Disconnected"; function handleAgentIdChange(value: string) { From 9b58b7d5981bfaddf35b7c7bda3a66af407f7009 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 13 Apr 2026 19:48:04 +0000 Subject: [PATCH 4/4] Fix cleared agent lookup state --- agents/nextjs/quickstart/example/app/page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agents/nextjs/quickstart/example/app/page.tsx b/agents/nextjs/quickstart/example/app/page.tsx index abef8cd7..163b5ea4 100644 --- a/agents/nextjs/quickstart/example/app/page.tsx +++ b/agents/nextjs/quickstart/example/app/page.tsx @@ -118,6 +118,10 @@ function VoiceAgentPage({ function handleAgentIdChange(value: string) { setAgentIdInput(value); + if (!value.trim()) { + setAgentLookupOk(false); + setAgentLookupError(null); + } } async function handleCreateAgent() {