- Allow the chat-widget to connect to multiple servers (e.g. development and production)
- Created a full session overview
This commit is contained in:
@@ -1,470 +0,0 @@
|
||||
class EveAIChatWidget extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['tenant-id', 'api-key', 'domain', 'language', 'languages'];
|
||||
}
|
||||
|
||||
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;
|
||||
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 = 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.currentLanguage = this.language;
|
||||
console.log('Updated attributes:', {
|
||||
tenantId: this.tenantId,
|
||||
apiKey: this.apiKey,
|
||||
domain: this.domain,
|
||||
language: this.language,
|
||||
currentLanguage: this.currentLanguage,
|
||||
languages: this.languages
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
console.log('Checking if all attributes are set:', {
|
||||
tenantId,
|
||||
apiKey,
|
||||
domain,
|
||||
language,
|
||||
languages
|
||||
});
|
||||
return tenantId && apiKey && domain && language && languages;
|
||||
}
|
||||
|
||||
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('https://evie.askeveai.com', {
|
||||
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
|
||||
});
|
||||
|
||||
console.log(`Finished initializing socket connection to Evie`);
|
||||
|
||||
this.socket.on('connect', (data) => {
|
||||
console.log('Socket connected OK');
|
||||
this.setStatusMessage('Connected to EveAI.');
|
||||
this.updateConnectionStatus(true);
|
||||
this.startHeartbeat();
|
||||
if (data.room) {
|
||||
this.room = data.room;
|
||||
console.log(`Joined room: ${this.room}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('authenticated', (data) => {
|
||||
console.log('Authenticated event received: ', data);
|
||||
this.setStatusMessage('Authenticated.');
|
||||
if (data.token) {
|
||||
this.jwtToken = data.token; // Store the JWT token received from the server
|
||||
}
|
||||
if (data.room) {
|
||||
this.room = data.room;
|
||||
console.log(`Confirmed room: ${this.room}`);
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
if (data.tenantId === this.tenantId) {
|
||||
console.log('Initial response received:', data);
|
||||
console.log('Task ID received:', data.taskId);
|
||||
this.checkTaskStatus(data.taskId);
|
||||
this.setStatusMessage('Processing...');
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('task_status', (data) => {
|
||||
console.log('Task status received:', data.status);
|
||||
console.log('Task ID received:', data.taskId);
|
||||
console.log('Citations type:', typeof data.citations, 'Citations:', data.citations);
|
||||
|
||||
if (data.status === 'pending') {
|
||||
this.updateProgress();
|
||||
setTimeout(() => this.checkTaskStatus(data.taskId), 1000); // Poll every second
|
||||
} else if (data.status === 'success') {
|
||||
this.addBotMessage(data.answer, data.interaction_id, data.algorithm, data.citations);
|
||||
this.clearProgress(); // Clear progress indicator when done
|
||||
} else {
|
||||
this.setStatusMessage('Failed to process message.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="chat-container">
|
||||
<div class="messages-area"></div>
|
||||
<div class="disclaimer">Evie can make mistakes. Please double-check responses.</div>
|
||||
<div class="question-area">
|
||||
<textarea placeholder="Type your message here..." rows="3"></textarea>
|
||||
<div class="right-side">
|
||||
<select class="language-select"></select>
|
||||
<i class="material-icons send-icon outlined">send</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-line">
|
||||
<i class="material-icons connection-status-icon">link_off</i>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
addUserMessage(text) {
|
||||
const message = document.createElement('div');
|
||||
message.classList.add('message', 'user');
|
||||
message.innerHTML = `<p>${text}</p>`;
|
||||
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 => `<a href="${url}" target="_blank">${url}</a>`).join('<br>');
|
||||
|
||||
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 = `
|
||||
<p>${content}</p>
|
||||
${citationsHtml ? `<p class="citations">${citationsHtml}</p>` : ''}
|
||||
<div class="message-icons">
|
||||
<i class="material-icons ${algorithmClass}">fingerprint</i>
|
||||
<i class="material-icons thumb-icon outlined" data-feedback="up" data-interaction-id="${interactionId}">thumb_up_off_alt</i>
|
||||
<i class="material-icons thumb-icon outlined" data-feedback="down" data-interaction-id="${interactionId}">thumb_down_off_alt</i>
|
||||
</div>
|
||||
`;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
console.log('Sending message to backend');
|
||||
this.socket.emit('user_message', {
|
||||
tenantId: this.tenantId,
|
||||
token: this.jwtToken,
|
||||
message,
|
||||
language: selectedLanguage,
|
||||
timezone: this.userTimezone
|
||||
});
|
||||
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);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// static/js/eveai-sdk.js
|
||||
class EveAI {
|
||||
constructor(tenantId, apiKey, domain, language, languages) {
|
||||
this.tenantId = tenantId;
|
||||
this.apiKey = apiKey;
|
||||
this.domain = domain;
|
||||
this.language = language;
|
||||
this.languages = languages;
|
||||
|
||||
console.log('EveAI constructor:', { tenantId, apiKey, domain, language, languages });
|
||||
}
|
||||
|
||||
initializeChat(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (container) {
|
||||
container.innerHTML = '<eveai-chat-widget></eveai-chat-widget>';
|
||||
customElements.whenDefined('eveai-chat-widget').then(() => {
|
||||
const chatWidget = container.querySelector('eveai-chat-widget');
|
||||
chatWidget.setAttribute('tenant-id', this.tenantId);
|
||||
chatWidget.setAttribute('api-key', this.apiKey);
|
||||
chatWidget.setAttribute('domain', this.domain);
|
||||
chatWidget.setAttribute('language', this.language);
|
||||
chatWidget.setAttribute('languages', this.languages);
|
||||
});
|
||||
} else {
|
||||
console.error('Container not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user