/** * PolicyWise Chat Interface * Interactive web chat for the RAG application */ class ChatInterface { constructor() { this.messageInput = document.getElementById('messageInput'); this.sendButton = document.getElementById('sendButton'); this.chatForm = document.getElementById('chatForm'); this.messagesContainer = document.getElementById('messagesContainer'); this.includeSources = document.getElementById('includeSources'); this.loadingOverlay = document.getElementById('loadingOverlay'); this.statusIndicator = document.getElementById('statusIndicator'); this.charCount = document.getElementById('charCount'); this.sourcePanel = document.getElementById('sourcePanel'); this.conversationHistoryPanel = document.getElementById('conversationHistoryPanel'); this.swipeIndicator = document.getElementById('swipeIndicator'); // Search and filter elements this.searchConversations = document.getElementById('searchConversations'); this.exportConversationsBtn = document.getElementById('exportConversationsBtn'); this.importConversations = document.getElementById('importConversations'); this.messages = []; // Store messages in memory this.conversationId = this.loadOrCreateConversation(); this.isLoading = false; this.autoRetryCount = 0; this.maxAutoRetries = 3; // Track focused elements for keyboard navigation this.lastFocusedElement = null; // Touch handling for mobile swipe this.touchStartX = 0; this.touchEndX = 0; this.initializeEventListeners(); this.setupKeyboardNavigation(); this.setupTouchHandlers(); this.checkSystemHealth(); this.loadConversationHistory(); this.loadQuerySuggestions(); this.focusInput(); this.initializeSourcePanel(); // Setup initial policy suggestion buttons if they exist this.setupPolicySuggestionButtons(); } /** * Set up keyboard navigation and accessibility */ setupKeyboardNavigation() { // Trap focus within modal dialogs const handleTabKey = (e, containerElement) => { if (e.key !== 'Tab') return; const focusableElements = containerElement.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } }; // Add keyboard event listeners to side panels if (this.conversationHistoryPanel) { this.conversationHistoryPanel.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.closeConversationPanel(); } else if (e.key === 'Tab') { handleTabKey(e, this.conversationHistoryPanel); } }); } if (this.sourcePanel) { document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.sourcePanel.classList.contains('show')) { this.closeSourcePanel(); } else if (e.key === 'Tab' && this.sourcePanel.classList.contains('show')) { handleTabKey(e, this.sourcePanel); } }); } // Add keyboard shortcuts document.addEventListener('keydown', (e) => { // Cmd+/ or Ctrl+/ to focus search if ((e.metaKey || e.ctrlKey) && e.key === '/') { e.preventDefault(); this.focusInput(); } // Cmd+Shift+E or Ctrl+Shift+E to export conversations if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'E') { e.preventDefault(); this.exportConversationsToFile(); } }); } /** * Set up touch handlers for mobile swipe gestures */ setupTouchHandlers() { // Only set up on mobile devices if (window.innerWidth <= 768) { document.addEventListener('touchstart', (e) => { this.touchStartX = e.changedTouches[0].screenX; }, { passive: true }); document.addEventListener('touchend', (e) => { this.touchEndX = e.changedTouches[0].screenX; this.handleSwipeGesture(); }, { passive: true }); // Show swipe indicator occasionally setTimeout(() => { if (this.swipeIndicator && !this.conversationHistoryPanel.classList.contains('show')) { this.swipeIndicator.style.display = 'flex'; setTimeout(() => { this.swipeIndicator.style.display = 'none'; }, 3000); } }, 5000); } } /** * Initialize source document panel */ /** * Load query suggestions from the server */ async loadQuerySuggestions() { const suggestionsContainer = document.getElementById('suggestedQueries'); if (!suggestionsContainer) return; try { const response = await fetch('/chat/suggestions'); const data = await response.json(); if (response.ok && data.status === 'success' && data.suggestions && data.suggestions.length > 0) { suggestionsContainer.innerHTML = ''; data.suggestions.forEach(suggestion => { const suggestionDiv = document.createElement('div'); suggestionDiv.className = 'query-suggestion'; const iconSpan = document.createElement('span'); iconSpan.className = 'suggestion-icon'; iconSpan.innerHTML = ` `; const textSpan = document.createElement('span'); textSpan.className = 'suggestion-text'; textSpan.textContent = suggestion; suggestionDiv.appendChild(iconSpan); suggestionDiv.appendChild(textSpan); // Add click event to populate the input suggestionDiv.addEventListener('click', () => { this.messageInput.value = suggestion; this.updateCharCount(); this.autoResizeTextarea(); this.updateSendButton(); this.focusInput(); }); suggestionsContainer.appendChild(suggestionDiv); }); } } catch (error) { console.warn('Failed to load query suggestions:', error); } } initializeSourcePanel() { // Create the source panel if it doesn't exist if (!this.sourcePanel) { const sourcePanelHTML = `
`; const tempDiv = document.createElement('div'); tempDiv.innerHTML = sourcePanelHTML; document.body.appendChild(tempDiv.firstElementChild); this.sourcePanel = document.getElementById('sourcePanel'); // Add ESC key handler for accessibility this.sourcePanel.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.closeSourcePanel(); } }); } } /** * Initialize all event listeners */ initializeEventListeners() { // Form submission this.chatForm.addEventListener('submit', (e) => { e.preventDefault(); this.sendMessage(); }); // Input validation and auto-resize this.messageInput.addEventListener('input', () => { this.updateCharCount(); this.autoResizeTextarea(); this.updateSendButton(); }); // Enter key handling (Shift+Enter for new line, Enter to send) this.messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!this.isLoading && this.messageInput.value.trim()) { this.sendMessage(); } } }); // Clear welcome message on first input this.messageInput.addEventListener('focus', () => { this.clearWelcomeMessage(); }, { once: true }); // Conversation history panel const conversationHistoryBtn = document.getElementById('conversationHistoryBtn'); const closeConversationsBtn = document.getElementById('closeConversationsBtn'); const newConversationBtn = document.getElementById('newConversationBtn'); if (conversationHistoryBtn) { conversationHistoryBtn.addEventListener('click', () => { this.openConversationPanel(); }); } if (closeConversationsBtn) { closeConversationsBtn.addEventListener('click', () => { this.closeConversationPanel(); }); } if (newConversationBtn) { newConversationBtn.addEventListener('click', () => { this.startNewConversation(); }); } // Conversation search functionality if (this.searchConversations) { this.searchConversations.addEventListener('input', () => { this.filterConversations(this.searchConversations.value); }); } // Export/Import functionality if (this.exportConversationsBtn) { this.exportConversationsBtn.addEventListener('click', () => { this.exportConversationsToFile(); }); } if (this.importConversations) { this.importConversations.addEventListener('change', (e) => { this.importConversationsFromFile(e.target.files[0]); }); } } /** * Handle swipe gestures for mobile */ handleSwipeGesture() { const swipeThreshold = 100; // Right to left swipe (open conversation panel) if (this.touchEndX < this.touchStartX - swipeThreshold) { if (!this.conversationHistoryPanel.classList.contains('show')) { this.openConversationPanel(); } } // Left to right swipe (close conversation panel) if (this.touchEndX > this.touchStartX + swipeThreshold) { if (this.conversationHistoryPanel.classList.contains('show')) { this.closeConversationPanel(); } } } /** * Open conversation panel with focus management */ openConversationPanel() { this.updateConversationList(); this.conversationHistoryPanel.classList.add('show'); this.conversationHistoryPanel.setAttribute('aria-hidden', 'false'); // Store last focused element to restore focus when closing this.lastFocusedElement = document.activeElement; // Focus the close button const closeBtn = document.getElementById('closeConversationsBtn'); if (closeBtn) { setTimeout(() => closeBtn.focus(), 100); } // Hide the swipe indicator if (this.swipeIndicator) { this.swipeIndicator.style.display = 'none'; } // Add class to the body for potential styling document.body.classList.add('panel-open'); } /** * Close conversation panel with focus management */ closeConversationPanel() { this.conversationHistoryPanel.classList.remove('show'); this.conversationHistoryPanel.setAttribute('aria-hidden', 'true'); // Restore focus to previous element if (this.lastFocusedElement) { this.lastFocusedElement.focus(); } document.body.classList.remove('panel-open'); } /** * Close source document panel */ closeSourcePanel() { if (this.sourcePanel) { this.sourcePanel.classList.remove('show'); document.body.classList.remove('source-panel-open'); // Restore focus if (this.lastFocusedElement) { this.lastFocusedElement.focus(); } } } /** * Update the conversation list in the side panel */ updateConversationList() { const conversationList = document.getElementById('conversationList'); if (!conversationList) return; // Clear current list conversationList.innerHTML = ''; // Get conversations from storage const conversations = this.getConversationsFromStorage(); if (conversations.length === 0) { const emptyState = document.createElement('div'); emptyState.className = 'empty-state'; emptyState.innerHTML = 'No conversation history found.
'; conversationList.appendChild(emptyState); return; } // Sort conversations by last updated (newest first) conversations.sort((a, b) => { const dateA = new Date(a.metadata.last_updated); const dateB = new Date(b.metadata.last_updated); return dateB - dateA; }); // Add each conversation to the list conversations.forEach(conversation => { const item = document.createElement('div'); item.className = 'conversation-item'; item.setAttribute('role', 'listitem'); item.setAttribute('tabindex', '0'); item.setAttribute('aria-label', `${conversation.title || 'Untitled Conversation'}, ${conversation.metadata.message_count} messages, last updated ${this.formatDateTime(conversation.metadata.last_updated)}`); if (conversation.id === this.conversationId) { item.classList.add('active'); item.setAttribute('aria-current', 'true'); } const title = document.createElement('h4'); title.textContent = conversation.title || 'Untitled Conversation'; const meta = document.createElement('div'); meta.className = 'conversation-meta'; const date = new Date(conversation.metadata.last_updated); const formattedDate = date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); meta.textContent = `${conversation.metadata.message_count} messages · ${formattedDate}`; const actions = document.createElement('div'); actions.className = 'conversation-actions'; const deleteBtn = document.createElement('button'); deleteBtn.className = 'icon-button small'; deleteBtn.setAttribute('aria-label', `Delete conversation: ${conversation.title || 'Untitled Conversation'}`); deleteBtn.innerHTML = ` `; deleteBtn.title = 'Delete conversation'; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); this.deleteConversation(conversation.id); }); actions.appendChild(deleteBtn); item.appendChild(title); item.appendChild(meta); item.appendChild(actions); // Add click event to load this conversation item.addEventListener('click', () => { this.loadConversation(conversation.id); this.closeConversationPanel(); }); // Add keyboard support item.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.loadConversation(conversation.id); this.closeConversationPanel(); } }); conversationList.appendChild(item); }); } /** * Filter conversations based on search query */ filterConversations(query) { if (!query) { this.updateConversationList(); return; } query = query.toLowerCase(); const conversationList = document.getElementById('conversationList'); if (!conversationList) return; // Clear current list conversationList.innerHTML = ''; // Get conversations from storage const conversations = this.getConversationsFromStorage(); // Filter conversations const filteredConversations = conversations.filter(conv => { // Search in title if (conv.title && conv.title.toLowerCase().includes(query)) { return true; } // Search in messages if (conv.messages && conv.messages.some(msg => msg.content && msg.content.toLowerCase().includes(query))) { return true; } return false; }); if (filteredConversations.length === 0) { const emptyState = document.createElement('div'); emptyState.className = 'empty-state'; emptyState.innerHTML = `No conversations matching "${query}"
`; conversationList.appendChild(emptyState); return; } // Sort and display filtered conversations filteredConversations.sort((a, b) => { const dateA = new Date(a.metadata.last_updated); const dateB = new Date(b.metadata.last_updated); return dateB - dateA; }); // Add each filtered conversation to the list filteredConversations.forEach(conversation => { const item = document.createElement('div'); item.className = 'conversation-item'; item.setAttribute('role', 'listitem'); item.setAttribute('tabindex', '0'); if (conversation.id === this.conversationId) { item.classList.add('active'); item.setAttribute('aria-current', 'true'); } const title = document.createElement('h4'); title.textContent = conversation.title || 'Untitled Conversation'; const meta = document.createElement('div'); meta.className = 'conversation-meta'; const date = new Date(conversation.metadata.last_updated); const formattedDate = this.formatDateTime(conversation.metadata.last_updated); meta.textContent = `${conversation.metadata.message_count} messages · ${formattedDate}`; item.appendChild(title); item.appendChild(meta); // Add click event to load this conversation item.addEventListener('click', () => { this.loadConversation(conversation.id); this.closeConversationPanel(); }); // Add keyboard support item.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.loadConversation(conversation.id); this.closeConversationPanel(); } }); conversationList.appendChild(item); }); } /** * Export conversations to a JSON file */ exportConversationsToFile() { try { const conversations = this.getConversationsFromStorage(); if (conversations.length === 0) { alert('No conversations to export.'); return; } const dataStr = JSON.stringify(conversations, null, 2); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const downloadLink = document.createElement('a'); downloadLink.href = url; downloadLink.download = `policywise_conversations_${new Date().toISOString().split('T')[0]}.json`; // Append to the document, click it, then remove it document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); // Clean up the URL setTimeout(() => URL.revokeObjectURL(url), 100); // Show confirmation const statusText = this.statusIndicator.querySelector('.status-text'); const originalText = statusText.textContent; statusText.textContent = 'Conversations exported!'; setTimeout(() => { statusText.textContent = originalText; }, 3000); } catch (error) { console.error('Failed to export conversations:', error); alert('Failed to export conversations. Please try again.'); } } /** * Import conversations from a JSON file */ importConversationsFromFile(file) { if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const importedData = JSON.parse(event.target.result); if (!Array.isArray(importedData)) { throw new Error('Invalid format: expected an array of conversations'); } // Validate the structure const validConversations = importedData.filter(conv => { return ( conv.id && conv.messages && Array.isArray(conv.messages) && conv.metadata ); }); if (validConversations.length === 0) { throw new Error('No valid conversations found in the file'); } // Merge with existing conversations const existingConversations = this.getConversationsFromStorage(); const existingIds = new Set(existingConversations.map(c => c.id)); // Add only conversations that don't exist const newConversations = validConversations.filter(c => !existingIds.has(c.id)); const mergedConversations = [...existingConversations, ...newConversations]; // Save to localStorage this.saveConversationsToStorage(mergedConversations); // Update UI this.updateConversationList(); // Show confirmation with count of imported conversations alert(`Successfully imported ${newConversations.length} conversation(s).`); } catch (error) { console.error('Failed to import conversations:', error); alert(`Failed to import conversations: ${error.message}`); } }; reader.readAsText(file); } /** * Start a new conversation */ startNewConversation() { // Generate new ID this.conversationId = this.generateConversationId(); // Clear messages this.messages = []; this.messagesContainer.innerHTML = ''; // Add welcome message this.addWelcomeMessage(); // Update URL without reloading const url = new URL(window.location.href); url.searchParams.set('conversation_id', this.conversationId); window.history.pushState({}, '', url); // Close conversation panel document.getElementById('conversationHistoryPanel').classList.remove('show'); // Focus input this.focusInput(); } /** * Load a conversation by ID */ loadConversation(conversationId) { const conversations = this.getConversationsFromStorage(); const conversation = conversations.find(c => c.id === conversationId); if (conversation) { // Update current conversation ID this.conversationId = conversationId; // Update URL without reloading const url = new URL(window.location.href); url.searchParams.set('conversation_id', this.conversationId); window.history.pushState({}, '', url); // Clear current messages this.messages = []; this.messagesContainer.innerHTML = ''; // Load messages conversation.messages.forEach(msg => { this.addMessage(msg.content, msg.sender, msg.metadata, false); }); // Add messages to memory this.messages = [...conversation.messages]; // Update conversation metadata conversation.metadata.last_accessed = new Date().toISOString(); this.saveConversationsToStorage(conversations); // Focus input this.focusInput(); } } /** * Delete a conversation by ID */ deleteConversation(conversationId) { if (confirm('Are you sure you want to delete this conversation? This cannot be undone.')) { const conversations = this.getConversationsFromStorage(); const updatedConversations = conversations.filter(c => c.id !== conversationId); this.saveConversationsToStorage(updatedConversations); // If we deleted the current conversation, start a new one if (conversationId === this.conversationId) { this.startNewConversation(); } // Update the conversation list this.updateConversationList(); } } /** * Add welcome message to the UI */ addWelcomeMessage() { const welcomeDiv = document.createElement('div'); welcomeDiv.className = 'welcome-message'; welcomeDiv.innerHTML = `I'm here to help you find information about company policies and procedures. Ask me anything about:
What was the issue with this response?
Loading source document...
'; // Show the panel sourcePanel.classList.add('show'); sourcePanel.setAttribute('aria-hidden', 'false'); document.body.classList.add('source-panel-open'); // Set up close button if (closeBtn) { closeBtn.addEventListener('click', () => { this.closeSourcePanel(); }, { once: true }); // Focus the close button for keyboard accessibility setTimeout(() => closeBtn.focus(), 100); } try { // Set timeout for the request (15 seconds) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); // Fetch source document content const response = await fetch(`/chat/source/${sourceId}`, { signal: controller.signal }); clearTimeout(timeoutId); const data = await response.json(); if (response.ok && data.status === 'success') { // Create content elements const documentTitle = data.metadata?.filename || data.metadata?.title || title; const contentHTML = `${withBreaks}
`); } } return formattedSections.join('\n'); } /** * Render error message with retry functionality for source documents */ renderSourceError(message, sourceId, title, sourceContent) { const errorHtml = ` `; sourceContent.innerHTML = errorHtml; // Add retry functionality document.getElementById('retrySourceLoad')?.addEventListener('click', () => { this.showSourceDocument(sourceId, title); }); } /** * Format document content for display */ formatDocumentContent(content) { if (!content) return 'No content available
'; // First escape HTML to prevent XSS const escapedContent = this.escapeHtml(content); // Check if content is markdown and convert if needed if (content.includes('#') || content.includes('*')) { // Simple markdown formatting on escaped content return escapedContent .replace(/^# (.+)$/gm, '')
.replace(/\n/g, '
')
.replace(/^(.+)$/gm, function(match) {
if (!match.startsWith('<')) return '
' + match + '
'; return match; }); } // If not markdown, wrap escaped content in paragraphs return '' + escapedContent + '
'; } /** * Add confidence score visualization */ addConfidenceScore(contentDiv, confidence) { const confidenceDiv = document.createElement('div'); confidenceDiv.className = 'confidence-score'; const labelSpan = document.createElement('span'); labelSpan.textContent = `Confidence: ${Math.round(confidence * 100)}%`; const barDiv = document.createElement('div'); barDiv.className = 'confidence-bar'; const fillDiv = document.createElement('div'); fillDiv.className = 'confidence-fill'; fillDiv.style.width = `${confidence * 100}%`; barDiv.appendChild(fillDiv); confidenceDiv.appendChild(labelSpan); confidenceDiv.appendChild(barDiv); contentDiv.appendChild(confidenceDiv); } /** * Add an error message to the chat with retry options */ addErrorMessage(errorText, error = null, canRetry = true) { const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const timestamp = new Date().toISOString(); const messageDiv = document.createElement('div'); messageDiv.className = 'message message-assistant'; messageDiv.dataset.messageId = messageId; // Add header with timestamp const messageHeader = document.createElement('div'); messageHeader.className = 'message-header'; const senderLabel = document.createElement('span'); senderLabel.className = 'sender-label'; senderLabel.textContent = 'System'; const timestampSpan = document.createElement('span'); timestampSpan.className = 'message-timestamp'; timestampSpan.textContent = this.formatDateTime(timestamp); messageHeader.appendChild(senderLabel); messageHeader.appendChild(timestampSpan); messageDiv.appendChild(messageHeader); const contentDiv = document.createElement('div'); contentDiv.className = 'message-content error-message'; const strongElement = document.createElement('strong'); strongElement.textContent = 'Error:'; strongElement.setAttribute('aria-hidden', 'true'); const textSpan = document.createElement('span'); textSpan.textContent = ` ${errorText}`; textSpan.setAttribute('role', 'alert'); contentDiv.appendChild(strongElement); contentDiv.appendChild(textSpan); // Add retry button if applicable if (canRetry) { const retryButton = document.createElement('button'); retryButton.className = 'retry-button'; retryButton.innerHTML = ` Retry `; retryButton.setAttribute('aria-label', 'Retry sending your last message'); retryButton.addEventListener('click', () => { // Remove the error message messageDiv.remove(); // Retry the last user message this.retryLastMessage(); }); contentDiv.appendChild(retryButton); } // Add more detailed error info if available if (error && (error.status || error.code)) { const detailsDiv = document.createElement('div'); detailsDiv.className = 'error-details'; let detailsText = ''; if (error.status) detailsText += `Status code: ${error.status}. `; if (error.code) detailsText += `Error code: ${error.code}. `; if (error.message) detailsText += error.message; detailsDiv.textContent = detailsText; contentDiv.appendChild(detailsDiv); } messageDiv.appendChild(contentDiv); this.messagesContainer.appendChild(messageDiv); // Store in messages array this.messages.push({ id: messageId, sender: 'system', content: errorText, timestamp, metadata: { error: true } }); // Save to localStorage this.saveCurrentConversation(); this.scrollToBottom(); // Auto retry for server errors (5xx) if retry count is under the limit if (error && error.status && error.status >= 500 && this.autoRetryCount < this.maxAutoRetries) { this.autoRetryCount++; // Show auto-retry status const retryStatus = document.createElement('div'); retryStatus.className = 'auto-retry-status'; retryStatus.innerHTML = ` Retrying in 5 seconds... `; contentDiv.appendChild(retryStatus); // Countdown timer const countdownEl = retryStatus.querySelector('.retry-countdown'); let countdown = 5; const countdownInterval = setInterval(() => { countdown--; if (countdownEl) countdownEl.textContent = countdown.toString(); if (countdown <= 0) { clearInterval(countdownInterval); messageDiv.remove(); this.retryLastMessage(); } }, 1000); } } /** * Retry sending the last user message */ retryLastMessage() { // Find the last user message const lastUserMessage = [...this.messages].reverse().find(msg => msg.sender === 'user'); if (lastUserMessage) { // Reset auto retry count if this is a manual retry this.autoRetryCount = 0; // Resend the message this.sendMessageToAPI(lastUserMessage.content); } else { this.addErrorMessage("Couldn't find a previous message to retry.", null, false); } } /** * Submit simple feedback (helpful/not helpful) */ submitFeedback(messageId, isHelpful) { const message = this.messages.find(msg => msg.id === messageId); if (!message) return; // Update message with feedback message.feedback = { rating: isHelpful ? 5 : 1, timestamp: new Date().toISOString() }; // Save to localStorage this.saveCurrentConversation(); // Send to server if available this.sendFeedbackToServer(messageId, isHelpful); } /** * Submit detailed feedback */ submitDetailedFeedback(messageId, reason, detail) { const message = this.messages.find(msg => msg.id === messageId); if (!message) return; // Update message with detailed feedback message.feedback = { rating: 1, // Not helpful reason: reason, detail: detail, timestamp: new Date().toISOString() }; // Save to localStorage this.saveCurrentConversation(); // Send to server if available this.sendDetailedFeedbackToServer(messageId, reason, detail); } /** * Send feedback to server */ sendFeedbackToServer(messageId, isHelpful) { try { const feedback = { feedback_id: 'feedback_' + Date.now(), conversation_id: this.conversationId, message_id: messageId, feedback_type: 'response_rating', rating: isHelpful ? 5 : 1, timestamp: new Date().toISOString() }; fetch('/chat/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(feedback) }) .then(response => { if (!response.ok) { console.warn('Failed to send feedback to server:', response.status); } }) .catch(error => { console.warn('Error sending feedback to server:', error); }); } catch (error) { console.warn('Error preparing feedback:', error); } } /** * Send detailed feedback to server */ sendDetailedFeedbackToServer(messageId, reason, detail) { try { const feedback = { feedback_id: 'feedback_' + Date.now(), conversation_id: this.conversationId, message_id: messageId, feedback_type: 'detailed', rating: 1, reason: reason, comment: detail, timestamp: new Date().toISOString() }; fetch('/chat/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(feedback) }) .then(response => { if (!response.ok) { console.warn('Failed to send detailed feedback to server:', response.status); } }) .catch(error => { console.warn('Error sending detailed feedback to server:', error); }); } catch (error) { console.warn('Error preparing detailed feedback:', error); } } /** * Scroll to the bottom of the messages container */ scrollToBottom() { this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; } /** * Format ISO datetime string to a user-friendly format */ formatDateTime(isoString) { try { const date = new Date(isoString); // For messages from today, just show the time const today = new Date(); if (date.toDateString() === today.toDateString()) { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } // For messages from this year, show month, day and time if (date.getFullYear() === today.getFullYear()) { return date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' at ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } // For older messages, show full date return date.toLocaleDateString([], { year: 'numeric', month: 'short', day: 'numeric' }) + ' at ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch (e) { console.warn('Invalid timestamp format:', isoString); return 'Unknown time'; } } } // CSS for additional status states const additionalStyles = ` .status-dot.loading { background: #f59e0b; } .status-dot.warning { background: #f59e0b; } .status-dot.error { background: #ef4444; } .status-dot.ready { background: #10b981; } /* Source document panel styles */ .source-document-panel { position: fixed; top: 0; right: -500px; width: 450px; max-width: 90vw; height: 100vh; background: white; box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1); z-index: 101; transition: right 0.3s ease; display: flex; flex-direction: column; } .source-document-panel.show { right: 0; } .source-document-content { padding: 1rem; overflow-y: auto; } .source-document-content h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #1e293b; } .source-document-content h2 { font-size: 1.25rem; margin: 1.5rem 0 0.75rem 0; color: #1e293b; } .source-document-content h3 { font-size: 1.125rem; margin: 1.25rem 0 0.5rem 0; color: #1e293b; } .source-document-content p { margin: 0.75rem 0; color: #4b5563; line-height: 1.6; } .source-document-content .metadata { margin: 1rem 0; padding: 0.75rem; background: #f1f5f9; border-radius: 6px; font-size: 0.875rem; color: #64748b; } .source-document-content .metadata-item { margin: 0.25rem 0; } .source-document-content .metadata-item span { font-weight: 600; } .source-citation.clickable { cursor: pointer; transition: background-color 0.2s; } .source-citation.clickable:hover { background: #e2e8f0; } `; // Add additional styles to document const styleSheet = document.createElement('style'); styleSheet.textContent = additionalStyles; document.head.appendChild(styleSheet); // Initialize the chat interface when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { new ChatInterface(); }); // Service worker registration for potential offline functionality if ('serviceWorker' in navigator) { window.addEventListener('load', () => { // Optional: register a service worker for offline functionality // navigator.serviceWorker.register('/sw.js').catch(console.warn); }); }