Skip to content

Commit d321db8

Browse files
committed
refactor: voice reconnect with session storage
1 parent dddf82c commit d321db8

File tree

7 files changed

+125
-104
lines changed

7 files changed

+125
-104
lines changed

package-lock.json

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
],
6969
"license": "",
7070
"peerDependencies": {
71-
"@vapi-ai/web": "^2.3.7",
71+
"@vapi-ai/web": "^2.5.0",
7272
"react": ">=16.8.0",
7373
"react-dom": ">=16.8.0"
7474
},

src/hooks/useVapiCall.ts

Lines changed: 33 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
import { useState, useEffect, useRef, useCallback } from 'react';
22
import Vapi from '@vapi-ai/web';
3-
4-
interface StoredCallData {
5-
webCallUrl: string;
6-
id?: string;
7-
artifactPlan?: {
8-
videoRecordingEnabled?: boolean;
9-
};
10-
assistant?: {
11-
voice?: {
12-
provider?: string;
13-
};
14-
};
15-
timestamp: number;
16-
}
3+
import * as vapiCallStorage from '../utils/vapiCallStorage';
174

185
export interface VapiCallState {
196
isCallActive: boolean;
@@ -37,7 +24,7 @@ export interface UseVapiCallOptions {
3724
callOptions: any;
3825
apiUrl?: string;
3926
enabled?: boolean;
40-
roomDeleteOnUserLeaveEnabled?: boolean;
27+
voiceAutoReconnect?: boolean;
4128
reconnectStorageKey?: string;
4229
onCallStart?: () => void;
4330
onCallEnd?: () => void;
@@ -55,7 +42,7 @@ export const useVapiCall = ({
5542
callOptions,
5643
apiUrl,
5744
enabled = true,
58-
roomDeleteOnUserLeaveEnabled = false,
45+
voiceAutoReconnect = false,
5946
reconnectStorageKey = 'vapi_widget_web_call',
6047
onCallStart,
6148
onCallEnd,
@@ -83,54 +70,6 @@ export const useVapiCall = ({
8370
onTranscript,
8471
});
8572

86-
// localStorage utilities
87-
const storeCallData = useCallback(
88-
(call: any) => {
89-
if (!roomDeleteOnUserLeaveEnabled) {
90-
// Extract webCallUrl from the call object
91-
// The webCallUrl can be in call.webCallUrl or call.transport.callUrl
92-
const webCallUrl =
93-
(call as any).webCallUrl || (call.transport as any)?.callUrl;
94-
95-
if (webCallUrl) {
96-
const webCallToStore = {
97-
webCallUrl,
98-
id: call.id,
99-
artifactPlan: call.artifactPlan,
100-
assistant: call.assistant,
101-
timestamp: Date.now(),
102-
};
103-
localStorage.setItem(
104-
reconnectStorageKey,
105-
JSON.stringify(webCallToStore)
106-
);
107-
console.log('Stored call data for reconnection:', webCallToStore);
108-
} else {
109-
console.warn(
110-
'No webCallUrl found in call object, cannot store for reconnection'
111-
);
112-
}
113-
}
114-
},
115-
[roomDeleteOnUserLeaveEnabled, reconnectStorageKey]
116-
);
117-
118-
const getStoredCallData = useCallback((): StoredCallData | null => {
119-
try {
120-
const stored = localStorage.getItem(reconnectStorageKey);
121-
if (!stored) return null;
122-
123-
return JSON.parse(stored);
124-
} catch {
125-
localStorage.removeItem(reconnectStorageKey);
126-
return null;
127-
}
128-
}, [reconnectStorageKey]);
129-
130-
const clearStoredCall = useCallback(() => {
131-
localStorage.removeItem(reconnectStorageKey);
132-
}, [reconnectStorageKey]);
133-
13473
useEffect(() => {
13574
callbacksRef.current = {
13675
onCallStart,
@@ -159,7 +98,7 @@ export const useVapiCall = ({
15998
setIsSpeaking(false);
16099
setIsMuted(false);
161100
// Clear stored call data on successful call end
162-
clearStoredCall();
101+
vapiCallStorage.clearStoredCall(reconnectStorageKey);
163102
callbacksRef.current.onCallEnd?.();
164103
};
165104

@@ -214,7 +153,7 @@ export const useVapiCall = ({
214153
vapi.removeListener('message', handleMessage);
215154
vapi.removeListener('error', handleError);
216155
};
217-
}, [vapi, clearStoredCall]);
156+
}, [vapi, reconnectStorageKey]);
218157

219158
useEffect(() => {
220159
return () => {
@@ -233,7 +172,7 @@ export const useVapiCall = ({
233172
try {
234173
console.log('Starting call with configuration:', callOptions);
235174
console.log('Starting call with options:', {
236-
roomDeleteOnUserLeaveEnabled,
175+
voiceAutoReconnect,
237176
});
238177
setConnectionStatus('connecting');
239178
const call = await vapi.start(
@@ -249,20 +188,20 @@ export const useVapiCall = ({
249188
undefined,
250189
// options
251190
{
252-
roomDeleteOnUserLeaveEnabled,
191+
roomDeleteOnUserLeaveEnabled: !voiceAutoReconnect,
253192
}
254193
);
255194

256195
// Store call data for reconnection if call was successful and auto-reconnect is enabled
257-
if (call && !roomDeleteOnUserLeaveEnabled) {
258-
storeCallData(call);
196+
if (call && voiceAutoReconnect) {
197+
vapiCallStorage.storeCallData(reconnectStorageKey, call, callOptions);
259198
}
260199
} catch (error) {
261200
console.error('Error starting call:', error);
262201
setConnectionStatus('disconnected');
263202
callbacksRef.current.onError?.(error as Error);
264203
}
265-
}, [vapi, callOptions, enabled, roomDeleteOnUserLeaveEnabled, storeCallData]);
204+
}, [vapi, callOptions, enabled, voiceAutoReconnect, reconnectStorageKey]);
266205

267206
const endCall = useCallback(
268207
async ({ force = false }: { force?: boolean } = {}) => {
@@ -311,13 +250,26 @@ export const useVapiCall = ({
311250
return;
312251
}
313252

314-
const storedData = getStoredCallData();
253+
const storedData = vapiCallStorage.getStoredCallData(reconnectStorageKey);
254+
315255
if (!storedData) {
316256
console.warn('No stored call data found for reconnection');
317257
return;
318258
}
319259

260+
// Check if callOptions match before reconnecting
261+
if (
262+
!vapiCallStorage.areCallOptionsEqual(storedData.callOptions, callOptions)
263+
) {
264+
console.warn(
265+
'CallOptions have changed since last call, clearing stored data and skipping reconnection'
266+
);
267+
vapiCallStorage.clearStoredCall(reconnectStorageKey);
268+
return;
269+
}
270+
320271
setConnectionStatus('connecting');
272+
321273
try {
322274
await vapi.reconnect({
323275
webCallUrl: storedData.webCallUrl,
@@ -329,28 +281,21 @@ export const useVapiCall = ({
329281
} catch (error) {
330282
setConnectionStatus('disconnected');
331283
console.error('Reconnection failed:', error);
332-
clearStoredCall();
284+
vapiCallStorage.clearStoredCall(reconnectStorageKey);
333285
callbacksRef.current.onError?.(error as Error);
334286
}
335-
}, [vapi, enabled, getStoredCallData, clearStoredCall]);
287+
}, [vapi, enabled, reconnectStorageKey, callOptions]);
288+
289+
const clearStoredCall = useCallback(() => {
290+
vapiCallStorage.clearStoredCall(reconnectStorageKey);
291+
}, [reconnectStorageKey]);
336292

337-
// Check for stored call data on initialization and attempt reconnection
338293
useEffect(() => {
339-
if (!vapi || !enabled || roomDeleteOnUserLeaveEnabled) {
294+
if (!vapi || !enabled || !voiceAutoReconnect) {
340295
return;
341296
}
342-
343-
const storedData = getStoredCallData();
344-
if (storedData) {
345-
reconnect();
346-
}
347-
}, [
348-
vapi,
349-
enabled,
350-
roomDeleteOnUserLeaveEnabled,
351-
getStoredCallData,
352-
reconnect,
353-
]);
297+
reconnect();
298+
}, [vapi, enabled, voiceAutoReconnect, reconnect, reconnectStorageKey]);
354299

355300
return {
356301
// State

src/hooks/useVapiWidget.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const useVapiWidget = ({
6767
callOptions: buildCallOptions(),
6868
apiUrl,
6969
enabled: voiceEnabled,
70-
roomDeleteOnUserLeaveEnabled: voiceAutoReconnect,
70+
voiceAutoReconnect,
7171
reconnectStorageKey,
7272
onCallStart: () => {
7373
// In hybrid mode, clear all conversations when starting voice

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './vapiChatClient';
2+
export * from './vapiCallStorage';

src/utils/vapiCallStorage.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
export interface StoredCallData {
2+
webCallUrl: string;
3+
id?: string;
4+
artifactPlan?: {
5+
videoRecordingEnabled?: boolean;
6+
};
7+
assistant?: {
8+
voice?: {
9+
provider?: string;
10+
};
11+
};
12+
callOptions?: any;
13+
timestamp: number;
14+
}
15+
16+
export const storeCallData = (
17+
reconnectStorageKey: string,
18+
call: any,
19+
callOptions?: any
20+
) => {
21+
const webCallUrl =
22+
(call as any).webCallUrl || (call.transport as any)?.callUrl;
23+
24+
if (webCallUrl) {
25+
const webCallToStore = {
26+
webCallUrl,
27+
id: call.id,
28+
artifactPlan: call.artifactPlan,
29+
assistant: call.assistant,
30+
callOptions,
31+
timestamp: Date.now(),
32+
};
33+
sessionStorage.setItem(reconnectStorageKey, JSON.stringify(webCallToStore));
34+
console.log('Stored call data for reconnection:', webCallToStore);
35+
} else {
36+
console.warn(
37+
'No webCallUrl found in call object, cannot store for reconnection'
38+
);
39+
}
40+
};
41+
42+
export const getStoredCallData = (
43+
reconnectStorageKey: string
44+
): StoredCallData | null => {
45+
try {
46+
const stored = sessionStorage.getItem(reconnectStorageKey);
47+
if (!stored) return null;
48+
49+
return JSON.parse(stored);
50+
} catch {
51+
sessionStorage.removeItem(reconnectStorageKey);
52+
return null;
53+
}
54+
};
55+
56+
export const clearStoredCall = (reconnectStorageKey: string) => {
57+
sessionStorage.removeItem(reconnectStorageKey);
58+
};
59+
60+
export const areCallOptionsEqual = (options1: any, options2: any): boolean => {
61+
// Handle null/undefined cases
62+
if (options1 === options2) return true;
63+
if (!options1 || !options2) return false;
64+
65+
try {
66+
// Deep comparison using JSON serialization
67+
// This works for most cases but may not handle functions, dates, etc.
68+
return JSON.stringify(options1) === JSON.stringify(options2);
69+
} catch {
70+
// Fallback to reference equality if JSON.stringify fails
71+
return options1 === options2;
72+
}
73+
};

src/widget/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ function initializeWidgets() {
168168

169169
// Voice Configuration
170170
'voice-show-transcript': 'voiceShowTranscript',
171+
'voice-auto-reconnect': 'voiceAutoReconnect',
172+
'reconnect-storage-key': 'reconnectStorageKey',
171173

172174
// Consent Configuration
173175
'consent-required': 'consentRequired',

0 commit comments

Comments
 (0)