Spaces:
Runtime error
Runtime error
| // src/static/js/chat.js | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Configure marked for safe rendering | |
| marked.setOptions({ | |
| breaks: true, | |
| gfm: true, | |
| sanitize: false, | |
| silent: false | |
| }); | |
| const chatInput = document.getElementById('chatInput'); | |
| const chatForm = document.getElementById('chatForm'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const chatMessages = document.getElementById('chatMessages'); | |
| const mobileSidebarToggle = document.getElementById('mobileSidebarToggle'); | |
| const chatSidebar = document.getElementById('chatSidebar'); | |
| const chatOverlay = document.getElementById('chatOverlay'); | |
| // Get configuration from window.chatConfig | |
| const config = window.chatConfig || {}; | |
| // Track current session ID | |
| let currentSessionId = config.sessionId; | |
| // Function to trigger MathJax rendering | |
| function renderMath(element) { | |
| if (window.MathJax && window.MathJax.typesetPromise) { | |
| window.MathJax.typesetPromise([element]).catch(function (err) { | |
| console.warn('MathJax rendering failed:', err); | |
| }); | |
| } | |
| } | |
| // Auto-resize textarea | |
| chatInput.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 150) + 'px'; | |
| }); | |
| // Mobile sidebar toggle | |
| if (mobileSidebarToggle) { | |
| mobileSidebarToggle.addEventListener('click', function() { | |
| chatSidebar.classList.toggle('show'); | |
| chatOverlay.classList.toggle('show'); | |
| }); | |
| } | |
| if (chatOverlay) { | |
| chatOverlay.addEventListener('click', function() { | |
| chatSidebar.classList.remove('show'); | |
| chatOverlay.classList.remove('show'); | |
| }); | |
| } | |
| // Handle form submission | |
| chatForm.addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| // Disable form | |
| sendBtn.disabled = true; | |
| chatInput.disabled = true; | |
| // Add user message to chat | |
| addMessage('user', message); | |
| chatInput.value = ''; | |
| chatInput.style.height = 'auto'; | |
| // Prepare URL based on session | |
| let url = config.chatUrl; | |
| if (currentSessionId) { | |
| url = '/chat/' + currentSessionId + '/'; | |
| } | |
| // Send message with real streaming | |
| sendStreamingMessage(message); | |
| }); | |
| // *** FIX: Listen for Enter key to send message *** | |
| chatInput.addEventListener('keydown', function(event) { | |
| // Check if 'Enter' is pressed without the Shift key | |
| if (event.key === 'Enter' && !event.shiftKey) { | |
| // Prevent default action (new line) | |
| event.preventDefault(); | |
| // Trigger the send button click | |
| sendBtn.click(); | |
| } | |
| }); | |
| // Send streaming message with real-time display | |
| function sendStreamingMessage(message) { | |
| // Show thinking indicator while waiting | |
| const thinkingIndicator = showThinkingIndicator(); | |
| let aiMessageDiv = null; | |
| let messageContent = null; | |
| let accumulatedContent = ''; | |
| // Prepare streaming URL | |
| let streamUrl = config.streamUrl; | |
| if (currentSessionId) { | |
| streamUrl = '/chat/' + currentSessionId + '/stream/'; | |
| } | |
| // Send POST request and read streaming response | |
| fetch(streamUrl, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value, | |
| }, | |
| body: JSON.stringify({ | |
| 'message': message | |
| }) | |
| }) | |
| .then(response => { | |
| if (!response.ok) { | |
| throw new Error('Network response was not ok'); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| function readStream() { | |
| return reader.read().then(({ done, value }) => { | |
| if (done) { | |
| // Re-enable form when streaming is complete | |
| sendBtn.disabled = false; | |
| chatInput.disabled = false; | |
| chatInput.focus(); | |
| return; | |
| } | |
| // Decode chunk and process lines | |
| const chunk = decoder.decode(value, { stream: true }); | |
| const lines = chunk.split('\n'); | |
| lines.forEach(line => { | |
| if (line.startsWith('data: ') && line.length > 6) { | |
| try { | |
| const data = JSON.parse(line.slice(6)); | |
| if (data.type === 'start') { | |
| // Keep thinking indicator for now | |
| } else if (data.type === 'chunk') { | |
| // Replace thinking indicator with actual message on first chunk | |
| if (!aiMessageDiv) { | |
| hideThinkingIndicator(); | |
| aiMessageDiv = addMessage('ai', ''); | |
| messageContent = aiMessageDiv.querySelector('.message-content'); | |
| } | |
| accumulatedContent += data.content; | |
| // Update message content with markdown parsing | |
| try { | |
| messageContent.innerHTML = marked.parse(accumulatedContent); | |
| } catch (e) { | |
| messageContent.innerHTML = accumulatedContent.replace(/\n/g, '<br>'); | |
| } | |
| // Trigger MathJax rendering for the updated content | |
| renderMath(messageContent); | |
| scrollToBottom(); | |
| } else if (data.type === 'complete') { | |
| if (data.session_id) { | |
| currentSessionId = data.session_id; | |
| const newUrl = '/chat/' + data.session_id + '/'; | |
| window.history.replaceState({}, '', newUrl); | |
| } | |
| // Add timestamp | |
| const timeDiv = document.createElement('div'); | |
| timeDiv.className = 'message-time'; | |
| timeDiv.textContent = new Date().toLocaleString(); | |
| messageContent.appendChild(timeDiv); | |
| } else if (data.type === 'error') { | |
| // Replace thinking indicator with error message | |
| if (!aiMessageDiv) { | |
| hideThinkingIndicator(); | |
| aiMessageDiv = addMessage('ai', ''); | |
| messageContent = aiMessageDiv.querySelector('.message-content'); | |
| } | |
| messageContent.textContent = data.error || 'Sorry, I encountered an error.'; | |
| } | |
| } catch (e) { | |
| console.warn('Failed to parse streaming data:', line); | |
| } | |
| } | |
| }); | |
| return readStream(); | |
| }); | |
| } | |
| return readStream(); | |
| }) | |
| .catch(error => { | |
| console.error('Streaming error:', error); | |
| hideThinkingIndicator(); | |
| // Create error message if no AI message exists yet | |
| if (!aiMessageDiv) { | |
| aiMessageDiv = addMessage('ai', ''); | |
| messageContent = aiMessageDiv.querySelector('.message-content'); | |
| } | |
| messageContent.textContent = 'Sorry, I encountered an error. Please try again.'; | |
| sendBtn.disabled = false; | |
| chatInput.disabled = false; | |
| chatInput.focus(); | |
| }); | |
| } | |
| // Show thinking indicator | |
| function showThinkingIndicator() { | |
| const thinkingDiv = document.createElement('div'); | |
| thinkingDiv.className = 'thinking-indicator'; | |
| thinkingDiv.id = 'thinking-indicator'; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.innerHTML = '<i class="fas fa-robot"></i>'; | |
| avatar.style.background = 'var(--secondary-color)'; | |
| avatar.style.color = 'white'; | |
| const thinkingContent = document.createElement('div'); | |
| thinkingContent.className = 'thinking-content'; | |
| const thinkingText = document.createElement('span'); | |
| thinkingText.textContent = 'Thinking'; | |
| const thinkingDots = document.createElement('div'); | |
| thinkingDots.className = 'thinking-dots'; | |
| for (let i = 0; i < 3; i++) { | |
| const dot = document.createElement('div'); | |
| dot.className = 'thinking-dot'; | |
| thinkingDots.appendChild(dot); | |
| } | |
| thinkingContent.appendChild(thinkingText); | |
| thinkingContent.appendChild(thinkingDots); | |
| thinkingDiv.appendChild(avatar); | |
| thinkingDiv.appendChild(thinkingContent); | |
| chatMessages.appendChild(thinkingDiv); | |
| scrollToBottom(); | |
| return thinkingDiv; | |
| } | |
| // Hide thinking indicator | |
| function hideThinkingIndicator() { | |
| const thinkingIndicator = document.getElementById('thinking-indicator'); | |
| if (thinkingIndicator) { | |
| thinkingIndicator.remove(); | |
| } | |
| } | |
| // Scroll to bottom of messages | |
| function scrollToBottom() { | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| // Add message to chat | |
| function addMessage(role, content) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${role}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| if (role === 'user') { | |
| avatar.textContent = config.userInitial; | |
| } else { | |
| avatar.innerHTML = '<i class="fas fa-robot"></i>'; | |
| } | |
| const messageContent = document.createElement('div'); | |
| messageContent.className = 'message-content'; | |
| if (role === 'user') { | |
| // For user messages, just convert newlines to <br> | |
| messageContent.innerHTML = content.replace(/\n/g, '<br>'); | |
| } else if (content) { | |
| // For AI messages, render markdown | |
| try { | |
| messageContent.innerHTML = marked.parse(content); | |
| } catch (error) { | |
| console.warn('Markdown parsing failed, using plain text:', error); | |
| messageContent.innerHTML = content.replace(/\n/g, '<br>'); | |
| } | |
| // Trigger MathJax rendering for AI messages | |
| renderMath(messageContent); | |
| } | |
| const timeDiv = document.createElement('div'); | |
| timeDiv.className = 'message-time'; | |
| timeDiv.textContent = new Date().toLocaleString(); | |
| messageContent.appendChild(timeDiv); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(messageContent); | |
| chatMessages.appendChild(messageDiv); | |
| scrollToBottom(); | |
| return messageDiv; // Return the message element for streaming updates | |
| } | |
| // Initial scroll to bottom | |
| scrollToBottom(); | |
| // Process existing messages for markdown rendering after DOM is ready | |
| processExistingMessages(); | |
| // Process existing messages for markdown rendering | |
| function processExistingMessages() { | |
| const messageContents = document.querySelectorAll('.message-content[data-markdown="true"]'); | |
| messageContents.forEach(function(messageContent) { | |
| // Get the current content (raw markdown with linebreaks converted to <br>) | |
| const rawContent = messageContent.innerHTML; | |
| // Extract the timestamp element to preserve it | |
| const timeElement = messageContent.querySelector('.message-time'); | |
| const timeHTML = timeElement ? timeElement.outerHTML : ''; | |
| // Remove the time element from the raw content to get just the message | |
| let messageText = rawContent; | |
| if (timeElement) { | |
| messageText = rawContent.replace(timeElement.outerHTML, ''); | |
| } | |
| // Convert <br> tags back to newlines for proper markdown processing | |
| messageText = messageText.replace(/<br\s*\/?>/gi, '\n'); | |
| // Remove any extra HTML tags that might have been added by linebreaks filter | |
| messageText = messageText.replace(/<p>/gi, '').replace(/<\/p>/gi, '\n'); | |
| // Clean up extra whitespace | |
| messageText = messageText.trim(); | |
| try { | |
| // Render the markdown | |
| const renderedContent = marked.parse(messageText); | |
| // Update the message content with rendered markdown and restore timestamp | |
| messageContent.innerHTML = renderedContent + timeHTML; | |
| // Trigger MathJax rendering for the updated content | |
| renderMath(messageContent); | |
| } catch (error) { | |
| console.warn('Failed to process markdown for existing message:', error); | |
| // Fall back to original content if markdown processing fails | |
| messageContent.innerHTML = messageText.replace(/\n/g, '<br>') + timeHTML; | |
| } | |
| }); | |
| // Scroll to bottom after processing messages | |
| scrollToBottom(); | |
| } | |
| }); | |
| // Load chat session | |
| function loadChatSession(sessionId) { | |
| window.location.href = '/chat/' + sessionId + '/'; | |
| } | |
| // Start new chat | |
| document.getElementById('newChatBtn')?.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| window.location.href = window.chatConfig?.chatUrl || '/chat/'; | |
| }); | |
| // Chat mode selection | |
| function showChatModeModal() { | |
| const modal = new bootstrap.Modal(document.getElementById('chatModeModal')); | |
| modal.show(); | |
| } | |
| function setChatMode(mode) { | |
| const documentSection = document.getElementById('documentSection'); | |
| const subjectSection = document.getElementById('subjectSection'); | |
| if (mode === 'document') { | |
| documentSection.style.display = 'block'; | |
| subjectSection.style.display = 'none'; | |
| } else if (mode === 'subject') { | |
| documentSection.style.display = 'none'; | |
| subjectSection.style.display = 'block'; | |
| } | |
| } | |
| // Set suggested prompt | |
| function setSuggestedPrompt(prompt) { | |
| const chatInput = document.getElementById('chatInput'); | |
| if (chatInput) { | |
| chatInput.value = prompt; | |
| chatInput.focus(); | |
| // Auto-resize the textarea | |
| chatInput.style.height = 'auto'; | |
| chatInput.style.height = Math.min(chatInput.scrollHeight, 150) + 'px'; | |
| } | |
| } | |