Skip to content

Update examples to use latest react sdk#225

Merged
tadaspetra merged 4 commits intomainfrom
react-1
Apr 13, 2026
Merged

Update examples to use latest react sdk#225
tadaspetra merged 4 commits intomainfrom
react-1

Conversation

@tadaspetra
Copy link
Copy Markdown
Collaborator

@tadaspetra tadaspetra commented Apr 13, 2026

Note

Low Risk
Low risk: changes are confined to example Next.js pages and prompt docs, plus dependency bumps to newer ElevenLabs SDK versions. Main risk is minor behavior/regression in the demos due to the major @elevenlabs/react API migration and reliance on inferred connection type.

Overview
Migrates the quickstart and guardrails Next.js example pages from useConversation to the new ConversationProvider + granular hooks (useConversationControls, useConversationStatus, and useConversationMode for guardrails), moving event handlers (onMessage, onDebug, onGuardrailTriggered) onto the provider.

Session startup is adjusted to rely on SDK connection-type inference (removes hardcoded connectionType: "webrtc"), with small UI/state tweaks around agent-id lookup and status messaging. Docs/prompts are updated accordingly, and both examples bump @elevenlabs/react to ^1.1.0 and @elevenlabs/elevenlabs-js to ^2.43.0 (also renames quickstart package).

Reviewed by Cursor Bugbot for commit 9b58b7d. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Agent lookup state reset without matching effect re-run
    • This was a real bug, and I fixed it by letting the lookup effect exclusively manage lookup state so whitespace-only input changes no longer clear a valid result permanently.
