Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Brain Tumor Classification</title> | |
| <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| } | |
| .glass-effect { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .gradient-accent { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .spinner { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| .spinner.hidden { | |
| display: none; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div class="min-h-screen flex items-center justify-center px-4 py-8"> | |
| <div class="w-full max-w-2xl"> | |
| <!-- Header --> | |
| <div class="mb-8 text-center"> | |
| <h1 class="text-4xl font-bold text-gray-900 mb-2">Brain Tumor Classification</h1> | |
| <p class="text-gray-600">Upload an MRI scan for AI-powered analysis</p> | |
| </div> | |
| <!-- Main Card --> | |
| <div id="mainCard" class="glass-effect rounded-2xl shadow-xl p-8 mb-6"> | |
| <!-- Upload Section --> | |
| <div id="uploadSection" class="mb-8"> | |
| <label for="imageInput" class="block mb-4"> | |
| <div class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:border-purple-500 transition-colors"> | |
| <svg class="w-12 h-12 mx-auto text-gray-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path> | |
| </svg> | |
| <p class="text-gray-700 font-semibold mb-1">Click to upload or drag and drop</p> | |
| <p class="text-sm text-gray-500">PNG, JPG, GIF up to 10MB</p> | |
| </div> | |
| </label> | |
| <input type="file" id="imageInput" accept="image/*" class="hidden" /> | |
| </div> | |
| <!-- Preview Section --> | |
| <div id="previewSection" class="hidden mb-8"> | |
| <div class="relative rounded-xl overflow-hidden bg-gray-100 mb-4"> | |
| <img id="previewImage" src="" alt="Preview" class="w-full h-auto max-h-96 object-contain" /> | |
| </div> | |
| <button id="removeButton" class="w-full bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold py-2 rounded-lg transition-colors"> | |
| Choose Different Image | |
| </button> | |
| </div> | |
| <!-- Submit Button --> | |
| <button id="classifyButton" class="w-full gradient-accent text-white font-semibold py-3 rounded-lg hover:opacity-90 transition-opacity mb-4 flex items-center justify-center gap-2"> | |
| <span id="buttonText">Classify Image</span> | |
| <span id="spinner" class="hidden spinner"></span> | |
| </button> | |
| <!-- Error Message --> | |
| <div id="errorMessage" class="hidden bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm"></div> | |
| </div> | |
| <!-- Results Section --> | |
| <div id="resultsSection" class="hidden glass-effect rounded-2xl shadow-xl p-8"> | |
| <h2 class="text-2xl font-bold text-gray-900 mb-6">Classification Results</h2> | |
| <!-- Main Prediction --> | |
| <div class="mb-8 p-6 gradient-accent text-white rounded-xl"> | |
| <p class="text-sm font-semibold opacity-90 mb-2">DIAGNOSIS</p> | |
| <p id="mainPrediction" class="text-3xl font-bold mb-2">-</p> | |
| <p id="mainConfidence" class="text-lg opacity-90">Confidence: -%</p> | |
| </div> | |
| <!-- Detailed Breakdown --> | |
| <div class="mb-8"> | |
| <h3 class="text-lg font-semibold text-gray-900 mb-4">Confidence Scores</h3> | |
| <div id="predictionsList" class="space-y-3"></div> | |
| </div> | |
| <!-- Action Buttons --> | |
| <button id="analyzeButton" class="w-full gradient-accent text-white font-semibold py-3 rounded-lg hover:opacity-90 transition-opacity"> | |
| Analyze Another Image | |
| </button> | |
| </div> | |
| <!-- Footer --> | |
| <div class="mt-8 text-center text-gray-600 text-sm"> | |
| <p>Vision Transformer (ViT) powered classification</p> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const imageInput = document.getElementById('imageInput'); | |
| const uploadSection = document.getElementById('uploadSection'); | |
| const previewSection = document.getElementById('previewSection'); | |
| const previewImage = document.getElementById('previewImage'); | |
| const classifyButton = document.getElementById('classifyButton'); | |
| const removeButton = document.getElementById('removeButton'); | |
| const mainCard = document.getElementById('mainCard'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const errorMessage = document.getElementById('errorMessage'); | |
| const analyzeButton = document.getElementById('analyzeButton'); | |
| const mainPrediction = document.getElementById('mainPrediction'); | |
| const mainConfidence = document.getElementById('mainConfidence'); | |
| const predictionsList = document.getElementById('predictionsList'); | |
| const buttonText = document.getElementById('buttonText'); | |
| const spinner = document.getElementById('spinner'); | |
| imageInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| previewImage.src = event.target.result; | |
| uploadSection.classList.add('hidden'); | |
| previewSection.classList.remove('hidden'); | |
| resultsSection.classList.add('hidden'); | |
| errorMessage.classList.add('hidden'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| removeButton.addEventListener('click', () => { | |
| imageInput.value = ''; | |
| previewSection.classList.add('hidden'); | |
| uploadSection.classList.remove('hidden'); | |
| resultsSection.classList.add('hidden'); | |
| errorMessage.classList.add('hidden'); | |
| }); | |
| classifyButton.addEventListener('click', async () => { | |
| const file = imageInput.files[0]; | |
| if (!file) { | |
| showError('Please select an image'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| classifyButton.disabled = true; | |
| buttonText.textContent = 'Classifying...'; | |
| spinner.classList.remove('hidden'); | |
| errorMessage.classList.add('hidden'); | |
| try { | |
| const response = await fetch('/api/v1/classify', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| const error = await response.json(); | |
| showError(error.detail || 'Classification failed'); | |
| return; | |
| } | |
| const data = await response.json(); | |
| displayResults(data.prediction); | |
| } catch (error) { | |
| showError('Network error: ' + error.message); | |
| } finally { | |
| classifyButton.disabled = false; | |
| buttonText.textContent = 'Classify Image'; | |
| spinner.classList.add('hidden'); | |
| } | |
| }); | |
| analyzeButton.addEventListener('click', () => { | |
| imageInput.value = ''; | |
| previewSection.classList.add('hidden'); | |
| uploadSection.classList.remove('hidden'); | |
| resultsSection.classList.add('hidden'); | |
| mainCard.classList.remove('hidden'); | |
| errorMessage.classList.add('hidden'); | |
| }); | |
| function displayResults(prediction) { | |
| mainPrediction.textContent = prediction.predicted_class; | |
| mainConfidence.textContent = `Confidence: ${prediction.confidence}%`; | |
| predictionsList.innerHTML = ''; | |
| Object.entries(prediction.all_predictions).forEach(([className, confidence]) => { | |
| const progressPercent = Math.round(confidence); | |
| const barColor = className === prediction.predicted_class ? 'bg-purple-500' : 'bg-gray-300'; | |
| const html = ` | |
| <div> | |
| <div class="flex justify-between items-center mb-1"> | |
| <span class="text-gray-700 font-medium">${className}</span> | |
| <span class="text-gray-600 text-sm">${progressPercent}%</span> | |
| </div> | |
| <div class="w-full bg-gray-200 rounded-full h-2"> | |
| <div class="${barColor} h-2 rounded-full transition-all" style="width: ${progressPercent}%"></div> | |
| </div> | |
| </div> | |
| `; | |
| predictionsList.innerHTML += html; | |
| }); | |
| mainCard.classList.add('hidden'); | |
| resultsSection.classList.remove('hidden'); | |
| } | |
| function showError(message) { | |
| errorMessage.textContent = message; | |
| errorMessage.classList.remove('hidden'); | |
| } | |
| const dropZone = document.querySelector('[for="imageInput"]'); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| dropZone.addEventListener('drop', (e) => { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| imageInput.files = files; | |
| const event = new Event('change', { bubbles: true }); | |
| imageInput.dispatchEvent(event); | |
| }); | |
| </script> | |
| </body> | |
| </html> |