AegisRoad-Detector / static /index.html
rudrapatel-1908's picture
Update static/index.html
0a37f16 verified
Raw
History Blame Contribute Delete
33.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AegisRoad — Road Damage Detection System</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Share+Tech+Mono&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-void: #080a0c;
--bg-deep: #0d1117;
--bg-panel: #111820;
--bg-card: #16202c;
--bg-hover: #1c2b3a;
--border: #1e2d3d;
--border-bright: #2a3f55;
--amber: #f5a623;
--amber-dim: #b07518;
--amber-glow: rgba(245,166,35,0.12);
--amber-glow-strong: rgba(245,166,35,0.25);
--red: #e05252;
--red-glow: rgba(224,82,82,0.15);
--orange: #e8833a;
--yellow: #e8c63a;
--green: #3ae87a;
--green-glow: rgba(58,232,122,0.12);
--cyan: #3ab8e8;
--text-primary: #e8edf2;
--text-secondary: #7a9bb5;
--text-muted: #3d5a73;
--mono: 'Share Tech Mono', monospace;
--display: 'Bebas Neue', sans-serif;
--body: 'DM Sans', sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg-void);
color: var(--text-primary);
font-family: var(--body);
min-height: 100vh;
overflow-x: hidden;
}
/* Scanline overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.03) 2px,
rgba(0,0,0,0.03) 4px
);
pointer-events: none;
z-index: 9999;
}
/* ── HEADER ── */
header {
position: sticky;
top: 0;
z-index: 100;
background: rgba(8,10,12,0.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 36px;
height: 36px;
position: relative;
}
.logo-icon svg { width: 100%; height: 100%; }
.logo-text {
font-family: var(--display);
font-size: 1.8rem;
letter-spacing: 0.05em;
color: var(--amber);
line-height: 1;
}
.logo-sub {
font-family: var(--mono);
font-size: 0.6rem;
color: var(--text-muted);
letter-spacing: 0.15em;
text-transform: uppercase;
line-height: 1;
margin-top: 2px;
}
.header-status {
display: flex;
align-items: center;
gap: 1.5rem;
}
.status-pill {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 6px 12px;
border-radius: 3px;
border: 1px solid var(--border);
color: var(--text-muted);
transition: all 0.3s;
}
.status-pill.online {
color: var(--green);
border-color: rgba(58,232,122,0.3);
background: var(--green-glow);
}
.status-pill.offline {
color: var(--red);
border-color: rgba(224,82,82,0.3);
background: var(--red-glow);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.status-pill.online .status-dot {
animation: pulse-dot 2s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.7); }
}
.mode-badge {
font-family: var(--mono);
font-size: 0.65rem;
letter-spacing: 0.12em;
padding: 4px 10px;
border-radius: 2px;
text-transform: uppercase;
}
.mode-badge.live {
background: var(--amber-glow);
color: var(--amber);
border: 1px solid var(--amber-dim);
}
.mode-badge.demo {
background: rgba(58,184,232,0.1);
color: var(--cyan);
border: 1px solid rgba(58,184,232,0.3);
}
/* ── HERO ── */
.hero {
padding: 3rem 2rem 2rem;
max-width: 1400px;
margin: 0 auto;
}
.hero-grid {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
align-items: end;
margin-bottom: 2rem;
}
.hero-title {
font-family: var(--display);
font-size: clamp(3rem, 6vw, 5.5rem);
line-height: 0.95;
letter-spacing: 0.02em;
color: var(--text-primary);
}
.hero-title span {
color: var(--amber);
}
.hero-meta {
text-align: right;
font-family: var(--mono);
font-size: 0.7rem;
color: var(--text-muted);
letter-spacing: 0.1em;
line-height: 2;
}
.divider {
height: 1px;
background: linear-gradient(90deg, var(--amber) 0%, var(--border) 40%, transparent 100%);
margin-bottom: 2rem;
}
/* ── MAIN LAYOUT ── */
.main {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem 4rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
align-items: start;
}
@media (max-width: 900px) {
.main { grid-template-columns: 1fr; }
.hero-grid { grid-template-columns: 1fr; }
.hero-meta { text-align: left; }
}
/* ── PANELS ── */
.panel {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.25rem;
border-bottom: 1px solid var(--border);
background: var(--bg-deep);
}
.panel-label {
font-family: var(--mono);
font-size: 0.65rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 8px;
}
.panel-label::before {
content: '';
width: 3px;
height: 12px;
background: var(--amber);
border-radius: 1px;
display: block;
}
.panel-action {
font-family: var(--mono);
font-size: 0.6rem;
color: var(--text-muted);
letter-spacing: 0.1em;
}
.panel-body {
padding: 1.5rem;
}
/* ── UPLOAD ZONE ── */
.upload-zone {
border: 2px dashed var(--border-bright);
border-radius: 4px;
padding: 3rem 2rem;
text-align: center;
cursor: pointer;
transition: all 0.25s ease;
position: relative;
background: var(--bg-deep);
min-height: 240px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--amber);
background: var(--amber-glow);
transform: scale(1.005);
}
.upload-zone.has-file {
border-color: var(--amber);
border-style: solid;
padding: 0;
min-height: 300px;
}
.upload-icon {
width: 48px;
height: 48px;
color: var(--amber-dim);
}
.upload-title {
font-family: var(--display);
font-size: 1.4rem;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.upload-hint {
font-family: var(--mono);
font-size: 0.65rem;
color: var(--text-muted);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.upload-zone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}
.preview-img {
width: 100%;
height: 100%;
object-fit: contain;
max-height: 400px;
display: block;
padding: 1rem;
}
/* ── ANALYZE BUTTON ── */
.analyze-btn {
width: 100%;
margin-top: 1rem;
padding: 0.9rem 2rem;
background: var(--amber);
color: #000;
border: none;
border-radius: 3px;
font-family: var(--display);
font-size: 1.3rem;
letter-spacing: 0.1em;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.analyze-btn::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.2) 50%, transparent 100%);
transform: translateX(-100%);
transition: transform 0.5s ease;
}
.analyze-btn:hover:not(:disabled)::after {
transform: translateX(100%);
}
.analyze-btn:hover:not(:disabled) {
background: #ffc040;
transform: translateY(-1px);
box-shadow: 0 4px 20px var(--amber-glow-strong);
}
.analyze-btn:active { transform: translateY(0); }
.analyze-btn:disabled {
background: var(--bg-card);
color: var(--text-muted);
border: 1px solid var(--border);
cursor: not-allowed;
}
.analyze-btn.loading {
background: var(--bg-card);
color: var(--amber);
border: 1px solid var(--amber-dim);
cursor: wait;
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--amber-dim);
border-top-color: var(--amber);
border-radius: 50%;
animation: spin 0.7s linear infinite;
vertical-align: middle;
margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── RESULTS PANEL ── */
.result-image-container {
position: relative;
background: var(--bg-deep);
border-radius: 3px;
overflow: hidden;
min-height: 240px;
display: flex;
align-items: center;
justify-content: center;
}
.result-image-container img {
width: 100%;
object-fit: contain;
max-height: 400px;
display: block;
}
.result-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 3rem;
color: var(--text-muted);
}
.result-placeholder-icon {
font-size: 2.5rem;
opacity: 0.3;
}
.result-placeholder-text {
font-family: var(--mono);
font-size: 0.65rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
/* ── DETECTION CARDS ── */
.detections-list {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.detection-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-left: 3px solid var(--amber);
border-radius: 3px;
padding: 0.75rem 1rem;
display: grid;
grid-template-columns: 1fr auto;
gap: 0.5rem;
align-items: center;
animation: card-in 0.3s ease forwards;
opacity: 0;
transform: translateX(-8px);
}
@keyframes card-in {
to { opacity: 1; transform: translateX(0); }
}
.detection-card[data-severity="critical"] { border-left-color: var(--red); }
.detection-card[data-severity="high"] { border-left-color: var(--orange); }
.detection-card[data-severity="medium"] { border-left-color: var(--yellow); }
.detection-card[data-severity="low"] { border-left-color: var(--cyan); }
.detection-class {
font-family: var(--display);
font-size: 1.1rem;
letter-spacing: 0.05em;
color: var(--text-primary);
}
.detection-name {
font-family: var(--body);
font-size: 0.75rem;
color: var(--text-secondary);
font-weight: 300;
margin-top: 1px;
}
.detection-right {
text-align: right;
}
.detection-confidence {
font-family: var(--mono);
font-size: 0.85rem;
color: var(--amber);
}
.severity-tag {
font-family: var(--mono);
font-size: 0.55rem;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 2px 6px;
border-radius: 2px;
margin-top: 3px;
display: inline-block;
}
.severity-critical { background: var(--red-glow); color: var(--red); border: 1px solid rgba(224,82,82,0.3); }
.severity-high { background: rgba(232,131,58,0.12); color: var(--orange); border: 1px solid rgba(232,131,58,0.3); }
.severity-medium { background: rgba(232,198,58,0.1); color: var(--yellow); border: 1px solid rgba(232,198,58,0.3); }
.severity-low { background: rgba(58,184,232,0.1); color: var(--cyan); border: 1px solid rgba(58,184,232,0.3); }
.conf-bar {
margin-top: 0.5rem;
height: 2px;
background: var(--border);
border-radius: 1px;
overflow: hidden;
}
.conf-fill {
height: 100%;
background: var(--amber);
border-radius: 1px;
width: 0%;
transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.detection-card[data-severity="critical"] .conf-fill { background: var(--red); }
.detection-card[data-severity="high"] .conf-fill { background: var(--orange); }
.detection-card[data-severity="medium"] .conf-fill { background: var(--yellow); }
.detection-card[data-severity="low"] .conf-fill { background: var(--cyan); }
/* ── SUMMARY BAR ── */
.summary-bar {
display: flex;
gap: 1rem;
padding: 0.75rem 1rem;
background: var(--bg-deep);
border-top: 1px solid var(--border);
flex-wrap: wrap;
}
.summary-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.summary-value {
font-family: var(--mono);
font-size: 1.2rem;
color: var(--amber);
}
.summary-label {
font-family: var(--mono);
font-size: 0.55rem;
color: var(--text-muted);
letter-spacing: 0.12em;
text-transform: uppercase;
}
.summary-sep {
width: 1px;
background: var(--border);
align-self: stretch;
}
/* ── CLASS REFERENCE ── */
.class-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.class-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 3px;
padding: 1rem;
transition: border-color 0.2s;
}
.class-card:hover { border-color: var(--border-bright); }
.class-code {
font-family: var(--display);
font-size: 1.5rem;
letter-spacing: 0.05em;
color: var(--amber);
margin-bottom: 4px;
}
.class-name {
font-size: 0.8rem;
color: var(--text-secondary);
font-weight: 500;
margin-bottom: 6px;
}
.class-sev {
font-family: var(--mono);
font-size: 0.6rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
/* ── ERROR TOAST ── */
.toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background: var(--bg-panel);
border: 1px solid rgba(224,82,82,0.4);
border-left: 3px solid var(--red);
border-radius: 4px;
padding: 1rem 1.25rem;
font-family: var(--mono);
font-size: 0.75rem;
color: var(--red);
max-width: 360px;
z-index: 1000;
animation: toast-in 0.3s ease;
display: none;
}
.toast.show { display: block; }
@keyframes toast-in {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ── TICKER ── */
.ticker-bar {
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
background: var(--bg-deep);
overflow: hidden;
padding: 6px 0;
margin-bottom: 0;
}
.ticker-inner {
display: flex;
gap: 3rem;
animation: ticker 30s linear infinite;
width: max-content;
font-family: var(--mono);
font-size: 0.62rem;
color: var(--text-muted);
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
padding: 0 2rem;
}
@keyframes ticker {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.ticker-item { display: flex; align-items: center; gap: 0.5rem; }
.ticker-dot { color: var(--amber); }
/* ── FOOTER ── */
footer {
border-top: 1px solid var(--border);
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--mono);
font-size: 0.62rem;
color: var(--text-muted);
letter-spacing: 0.08em;
max-width: 1400px;
margin: 0 auto;
}
/* ── NO DETECTION STATE ── */
.no-detections {
text-align: center;
padding: 2rem;
font-family: var(--mono);
font-size: 0.7rem;
color: var(--green);
letter-spacing: 0.1em;
border: 1px solid rgba(58,232,122,0.2);
background: var(--green-glow);
border-radius: 3px;
margin-top: 1rem;
}
/* scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg-void); }
::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 3px; }
/* ── COORDS OVERLAY ── */
.coords-overlay {
position: absolute;
bottom: 8px;
right: 8px;
font-family: var(--mono);
font-size: 0.6rem;
color: var(--amber);
letter-spacing: 0.08em;
background: rgba(8,10,12,0.8);
padding: 3px 8px;
border-radius: 2px;
border: 1px solid var(--border);
}
/* ── PROCESSING OVERLAY ── */
.processing-overlay {
position: absolute;
inset: 0;
background: rgba(8,10,12,0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
z-index: 10;
display: none;
}
.processing-overlay.active { display: flex; }
.processing-icon {
width: 48px;
height: 48px;
border: 2px solid var(--border);
border-top-color: var(--amber);
border-right-color: var(--amber);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.processing-text {
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.15em;
color: var(--amber);
text-transform: uppercase;
animation: blink 1.2s ease-in-out infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="logo">
<div class="logo-icon">
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="26" width="32" height="4" rx="1" fill="#f5a623" opacity="0.3"/>
<rect x="2" y="30" width="32" height="4" rx="1" fill="#f5a623" opacity="0.15"/>
<path d="M6 26 L18 4 L30 26" stroke="#f5a623" stroke-width="2" fill="none" stroke-linejoin="round"/>
<circle cx="18" cy="16" r="3" fill="#f5a623"/>
<line x1="18" y1="4" x2="18" y2="8" stroke="#f5a623" stroke-width="1.5" stroke-dasharray="2 2"/>
<rect x="10" y="18" width="4" height="8" rx="0.5" fill="#f5a623" opacity="0.5"/>
<rect x="22" y="18" width="4" height="8" rx="0.5" fill="#f5a623" opacity="0.5"/>
</svg>
</div>
<div>
<div class="logo-text">AEGISROAD</div>
<div class="logo-sub">Road Damage Detection System v3.0</div>
</div>
</div>
<div class="header-status">
<div class="status-pill" id="api-status">
<div class="status-dot"></div>
CONNECTING
</div>
<div class="mode-badge demo" id="mode-badge">DEMO</div>
</div>
</header>
<!-- Ticker -->
<div class="ticker-bar">
<div class="ticker-inner" id="ticker">
<span class="ticker-item"><span class="ticker-dot"></span> YOLOv11 Road Damage Detector</span>
<span class="ticker-item"><span class="ticker-dot"></span> Classes: D00 · D10 · D20 · D40</span>
<span class="ticker-item"><span class="ticker-dot"></span> Trained on RDD2022 Dataset</span>
<span class="ticker-item"><span class="ticker-dot"></span> Supports: JPG · PNG · WEBP</span>
<span class="ticker-item"><span class="ticker-dot"></span> Post /predict for API access</span>
<span class="ticker-item"><span class="ticker-dot"></span> YOLOv11 Road Damage Detector</span>
<span class="ticker-item"><span class="ticker-dot"></span> Classes: D00 · D10 · D20 · D40</span>
<span class="ticker-item"><span class="ticker-dot"></span> Trained on RDD2022 Dataset</span>
<span class="ticker-item"><span class="ticker-dot"></span> Supports: JPG · PNG · WEBP</span>
<span class="ticker-item"><span class="ticker-dot"></span> Post /predict for API access</span>
</div>
</div>
<!-- Hero -->
<div class="hero">
<div class="hero-grid">
<div>
<h1 class="hero-title">
ROAD<br>
<span>DAMAGE</span><br>
SCANNER
</h1>
</div>
<div class="hero-meta">
<div>MODEL // YOLOV11</div>
<div>DATASET // RDD2022</div>
<div>BACKEND // FASTAPI</div>
<div id="timestamp">SYS // --:--:-- UTC</div>
</div>
</div>
<div class="divider"></div>
</div>
<!-- Main -->
<div class="main">
<!-- Left: Upload -->
<div>
<div class="panel">
<div class="panel-header">
<span class="panel-label">INPUT — IMAGE FEED</span>
<span class="panel-action" id="file-size-info"></span>
</div>
<div class="panel-body">
<div class="upload-zone" id="drop-zone">
<input type="file" id="file-input" accept="image/*">
<svg class="upload-icon" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="32" width="40" height="8" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M24 28V8M24 8L16 16M24 8L32 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div class="upload-title">DROP IMAGE HERE</div>
<div class="upload-hint">or click to browse · jpg / png / webp</div>
<img id="preview-img" class="preview-img" style="display:none;" alt="Preview">
</div>
<button class="analyze-btn" id="analyze-btn" disabled>ANALYZE IMAGE</button>
</div>
</div>
<!-- Class Reference -->
<div class="panel" style="margin-top: 1.5rem;">
<div class="panel-header">
<span class="panel-label">CLASS REFERENCE</span>
</div>
<div class="panel-body" style="padding: 1rem;">
<div class="class-grid">
<div class="class-card">
<div class="class-code">D00</div>
<div class="class-name">Longitudinal Crack</div>
<div class="class-sev" style="color:var(--cyan)">● Low Severity</div>
</div>
<div class="class-card">
<div class="class-code">D10</div>
<div class="class-name">Transverse Crack</div>
<div class="class-sev" style="color:var(--yellow)">● Medium Severity</div>
</div>
<div class="class-card">
<div class="class-code">D20</div>
<div class="class-name">Alligator Cracking</div>
<div class="class-sev" style="color:var(--orange)">● High Severity</div>
</div>
<div class="class-card">
<div class="class-code">D40</div>
<div class="class-name">Pothole</div>
<div class="class-sev" style="color:var(--red)">● Critical Severity</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right: Results -->
<div>
<div class="panel">
<div class="panel-header">
<span class="panel-label">OUTPUT — DETECTION RESULTS</span>
<span class="panel-action" id="detection-count">0 DETECTIONS</span>
</div>
<div class="panel-body" style="padding: 0;">
<!-- Annotated image -->
<div class="result-image-container" id="result-image-wrap">
<div class="result-placeholder" id="result-placeholder">
<div class="result-placeholder-icon"></div>
<div class="result-placeholder-text">Awaiting image input</div>
</div>
<img id="result-img" style="display:none;" alt="Annotated Result">
<div class="coords-overlay" id="coords-overlay" style="display:none;">0px × 0px</div>
<div class="processing-overlay" id="processing-overlay">
<div class="processing-icon"></div>
<div class="processing-text">ANALYZING ROAD SURFACE</div>
</div>
</div>
<!-- Summary bar -->
<div class="summary-bar" id="summary-bar" style="display:none;">
<div class="summary-stat">
<div class="summary-value" id="sum-total">0</div>
<div class="summary-label">Total Detections</div>
</div>
<div class="summary-sep"></div>
<div class="summary-stat">
<div class="summary-value" id="sum-highest"></div>
<div class="summary-label">Highest Severity</div>
</div>
<div class="summary-sep"></div>
<div class="summary-stat">
<div class="summary-value" id="sum-confidence"></div>
<div class="summary-label">Max Confidence</div>
</div>
<div class="summary-sep"></div>
<div class="summary-stat">
<div class="summary-value" id="sum-time"></div>
<div class="summary-label">Inference ms</div>
</div>
</div>
<!-- Detection cards -->
<div style="padding: 1rem;">
<div class="detections-list" id="detections-list"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer>
<span>AEGISROAD v3.0 // YOLOV11 // RDD2022 // FASTAPI</span>
<span id="footer-endpoint">POST /predict · GET /health · GET /classes</span>
</footer>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
// ── Clock ──
function updateClock() {
const now = new Date();
const h = String(now.getUTCHours()).padStart(2,'0');
const m = String(now.getUTCMinutes()).padStart(2,'0');
const s = String(now.getUTCSeconds()).padStart(2,'0');
document.getElementById('timestamp').textContent = `SYS // ${h}:${m}:${s} UTC`;
}
setInterval(updateClock, 1000);
updateClock();
// ── Health Check ──
const statusEl = document.getElementById('api-status');
const modeBadge = document.getElementById('mode-badge');
async function checkHealth() {
try {
const res = await fetch('/health');
const data = await res.json();
statusEl.className = 'status-pill online';
statusEl.innerHTML = '<div class="status-dot"></div> ONLINE';
if (data.mode === 'live') {
modeBadge.className = 'mode-badge live';
modeBadge.textContent = 'LIVE';
} else {
modeBadge.className = 'mode-badge demo';
modeBadge.textContent = 'DEMO';
}
} catch (e) {
statusEl.className = 'status-pill offline';
statusEl.innerHTML = '<div class="status-dot"></div> OFFLINE';
}
}
checkHealth();
setInterval(checkHealth, 15000);
// ── Upload Zone ──
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const previewImg = document.getElementById('preview-img');
const analyzeBtn = document.getElementById('analyze-btn');
const fileSizeInfo = document.getElementById('file-size-info');
let selectedFile = null;
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
fileInput.addEventListener('change', (e) => {
if (e.target.files[0]) handleFile(e.target.files[0]);
});
function handleFile(file) {
if (!file.type.startsWith('image/')) {
showToast('Invalid file type. Please upload an image (JPG, PNG, WEBP).');
return;
}
selectedFile = file;
const kb = (file.size / 1024).toFixed(1);
const mb = (file.size / 1024 / 1024).toFixed(2);
fileSizeInfo.textContent = `${file.name} · ${mb > 1 ? mb + ' MB' : kb + ' KB'}`;
const reader = new FileReader();
reader.onload = (e) => {
previewImg.src = e.target.result;
previewImg.style.display = 'block';
// Hide upload UI elements
dropZone.querySelectorAll('.upload-icon, .upload-title, .upload-hint').forEach(el => el.style.display = 'none');
dropZone.classList.add('has-file');
};
reader.readAsDataURL(file);
analyzeBtn.disabled = false;
analyzeBtn.textContent = 'ANALYZE IMAGE';
}
// ── Analyze ──
const SEVERITY_MAP = {
'D00': { name: 'Longitudinal Crack', severity: 'low' },
'D10': { name: 'Transverse Crack', severity: 'medium' },
'D20': { name: 'Alligator Cracking', severity: 'high' },
'D40': { name: 'Pothole', severity: 'critical' },
};
const SEV_ORDER = { critical: 4, high: 3, medium: 2, low: 1 };
analyzeBtn.addEventListener('click', runInference);
async function runInference() {
if (!selectedFile) return;
analyzeBtn.disabled = true;
analyzeBtn.className = 'analyze-btn loading';
analyzeBtn.innerHTML = '<span class="spinner"></span> ANALYZING...';
document.getElementById('processing-overlay').classList.add('active');
document.getElementById('detections-list').innerHTML = '';
document.getElementById('summary-bar').style.display = 'none';
document.getElementById('detection-count').textContent = '0 DETECTIONS';
const form = new FormData();
form.append('file', selectedFile);
const t0 = performance.now();
try {
const res = await fetch('/predict', { method: 'POST', body: form });
if (!res.ok) throw new Error(`Server error ${res.status}`);
const data = await res.json();
const elapsed = Math.round(performance.now() - t0);
renderResults(data, elapsed);
} catch (err) {
showToast('Detection failed: ' + err.message);
document.getElementById('processing-overlay').classList.remove('active');
} finally {
analyzeBtn.disabled = false;
analyzeBtn.className = 'analyze-btn';
analyzeBtn.textContent = 'ANALYZE AGAIN';
}
}
function renderResults(data, elapsed) {
document.getElementById('processing-overlay').classList.remove('active');
// Annotated image
const resultImg = document.getElementById('result-img');
const placeholder = document.getElementById('result-placeholder');
if (data.annotated_image) {
resultImg.src = 'data:image/jpeg;base64,' + data.annotated_image;
resultImg.style.display = 'block';
placeholder.style.display = 'none';
resultImg.onload = () => {
const coordsEl = document.getElementById('coords-overlay');
coordsEl.style.display = 'block';
coordsEl.textContent = `${resultImg.naturalWidth}px × ${resultImg.naturalHeight}px`;
};
} else if (data.image_url) {
resultImg.src = data.image_url;
resultImg.style.display = 'block';
placeholder.style.display = 'none';
}
const detections = data.detections || [];
const list = document.getElementById('detections-list');
list.innerHTML = '';
document.getElementById('detection-count').textContent = `${detections.length} DETECTION${detections.length !== 1 ? 'S' : ''}`;
if (detections.length === 0) {
list.innerHTML = '<div class="no-detections">✓ NO ROAD DAMAGE DETECTED — SURFACE CLEAR</div>';
document.getElementById('summary-bar').style.display = 'none';
return;
}
// Sort by severity descending
const sorted = [...detections].sort((a, b) => {
const as = SEVERITY_MAP[a.class]?.severity || 'low';
const bs = SEVERITY_MAP[b.class]?.severity || 'low';
return (SEV_ORDER[bs] || 0) - (SEV_ORDER[as] || 0);
});
let maxSev = 'low', maxConf = 0;
sorted.forEach((det, i) => {
const info = SEVERITY_MAP[det.class] || { name: det.class, severity: 'low' };
const conf = Math.round((det.confidence || det.score || 0) * 100);
if (conf > maxConf) maxConf = conf;
if (SEV_ORDER[info.severity] > SEV_ORDER[maxSev]) maxSev = info.severity;
const card = document.createElement('div');
card.className = 'detection-card';
card.dataset.severity = info.severity;
card.style.animationDelay = `${i * 80}ms`;
card.innerHTML = `
<div>
<div class="detection-class">${det.class}</div>
<div class="detection-name">${info.name}</div>
<div class="conf-bar"><div class="conf-fill" data-conf="${conf}"></div></div>
</div>
<div class="detection-right">
<div class="detection-confidence">${conf}%</div>
<div class="severity-tag severity-${info.severity}">${info.severity}</div>
</div>
`;
list.appendChild(card);
// Animate confidence bar
setTimeout(() => {
card.querySelector('.conf-fill').style.width = conf + '%';
}, i * 80 + 50);
});
// Summary
const sumBar = document.getElementById('summary-bar');
sumBar.style.display = 'flex';
document.getElementById('sum-total').textContent = detections.length;
document.getElementById('sum-highest').textContent = maxSev.toUpperCase();
document.getElementById('sum-confidence').textContent = maxConf + '%';
document.getElementById('sum-time').textContent = elapsed;
// Color highest severity
const sevEl = document.getElementById('sum-highest');
sevEl.style.color = maxSev === 'critical' ? 'var(--red)'
: maxSev === 'high' ? 'var(--orange)'
: maxSev === 'medium' ? 'var(--yellow)'
: 'var(--cyan)';
}
// ── Toast ──
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = '⚠ ' + msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 5000);
}
</script>
</body>
</html>