Skip to content

Commit fe899a8

Browse files
amethystaniclaude
andcommitted
fix: switch voice AI from broken WebSocket path to ElevenLabs LiveKit WebRTC
Remove the custom signed-URL Netlify webhook and pass agentId directly to useConversation.startSession(). This triggers the SDK's modern LiveKit WebRTC path instead of the legacy WebSocket path to api.elevenlabs.io. The WebSocket path (signedUrl) was causing repeated 'WebSocket is already in CLOSING or CLOSED state' errors from onInputWorkletMessage — the audio worklet kept firing after the server closed the socket without the client realising it. With agentId, ElevenLabs SDK fetches its own LiveKit JWT from /v1/convai/conversation/token and connects via WebRTC — no custom server function or signed-URL proxy needed. Requirement: VITE_ELEVENLABS_AGENT_ID must be set in Netlify env vars (VITE_ prefix so Vite bundles it into the frontend build). The agent must have public access enabled in the ElevenLabs dashboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 57cefac commit fe899a8

File tree

1 file changed

+17
-22
lines changed

1 file changed

+17
-22
lines changed

src/components/DemoCall/UserPhoneInterface.tsx

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { Phone, PhoneOff, Volume2, VolumeX, Minimize2, Maximize2, ArrowLeft } fr
44
import { useConversation } from '@elevenlabs/react';
55
import { cn } from '../../utils/cn';
66
import { useDemoCall } from '../../contexts/DemoCallContext';
7-
import { fetchElevenLabsSignedUrl } from '../../services/elevenlabsSignedUrl';
7+
// ElevenLabs agent ID — safe to expose in the frontend (it's not a secret).
8+
// Set VITE_ELEVENLABS_AGENT_ID in Netlify env vars (with the VITE_ prefix so
9+
// Vite bundles it). The SDK fetches its own LiveKit token from ElevenLabs
10+
// using this ID — no custom signed-URL webhook needed.
11+
const ELEVENLABS_AGENT_ID = import.meta.env.VITE_ELEVENLABS_AGENT_ID as string | undefined;
812

913
interface UserPhoneInterfaceProps {
1014
isDark?: boolean;
@@ -116,45 +120,36 @@ export default function UserPhoneInterface({
116120

117121
const isActive = currentCall?.status === 'active';
118122

119-
// Fetch signed URL from our Netlify function (keeps API key server-side)
120-
const getSignedUrl = useCallback(async (): Promise<string> => {
121-
return fetchElevenLabsSignedUrl();
122-
}, []);
123-
124123
const handleStartCall = useCallback(async () => {
125124
setConnectionError(null);
126125
setIsEnding(false);
127126
startCall('voice');
128127
setAgentStatus('processing');
129128

129+
if (!ELEVENLABS_AGENT_ID) {
130+
setConnectionError('ElevenLabs agent ID not configured. Set VITE_ELEVENLABS_AGENT_ID in Netlify env vars.');
131+
setAgentStatus('idle');
132+
return;
133+
}
134+
130135
try {
131-
// Get signed URL from our server (keeps API key safe)
132-
const signedUrl = await getSignedUrl();
133-
134-
// Only override firstMessage (greeting) — do NOT override the full
135-
// agent prompt. ElevenLabs injects its own tool-call and KB-search
136-
// instructions into the agent system prompt at the platform level;
137-
// replacing prompt.prompt strips those hidden instructions and causes
138-
// the agent to silently fail to respond (timeout disconnect).
136+
// Use agentId directly — ElevenLabs SDK fetches its own LiveKit token
137+
// and connects via WebRTC (the modern stable path). No custom signed-URL
138+
// webhook needed; the WebSocket path was causing disconnect issues.
139139
const overrides = knowledgeBase.greeting
140-
? {
141-
agent: {
142-
// SDK reads firstMessage (camelCase), not first_message
143-
firstMessage: knowledgeBase.greeting,
144-
},
145-
}
140+
? { agent: { firstMessage: knowledgeBase.greeting } }
146141
: undefined;
147142

148143
await conversation.startSession({
149-
signedUrl,
144+
agentId: ELEVENLABS_AGENT_ID,
150145
...(overrides ? { overrides } : {}),
151146
});
152147
} catch (error) {
153148
console.error('📞 Failed to start ElevenLabs session:', error);
154149
setConnectionError(error instanceof Error ? error.message : 'Failed to connect');
155150
setAgentStatus('idle');
156151
}
157-
}, [startCall, getSignedUrl, conversation, knowledgeBase]);
152+
}, [startCall, conversation, knowledgeBase]);
158153

159154
const handleEndCall = useCallback(async () => {
160155
console.log('🔴 End call button pressed');

0 commit comments

Comments
 (0)