Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Batik Classifier - Test Web Interface</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| } | |
| .container { | |
| background: white; | |
| border-radius: 20px; | |
| padding: 40px; | |
| max-width: 600px; | |
| width: 100%; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #333; | |
| margin-bottom: 10px; | |
| } | |
| .subtitle { | |
| text-align: center; | |
| color: #666; | |
| margin-bottom: 30px; | |
| } | |
| .upload-area { | |
| border: 3px dashed #667eea; | |
| border-radius: 10px; | |
| padding: 40px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-bottom: 20px; | |
| } | |
| .upload-area:hover { | |
| border-color: #764ba2; | |
| background: #f8f9ff; | |
| } | |
| .upload-area.dragover { | |
| background: #e8ecff; | |
| border-color: #5568d3; | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .upload-icon { | |
| font-size: 48px; | |
| margin-bottom: 10px; | |
| } | |
| .btn { | |
| width: 100%; | |
| padding: 15px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| } | |
| .btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .preview { | |
| text-align: center; | |
| margin: 20px 0; | |
| } | |
| .preview img { | |
| max-width: 100%; | |
| max-height: 300px; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| } | |
| .result { | |
| margin-top: 20px; | |
| padding: 20px; | |
| background: #f8f9ff; | |
| border-radius: 10px; | |
| display: none; | |
| } | |
| .result.show { | |
| display: block; | |
| } | |
| .prediction { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #667eea; | |
| margin-bottom: 10px; | |
| } | |
| .confidence { | |
| font-size: 18px; | |
| color: #666; | |
| margin-bottom: 20px; | |
| } | |
| .top-predictions { | |
| margin-top: 20px; | |
| } | |
| .top-predictions h3 { | |
| margin-bottom: 10px; | |
| color: #333; | |
| } | |
| .pred-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 10px; | |
| background: white; | |
| border-radius: 5px; | |
| margin-bottom: 5px; | |
| } | |
| .loading { | |
| text-align: center; | |
| display: none; | |
| } | |
| .loading.show { | |
| display: block; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 20px auto; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .error { | |
| color: #e74c3c; | |
| background: #ffe8e8; | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin-top: 20px; | |
| display: none; | |
| } | |
| .error.show { | |
| display: block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🎨 Batik Classifier</h1> | |
| <p class="subtitle">AI-Powered Batik Motif Recognition (95% Accuracy)</p> | |
| <div class="upload-area" id="uploadArea"> | |
| <div class="upload-icon">📸</div> | |
| <p><strong>Click to upload</strong> or drag and drop</p> | |
| <p style="font-size: 14px; color: #999; margin-top: 5px;">PNG, JPG up to 10MB</p> | |
| <input type="file" id="fileInput" accept="image/*"> | |
| </div> | |
| <div class="preview" id="preview"></div> | |
| <button class="btn" id="predictBtn" disabled>🔍 Analyze Batik</button> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p>Analyzing image...</p> | |
| </div> | |
| <div class="error" id="error"></div> | |
| <div class="result" id="result"> | |
| <div class="prediction"> | |
| <span id="predictionText"></span> | |
| </div> | |
| <div class="confidence"> | |
| Confidence: <strong id="confidenceText"></strong> | |
| </div> | |
| <div class="top-predictions"> | |
| <h3>📊 Top 5 Predictions:</h3> | |
| <div id="topPredictions"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_URL = 'http://192.168.0.106:5000'; | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const predictBtn = document.getElementById('predictBtn'); | |
| const preview = document.getElementById('preview'); | |
| const loading = document.getElementById('loading'); | |
| const result = document.getElementById('result'); | |
| const error = document.getElementById('error'); | |
| let selectedFile = null; | |
| // Click to upload | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| // File selection | |
| fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| handleFile(file); | |
| } | |
| }); | |
| // Drag and drop | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('dragover'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| const file = e.dataTransfer.files[0]; | |
| if (file && file.type.startsWith('image/')) { | |
| handleFile(file); | |
| } | |
| }); | |
| // Handle file | |
| function handleFile(file) { | |
| selectedFile = file; | |
| // Show preview | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| preview.innerHTML = `<img src="${e.target.result}" alt="Preview">`; | |
| }; | |
| reader.readAsDataURL(file); | |
| // Enable predict button | |
| predictBtn.disabled = false; | |
| // Hide previous results | |
| result.classList.remove('show'); | |
| error.classList.remove('show'); | |
| } | |
| // Predict | |
| predictBtn.addEventListener('click', async () => { | |
| if (!selectedFile) return; | |
| // Show loading | |
| loading.classList.add('show'); | |
| result.classList.remove('show'); | |
| error.classList.remove('show'); | |
| predictBtn.disabled = true; | |
| try { | |
| const formData = new FormData(); | |
| formData.append('image', selectedFile); | |
| const response = await fetch(`${API_URL}/predict`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| // Show result | |
| document.getElementById('predictionText').textContent = data.prediction; | |
| document.getElementById('confidenceText').textContent = data.percentage; | |
| const topPredDiv = document.getElementById('topPredictions'); | |
| topPredDiv.innerHTML = data.top_5_predictions.map((pred, idx) => ` | |
| <div class="pred-item"> | |
| <span>${idx + 1}. ${pred.class}</span> | |
| <strong>${pred.percentage}</strong> | |
| </div> | |
| `).join(''); | |
| result.classList.add('show'); | |
| } else { | |
| error.textContent = data.error || 'Prediction failed'; | |
| error.classList.add('show'); | |
| } | |
| } catch (err) { | |
| error.textContent = `Error: ${err.message}. Make sure the API server is running.`; | |
| error.classList.add('show'); | |
| } finally { | |
| loading.classList.remove('show'); | |
| predictBtn.disabled = false; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |