- Created a new eveai_chat plugin to support the new dynamic possibilities of the Specialists. Currently only supports standard Rag retrievers (i.e. no extra arguments).

This commit is contained in:
Josako
2024-11-26 13:35:29 +01:00
parent 7702a6dfcc
commit 07d89d204f
42 changed files with 1771 additions and 989 deletions

View File

@@ -0,0 +1,294 @@
/* eveai_chat.css */
:root {
--user-message-bg: #292929; /* Default user message background color */
--bot-message-bg: #1e1e1e; /* Default bot message background color */
--chat-bg: #1e1e1e; /* Default chat background color */
--status-line-color: #e9e9e9; /* Color for the status line text */
--status-line-bg: #1e1e1e; /* Background color for the status line */
--status-line-height: 30px; /* Fixed height for the status line */
--algorithm-color-rag-tenant: #0f0; /* Green for RAG_TENANT */
--algorithm-color-rag-wikipedia: #00f; /* Blue for RAG_WIKIPEDIA */
--algorithm-color-rag-google: #ff0; /* Yellow for RAG_GOOGLE */
--algorithm-color-llm: #800080; /* Purple for RAG_LLM */
/*--font-family: 'Arial, sans-serif'; !* Default font family *!*/
--font-family: 'Segoe UI, Roboto, Cantarell, Noto Sans, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol';
--font-color: #e9e9e9; /* Default font color */
--user-message-font-color: #e9e9e9; /* User message font color */
--bot-message-font-color: #e9e9e9; /* Bot message font color */
--input-bg: #292929; /* Input background color */
--input-border: #ccc; /* Input border color */
--input-text-color: #e9e9e9; /* Input text color */
--button-color: #007bff; /* Button text color */
/* Variables for hyperlink backgrounds */
--link-bg: #1e1e1e; /* Default background color for hyperlinks */
--link-hover-bg: #1e1e1e; /* Background color on hover for hyperlinks */
--link-color: #dec981; /* Default text color for hyperlinks */
--link-hover-color: #D68F53; /* Text color on hover for hyperlinks */
/* New scrollbar variables */
--scrollbar-bg: #292929; /* Background color for the scrollbar track */
--scrollbar-thumb: #4b4b4b; /* Color for the scrollbar thumb */
--scrollbar-thumb-hover: #dec981; /* Color for the thumb on hover */
--scrollbar-thumb-active: #D68F53; /* Color for the thumb when active (dragged) */
/* Thumb colors */
--thumb-icon-outlined: #4b4b4b;
--thumb-icon-filled: #e9e9e9;
/* Connection Status colors */
--status-connected-color: #28a745; /* Green color for connected status */
--status-disconnected-color: #ffc107; /* Orange color for disconnected status */
}
/* Connection status styles */
.connection-status-icon {
vertical-align: middle;
font-size: 24px;
margin-right: 8px;
}
.status-connected {
color: var(--status-connected-color);
}
.status-disconnected {
color: var(--status-disconnected-color);
}
/* Custom scrollbar styles */
.messages-area::-webkit-scrollbar {
width: 12px; /* Width of the scrollbar */
}
.messages-area::-webkit-scrollbar-track {
background: var(--scrollbar-bg); /* Background color for the track */
}
.messages-area::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb); /* Color of the thumb */
border-radius: 10px; /* Rounded corners for the thumb */
border: 3px solid var(--scrollbar-bg); /* Space around the thumb */
}
.messages-area::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover); /* Color when hovering over the thumb */
}
.messages-area::-webkit-scrollbar-thumb:active {
background-color: var(--scrollbar-thumb-active); /* Color when active (dragging) */
}
/* For Firefox */
.messages-area {
scrollbar-width: thin; /* Make scrollbar thinner */
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg); /* Thumb and track colors */
}
/* General Styles */
.chat-container {
display: flex;
flex-direction: column;
height: 75vh;
/*max-height: 100vh;*/
max-width: 600px;
margin: auto;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
background-color: var(--chat-bg);
font-family: var(--font-family); /* Apply the default font family */
color: var(--font-color); /* Apply the default font color */
}
.disclaimer {
font-size: 0.7em;
text-align: right;
padding: 5px 20px 5px 5px;
margin-bottom: 5px;
}
.messages-area {
flex: 1;
overflow-y: auto;
padding: 10px;
background-color: var(--bot-message-bg);
}
.message {
max-width: 90%;
margin-bottom: 10px;
padding: 10px;
border-radius: 15px;
font-size: 1rem;
}
.message.user {
margin-left: auto;
background-color: var(--user-message-bg);
color: var(--user-message-font-color); /* Apply user message font color */
}
.message.bot {
background-color: var(--bot-message-bg);
color: var(--bot-message-font-color); /* Apply bot message font color */
}
.message-icons {
display: flex;
align-items: center;
}
/* Scoped styles for thumb icons */
.thumb-icon.outlined {
color: var(--thumb-icon-outlined); /* Color for outlined state */
}
.thumb-icon.filled {
color: var(--thumb-icon-filled); /* Color for filled state */
}
/* Default styles for material icons */
.material-icons {
font-size: 24px;
vertical-align: middle;
cursor: pointer;
}
.question-area {
display: flex;
flex-direction: row;
align-items: center;
background-color: var(--user-message-bg);
padding: 10px;
}
.language-select-container {
width: 100%;
margin-bottom: 10px; /* Spacing between the dropdown and the textarea */
}
.language-select {
width: 100%;
margin-bottom: 5px; /* Space between the dropdown and the send button */
padding: 8px;
border-radius: 5px;
border: 1px solid var(--input-border);
background-color: var(--input-bg);
color: var(--input-text-color);
font-size: 1rem;
}
.question-area textarea {
flex: 1;
border: none;
padding: 10px;
border-radius: 15px;
background-color: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--input-text-color);
font-family: var(--font-family); /* Apply the default font family */
font-size: 1rem;
resize: vertical;
min-height: 60px;
max-height: 150px;
overflow-y: auto;
margin-right: 10px; /* Space between textarea and right-side container */
}
.right-side {
display: flex;
flex-direction: column;
align-items: center;
}
.question-area button {
background: none;
border: none;
cursor: pointer;
color: var(--button-color);
}
/* Styles for the send icon */
.send-icon {
font-size: 24px;
color: var(--button-color);
cursor: pointer;
}
.send-icon.disabled {
color: grey; /* Color for the disabled state */
cursor: not-allowed; /* Change cursor to indicate disabled state */
}
/* New CSS for the status-line */
.status-line {
height: var(--status-line-height); /* Fixed height for the status line */
padding: 5px 10px;
background-color: var(--status-line-bg); /* Background color */
color: var(--status-line-color); /* Text color */
font-size: 0.9rem; /* Slightly smaller font size */
text-align: center; /* Centered text */
border-top: 1px solid #ccc; /* Subtle top border */
display: flex;
align-items: center;
justify-content: flex-start;
}
/* Algorithm-specific colors for fingerprint icon */
.fingerprint-rag-tenant {
color: var(--algorithm-color-rag-tenant);
}
.fingerprint-rag-wikipedia {
color: var(--algorithm-color-rag-wikipedia);
}
.fingerprint-rag-google {
color: var(--algorithm-color-rag-google);
}
.fingerprint-llm {
color: var(--algorithm-color-llm);
}
/* Styling for citation links */
.citations a {
background-color: var(--link-bg); /* Apply default background color */
color: var(--link-color); /* Apply default link color */
padding: 2px 4px; /* Add padding for better appearance */
border-radius: 3px; /* Add slight rounding for a modern look */
text-decoration: none; /* Remove default underline */
transition: background-color 0.3s, color 0.3s; /* Smooth transition for hover effects */
}
.citations a:hover {
background-color: var(--link-hover-bg); /* Background color on hover */
color: var(--link-hover-color); /* Text color on hover */
}
/* Media queries for responsiveness */
@media (max-width: 768px) {
.chat-container {
max-width: 90%; /* Reduce max width on smaller screens */
}
}
@media (max-width: 480px) {
.chat-container {
max-width: 95%; /* Further reduce max width on very small screens */
}
.question-area input {
font-size: 0.9rem; /* Adjust input font size for smaller screens */
}
.status-line {
font-size: 0.8rem; /* Adjust status line font size for smaller screens */
}
}

