/** * ============================================================================== * EATHVISION FRONTEND LOGIC * Description: Handles DOM manipulation, file uploads, and async API requests. * ============================================================================== */ document.addEventListener('DOMContentLoaded', () => { // --- DOM Elements --- const elements = { fileInput: document.getElementById('fileInput'), dropZone: document.getElementById('dropZone'), imagePreview: document.getElementById('imagePreview'), uploadUI: document.getElementById('uploadUI'), portionSlider: document.getElementById('portionSlider'), portionLabel: document.getElementById('portionLabel'), analyzeBtn: document.getElementById('analyzeBtn'), modelSelector: document.getElementById('modelSelector'), // State Containers states: { welcome: document.getElementById('welcomeState'), loading: document.getElementById('loadingState'), results: document.getElementById('resultsState') }, // Result Fields res: { badge: document.getElementById('badgeModel'), name: document.getElementById('resDishName'), confText: document.getElementById('resConfText'), confBar: document.getElementById('resConfBar'), portion: document.getElementById('resPortion'), kcal: document.getElementById('resKcal'), prot: document.getElementById('resProt'), fat: document.getElementById('resFat'), carb: document.getElementById('resCarb'), ingList: document.getElementById('ingredientsList'), noIngMsg: document.getElementById('noIngredientsMsg') } }; // --- Event Listeners --- // 1. Update portion slider label dynamically elements.portionSlider.addEventListener('input', (e) => { elements.portionLabel.innerText = `${e.target.value} g`; }); // 2. Handle File Selection & Image Preview elements.fileInput.addEventListener('change', handleFileSelect); // 3. Drag and Drop Visual Feedback elements.dropZone.addEventListener('dragover', (e) => { e.preventDefault(); elements.dropZone.classList.add('drag-active'); }); elements.dropZone.addEventListener('dragleave', () => { elements.dropZone.classList.remove('drag-active'); }); elements.dropZone.addEventListener('drop', (e) => { e.preventDefault(); elements.dropZone.classList.remove('drag-active'); if (e.dataTransfer.files.length > 0) { elements.fileInput.files = e.dataTransfer.files; handleFileSelect(); } }); // 4. API Request Execution elements.analyzeBtn.addEventListener('click', executeAnalysis); // --- Core Functions --- function handleFileSelect() { const file = elements.fileInput.files[0]; if (file) { const reader = new FileReader(); reader.onload = function (e) { elements.imagePreview.src = e.target.result; elements.imagePreview.classList.remove('hidden'); elements.uploadUI.classList.add('hidden'); // Make preview slightly transparent so the drop zone remains visible elements.imagePreview.classList.add('opacity-90'); } reader.readAsDataURL(file); } } async function executeAnalysis() { if (elements.fileInput.files.length === 0) { alert("⚠️ Please select an image first."); return; } const file = elements.fileInput.files[0]; const formData = new FormData(); formData.append('file', file); formData.append('model_type', elements.modelSelector.value); formData.append('portion_g', elements.portionSlider.value); // Transition UI to Loading toggleState('loading'); elements.analyzeBtn.disabled = true; try { // Call the local FastAPI backend const response = await fetch('/api/predict', { method: 'POST', body: formData }); const data = await response.json(); if (!response.ok) throw new Error(data.detail || "Server Error"); // Populate AI Data elements.res.badge.innerText = data.ai_prediction.model_used.toUpperCase(); elements.res.name.innerText = data.ai_prediction.dish_name; elements.res.confText.innerText = `${data.ai_prediction.confidence}%`; // Delay width application for smooth CSS transition setTimeout(() => { elements.res.confBar.style.width = `${data.ai_prediction.confidence}%`; }, 100); // Populate Nutrition Data elements.res.portion.innerText = data.nutrition_insights.portion_g; if (data.nutrition_insights.status === "success") { const b = data.nutrition_insights.base_data; elements.res.kcal.innerText = b.energy_kcal; elements.res.prot.innerText = b.protein_g; elements.res.fat.innerText = b.fat_g; elements.res.carb.innerText = b.carbs_g; } else { elements.res.kcal.innerText = "--"; elements.res.prot.innerText = "--"; elements.res.fat.innerText = "--"; elements.res.carb.innerText = "--"; } // Populate Ingredients Data elements.res.ingList.innerHTML = ''; // Clear previous if (data.ingredients && data.ingredients.length > 0) { elements.res.noIngMsg.classList.add('hidden'); data.ingredients.forEach(ing => { const li = document.createElement('li'); li.innerText = ing; // Capitalize first letter li.style.textTransform = "capitalize"; elements.res.ingList.appendChild(li); }); } else { elements.res.noIngMsg.classList.remove('hidden'); } // Show Results toggleState('results'); } catch (error) { console.error("API Error:", error); alert("❌ Analysis failed: " + error.message); toggleState('welcome'); } finally { elements.analyzeBtn.disabled = false; } } /** * Helper to toggle main view states * @param {string} targetState - 'welcome', 'loading', or 'results' */ function toggleState(targetState) { elements.states.welcome.classList.add('hidden'); elements.states.loading.classList.add('hidden'); elements.states.results.classList.add('hidden'); elements.states[targetState].classList.remove('hidden'); // Reset progress bar if not in results view if (targetState !== 'results') { elements.res.confBar.style.width = '0%'; } } });