// 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 = `

Severity Level

${conditionInfo.severity.charAt(0).toUpperCase() + conditionInfo.severity.slice(1)}

This is based on typical characteristics of this condition.

`; } severityIndicator.innerHTML = severityHTML; // Display all probabilities probabilitiesContainer.innerHTML = ''; data.predictions.forEach(prediction => { const probabilityPercent = Math.round(prediction.confidence * 100); const probabilityHTML = `
${CLASS_DESCRIPTIONS[prediction.class].name}
${probabilityPercent}%
`; 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 = `
  • ${resource.name}
  • `; 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'); } }); }); }); });