Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18143,6 +18143,15 @@ export interface CreateWebCallDTO {
workflow?: CreateWorkflowDTO;
/** These are the overrides for the `workflow` or `workflowId`'s settings and template variables. */
workflowOverrides?: WorkflowOverrides;
/**
* This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
* If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
* If set to `true`, the room will be deleted and reconnection will not be allowed.
*
* Defaults to `true`.
* @example true
*/
roomDeleteOnUserLeaveEnabled?: boolean;
}

export interface UpdateCallDTO {
Expand Down
141 changes: 133 additions & 8 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import Vapi from '@vapi-ai/web';

const VAPI_PUBLIC_KEY = import.meta.env.VITE_VAPI_PUBLIC_KEY;
const VAPI_API_BASE_URL = import.meta.env.VITE_VAPI_API_BASE_URL;

if (!VAPI_PUBLIC_KEY) {
throw new Error('VITE_VAPI_PUBLIC_KEY is required. Please set it in your .env.local file.');
Expand All @@ -14,7 +15,7 @@ interface Message {
}

function App() {
const [vapi] = useState(() => new Vapi(VAPI_PUBLIC_KEY));
const [vapi] = useState(() => new Vapi(VAPI_PUBLIC_KEY, VAPI_API_BASE_URL));
const [connected, setConnected] = useState(false);
const [assistantIsSpeaking, setAssistantIsSpeaking] = useState(false);
const [volumeLevel, setVolumeLevel] = useState(0);
Expand All @@ -25,16 +26,29 @@ function App() {
const [interruptionsEnabled, setInterruptionsEnabled] = useState(true);
const [interruptAssistantEnabled, setInterruptAssistantEnabled] = useState(true);
const [endCallAfterSay, setEndCallAfterSay] = useState(false);
const [storedWebCall, setStoredWebCall] = useState<any>(null);

useEffect(() => {
// Check for stored webCall on component mount
const stored = localStorage.getItem('vapi-webcall');
if (stored) {
try {
const parsedWebCall = JSON.parse(stored);
setStoredWebCall(parsedWebCall);
} catch (error) {
console.error('Error parsing stored webCall:', error);
localStorage.removeItem('vapi-webcall');
}
}

// Update current time every second
const timer = setInterval(() => {
setCurrentTime(new Date().toLocaleTimeString());
}, 1000);

// Set up Vapi event listeners
vapi.on('call-start', () => {
console.log('Call started');
console.log('Call started - call-start event fired');
setConnected(true);
addMessage('system', 'Call connected');
});
Expand All @@ -44,7 +58,7 @@ function App() {
setConnected(false);
setAssistantIsSpeaking(false);
setVolumeLevel(0);
addMessage('system', 'Call ended');
addMessage('system', 'Call ended - webCall data preserved for reconnection');
});

vapi.on('speech-start', () => {
Expand Down Expand Up @@ -84,7 +98,7 @@ function App() {
console.error('Vapi error:', error);
addMessage('system', `Error: ${error.message || error}`);
});

return () => {
clearInterval(timer);
vapi.stop();
Expand All @@ -104,7 +118,7 @@ function App() {
addMessage('system', 'Starting call...');

// Start call with assistant configuration
await vapi.start({
const webCall = await vapi.start({
// Basic assistant configuration
model: {
provider: "openai",
Expand Down Expand Up @@ -137,16 +151,66 @@ function App() {

// Max call duration (in seconds) - 10 minutes
maxDurationSeconds: 600
}, undefined, undefined, undefined, undefined, {
roomDeleteOnUserLeaveEnabled: false
});

// Store webCall in localStorage if it was created successfully
if (webCall) {
const webCallToStore = {
webCallUrl: (webCall as any).webCallUrl,
id: webCall.id,
artifactPlan: webCall.artifactPlan,
assistant: webCall.assistant
};
localStorage.setItem('vapi-webcall', JSON.stringify(webCallToStore));
setStoredWebCall(webCallToStore);
addMessage('system', 'Call data stored for reconnection');
}

} catch (error) {
console.error('Error starting call:', error);
addMessage('system', `Failed to start call: ${error}`);
}
};

const stopCall = () => {
vapi.stop();
vapi.end();
};

const reconnectCall = async () => {
if (!storedWebCall) {
addMessage('system', 'No stored call data found');
return;
}

try {
addMessage('system', 'Reconnecting to previous call...');
console.log('Attempting reconnect with data:', storedWebCall);
await vapi.reconnect(storedWebCall);
addMessage('system', 'Reconnect method completed successfully');

// Add a small delay to allow events to propagate
setTimeout(() => {
if (!connected) {
addMessage('system', 'Warning: Reconnect completed but connected state not updated. This may indicate an issue with event handling.');
}
}, 1000);

} catch (error) {
console.error('Error reconnecting:', error);
addMessage('system', `Failed to reconnect: ${error}`);

// Clear invalid stored data
localStorage.removeItem('vapi-webcall');
setStoredWebCall(null);
}
};

const clearStoredCall = () => {
localStorage.removeItem('vapi-webcall');
setStoredWebCall(null);
addMessage('system', 'Cleared stored call data');
};

const toggleMute = () => {
Expand Down Expand Up @@ -216,7 +280,7 @@ function App() {
borderRadius: '8px',
marginBottom: '20px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '10px' }}>
<div>
<strong>Status:</strong>
<span style={{
Expand All @@ -225,10 +289,33 @@ function App() {
}}>
{connected ? 'Connected' : 'Disconnected'}
</span>
{storedWebCall && !connected && (
<span style={{
color: '#f59e0b',
marginLeft: '8px',
fontSize: '14px'
}}>
(Reconnect Available)
</span>
)}
</div>
<div>Current Time: {currentTime}</div>
</div>

{storedWebCall && (
<div style={{
marginTop: '10px',
padding: '8px 12px',
backgroundColor: connected ? '#dcfce7' : '#fef3c7',
borderRadius: '4px',
fontSize: '14px',
color: connected ? '#166534' : '#92400e'
}}>
<strong>Stored Call:</strong> ID {storedWebCall.id || 'Unknown'} -
{connected ? ' Currently active' : ' Ready to reconnect'}
</div>
)}

{connected && (
<div style={{ marginTop: '10px' }}>
<div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
Expand Down Expand Up @@ -261,7 +348,8 @@ function App() {
display: 'flex',
gap: '10px',
justifyContent: 'center',
marginBottom: '20px'
marginBottom: '20px',
flexWrap: 'wrap'
}}>
<button
onClick={startCall}
Expand All @@ -278,6 +366,23 @@ function App() {
>
Start Call
</button>

{storedWebCall && !connected && (
<button
onClick={reconnectCall}
style={{
padding: '12px 24px',
backgroundColor: '#f59e0b',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '16px'
}}
>
Reconnect to Stored Call
</button>
)}

<button
onClick={stopCall}
Expand Down Expand Up @@ -326,6 +431,23 @@ function App() {
>
Send Context
</button>

{storedWebCall && (
<button
onClick={clearStoredCall}
style={{
padding: '12px 24px',
backgroundColor: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '16px'
}}
>
Clear Stored Call
</button>
)}
</div>

{/* Manual Say Controls */}
Expand Down Expand Up @@ -539,6 +661,9 @@ function App() {
<li>Use "Mute" to temporarily disable your microphone</li>
<li>Say "goodbye" or "end call" to end the conversation</li>
<li>Click "Stop Call" to manually end the call</li>
<li><strong>Persistent Storage:</strong> Call data is automatically saved and persists even after calls end</li>
<li><strong>Reconnection:</strong> Use "Reconnect to Stored Call" to rejoin your previous session anytime</li>
<li>Use "Clear Stored Call" to permanently remove saved call data when no longer needed</li>
</ul>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"test:example": "npm run pack:local && cd example && npm install && npm run build",
"pack:local": "./scripts/build-latest.sh",
"clean-builds": "rm -f vapi-ai-web-*.tgz && echo '🧹 Cleaned up all build tarballs'",
"dev:example": "npm run pack:local && cd example && npm install && npm run dev"
"dev:example": "npm run pack:local && cd example && npm install && npm run dev",
"re-build-local-example": "./scripts/re-build-local-example.sh"
},
"repository": {
"type": "git",
Expand Down
17 changes: 17 additions & 0 deletions scripts/re-build-local-example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

set -e

# Clean up the example node_modules and package-lock.json
rm -rf example/node_modules
rm -f example/package-lock.json

# Clean up the local build clean-builds
npm run clean-builds

# Re-pack local build
npm run pack:local

# Re-install the dependencies
cd example
npm install
Loading
Loading