/** * Server-Sent Events (SSE) Progress Handler * Handles real-time streaming updates for learning path generation */ let eventSource = null; let progressModal = null; let progressBar = null; let progressMessage = null; let cancelButton = null; // Initialize SSE progress handler function initSSEProgress() { // Create progress modal HTML const modalHTML = `
`; // Add modal to body document.body.insertAdjacentHTML('beforeend', modalHTML); // Get references progressModal = document.getElementById('progress-modal'); progressBar = document.getElementById('progress-bar'); progressMessage = document.getElementById('progress-message'); cancelButton = document.getElementById('cancel-generation'); // Cancel button handler cancelButton.addEventListener('click', cancelGeneration); } // Show progress modal function showProgressModal() { if (progressModal) { progressModal.classList.remove('hidden'); progressModal.classList.add('flex'); resetProgress(); } } // Hide progress modal function hideProgressModal() { if (progressModal) { progressModal.classList.add('hidden'); progressModal.classList.remove('flex'); } } // Reset progress function resetProgress() { if (progressBar) progressBar.style.width = '0%'; if (progressMessage) progressMessage.textContent = 'Initializing...'; document.getElementById('progress-percent').textContent = '0%'; } // Update progress function updateProgress(progress, message) { if (progressBar) { progressBar.style.width = `${progress}%`; } if (progressMessage) { progressMessage.textContent = message; } document.getElementById('progress-percent').textContent = `${progress}%`; } // Show error function showError(errorMessage) { if (progressMessage) { progressMessage.innerHTML = `❌ ${errorMessage}`; } if (progressBar) { progressBar.classList.remove('bg-gradient-to-r', 'from-neon-cyan', 'to-neon-purple'); progressBar.classList.add('bg-status-error'); } // Change cancel button to "Close" if (cancelButton) { cancelButton.textContent = 'Close'; cancelButton.classList.remove('border-status-error', 'text-status-error'); cancelButton.classList.add('border-neon-cyan', 'text-neon-cyan'); } } // Cancel generation function cancelGeneration() { if (eventSource) { eventSource.close(); eventSource = null; } hideProgressModal(); } // Start SSE streaming function startSSEGeneration(formData) { // Show progress modal showProgressModal(); // Close any existing connection if (eventSource) { eventSource.close(); } // Create FormData for POST request const urlParams = new URLSearchParams(formData); // Create EventSource with POST data (using a workaround) // Note: EventSource only supports GET, so we'll use fetch with streaming fetch('/generate-stream', { method: 'POST', body: urlParams, headers: { 'Content-Type': 'application/x-www-form-urlencoded', } }).then(response => { const reader = response.body.getReader(); const decoder = new TextDecoder(); function readStream() { reader.read().then(({ done, value }) => { if (done) { console.log('Stream complete'); return; } // Decode the chunk const chunk = decoder.decode(value, { stream: true }); // Split by SSE message delimiter const messages = chunk.split('\n\n'); messages.forEach(message => { if (message.startsWith('data: ')) { const data = message.substring(6); try { const parsed = JSON.parse(data); handleSSEMessage(parsed); } catch (e) { console.error('Failed to parse SSE message:', data); } } }); // Continue reading readStream(); }).catch(error => { console.error('Stream error:', error); showError('Connection lost. Please try again.'); }); } readStream(); }).catch(error => { console.error('Fetch error:', error); showError('Failed to start generation. Please try again.'); }); } // Handle SSE message function handleSSEMessage(data) { console.log('SSE message:', data); // Handle error if (data.error) { showError(data.error); setTimeout(() => { hideProgressModal(); }, 3000); return; } // Update progress if (data.progress !== undefined) { updateProgress(data.progress, data.message || ''); } // Handle completion if (data.done) { // Add success animation if (progressBar) { progressBar.classList.add('animate-pulse'); } // Redirect after short delay setTimeout(() => { if (data.redirect_url) { console.log('Redirecting to:', data.redirect_url); window.location.href = data.redirect_url; } else { console.error('No redirect URL provided'); hideProgressModal(); } }, 500); } } // Attach to form submission document.addEventListener('DOMContentLoaded', function() { // Initialize SSE progress initSSEProgress(); // Find the generation form const form = document.querySelector('form[action="/generate"]'); if (form) { // Add checkbox for streaming mode const streamingCheckbox = document.createElement('div'); streamingCheckbox.className = 'flex items-center gap-2 mt-4'; streamingCheckbox.innerHTML = ` `; // Insert before submit button const submitButton = form.querySelector('button[type="submit"]'); if (submitButton) { submitButton.parentNode.insertBefore(streamingCheckbox, submitButton); } // Intercept form submission form.addEventListener('submit', function(e) { const useStreaming = document.getElementById('use-streaming')?.checked; if (useStreaming) { e.preventDefault(); // Get form data const formData = new FormData(form); // Start SSE generation startSSEGeneration(formData); } // If not using streaming, let form submit normally }); } });