Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>X-ray Fracture Detection</title> | |
| <!-- Tailwind CSS for styling --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Inter font from Google Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| /* Simple spinner animation */ | |
| .loader { | |
| border: 4px solid #f3f3f3; | |
| border-radius: 50%; | |
| border-top: 4px solid #3498db; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen flex items-center justify-center"> | |
| <div class="w-full max-w-lg mx-auto bg-white rounded-xl shadow-lg p-8 md:p-12"> | |
| <!-- Header --> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-3xl font-bold text-gray-800">Fracture Detection AI</h1> | |
| <p class="text-gray-500 mt-2">Upload an X-ray image to check for fractures.</p> | |
| </div> | |
| <!-- File Upload Section --> | |
| <div> | |
| <!-- Styled file input --> | |
| <label for="file-upload" class="w-full cursor-pointer bg-gray-200 text-gray-700 font-semibold py-3 px-4 rounded-lg inline-flex items-center justify-center hover:bg-gray-300 transition duration-300"> | |
| <svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg> | |
| <span id="file-label">Select an X-ray Image</span> | |
| </label> | |
| <input id="file-upload" type="file" class="hidden" accept="image/*"> | |
| </div> | |
| <!-- Image Preview --> | |
| <div id="image-preview-container" class="mt-6 text-center hidden"> | |
| <p class="text-sm font-medium text-gray-600 mb-2">Image Preview:</p> | |
| <img id="image-preview" src="#" alt="Image preview" class="max-w-xs mx-auto rounded-lg shadow-md"/> | |
| </div> | |
| <!-- Predict Button --> | |
| <div class="mt-8"> | |
| <button id="predict-button" class="w-full bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 transition duration-300 disabled:bg-gray-400 disabled:cursor-not-allowed" disabled> | |
| Upload & Predict | |
| </button> | |
| </div> | |
| <!-- Results Section --> | |
| <div id="results" class="mt-8 text-center hidden"> | |
| <!-- Spinner --> | |
| <div id="loader" class="loader mx-auto hidden"></div> | |
| <!-- Result Text --> | |
| <div id="result-text" class="mt-4 p-4 rounded-lg"></div> | |
| </div> | |
| <!-- Error Message --> | |
| <div id="error-message" class="mt-6 text-center text-red-600 font-semibold hidden"></div> | |
| </div> | |
| <script> | |
| const fileUpload = document.getElementById('file-upload'); | |
| const fileLabel = document.getElementById('file-label'); | |
| const imagePreviewContainer = document.getElementById('image-preview-container'); | |
| const imagePreview = document.getElementById('image-preview'); | |
| const predictButton = document.getElementById('predict-button'); | |
| const resultsDiv = document.getElementById('results'); | |
| const resultText = document.getElementById('result-text'); | |
| const loader = document.getElementById('loader'); | |
| const errorMessage = document.getElementById('error-message'); | |
| let selectedFile = null; | |
| // Listen for file selection | |
| fileUpload.addEventListener('change', (event) => { | |
| selectedFile = event.target.files[0]; | |
| if (selectedFile) { | |
| // Update label text | |
| fileLabel.textContent = selectedFile.name; | |
| // Show image preview | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| imagePreview.src = e.target.result; | |
| imagePreviewContainer.classList.remove('hidden'); | |
| }; | |
| reader.readAsDataURL(selectedFile); | |
| // Enable predict button | |
| predictButton.disabled = false; | |
| // Reset previous results | |
| resultsDiv.classList.add('hidden'); | |
| errorMessage.classList.add('hidden'); | |
| } | |
| }); | |
| // Listen for predict button click | |
| predictButton.addEventListener('click', async () => { | |
| if (!selectedFile) return; | |
| // Prepare for API call | |
| const formData = new FormData(); | |
| formData.append('file', selectedFile); | |
| // Show loading spinner and hide previous results | |
| resultsDiv.classList.remove('hidden'); | |
| loader.classList.remove('hidden'); | |
| resultText.classList.add('hidden'); | |
| errorMessage.classList.add('hidden'); | |
| predictButton.disabled = true; | |
| try { | |
| // IMPORTANT: This assumes your FastAPI server is running on http://127.0.0.1:8000 | |
| const response = await fetch('http://127.0.0.1:8000/predict', { | |
| method: 'POST', | |
| body: formData, | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.detail || `Server error: ${response.statusText}`); | |
| } | |
| const data = await response.json(); | |
| // Display the result | |
| displayResult(data); | |
| } catch (error) { | |
| // Display error message | |
| console.error('Error:', error); | |
| errorMessage.textContent = `Error: Could not connect to the API or the file is invalid. Make sure the server is running. Details: ${error.message}`; | |
| errorMessage.classList.remove('hidden'); | |
| resultsDiv.classList.add('hidden'); | |
| } finally { | |
| // Hide loader and re-enable button | |
| loader.classList.add('hidden'); | |
| predictButton.disabled = false; | |
| } | |
| }); | |
| function displayResult(data) { | |
| const prediction = data.prediction; | |
| const confidence = parseFloat(data.confidence) * 100; | |
| // Clear previous styles | |
| resultText.classList.remove('bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-yellow-100', 'text-yellow-800'); | |
| let resultHTML = `<p class="text-xl font-bold">Prediction: <span class="capitalize">${prediction}</span></p> | |
| <p class="text-md mt-1">Confidence: ${confidence.toFixed(2)}%</p>`; | |
| // Style based on prediction | |
| if (prediction.toLowerCase().includes('fracture')) { | |
| resultText.classList.add('bg-red-100', 'text-red-800'); | |
| } else { | |
| resultText.classList.add('bg-green-100', 'text-green-800'); | |
| } | |
| resultText.innerHTML = resultHTML; | |
| resultText.classList.remove('hidden'); | |
| } | |
| </script> | |
| </body> | |
| </html> | |