View File

@@ -0,0 +1,696 @@
class EveAIChatWidget extends HTMLElement {
static get observedAttributes() {
return [
'tenant-id',
'session-token',
'language',
'languages',
'specialist-id',
'server-url'
];
}
constructor() {
super();
// Networking attributes
this.socket = null; // Initialize socket to null
this.room = null;
this.lastRoom = null; // Store last known room
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 hour in milliseconds
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
// EveAI specific attributes
this.languages = []
this.currentLanguage = null;
this.specialistId = null;
console.log('EveAIChatWidget constructor called');
// Bind methods to ensure correct 'this' context
this.handleSendMessage = this.handleSendMessage.bind(this);
this.handleTokenUpdate = this.handleTokenUpdate.bind(this);
this.updateAttributes = this.updateAttributes.bind(this);
}
connectedCallback() {
console.log('Chat Widget Connected');
this.innerHTML = this.getTemplate();
this.setupElements()
this.populateLanguageDropdown()
this.addEventListeners()
if (this.areAllAttributesSet()) {
console.log('All attributes are set, initializing socket');
this.initializeSocket();
} else {
console.warn('Not all required attributes are set yet');
}
}
setupElements() {
// Centralizes element querying
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');
}
addEventListeners() {
// Centralized event listener setup
this.sendButton.addEventListener('click', this.handleSendMessage);
this.questionInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
this.handleSendMessage();
}
});
}
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(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
// Handle token updates specially
if (name === 'session-token' && oldValue !== newValue) {
this.updateAttributes();
if (newValue) {
console.log('Received new session token');
this.sessionToken = newValue;
// If socket exists, reconnect with new token
if (this.socket) {
this.socket.disconnect();
this.initializeSocket();
} else if (this.areAllAttributesSet()) {
// Initialize socket if all other attributes are ready
this.initializeSocket();
}
}
return;
}
if (name === 'languages' || name === 'language') {
this.updateAttributes();
this.populateLanguageDropdown();
return;
}
this.updateAttributes();
}
updateAttributes() {
this.tenantId = parseInt(this.getAttribute('tenant-id'));
this.sessionToken = this.getAttribute('session-token');
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,
sessionToken: this.sessionToken,
language: this.language,
currentLanguage: this.currentLanguage,
languages: this.languages,
serverUrl: this.serverUrl,
specialistId: this.specialistId
});
}
areAllAttributesSet() {
console.log('Checking if all attributes are set:', {
tenantId: this.tenantId,
sessionToken: this.sessionToken,
language: this.language,
languages: this.languages,
serverUrl: this.serverUrl,
specialistId: this.specialistId
});
const requiredAttributes = [
'tenant-id',
'session-token',
'language',
'languages',
'specialist-id',
'server-url'
];
return requiredAttributes.every(attr => this.getAttribute(attr));
}
handleTokenUpdate(newToken) {
if (this.socket && this.socket.connected) {
console.log('Updating socket connection with new token');
// Emit token update event to server
this.socket.emit('update_token', { token: newToken });
} else if (newToken && !this.socket) {
// If we have a new token but no socket, try to initialize
this.initializeSocket();
}
}
initializeSocket() {
console.log(`Initializing socket connection to Evie at ${this.serverUrl}`);
if (this.socket) {
console.log('Socket already initialized');
return;
}
if (!this.sessionToken) {
console.error('Cannot initialize socket without session token');
return;
}
this.socket = io(this.serverUrl, {
path: '/socket.io/',
transports: ['websocket'],
query: { // Change from auth to query
token: this.sessionToken
},
reconnectionAttempts: 5, // Infinite reconnection attempts
reconnectionDelay: 5000, // Delay between reconnections
timeout: 20000, // Connection timeout
});
if (!this.socket) {
console.error('Error initializing socket')
} else {
console.log('Socket initialized')
}
this.setupSocketEventHandlers();
}
setupSocketEventHandlers() {
// connect handler --------------------------------------------------------
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;
this.lastRoom = this.room;
console.log(`Joined room: ${this.room}`);
} else {
console.log('Room information not received on connect');
}
});
// authenticated handler --------------------------------------------------
this.socket.on('authenticated', (data) => {
console.log('Authenticated event received');
this.setStatusMessage('Authenticated.');
if (data?.room) {
this.room = data.room;
this.lastRoom = this.room;
console.log(`Confirmed room: ${this.room}`);
} else {
console.log('Room information not received on authentication');
}
});
// Room join handler ------------------------------------------------------
this.socket.on('room_join', (data) => {
console.log('Room join event received:', data);
if (data?.room) {
this.room = data.room;
this.lastRoom = this.room;
console.log(`Joined room: ${this.room}`);
}
});
// connect-error handler --------------------------------------------------
this.socket.on('connect_error', (err) => {
console.error('Socket connection error:', err);
this.setStatusMessage('Connection Error: EveAI Chat Widget needs further configuration by site administrator.');
this.updateConnectionStatus(false);
});
// connect-timeout handler ------------------------------------------------
this.socket.on('connect_timeout', () => {
console.error('Socket connection timeout');
this.setStatusMessage('Connection Timeout: EveAI Chat Widget needs further configuration by site administrator.');
this.updateConnectionStatus(false);
});
// disconnect handler -----------------------------------------------------
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.room = null;
});
// Token related handlers -------------------------------------------------
this.socket.on('token_expired', () => {
console.log('Token expired');
this.setStatusMessage('Session expired. Please refresh the page.');
this.updateConnectionStatus(false);
});
// reconnect_attempt handler ----------------------------------------------
this.socket.on('reconnect_attempt', (attemptNumber) => {
console.log(`Reconnection attempt ${attemptNumber}`);
this.setStatusMessage(`Reconnecting... (Attempt ${attemptNumber})`);
this.reconnectAttempts = attemptNumber;
});
// reconnect handler ------------------------------------------------------
this.socket.on('reconnect', () => {
console.log('Successfully reconnected to the server');
this.setStatusMessage('Reconnected to EveAI.');
this.updateConnectionStatus(true);
this.startHeartbeat();
});
// reconnect failed -------------------------------------------------------
this.socket.on('reconnect_failed', () => {
console.log('Reconnection failed');
this.setStatusMessage('Unable to reconnect. Please refresh the page.');
this.handleReconnectFailure();
});
// room rejoin result -----------------------------------------------------
this.socket.on('room_rejoin_result', (response) => {
if (response.success) {
console.log('Successfully rejoined room');
this.room = response.room;
this.setStatusMessage('Reconnected successfully.');
} else {
console.error('Failed to rejoin room');
this.handleRoomRejoinFailure();
}
});
// bot_response handler ---------------------------------------------------
this.socket.on('bot_response', (data) => {
console.log('Bot response received: ', data);
if (data.tenantId === this.tenantId && data?.room === this.room) {
setTimeout(() => this.startTaskCheck(data.taskId), 1000);
this.setStatusMessage('Processing...');
} else {
console.log('Received message for different room or tenant, ignoring');
}
});
// task_status handler ----------------------------------------------------
this.socket.on('task_status', (data) => {
console.log('Task status received:', data);
if (!this.room) {
console.log('No room assigned, cannot process task status');
return;
}
this.handleTaskStatus(data);
});
// Feedback handler -------------------------------------------------------
this.socket.on('feedback_received', (data) => {
if (data?.room === this.room) {
this.setStatusMessage(data.status === 'success' ? 'Feedback recorded.' : 'Failed to record feedback.');
}
});
}
attemptRoomRejoin() {
console.log(`Attempting to rejoin room: ${this.lastRoom}`);
this.socket.emit('rejoin_room', {
token: this.sessionToken,
tenantId: this.tenantId,
previousRoom: this.lastRoom,
timestamp: Date.now()
});
}
handleReconnectFailure() {
this.room = null;
this.lastRoom = null;
this.reconnectAttempts = 0;
this.updateConnectionStatus(false);
// Optionally reload the widget
if (confirm('Connection lost. Would you like to refresh the chat?')) {
window.location.reload();
}
}
handleRoomRejoinFailure() {
// Clear room state
this.room = null;
this.lastRoom = null;
// Request new room
this.socket.emit('request_new_room', {
token: this.sessionToken,
tenantId: this.tenantId
});
}
clearRoomState() {
// Use when intentionally leaving/clearing a room
this.room = null;
this.lastRoom = null;
this.reconnectAttempts = 0;
}
handleAuthError(error) {
console.error('Authentication error:', error);
this.setStatusMessage('Authentication failed. Please refresh the page.');
this.updateConnectionStatus(false);
if (this.socket) {
this.socket.disconnect();
}
}
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.validateRoom()) {
console.log("No valid room to handle feedback")
return;
}
console.log(`Sending feedback for ${interactionId}: ${feedback}`);
this.socket.emit('feedback', { tenant_id: this.tenantId, token: this.sessionToken, feedback, interactionId, room: this.room });
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
if (!this.socket?.connected) {
console.error('Cannot send message: socket not connected');
this.setStatusMessage('Not connected to server. Please try again.');
return;
}
if (!this.room) {
console.error('Cannot send message: no room assigned');
this.setStatusMessage('Connection not ready. Please wait...');
// Try to rejoin room if we have a last known room
if (this.lastRoom) {
this.attemptRoomRejoin();
}
return;
}
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) {
if (!this.validateRoom()) {
console.error('Cannot check task status: no room assigned');
return;
}
console.log('Emitting check_task_status for:', taskId);
this.socket.emit('check_task_status', {
task_id: taskId,
token: this.sessionToken,
tenant_id: this.tenantId,
room: this.room
});
}
handleTaskStatus(data) {
if (data.status === 'pending') {
this.updateProgress();
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) {
if (!this.socket || !this.room) {
console.error('Cannot send message: socket or room not available');
return;
}
if (!this.validateRoom()) {
return;
}
const selectedLanguage = this.languageSelect.value;
const messageData = {
tenant_id: parseInt(this.tenantId),
token: this.sessionToken,
specialist_id: parseInt(this.specialistId),
arguments: {
language: selectedLanguage,
query: message
},
timezone: this.userTimezone,
room: this.room
};
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
}
}
validateRoom() {
if (!this.room) {
console.error('No room assigned');
this.setStatusMessage('Connection not ready. Please wait...');
// Try to rejoin room if we have a last known room
if (this.lastRoom) {
this.attemptRoomRejoin();
}
return false;
}
return true;
}
}
customElements.define('eveai-chat-widget', EveAIChatWidget);