Preview (394e083d19)
diff --git a/agents/nextjs/guardrails/PROMPT.md b/agents/nextjs/guardrails/PROMPT.md
--- 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`
 

@@ -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.

@@ -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
--- 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";

@@ -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<string | null>(null);
-
-  const [createError, setCreateError] = useState<string | null>(null);
-  const [creating, setCreating] = useState(false);
-
-  const [transcript, setTranscript] = useState<TranscriptLine[]>([]);
-  const [guardrailFired, setGuardrailFired] = useState(false);
-  const [sessionError, setSessionError] = useState<string | null>(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([]);

@@ -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<string | null>(null);
-
-  const [createError, setCreateError] = useState<string | null>(null);
-  const [creating, setCreating] = useState(false);
-
-  const [transcript, setTranscript] = useState<TranscriptLine[]>([]);
-  const [guardrailFired, setGuardrailFired] = useState(false);
-  const [sessionError, setSessionError] = useState<string | null>(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,21 +110,21 @@ 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) {
         setSessionError(data.error ?? "Could not get conversation token.");
         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);
     }
   };
 

@@ -156,21 +110,21 @@ 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) {
         setSessionError(data.error ?? "Could not get conversation token.");
         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 {

@@ -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
           </h1>
           <p className="text-sm text-neutral-500">
-            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.
           </p>
         </header>
 

@@ -200,8 +156,8 @@ export default function Home() {
             Voice agent guardrails
           </h1>
           <p className="text-sm text-neutral-500">
-            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.
           </p>
         </header>
 
@@ -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)}
               />
             </div>
           </div>

@@ -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)}
               />
             </div>
           </div>
@@ -311,3 +267,121 @@ export default function Home() {
     </main>
   );
 }
+
+export default function Home() {
+  const [agentIdInput, setAgentIdInput] = useState("");
+  const [lookupStatus, setLookupStatus] = useState<
+    "idle" | "loading" | "ok" | "error"
+  >("idle");
+  const [lookupError, setLookupError] = useState<string | null>(null);
+
+  const [createError, setCreateError] = useState<string | null>(null);
+  const [creating, setCreating] = useState(false);
+
+  const [transcript, setTranscript] = useState<TranscriptLine[]>([]);
+  const [guardrailFired, setGuardrailFired] = useState(false);
+  const [sessionError, setSessionError] = useState<string | null>(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 (
+    <ConversationProvider
+      onConnect={() => {
+        setSessionError(null);
+      }}
+      onDisconnect={() => {
+        setSessionError(null);
+      }}
+      onError={(error: unknown) => {
+        const nextMessage =
+          error instanceof Error ? error.message : String(error);
+        setSessionError(nextMessage);
+      }}
+      onGuardrailTriggered={handleGuardrailTriggered}
+      onMessage={handleMessage}
+    >
+      <GuardrailsPage
+        agentIdInput={agentIdInput}
+        createError={createError}
+        creating={creating}
+        guardrailFired={guardrailFired}
+        lookupError={lookupError}
+        lookupStatus={lookupStatus}
+        sessionError={sessionError}
+        setAgentIdInput={setAgentIdInput}
+        setCreateError={setCreateError}
+        setCreating={setCreating}
+        setGuardrailFired={setGuardrailFired}
+        setLookupError={setLookupError}
+        setLookupStatus={setLookupStatus}
+        setSessionError={setSessionError}
+        setTranscript={setTranscript}
+        transcript={transcript}
+      />
+    </ConversationProvider>
+  );
+}

@@ -311,3 +267,121 @@ export default function Home() {
     </main>
   );
 }
+
+export default function Home() {
+  const [agentIdInput, setAgentIdInput] = useState("");
+  const [lookupStatus, setLookupStatus] = useState<
+    "idle" | "loading" | "ok" | "error"
+  >("idle");
+  const [lookupError, setLookupError] = useState<string | null>(null);
+
+  const [createError, setCreateError] = useState<string | null>(null);
+  const [creating, setCreating] = useState(false);
+
+  const [transcript, setTranscript] = useState<TranscriptLine[]>([]);
+  const [guardrailFired, setGuardrailFired] = useState(false);
+  const [sessionError, setSessionError] = useState<string | null>(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 (
+    <ConversationProvider
+      onConnect={() => {
+        setSessionError(null);
+      }}
... diff truncated: showing 800 of 1969 lines

You can send follow-ups to the cloud agent here.

Comment thread agents/nextjs/quickstart/example/app/page.tsx
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale lookup error persists after clearing agent input
    • Clearing the agent input now immediately resets the lookup success and error state so stale lookup messages disappear when the field is emptied.
Preview (9b58b7d598)
diff --git a/agents/nextjs/guardrails/PROMPT.md b/agents/nextjs/guardrails/PROMPT.md
--- a/agents/nextjs/guardrails/PROMPT.md
+++ b/agents/nextjs/guardrails/PROMPT.md
@@ -2,7 +2,7 @@
 
 ## 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 @@
 
 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
--- 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 @@
   event_id?: number;
 };
 
-export default function Home() {
-  const [agentIdInput, setAgentIdInput] = useState("");
-  const [lookupStatus, setLookupStatus] = useState<
-    "idle" | "loading" | "ok" | "error"
-  >("idle");
-  const [lookupError, setLookupError] = useState<string | null>(null);
+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[];
+};
 
-  const [createError, setCreateError] = useState<string | null>(null);
-  const [creating, setCreating] = useState(false);
+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 [transcript, setTranscript] = useState<TranscriptLine[]>([]);
-  const [guardrailFired, setGuardrailFired] = useState(false);
-  const [sessionError, setSessionError] = useState<string | null>(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);
-
-    return () => clearTimeout(handle);
-  }, [agentIdInput]);
-
   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";
+
+  function handleAgentIdChange(value: string) {
+    setAgentIdInput(value);
+    setLookupError(null);
+    setLookupStatus(value.trim() ? "loading" : "idle");
   }
 
-  const sessionLive =
-    conversation.status === "connected" || conversation.status === "connecting";
-
   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 @@
 
     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 @@
         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 @@
         return;
       }
       setAgentIdInput(data.agentId);
+      setLookupError(null);
+      setLookupStatus("ok");
     } catch {
       setCreateError("Network error while creating agent.");
     } finally {
@@ -200,8 +156,8 @@
             Voice agent guardrails
           </h1>
           <p className="text-sm text-neutral-500">
-            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.
           </p>
         </header>
 
@@ -224,7 +180,7 @@
                 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)}
               />
             </div>
           </div>
@@ -311,3 +267,121 @@
     </main>
   );
 }
+
+export default function Home() {
+  const [agentIdInput, setAgentIdInput] = useState("");
+  const [lookupStatus, setLookupStatus] = useState<
+    "idle" | "loading" | "ok" | "error"
+  >("idle");
+  const [lookupError, setLookupError] = useState<string | null>(null);
+
+  const [createError, setCreateError] = useState<string | null>(null);
+  const [creating, setCreating] = useState(false);
+
+  const [transcript, setTranscript] = useState<TranscriptLine[]>([]);
+  const [guardrailFired, setGuardrailFired] = useState(false);
+  const [sessionError, setSessionError] = useState<string | null>(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 (
+    <ConversationProvider
+      onConnect={() => {
+        setSessionError(null);
+      }}
+      onDisconnect={() => {
+        setSessionError(null);
+      }}
+      onError={(error: unknown) => {
+        const nextMessage =
+          error instanceof Error ? error.message : String(error);
+        setSessionError(nextMessage);
+      }}
+      onGuardrailTriggered={handleGuardrailTriggered}
+      onMessage={handleMessage}
+    >
+      <GuardrailsPage
+        agentIdInput={agentIdInput}
+        createError={createError}
+        creating={creating}
+        guardrailFired={guardrailFired}
+        lookupError={lookupError}
+        lookupStatus={lookupStatus}
+        sessionError={sessionError}
+        setAgentIdInput={setAgentIdInput}
+        setCreateError={setCreateError}
+        setCreating={setCreating}
+        setGuardrailFired={setGuardrailFired}
+        setLookupError={setLookupError}
+        setLookupStatus={setLookupStatus}
+        setSessionError={setSessionError}
+        setTranscript={setTranscript}
+        transcript={transcript}
+      />
+    </ConversationProvider>
+  );
+}

diff --git a/agents/nextjs/guardrails/example/package.json b/agents/nextjs/guardrails/example/package.json
--- 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
--- a/agents/nextjs/quickstart/PROMPT.md
+++ b/agents/nextjs/quickstart/PROMPT.md
@@ -2,7 +2,7 @@
 
 ## 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 @@
 
 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
--- 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 @@
   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<number>;
+};
+
 type ConversationMessage = {
   source: "user" | "ai";
   message: unknown;
@@ -51,162 +81,49 @@
   return extractMessageText(value.message) !== null;
 }
 
-export default function Home() {
-  const [agentIdInput, setAgentIdInput] = useState("");
-  const [agentLookupError, setAgentLookupError] = useState<string | null>(null);
-  const [agentLookupOk, setAgentLookupOk] = useState(false);
-  const [createError, setCreateError] = useState<string | null>(null);
-  const [creating, setCreating] = useState(false);
-  const [sessionError, setSessionError] = useState<string | null>(null);
-  const [starting, setStarting] = useState(false);
-  const [lines, setLines] = useState<TranscriptLine[]>([]);
+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 lookupTimer = useRef<ReturnType<typeof setTimeout> | 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);
-    },
-  });
-
   const trimmedId = agentIdInput.trim();
   const canStart = trimmedId.length > 0 && !starting;
+  const sessionActive = status === "connected" || status === "connecting";
 
-  useEffect(() => {
-    if (!trimmedId) {
+  const statusLabel =
+    status === "connected"
+      ? "Connected"
+      : status === "connecting"
+        ? "Connecting…"
+        : status === "error"
+          ? (message ?? "Connection error")
+          : "Disconnected";
+
+  function handleAgentIdChange(value: string) {
+    setAgentIdInput(value);
+    if (!value.trim()) {
       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]);
-
   async function handleCreateAgent() {
     setCreateError(null);
     setCreating(true);
@@ -221,8 +138,8 @@
       }
       const id = data.agentId as string;
       setAgentIdInput(id);
+      setAgentLookupError(null);
       setAgentLookupOk(true);
-      setAgentLookupError(null);
     } catch {
       setCreateError("Network error while creating agent.");
     } finally {
@@ -233,18 +150,15 @@
   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 +174,7 @@
 
     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 +187,16 @@
         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 (
     <main className="min-h-screen bg-white text-neutral-900">
       <div className="mx-auto w-full max-w-2xl px-6 py-12 sm:py-16">
@@ -297,7 +205,7 @@
             Voice agent
           </h1>
           <p className="text-sm text-neutral-500">
-            Talk in real time with an ElevenLabs conversational agent (WebRTC).
+            Talk in real time with an ElevenLabs conversational agent.
           </p>
         </header>
 
@@ -312,7 +220,7 @@
                 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 ? (
                 <p className="text-xs text-red-600">{agentLookupError}</p>
@@ -392,3 +300,170 @@
     </main>
   );
 }
+
+export default function Home() {
+  const [agentIdInput, setAgentIdInput] = useState("");
+  const [agentLookupError, setAgentLookupError] = useState<string | null>(null);
... diff truncated: showing 800 of 985 lines

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit aaf7964. Configure here.

Comment thread agents/nextjs/quickstart/example/app/page.tsx
@tadaspetra tadaspetra merged commit 3b30449 into main Apr 13, 2026
3 checks passed
@tadaspetra tadaspetra deleted the react-1 branch April 13, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants