class EveAIChatWidget extends HTMLElement { static get observedAttributes() { return ['tenant-id', 'api-key', 'domain', 'language', 'languages', 'server-url', 'specialist-id']; } constructor() { super(); this.socket = null; // Initialize socket to null this.attributesSet = false; // Flag to check if all attributes are set this.jwtToken = null; // Initialize jwtToken to null this.userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Detect user's timezone this.heartbeatInterval = null; this.idleTime = 0; // in milliseconds this.maxConnectionIdleTime = 1 * 60 * 60 * 1000; // 1 hours in milliseconds this.languages = [] this.room = null; this.specialistId = null; console.log('EveAIChatWidget constructor called'); } connectedCallback() { console.log('connectedCallback called'); this.innerHTML = this.getTemplate(); this.messagesArea = this.querySelector('.messages-area'); this.questionInput = this.querySelector('.question-area textarea'); this.sendButton = this.querySelector('.send-icon'); this.languageSelect = this.querySelector('.language-select'); this.statusLine = this.querySelector('.status-line'); this.statusMessage = this.querySelector('.status-message'); this.connectionStatusIcon = this.querySelector('.connection-status-icon'); this.sendButton.addEventListener('click', () => this.handleSendMessage()); this.questionInput.addEventListener('keydown', (event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); // Prevent adding a new line this.handleSendMessage(); } }); if (this.areAllAttributesSet() && !this.socket) { console.log('Attributes already set in connectedCallback, initializing socket'); this.initializeSocket(); } } populateLanguageDropdown() { // Clear existing options this.languageSelect.innerHTML = ''; console.log(`languages for options: ${this.languages}`) // Populate with new options this.languages.forEach(lang => { const option = document.createElement('option'); option.value = lang; option.textContent = lang.toUpperCase(); if (lang === this.currentLanguage) { option.selected = true; } console.log(`Adding option for language: ${lang}`) this.languageSelect.appendChild(option); }); // Add event listener for language change this.languageSelect.addEventListener('change', (e) => { this.currentLanguage = e.target.value; // You might want to emit an event or update the backend about the language change }); } attributeChangedCallback(name, oldValue, newValue) { console.log(`attributeChangedCallback called: ${name} changed from ${oldValue} to ${newValue}`); this.updateAttributes(); if (this.areAllAttributesSet() && !this.socket) { console.log('All attributes set in attributeChangedCallback, initializing socket'); this.attributesSet = true; console.log('All attributes are set, populating language dropdown'); this.populateLanguageDropdown(); console.log('All attributes are set, initializing socket') this.initializeSocket(); } } updateAttributes() { this.tenantId = parseInt(this.getAttribute('tenant-id')); this.apiKey = this.getAttribute('api-key'); this.domain = this.getAttribute('domain'); this.language = this.getAttribute('language'); const languageAttr = this.getAttribute('languages'); this.languages = languageAttr ? languageAttr.split(',') : []; this.serverUrl = this.getAttribute('server-url'); this.currentLanguage = this.language; this.specialistId = this.getAttribute('specialist-id'); console.log('Updated attributes:', { tenantId: this.tenantId, apiKey: this.apiKey, domain: this.domain, language: this.language, currentLanguage: this.currentLanguage, languages: this.languages, serverUrl: this.serverUrl, specialistId: this.specialistId }); } areAllAttributesSet() { const tenantId = this.getAttribute('tenant-id'); const apiKey = this.getAttribute('api-key'); const domain = this.getAttribute('domain'); const language = this.getAttribute('language'); const languages = this.getAttribute('languages'); const serverUrl = this.getAttribute('server-url'); const specialistId = this.getAttribute('specialist-id') console.log('Checking if all attributes are set:', { tenantId, apiKey, domain, language, languages, serverUrl, specialistId }); return tenantId && apiKey && domain && language && languages && serverUrl && specialistId; } createLanguageDropdown() { const select = document.createElement('select'); select.id = 'languageSelect'; this.languages.forEach(lang => { const option = document.createElement('option'); option.value = lang; option.textContent = lang.toUpperCase(); if (lang === this.currentLanguage) { option.selected = true; } select.appendChild(option); }); select.addEventListener('change', (e) => { this.currentLanguage = e.target.value; // You might want to emit an event or update the backend about the language change }); return select; } initializeSocket() { if (this.socket) { console.log('Socket already initialized'); return; } console.log(`Initializing socket connection to Evie`); // Ensure apiKey is passed in the query parameters this.socket = io(this.serverUrl, { path: '/chat/socket.io/', transports: ['websocket', 'polling'], query: { tenantId: this.tenantId, apiKey: this.apiKey // Ensure apiKey is included here }, auth: { token: 'Bearer ' + this.apiKey // Ensure token is included here }, reconnectionAttempts: Infinity, // Infinite reconnection attempts reconnectionDelay: 5000, // Delay between reconnections timeout: 20000, // Connection timeout debug: true }); console.log(`Finished initializing socket connection to Evie`); this.socket.on('connect', (data) => { console.log('Socket connected OK'); console.log('Connect event data:', data); console.log('Connect event this:', this); this.setStatusMessage('Connected to EveAI.'); this.updateConnectionStatus(true); this.startHeartbeat(); if (data && data.room) { this.room = data.room; console.log(`Joined room: ${this.room}`); } else { console.log('Room information not received on connect'); } }); this.socket.on('authenticated', (data) => { console.log('Authenticated event received'); console.log('Authentication event data:', data); console.log('Authentication event this:', this); this.setStatusMessage('Authenticated.'); if (data && data.token) { this.jwtToken = data.token; } if (data && data.room) { this.room = data.room; console.log(`Confirmed room: ${this.room}`); } else { console.log('Room information not received on authentication'); } }); this.socket.on('connect_error', (err) => { console.error('Socket connection error:', err); this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.'); this.updateConnectionStatus(false); }); this.socket.on('connect_timeout', () => { console.error('Socket connection timeout'); this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.'); this.updateConnectionStatus(false); }); this.socket.on('disconnect', (reason) => { console.log('Socket disconnected: ', reason); if (reason === 'io server disconnect') { // Server disconnected the socket this.socket.connect(); // Attempt to reconnect } this.setStatusMessage('Disconnected from EveAI. Please refresh the page for further interaction.'); this.updateConnectionStatus(false); this.stopHeartbeat(); }); this.socket.on('reconnect_attempt', () => { console.log('Attempting to reconnect to the server...'); this.setStatusMessage('Attempting to reconnect...'); }); this.socket.on('reconnect', () => { console.log('Successfully reconnected to the server'); this.setStatusMessage('Reconnected to EveAI.'); this.updateConnectionStatus(true); this.startHeartbeat(); }); this.socket.on('bot_response', (data) => { console.log('Bot response received: ', data); console.log('data tenantId: ', data.tenantId) console.log('this tenantId: ', this.tenantId) if (data.tenantId === this.tenantId) { console.log('Starting task status check for:', data.taskId); setTimeout(() => this.startTaskCheck(data.taskId), 1000); this.setStatusMessage('Processing...'); } }); this.socket.on('task_status', (data) => { console.log('Task status received:', data); this.handleTaskStatus(data); }); } setStatusMessage(message) { this.statusMessage.textContent = message; } updateConnectionStatus(isConnected) { if (isConnected) { this.connectionStatusIcon.textContent = 'link'; this.connectionStatusIcon.classList.remove('status-disconnected'); this.connectionStatusIcon.classList.add('status-connected'); } else { this.connectionStatusIcon.textContent = 'link_off'; this.connectionStatusIcon.classList.remove('status-connected'); this.connectionStatusIcon.classList.add('status-disconnected'); } } startHeartbeat() { this.stopHeartbeat(); // Clear any existing interval this.heartbeatInterval = setInterval(() => { if (this.socket && this.socket.connected) { this.socket.emit('heartbeat'); this.idleTime += 30000; if (this.idleTime >= this.maxConnectionIdleTime) { this.socket.disconnect(); this.setStatusMessage('Disconnected due to inactivity.'); this.updateConnectionStatus(false); this.stopHeartbeat(); } } }, 30000); // Send a heartbeat every 30 seconds } stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } updateProgress() { if (!this.statusMessage.textContent) { this.statusMessage.textContent = 'Processing...'; } else { this.statusMessage.textContent += '.'; // Append a dot } } clearProgress() { this.statusMessage.textContent = ''; this.toggleSendButton(false); // Re-enable and revert send button to outlined version } checkTaskStatus(taskId) { this.updateProgress(); this.socket.emit('check_task_status', { task_id: taskId }); } getTemplate() { return `
Evie can make mistakes. Please double-check responses.
send
link_off
`; } addUserMessage(text) { const message = document.createElement('div'); message.classList.add('message', 'user'); message.innerHTML = `

${text}

`; this.messagesArea.appendChild(message); this.messagesArea.scrollTop = this.messagesArea.scrollHeight; } handleFeedback(feedback, interactionId) { // Send feedback to the backend console.log('handleFeedback called'); if (!this.socket) { console.error('Socket is not initialized'); return; } if (!this.jwtToken) { console.error('JWT token is not available'); return; } console.log('Sending message to backend'); console.log(`Feedback for ${interactionId}: ${feedback}`); this.socket.emit('feedback', { tenantId: this.tenantId, token: this.jwtToken, feedback, interactionId }); this.setStatusMessage('Feedback sent.'); } addBotMessage(text, interactionId, algorithm = 'default', citations = []) { const message = document.createElement('div'); message.classList.add('message', 'bot'); let content = marked.parse(text); let citationsHtml = citations.map(url => `${url}`).join('
'); let algorithmClass; switch (algorithm) { case 'RAG_TENANT': algorithmClass = 'fingerprint-rag-tenant'; break; case 'RAG_WIKIPEDIA': algorithmClass = 'fingerprint-rag-wikipedia'; break; case 'RAG_GOOGLE': algorithmClass = 'fingerprint-rag-google'; break; case 'LLM': algorithmClass = 'fingerprint-llm'; break; default: algorithmClass = ''; } message.innerHTML = `

${content}

${citationsHtml ? `

${citationsHtml}

` : ''}
fingerprint thumb_up_off_alt thumb_down_off_alt
`; this.messagesArea.appendChild(message); // Add event listeners for feedback buttons const thumbsUp = message.querySelector('i[data-feedback="up"]'); const thumbsDown = message.querySelector('i[data-feedback="down"]'); thumbsUp.addEventListener('click', () => this.toggleFeedback(thumbsUp, thumbsDown, 'up', interactionId)); thumbsDown.addEventListener('click', () => this.toggleFeedback(thumbsUp, thumbsDown, 'down', interactionId)); this.messagesArea.scrollTop = this.messagesArea.scrollHeight; } toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) { console.log('feedback called'); this.idleTime = 0; // Reset idle time if (feedback === 'up') { thumbsUp.textContent = 'thumb_up'; // Change to filled icon thumbsUp.classList.remove('outlined'); thumbsUp.classList.add('filled'); thumbsDown.textContent = 'thumb_down_off_alt'; // Keep the other icon outlined thumbsDown.classList.add('outlined'); thumbsDown.classList.remove('filled'); } else { thumbsDown.textContent = 'thumb_down'; // Change to filled icon thumbsDown.classList.remove('outlined'); thumbsDown.classList.add('filled'); thumbsUp.textContent = 'thumb_up_off_alt'; // Keep the other icon outlined thumbsUp.classList.add('outlined'); thumbsUp.classList.remove('filled'); } // Send feedback to the backend this.handleFeedback(feedback, interactionId); } handleSendMessage() { console.log('handleSendMessage called'); this.idleTime = 0; // Reset idle time const message = this.questionInput.value.trim(); if (message) { this.addUserMessage(message); this.questionInput.value = ''; this.sendMessageToBackend(message); this.toggleSendButton(true); // Disable and change send button to filled version } } startTaskCheck(taskId) { console.log('Emitting check_task_status for:', taskId); this.socket.emit('check_task_status', { task_id: taskId, token: this.jwtToken, tenantId: this.tenantId }); } handleTaskStatus(data) { console.log('Handling task status:', data); if (data.status === 'pending') { this.updateProgress(); // Continue checking setTimeout(() => this.startTaskCheck(data.taskId), 1000); } else if (data.status === 'success') { if (data.results) { this.addBotMessage( data.results.answer, data.interaction_id, 'RAG_TENANT', data.results.citations || [] ); } else { console.error('Missing results in task status response:', data); } this.clearProgress(); } else { console.error('Task error:', data); this.setStatusMessage('Failed to process message.'); } } sendMessageToBackend(message) { console.log('sendMessageToBackend called'); if (!this.socket) { console.error('Socket is not initialized'); return; } if (!this.jwtToken) { console.error('JWT token is not available'); return; } const selectedLanguage = this.languageSelect.value; // Updated message structure to match specialist execution format const messageData = { tenantId: parseInt(this.tenantId), token: this.jwtToken, specialistId: parseInt(this.specialistId), arguments: { language: selectedLanguage, query: message }, timezone: this.userTimezone }; console.log('Sending message to backend:', messageData); this.socket.emit('user_message', messageData); this.setStatusMessage('Processing started ...'); } toggleSendButton(isProcessing) { if (isProcessing) { this.sendButton.textContent = 'send'; // Filled send icon this.sendButton.classList.remove('outlined'); this.sendButton.classList.add('filled'); this.sendButton.classList.add('disabled'); // Add disabled class for styling this.sendButton.style.pointerEvents = 'none'; // Disable click events } else { this.sendButton.textContent = 'send'; // Outlined send icon this.sendButton.classList.add('outlined'); this.sendButton.classList.remove('filled'); this.sendButton.classList.remove('disabled'); // Remove disabled class this.sendButton.style.pointerEvents = 'auto'; // Re-enable click events } } } customElements.define('eveai-chat-widget', EveAIChatWidget);