// ===== GLOBAL STATE ===== let currentResults = null; let explanations = null; // ===== INITIALIZATION ===== document.addEventListener('DOMContentLoaded', () => { // Navigation is now handled via onclick="showSection", but we init other listeners initFileUpload(); initScrollAnimations(); initEnterKey(); }); // ===== NAVIGATION (Compatible with Tailwind HTML) ===== function showSection(sectionId) { // Hide all sections document.querySelectorAll('.section').forEach(sec => { sec.classList.remove('active'); sec.style.display = 'none'; }); // Show target section const target = document.getElementById(sectionId); if (target) { target.style.display = 'block'; // Small timeout to allow display:block to apply before adding active class for animation setTimeout(() => { target.classList.add('active'); }, 10); // Scroll to top window.scrollTo({ top: 0, behavior: 'smooth' }); } } // ===== SCROLL ANIMATIONS ===== function initScrollAnimations() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.remove('opacity-0', 'translate-y-4'); entry.target.classList.add('opacity-100', 'translate-y-0'); } }); }, { threshold: 0.1 }); // Add animation classes to cards document.querySelectorAll('.feature-card, .upload-card').forEach(card => { card.classList.add('transition-all', 'duration-700', 'opacity-0', 'translate-y-4'); observer.observe(card); }); } // ===== FILE UPLOAD ===== function initFileUpload() { const fileInput = document.getElementById('fileInput'); const uploadArea = document.getElementById('uploadArea'); if (!fileInput || !uploadArea) return; // Click to upload (handled by onclick in HTML, but keeping listener just in case) fileInput.addEventListener('change', handleFileSelect); // Drag and drop Visuals uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); // Add Tailwind classes for drag state uploadArea.classList.add('border-secondary', 'bg-blue-50', 'scale-[1.02]'); }); uploadArea.addEventListener('dragleave', () => { // Remove Tailwind classes uploadArea.classList.remove('border-secondary', 'bg-blue-50', 'scale-[1.02]'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('border-secondary', 'bg-blue-50', 'scale-[1.02]'); const file = e.dataTransfer.files[0]; if (file && file.type === 'application/pdf') { uploadFile(file); } else { showToast('Please upload a PDF file', 'error'); } }); } function handleFileSelect(e) { const file = e.target.files[0]; if (file) { uploadFile(file); } } async function uploadFile(file) { const formData = new FormData(); formData.append('file', file); // Show progress document.getElementById('uploadArea').classList.add('hidden'); document.getElementById('uploadProgress').classList.remove('hidden'); try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Upload failed'); } currentResults = data.results; showToast(`✓ Found ${data.count} lab results!`, 'success'); // Generate explanations await generateExplanations(); // Display results displayResults(); // Switch to results tab automatically showSection('results-section'); } catch (error) { showToast(error.message, 'error'); resetUploadArea(); } } async function generateExplanations() { // Optional: show a mini toast or loading indicator try { const response = await fetch('/api/explain', { method: 'POST' }); const data = await response.json(); if (!response.ok) throw new Error(data.error); explanations = data.explanations; } catch (error) { console.warn('Auto-explanation generation failed:', error); // We continue anyway, results will just say "Loading..." or show basic info } } function displayResults() { const container = document.getElementById('resultsContainer'); if (!currentResults || currentResults.length === 0) { container.innerHTML = '
No results found
'; return; } // Update summary stats in the Summary tab updateSummaryStats(); // Clear container container.innerHTML = ''; // Create result cards with Tailwind Styling currentResults.forEach((result, index) => { const card = createResultCard(result, index); container.appendChild(card); }); // Reset upload area for next time resetUploadArea(); } function updateSummaryStats() { const stats = { normal: 0, high: 0, low: 0 }; currentResults.forEach(result => { // Normalize status string const status = result.status ? result.status.toLowerCase() : 'normal'; if (status.includes('high')) stats.high++; else if (status.includes('low')) stats.low++; else stats.normal++; }); // Update the DOM elements in the Summary Section document.getElementById('normalCount').textContent = stats.normal; document.getElementById('highCount').textContent = stats.high; document.getElementById('lowCount').textContent = stats.low; // Reveal the stats container const statsContainer = document.getElementById('summaryStats'); if(statsContainer) statsContainer.classList.remove('hidden'); const generateBtn = document.getElementById('generateSummaryBtn'); if(generateBtn) generateBtn.classList.remove('hidden'); } function createResultCard(result, index) { const card = document.createElement('div'); // TAILWIND STYLING: Card container card.className = 'bg-white rounded-xl shadow-sm border border-slate-100 p-5 mb-4 hover:shadow-md transition-shadow'; // Status colors let statusColors = 'bg-green-100 text-green-700'; // Default normal let borderClass = 'border-l-4 border-green-500'; const statusLower = result.status ? result.status.toLowerCase() : ''; if (statusLower.includes('high')) { statusColors = 'bg-red-100 text-red-700'; borderClass = 'border-l-4 border-red-500'; } else if (statusLower.includes('low')) { statusColors = 'bg-yellow-100 text-yellow-800'; borderClass = 'border-l-4 border-yellow-500'; } const explanation = explanations && explanations[result.test_name] ? explanations[result.test_name] : 'Analysis available in Chat or Summary.'; // HTML Structure using Tailwind card.innerHTML = `

${escapeHtml(result.test_name)}

Ref Range: ${escapeHtml(result.reference_range || 'N/A')}

${escapeHtml(result.value)} ${escapeHtml(result.unit)} ${escapeHtml(result.status)}

Insight: ${escapeHtml(explanation)}

`; return card; } function resetUploadArea() { document.getElementById('uploadArea').classList.remove('hidden'); document.getElementById('uploadProgress').classList.add('hidden'); document.getElementById('fileInput').value = ''; } // ===== SUMMARY GENERATION ===== async function generateSummary() { if (!currentResults) { showToast('Please upload a lab report first', 'error'); return; } const contentDiv = document.getElementById('summaryContent'); contentDiv.innerHTML = '
'; try { const response = await fetch('/api/summary'); const data = await response.json(); if (!response.ok) throw new Error(data.error); // Render summary with Markdown-like paragraphs contentDiv.innerHTML = `

Analysis Report

${escapeHtml(data.summary).replace(/\n/g, '
')}
`; } catch (error) { showToast('Error: ' + error.message, 'error'); contentDiv.innerHTML = '

Failed to generate summary.

'; } } // ===== CHAT FUNCTIONALITY ===== function initEnterKey() { const input = document.getElementById('chatInput'); if (input) { input.addEventListener('keypress', (e) => { if (e.key === 'Enter') askQuestion(); }); } } async function askQuestion() { if (!currentResults) { showToast('Please upload a lab report first', 'error'); return; } const input = document.getElementById('chatInput'); const question = input.value.trim(); if (!question) return; // Clear input input.value = ''; // Add user message addChatMessage(question, 'user'); // Show loading const loadingId = addChatMessage('Analyzing...', 'assistant', true); try { const response = await fetch('/api/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question }) }); const data = await response.json(); if (!response.ok) throw new Error(data.error); // Remove loading const loadingEl = document.getElementById(loadingId); if(loadingEl) loadingEl.remove(); // Add response addChatMessage(data.answer, 'assistant'); } catch (error) { const loadingEl = document.getElementById(loadingId); if(loadingEl) loadingEl.remove(); showToast(error.message, 'error'); } } function addChatMessage(text, sender, isLoading = false) { const container = document.getElementById('chatMessages'); const wrapper = document.createElement('div'); // Flex alignment based on sender wrapper.className = `flex w-full mb-4 ${sender === 'user' ? 'justify-end' : 'justify-start'}`; const bubble = document.createElement('div'); // Bubble Styling const baseStyle = "max-w-[85%] rounded-2xl px-5 py-3 shadow-sm text-sm leading-relaxed"; const userStyle = "bg-secondary text-white rounded-br-none"; // Blue bubble const aiStyle = "bg-slate-100 text-slate-800 rounded-tl-none border border-slate-200"; // Grey bubble bubble.className = `${baseStyle} ${sender === 'user' ? userStyle : aiStyle} ${isLoading ? 'animate-pulse' : ''}`; bubble.innerHTML = escapeHtml(text).replace(/\n/g, '
'); if (isLoading) bubble.id = `loading-${Date.now()}`; wrapper.appendChild(bubble); container.appendChild(wrapper); // Scroll to bottom container.scrollTop = container.scrollHeight; return bubble.id; } // ===== TOAST NOTIFICATIONS ===== function showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; // Tailwind classes for toast toast.className = `fixed bottom-5 right-5 px-6 py-3 rounded-lg shadow-2xl transform transition-all duration-300 z-50 text-white font-medium translate-y-0 opacity-100`; if (type === 'error') toast.classList.add('bg-red-600'); else if (type === 'success') toast.classList.add('bg-green-600'); else toast.classList.add('bg-slate-800'); setTimeout(() => { toast.classList.remove('translate-y-0', 'opacity-100'); toast.classList.add('translate-y-20', 'opacity-0'); }, 4000); } // ===== UTILS ===== function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }