React
The useAgentCall hook, for React and Next.js
useAgentCall gives a React component a call to your agent: its live state to render, and the controls to drive it. It works with React 18 and 19, Next.js (App and Pages Router), Remix and Vite.
npm install @voice213/agent-client"use client";
import { useAgentCall } from "@voice213/agent-client/react";
export function TalkToUs() {
const { status, isSpeaking, isMuted, transcript, partial, error, elapsedSeconds, start, end, toggleMute } = useAgentCall({
agentId: "k3x9m2p7q4wz",
});
if (status !== "connected") {
return (
<div>
<button onClick={start} disabled={status === "connecting"}>
{status === "connecting" ? "Connecting…" : status === "ended" ? "Call again" : "Talk to us"}
</button>
{error && <p role="alert">{error.message}</p>}
</div>
);
}
return (
<div>
<p>
{isSpeaking ? "Speaking…" : "Listening…"} · {Math.floor(elapsedSeconds / 60)}:{String(elapsedSeconds % 60).padStart(2, "0")}
</p>
<button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>
<button onClick={end}>Hang up</button>
<ul>
{transcript.map((m) => (
<li key={m.id} dir="auto">{m.role === "tool" ? `⚙ ${m.text}` : m.text}</li>
))}
{partial.agent && <li dir="auto">{partial.agent}…</li>}
</ul>
</div>
);
}What it returns
Everything in the call's state, plus the controls:
| Field | Type | |
|---|---|---|
status | "idle" | "connecting" | "connected" | "ended" | "error" | Where the call is |
isSpeaking, mode | boolean, "listening" | "speaking" | Whether the agent is talking |
isMuted | boolean | Whether the caller is muted |
transcript | TranscriptMessage[] | Completed turns and tool runs: { id, role, text, ok?, at } |
partial | { user, agent } | The words being spoken right now |
tool | string | null | The tool the agent is running |
error | Voice213Error | null | Why the call failed. See Errors. |
ended | CallEnded | null | The summary once it ended: { reason, durationSeconds, billedSeconds, conversationId } |
elapsedSeconds | number | Seconds since it connected, updated every second |
conversationId, maxSeconds, agent, startedAt | As on AgentCall | |
start() | () => Promise<void> | Starts the call. Call it from a click. It never throws: a failure lands in error. |
end() | () => Promise<CallEnded | null> | Hangs up |
setMuted(muted), toggleMute() | Mute the caller | |
sendText(text) | (text) => boolean | Sends a typed message |
call | AgentCall | The underlying client: levels and spectra for visualisers, device switching, events |
The options are those of AgentCall, callbacks included (onMessage, onEnd…). They can change from one render to the next, and the call always uses the latest. The call ends when the component unmounts.
Next.js
The hook uses the microphone and Web Audio, so the component that calls it must be a Client Component: put "use client" at the top of its file. The page around it can stay a Server Component:
import { TalkToUs } from "./TalkToUs"; // the "use client" component above
export default function ContactPage() {
return (
<main>
<h1>Talk to our team</h1>
<TalkToUs />
</main>
);
}On the server, the hook renders the idle state, so server rendering and hydration need no special care.
React to the conversation
Use callbacks to act on what happens, such as saving the transcript when the call ends or showing a toast when a tool runs:
const { start } = useAgentCall({
agentId: "k3x9m2p7q4wz",
onToolCall: ({ name, status }) => status === "running" && toast(`Checking ${name.replaceAll("_", " ")}…`),
onEnd: ({ reason, durationSeconds, conversationId }) => analytics.track("agent_call", { reason, durationSeconds, conversationId }),
onError: (error) => error.code === "MIC_DENIED" && setShowMicHelp(true),
});For the conversation's summary and outcome, use webhooks: they're written on the server after the call.
A visualiser
Levels change many times a second. Read them in an animation frame instead of React state, so the page doesn't re-render at audio rate:
"use client";
import { useEffect, useRef } from "react";
import { useAgentCall } from "@voice213/agent-client/react";
export function Orb() {
const { call, status, start, end } = useAgentCall({ agentId: "k3x9m2p7q4wz" });
const orb = useRef(null);
useEffect(() => {
if (status !== "connected") return;
let frame;
const draw = () => {
const level = call.isSpeaking ? call.getOutputVolume() : call.getInputVolume();
orb.current.style.transform = `scale(${1 + level * 0.6})`;
frame = requestAnimationFrame(draw);
};
frame = requestAnimationFrame(draw);
return () => cancelAnimationFrame(frame);
}, [call, status]);
return (
<button onClick={status === "connected" ? end : start} aria-label={status === "connected" ? "Hang up" : "Talk to us"}>
<span ref={orb} style={{ display: "block", width: 96, height: 96, borderRadius: "50%", background: "linear-gradient(140deg, #FF9A62, #F0436E)", transition: "transform 80ms" }} />
</button>
);
}For a spectrum, use call.getOutputFrequencyData() (128 values, 0–255) the same way.
Typed messages
sendText lets a visitor type when they can't talk:
function TypeInstead() {
const { status, sendText } = useAgentCall({ agentId: "k3x9m2p7q4wz" });
return (
<form onSubmit={(e) => { e.preventDefault(); if (sendText(e.currentTarget.message.value)) e.currentTarget.reset(); }}>
<input name="message" placeholder="Type a message" disabled={status !== "connected"} />
</form>
);
}The agent answers it by voice, like speech, and it appears in transcript as the caller's turn.
Was this page helpful?