The useVoiceAgentControls hook provides methods to control the ElevenLabs voice agent, including starting/ending sessions, sending system updates, and monitoring audio levels.
Hook signature
function useVoiceAgentControls(): VoiceAgentControls
Return value
The hook returns an object with control methods:
sendSystemUpdate
(message: string) => void
Send a system update message to the voice agent that will be announced
Start a voice agent session with ElevenLabs
End the current voice agent session
Get the current microphone input volume level (0-1)
Get the current speaker output volume level (0-1)
Usage examples
Starting and ending sessions
import { useVoiceAgentControls } from '@/components/builder/voice/voice-agent';
import { useState } from 'react';
function VoiceControl() {
const voiceAgent = useVoiceAgentControls();
const [isConnected, setIsConnected] = useState(false);
const handleConnect = async () => {
try {
await voiceAgent.startSession();
setIsConnected(true);
} catch (error) {
console.error('Failed to start session:', error);
}
};
const handleDisconnect = () => {
voiceAgent.endSession();
setIsConnected(false);
};
return (
<div>
{isConnected ? (
<button onClick={handleDisconnect}>
Disconnect
</button>
) : (
<button onClick={handleConnect}>
Connect
</button>
)}
</div>
);
}
Sending system updates
import { useVoiceAgentControls } from '@/components/builder/voice/voice-agent';
import { useEffect } from 'react';
function GenerationNotifier({ isGenerating }: { isGenerating: boolean }) {
const voiceAgent = useVoiceAgentControls();
useEffect(() => {
if (isGenerating) {
voiceAgent.sendSystemUpdate('Alright, let me build that for you');
}
}, [isGenerating, voiceAgent]);
return null;
}
Monitoring audio levels
import { useVoiceAgentControls } from '@/components/builder/voice/voice-agent';
import { useEffect, useState } from 'react';
function AudioVisualizer() {
const voiceAgent = useVoiceAgentControls();
const [inputLevel, setInputLevel] = useState(0);
const [outputLevel, setOutputLevel] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setInputLevel(voiceAgent.getInputVolume());
setOutputLevel(voiceAgent.getOutputVolume());
}, 100);
return () => clearInterval(interval);
}, [voiceAgent]);
return (
<div className="audio-levels">
<div className="level">
<span>Input</span>
<div className="bar" style={{ width: `${inputLevel * 100}%` }} />
</div>
<div className="level">
<span>Output</span>
<div className="bar" style={{ width: `${outputLevel * 100}%` }} />
</div>
</div>
);
}
import { useVoiceAgentControls } from '@/components/builder/voice/voice-agent';
import { useCallback, useState } from 'react';
function VoiceSidebar() {
const voiceControls = useVoiceAgentControls();
const [status, setStatus] = useState<'disconnected' | 'connecting' | 'connected'>('disconnected');
const handleConnect = useCallback(() => {
voiceControls.startSession();
}, [voiceControls]);
const handleDisconnect = useCallback(() => {
voiceControls.endSession();
}, [voiceControls]);
const isConnected = status === 'connected';
const isConnecting = status === 'connecting';
return (
<aside className="voice-sidebar">
<div className="controls">
{isConnected ? (
<button onClick={handleDisconnect}>
End Session
</button>
) : (
<button
onClick={handleConnect}
disabled={isConnecting}
>
{isConnecting ? 'Connecting...' : 'Start Voice'}
</button>
)}
</div>
<div className="status">
{isConnected && <p>Voice agent is listening</p>}
</div>
</aside>
);
}
Builder page integration
import { useVoiceAgentControls } from '@/components/builder/voice/voice-agent';
import { useSandbox } from '@/lib/hooks/use-sandbox';
import { useEffect, useRef } from 'react';
function BuilderPage() {
const sandbox = useSandbox();
const voiceAgent = useVoiceAgentControls();
const prevSandboxReady = useRef(false);
// Auto-start voice session when sandbox is ready
useEffect(() => {
if (sandbox.isReady && !prevSandboxReady.current) {
setTimeout(() => {
voiceAgent.startSession().catch((err) => {
console.warn('Failed to auto-start voice session:', err);
});
}, 800);
}
prevSandboxReady.current = sandbox.isReady;
}, [sandbox.isReady, voiceAgent]);
return (
<div className="builder">
{/* Your builder UI */}
</div>
);
}
Announcing file generation progress
import { useVoiceAgentControls } from '@/components/builder/voice/voice-agent';
import { useGeneration } from '@/lib/hooks/use-generation';
import { useEffect, useRef } from 'react';
function GenerationAnnouncer() {
const generation = useGeneration();
const voiceAgent = useVoiceAgentControls();
const announcedCountRef = useRef(0);
useEffect(() => {
const MAX_ANNOUNCEMENTS = 4;
const totalCompleted = generation.streamingFiles.length;
if (
generation.isGenerating &&
totalCompleted > 0 &&
announcedCountRef.current < MAX_ANNOUNCEMENTS
) {
const shouldAnnounce =
(announcedCountRef.current === 0 && totalCompleted >= 1) ||
(announcedCountRef.current === 1 && totalCompleted >= 3) ||
(announcedCountRef.current === 2 && totalCompleted >= 5) ||
(announcedCountRef.current === 3 && totalCompleted >= 7);
if (shouldAnnounce) {
voiceAgent.sendSystemUpdate(
`I've created ${totalCompleted} file${totalCompleted === 1 ? '' : 's'} so far`
);
announcedCountRef.current++;
}
}
}, [generation.streamingFiles.length, generation.isGenerating, voiceAgent]);
return null;
}
System updates sent via sendSystemUpdate() are prefixed with [UPDATE] internally. The voice agent’s prompt is configured to recognize this pattern and announce the message verbatim.
// When you call:
voiceAgent.sendSystemUpdate('Generation complete');
// The agent receives:
// "[UPDATE] Generation complete"
// And announces: "Generation complete"
Global state pattern
The hook uses a global singleton pattern to maintain voice agent state across component renders. This allows:
- Multiple components to control the same voice session
- Consistent state even if the hook is called from different components
- Reliable cleanup when components unmount
// Global reference (internal)
let globalVoiceAgent: {
sendSystemUpdate: (message: string) => void;
startSession: () => Promise<void>;
endSession: () => void;
getInputVolume: () => number;
getOutputVolume: () => number;
} | null = null;
VoiceAgent component
The VoiceAgent component must be rendered to initialize the voice agent. It handles:
- ElevenLabs conversation setup
- Client tool registration
- Connection status management
- Message handling
import { VoiceAgent } from '@/components/builder/voice/voice-agent';
function App() {
return (
<>
<VoiceAgent
onStatusChange={(status) => console.log('Status:', status)}
onMessage={(message) => console.log('Message:', message)}
onNavigate={(panel) => ({ success: true, message: 'Navigated' })}
onGenerate={async (options) => { /* ... */ }}
sandboxId={sandboxId}
isReady={isReady}
/>
{/* Your app */}
</>
);
}
Session lifecycle
- Disconnected - No active session
- Connecting -
startSession() called, connecting to ElevenLabs
- Connected - WebSocket established, agent listening
- Disconnected -
endSession() called or connection lost
Error handling
The hook gracefully handles errors:
- Returns default values (0) when agent not connected
- Logs warnings when sending updates to disconnected agent
- Catches session start failures without throwing
const voiceAgent = useVoiceAgentControls();
// Safe to call even if not connected
voiceAgent.sendSystemUpdate('Hello'); // Logs warning if disconnected
const volume = voiceAgent.getInputVolume(); // Returns 0 if not connected
Volume monitoring
Volume levels are normalized to 0-1 range:
0 = Silence
0.5 = Medium volume
1.0 = Maximum volume
Use polling to continuously monitor levels:
setInterval(() => {
const input = voiceAgent.getInputVolume();
const output = voiceAgent.getOutputVolume();
updateVisualizer(input, output);
}, 100); // Update every 100ms