Spaces:
Running
Running
// 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 = `<span class="color-swatch" style="background-color: ${color};"></span>`; | |
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 = `<em>${data.slogan || "An enigmatic essence."}</em>`; | |
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(); | |
}); |