document.addEventListener('DOMContentLoaded', () => { // ID from URL path: /progress/{job_id} const jobId = window.location.pathname.split('/').pop(); if (!jobId) { alert("Invalid Job ID"); return; } const progressCircle = document.getElementById('progress-circle'); const percentageText = document.getElementById('progress-percentage-text'); const statusPhase = document.getElementById('status-phase'); const statusHeadline = document.getElementById('status-headline'); const statusDetail = document.getElementById('status-detail'); // Token Counter Simulation const tokenCounter = document.getElementById('token-counter'); const timeElapsed = document.getElementById('time-elapsed'); let startTime = Date.now(); let tokenCount = 0; // Simulate token count increasing setInterval(() => { tokenCount += Math.floor(Math.random() * 550); if (tokenCounter) tokenCounter.innerText = (tokenCount / 1000).toFixed(1) + 'K'; const seconds = Math.floor((Date.now() - startTime) / 1000); if (timeElapsed) timeElapsed.innerText = seconds + 's'; }, 200); const steps = { 'news': document.getElementById('step-news'), 'fundamentals': document.getElementById('step-fundamentals'), 'signal': document.getElementById('step-signal') // Peers is usually part of fundamentals or signal in this simplified view }; function updateStep(element, status) { if (!element) return; const iconContainer = element.querySelector('.step-icon'); const icon = element.querySelector('.icon-content'); const labelText = element.querySelector('.step-label'); const statusText = element.querySelector('.step-status'); const progressBar = element.querySelector('.progress-bar'); // Reset base classes iconContainer.className = 'step-icon size-6 flex items-center justify-center rounded-full border transition-all duration-300'; labelText.className = 'text-xs font-mono step-label transition-colors duration-300'; statusText.className = 'text-xs font-mono step-status transition-colors duration-300'; if (status === 'pending') { iconContainer.classList.add('border-white/20', 'text-gray-500'); icon.textContent = 'lock_clock'; labelText.classList.add('text-gray-500'); statusText.classList.add('text-gray-500'); statusText.textContent = 'PENDING'; progressBar.style.width = '0%'; } else if (status === 'active') { iconContainer.classList.add('border-[#33f20d]', 'text-[#33f20d]', 'animate-pulse'); icon.textContent = 'sync'; labelText.classList.add('text-white', 'font-bold'); statusText.classList.add('text-white'); statusText.textContent = 'IN PROGRESS'; progressBar.style.width = '60%'; // Indeterminate visual progressBar.classList.add('animate-pulse'); } else if (status === 'complete') { iconContainer.classList.add('bg-[#33f20d]', 'border-[#33f20d]', 'text-black'); icon.textContent = 'check'; labelText.classList.add('text-gray-300'); statusText.classList.add('text-[#33f20d]'); statusText.textContent = 'COMPLETE'; progressBar.style.width = '100%'; progressBar.classList.remove('animate-pulse'); } } // Polling Function async function checkProgress() { try { const response = await fetch(`/api/status/${jobId}`); if (!response.ok) throw new Error("Status check failed"); const data = await response.json(); // Update Ring (283 is circumference) const circumference = 283; const offset = circumference - (data.progress / 100) * circumference; progressCircle.style.strokeDashoffset = offset; // Update Text percentageText.innerText = `${data.progress}%`; statusPhase.innerText = data.current_step.toUpperCase(); // Update Steps based on progress/state // Logic derived from main.py process_analysis function // 0-30: News // 30-60: Fundamentals // 60-80: Peers (Merged into fundamentals visually or signal) // 80-100: Signal if (data.progress < 30) { updateStep(steps['news'], 'active'); updateStep(steps['fundamentals'], 'pending'); updateStep(steps['signal'], 'pending'); statusHeadline.innerText = "Analyzing News Sentiment..."; statusDetail.innerText = "Scanning global sources for contrarian signals."; } else if (data.progress < 60) { updateStep(steps['news'], 'complete'); updateStep(steps['fundamentals'], 'active'); updateStep(steps['signal'], 'pending'); statusHeadline.innerText = "Processing Fundamentals..."; statusDetail.innerText = "Ingesting RAG documents and analyzing financial ratios."; } else if (data.progress < 90) { updateStep(steps['news'], 'complete'); updateStep(steps['fundamentals'], 'complete'); updateStep(steps['signal'], 'active'); statusHeadline.innerText = "Generating Signal..."; statusDetail.innerText = "Synthesizing data points to detect market anomalies."; } else { // All complete updateStep(steps['news'], 'complete'); updateStep(steps['fundamentals'], 'complete'); updateStep(steps['signal'], 'complete'); } if (data.status === 'completed') { statusHeadline.innerText = "Analysis Complete."; setTimeout(() => { window.location.href = `/results/${jobId}`; }, 1000); } else if (data.status === 'failed') { statusHeadline.innerText = "Analysis Failed"; statusDetail.innerText = data.error || "Unknown error occurred."; statusDetail.classList.add('text-red-500'); progressCircle.style.stroke = '#ff2a4d'; } else { // Continue polling setTimeout(checkProgress, 1000); // 1 second poll } } catch (error) { console.error("Polling error:", error); // Retry anyway setTimeout(checkProgress, 3000); } } const abortBtn = document.getElementById('abortBtn'); if (abortBtn) { abortBtn.addEventListener('click', async (e) => { e.preventDefault(); if (confirm("Are you sure you want to stop the analysis?")) { try { await fetch(`/api/cancel/${jobId}`, { method: 'POST' }); window.location.href = '/'; } catch (err) { console.error("Cancellation failed", err); alert("Could not cancel job."); } } }); } // Start checkProgress(); });