File size: 20,979 Bytes
d8c0d49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551

// At the beginning of your file, add this to ensure CLASS_DESCRIPTIONS is defined
// This assumes your class descriptions are passed from the Flask backend
const CLASS_DESCRIPTIONS = window.CLASS_DESCRIPTIONS || {
    'akiec': { name: 'Actinic Keratosis', description: 'A precancerous growth caused by sun damage.' },
    'bcc': { name: 'Basal Cell Carcinoma', description: 'The most common type of skin cancer.' },
    'bkl': { name: 'Benign Keratosis', description: 'A non-cancerous growth on the skin.' },
    'df': { name: 'Dermatofibroma', description: 'A common benign skin growth.' },
    'mel': { name: 'Melanoma', description: 'The most serious form of skin cancer.' },
    'nv': { name: 'Melanocytic Nevus', description: 'A common mole.' },
    'vasc': { name: 'Vascular Lesion', description: 'An abnormality of blood vessels.' }
};

// Define CONDITION_INFO if not already defined
const CONDITION_INFO = window.CONDITION_INFO || {
    'akiec': {
        severity: 'moderate',
        description: 'Actinic Keratosis is a precancerous growth caused by sun damage.',
        resources: [{ name: 'Mayo Clinic', url: 'https://www.mayoclinic.org/diseases-conditions/actinic-keratosis/symptoms-causes/syc-20354969' }]
    },
    'bcc': {
        severity: 'high',
        description: 'Basal Cell Carcinoma is the most common type of skin cancer.',
        resources: [{ name: 'Skin Cancer Foundation', url: 'https://www.skincancer.org/skin-cancer-information/basal-cell-carcinoma/' }]
    },
    'bkl': {
        severity: 'low',
        description: 'Benign Keratosis is a non-cancerous growth that appears as a waxy, scaly growth on the skin.',
        resources: [{ name: 'American Academy of Dermatology', url: 'https://www.aad.org/public/diseases/bumps-and-growths/seborrheic-keratoses' }]
    },
    'df': {
        severity: 'low',
        description: 'Dermatofibroma is a common benign skin growth that often appears as a small, firm bump.',
        resources: [{ name: 'DermNet NZ', url: 'https://dermnetnz.org/topics/dermatofibroma' }]
    },
    'mel': {
        severity: 'very high',
        description: 'Melanoma is the most serious form of skin cancer that develops in the cells that produce melanin.',
        resources: [{ name: 'American Cancer Society', url: 'https://www.cancer.org/cancer/melanoma-skin-cancer.html' }]
    },
    'nv': {
        severity: 'low',
        description: 'Melanocytic Nevus is a common mole that appears as a small, dark brown spot caused by clusters of pigmented cells.',
        resources: [{ name: 'Cleveland Clinic', url: 'https://my.clevelandclinic.org/health/diseases/21880-moles' }]
    },
    'vasc': {
        severity: 'moderate',
        description: 'Vascular Lesion is an abnormality of blood vessels that can appear as red or purple marks on the skin.',
        resources: [{ name: 'Stanford Health Care', url: 'https://stanfordhealthcare.org/medical-conditions/skin-hair-and-nails/vascular-malformations.html' }]
    }
};


