batik-classifier / test_web.html
Maftuuh1922
Initial commit - BatikLens API v2 (clean)
aa5c395
<!DOCTYPE html>
<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>