Spaces:
Sleeping
Sleeping
| // ===== GLOBAL STATE ===== | |
| let currentResults = null; | |
| let explanations = null; | |
| let sessionId = null; | |
| // ===== SECTION NAVIGATION (CRITICAL - MISSING IN YOUR CODE) ===== | |
| function showSection(sectionId) { | |
| console.log('π Showing section:', sectionId); | |
| // Hide all sections | |
| document.querySelectorAll('.section').forEach(section => { | |
| section.classList.remove('active'); | |
| }); | |
| // Show target section | |
| const targetSection = document.getElementById(sectionId); | |
| if (targetSection) { | |
| targetSection.classList.add('active'); | |
| targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } else { | |
| console.error('β Section not found:', sectionId); | |
| } | |
| } | |
| // ===== INITIALIZATION ===== | |
| document.addEventListener('DOMContentLoaded', () => { | |
| console.log('β DOM loaded, initializing...'); | |
| initFileUpload(); | |
| // Show upload section by default | |
| showSection('upload-section'); | |
| }); | |
| // ===== FILE UPLOAD ===== | |
| function initFileUpload() { | |
| const fileInput = document.getElementById('fileInput'); | |
| const uploadArea = document.getElementById('uploadArea'); | |
| if (!fileInput || !uploadArea) { | |
| console.error('β Upload elements not found'); | |
| return; | |
| } | |
| console.log('β Upload initialized'); | |
| // Click to upload | |
| fileInput.addEventListener('change', handleFileSelect); | |
| // Drag and drop | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('border-secondary', 'bg-blue-100'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('border-secondary', 'bg-blue-100'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('border-secondary', 'bg-blue-100'); | |
| 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 { | |
| console.log('π€ Uploading file:', file.name); | |
| const response = await fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| console.log('π₯ Upload response:', data); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'Upload failed'); | |
| } | |
| // Store session ID and results | |
| sessionId = data.session_id; | |
| currentResults = data.results; | |
| console.log('β Session ID:', sessionId); | |
| console.log('β Results:', currentResults.length); | |
| showToast(`β Found ${data.count} lab results!`, 'success'); | |
| // Display results immediately with template explanations | |
| console.log('π¨ Displaying template results...'); | |
| displayResults(); | |
| updateSummaryStats(); | |
| // Show stats and buttons | |
| document.getElementById('summaryStats').classList.remove('hidden'); | |
| document.getElementById('generateSummaryBtn').classList.remove('hidden'); | |
| // Switch to results section | |
| setTimeout(() => { | |
| showSection('results-section'); | |
| }, 500); | |
| // Fetch AI explanations in background | |
| console.log('π€ Fetching AI explanations...'); | |
| showToast('Generating AI explanations...', 'info'); | |
| await generateExplanations(); | |
| // Update display with AI explanations | |
| console.log('π Updating with AI explanations...'); | |
| displayResults(); | |
| showToast('β AI explanations loaded!', 'success'); | |
| } catch (error) { | |
| console.error('β Upload error:', error); | |
| showToast(error.message, 'error'); | |
| resetUploadArea(); | |
| } | |
| } | |
| async function generateExplanations() { | |
| try { | |
| console.log('π Calling /api/explain with session:', sessionId); | |
| const response = await fetch('/api/explain', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ session_id: sessionId }) | |
| }); | |
| const data = await response.json(); | |
| console.log('π₯ Explanation response:', data); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'Failed to generate explanations'); | |
| } | |
| explanations = data.explanations; | |
| console.log('β Explanations loaded:', Object.keys(explanations).length); | |
| } catch (error) { | |
| console.error('β Explanation error:', error); | |
| showToast('Using basic explanations', 'info'); | |
| } | |
| } | |
| function displayResults() { | |
| const container = document.getElementById('resultsContainer'); | |
| console.log('π― displayResults() called'); | |
| console.log('π Results:', currentResults?.length); | |
| console.log('π¬ Explanations:', explanations ? Object.keys(explanations).length : 0); | |
| if (!currentResults || currentResults.length === 0) { | |
| console.log('β No results to display'); | |
| container.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-slate-400 py-20"><div class="text-6xl mb-4 opacity-20">π</div><p>Upload a lab report to view results here</p></div>'; | |
| return; | |
| } | |
| console.log(`β Displaying ${currentResults.length} results`); | |
| // Clear container | |
| container.innerHTML = ''; | |
| // Create result cards | |
| currentResults.forEach((result, index) => { | |
| console.log(`π Card ${index + 1}:`, result.test_name, result.status); | |
| const card = createResultCard(result, index); | |
| container.appendChild(card); | |
| }); | |
| // Reset upload area | |
| resetUploadArea(); | |
| console.log('β All cards rendered'); | |
| } | |
| function createResultCard(result, index) { | |
| const card = document.createElement('div'); | |
| card.className = 'bg-white rounded-xl shadow-md border border-slate-200 p-6 mb-4 hover:shadow-lg transition-shadow'; | |
| // Status colors | |
| const statusColors = { | |
| normal: { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-200' }, | |
| high: { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-200' }, | |
| low: { bg: 'bg-yellow-50', text: 'text-yellow-700', border: 'border-yellow-200' }, | |
| unknown: { bg: 'bg-slate-50', text: 'text-slate-700', border: 'border-slate-200' } | |
| }; | |
| const colors = statusColors[result.status] || statusColors.unknown; | |
| // Get explanation | |
| let explanation = ''; | |
| if (explanations && explanations[result.test_name]) { | |
| explanation = explanations[result.test_name]; | |
| console.log(` β Using AI explanation for ${result.test_name}`); | |
| } else { | |
| console.log(` β οΈ Using template for ${result.test_name}`); | |
| // Template explanation | |
| if (result.status === 'normal') { | |
| explanation = `β <strong>Good news!</strong> Your ${result.test_name} level of ${result.value} ${result.unit} is within the normal range (${result.reference_range}).<br><br>This indicates healthy levels. Keep up your current health habits!`; | |
| } else if (result.status === 'high') { | |
| explanation = `β οΈ Your ${result.test_name} level of ${result.value} ${result.unit} is <strong>above</strong> the normal range (${result.reference_range}).<br><br>This may require attention. Please consult your healthcare provider.`; | |
| } else if (result.status === 'low') { | |
| explanation = `β οΈ Your ${result.test_name} level of ${result.value} ${result.unit} is <strong>below</strong> the normal range (${result.reference_range}).<br><br>This may require attention. Please consult your healthcare provider.`; | |
| } else { | |
| explanation = `Your ${result.test_name} result is ${result.value} ${result.unit}.<br><br>Reference range: ${result.reference_range}`; | |
| } | |
| } | |
| card.innerHTML = ` | |
| <div class="flex justify-between items-start mb-4"> | |
| <div> | |
| <h3 class="text-xl font-bold text-slate-800">${escapeHtml(result.test_name)}</h3> | |
| <p class="text-sm text-slate-500">Reference: ${escapeHtml(result.reference_range || 'N/A')}</p> | |
| </div> | |
| <div class="text-right"> | |
| <div class="text-2xl font-bold text-slate-800">${escapeHtml(result.value)} <span class="text-lg text-slate-500">${escapeHtml(result.unit)}</span></div> | |
| <span class="inline-block mt-2 px-3 py-1 rounded-full text-sm font-medium ${colors.bg} ${colors.text} border ${colors.border}"> | |
| ${result.status.toUpperCase()} | |
| </span> | |
| </div> | |
| </div> | |
| <div class="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-100"> | |
| <div class="font-semibold text-blue-900 mb-2">π‘ What does this mean?</div> | |
| <div class="text-sm text-slate-700 leading-relaxed">${explanation}</div> | |
| </div> | |
| `; | |
| return card; | |
| } | |
| function updateSummaryStats() { | |
| const stats = { normal: 0, high: 0, low: 0 }; | |
| currentResults.forEach(result => { | |
| if (result.status in stats) { | |
| stats[result.status]++; | |
| } | |
| }); | |
| document.getElementById('normalCount').textContent = stats.normal; | |
| document.getElementById('highCount').textContent = stats.high; | |
| document.getElementById('lowCount').textContent = stats.low; | |
| console.log('π Stats updated:', stats); | |
| } | |
| function resetUploadArea() { | |
| document.getElementById('uploadArea').classList.remove('hidden'); | |
| document.getElementById('uploadProgress').classList.add('hidden'); | |
| document.getElementById('fileInput').value = ''; | |
| } | |
| // ===== CHAT FUNCTIONALITY ===== | |
| async function askQuestion() { | |
| const input = document.getElementById('chatInput'); | |
| const question = input.value.trim(); | |
| if (!question) return; | |
| if (!sessionId) { | |
| showToast('Please upload a lab report first', 'error'); | |
| return; | |
| } | |
| console.log('π¬ Asking:', question); | |
| // Clear input | |
| input.value = ''; | |
| // Add user message | |
| addChatMessage(question, 'user'); | |
| // Add loading message | |
| const loadingId = addChatMessage('Thinking...', 'assistant', true); | |
| try { | |
| const response = await fetch('/api/ask', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| question: question, | |
| session_id: sessionId | |
| }) | |
| }); | |
| const data = await response.json(); | |
| console.log('π¬ Answer:', data); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'Failed to get answer'); | |
| } | |
| // Remove loading message | |
| document.getElementById(loadingId).remove(); | |
| // Add assistant response | |
| addChatMessage(data.answer, 'assistant'); | |
| } catch (error) { | |
| console.error('β Chat error:', error); | |
| document.getElementById(loadingId).remove(); | |
| addChatMessage(`Sorry, I encountered an error: ${error.message}`, 'assistant'); | |
| showToast(error.message, 'error'); | |
| } | |
| } | |
| function addChatMessage(text, sender, isLoading = false) { | |
| const messagesContainer = document.getElementById('chatMessages'); | |
| // Remove placeholder text if exists | |
| const placeholder = messagesContainer.querySelector('.text-slate-400'); | |
| if (placeholder && placeholder.closest('.text-center')) { | |
| placeholder.closest('.text-center').remove(); | |
| } | |
| const messageId = `msg-${Date.now()}-${Math.random()}`; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.id = messageId; | |
| messageDiv.className = `mb-4 ${sender === 'user' ? 'text-right' : 'text-left'}`; | |
| const bubbleClass = sender === 'user' | |
| ? 'inline-block bg-secondary text-white px-4 py-2 rounded-2xl rounded-tr-sm max-w-[80%]' | |
| : 'inline-block bg-slate-100 text-slate-800 px-4 py-2 rounded-2xl rounded-tl-sm max-w-[80%]'; | |
| messageDiv.innerHTML = `<div class="${bubbleClass} ${isLoading ? 'animate-pulse' : ''}">${escapeHtml(text)}</div>`; | |
| messagesContainer.appendChild(messageDiv); | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| return messageId; | |
| } | |
| // ===== SUMMARY GENERATION ===== | |
| async function generateSummary() { | |
| if (!sessionId) { | |
| showToast('Please upload a lab report first', 'error'); | |
| return; | |
| } | |
| console.log('π Generating summary...'); | |
| const summaryContent = document.getElementById('summaryContent'); | |
| summaryContent.innerHTML = '<div class="text-center py-10"><div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-200 border-t-secondary"></div><p class="text-secondary mt-3">Generating summary...</p></div>'; | |
| try { | |
| const response = await fetch('/api/summary', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ session_id: sessionId }) | |
| }); | |
| const data = await response.json(); | |
| console.log('π Summary:', data); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'Failed to generate summary'); | |
| } | |
| // Display summary | |
| summaryContent.innerHTML = ` | |
| <div class="prose max-w-none"> | |
| <div class="text-slate-700 leading-relaxed whitespace-pre-wrap">${escapeHtml(data.summary)}</div> | |
| </div> | |
| `; | |
| showToast('β Summary generated!', 'success'); | |
| } catch (error) { | |
| console.error('β Summary error:', error); | |
| summaryContent.innerHTML = '<div class="text-center text-slate-400 py-10"><p>Error generating summary. Please try again.</p></div>'; | |
| showToast('Error: ' + error.message, 'error'); | |
| } | |
| } | |
| // ===== TOAST NOTIFICATIONS ===== | |
| function showToast(message, type = 'info') { | |
| const toast = document.getElementById('toast'); | |
| const colors = { | |
| success: 'bg-green-600', | |
| error: 'bg-red-600', | |
| info: 'bg-blue-600' | |
| }; | |
| toast.textContent = message; | |
| toast.className = `fixed bottom-5 right-5 ${colors[type] || colors.info} text-white px-6 py-3 rounded-lg shadow-2xl z-50 transform transition-all duration-300`; | |
| toast.style.transform = 'translateY(0)'; | |
| toast.style.opacity = '1'; | |
| setTimeout(() => { | |
| toast.style.transform = 'translateY(100px)'; | |
| toast.style.opacity = '0'; | |
| }, 4000); | |
| } | |
| // ===== UTILITY FUNCTIONS ===== | |
| function escapeHtml(text) { | |
| if (!text) return ''; | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } |