Hanan-Alnakhal's picture
Upload 12 files
8a693e2 verified
raw
history blame
13.7 kB
// ===== 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 = '<div class="text-center text-gray-500 py-10">No results found</div>';
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 = `
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 ${borderClass} pl-4">
<div class="flex-grow">
<h3 class="text-lg font-bold text-slate-800">${escapeHtml(result.test_name)}</h3>
<p class="text-sm text-slate-500">Ref Range: ${escapeHtml(result.reference_range || 'N/A')}</p>
</div>
<div class="flex items-center gap-3 w-full md:w-auto justify-between md:justify-end">
<span class="text-xl font-bold text-slate-700">${escapeHtml(result.value)} <span class="text-sm font-normal text-slate-500">${escapeHtml(result.unit)}</span></span>
<span class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide ${statusColors}">
${escapeHtml(result.status)}
</span>
</div>
</div>
<div class="mt-4 pt-3 border-t border-slate-50">
<p class="text-sm text-slate-600 leading-relaxed">
<span class="font-semibold text-primary">Insight:</span> ${escapeHtml(explanation)}
</p>
</div>
`;
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 = '<div class="flex justify-center p-8"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>';
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 = `
<div class="prose prose-slate max-w-none">
<h3 class="text-xl font-semibold mb-4 text-primary">Analysis Report</h3>
<div class="text-slate-700 leading-relaxed whitespace-pre-line">
${escapeHtml(data.summary).replace(/\n/g, '<br>')}
</div>
</div>
`;
} catch (error) {
showToast('Error: ' + error.message, 'error');
contentDiv.innerHTML = '<p class="text-red-500 text-center">Failed to generate summary.</p>';
}
}
// ===== 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, '<br>');
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;
}