217 lines
6.4 KiB
JavaScript
217 lines
6.4 KiB
JavaScript
/**
|
||
* Copyright (c) 2024–2025, Daily
|
||
*
|
||
* SPDX-License-Identifier: BSD 2-Clause License
|
||
*/
|
||
|
||
import Daily from "@daily-co/daily-js";
|
||
|
||
/**
|
||
* ChatbotClient handles the connection and media management for a real-time
|
||
* voice interaction with an AI bot.
|
||
*/
|
||
class ChatbotClient {
|
||
constructor() {
|
||
// Initialize client state
|
||
this.dailyCallObject = null;
|
||
this.setupDOMElements();
|
||
this.setupEventListeners();
|
||
}
|
||
|
||
/**
|
||
* Set up references to DOM elements and create necessary media elements
|
||
*/
|
||
setupDOMElements() {
|
||
// Get references to UI control elements
|
||
this.connectBtn = document.getElementById('connect-btn');
|
||
this.disconnectBtn = document.getElementById('disconnect-btn');
|
||
this.statusSpan = document.getElementById('connection-status');
|
||
this.debugLog = document.getElementById('debug-log');
|
||
|
||
// Create an audio element for bot's voice output
|
||
this.botAudio = document.createElement('audio');
|
||
this.botAudio.autoplay = true;
|
||
this.botAudio.playsInline = true;
|
||
document.body.appendChild(this.botAudio);
|
||
}
|
||
|
||
/**
|
||
* Set up event listeners for connect/disconnect buttons
|
||
*/
|
||
setupEventListeners() {
|
||
this.connectBtn.addEventListener('click', () => this.connect());
|
||
this.disconnectBtn.addEventListener('click', () => this.disconnect());
|
||
}
|
||
|
||
/**
|
||
* Add a timestamped message to the debug log
|
||
*/
|
||
log(message) {
|
||
const entry = document.createElement('div');
|
||
entry.textContent = `${new Date().toISOString()} - ${message}`;
|
||
|
||
// Add styling based on message type
|
||
if (message.startsWith('User: ')) {
|
||
entry.style.color = '#2196F3'; // blue for user
|
||
} else if (message.startsWith('Bot: ')) {
|
||
entry.style.color = '#4CAF50'; // green for bot
|
||
}
|
||
|
||
this.debugLog.appendChild(entry);
|
||
this.debugLog.scrollTop = this.debugLog.scrollHeight;
|
||
console.log(message);
|
||
}
|
||
|
||
/**
|
||
* Update the connection status display
|
||
*/
|
||
updateStatus(status) {
|
||
this.statusSpan.textContent = status;
|
||
this.log(`Status: ${status}`);
|
||
}
|
||
|
||
handleEventToConsole (evt) {
|
||
this.log(`Received event: ${evt.action}`);
|
||
};
|
||
|
||
/**
|
||
* Set up listeners for track events (start/stop)
|
||
* This handles new tracks being added during the session
|
||
*/
|
||
setupTrackListeners() {
|
||
if (!this.dailyCallObject) return;
|
||
|
||
this.dailyCallObject.on("joined-meeting", () => {
|
||
this.updateStatus('Connected');
|
||
this.connectBtn.disabled = true;
|
||
this.disconnectBtn.disabled = false;
|
||
this.log('Client connected');
|
||
});
|
||
this.dailyCallObject.on("track-started", (evt) => {
|
||
if (evt.track.kind === "audio" && evt.participant.local === false) {
|
||
this.log("Audio track started.")
|
||
this.setupAudioTrack(evt.track);
|
||
}
|
||
});
|
||
this.dailyCallObject.on("track-stopped", this.handleEventToConsole.bind(this));
|
||
this.dailyCallObject.on("participant-joined", this.handleEventToConsole.bind(this));
|
||
this.dailyCallObject.on("participant-updated", this.handleEventToConsole.bind(this));
|
||
this.dailyCallObject.on("participant-left", () => {
|
||
// When the bot leaves, we are also disconnecting from the call
|
||
this.disconnect()
|
||
});
|
||
this.dailyCallObject.on("left-meeting", () => {
|
||
this.updateStatus('Disconnected');
|
||
this.connectBtn.disabled = false;
|
||
this.disconnectBtn.disabled = true;
|
||
this.log('Client disconnected');
|
||
});
|
||
this.dailyCallObject.on("error", this.handleEventToConsole.bind(this));
|
||
}
|
||
|
||
/**
|
||
* Set up an audio track for playback
|
||
* Handles both initial setup and track updates
|
||
*/
|
||
setupAudioTrack(track) {
|
||
this.log(`Setting up audio track, track state: ${track.readyState}, muted: ${track.muted}`);
|
||
|
||
// Check if we're already playing this track
|
||
if (this.botAudio.srcObject) {
|
||
const oldTrack = this.botAudio.srcObject.getAudioTracks()[0];
|
||
if (oldTrack?.id === track.id) return;
|
||
}
|
||
// Create a new MediaStream with the track and set it as the audio source
|
||
this.botAudio.srcObject = new MediaStream([track]);
|
||
this.botAudio.onplaying = async (event) => {
|
||
this.log("onplaying")
|
||
this.log("Will send the audio message to play the audio at the next tick")
|
||
this.dailyCallObject.sendAppMessage("playable")
|
||
}
|
||
}
|
||
|
||
async fetchRoomInfo() {
|
||
let connectUrl = '/connect'
|
||
let res = await fetch(connectUrl, {
|
||
method: "POST",
|
||
mode: "cors",
|
||
headers: new Headers({
|
||
"Content-Type": "application/json"
|
||
}),
|
||
})
|
||
if (res.ok) {
|
||
return res.json();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Initialize and connect to the bot
|
||
* This sets up the RTVI client, initializes devices, and establishes the connection
|
||
*/
|
||
async connect() {
|
||
try {
|
||
// Initialize the client
|
||
this.dailyCallObject = Daily.createCallObject({
|
||
subscribeToTracksAutomatically: true,
|
||
});
|
||
|
||
// Set up listeners for media track events
|
||
this.setupTrackListeners();
|
||
|
||
this.log('Creating the bot...');
|
||
let roomInfo = await this.fetchRoomInfo()
|
||
|
||
// Connect to the bot
|
||
this.log('Connecting to bot...');
|
||
// Only for making debugger easier
|
||
window.callObject = this.dailyCallObject;
|
||
await this.dailyCallObject.join({
|
||
url: roomInfo.room_url,
|
||
});
|
||
|
||
this.log('Connection complete');
|
||
} catch (error) {
|
||
// Handle any errors during connection
|
||
this.log(`Error connecting: ${error.message}`);
|
||
this.log(`Error stack: ${error.stack}`);
|
||
this.updateStatus('Error');
|
||
|
||
// Clean up if there's an error
|
||
if (this.dailyCallObject) {
|
||
try {
|
||
await this.dailyCallObject.leave();
|
||
} catch (disconnectError) {
|
||
this.log(`Error during disconnect: ${disconnectError.message}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Disconnect from the bot and clean up media resources
|
||
*/
|
||
async disconnect() {
|
||
if (this.dailyCallObject) {
|
||
try {
|
||
// Disconnect the RTVI client
|
||
await this.dailyCallObject.leave();
|
||
await this.dailyCallObject.destroy();
|
||
this.dailyCallObject = null;
|
||
|
||
// Clean up audio
|
||
if (this.botAudio.srcObject) {
|
||
this.botAudio.srcObject.getTracks().forEach((track) => track.stop());
|
||
this.botAudio.srcObject = null;
|
||
}
|
||
} catch (error) {
|
||
this.log(`Error disconnecting: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize the client when the page loads
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
new ChatbotClient();
|
||
});
|