From c5370c8026d2e3b0484665e7a5b21f7783f9ff09 Mon Sep 17 00:00:00 2001 From: Josako Date: Fri, 14 Jun 2024 15:05:46 +0200 Subject: [PATCH] Improvements on the chat UI, prepare for supporting multiple models, and adding feedback to interactions. --- common/models/interaction.py | 2 +- eveai_chat/__init__.py | 2 +- eveai_chat/socket_handlers/chat_handler.py | 83 +++++++---- eveai_chat_workers/tasks.py | 1 + public/chat_ae.html | 2 +- static/css/eveai-chat-style.css | 145 +++++++++++++++++-- static/js/eveai-chat-widget.js | 155 +++++++++++++++------ static/js/eveai-sdk.js | 6 - 8 files changed, 301 insertions(+), 95 deletions(-) diff --git a/common/models/interaction.py b/common/models/interaction.py index ad81513..08773dc 100644 --- a/common/models/interaction.py +++ b/common/models/interaction.py @@ -25,7 +25,7 @@ class Interaction(db.Model): answer = db.Column(db.Text, nullable=True) algorithm_used = db.Column(db.String(20), nullable=True) language = db.Column(db.String(2), nullable=False) - appreciation = db.Column(db.Integer, nullable=True, default=100) + appreciation = db.Column(db.Integer, nullable=True) # Timing information question_at = db.Column(db.DateTime, nullable=False) diff --git a/eveai_chat/__init__.py b/eveai_chat/__init__.py index bcd8d04..4b34d7f 100644 --- a/eveai_chat/__init__.py +++ b/eveai_chat/__init__.py @@ -27,7 +27,7 @@ def create_app(config_file=None): init_celery(app.celery, app) # Register Blueprints - register_blueprints(app) + # register_blueprints(app) @app.route('/ping') def ping(): diff --git a/eveai_chat/socket_handlers/chat_handler.py b/eveai_chat/socket_handlers/chat_handler.py index a5aaf38..24fbc33 100644 --- a/eveai_chat/socket_handlers/chat_handler.py +++ b/eveai_chat/socket_handlers/chat_handler.py @@ -9,6 +9,7 @@ from common.extensions import socketio, kms_client, db from common.models.user import Tenant from common.models.interaction import Interaction from common.utils.celery_utils import current_celery +from common.utils.database import Database @socketio.on('connect') @@ -35,7 +36,9 @@ def handle_connect(): session['session_id'] = str(uuid.uuid4()) # Communicate connection to client + current_app.logger.debug(f'SocketIO: Connection handling sending status to client for tenant {tenant_id}') emit('connect', {'status': 'Connected', 'tenant_id': tenant_id}) + current_app.logger.debug(f'SocketIO: Connection handling sending authentication token to client') emit('authenticated', {'token': token}) # Emit custom event with the token current_app.logger.debug(f'SocketIO: Connection handling sent token to client for tenant {tenant_id}') except Exception as e: @@ -55,25 +58,8 @@ def handle_message(data): try: current_app.logger.debug(f"SocketIO: Message handling received message from tenant {data['tenantId']}: " f"{data['message']} with token {data['token']}") - token = data.get('token') - if not token: - raise Exception("Missing token") - # decoded_token = decode_token(token.split(" ")[1]) # remove "Bearer " - decoded_token = decode_token(token) - if not decoded_token: - raise Exception("Invalid token") - current_app.logger.debug(f"SocketIO: Message handling decoded token: {decoded_token}") - - token_sub = decoded_token.get('sub') - - current_tenant_id = token_sub.get('tenant_id') - if not current_tenant_id: - raise Exception("Missing tenant_id") - - current_api_key = token_sub.get('api_key') - if not current_api_key: - raise Exception("Missing api_key") + current_tenant_id = validate_incoming_data(data) # Offload actual processing of question task = current_celery.send_task('ask_question', queue='llm_interactions', args=[ @@ -128,19 +114,30 @@ def check_task_status(data): @socketio.on('feedback') def handle_feedback(data): - interaction_id = data.get('interaction_id') - feedback = data.get('feedback') # 'up' or 'down' - # Store feedback in the database associated with the interaction_id - interaction = Interaction.query.get_or_404(interaction_id) - interaction.feedback = 0 if feedback == 'down' else 1 try: - db.session.commit() - emit('feedback_received', {'status': 'success', 'interaction_id': interaction_id}) - except SQLAlchemyError as e: - current_app.logger.error(f'SocketIO: Feedback handling failed: {e}') - db.session.rollback() - emit('feedback_received', {'status': 'Could not register feedback', 'interaction_id': interaction_id}) - raise e + current_app.logger.debug(f'SocketIO: Feedback handling received feedback with data: {data}') + + current_tenant_id = validate_incoming_data(data) + + interaction_id = data.get('interactionId') + feedback = data.get('feedback') # 'up' or 'down' + + Database(current_tenant_id).switch_schema() + + interaction = Interaction.query.get_or_404(interaction_id) + current_app.logger.debug(f'Processing feedback for interaction: {interaction}') + interaction.appreciation = 0 if feedback == 'down' else 100 + try: + db.session.commit() + emit('feedback_received', {'status': 'success', 'interaction_id': interaction_id}) + except SQLAlchemyError as e: + current_app.logger.error(f'SocketIO: Feedback handling failed: {e}') + db.session.rollback() + emit('feedback_received', {'status': 'Could not register feedback', 'interaction_id': interaction_id}) + raise e + except Exception as e: + current_app.logger.debug(f'SocketIO: Feedback handling failed: {e}') + disconnect() def validate_api_key(tenant_id, api_key): @@ -148,3 +145,29 @@ def validate_api_key(tenant_id, api_key): decrypted_api_key = kms_client.decrypt_api_key(tenant.encrypted_chat_api_key) return decrypted_api_key == api_key + + +def validate_incoming_data(data): + token = data.get('token') + if not token: + raise Exception("Missing token") + + decoded_token = decode_token(token) + if not decoded_token: + raise Exception("Invalid token") + + token_sub = decoded_token.get('sub') + + if not token_sub: + raise Exception("Missing token subject") + tenant_id = token_sub.get('tenant_id') + + current_tenant_id = token_sub.get('tenant_id') + if not current_tenant_id: + raise Exception("Missing tenant_id") + + current_api_key = token_sub.get('api_key') + if not current_api_key: + raise Exception("Missing api_key") + + return current_tenant_id diff --git a/eveai_chat_workers/tasks.py b/eveai_chat_workers/tasks.py index 68071cf..fae2b83 100644 --- a/eveai_chat_workers/tasks.py +++ b/eveai_chat_workers/tasks.py @@ -84,6 +84,7 @@ def ask_question(tenant_id, question, language, session_id): new_interaction = Interaction() new_interaction.question = question new_interaction.language = language + new_interaction.appreciation = None new_interaction.chat_session_id = chat_session.id new_interaction.question_at = dt.now(tz.utc) new_interaction.algorithm_used = current_app.config['INTERACTION_ALGORITHMS']['RAG_TENANT']['name'] diff --git a/public/chat_ae.html b/public/chat_ae.html index 3dffd20..22b7aaa 100644 --- a/public/chat_ae.html +++ b/public/chat_ae.html @@ -4,12 +4,12 @@ Chat Client AE - +
diff --git a/static/css/eveai-chat-style.css b/static/css/eveai-chat-style.css index d9f83fe..a14e742 100644 --- a/static/css/eveai-chat-style.css +++ b/static/css/eveai-chat-style.css @@ -1,17 +1,74 @@ /* eveai_chat.css */ :root { - --user-message-bg: #d1e7dd; /* Default user message background color */ - --bot-message-bg: #ffffff; /* Default bot message background color */ - --chat-bg: #f8f9fa; /* Default chat background color */ - --algorithm-color-default: #ccc; /* Default algorithm indicator color */ - --algorithm-color-alg1: #f00; /* Algorithm 1 color */ - --algorithm-color-alg2: #0f0; /* Algorithm 2 color */ - --algorithm-color-alg3: #00f; /* Algorithm 3 color */ - --status-line-color: #6c757d; /* Color for the status line text */ - --status-line-bg: #e9ecef; /* Background color for the status line */ + --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-rag-llm: #800080; /* Purple for RAG_LLM */ + + /*--font-family: 'Arial, sans-serif'; !* Default font family *!*/ + --font-family: 'ui-sans-serif, -apple-system, system-ui, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, Helvetica, Apple Color Emoji, Arial, 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; } +/* 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; @@ -23,6 +80,8 @@ 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 */ } .messages-area { @@ -42,10 +101,12 @@ .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 { @@ -53,8 +114,19 @@ align-items: center; } -.message-icons i { - margin-left: 5px; +/* 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; } @@ -71,13 +143,27 @@ padding: 10px; border-radius: 15px; margin-right: 10px; + background-color: var(--input-bg); /* Apply input background color */ + border: 1px solid var(--input-border); /* Apply input border color */ + color: var(--input-text-color); /* Apply input text color */ } .question-area button { background: none; border: none; cursor: pointer; - color: #007bff; + color: var(--button-color); +} + +/* Styles for the send icon */ +.send-icon { + font-size: 24px; /* Size of the icon */ + color: var(--button-color); /* Color of the icon */ +} + +.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 */ @@ -94,6 +180,38 @@ justify-content: center; } +/* 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-rag-llm { + color: var(--algorithm-color-rag-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 { @@ -114,3 +232,6 @@ font-size: 0.8rem; /* Adjust status line font size for smaller screens */ } } + + + diff --git a/static/js/eveai-chat-widget.js b/static/js/eveai-chat-widget.js index ef12da9..c67b89f 100644 --- a/static/js/eveai-chat-widget.js +++ b/static/js/eveai-chat-widget.js @@ -16,9 +16,10 @@ class EveAIChatWidget extends HTMLElement { this.innerHTML = this.getTemplate(); this.messagesArea = this.querySelector('.messages-area'); this.questionInput = this.querySelector('.question-area input'); + this.sendButton = this.querySelector('.send-icon'); this.statusLine = this.querySelector('.status-line'); - this.querySelector('.question-area button').addEventListener('click', () => this.handleSendMessage()); + this.sendButton.addEventListener('click', () => this.handleSendMessage()); this.questionInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { this.handleSendMessage(); @@ -34,12 +35,6 @@ class EveAIChatWidget extends HTMLElement { attributeChangedCallback(name, oldValue, newValue) { console.log(`attributeChangedCallback called: ${name} changed from ${oldValue} to ${newValue}`); this.updateAttributes(); - console.log('Current attributes:', { - tenantId: this.getAttribute('tenant-id'), - apiKey: this.getAttribute('api-key'), - domain: this.getAttribute('domain'), - language: this.getAttribute('language') - }); if (this.areAllAttributesSet() && !this.socket) { console.log('All attributes set in attributeChangedCallback, initializing socket'); @@ -49,7 +44,6 @@ class EveAIChatWidget extends HTMLElement { } updateAttributes() { - console.log('Updating attributes:'); this.tenantId = this.getAttribute('tenant-id'); this.apiKey = this.getAttribute('api-key'); this.domain = this.getAttribute('domain'); @@ -73,7 +67,7 @@ class EveAIChatWidget extends HTMLElement { domain, language }); - return tenantId && apiKey && domain; + return tenantId && apiKey && domain && language; } initializeSocket() { @@ -83,7 +77,7 @@ class EveAIChatWidget extends HTMLElement { } if (!this.domain || this.domain === 'null') { console.error('Domain attribute is missing or invalid'); - this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.') + this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.'); return; } console.log(`Initializing socket connection to ${this.domain}`); @@ -101,14 +95,16 @@ class EveAIChatWidget extends HTMLElement { } }); + console.log(`Finished initializing socket connection to ${this.domain}`); + this.socket.on('connect', (data) => { - console.log('Socket connected'); - this.setStatusMessage('Connected to EveAI.') + console.log('Socket connected OK'); + this.setStatusMessage('Connected to EveAI.'); }); this.socket.on('authenticated', (data) => { console.log('Authenticated event received: ', data); - this.setStatusMessage('Authenticated.') + this.setStatusMessage('Authenticated.'); if (data.token) { this.jwtToken = data.token; // Store the JWT token received from the server } @@ -116,31 +112,31 @@ class EveAIChatWidget extends HTMLElement { this.socket.on('connect_error', (err) => { console.error('Socket connection error:', err); - this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.') + this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.'); }); this.socket.on('connect_timeout', () => { console.error('Socket connection timeout'); - this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.') + this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.'); }); this.socket.on('disconnect', () => { console.log('Socket disconnected'); - this.setStatusMessage('Disconnected from EveAI. Please refresh the page for further interaction.') + this.setStatusMessage('Disconnected from EveAI. Please refresh the page for further interaction.'); }); 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...') + 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('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') { @@ -169,6 +165,7 @@ class EveAIChatWidget extends HTMLElement { clearProgress() { this.statusLine.textContent = ''; + this.toggleSendButton(false); // Re-enable and revert send button to outlined version } checkTaskStatus(taskId) { @@ -182,7 +179,7 @@ class EveAIChatWidget extends HTMLElement {
- + send
@@ -197,31 +194,89 @@ class EveAIChatWidget extends HTMLElement { 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); // Use marked to convert markdown to HTML - // Ensure citations is an array - if (!Array.isArray(citations)) { - console.error('Expected citations to be an array, but got:', citations); - citations = []; // Default to an empty array - } + let content = marked.parse(text); let citationsHtml = citations.map(url => `${url}`).join('
'); - message.innerHTML = ` -

${content}

- ${citationsHtml ? `

${citationsHtml}

` : ''} -
- ${algorithm} - thumb_up - thumb_down -
- `; - this.messagesArea.appendChild(message); + 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 'RAG_LLM': + algorithmClass = 'fingerprint-rag-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) { + 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'); const message = this.questionInput.value.trim(); @@ -229,6 +284,7 @@ class EveAIChatWidget extends HTMLElement { this.addUserMessage(message); this.questionInput.value = ''; this.sendMessageToBackend(message); + this.toggleSendButton(true); // Disable and change send button to filled version } } @@ -246,12 +302,23 @@ class EveAIChatWidget extends HTMLElement { this.socket.emit('user_message', { tenantId: this.tenantId, token: this.jwtToken, message, language: this.language }); 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); -function handleFeedback(feedback, interactionId) { - // Send feedback to the backend - console.log(`Feedback for ${interactionId}: ${feedback}`); - this.socket.emit('feedback', { feedback, interaction_id: interactionId }); -} diff --git a/static/js/eveai-sdk.js b/static/js/eveai-sdk.js index ac08330..9e53d41 100644 --- a/static/js/eveai-sdk.js +++ b/static/js/eveai-sdk.js @@ -19,12 +19,6 @@ class EveAI { chatWidget.setAttribute('api-key', this.apiKey); chatWidget.setAttribute('domain', this.domain); chatWidget.setAttribute('language', this.language); - console.log('Attributes set in chat widget:', { - tenantId: chatWidget.getAttribute('tenant-id'), - apiKey: chatWidget.getAttribute('api-key'), - domain: chatWidget.getAttribute('domain'), - language: chatWidget.getAttribute('language') - }); }); } else { console.error('Container not found');