EduMentorAI / src /static /js /chat.js
ziadtarek12's picture
OCR Done!
65c4ece
Raw
History Blame Contribute Delete
16 kB
// 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';
}
}