bhd82's picture
Upload 22 files
d8c0d49 verified
// 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');
}
});
});
});
});