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