Spaces:
Running
Running
File size: 12,304 Bytes
5d88515 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
// 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();
}); |