Spaces:
Runtime error
Runtime error
| /** | |
| * ============================================================================== | |
| * 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%'; | |
| } | |
| } | |
| }); |