Sara-Adjo's picture
Update templates/index.html
1374302 verified
Raw
History Blame Contribute Delete
15.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Intel Scene Classifier</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet" />
<style>
/* Reset & CSS vars */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0a0f;
--surface: #111118;
--border: rgba(255,255,255,0.08);
--accent: #7c6aff;
--accent2: #ff6ab0;
--text: #e8e8f0;
--muted: #6b6b80;
--success: #4dffb0;
--radius: 16px;
--mono: "Space Mono", monospace;
--sans: "Syne", sans-serif;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px 80px;
}
/* Animated grid background */
body::before {
content: "";
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(124,106,255,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(124,106,255,0.04) 1px, transparent 1px);
background-size: 48px 48px;
pointer-events: none;
z-index: 0;
}
/* Header */
header {
text-align: center;
margin-bottom: 48px;
position: relative;
z-index: 1;
}
.logo-badge {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.15em;
color: var(--accent);
background: rgba(124,106,255,0.1);
border: 1px solid rgba(124,106,255,0.3);
padding: 6px 16px;
border-radius: 100px;
margin-bottom: 20px;
}
.logo-badge::before {
content: "";
width: 6px; height: 6px;
background: var(--accent);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.8); }
}
h1 {
font-size: clamp(2rem, 5vw, 3.4rem);
font-weight: 800;
line-height: 1.1;
background: linear-gradient(135deg, #fff 0%, var(--accent) 60%, var(--accent2) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
color: var(--muted);
font-family: var(--mono);
font-size: 13px;
margin-top: 12px;
letter-spacing: 0.05em;
}
/* Main card */
.card {
width: 100%;
max-width: 640px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 36px;
position: relative;
z-index: 1;
backdrop-filter: blur(12px);
}
.card + .card { margin-top: 24px; }
/* Form */
.field-label {
font-size: 11px;
letter-spacing: 0.12em;
color: var(--muted);
font-family: var(--mono);
text-transform: uppercase;
margin-bottom: 10px;
display: block;
}
/* Model selector */
.model-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 28px;
}
.model-option { display: none; }
.model-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 18px 12px;
border: 1px solid var(--border);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
background: transparent;
user-select: none;
}
.model-btn:hover {
border-color: var(--accent);
background: rgba(124,106,255,0.06);
}
.model-option:checked + .model-btn {
border-color: var(--accent);
background: rgba(124,106,255,0.12);
box-shadow: 0 0 0 1px var(--accent);
}
.model-btn .icon {
font-size: 28px;
}
.model-btn .name {
font-weight: 600;
font-size: 14px;
}
.model-btn .tag {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
}
/* File upload zone */
.upload-zone {
border: 2px dashed var(--border);
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
margin-bottom: 24px;
}
.upload-zone:hover, .upload-zone.drag-over {
border-color: var(--accent);
background: rgba(124,106,255,0.05);
}
.upload-zone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}
.upload-icon { font-size: 40px; margin-bottom: 12px; }
.upload-text {
font-size: 15px;
font-weight: 600;
}
.upload-hint {
font-size: 12px;
font-family: var(--mono);
color: var(--muted);
margin-top: 6px;
}
#file-name {
margin-top: 10px;
font-size: 12px;
font-family: var(--mono);
color: var(--accent);
word-break: break-all;
}
/* Preview image */
#preview-wrap {
display: none;
margin-bottom: 24px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border);
}
#preview-wrap img {
width: 100%;
max-height: 260px;
object-fit: cover;
display: block;
}
/* Submit button */
.btn-predict {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: #fff;
font-family: var(--sans);
font-size: 16px;
font-weight: 700;
border: none;
border-radius: 12px;
cursor: pointer;
transition: opacity 0.2s, transform 0.15s;
letter-spacing: 0.03em;
}
.btn-predict:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-predict:active { transform: translateY(0); }
/* Loading state */
.btn-predict.loading {
pointer-events: none;
opacity: 0.7;
}
/* Error banner */
.error-banner {
background: rgba(255, 80, 80, 0.1);
border: 1px solid rgba(255, 80, 80, 0.3);
color: #ff7070;
padding: 14px 18px;
border-radius: 10px;
font-family: var(--mono);
font-size: 13px;
margin-bottom: 24px;
}
/* Result section */
.result-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 28px;
}
.result-thumb {
width: 80px;
height: 80px;
border-radius: 10px;
object-fit: cover;
border: 2px solid var(--accent);
flex-shrink: 0;
}
.result-meta { flex: 1; }
.result-label {
font-size: 11px;
font-family: var(--mono);
color: var(--muted);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 4px;
}
.result-class {
font-size: 28px;
font-weight: 800;
display: flex;
align-items: center;
gap: 10px;
}
.result-conf {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-family: var(--mono);
font-size: 13px;
color: var(--success);
}
.model-pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
padding: 3px 10px;
border-radius: 100px;
background: rgba(124,106,255,0.15);
border: 1px solid rgba(124,106,255,0.3);
color: var(--accent);
text-transform: uppercase;
margin-top: 8px;
}
/* Probability bars */
.prob-list { display: flex; flex-direction: column; gap: 10px; }
.prob-row {
display: grid;
grid-template-columns: 90px 1fr 54px;
align-items: center;
gap: 12px;
}
.prob-name {
font-size: 13px;
font-family: var(--mono);
color: var(--muted);
white-space: nowrap;
}
.prob-bar-bg {
height: 8px;
background: rgba(255,255,255,0.05);
border-radius: 100px;
overflow: hidden;
}
.prob-bar-fill {
height: 100%;
border-radius: 100px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
transition: width 0.6s cubic-bezier(0.4,0,0.2,1);
}
.prob-val {
font-family: var(--mono);
font-size: 12px;
color: var(--text);
text-align: right;
}
.prob-row.top .prob-name { color: var(--text); font-weight: bold; }
.prob-row.top .prob-val { color: var(--success); }
/* Try again button */
.btn-reset {
margin-top: 24px;
display: block;
text-align: center;
padding: 12px;
border: 1px solid var(--border);
border-radius: 10px;
color: var(--muted);
font-size: 13px;
font-family: var(--mono);
text-decoration: none;
transition: all 0.2s;
}
.btn-reset:hover {
border-color: var(--accent);
color: var(--accent);
}
/* Footer */
footer {
margin-top: 48px;
font-size: 11px;
font-family: var(--mono);
color: var(--muted);
text-align: center;
position: relative;
z-index: 1;
}
footer span { color: var(--accent); }
/* Responsive */
@media (max-width: 480px) {
.card { padding: 24px 18px; }
.model-btn .icon { font-size: 22px; }
}
</style>
</head>
<body>
<header>
<div class="logo-badge">NEURAL SCENE CLASSIFIER</div>
<h1><br/>Image Classification</h1>
<p class="subtitle">· PyTorch & TensorFlow ·</p>
</header>
{% if error %}
<div class="card" style="max-width:640px;width:100%;position:relative;z-index:1;">
<div class="error-banner">⚠ {{ error }}</div>
</div>
{% endif %}
{% if result %}
<div class="card">
<div class="result-header">
<img class="result-thumb"
src="data:image/jpeg;base64,{{ img_b64 }}"
alt="uploaded image" />
<div class="result-meta">
<div class="result-label">Predicted Scene</div>
<div class="result-class">
{{ class_icons.get(result.predicted_class, '🔍') }}
{{ result.predicted_class | capitalize }}
</div>
<div class="result-conf">
✓ {{ result.confidence }}% confidence
</div>
<div class="model-pill">
{% if model_used == 'pytorch' %}🔥 PyTorch{% else %}⚡ TensorFlow{% endif %}
</div>
</div>
</div>
<!-- Probability bars -->
<span class="field-label">All class probabilities</span>
<div class="prob-list">
{% for cls, prob in sorted_probs %}
<div class="prob-row {% if cls == result.predicted_class %}top{% endif %}">
<span class="prob-name">
{{ class_icons.get(cls, '') }} {{ cls }}
</span>
<div class="prob-bar-bg">
<div class="prob-bar-fill" :style="{ width: prob + '%' }"></div>
</div>
<span class="prob-val">{{ prob }}%</span>
</div>
{% endfor %}
</div>
<a href="/" class="btn-reset"> Classify another image</a>
</div>
<!-- UPLOAD FORM -->
{% else %}
<div class="card">
<form method="POST" action="/predict" enctype="multipart/form-data" id="upload-form">
<!-- Model selector -->
<span class="field-label">Select model backend</span>
<div class="model-selector">
<input class="model-option" type="radio" name="model"
id="opt-pytorch" value="pytorch" checked />
<label class="model-btn" for="opt-pytorch">
<span class="icon">🔥</span>
<span class="name">PyTorch</span>
<span class="tag">sara_model.pth</span>
</label>
<input class="model-option" type="radio" name="model"
id="opt-tensorflow" value="tensorflow" />
<label class="model-btn" for="opt-tensorflow">
<span class="icon"></span>
<span class="name">TensorFlow</span>
<span class="tag">sara_model.keras</span>
</label>
</div>
<!-- File upload -->
<span class="field-label">Upload image</span>
<div class="upload-zone" id="drop-zone">
<input type="file" name="image" id="file-input"
accept=".jpg,.jpeg,.png,.webp,.bmp" required />
<div class="upload-icon">📸</div>
<div class="upload-text">Drop your image here</div>
<div class="upload-hint">Click to browse · JPG PNG WEBP · max 5 MB</div>
<div id="file-name"></div>
</div>
<!-- Preview -->
<div id="preview-wrap">
<img id="preview-img" src="" alt="preview" />
</div>
<!-- Submit -->
<button type="submit" class="btn-predict" id="submit-btn">
Classify Image
</button>
</form>
</div>
{% endif %}
<footer>
Classification made with Pytorch and TensorFlow ·
</footer>
<!-- JS: drag-drop, preview, loading state -->
<script>
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const fileName = document.getElementById('file-name');
const preview = document.getElementById('preview-img');
const previewW = document.getElementById('preview-wrap');
const submitBtn = document.getElementById('submit-btn');
const form = document.getElementById('upload-form');
if (!fileInput) { /* result page — skip */ }
else {
// Show file name + preview on selection
fileInput.addEventListener('change', () => {
const file = fileInput.files[0];
if (!file) return;
fileName.textContent = '✓ ' + file.name;
const reader = new FileReader();
reader.onload = e => {
preview.src = e.target.result;
previewW.style.display = 'block';
};
reader.readAsDataURL(file);
});
// Drag-and-drop visual feedback
['dragenter','dragover'].forEach(ev =>
dropZone.addEventListener(ev, e => {
e.preventDefault();
dropZone.classList.add('drag-over');
})
);
['dragleave','drop'].forEach(ev =>
dropZone.addEventListener(ev, e => {
e.preventDefault();
dropZone.classList.remove('drag-over');
})
);
dropZone.addEventListener('drop', e => {
const dt = e.dataTransfer;
if (dt.files[0]) {
// Assign dropped file to input
const dtTransfer = new DataTransfer();
dtTransfer.items.add(dt.files[0]);
fileInput.files = dtTransfer.files;
fileInput.dispatchEvent(new Event('change'));
}
});
// Loading state on submit
form.addEventListener('submit', () => {
submitBtn.textContent = 'Classifying…';
submitBtn.classList.add('loading');
});
}
</script>
</body>
</html>