document.addEventListener('DOMContentLoaded', function() {
    // DOM Elements
    const uploadBox = document.getElementById('upload-box');
    const fileInput = document.getElementById('file-input');
    const previewContainer = document.getElementById('preview-container');
    const previewImage = document.getElementById('image-preview')
    const removeImageBtn = document.getElementById('remove-image');
    const analyzeBtn = document.getElementById('analyze-button');
    const uploadForm = document.getElementById('upload-form');
    const loadingIndicator = document.getElementById('loading');
    const resultContainer = document.getElementById('result');
    const errorMessage = document.getElementById('error-message');
    const errorText = document.getElementById('error-text');
    const modelSelect = document.getElementById('model-select');
    const modelUsedBadge = document.getElementById('model-used-badge');

    // Tab elements
    const tabButtons = document.querySelectorAll('.tab-button');
    const tabPanes = document.querySelectorAll('.tab-pane');

    // Result elements
    const resultImage = document.getElementById('result-image');
    const predictionElement = document.getElementById('prediction');
    const descriptionElement = document.getElementById('description');
    const confidenceElement = document.getElementById('confidence');
    const confidenceFill = document.getElementById('confidence-fill');
    const probabilitiesContainer = document.getElementById('probabilities');
    const severityIndicator = document.getElementById('severity-indicator');
    const infoConditionName = document.getElementById('info-condition-name');
    const conditionInformation = document.getElementById('condition-information');
    const resourcesList = document.getElementById('resources-list');
    const saveResultBtn = document.getElementById('save-result');
    const newAnalysisBtn = document.getElementById('new-analysis');

    // State
    let fileSelected = false;

    // Event Listeners
    uploadBox.addEventListener('click', function() {
        fileInput.click();
    });

    fileInput.addEventListener('change', handleFileSelect);

    removeImageBtn.addEventListener('click', function(e) {
        e.stopPropagation();
        resetUpload();
    });

    uploadForm.addEventListener('submit', handleFormSubmit);

    tabButtons.forEach(button => {
        button.addEventListener('click', function() {
            const tabName = this.getAttribute('data-tab');
            switchTab(tabName);
        });
    });

    newAnalysisBtn.addEventListener('click', resetAll);

    saveResultBtn.addEventListener('click', saveResults);

    // Functions
    function handleFileSelect(e) {
        const file = e.target.files[0];

        if (!file) return;

        if (!file.type.match('image.*')) {
            showError('Please select an image file (JPEG, PNG, etc.)');
            return;
        }

        const reader = new FileReader();

        reader.onload = function(e) {
            previewImage.src = e.target.result;
            previewContainer.classList.remove('hidden');
            removeImageBtn.classList.remove('hidden');
            uploadBox.classList.add('has-image');
            fileSelected = true;
            analyzeBtn.disabled = false;
        };

        reader.readAsDataURL(file);
    }

    function resetUpload() {
        fileInput.value = '';
        previewContainer.classList.add('hidden');
        removeImageBtn.classList.add('hidden');
        uploadBox.classList.remove('has-image');
        fileSelected = false;
        analyzeBtn.disabled = true;
    }

    function handleFormSubmit(e) {
        e.preventDefault();

        if (!fileSelected) {
            showError('Please select an image to analyze');
            return;
        }

        // Hide any previous results or errors
        resultContainer.classList.add('hidden');
        errorMessage.classList.add('hidden');

        // Show loading indicator
        loadingIndicator.style.display = 'block';

        // Get the selected model
        const selectedModel = modelSelect.value;

        // Create form data
        const formData = new FormData();
        formData.append('file', fileInput.files[0]);
        formData.append('model', selectedModel);

        console.log('Uploading file:', fileInput.files[0].name);
        console.log('Selected model:', selectedModel);

        // Send the request
        fetch('/predict', {
            method: 'POST',
            body: formData
        })
        .then(response => {
            if (!response.ok) {
                return response.json().then(data => {
                    throw new Error(data.error || `Server error: ${response.status}`);
                }).catch(e => {
                    // If we can't parse JSON, use the status text
                    throw new Error(`Server error: ${response.status} ${response.statusText}`);
                });
            }
            return response.json();
        })
        .then(data => {
            // Hide loading indicator
            loadingIndicator.style.display = 'none';

            // Check if we have predictions
            if (!data.predictions || data.predictions.length === 0) {
                throw new Error('No predictions returned from the server');
            }

            // Display results
            displayResults(data, selectedModel);
        })
        .catch(error => {
            // Hide loading indicator
            loadingIndicator.style.display = 'none';

            // Show error message
            showError(`Error: ${error.message}`);
            console.error('Error:', error);
        });
    }

    function displayResults(data, modelName) {
        // Set the model badge
        modelUsedBadge.textContent = modelName;

        // Set the result image
        resultImage.src = previewImage.src;

        // Get the top prediction
        const topPrediction = data.predictions[0];
        const predictionClass = topPrediction.class;
        const confidence = topPrediction.confidence;

        // Set prediction details
        predictionElement.textContent = CLASS_DESCRIPTIONS[predictionClass].name;
        descriptionElement.textContent = CLASS_DESCRIPTIONS[predictionClass].description;

        // Set confidence
        const confidencePercent = Math.round(confidence * 100);
        confidenceElement.textContent = confidencePercent + '%';
        confidenceFill.style.width = confidencePercent + '%';

        // Set confidence color based on value
        if (confidencePercent >= 80) {
            confidenceFill.style.backgroundColor = 'var(--success-color)';
        } else if (confidencePercent >= 50) {
            confidenceFill.style.backgroundColor = 'var(--warning-color)';
        } else {
            confidenceFill.style.backgroundColor = 'var(--danger-color)';
        }

        // Display severity indicator
        const conditionInfo = CONDITION_INFO[predictionClass];
        let severityHTML = '';

        if (conditionInfo && conditionInfo.severity) {
            let severityClass = '';
            let severityIcon = '';

            switch (conditionInfo.severity) {
                case 'low':
                    severityClass = 'severity-low';
                    severityIcon = 'fa-check-circle';
                    break;
                case 'moderate':
                    severityClass = 'severity-moderate';
                    severityIcon = 'fa-exclamation-circle';
                    break;
                case 'high':
                    severityClass = 'severity-high';
                    severityIcon = 'fa-exclamation-triangle';
                    break;
                case 'very high':
                    severityClass = 'severity-very-high';
                    severityIcon = 'fa-radiation';
                    break;
            }

            severityHTML = `
            <h3>Severity Level</h3>
            <div class="severity-level ${severityClass}">
            <i class="fas ${severityIcon}"></i>
            <span class="severity-text">${conditionInfo.severity.charAt(0).toUpperCase() + conditionInfo.severity.slice(1)}</span>
            </div>
            <p class="mt-2">This is based on typical characteristics of this condition.</p>
            `;
        }

        severityIndicator.innerHTML = severityHTML;

        // Display all probabilities
        probabilitiesContainer.innerHTML = '';
        data.predictions.forEach(prediction => {
            const probabilityPercent = Math.round(prediction.confidence * 100);
            const probabilityHTML = `
            <div class="probability-item">
            <div class="probability-class">${CLASS_DESCRIPTIONS[prediction.class].name}</div>
            <div class="probability-bar">
            <div class="probability-fill" style="width: ${probabilityPercent}%"></div>
            </div>
            <div class="probability-value">${probabilityPercent}%</div>
            </div>
            `;
            probabilitiesContainer.innerHTML += probabilityHTML;
        });

        // Set condition information
        infoConditionName.textContent = CLASS_DESCRIPTIONS[predictionClass].name;

        if (conditionInfo && conditionInfo.description) {
            conditionInformation.textContent = conditionInfo.description;
        } else {
            conditionInformation.textContent = CLASS_DESCRIPTIONS[predictionClass].description;
        }

        // Set resources
        resourcesList.innerHTML = '';
        if (conditionInfo && conditionInfo.resources) {
            conditionInfo.resources.forEach(resource => {
                const resourceHTML = `
                <li>
                <a href="${resource.url}" target="_blank">
                <i class="fas fa-external-link-alt"></i>
                ${resource.name}
                </a>
                </li>
                `;
                resourcesList.innerHTML += resourceHTML;
            });
        }

        // Show the result container
        resultContainer.classList.remove('hidden');

        // Switch to the first tab
        switchTab('probabilities');

        // Scroll to results
        resultContainer.scrollIntoView({ behavior: 'smooth' });
    }

    function showError(message) {
        errorText.textContent = message;
        errorMessage.classList.remove('hidden');
    }

    function switchTab(tabName) {
        // Update active tab button
        tabButtons.forEach(button => {
            if (button.getAttribute('data-tab') === tabName) {
                button.classList.add('active');
            } else {
                button.classList.remove('active');
            }
        });

        // Update active tab pane
        tabPanes.forEach(pane => {
            if (pane.id === tabName + '-tab') {
                pane.classList.add('active');
            } else {
                pane.classList.remove('active');
            }
        });
    }

    function resetAll() {
        resetUpload();
        resultContainer.classList.add('hidden');
        errorMessage.classList.add('hidden');
    }

    function saveResults() {
        // Create a canvas element
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        // Set canvas dimensions
        canvas.width = 800;
        canvas.height = 1200;

        // Fill background
        ctx.fillStyle = '#ffffff';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Draw header
        ctx.fillStyle = '#2A5C82';
        ctx.fillRect(0, 0, canvas.width, 100);

        // Draw header text
        ctx.font = 'bold 30px Poppins, sans-serif';
        ctx.fillStyle = '#ffffff';
        ctx.textAlign = 'center';
        ctx.fillText('SkinAI Analysis Results', canvas.width / 2, 60);

        // Load and draw the image
        const img = new Image();
        img.onload = function() {
            // Calculate image dimensions to maintain aspect ratio
            const maxWidth = 400;
            const maxHeight = 300;
            let width = img.width;
            let height = img.height;

            if (width > height) {
                if (width > maxWidth) {
                    height *= maxWidth / width;
                    width = maxWidth;
                }
            } else {
                if (height > maxHeight) {
                    width *= maxHeight / height;
                    height = maxHeight;
                }
            }

            // Draw image
            const x = (canvas.width - width) / 2;
            ctx.drawImage(img, x, 130, width, height);

            // Draw prediction info
            ctx.fillStyle = '#333333';
            ctx.font = 'bold 24px Poppins, sans-serif';
            ctx.textAlign = 'center';
            ctx.fillText('Primary Prediction', canvas.width / 2, 480);

            ctx.font = 'bold 28px Poppins, sans-serif';
            ctx.fillStyle = '#2A5C82';
            ctx.fillText(predictionElement.textContent, canvas.width / 2, 520);

            // Draw confidence
            ctx.font = '18px Poppins, sans-serif';
            ctx.fillStyle = '#333333';
            ctx.fillText(`Confidence: ${confidenceElement.textContent}`, canvas.width / 2, 550);

            // Draw description
            ctx.font = '16px Poppins, sans-serif';
            ctx.fillStyle = '#666666';
            ctx.textAlign = 'left';

            // Wrap text function
            const wrapText = function(context, text, x, y, maxWidth, lineHeight) {
                const words = text.split(' ');
                let line = '';
                let testLine = '';
                let lineArray = [];

                for (let n = 0; n < words.length; n++) {
                    testLine = line + words[n] + ' ';
                    const metrics = context.measureText(testLine);
                    const testWidth = metrics.width;

                    if (testWidth > maxWidth && n > 0) {
                        lineArray.push([line, x, y]);
                        line = words[n] + ' ';
                        y += lineHeight;
                    } else {
                        line = testLine;
                    }
                }

                lineArray.push([line, x, y]);
                return lineArray;
            };

            const descriptionLines = wrapText(ctx, descriptionElement.textContent, 100, 600, canvas.width - 200, 25);

            for (let i = 0; i < descriptionLines.length; i++) {
                ctx.fillText(descriptionLines[i][0], descriptionLines[i][1], descriptionLines[i][2]);
            }

            // Draw probabilities title
            ctx.font = 'bold 24px Poppins, sans-serif';
            ctx.fillStyle = '#333333';
            ctx.textAlign = 'center';
            ctx.fillText('Class Probabilities', canvas.width / 2, 700);

            // Draw probabilities
            const predictions = Array.from(document.querySelectorAll('.probability-item'));
            let yPos = 740;

            predictions.slice(0, 5).forEach(prediction => {
                const className = prediction.querySelector('.probability-class').textContent;
                const probabilityValue = prediction.querySelector('.probability-value').textContent;

                ctx.font = '16px Poppins, sans-serif';
                ctx.fillStyle = '#333333';
                ctx.textAlign = 'left';
                ctx.fillText(className, 200, yPos);

                ctx.textAlign = 'right';
                ctx.fillText(probabilityValue, canvas.width - 200, yPos);

                // Draw probability bar background
                ctx.fillStyle = '#e0e0e0';
                ctx.fillRect(200, yPos + 10, 400, 10);

                // Draw probability bar fill
                ctx.fillStyle = '#2A5C82';
                const width = parseInt(probabilityValue) * 4; // Scale to 400px max width
                ctx.fillRect(200, yPos + 10, width, 10);

                yPos += 40;
            });

            // Draw disclaimer
            ctx.fillStyle = '#fff8e1';
            ctx.fillRect(100, 1000, canvas.width - 200, 100);

            ctx.font = 'bold 16px Poppins, sans-serif';
            ctx.fillStyle = '#856404';
            ctx.textAlign = 'center';
            ctx.fillText('Important Disclaimer', canvas.width / 2, 1030);

            ctx.font = '14px Poppins, sans-serif';
            ctx.fillText('This AI analysis is for educational purposes only and is not a medical diagnosis.', canvas.width / 2, 1060);
            ctx.fillText('Always consult a healthcare professional for medical concerns.', canvas.width / 2, 1080);

            // Draw date
            const date = new Date();
            ctx.font = '12px Poppins, sans-serif';
            ctx.fillStyle = '#999999';
            ctx.textAlign = 'right';
            ctx.fillText(`Generated on: ${date.toLocaleDateString()} ${date.toLocaleTimeString()}`, canvas.width - 100, 1150);

            // Convert to image and download
            const dataUrl = canvas.toDataURL('image/png');
            const link = document.createElement('a');
            link.download = 'skinai-analysis.png';
            link.href = dataUrl;
            link.click();
        };

        img.src = resultImage.src;
    }
});

// FAQ Accordion
document.addEventListener('DOMContentLoaded', function() {
    const faqItems = document.querySelectorAll('.faq-item');

    faqItems.forEach(item => {
        const question = item.querySelector('.faq-question');

        question.addEventListener('click', () => {
            // Toggle active class on the clicked item
            item.classList.toggle('active');

            // Close other items
            faqItems.forEach(otherItem => {
                if (otherItem !== item) {
                    otherItem.classList.remove('active');
                }
            });
        });
    });
});