// 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();
});