Skip to content

Commit 03ff4cd

Browse files
Widget v0.1.0
1 parent e837d21 commit 03ff4cd

7 files changed

Lines changed: 148 additions & 137 deletions

File tree

bun.lock

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

dist/widget.iife.js

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

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,8 @@
2525
"author": "Structured Labs",
2626
"devDependencies": {
2727
"vite": "^7.0.3"
28+
},
29+
"dependencies": {
30+
"@vapi-ai/web": "^2.3.8"
2831
}
2932
}

src/vapi.js

Lines changed: 8 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,17 @@
1+
import Vapi from '@vapi-ai/web';
2+
13
export class VapiClient {
24
constructor(config) {
35
this.config = config;
46
this.vapi = null;
57
this.isCallActive = false;
68
this.eventListeners = new Map();
7-
8-
this.loadVapiSDK();
9+
this.initVapi();
910
}
1011

11-
async loadVapiSDK() {
12-
try {
13-
// Load Vapi Web SDK if not already loaded
14-
if (typeof window.vapiSDK === 'undefined') {
15-
await this.loadScript('https://cdn.jsdelivr.net/gh/VapiAI/html-script-tag@latest/dist/assets/index.js');
16-
}
17-
18-
// Verify Vapi SDK is available
19-
if (typeof window.vapiSDK === 'undefined') {
20-
throw new Error('Failed to load Vapi SDK');
21-
}
22-
23-
// Initialize Vapi client
24-
this.vapi = window.vapiSDK.run({
25-
apiKey: this.config.apiKey,
26-
assistant: this.config.assistant, // We'll need to add this to config
27-
config: {}
28-
});
29-
30-
this.setupEventListeners();
31-
} catch (error) {
32-
console.error('Failed to load Vapi SDK:', error);
33-
throw new Error('Voice service unavailable. Please try again later.');
34-
}
35-
}
36-
37-
loadScript(src) {
38-
return new Promise((resolve, reject) => {
39-
// Check if script is already loaded
40-
const existingScript = document.querySelector(`script[src="${src}"]`);
41-
if (existingScript) {
42-
resolve();
43-
return;
44-
}
45-
46-
const script = document.createElement('script');
47-
script.src = src;
48-
script.onload = resolve;
49-
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
50-
script.ontimeout = () => reject(new Error(`Script load timeout: ${src}`));
51-
52-
// Add timeout
53-
setTimeout(() => {
54-
reject(new Error(`Script load timeout: ${src}`));
55-
}, 10000);
56-
57-
document.head.appendChild(script);
58-
});
12+
initVapi() {
13+
this.vapi = new Vapi(this.config.apiKey);
14+
this.setupEventListeners();
5915
}
6016

6117
setupEventListeners() {
@@ -112,8 +68,8 @@ export class VapiClient {
11268
}
11369

11470
try {
115-
// Start call using Vapi SDK
116-
await this.vapi.start();
71+
// Start call using Vapi SDK with assistant ID
72+
await this.vapi.start(this.config.assistant);
11773
} catch (error) {
11874
console.error('Failed to start Vapi call:', error);
11975
throw error;
@@ -141,7 +97,6 @@ export class VapiClient {
14197

14298
off(event, callback) {
14399
if (!this.eventListeners.has(event)) return;
144-
145100
const listeners = this.eventListeners.get(event);
146101
const index = listeners.indexOf(callback);
147102
if (index > -1) {
@@ -151,7 +106,6 @@ export class VapiClient {
151106

152107
emit(event, data) {
153108
if (!this.eventListeners.has(event)) return;
154-
155109
this.eventListeners.get(event).forEach(callback => {
156110
try {
157111
callback(data);

src/widget.js

Lines changed: 51 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ import { VapiClient } from './vapi.js';
33
export class BuzzwaldWidget {
44
constructor(config = {}) {
55
this.config = {
6-
apiKey: '',
76
vapiKey: '',
8-
assistant: '', // Vapi assistant ID
7+
assistant: '',
98
phoneNumber: '',
109
position: 'bottom-right',
1110
backgroundColor: '#FFFF00',
1211
iconColor: '#000000',
13-
mockMode: false, // Set to true for testing without backend
1412
...config
1513
};
1614

@@ -266,6 +264,14 @@ export class BuzzwaldWidget {
266264
left: 15px;
267265
}
268266
}
267+
268+
/* Hide Vapi's default button */
269+
.vapi-support-btn,
270+
#vapi-support-btn,
271+
[id*="vapi"],
272+
[class*="vapi"] {
273+
display: none !important;
274+
}
269275
`;
270276
document.head.appendChild(style);
271277
}
@@ -287,6 +293,37 @@ export class BuzzwaldWidget {
287293
// Add widget to DOM
288294
this.widgetElement.appendChild(this.buttonElement);
289295
document.body.appendChild(this.widgetElement);
296+
297+
// Hide any Vapi buttons that might appear
298+
this.hideVapiButtons();
299+
}
300+
301+
hideVapiButtons() {
302+
// Function to hide Vapi buttons
303+
const hideButtons = () => {
304+
const vapiButtons = document.querySelectorAll('.vapi-support-btn, #vapi-support-btn, [id*="vapi"], [class*="vapi"]:not([class*="buzzwald"])');
305+
vapiButtons.forEach(button => {
306+
if (button && !button.closest('.buzzwald-widget')) {
307+
button.style.display = 'none';
308+
}
309+
});
310+
};
311+
312+
// Hide immediately
313+
hideButtons();
314+
315+
// Keep checking for new Vapi buttons
316+
const observer = new MutationObserver(() => {
317+
hideButtons();
318+
});
319+
320+
observer.observe(document.body, {
321+
childList: true,
322+
subtree: true
323+
});
324+
325+
// Store observer for cleanup
326+
this.vapiButtonObserver = observer;
290327
}
291328

292329
getPhoneIcon() {
@@ -394,15 +431,11 @@ export class BuzzwaldWidget {
394431
}
395432

396433
initializeVapi() {
397-
if (this.config.mockMode) {
398-
this.vapi = this.createMockVapiClient();
399-
} else {
400-
this.vapi = new VapiClient({
401-
apiKey: this.config.vapiKey,
402-
assistant: this.config.assistant,
403-
phoneNumber: this.config.phoneNumber
404-
});
405-
}
434+
this.vapi = new VapiClient({
435+
apiKey: this.config.vapiKey,
436+
assistant: this.config.assistant,
437+
phoneNumber: this.config.phoneNumber
438+
});
406439

407440
// Listen to call state changes
408441
this.vapi.on('call-start', () => this.updateCallState('connecting'));
@@ -418,62 +451,6 @@ export class BuzzwaldWidget {
418451
});
419452
}
420453

421-
createMockVapiClient() {
422-
return {
423-
eventListeners: new Map(),
424-
425-
async start() {
426-
console.log('🎭 Mock: Starting Vapi call');
427-
this.emit('call-start');
428-
429-
// Simulate call progression
430-
setTimeout(() => this.emit('speech-start'), 2000);
431-
432-
// Auto-end call after 10 seconds for demo
433-
setTimeout(() => {
434-
console.log('🎭 Mock: Auto-ending call');
435-
this.emit('call-end');
436-
}, 10000);
437-
},
438-
439-
stop() {
440-
console.log('🎭 Mock: Stopping call');
441-
this.emit('call-end');
442-
},
443-
444-
on(event, callback) {
445-
if (!this.eventListeners.has(event)) {
446-
this.eventListeners.set(event, []);
447-
}
448-
this.eventListeners.get(event).push(callback);
449-
},
450-
451-
off(event, callback) {
452-
if (!this.eventListeners.has(event)) return;
453-
const listeners = this.eventListeners.get(event);
454-
const index = listeners.indexOf(callback);
455-
if (index > -1) {
456-
listeners.splice(index, 1);
457-
}
458-
},
459-
460-
emit(event, data) {
461-
if (!this.eventListeners.has(event)) return;
462-
this.eventListeners.get(event).forEach(callback => {
463-
try {
464-
callback(data);
465-
} catch (error) {
466-
console.error(`Error in event listener for ${event}:`, error);
467-
}
468-
});
469-
},
470-
471-
destroy() {
472-
this.eventListeners.clear();
473-
}
474-
};
475-
}
476-
477454
updateCallState(state) {
478455
this.currentCallState = state;
479456

@@ -529,6 +506,12 @@ export class BuzzwaldWidget {
529506
this.retryTimeout = null;
530507
}
531508

509+
// Clean up mutation observer
510+
if (this.vapiButtonObserver) {
511+
this.vapiButtonObserver.disconnect();
512+
this.vapiButtonObserver = null;
513+
}
514+
532515
this.isInitialized = false;
533516
} catch (error) {
534517
console.error('Buzzwald: Error during widget destruction', error);

test.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,15 @@ <h3>Call States:</h3>
124124
</div>
125125
</div>
126126

127+
<!-- Widget script is loaded in dev mode below -->
127128
<script>
128129
// Configuration for the widget
129130
window.BuzzwaldConfig = {
130-
vapiKey: 'your-vapi-key-here', // Add your Vapi API key
131-
assistant: 'your-assistant-id-here', // Add your Vapi assistant ID
132-
phoneNumber: '+1234567890',
131+
vapiKey: '', // Add your Vapi API key
132+
assistant: '',
133133
position: 'bottom-right',
134134
backgroundColor: '#FFFF00',
135135
iconColor: '#000000',
136-
mockMode: true // Enable mock mode for testing without Vapi key
137136
};
138137

139138
// Display configuration

test2.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ <h2>Testing Instructions</h2>
7575
<!-- Buzzwald Widget Configuration -->
7676
<script>
7777
window.BuzzwaldConfig = {
78-
apiKey: 'customer-api-key-here',
79-
vapiKey: 'd6a96be9-4c92-46f4-a3ce-df34cdf017e2',
78+
vapiKey: '',
79+
assistant: '',
8080
position: 'bottom-right', // optional
8181
backgroundColor: '#FFFF00', // optional
8282
iconColor: '#000000' // optional
8383
};
8484
</script>
8585

86-
<!-- Buzzwald Widget Script -->
86+
<!-- Buzzwald Widget Script (now includes Vapi SDK, no separate Vapi script needed) -->
8787
<script src="https://cdn.jsdelivr.net/gh/StructuredLabs/buzzwald-client@latest/dist/widget.iife.js"></script>
8888
</body>
8989
</html>

0 commit comments

Comments
 (0)