11import { useState , useEffect , useRef , useCallback } from 'react' ;
22import Vapi from '@vapi-ai/web' ;
3+ import * as vapiCallStorage from '../utils/vapiCallStorage' ;
34
45export interface VapiCallState {
56 isCallActive : boolean ;
@@ -11,16 +12,20 @@ export interface VapiCallState {
1112
1213export interface VapiCallHandlers {
1314 startCall : ( ) => Promise < void > ;
14- endCall : ( ) => Promise < void > ;
15- toggleCall : ( ) => Promise < void > ;
15+ endCall : ( opts ?: { force ?: boolean } ) => Promise < void > ;
16+ toggleCall : ( opts ?: { force ?: boolean } ) => Promise < void > ;
1617 toggleMute : ( ) => void ;
18+ reconnect : ( ) => Promise < void > ;
19+ clearStoredCall : ( ) => void ;
1720}
1821
1922export interface UseVapiCallOptions {
2023 publicKey : string ;
2124 callOptions : any ;
2225 apiUrl ?: string ;
2326 enabled ?: boolean ;
27+ voiceAutoReconnect ?: boolean ;
28+ reconnectStorageKey ?: string ;
2429 onCallStart ?: ( ) => void ;
2530 onCallEnd ?: ( ) => void ;
2631 onMessage ?: ( message : any ) => void ;
@@ -37,6 +42,8 @@ export const useVapiCall = ({
3742 callOptions,
3843 apiUrl,
3944 enabled = true ,
45+ voiceAutoReconnect = false ,
46+ reconnectStorageKey = 'vapi_widget_web_call' ,
4047 onCallStart,
4148 onCallEnd,
4249 onMessage,
@@ -90,6 +97,8 @@ export const useVapiCall = ({
9097 setVolumeLevel ( 0 ) ;
9198 setIsSpeaking ( false ) ;
9299 setIsMuted ( false ) ;
100+ // Clear stored call data on successful call end
101+ vapiCallStorage . clearStoredCall ( reconnectStorageKey ) ;
93102 callbacksRef . current . onCallEnd ?.( ) ;
94103 } ;
95104
@@ -144,7 +153,7 @@ export const useVapiCall = ({
144153 vapi . removeListener ( 'message' , handleMessage ) ;
145154 vapi . removeListener ( 'error' , handleError ) ;
146155 } ;
147- } , [ vapi ] ) ;
156+ } , [ vapi , reconnectStorageKey ] ) ;
148157
149158 useEffect ( ( ) => {
150159 return ( ) => {
@@ -161,33 +170,68 @@ export const useVapiCall = ({
161170 }
162171
163172 try {
164- console . log ( 'Starting call with options:' , callOptions ) ;
173+ console . log ( 'Starting call with configuration:' , callOptions ) ;
174+ console . log ( 'Starting call with options:' , {
175+ voiceAutoReconnect,
176+ } ) ;
165177 setConnectionStatus ( 'connecting' ) ;
166- await vapi . start ( callOptions ) ;
178+ const call = await vapi . start (
179+ // assistant
180+ callOptions ,
181+ // assistant overrides,
182+ undefined ,
183+ // squad
184+ undefined ,
185+ // workflow
186+ undefined ,
187+ // workflow overrides
188+ undefined ,
189+ // options
190+ {
191+ roomDeleteOnUserLeaveEnabled : ! voiceAutoReconnect ,
192+ }
193+ ) ;
194+
195+ // Store call data for reconnection if call was successful and auto-reconnect is enabled
196+ if ( call && voiceAutoReconnect ) {
197+ vapiCallStorage . storeCallData ( reconnectStorageKey , call , callOptions ) ;
198+ }
167199 } catch ( error ) {
168200 console . error ( 'Error starting call:' , error ) ;
169201 setConnectionStatus ( 'disconnected' ) ;
170202 callbacksRef . current . onError ?.( error as Error ) ;
171203 }
172- } , [ vapi , callOptions , enabled ] ) ;
204+ } , [ vapi , callOptions , enabled , voiceAutoReconnect , reconnectStorageKey ] ) ;
173205
174- const endCall = useCallback ( async ( ) => {
175- if ( ! vapi ) {
176- console . log ( 'Cannot end call: no vapi instance' ) ;
177- return ;
178- }
206+ const endCall = useCallback (
207+ async ( { force = false } : { force ?: boolean } = { } ) => {
208+ if ( ! vapi ) {
209+ console . log ( 'Cannot end call: no vapi instance' ) ;
210+ return ;
211+ }
179212
180- console . log ( 'Ending call' ) ;
181- vapi . stop ( ) ;
182- } , [ vapi ] ) ;
213+ console . log ( 'Ending call with force:' , force ) ;
214+ if ( force ) {
215+ // end vapi call and delete daily room
216+ vapi . end ( ) ;
217+ } else {
218+ // simply disconnect from daily room
219+ vapi . stop ( ) ;
220+ }
221+ } ,
222+ [ vapi ]
223+ ) ;
183224
184- const toggleCall = useCallback ( async ( ) => {
185- if ( isCallActive ) {
186- await endCall ( ) ;
187- } else {
188- await startCall ( ) ;
189- }
190- } , [ isCallActive , startCall , endCall ] ) ;
225+ const toggleCall = useCallback (
226+ async ( { force = false } : { force ?: boolean } = { } ) => {
227+ if ( isCallActive ) {
228+ await endCall ( { force } ) ;
229+ } else {
230+ await startCall ( ) ;
231+ }
232+ } ,
233+ [ isCallActive , startCall , endCall ]
234+ ) ;
191235
192236 const toggleMute = useCallback ( ( ) => {
193237 if ( ! vapi || ! isCallActive ) {
@@ -200,6 +244,59 @@ export const useVapiCall = ({
200244 setIsMuted ( newMutedState ) ;
201245 } , [ vapi , isCallActive , isMuted ] ) ;
202246
247+ const reconnect = useCallback ( async ( ) => {
248+ if ( ! vapi || ! enabled ) {
249+ console . error ( 'Cannot reconnect: no vapi instance or not enabled' ) ;
250+ return ;
251+ }
252+
253+ const storedData = vapiCallStorage . getStoredCallData ( reconnectStorageKey ) ;
254+
255+ if ( ! storedData ) {
256+ console . warn ( 'No stored call data found for reconnection' ) ;
257+ return ;
258+ }
259+
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+
271+ setConnectionStatus ( 'connecting' ) ;
272+
273+ try {
274+ await vapi . reconnect ( {
275+ webCallUrl : storedData . webCallUrl ,
276+ id : storedData . id ,
277+ artifactPlan : storedData . artifactPlan ,
278+ assistant : storedData . assistant ,
279+ } ) ;
280+ console . log ( 'Successfully reconnected to call' ) ;
281+ } catch ( error ) {
282+ setConnectionStatus ( 'disconnected' ) ;
283+ console . error ( 'Reconnection failed:' , error ) ;
284+ vapiCallStorage . clearStoredCall ( reconnectStorageKey ) ;
285+ callbacksRef . current . onError ?.( error as Error ) ;
286+ }
287+ } , [ vapi , enabled , reconnectStorageKey , callOptions ] ) ;
288+
289+ const clearStoredCall = useCallback ( ( ) => {
290+ vapiCallStorage . clearStoredCall ( reconnectStorageKey ) ;
291+ } , [ reconnectStorageKey ] ) ;
292+
293+ useEffect ( ( ) => {
294+ if ( ! vapi || ! enabled || ! voiceAutoReconnect ) {
295+ return ;
296+ }
297+ reconnect ( ) ;
298+ } , [ vapi , enabled , voiceAutoReconnect , reconnect , reconnectStorageKey ] ) ;
299+
203300 return {
204301 // State
205302 isCallActive,
@@ -212,5 +309,7 @@ export const useVapiCall = ({
212309 endCall,
213310 toggleCall,
214311 toggleMute,
312+ reconnect,
313+ clearStoredCall,
215314 } ;
216315} ;
0 commit comments