11import { useState , useEffect , useRef , useCallback } from 'react' ;
22import Vapi from '@vapi-ai/web' ;
33
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+ }
17+
418export interface VapiCallState {
519 isCallActive : boolean ;
620 isSpeaking : boolean ;
@@ -11,16 +25,20 @@ export interface VapiCallState {
1125
1226export interface VapiCallHandlers {
1327 startCall : ( ) => Promise < void > ;
14- endCall : ( ) => Promise < void > ;
15- toggleCall : ( ) => Promise < void > ;
28+ endCall : ( opts ?: { force ?: boolean } ) => Promise < void > ;
29+ toggleCall : ( opts ?: { force ?: boolean } ) => Promise < void > ;
1630 toggleMute : ( ) => void ;
31+ reconnect : ( ) => Promise < void > ;
32+ clearStoredCall : ( ) => void ;
1733}
1834
1935export interface UseVapiCallOptions {
2036 publicKey : string ;
2137 callOptions : any ;
2238 apiUrl ?: string ;
2339 enabled ?: boolean ;
40+ roomDeleteOnUserLeaveEnabled ?: boolean ;
41+ reconnectStorageKey ?: string ;
2442 onCallStart ?: ( ) => void ;
2543 onCallEnd ?: ( ) => void ;
2644 onMessage ?: ( message : any ) => void ;
@@ -37,6 +55,8 @@ export const useVapiCall = ({
3755 callOptions,
3856 apiUrl,
3957 enabled = true ,
58+ roomDeleteOnUserLeaveEnabled = false ,
59+ reconnectStorageKey = 'vapi_widget_web_call' ,
4060 onCallStart,
4161 onCallEnd,
4262 onMessage,
@@ -63,6 +83,54 @@ export const useVapiCall = ({
6383 onTranscript,
6484 } ) ;
6585
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+
66134 useEffect ( ( ) => {
67135 callbacksRef . current = {
68136 onCallStart,
@@ -90,6 +158,8 @@ export const useVapiCall = ({
90158 setVolumeLevel ( 0 ) ;
91159 setIsSpeaking ( false ) ;
92160 setIsMuted ( false ) ;
161+ // Clear stored call data on successful call end
162+ clearStoredCall ( ) ;
93163 callbacksRef . current . onCallEnd ?.( ) ;
94164 } ;
95165
@@ -144,7 +214,7 @@ export const useVapiCall = ({
144214 vapi . removeListener ( 'message' , handleMessage ) ;
145215 vapi . removeListener ( 'error' , handleError ) ;
146216 } ;
147- } , [ vapi ] ) ;
217+ } , [ vapi , clearStoredCall ] ) ;
148218
149219 useEffect ( ( ) => {
150220 return ( ) => {
@@ -161,33 +231,68 @@ export const useVapiCall = ({
161231 }
162232
163233 try {
164- console . log ( 'Starting call with options:' , callOptions ) ;
234+ console . log ( 'Starting call with configuration:' , callOptions ) ;
235+ console . log ( 'Starting call with options:' , {
236+ roomDeleteOnUserLeaveEnabled,
237+ } ) ;
165238 setConnectionStatus ( 'connecting' ) ;
166- await vapi . start ( callOptions ) ;
239+ const call = await vapi . start (
240+ // assistant
241+ callOptions ,
242+ // assistant overrides,
243+ undefined ,
244+ // squad
245+ undefined ,
246+ // workflow
247+ undefined ,
248+ // workflow overrides
249+ undefined ,
250+ // options
251+ {
252+ roomDeleteOnUserLeaveEnabled,
253+ }
254+ ) ;
255+
256+ // Store call data for reconnection if call was successful and auto-reconnect is enabled
257+ if ( call && ! roomDeleteOnUserLeaveEnabled ) {
258+ storeCallData ( call ) ;
259+ }
167260 } catch ( error ) {
168261 console . error ( 'Error starting call:' , error ) ;
169262 setConnectionStatus ( 'disconnected' ) ;
170263 callbacksRef . current . onError ?.( error as Error ) ;
171264 }
172- } , [ vapi , callOptions , enabled ] ) ;
265+ } , [ vapi , callOptions , enabled , roomDeleteOnUserLeaveEnabled , storeCallData ] ) ;
173266
174- const endCall = useCallback ( async ( ) => {
175- if ( ! vapi ) {
176- console . log ( 'Cannot end call: no vapi instance' ) ;
177- return ;
178- }
267+ const endCall = useCallback (
268+ async ( { force = false } : { force ?: boolean } = { } ) => {
269+ if ( ! vapi ) {
270+ console . log ( 'Cannot end call: no vapi instance' ) ;
271+ return ;
272+ }
179273
180- console . log ( 'Ending call' ) ;
181- vapi . stop ( ) ;
182- } , [ vapi ] ) ;
274+ console . log ( 'Ending call with force:' , force ) ;
275+ if ( force ) {
276+ // end vapi call and delete daily room
277+ vapi . end ( ) ;
278+ } else {
279+ // simply disconnect from daily room
280+ vapi . stop ( ) ;
281+ }
282+ } ,
283+ [ vapi ]
284+ ) ;
183285
184- const toggleCall = useCallback ( async ( ) => {
185- if ( isCallActive ) {
186- await endCall ( ) ;
187- } else {
188- await startCall ( ) ;
189- }
190- } , [ isCallActive , startCall , endCall ] ) ;
286+ const toggleCall = useCallback (
287+ async ( { force = false } : { force ?: boolean } = { } ) => {
288+ if ( isCallActive ) {
289+ await endCall ( { force } ) ;
290+ } else {
291+ await startCall ( ) ;
292+ }
293+ } ,
294+ [ isCallActive , startCall , endCall ]
295+ ) ;
191296
192297 const toggleMute = useCallback ( ( ) => {
193298 if ( ! vapi || ! isCallActive ) {
@@ -200,6 +305,53 @@ export const useVapiCall = ({
200305 setIsMuted ( newMutedState ) ;
201306 } , [ vapi , isCallActive , isMuted ] ) ;
202307
308+ const reconnect = useCallback ( async ( ) => {
309+ if ( ! vapi || ! enabled ) {
310+ console . error ( 'Cannot reconnect: no vapi instance or not enabled' ) ;
311+ return ;
312+ }
313+
314+ const storedData = getStoredCallData ( ) ;
315+ if ( ! storedData ) {
316+ console . warn ( 'No stored call data found for reconnection' ) ;
317+ return ;
318+ }
319+
320+ setConnectionStatus ( 'connecting' ) ;
321+ try {
322+ await vapi . reconnect ( {
323+ webCallUrl : storedData . webCallUrl ,
324+ id : storedData . id ,
325+ artifactPlan : storedData . artifactPlan ,
326+ assistant : storedData . assistant ,
327+ } ) ;
328+ console . log ( 'Successfully reconnected to call' ) ;
329+ } catch ( error ) {
330+ setConnectionStatus ( 'disconnected' ) ;
331+ console . error ( 'Reconnection failed:' , error ) ;
332+ clearStoredCall ( ) ;
333+ callbacksRef . current . onError ?.( error as Error ) ;
334+ }
335+ } , [ vapi , enabled , getStoredCallData , clearStoredCall ] ) ;
336+
337+ // Check for stored call data on initialization and attempt reconnection
338+ useEffect ( ( ) => {
339+ if ( ! vapi || ! enabled || roomDeleteOnUserLeaveEnabled ) {
340+ return ;
341+ }
342+
343+ const storedData = getStoredCallData ( ) ;
344+ if ( storedData ) {
345+ reconnect ( ) ;
346+ }
347+ } , [
348+ vapi ,
349+ enabled ,
350+ roomDeleteOnUserLeaveEnabled ,
351+ getStoredCallData ,
352+ reconnect ,
353+ ] ) ;
354+
203355 return {
204356 // State
205357 isCallActive,
@@ -212,5 +364,7 @@ export const useVapiCall = ({
212364 endCall,
213365 toggleCall,
214366 toggleMute,
367+ reconnect,
368+ clearStoredCall,
215369 } ;
216370} ;
0 commit comments