// static/js/script.js document.addEventListener('DOMContentLoaded', function () { const drumElement = document.getElementById('drumElement'); const controlsDisplay = document.getElementById('controlsDisplay'); // For animation messages const perfumeLiquid = document.getElementById('perfumeLiquid'); const perfumeFlask = document.getElementById('perfumeFlask'); const dispensingStream = document.getElementById('dispensingStream'); const dispenserArea = document.getElementById('dispenserArea'); // Parent of stream const aromaLegendBody = document.getElementById('aromaLegendBody'); const controlsElement = document.querySelector('.controls'); // The whole controls div for animation // Image Upload Elements const imageUploadInput = document.getElementById('imageUploadInput'); const imagePreview = document.getElementById('imagePreview'); const analyzeImageBtn = document.getElementById('analyzeImageBtn'); const imageUploadLabelSpan = document.querySelector('.image-upload-label span'); const placeholderSrc = imagePreview.src; // Store initial placeholder const loadingIndicator = document.getElementById('loadingIndicator'); const apiErrorDisplay = document.getElementById('apiErrorDisplay'); // Perfume Info Display Elements const perfumeNameDisplay = document.getElementById('perfumeNameDisplay'); const perfumeSloganDisplay = document.getElementById('perfumeSloganDisplay'); const NUM_AROMAS = BASE_AROMAS_ORDERED.length; const AROMA_COLORS_MAP = { "Rose": '#FF69B4', "Ocean Breeze": '#1E90FF', "Fresh Cut Grass": '#32CD32', "Lemon Zest": '#FFD700', "Lavender": '#BA55D3', "Sweet Orange": '#FF7F50', "Cool Mint": '#00CED1', "Vanilla Bean": '#F4A460', "Wild Berry": '#DA70D6', "Spring Rain": '#87CEEB' }; // --- CORRECTED --- const MAX_FLASK_CAPACITY_ML = 10.0; // The API aims for a total dose of 10.0 for 100% full // --- END CORRECTED --- let totalLiquidInFlask = 0; function populateAromaLegend(apiAromasData = []) { // Ensure it's an array aromaLegendBody.innerHTML = ''; const apiAromas = Array.isArray(apiAromasData) ? apiAromasData : []; const apiAromasMap = new Map(apiAromas.map(a => [a.name, parseFloat(a.dose) || 0])); BASE_AROMAS_ORDERED.forEach((aromaName, index) => { const row = aromaLegendBody.insertRow(); const cellNum = row.insertCell(); const cellColor = row.insertCell(); const cellName = row.insertCell(); const cellDose = row.insertCell(); cellNum.textContent = index + 1; const color = AROMA_COLORS_MAP[aromaName] || '#ccc'; cellColor.innerHTML = ``; cellName.textContent = aromaName; const doseValue = apiAromasMap.get(aromaName) || 0.0; // Display dose with 1 or 2 decimal places, or "—" if 0 cellDose.textContent = doseValue > 0 ? doseValue.toFixed(doseValue % 1 === 0 ? 1 : 2) : "—"; }); } function createBottles() { drumElement.innerHTML = ''; const angleStep = 360 / NUM_AROMAS; BASE_AROMAS_ORDERED.forEach((aromaName, i) => { const angle = i * angleStep; const slot = document.createElement('div'); slot.classList.add('bottle-slot'); slot.style.transform = `rotate(${angle}deg)`; const bottle = document.createElement('div'); bottle.classList.add('base-aroma-bottle'); bottle.textContent = i + 1; bottle.style.backgroundColor = AROMA_COLORS_MAP[aromaName] || '#ccc'; bottle.title = aromaName; slot.appendChild(bottle); drumElement.appendChild(slot); }); } function rotateDrum(aromaIndex) { const angleStep = 360 / NUM_AROMAS; let drumRotation = -(aromaIndex * angleStep); drumElement.style.transform = `rotate(${drumRotation}deg)`; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function simulateDispensing(aromaName, dose) { // dose is now the absolute amount e.g. 0.4, 1.5 etc. const aromaIndex = BASE_AROMAS_ORDERED.indexOf(aromaName); if (aromaIndex === -1) { console.warn(`Aroma "${aromaName}" not found in base list for simulation.`); return; } controlsDisplay.textContent = `Selecting ${aromaName}...`; rotateDrum(aromaIndex); await delay(1000); // --- CORRECTED --- // Display the actual dose amount being dispensed. // The API returns dose like 0.4, which means 0.4 units out of 10.0 total for the flask. controlsDisplay.textContent = `Dispensing ${dose.toFixed(2)} units of ${aromaName}...`; // --- END CORRECTED --- const streamStartY_relative = drumElement.offsetTop; const flaskRect = perfumeFlask.getBoundingClientRect(); const dispenserAreaRect = dispenserArea.getBoundingClientRect(); const flaskNeckTopY_relative = (flaskRect.top - dispenserAreaRect.top) - 18; let streamHeight = flaskNeckTopY_relative - streamStartY_relative; streamHeight = Math.max(10, streamHeight); dispensingStream.style.top = `${streamStartY_relative}px`; dispensingStream.style.backgroundColor = AROMA_COLORS_MAP[aromaName] || '#ccc'; dispensingStream.style.height = `${streamHeight}px`; await delay(250); // --- CORRECTED --- totalLiquidInFlask += dose; // Add the absolute dose amount // Calculate fill percentage based on MAX_FLASK_CAPACITY_ML which is 10.0 const fillPercentage = Math.min(100, (totalLiquidInFlask / MAX_FLASK_CAPACITY_ML) * 100); // --- END CORRECTED --- perfumeLiquid.style.backgroundColor = AROMA_COLORS_MAP[aromaName] || '#ccc'; perfumeLiquid.style.height = `${fillPercentage}%`; await delay(600); dispensingStream.style.height = '0px'; await delay(300); } async function runDispensingSequence(aromasToDispense) { controlsElement.style.display = 'flex'; totalLiquidInFlask = 0; perfumeLiquid.style.height = '0%'; perfumeLiquid.style.backgroundColor = 'transparent'; // Ensure aromasToDispense is an array const aromas = Array.isArray(aromasToDispense) ? aromasToDispense : []; for (const aroma of aromas) { // Ensure dose is a number before using it const doseValue = parseFloat(aroma.dose); if (aroma.name && !isNaN(doseValue) && doseValue > 0) { await simulateDispensing(aroma.name, doseValue); } else { console.warn("Skipping aroma with invalid name or dose:", aroma); } } controlsDisplay.textContent = "Perfume Composition Complete!"; await delay(2000); // controlsElement.style.display = 'none'; } imageUploadInput.addEventListener('change', function(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { imagePreview.src = e.target.result; imageUploadLabelSpan.style.display = 'none'; } reader.readAsDataURL(file); analyzeImageBtn.disabled = false; apiErrorDisplay.style.display = 'none'; } }); analyzeImageBtn.addEventListener('click', async function() { const file = imageUploadInput.files[0]; if (!file) { apiErrorDisplay.textContent = "Please select an image first."; apiErrorDisplay.style.display = 'block'; return; } analyzeImageBtn.disabled = true; loadingIndicator.style.display = 'block'; apiErrorDisplay.style.display = 'none'; controlsElement.style.display = 'none'; // Hide animation controls during analysis const formData = new FormData(); formData.append('imageFile', file); try { const response = await fetch('/analyze_image', { method: 'POST', body: formData }); const responseText = await response.text(); // Get raw text first loadingIndicator.style.display = 'none'; analyzeImageBtn.disabled = false; let data; if (!response.ok) { // Try to parse error if backend sends JSON error try { data = JSON.parse(responseText); apiErrorDisplay.textContent = `Error: ${data.error || response.statusText}`; } catch (e) { apiErrorDisplay.textContent = `Error: ${response.statusText} (Could not parse error response)`; } apiErrorDisplay.style.display = 'block'; updateUiWithPerfumeData(initialPerfumeData); return; } // If response.ok, try to parse the expected JSON from the backend try { // Clean the response string if it's wrapped in markdown code blocks let cleanedResponseText = responseText; if (cleanedResponseText.startsWith("```json")) { cleanedResponseText = cleanedResponseText.substring(7); // Remove ```json\n if (cleanedResponseText.endsWith("```")) { cleanedResponseText = cleanedResponseText.substring(0, cleanedResponseText.length - 3); } } cleanedResponseText = cleanedResponseText.trim(); data = JSON.parse(cleanedResponseText); } catch (e) { console.error("Error parsing JSON from API:", e, "Raw text:", responseText); apiErrorDisplay.textContent = "Error: Could not parse perfume data from AI. Please try a different image or prompt."; apiErrorDisplay.style.display = 'block'; updateUiWithPerfumeData(initialPerfumeData); return; } if (data.api_error || data.api_warning) { // Check for errors/warnings from backend logic apiErrorDisplay.textContent = data.api_error || data.api_warning; apiErrorDisplay.style.display = 'block'; } updateUiWithPerfumeData(data); if (data.aromas && data.aromas.length > 0) { runDispensingSequence(data.aromas); } } catch (error) { console.error('Error sending image or processing response:', error); loadingIndicator.style.display = 'none'; analyzeImageBtn.disabled = false; apiErrorDisplay.textContent = "An unexpected error occurred. Please try again."; apiErrorDisplay.style.display = 'block'; updateUiWithPerfumeData(initialPerfumeData); } }); function updateUiWithPerfumeData(data) { perfumeNameDisplay.textContent = data.perfume_name || "Untitled Creation"; perfumeSloganDisplay.innerHTML = `${data.slogan || "An enigmatic essence."}`; populateAromaLegend(data.aromas || []); // Pass the aromas array } function initializeApp() { updateUiWithPerfumeData(initialPerfumeData); createBottles(); const initialAromaName = (initialPerfumeData.aromas && initialPerfumeData.aromas.length > 0) ? initialPerfumeData.aromas[0].name : BASE_AROMAS_ORDERED[0]; let initialAromaIndex = BASE_AROMAS_ORDERED.indexOf(initialAromaName); if(initialAromaIndex === -1) initialAromaIndex = 0; rotateDrum(initialAromaIndex); } initializeApp(); });