View File

@@ -0,0 +1,167 @@
class EveAI {
constructor(config) {
// Required parameters
this.tenantId = config.tenantId;
// Chat configuration
this.language = config.language || 'en';
this.languages = config.languages?.split(',') || ['en'];
this.specialistId = config.specialistId;
// Server Configuration
this.socketUrl = config.socketUrl || 'https://chat.askeveai.com';
this.authUrl = config.authUrl || 'https://api.askeveai.com';
this.proxyUrl = config.proxyUrl; // URL for auth proxy (WP or standalone)
this.wpRestNamespace = 'eveai/v1'; // This should match the PHP constant
this.wpRestUrl = `${config.wpBaseUrl || '/wp-json'}/${this.wpRestNamespace}`;
// Initialize token management
this.tokenManager = new EveAITokenManager({
proxyUrl: this.proxyUrl,
onTokenChange: this.handleTokenChange.bind(this),
onError: this.handleAuthError.bind(this)
});
this.chatWidget = null;
}
async initialize(containerId) {
try {
if (!containerId) {
throw new Error('Container ID is required');
}
console.log('Starting initialization with settings:', {
tenantId: this.tenantId,
wpRestUrl: this.wpRestUrl
});
// Get the WordPress nonce
const wpNonce = window.eveaiWP?.nonce;
if (!wpNonce) {
throw new Error('WordPress nonce not found');
}
// Use WordPress REST API endpoint instead of direct API call
const response = await fetch(`${this.wpRestUrl}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpNonce,
},
credentials: 'same-origin', // Important for WP cookie handling
body: JSON.stringify({
tenant_id: this.tenantId
})
});
console.log('Token request response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('Auth error response:', errorText);
throw new Error('Authentication failed');
}
const { access_token, expires_in } = await response.json();
console.log('Token received:', access_token);
console.log('Token Expiry:', expires_in);
// Store token and expiry
this.sessionToken = access_token;
this.tokenExpiry = Date.now() + (expires_in * 1000);
// Initialize token refresh timer
this.setupTokenRefresh(expires_in);
return this.initializeChat(containerId, access_token);
} catch (error) {
console.error('Full initialization error:', error);
throw error;
}
}
setupTokenRefresh(expiresIn) {
// Set up refresh 5 minutes before expiry
const refreshTime = (expiresIn - 300) * 1000; // Convert to milliseconds
setTimeout(() => this.refreshToken(), refreshTime);
}
async refreshToken() {
try {
const response = await fetch(`${this.wpRestUrl}/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.sessionToken}`
}
});
if (response.ok) {
const { access_token, expires_in } = await response.json();
this.sessionToken = access_token;
this.tokenExpiry = Date.now() + (expires_in * 1000);
// Update token in chat widget
if (this.chatWidget) {
this.chatWidget.setAttribute('session-token', access_token);
}
// Setup next refresh
this.setupTokenRefresh(expires_in);
} else {
console.error('Token refresh failed');
}
} catch (error) {
console.error('Token refresh error:', error);
}
}
async initializeChat(containerId) {
const container = document.getElementById(containerId);
if (!container) {
throw new Error('Container not found');
}
// Create chat widget with all necessary attributes
const chatWidget = document.createElement('eveai-chat-widget');
// Set all required attributes
const attributes = {
'tenant-id': this.tenantId,
'session-token': this.sessionToken,
'language': this.language,
'languages': this.languages.join(','),
'specialist-id': this.specialistId,
'server-url': this.socketUrl
};
console.log('Setting widget attributes:', attributes);
Object.entries(attributes).forEach(([attr, value]) => {
if (value === null || value === undefined) {
console.warn(`Warning: ${attr} is ${value}`);
}
chatWidget.setAttribute(attr, value);
});
container.appendChild(chatWidget);
this.chatWidget = chatWidget;
return chatWidget;
}
handleTokenChange(newToken) {
if (this.chatWidget) {
this.chatWidget.setAttribute('session-token', newToken);
}
}
handleAuthError(error) {
if (this.chatWidget) {
this.chatWidget.handleAuthError(error);
}
}
}
// Make available globally
// window.EveAI = EveAI;

View File

@@ -0,0 +1,69 @@
// eveai-token-manager.js
class EveAITokenManager extends EventTarget {
constructor() {
super();
this.token = null;
this.checkInterval = null;
this.isRefreshing = false;
this.refreshThreshold = 60; // Refresh token if less than 60s remaining
}
// Initialize with a token
async initialize(token) {
this.token = token;
this.startTokenCheck();
this.dispatchEvent(new CustomEvent('tokenChanged', { detail: { token } }));
}
// Start periodic token verification
startTokenCheck() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
}
this.checkInterval = setInterval(async () => {
await this.verifyAndRefreshToken();
}, 5000); // Check every 5 seconds
}
// Verify token and refresh if needed
async verifyAndRefreshToken() {
if (!this.token || this.isRefreshing) return;
try {
const response = await fetch(`${this.proxyUrl}/verify`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
if (!response.ok) {
throw new Error('Token verification failed');
}
const data = await response.json();
if (data.expires_in < this.refreshThreshold) {
await this.refreshToken();
}
} catch (error) {
this.handleTokenError(error);
}
}
// Handle any token errors
handleTokenError(error) {
this.dispatchEvent(new CustomEvent('tokenError', { detail: { error } }));
this.token = null;
if (this.checkInterval) {
clearInterval(this.checkInterval);
}
}
// Clean up
destroy() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
}
this.token = null;
}
}