EathVision-API / static /js /script.js
AnsoATC's picture
Upload 11 files
f6e45d4 verified
/**
* ==============================================================================
* 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%';
}
}
});