import Vapi from '@vapi-ai/web';
import { useCallback, useState } from 'react';
const vapi = new Vapi('<YOUR_PUBLIC_KEY>');
function App() {
const [notification, setNotification] = useState<string | null>(null);
const handleUIUpdate = useCallback((message?: string) => {
setNotification(message || 'UI Update Triggered!');
setTimeout(() => setNotification(null), 3000);
}, []);
// 1) Listen for client tool-calls and update the UI
vapi.on('message', (message) => {
console.log('Message:', message);
if (message.type === 'tool-calls') {
const toolCalls = message.toolCallList;
toolCalls.forEach((toolCall) => {
const functionName = toolCall.function?.name;
let parameters: Record<string, unknown> = {};
try {
const args = toolCall.function?.arguments;
if (typeof args === 'string') {
parameters = JSON.parse(args || '{}');
} else if (typeof args === 'object' && args !== null) {
parameters = args as Record<string, unknown>;
} else {
parameters = {};
}
} catch (err) {
console.error('Failed to parse toolCall arguments:', err);
return;
}
if (functionName === 'updateUI') {
handleUIUpdate((parameters as any).message);
}
});
}
});
// 2) Start the call with a client-side tool (no server URL)
const startCall = useCallback(() => {
vapi.start({
model: {
provider: 'openai',
model: 'gpt-4.1',
messages: [
{
role: 'system',
content:
"You are an attentive assistant who can interact with the application's user interface by calling available tools. Whenever the user asks to update, refresh, change, or otherwise modify the UI, or hints that some UI update should occur, always use the 'updateUI' tool call with the requested action and relevant data. Use tool calls proactively if you determine that a UI update would be helpful.",
},
],
tools: [
{
type: 'function',
async: true,
function: {
name: 'updateUI',
description:
'Call this function to initiate any UI update whenever the user requests or implies they want the user interface to change (for example: show a message, highlight something, trigger an animation, etc). Provide an \'action\' describing the update and an optional \'data\' object with specifics.',
parameters: {
type: 'object',
properties: {
message: {
description:
'Feel free to start with any brief introduction message in 10 words.',
type: 'string',
default: '',
},
},
required: ['message'],
},
},
messages: [
{
type: 'request-start',
content: 'Updating UI...',
blocking: false,
},
],
},
],
},
voice: { provider: 'vapi', version: 2, voiceId: 'Elliot' },
transcriber: { provider: 'deepgram', model: 'nova-2', language: 'en' },
name: 'Alex - Test',
firstMessage: 'Hello.',
voicemailMessage: "Please call back when you're available.",
endCallMessage: 'Goodbye.',
clientMessages: ['tool-calls'], // subscribe to client-side tool calls
});
}, []);
const stopCall = useCallback(() => {
vapi.stop();
}, []);
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}}>
{notification && (
<div style={{
position: 'fixed',
top: 20,
left: '50%',
transform: 'translateX(-50%)',
background: '#10b981',
color: '#fff',
padding: '16px 24px',
textAlign: 'center',
borderRadius: '12px',
zIndex: 1000,
maxWidth: 400,
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.2)',
fontSize: '14px',
fontWeight: 500,
}}>
{notification}
</div>
)}
<div style={{
background: 'white',
padding: '48px',
borderRadius: '20px',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
textAlign: 'center',
maxWidth: '400px',
width: '100%',
}}>
<h1 style={{
fontSize: '32px',
fontWeight: 700,
color: '#1f2937',
marginBottom: '12px',
marginTop: 0,
}}>
Vapi Client Tool Calls
</h1>
<p style={{
fontSize: '16px',
color: '#6b7280',
marginBottom: '32px',
marginTop: 0,
}}>
Start a call and ask the assistant to trigger UI updates
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<button
onClick={startCall}
style={{
background:
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
border: 'none',
padding: '16px 32px',
borderRadius: '12px',
fontSize: '16px',
fontWeight: 600,
cursor: 'pointer',
transition: 'transform 0.2s, box-shadow 0.2s',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.4)',
}}
onMouseEnter={(e) => {
(e.target as HTMLButtonElement).style.transform = 'translateY(-2px)';
(e.target as HTMLButtonElement).style.boxShadow =
'0 6px 20px rgba(102, 126, 234, 0.5)';
}}
onMouseLeave={(e) => {
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
(e.target as HTMLButtonElement).style.boxShadow =
'0 4px 12px rgba(102, 126, 234, 0.4)';
}}
>
Start Call
</button>
<button
onClick={stopCall}
style={{
background: 'white',
color: '#ef4444',
border: '2px solid #ef4444',
padding: '16px 32px',
borderRadius: '12px',
fontSize: '16px',
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
(e.target as HTMLButtonElement).style.background = '#ef4444';
(e.target as HTMLButtonElement).style.color = 'white';
}}
onMouseLeave={(e) => {
(e.target as HTMLButtonElement).style.background = 'white';
(e.target as HTMLButtonElement).style.color = '#ef4444';
}}
>
Stop Call
</button>
</div>
</div>
</div>
);
}
export default App;