| <!DOCTYPE html>
|
| <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>
|
|
|
| <script src="https://cdn.tailwindcss.com"></script>
|
|
|
| <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;
|
| }
|
|
|
| .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">
|
|
|
|
|
| <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>
|
|
|
|
|
| <div>
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <div id="results" class="mt-8 text-center hidden">
|
|
|
| <div id="loader" class="loader mx-auto hidden"></div>
|
|
|
| <div id="result-text" class="mt-4 p-4 rounded-lg"></div>
|
| </div>
|
|
|
|
|
| <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;
|
|
|
|
|
| fileUpload.addEventListener('change', (event) => {
|
| selectedFile = event.target.files[0];
|
| if (selectedFile) {
|
|
|
| fileLabel.textContent = selectedFile.name;
|
|
|
|
|
| const reader = new FileReader();
|
| reader.onload = (e) => {
|
| imagePreview.src = e.target.result;
|
| imagePreviewContainer.classList.remove('hidden');
|
| };
|
| reader.readAsDataURL(selectedFile);
|
|
|
|
|
| predictButton.disabled = false;
|
|
|
|
|
| resultsDiv.classList.add('hidden');
|
| errorMessage.classList.add('hidden');
|
| }
|
| });
|
|
|
|
|
| predictButton.addEventListener('click', async () => {
|
| if (!selectedFile) return;
|
|
|
|
|
| const formData = new FormData();
|
| formData.append('file', selectedFile);
|
|
|
|
|
| resultsDiv.classList.remove('hidden');
|
| loader.classList.remove('hidden');
|
| resultText.classList.add('hidden');
|
| errorMessage.classList.add('hidden');
|
| predictButton.disabled = true;
|
|
|
| try {
|
|
|
| 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();
|
|
|
|
|
| displayResult(data);
|
|
|
| } catch (error) {
|
|
|
| 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 {
|
|
|
| loader.classList.add('hidden');
|
| predictButton.disabled = false;
|
| }
|
| });
|
|
|
| function displayResult(data) {
|
| const prediction = data.prediction;
|
| const confidence = parseFloat(data.confidence) * 100;
|
|
|
|
|
| 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>`;
|
|
|
|
|
| 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>
|
|
|