Spaces:
Sleeping
Sleeping
| <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> |