Spaces:
Running
Running
RELEASE: URBANFLOW: YOUR VISION PARTNER..
Browse files- backend/engine.py +4 -5
- backend/server.py +7 -5
- frontend/assets/{rf.png → shuriken.png} +2 -2
- frontend/assets/shurkien_b.png +3 -0
- frontend/css/initial.css +436 -30
- frontend/css/vehicles.css +1389 -450
- frontend/initial.html +64 -30
- frontend/js/initial.js +138 -40
- frontend/js/vehicles.js +484 -263
- frontend/manifest.json +24 -0
- frontend/sw.js +25 -0
- frontend/vehicles.html +284 -236
- requirements.txt +0 -4
backend/engine.py
CHANGED
|
@@ -55,9 +55,7 @@ class ThreadedVideoWriter:
|
|
| 55 |
def write(self, frame):
|
| 56 |
if not self.stopped:
|
| 57 |
try:
|
| 58 |
-
#
|
| 59 |
-
# to ensure the export video is complete and accurate.
|
| 60 |
-
self.queue.put(frame.copy())
|
| 61 |
except Exception as e:
|
| 62 |
print(f"[BACKEND] Writer queue error: {e}")
|
| 63 |
|
|
@@ -240,10 +238,11 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
|
|
| 240 |
"total_iters": total_iters,
|
| 241 |
"total_frames": total,
|
| 242 |
"active": active,
|
| 243 |
-
"
|
|
|
|
| 244 |
"class_in": {str(k): v for k, v in class_in.items()},
|
| 245 |
"class_out": {str(k): v for k, v in class_out.items()},
|
| 246 |
-
"
|
| 247 |
"elapsed": round(elapsed, 2),
|
| 248 |
"fps": round((frame_idx + 1) / elapsed, 2) if elapsed > 0 else 0,
|
| 249 |
}
|
|
|
|
| 55 |
def write(self, frame):
|
| 56 |
if not self.stopped:
|
| 57 |
try:
|
| 58 |
+
self.queue.put(frame) # no copy — frame ownership transfers to queue
|
|
|
|
|
|
|
| 59 |
except Exception as e:
|
| 60 |
print(f"[BACKEND] Writer queue error: {e}")
|
| 61 |
|
|
|
|
| 238 |
"total_iters": total_iters,
|
| 239 |
"total_frames": total,
|
| 240 |
"active": active,
|
| 241 |
+
"congestion_len": len(congestion), # just the length, not the full list
|
| 242 |
+
"congestion_last": congestion[-1] if congestion else 0, # only latest value
|
| 243 |
"class_in": {str(k): v for k, v in class_in.items()},
|
| 244 |
"class_out": {str(k): v for k, v in class_out.items()},
|
| 245 |
+
"flow_count": len(flow_times), # just the count
|
| 246 |
"elapsed": round(elapsed, 2),
|
| 247 |
"fps": round((frame_idx + 1) / elapsed, 2) if elapsed > 0 else 0,
|
| 248 |
}
|
backend/server.py
CHANGED
|
@@ -397,7 +397,6 @@ async def ws_run(ws: WebSocket):
|
|
| 397 |
"actual_fps": result["actual_fps"],
|
| 398 |
"speed_vs_realtime": result["speed_vs_realtime"],
|
| 399 |
"pcu": result.get("pcu", {}),
|
| 400 |
-
"speed_distribution": result.get("speed", {}).get("distribution", {}),
|
| 401 |
}))
|
| 402 |
await ws.close()
|
| 403 |
|
|
@@ -414,16 +413,19 @@ async def ws_run(ws: WebSocket):
|
|
| 414 |
|
| 415 |
from fastapi.staticfiles import StaticFiles
|
| 416 |
|
| 417 |
-
class
|
| 418 |
def is_not_modified(self, response_headers, request_headers) -> bool:
|
| 419 |
return False
|
| 420 |
-
|
| 421 |
async def get_response(self, path: str, scope):
|
| 422 |
resp = await super().get_response(path, scope)
|
| 423 |
-
|
|
|
|
|
|
|
|
|
|
| 424 |
return resp
|
| 425 |
|
| 426 |
-
app.mount("/",
|
| 427 |
|
| 428 |
if __name__ == "__main__":
|
| 429 |
import uvicorn
|
|
|
|
| 397 |
"actual_fps": result["actual_fps"],
|
| 398 |
"speed_vs_realtime": result["speed_vs_realtime"],
|
| 399 |
"pcu": result.get("pcu", {}),
|
|
|
|
| 400 |
}))
|
| 401 |
await ws.close()
|
| 402 |
|
|
|
|
| 413 |
|
| 414 |
from fastapi.staticfiles import StaticFiles
|
| 415 |
|
| 416 |
+
class SmartCacheStaticFiles(StaticFiles):
|
| 417 |
def is_not_modified(self, response_headers, request_headers) -> bool:
|
| 418 |
return False
|
| 419 |
+
|
| 420 |
async def get_response(self, path: str, scope):
|
| 421 |
resp = await super().get_response(path, scope)
|
| 422 |
+
if path.endswith(".html") or path in ("", "/"):
|
| 423 |
+
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
| 424 |
+
else:
|
| 425 |
+
resp.headers["Cache-Control"] = "public, max-age=3600"
|
| 426 |
return resp
|
| 427 |
|
| 428 |
+
app.mount("/", SmartCacheStaticFiles(directory=str(FRONTEND)), name="frontend")
|
| 429 |
|
| 430 |
if __name__ == "__main__":
|
| 431 |
import uvicorn
|
frontend/assets/{rf.png → shuriken.png}
RENAMED
|
File without changes
|
frontend/assets/shurkien_b.png
ADDED
|
Git LFS Details
|
frontend/css/initial.css
CHANGED
|
@@ -1,70 +1,476 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
:root {
|
| 2 |
-
--cocoa:
|
| 3 |
-
--cocoa-l:
|
| 4 |
--cocoa-xl: #d4b08a;
|
| 5 |
-
--t1:
|
| 6 |
-
--t2:
|
| 7 |
-
--border:
|
| 8 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
body {
|
|
|
|
|
|
|
| 10 |
font-family: 'Montserrat', sans-serif;
|
| 11 |
background-color: #000000;
|
| 12 |
color: var(--t1);
|
|
|
|
|
|
|
|
|
|
| 13 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
.fade-in {
|
| 15 |
animation: fadeIn 0.4s ease-in-out forwards;
|
| 16 |
}
|
| 17 |
@keyframes fadeIn {
|
| 18 |
-
from {
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
}
|
| 26 |
}
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
.traffic-dynamics-card {
|
| 29 |
background-color: #0a0a0a !important;
|
| 30 |
-
border:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
.traffic-dynamics-card:hover {
|
| 33 |
-
|
| 34 |
}
|
|
|
|
|
|
|
|
|
|
| 35 |
#dropzone {
|
| 36 |
transition: all 0.2s ease;
|
| 37 |
border-color: #2a2a2a;
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
-
#dropzone:hover
|
|
|
|
|
|
|
| 40 |
border-color: var(--cocoa-l) !important;
|
| 41 |
-
background-color: #
|
| 42 |
}
|
|
|
|
|
|
|
| 43 |
.core-badge {
|
| 44 |
background-color: var(--cocoa) !important;
|
| 45 |
color: var(--t1) !important;
|
| 46 |
}
|
| 47 |
-
|
|
|
|
| 48 |
.onboard-overlay {
|
| 49 |
position: fixed; inset: 0; z-index: 9999;
|
| 50 |
background: rgba(0,0,0,0.92);
|
| 51 |
display: flex; align-items: center; justify-content: center;
|
|
|
|
| 52 |
}
|
| 53 |
.onboard-card {
|
| 54 |
-
background: #0a0a0a;
|
| 55 |
-
border
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
.onboard-step { display: none; }
|
| 59 |
.onboard-step.active { display: block; }
|
| 60 |
-
.onboard-dots {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
.onboard-dot {
|
| 62 |
-
width: 8px; height: 8px;
|
| 63 |
-
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
.onboard-dot.active { background: var(--cocoa-l); }
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =============================================
|
| 2 |
+
UrbanFlow — initial.css (Mobile-First)
|
| 3 |
+
Same design system, fully touch-friendly
|
| 4 |
+
============================================= */
|
| 5 |
+
|
| 6 |
:root {
|
| 7 |
+
--cocoa: #8b5e3c;
|
| 8 |
+
--cocoa-l: #c89a6c;
|
| 9 |
--cocoa-xl: #d4b08a;
|
| 10 |
+
--t1: #f0ece6;
|
| 11 |
+
--t2: #a89f97;
|
| 12 |
+
--border: #2a2a2a;
|
| 13 |
}
|
| 14 |
+
|
| 15 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 16 |
+
|
| 17 |
+
html {
|
| 18 |
+
scrollbar-width: none;
|
| 19 |
+
}
|
| 20 |
+
html::-webkit-scrollbar {
|
| 21 |
+
display: none;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
body {
|
| 25 |
+
overflow-x: hidden;
|
| 26 |
+
width: 100%;
|
| 27 |
font-family: 'Montserrat', sans-serif;
|
| 28 |
background-color: #000000;
|
| 29 |
color: var(--t1);
|
| 30 |
+
margin: 0;
|
| 31 |
+
-webkit-tap-highlight-color: transparent;
|
| 32 |
+
scrollbar-width: none; /* Firefox */
|
| 33 |
}
|
| 34 |
+
body::-webkit-scrollbar {
|
| 35 |
+
display: none; /* Chrome, Safari, Edge */
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/* ---- Fade animation ---- */
|
| 39 |
.fade-in {
|
| 40 |
animation: fadeIn 0.4s ease-in-out forwards;
|
| 41 |
}
|
| 42 |
@keyframes fadeIn {
|
| 43 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 44 |
+
to { opacity: 1; transform: translateY(0); }
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* ---- Feature List Animation ---- */
|
| 48 |
+
@keyframes fadeSlideIn {
|
| 49 |
+
from { opacity: 0; transform: translateX(-8px); }
|
| 50 |
+
to { opacity: 1; transform: translateX(0); }
|
| 51 |
}
|
| 52 |
+
.hero-text-section ul li {
|
| 53 |
+
opacity: 0;
|
| 54 |
+
animation: fadeSlideIn 0.4s ease-out forwards;
|
| 55 |
+
}
|
| 56 |
+
.hero-text-section ul li:nth-child(1) { animation-delay: 0.1s; }
|
| 57 |
+
.hero-text-section ul li:nth-child(2) { animation-delay: 0.2s; }
|
| 58 |
+
.hero-text-section ul li:nth-child(3) { animation-delay: 0.3s; }
|
| 59 |
+
.hero-text-section ul li:nth-child(4) { animation-delay: 0.4s; }
|
| 60 |
+
.hero-text-section ul li:nth-child(5) { animation-delay: 0.5s; }
|
| 61 |
+
|
| 62 |
+
/* ---- Card ---- */
|
| 63 |
.traffic-dynamics-card {
|
| 64 |
background-color: #0a0a0a !important;
|
| 65 |
+
border: none !important;
|
| 66 |
+
position: relative;
|
| 67 |
+
transition: box-shadow 0.3s ease;
|
| 68 |
+
z-index: 0;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Shimmer border — only on the 2px ring, not inside the card */
|
| 72 |
+
.traffic-dynamics-card::before {
|
| 73 |
+
content: '';
|
| 74 |
+
position: absolute;
|
| 75 |
+
inset: 0;
|
| 76 |
+
border-radius: inherit;
|
| 77 |
+
padding: 2px;
|
| 78 |
+
background: linear-gradient(
|
| 79 |
+
90deg,
|
| 80 |
+
var(--cocoa) 0%,
|
| 81 |
+
var(--cocoa-l) 30%,
|
| 82 |
+
rgba(255,255,255,0.7) 50%,
|
| 83 |
+
var(--cocoa-l) 70%,
|
| 84 |
+
var(--cocoa) 100%
|
| 85 |
+
);
|
| 86 |
+
background-size: 200% 100%;
|
| 87 |
+
animation: borderShimmer 2.4s linear infinite;
|
| 88 |
+
-webkit-mask:
|
| 89 |
+
linear-gradient(#fff 0 0) content-box,
|
| 90 |
+
linear-gradient(#fff 0 0);
|
| 91 |
+
-webkit-mask-composite: xor;
|
| 92 |
+
mask-composite: exclude;
|
| 93 |
+
pointer-events: none;
|
| 94 |
+
}
|
| 95 |
+
@keyframes borderShimmer {
|
| 96 |
+
0% { background-position: 200% 0; }
|
| 97 |
+
100% { background-position: -200% 0; }
|
| 98 |
+
}
|
| 99 |
+
.traffic-dynamics-card:hover::before {
|
| 100 |
+
animation-duration: 1.2s;
|
| 101 |
}
|
| 102 |
.traffic-dynamics-card:hover {
|
| 103 |
+
box-shadow: 0 0 32px 0 rgba(200,154,108,0.18);
|
| 104 |
}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
/* ---- Dropzone ---- */
|
| 108 |
#dropzone {
|
| 109 |
transition: all 0.2s ease;
|
| 110 |
border-color: #2a2a2a;
|
| 111 |
+
/* finger-friendly minimum height */
|
| 112 |
+
min-height: 160px;
|
| 113 |
}
|
| 114 |
+
#dropzone:hover,
|
| 115 |
+
#dropzone:active,
|
| 116 |
+
#dropzone.dz-active {
|
| 117 |
border-color: var(--cocoa-l) !important;
|
| 118 |
+
background-color: #0d0d0d !important;
|
| 119 |
}
|
| 120 |
+
|
| 121 |
+
/* ---- Core badge ---- */
|
| 122 |
.core-badge {
|
| 123 |
background-color: var(--cocoa) !important;
|
| 124 |
color: var(--t1) !important;
|
| 125 |
}
|
| 126 |
+
|
| 127 |
+
/* ---- Onboarding ---- */
|
| 128 |
.onboard-overlay {
|
| 129 |
position: fixed; inset: 0; z-index: 9999;
|
| 130 |
background: rgba(0,0,0,0.92);
|
| 131 |
display: flex; align-items: center; justify-content: center;
|
| 132 |
+
padding: 16px;
|
| 133 |
}
|
| 134 |
.onboard-card {
|
| 135 |
+
background: #0a0a0a;
|
| 136 |
+
border: 1px solid #2a2a2a;
|
| 137 |
+
border-radius: 16px;
|
| 138 |
+
max-width: 440px;
|
| 139 |
+
width: 100%;
|
| 140 |
+
padding: 32px 24px;
|
| 141 |
+
text-align: center;
|
| 142 |
}
|
| 143 |
.onboard-step { display: none; }
|
| 144 |
.onboard-step.active { display: block; }
|
| 145 |
+
.onboard-dots {
|
| 146 |
+
display: flex; gap: 6px;
|
| 147 |
+
justify-content: center;
|
| 148 |
+
margin-top: 20px;
|
| 149 |
+
}
|
| 150 |
.onboard-dot {
|
| 151 |
+
width: 8px; height: 8px;
|
| 152 |
+
border-radius: 50%;
|
| 153 |
+
background: #333;
|
| 154 |
+
transition: background 0.2s;
|
| 155 |
}
|
| 156 |
.onboard-dot.active { background: var(--cocoa-l); }
|
| 157 |
+
|
| 158 |
+
/* Onboarding buttons — large touch targets */
|
| 159 |
+
.onboard-card .flex.gap-3 button {
|
| 160 |
+
min-height: 44px;
|
| 161 |
+
padding: 10px 20px;
|
| 162 |
+
font-size: 11px;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/* Onboarding Icon Pulse */
|
| 166 |
+
@keyframes iconPulse {
|
| 167 |
+
0% { transform: scale(1); }
|
| 168 |
+
50% { transform: scale(1.12); }
|
| 169 |
+
100% { transform: scale(1); }
|
| 170 |
+
}
|
| 171 |
+
.pulse-once {
|
| 172 |
+
animation: iconPulse 0.35s ease;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* ---- Canvas container — touch hint ---- */
|
| 176 |
+
#drawing-canvas {
|
| 177 |
+
touch-action: none; /* prevents scroll while drawing */
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/* ---- Upload progress ---- */
|
| 181 |
+
#upload-progress-container {
|
| 182 |
+
padding: 0 4px;
|
| 183 |
+
}
|
| 184 |
+
#upload-bar {
|
| 185 |
+
transition: width 0.3s ease-out;
|
| 186 |
+
position: relative;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/* Shimmer overlay */
|
| 190 |
+
#upload-bar::after {
|
| 191 |
+
content: '';
|
| 192 |
+
position: absolute;
|
| 193 |
+
top: 0; right: 0; bottom: 0; left: 0;
|
| 194 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
| 195 |
+
transform: translateX(-100%);
|
| 196 |
+
animation: shimmer 1.5s infinite;
|
| 197 |
+
}
|
| 198 |
+
@keyframes shimmer {
|
| 199 |
+
100% { transform: translateX(100%); }
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* =============================================
|
| 203 |
+
DESKTOP (≥1024px) — original layout
|
| 204 |
+
============================================= */
|
| 205 |
+
@media (min-width: 1024px) {
|
| 206 |
+
header { margin-top: 4rem; }
|
| 207 |
+
|
| 208 |
+
main {
|
| 209 |
+
display: grid !important;
|
| 210 |
+
grid-template-columns: repeat(12, 1fr) !important;
|
| 211 |
+
gap: 5rem !important;
|
| 212 |
+
padding: 2rem 2.5rem !important;
|
| 213 |
+
align-items: center;
|
| 214 |
+
min-height: 70vh !important;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/* Stable card on desktop */
|
| 218 |
+
main > div:last-child {
|
| 219 |
+
height: 520px !important;
|
| 220 |
+
overflow: hidden !important;
|
| 221 |
+
margin-bottom: 0 !important;
|
| 222 |
+
display: flex;
|
| 223 |
+
flex-direction: column;
|
| 224 |
+
justify-content: center;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
/* Steps scroll internally */
|
| 228 |
+
#step-modules, #step-upload, #step-draw {
|
| 229 |
+
overflow-y: auto !important;
|
| 230 |
+
height: auto !important;
|
| 231 |
+
max-height: 100% !important;
|
| 232 |
+
padding-right: 8px;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/* Clamped font size to avoid overflow */
|
| 236 |
+
.hero-title { font-size: clamp(3rem, 4.5vw, 4.5rem) !important; line-height: 1.1 !important; }
|
| 237 |
+
|
| 238 |
+
#dropzone { min-height: 200px; }
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* =============================================
|
| 242 |
+
TABLET (640px – 1023px)
|
| 243 |
+
============================================= */
|
| 244 |
+
@media (min-width: 640px) and (max-width: 1023px) {
|
| 245 |
+
header { margin-top: 2rem; }
|
| 246 |
+
header img { height: 8rem !important; }
|
| 247 |
+
|
| 248 |
+
main {
|
| 249 |
+
display: flex !important;
|
| 250 |
+
flex-direction: column !important;
|
| 251 |
+
padding: 1.5rem !important;
|
| 252 |
+
gap: 2rem !important;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.hero-title { font-size: 2.8rem !important; }
|
| 256 |
+
|
| 257 |
+
/* Hero text section — centered on tablet */
|
| 258 |
+
main > div:first-child {
|
| 259 |
+
text-align: center;
|
| 260 |
+
padding-bottom: 0 !important;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
main > div:first-child ul {
|
| 264 |
+
display: inline-block;
|
| 265 |
+
text-align: left;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/* Step card — full width */
|
| 269 |
+
main > div:last-child {
|
| 270 |
+
max-width: 100% !important;
|
| 271 |
+
width: 100% !important;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
#dropzone { min-height: 180px; }
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/* =============================================
|
| 278 |
+
MOBILE (< 640px) — redesigned mobile layout
|
| 279 |
+
============================================= */
|
| 280 |
+
@media (max-width: 639px) {
|
| 281 |
+
|
| 282 |
+
/* ---- Body: single-column scroll ---- */
|
| 283 |
+
body {
|
| 284 |
+
overflow-y: auto !important;
|
| 285 |
+
overflow-x: hidden !important;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/* ---- Header — compact logo ---- */
|
| 289 |
+
header {
|
| 290 |
+
margin-top: 2rem !important;
|
| 291 |
+
margin-bottom: 0 !important;
|
| 292 |
+
padding: 0 20px;
|
| 293 |
+
}
|
| 294 |
+
header img {
|
| 295 |
+
height: 7rem !important;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
/* ---- Main — stacked column ---- */
|
| 299 |
+
main {
|
| 300 |
+
display: flex !important;
|
| 301 |
+
flex-direction: column !important;
|
| 302 |
+
padding: 20px 16px 8px !important;
|
| 303 |
+
gap: 20px !important;
|
| 304 |
+
max-width: 100% !important;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
/* ---- Hero text: SHOW on mobile (compact) ---- */
|
| 308 |
+
.hero-text-section {
|
| 309 |
+
display: flex !important;
|
| 310 |
+
flex-direction: column;
|
| 311 |
+
align-items: center;
|
| 312 |
+
text-align: center;
|
| 313 |
+
padding-bottom: 0 !important;
|
| 314 |
+
order: 1;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/* Mobile hero title styling */
|
| 318 |
+
.hero-title {
|
| 319 |
+
font-size: 2.8rem !important;
|
| 320 |
+
line-height: 1.15 !important;
|
| 321 |
+
margin-top: 10px !important;
|
| 322 |
+
text-align: center !important;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
/* DEMO badge + tagline row */
|
| 326 |
+
.hero-text-section > p:first-of-type {
|
| 327 |
+
font-size: 11.5px !important;
|
| 328 |
+
letter-spacing: 0.18em !important;
|
| 329 |
+
margin-bottom: 14px !important;
|
| 330 |
+
justify-content: center;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
/* Feature bullets */
|
| 334 |
+
.hero-text-section ul {
|
| 335 |
+
display: flex !important;
|
| 336 |
+
flex-direction: column !important;
|
| 337 |
+
gap: 4px !important;
|
| 338 |
+
width: 100%;
|
| 339 |
+
text-align: left;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.hero-text-section ul li {
|
| 343 |
+
display: flex !important;
|
| 344 |
+
align-items: flex-start !important;
|
| 345 |
+
font-size: 14px !important;
|
| 346 |
+
font-weight: 500 !important;
|
| 347 |
+
line-height: 1.6 !important;
|
| 348 |
+
padding: 5px 0;
|
| 349 |
+
color: #a89f97 !important;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.hero-text-section ul li i {
|
| 353 |
+
font-size: 14px !important;
|
| 354 |
+
margin-right: 10px !important;
|
| 355 |
+
margin-top: 2px;
|
| 356 |
+
flex-shrink: 0;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
/* ---- Step card: full width, right below bullets ---- */
|
| 360 |
+
.step-card-section {
|
| 361 |
+
max-width: 100% !important;
|
| 362 |
+
width: 100% !important;
|
| 363 |
+
min-height: unset !important;
|
| 364 |
+
height: auto !important;
|
| 365 |
+
margin: 0 !important;
|
| 366 |
+
order: 2;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
/* The fixed-height container that holds all steps */
|
| 370 |
+
main > div:last-child {
|
| 371 |
+
height: auto !important;
|
| 372 |
+
min-height: unset !important;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
/* ---- Step headings ---- */
|
| 376 |
+
#step-modules h2,
|
| 377 |
+
#step-upload h2,
|
| 378 |
+
#step-draw h2 {
|
| 379 |
+
font-size: 1.4rem !important;
|
| 380 |
+
margin-bottom: 6px !important;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
#step-modules p,
|
| 384 |
+
#step-upload p {
|
| 385 |
+
font-size: 11px !important;
|
| 386 |
+
margin-bottom: 20px !important;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
/* ---- Traffic Dynamics card ---- */
|
| 390 |
+
.traffic-dynamics-card {
|
| 391 |
+
padding: 24px 20px !important;
|
| 392 |
+
border-radius: 1.5rem !important;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
/* ---- Dropzone — warm glow tap target ---- */
|
| 396 |
+
#dropzone {
|
| 397 |
+
padding: 2rem 1.25rem !important;
|
| 398 |
+
min-height: 150px;
|
| 399 |
+
border-radius: 1.25rem !important;
|
| 400 |
+
border-color: #2a2a2a !important;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
#dropzone span:first-of-type {
|
| 404 |
+
font-size: 0.95rem !important;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
/* ---- Canvas wrapper ---- */
|
| 408 |
+
.relative.w-full.aspect-video {
|
| 409 |
+
aspect-ratio: 4/3 !important;
|
| 410 |
+
border-radius: 1rem !important;
|
| 411 |
+
max-height: 40vh; /* Keeps continue button above fold */
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
/* ---- Proceed button — full width ---- */
|
| 415 |
+
#btn-proceed {
|
| 416 |
+
width: 100% !important;
|
| 417 |
+
padding: 14px !important;
|
| 418 |
+
font-size: 0.9rem !important;
|
| 419 |
+
border-radius: 999px !important;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
/* ---- Reset / Back buttons ---- */
|
| 423 |
+
button[onclick="resetCanvas()"],
|
| 424 |
+
button[onclick="showStep('modules')"] {
|
| 425 |
+
font-size: 11px !important;
|
| 426 |
+
padding: 10px 0 !important;
|
| 427 |
+
min-height: 40px;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
/* ---- Footer — padding only on mobile, layout handled by Tailwind ---- */
|
| 431 |
+
footer {
|
| 432 |
+
padding: 14px 20px 24px !important;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
/* ---- Onboarding card ---- */
|
| 436 |
+
.onboard-card {
|
| 437 |
+
padding: 28px 20px !important;
|
| 438 |
+
border-radius: 18px !important;
|
| 439 |
+
}
|
| 440 |
+
.onboard-card h3 {
|
| 441 |
+
font-size: 1rem !important;
|
| 442 |
+
}
|
| 443 |
+
.onboard-card p {
|
| 444 |
+
font-size: 11px !important;
|
| 445 |
+
}
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
/* =============================================
|
| 450 |
+
TOUCH: remove drag-related cursor on mobile
|
| 451 |
+
============================================= */
|
| 452 |
+
@media (hover: none) and (pointer: coarse) {
|
| 453 |
+
#dropzone {
|
| 454 |
+
cursor: pointer;
|
| 455 |
+
}
|
| 456 |
+
#dropzone:hover {
|
| 457 |
+
/* no hover state on touch — only active */
|
| 458 |
+
border-color: #2a2a2a !important;
|
| 459 |
+
background-color: transparent !important;
|
| 460 |
+
}
|
| 461 |
+
#dropzone:active,
|
| 462 |
+
#dropzone.dz-active {
|
| 463 |
+
border-color: var(--cocoa-l) !important;
|
| 464 |
+
background-color: #0d0d0d !important;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
/* Bigger touch targets for all buttons */
|
| 468 |
+
button {
|
| 469 |
+
min-height: 44px;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/* Canvas cursor for drawing */
|
| 473 |
+
#drawing-canvas {
|
| 474 |
+
cursor: crosshair;
|
| 475 |
+
}
|
| 476 |
+
}
|
frontend/css/vehicles.css
CHANGED
|
@@ -1,450 +1,1389 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =============================================
|
| 2 |
+
UrbanFlow — vehicles.css (Mobile-First)
|
| 3 |
+
Desktop layout preserved exactly.
|
| 4 |
+
Mobile: bottom nav, touch targets, stacked cards.
|
| 5 |
+
============================================= */
|
| 6 |
+
|
| 7 |
+
:root {
|
| 8 |
+
--cocoa: #8b5e3c;
|
| 9 |
+
--cocoa-l: #c89a6c;
|
| 10 |
+
--cocoa-xl: #d4b08a;
|
| 11 |
+
--mob-nav-h: 68px;
|
| 12 |
+
/* bottom nav height on mobile */
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
*,
|
| 16 |
+
*::before,
|
| 17 |
+
*::after {
|
| 18 |
+
box-sizing: border-box;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.hidden {
|
| 22 |
+
display: none !important;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
html {
|
| 26 |
+
overflow: hidden;
|
| 27 |
+
height: 100%;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
body {
|
| 31 |
+
font-family: 'Montserrat', sans-serif;
|
| 32 |
+
background-color: #000000;
|
| 33 |
+
color: #f0ece6;
|
| 34 |
+
-webkit-tap-highlight-color: transparent;
|
| 35 |
+
overscroll-behavior: none;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.mono-font {
|
| 39 |
+
font-family: 'JetBrains Mono', monospace;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* ---- Scrollbar: hide globally on mobile, keep #class-breakdown visible ---- */
|
| 43 |
+
@media (max-width: 1023px) {
|
| 44 |
+
* {
|
| 45 |
+
scrollbar-width: none;
|
| 46 |
+
-ms-overflow-style: none;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
*::-webkit-scrollbar {
|
| 50 |
+
display: none;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* Vehicle Classification section keeps its scrollbar on mobile */
|
| 54 |
+
#class-breakdown {
|
| 55 |
+
scrollbar-width: thin !important;
|
| 56 |
+
-ms-overflow-style: auto !important;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
#class-breakdown::-webkit-scrollbar {
|
| 60 |
+
display: block !important;
|
| 61 |
+
width: 4px !important;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
#class-breakdown::-webkit-scrollbar-track {
|
| 65 |
+
background: #000000 !important;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
#class-breakdown::-webkit-scrollbar-thumb {
|
| 69 |
+
background: #222222 !important;
|
| 70 |
+
border-radius: 4px !important;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#class-breakdown::-webkit-scrollbar-thumb:hover {
|
| 74 |
+
background: #333333 !important;
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* ---- Notification Glow ---- */
|
| 79 |
+
@keyframes glow-green {
|
| 80 |
+
0% {
|
| 81 |
+
color: #f0ece6;
|
| 82 |
+
filter: drop-shadow(0 0 0px #4ade80);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
50% {
|
| 86 |
+
color: #4ade80;
|
| 87 |
+
filter: drop-shadow(0 0 8px #4ade80);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
100% {
|
| 91 |
+
color: #f0ece6;
|
| 92 |
+
filter: drop-shadow(0 0 0px #4ade80);
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.notify-glow i {
|
| 97 |
+
animation: glow-green 1.5s infinite ease-in-out !important;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* ---- Info tooltip ---- */
|
| 101 |
+
.info-wrap {
|
| 102 |
+
position: relative;
|
| 103 |
+
display: inline-flex;
|
| 104 |
+
align-items: center;
|
| 105 |
+
margin-left: 6px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.info-btn {
|
| 109 |
+
display: inline-flex;
|
| 110 |
+
align-items: center;
|
| 111 |
+
justify-content: center;
|
| 112 |
+
width: 18px;
|
| 113 |
+
/* slightly larger for touch */
|
| 114 |
+
height: 18px;
|
| 115 |
+
border-radius: 50%;
|
| 116 |
+
background: #444444 !important;
|
| 117 |
+
color: #ffffff !important;
|
| 118 |
+
font-size: 8px;
|
| 119 |
+
cursor: pointer;
|
| 120 |
+
transition: all 0.2s ease;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.info-btn:hover,
|
| 124 |
+
.info-btn:active {
|
| 125 |
+
background: #666666 !important;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.info-tip {
|
| 129 |
+
display: none;
|
| 130 |
+
position: fixed;
|
| 131 |
+
z-index: 9999;
|
| 132 |
+
background: #0a0a0a;
|
| 133 |
+
color: #aaaaaa;
|
| 134 |
+
font-size: 10px;
|
| 135 |
+
font-weight: 500;
|
| 136 |
+
line-height: 1.4;
|
| 137 |
+
padding: 8px 12px;
|
| 138 |
+
border-radius: 6px;
|
| 139 |
+
max-width: 240px;
|
| 140 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8);
|
| 141 |
+
border: 1px solid #222222;
|
| 142 |
+
pointer-events: none;
|
| 143 |
+
text-transform: none;
|
| 144 |
+
letter-spacing: normal;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/* ---- Mobile Top Bar ---- */
|
| 148 |
+
.mobile-top-bar {
|
| 149 |
+
display: none;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
@media (max-width: 1023px) {
|
| 153 |
+
.mobile-top-bar {
|
| 154 |
+
display: flex;
|
| 155 |
+
align-items: center;
|
| 156 |
+
justify-content: center;
|
| 157 |
+
position: fixed;
|
| 158 |
+
top: 0;
|
| 159 |
+
left: 0;
|
| 160 |
+
right: 0;
|
| 161 |
+
height: 58px;
|
| 162 |
+
background: #000000;
|
| 163 |
+
border-bottom: 1px solid #1a1a1a;
|
| 164 |
+
z-index: 35;
|
| 165 |
+
flex-shrink: 0;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
#legal-menu {
|
| 169 |
+
animation: menuFadeIn 0.2s ease-out forwards;
|
| 170 |
+
transform-origin: top right;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
@keyframes menuFadeIn {
|
| 174 |
+
from {
|
| 175 |
+
opacity: 0;
|
| 176 |
+
transform: translateY(-10px) scale(0.95);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
to {
|
| 180 |
+
opacity: 1;
|
| 181 |
+
transform: translateY(0) scale(1);
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* ---- Sidebar nav states ---- */
|
| 187 |
+
.nav-item-active {
|
| 188 |
+
background-color: #111111 !important;
|
| 189 |
+
color: var(--cocoa-xl) !important;
|
| 190 |
+
border-left: 2px solid var(--cocoa-l) !important;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.nav-item-inactive {
|
| 194 |
+
color: #555555 !important;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.nav-item-inactive:hover {
|
| 198 |
+
color: #f0ece6 !important;
|
| 199 |
+
background-color: #050505 !important;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* ---- Card overrides ---- */
|
| 203 |
+
.bg-white {
|
| 204 |
+
background-color: #0a0a0a !important;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.border-slate-200,
|
| 208 |
+
.border-slate-100,
|
| 209 |
+
.border-slate-50,
|
| 210 |
+
.border-neutral-800,
|
| 211 |
+
.border-neutral-900 {
|
| 212 |
+
border-color: #2a2a2a !important;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.bg-slate-50\/50,
|
| 216 |
+
.bg-slate-50,
|
| 217 |
+
.bg-slate-900,
|
| 218 |
+
.bg-neutral-900 {
|
| 219 |
+
background-color: #0c0c0c !important;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.text-slate-900,
|
| 223 |
+
.text-slate-800,
|
| 224 |
+
.text-slate-700,
|
| 225 |
+
.text-neutral-900 {
|
| 226 |
+
color: #ffffff !important;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.text-slate-600,
|
| 230 |
+
.text-slate-500,
|
| 231 |
+
.text-slate-400,
|
| 232 |
+
.text-neutral-500,
|
| 233 |
+
.text-neutral-400 {
|
| 234 |
+
color: #888888 !important;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.shadow-sm {
|
| 238 |
+
box-shadow: none !important;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* ---- Toggle control ---- */
|
| 242 |
+
.toggle-track {
|
| 243 |
+
width: 36px;
|
| 244 |
+
/* slightly wider for touch */
|
| 245 |
+
height: 20px;
|
| 246 |
+
border-radius: 999px;
|
| 247 |
+
background: #1a1a1a;
|
| 248 |
+
border: 1px solid #333;
|
| 249 |
+
position: relative;
|
| 250 |
+
cursor: pointer;
|
| 251 |
+
flex-shrink: 0;
|
| 252 |
+
transition: background 0.2s ease;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.toggle-track.active {
|
| 256 |
+
background: #c89a6c !important;
|
| 257 |
+
border-color: #c89a6c !important;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.toggle-thumb {
|
| 261 |
+
width: 16px;
|
| 262 |
+
height: 16px;
|
| 263 |
+
border-radius: 50%;
|
| 264 |
+
background: #555555;
|
| 265 |
+
position: absolute;
|
| 266 |
+
top: 2px;
|
| 267 |
+
left: 2px;
|
| 268 |
+
transition: all 0.2s ease;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.toggle-track.active .toggle-thumb {
|
| 272 |
+
transform: translateX(16px);
|
| 273 |
+
background: #ffffff;
|
| 274 |
+
/* pure white for contrast on gold track */
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/* ---- Custom select ---- */
|
| 278 |
+
.custom-select {
|
| 279 |
+
appearance: none;
|
| 280 |
+
background-color: #111111;
|
| 281 |
+
border: 1px solid #222222;
|
| 282 |
+
border-radius: 6px;
|
| 283 |
+
padding: 4px 24px 4px 10px;
|
| 284 |
+
font-size: 11px;
|
| 285 |
+
font-weight: 600;
|
| 286 |
+
color: #ffffff;
|
| 287 |
+
outline: none;
|
| 288 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23666666'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z'/%3E%3C/svg%3E");
|
| 289 |
+
background-repeat: no-repeat;
|
| 290 |
+
background-position: right 8px center;
|
| 291 |
+
background-size: 12px;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
/* ---- Stepper ---- */
|
| 295 |
+
.s-stepper {
|
| 296 |
+
display: inline-flex;
|
| 297 |
+
border: 1px solid #222222;
|
| 298 |
+
border-radius: 6px;
|
| 299 |
+
background: #111111;
|
| 300 |
+
overflow: hidden;
|
| 301 |
+
flex-shrink: 0;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.s-stepper button {
|
| 305 |
+
padding: 8px 12px;
|
| 306 |
+
/* larger touch target than original 4px 8px */
|
| 307 |
+
color: #666666;
|
| 308 |
+
font-size: 14px;
|
| 309 |
+
min-width: 36px;
|
| 310 |
+
min-height: 36px;
|
| 311 |
+
display: flex;
|
| 312 |
+
align-items: center;
|
| 313 |
+
justify-content: center;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.s-stepper button:hover,
|
| 317 |
+
.s-stepper button:active {
|
| 318 |
+
background: #1a1a1a;
|
| 319 |
+
color: #ffffff;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.s-stepper .s-val {
|
| 323 |
+
min-width: 44px;
|
| 324 |
+
text-align: center;
|
| 325 |
+
font-family: 'JetBrains Mono', monospace;
|
| 326 |
+
font-size: 12px;
|
| 327 |
+
font-weight: 700;
|
| 328 |
+
color: #ffffff;
|
| 329 |
+
padding: 4px 0;
|
| 330 |
+
border-left: 1px solid #222222;
|
| 331 |
+
border-right: 1px solid #222222;
|
| 332 |
+
display: flex;
|
| 333 |
+
align-items: center;
|
| 334 |
+
justify-content: center;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/* ---- Settings row ---- */
|
| 338 |
+
.s-row {
|
| 339 |
+
display: flex;
|
| 340 |
+
align-items: center;
|
| 341 |
+
justify-content: space-between;
|
| 342 |
+
padding: 14px 0;
|
| 343 |
+
/* slightly more vertical padding */
|
| 344 |
+
border-bottom: 1px solid #1a1a1a;
|
| 345 |
+
gap: 12px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.s-row:last-child {
|
| 349 |
+
border-bottom: none;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.s-row>div:first-child {
|
| 353 |
+
flex: 1;
|
| 354 |
+
min-width: 0;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/* ---- Progress bar ---- */
|
| 358 |
+
#proc-bar {
|
| 359 |
+
background-color: var(--cocoa-l) !important;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
#proc-label {
|
| 363 |
+
color: #ffffff !important;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
/* ---- Disabled rows ---- */
|
| 367 |
+
.s-row.disabled {
|
| 368 |
+
opacity: 0.65 !important;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.s-row.disabled .s-stepper,
|
| 372 |
+
.s-row.disabled .custom-select,
|
| 373 |
+
.s-row.disabled .toggle-track,
|
| 374 |
+
.s-row.disabled .chip-container,
|
| 375 |
+
.s-row.disabled .uf-select-wrap,
|
| 376 |
+
.s-row.disabled .uf-select-trigger {
|
| 377 |
+
pointer-events: none !important;
|
| 378 |
+
opacity: 0.5 !important;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/* Force-collapse the dropdown panel when row is locked */
|
| 382 |
+
.s-row.disabled .uf-select-dropdown {
|
| 383 |
+
display: none !important;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.s-row.disabled .info-wrap {
|
| 387 |
+
pointer-events: auto !important;
|
| 388 |
+
opacity: 1 !important;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
#btn-start-processing {
|
| 392 |
+
font-family: 'Montserrat', sans-serif !important;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
/* ---- Chips ---- */
|
| 396 |
+
.chip-container {
|
| 397 |
+
display: flex;
|
| 398 |
+
flex-wrap: wrap;
|
| 399 |
+
gap: 8px;
|
| 400 |
+
margin-top: 12px;
|
| 401 |
+
padding-top: 12px;
|
| 402 |
+
border-top: 1px solid #1a1a1a;
|
| 403 |
+
transition: all 0.3s ease;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.chip {
|
| 407 |
+
display: inline-flex;
|
| 408 |
+
align-items: center;
|
| 409 |
+
gap: 6px;
|
| 410 |
+
padding: 8px 14px;
|
| 411 |
+
/* larger than original 6px 14px */
|
| 412 |
+
border-radius: 9999px;
|
| 413 |
+
font-size: 10px;
|
| 414 |
+
font-weight: 700;
|
| 415 |
+
cursor: pointer;
|
| 416 |
+
transition: all 0.2s ease;
|
| 417 |
+
user-select: none;
|
| 418 |
+
border: 1px solid #333333;
|
| 419 |
+
background: rgba(255, 255, 255, 0.03);
|
| 420 |
+
color: #888888;
|
| 421 |
+
min-height: 36px;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.chip.active {
|
| 425 |
+
background: var(--cocoa-l);
|
| 426 |
+
color: #000000;
|
| 427 |
+
border-color: var(--cocoa-l);
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.chip.frozen {
|
| 431 |
+
background: rgba(255, 255, 255, 0.4);
|
| 432 |
+
color: #000000;
|
| 433 |
+
border-color: transparent;
|
| 434 |
+
cursor: default !important;
|
| 435 |
+
pointer-events: none;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.chip:hover {
|
| 439 |
+
border-color: #666666;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.chip.active:hover {
|
| 443 |
+
background: var(--cocoa-xl);
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.chip i {
|
| 447 |
+
font-size: 9px;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.hidden-chip-container {
|
| 451 |
+
display: none !important;
|
| 452 |
+
margin: 0 !important;
|
| 453 |
+
padding: 0 !important;
|
| 454 |
+
height: 0 !important;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
/* ---- Toast ---- */
|
| 458 |
+
#toast-container {
|
| 459 |
+
position: fixed;
|
| 460 |
+
bottom: calc(var(--mob-nav-h) + 12px);
|
| 461 |
+
/* above bottom nav on mobile */
|
| 462 |
+
left: 50%;
|
| 463 |
+
transform: translateX(-50%);
|
| 464 |
+
z-index: 10000;
|
| 465 |
+
display: flex;
|
| 466 |
+
flex-direction: column;
|
| 467 |
+
align-items: center;
|
| 468 |
+
gap: 8px;
|
| 469 |
+
pointer-events: none;
|
| 470 |
+
width: 90%;
|
| 471 |
+
max-width: 360px;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.toast {
|
| 475 |
+
background: #111;
|
| 476 |
+
border: 1px solid #2a2a2a;
|
| 477 |
+
color: #f0ece6;
|
| 478 |
+
font-size: 11px;
|
| 479 |
+
font-weight: 600;
|
| 480 |
+
padding: 12px 18px;
|
| 481 |
+
border-radius: 10px;
|
| 482 |
+
display: flex;
|
| 483 |
+
align-items: center;
|
| 484 |
+
gap: 8px;
|
| 485 |
+
pointer-events: auto;
|
| 486 |
+
animation: toastIn 0.3s ease-out;
|
| 487 |
+
width: 100%;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.toast.toast-out {
|
| 491 |
+
animation: toastOut 0.3s ease-in forwards;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.toast-success {
|
| 495 |
+
border-color: #166534;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.toast-success i {
|
| 499 |
+
color: #22c55e;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.toast-error {
|
| 503 |
+
border-color: #7f1d1d;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.toast-error i {
|
| 507 |
+
color: #ef4444;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
.toast-info i {
|
| 511 |
+
color: var(--cocoa-l);
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
@keyframes toastIn {
|
| 515 |
+
from {
|
| 516 |
+
opacity: 0;
|
| 517 |
+
transform: translateY(20px);
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
to {
|
| 521 |
+
opacity: 1;
|
| 522 |
+
transform: translateY(0);
|
| 523 |
+
}
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
@keyframes toastOut {
|
| 527 |
+
from {
|
| 528 |
+
opacity: 1;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
to {
|
| 532 |
+
opacity: 0;
|
| 533 |
+
transform: translateY(20px);
|
| 534 |
+
}
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
/* ---- Stats empty overlay ---- */
|
| 538 |
+
.stats-empty-overlay {
|
| 539 |
+
position: absolute;
|
| 540 |
+
inset: 0;
|
| 541 |
+
z-index: 50;
|
| 542 |
+
display: flex;
|
| 543 |
+
flex-direction: column;
|
| 544 |
+
align-items: center;
|
| 545 |
+
justify-content: center;
|
| 546 |
+
background: rgba(10, 10, 10, 0.85);
|
| 547 |
+
backdrop-filter: blur(8px);
|
| 548 |
+
border-radius: 12px;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
/* ---- Feedback form ---- */
|
| 552 |
+
.fb-textarea {
|
| 553 |
+
background: #111;
|
| 554 |
+
border: 1px solid #2a2a2a;
|
| 555 |
+
border-radius: 8px;
|
| 556 |
+
color: #f0ece6;
|
| 557 |
+
font-size: 12px;
|
| 558 |
+
padding: 12px;
|
| 559 |
+
width: 100%;
|
| 560 |
+
min-height: 120px;
|
| 561 |
+
resize: vertical;
|
| 562 |
+
font-family: 'Inter', sans-serif;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.fb-textarea:focus {
|
| 566 |
+
outline: none;
|
| 567 |
+
border-color: var(--cocoa-l);
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.fb-select {
|
| 571 |
+
background: #111;
|
| 572 |
+
border: 1px solid #2a2a2a;
|
| 573 |
+
border-radius: 8px;
|
| 574 |
+
color: #f0ece6;
|
| 575 |
+
font-size: 11px;
|
| 576 |
+
padding: 10px 12px;
|
| 577 |
+
/* taller for touch */
|
| 578 |
+
width: 100%;
|
| 579 |
+
font-family: 'Inter', sans-serif;
|
| 580 |
+
min-height: 44px;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
.fb-select:focus {
|
| 584 |
+
outline: none;
|
| 585 |
+
border-color: var(--cocoa-l);
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.fb-stars {
|
| 589 |
+
display: flex;
|
| 590 |
+
gap: 8px;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.fb-star {
|
| 594 |
+
font-size: 28px;
|
| 595 |
+
/* larger for mobile tapping */
|
| 596 |
+
color: #333;
|
| 597 |
+
cursor: pointer;
|
| 598 |
+
transition: color 0.15s;
|
| 599 |
+
min-width: 36px;
|
| 600 |
+
min-height: 36px;
|
| 601 |
+
display: flex;
|
| 602 |
+
align-items: center;
|
| 603 |
+
justify-content: center;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.fb-star.active,
|
| 607 |
+
.fb-star:hover {
|
| 608 |
+
color: var(--cocoa-l);
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.fb-chip {
|
| 612 |
+
background: #050505;
|
| 613 |
+
border: 1px solid #222;
|
| 614 |
+
border-radius: 8px;
|
| 615 |
+
color: #666;
|
| 616 |
+
font-size: 10px;
|
| 617 |
+
font-weight: 700;
|
| 618 |
+
padding: 14px 12px;
|
| 619 |
+
/* taller for touch */
|
| 620 |
+
cursor: pointer;
|
| 621 |
+
transition: all 0.2s ease;
|
| 622 |
+
text-align: center;
|
| 623 |
+
text-transform: uppercase;
|
| 624 |
+
min-height: 44px;
|
| 625 |
+
display: flex;
|
| 626 |
+
align-items: center;
|
| 627 |
+
justify-content: center;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.fb-chip:hover {
|
| 631 |
+
border-color: #444;
|
| 632 |
+
color: #999;
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
.fb-chip.active {
|
| 636 |
+
border-color: var(--cocoa-l);
|
| 637 |
+
background: #111;
|
| 638 |
+
color: #fff;
|
| 639 |
+
box-shadow: 0 0 15px rgba(200, 154, 108, 0.15);
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
.fb-emoji-btn {
|
| 643 |
+
background: #111;
|
| 644 |
+
border: 1px solid #2a2a2a;
|
| 645 |
+
border-radius: 8px;
|
| 646 |
+
color: #555;
|
| 647 |
+
flex: 1;
|
| 648 |
+
text-align: center;
|
| 649 |
+
padding: 12px 4px;
|
| 650 |
+
/* taller */
|
| 651 |
+
cursor: pointer;
|
| 652 |
+
transition: all 0.2s ease;
|
| 653 |
+
min-height: 64px;
|
| 654 |
+
display: flex;
|
| 655 |
+
flex-direction: column;
|
| 656 |
+
align-items: center;
|
| 657 |
+
justify-content: center;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
.fb-emoji-btn:hover {
|
| 661 |
+
border-color: #444;
|
| 662 |
+
color: #888;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
.fb-emoji-btn.active {
|
| 666 |
+
border-color: var(--cocoa-l);
|
| 667 |
+
background: #1a1a1a;
|
| 668 |
+
color: var(--cocoa-l);
|
| 669 |
+
box-shadow: 0 0 15px rgba(200, 154, 108, 0.15);
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
/* =============================================
|
| 673 |
+
DESKTOP (≥1024px) — original layout intact
|
| 674 |
+
============================================= */
|
| 675 |
+
@media (min-width: 1024px) {
|
| 676 |
+
|
| 677 |
+
/* Sidebar visible */
|
| 678 |
+
aside.w-60 {
|
| 679 |
+
display: flex !important;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
/* Top mobile nav hidden */
|
| 683 |
+
.mobile-nav {
|
| 684 |
+
display: none !important;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
/* Bottom mobile nav hidden */
|
| 688 |
+
.mobile-bottom-nav {
|
| 689 |
+
display: none !important;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
/* Main — no bottom padding needed */
|
| 693 |
+
main {
|
| 694 |
+
padding-bottom: 1rem !important;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
/* Toast — desktop position: bottom-right */
|
| 698 |
+
#toast-container {
|
| 699 |
+
bottom: 20px;
|
| 700 |
+
left: unset;
|
| 701 |
+
right: 20px;
|
| 702 |
+
transform: none;
|
| 703 |
+
width: auto;
|
| 704 |
+
align-items: flex-end;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
/* Settings — 2 column grid */
|
| 708 |
+
#tab-settings .grid {
|
| 709 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
/* Run details — multi-column grids preserved */
|
| 713 |
+
#run-results-content {
|
| 714 |
+
grid-template-columns: repeat(3, 1fr) !important;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
.grid-cols-2 {
|
| 718 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.grid-cols-3 {
|
| 722 |
+
grid-template-columns: repeat(3, 1fr) !important;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
/* Reports grid */
|
| 726 |
+
#reports-grid,
|
| 727 |
+
#reports-pending {
|
| 728 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
/* About grid */
|
| 732 |
+
#tab-about .grid.grid-cols-3 {
|
| 733 |
+
grid-template-columns: repeat(3, 1fr) !important;
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
/* Post-process cards */
|
| 737 |
+
#post-process-cards {
|
| 738 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
/* Insights panel */
|
| 742 |
+
#insights-panel .grid {
|
| 743 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 744 |
+
}
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
/* =============================================
|
| 748 |
+
MOBILE (< 1024px) — full mobile overhaul
|
| 749 |
+
============================================= */
|
| 750 |
+
@media (max-width: 1023px) {
|
| 751 |
+
|
| 752 |
+
/* --- Hide desktop sidebar --- */
|
| 753 |
+
aside.w-60 {
|
| 754 |
+
display: none !important;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
/* --- Hide old top mobile nav bar --- */
|
| 758 |
+
.mobile-nav {
|
| 759 |
+
display: none !important;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
/* --- Body layout --- */
|
| 763 |
+
body {
|
| 764 |
+
height: 100dvh;
|
| 765 |
+
/* dynamic viewport height — accounts for mobile browser chrome */
|
| 766 |
+
overflow: hidden;
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
/* --- Main content — room for top and bottom nav --- */
|
| 770 |
+
main {
|
| 771 |
+
padding: 70px 12px calc(var(--mob-nav-h) + 8px) 12px !important;
|
| 772 |
+
gap: 12px !important;
|
| 773 |
+
display: flex !important;
|
| 774 |
+
flex-direction: column !important;
|
| 775 |
+
height: 100dvh !important;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
/* --- Tab Scrolling Fixes — force flex-1 to push progress bar down --- */
|
| 779 |
+
#tab-about,
|
| 780 |
+
#tab-overview,
|
| 781 |
+
#tab-run-details,
|
| 782 |
+
#tab-reports,
|
| 783 |
+
#tab-settings,
|
| 784 |
+
#tab-help,
|
| 785 |
+
#tab-feedback {
|
| 786 |
+
flex: 1 !important;
|
| 787 |
+
min-height: 0 !important;
|
| 788 |
+
padding-bottom: 20px !important;
|
| 789 |
+
overscroll-behavior: contain;
|
| 790 |
+
-webkit-overflow-scrolling: touch;
|
| 791 |
+
overflow-y: auto !important;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
/* --- About tab specific spacing --- */
|
| 795 |
+
#tab-about .space-y-8 {
|
| 796 |
+
gap: 16px !important;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
#tab-about .pt-8 {
|
| 800 |
+
padding-top: 16px !important;
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
#tab-overview:not(.hidden) {
|
| 804 |
+
display: flex !important;
|
| 805 |
+
flex-direction: column !important;
|
| 806 |
+
overflow-y: auto !important;
|
| 807 |
+
overflow-x: hidden !important;
|
| 808 |
+
-webkit-overflow-scrolling: touch;
|
| 809 |
+
overscroll-behavior: contain;
|
| 810 |
+
padding-bottom: calc(var(--mob-nav-h) + 24px) !important;
|
| 811 |
+
gap: 16px !important;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
#tab-overview>div:not(#stats-empty-state) {
|
| 815 |
+
grid-column: span 1 !important;
|
| 816 |
+
min-height: 280px;
|
| 817 |
+
flex-shrink: 0;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
.stats-empty-overlay {
|
| 821 |
+
position: fixed !important;
|
| 822 |
+
top: 58px;
|
| 823 |
+
/* below mobile top bar */
|
| 824 |
+
left: 0;
|
| 825 |
+
right: 0;
|
| 826 |
+
bottom: var(--mob-nav-h);
|
| 827 |
+
height: auto !important;
|
| 828 |
+
z-index: 100;
|
| 829 |
+
background: rgba(0, 0, 0, 0.98);
|
| 830 |
+
display: flex;
|
| 831 |
+
flex-direction: column;
|
| 832 |
+
align-items: center;
|
| 833 |
+
justify-content: center;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
/* CRITICAL: hide overlay when its parent tab is hidden */
|
| 837 |
+
#tab-overview.hidden .stats-empty-overlay {
|
| 838 |
+
display: none !important;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
/* Hide charts when curtain is up to prevent scroll jank */
|
| 842 |
+
#tab-overview.curtain-active {
|
| 843 |
+
overflow: hidden !important;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
/* --- Settings tab — tighter layout --- */
|
| 847 |
+
#tab-settings>div[class*="grid"] {
|
| 848 |
+
display: flex !important;
|
| 849 |
+
flex-direction: column !important;
|
| 850 |
+
gap: 12px !important;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
#tab-settings {
|
| 854 |
+
overflow-x: hidden !important;
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
/* Collapse chip panel completely when not shown — removes gap */
|
| 858 |
+
#chip-selector.hidden-chip-container {
|
| 859 |
+
display: none !important;
|
| 860 |
+
margin: 0 !important;
|
| 861 |
+
padding: 0 !important;
|
| 862 |
+
height: 0 !important;
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
/* When visible, give it breathing room */
|
| 866 |
+
#chip-selector:not(.hidden-chip-container) {
|
| 867 |
+
display: flex !important;
|
| 868 |
+
flex-wrap: wrap !important;
|
| 869 |
+
gap: 8px !important;
|
| 870 |
+
margin-top: 12px !important;
|
| 871 |
+
padding: 0 !important;
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
/* Ensure all s-row items are uniform flex rows */
|
| 875 |
+
.s-row {
|
| 876 |
+
display: flex !important;
|
| 877 |
+
align-items: center !important;
|
| 878 |
+
justify-content: space-between !important;
|
| 879 |
+
padding: 14px 0 !important;
|
| 880 |
+
border-bottom: 1px solid #1a1a1a !important;
|
| 881 |
+
gap: 12px !important;
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
.s-row:last-child {
|
| 885 |
+
border-bottom: none !important;
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
/* Never let mobile flex override Tailwind .hidden utility */
|
| 889 |
+
.s-row.hidden {
|
| 890 |
+
display: none !important;
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
/* The annotated video row overrides — it is flex-col on desktop; force flex-row on mobile */
|
| 894 |
+
.s-row[data-param="annotated"] {
|
| 895 |
+
flex-direction: column !important;
|
| 896 |
+
align-items: stretch !important;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
.s-row[data-param="annotated"]>.flex {
|
| 900 |
+
width: 100% !important;
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
.s-stepper {
|
| 904 |
+
width: 140px !important;
|
| 905 |
+
/* Compact fixed width */
|
| 906 |
+
scale: 0.9;
|
| 907 |
+
transform-origin: right;
|
| 908 |
+
display: inline-flex !important;
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
.toggle-track {
|
| 912 |
+
width: 36px !important;
|
| 913 |
+
scale: 0.9;
|
| 914 |
+
transform-origin: right;
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
@media (max-width: 480px) {
|
| 918 |
+
.s-row {
|
| 919 |
+
flex-direction: row !important;
|
| 920 |
+
align-items: center !important;
|
| 921 |
+
justify-content: space-between !important;
|
| 922 |
+
/* align toggles on right for discipline */
|
| 923 |
+
gap: 12px !important;
|
| 924 |
+
padding: 10px 16px !important;
|
| 925 |
+
width: 100% !important;
|
| 926 |
+
box-sizing: border-box !important;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
.s-row[data-param="annotated"] {
|
| 930 |
+
flex-direction: column !important;
|
| 931 |
+
align-items: stretch !important;
|
| 932 |
+
padding: 12px 16px !important;
|
| 933 |
+
/* matches normal s-row padding */
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
.s-row:not([data-param="annotated"]) {
|
| 937 |
+
padding: 12px 16px !important;
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
#tab-run-details .p-8 {
|
| 941 |
+
padding: 20px !important;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
#run-results-content {
|
| 945 |
+
grid-template-columns: 1fr !important;
|
| 946 |
+
gap: 16px !important;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
#panel-video .flex,
|
| 950 |
+
#panel-perf .flex,
|
| 951 |
+
#panel-model .flex,
|
| 952 |
+
#panel-infer .flex {
|
| 953 |
+
padding-bottom: 8px !important;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
.s-row .info-wrap {
|
| 957 |
+
display: inline-flex !important;
|
| 958 |
+
vertical-align: middle;
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
.s-row>div:first-child {
|
| 962 |
+
width: auto !important;
|
| 963 |
+
max-width: 75% !important;
|
| 964 |
+
flex: 1 !important;
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
.toggle-track {
|
| 968 |
+
width: 36px !important;
|
| 969 |
+
min-width: 36px !important;
|
| 970 |
+
height: 20px !important;
|
| 971 |
+
flex-shrink: 0 !important;
|
| 972 |
+
display: block !important;
|
| 973 |
+
position: relative !important;
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
#run-results-card .text-[10px] {
|
| 977 |
+
font-size: 9px !important;
|
| 978 |
+
letter-spacing: 0.05em !important;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
.s-row>.s-stepper {
|
| 982 |
+
width: 130px !important;
|
| 983 |
+
flex-shrink: 0 !important;
|
| 984 |
+
display: inline-flex !important;
|
| 985 |
+
flex-direction: row !important;
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
.chip-container {
|
| 989 |
+
display: grid !important;
|
| 990 |
+
grid-template-columns: 1fr 1fr !important;
|
| 991 |
+
gap: 6px !important;
|
| 992 |
+
margin-top: 12px !important;
|
| 993 |
+
padding: 10px !important;
|
| 994 |
+
background: rgba(255, 255, 255, 0.03);
|
| 995 |
+
border-radius: 8px;
|
| 996 |
+
border: 1px solid #1a1a1a;
|
| 997 |
+
width: 100% !important;
|
| 998 |
+
box-sizing: border-box !important;
|
| 999 |
+
}
|
| 1000 |
+
|
| 1001 |
+
.chip {
|
| 1002 |
+
padding: 6px !important;
|
| 1003 |
+
font-size: 9px !important;
|
| 1004 |
+
min-height: 32px !important;
|
| 1005 |
+
border-radius: 6px !important;
|
| 1006 |
+
justify-content: center !important;
|
| 1007 |
+
width: 100% !important;
|
| 1008 |
+
white-space: nowrap !important;
|
| 1009 |
+
}
|
| 1010 |
+
|
| 1011 |
+
.s-stepper {
|
| 1012 |
+
width: 130px !important;
|
| 1013 |
+
min-width: 130px !important;
|
| 1014 |
+
display: inline-flex !important;
|
| 1015 |
+
flex-direction: row !important;
|
| 1016 |
+
align-items: center !important;
|
| 1017 |
+
justify-content: space-between !important;
|
| 1018 |
+
transform-origin: right !important;
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
.toggle-track {
|
| 1022 |
+
transform-origin: right !important;
|
| 1023 |
+
}
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
/* --- Progress bar wrapper — remove extra margin to fix huge gap --- */
|
| 1027 |
+
#progress-bar-wrapper {
|
| 1028 |
+
width: 100% !important;
|
| 1029 |
+
max-width: 100% !important;
|
| 1030 |
+
box-sizing: border-box !important;
|
| 1031 |
+
margin-top: auto !important;
|
| 1032 |
+
margin-bottom: 4px !important;
|
| 1033 |
+
padding: 8px 12px !important;
|
| 1034 |
+
flex-direction: column !important;
|
| 1035 |
+
align-items: flex-start !important;
|
| 1036 |
+
gap: 6px !important;
|
| 1037 |
+
position: relative;
|
| 1038 |
+
z-index: 10;
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
#progress-bar-wrapper>div:first-child {
|
| 1042 |
+
width: 100% !important;
|
| 1043 |
+
flex: 1 !important;
|
| 1044 |
+
min-width: 0 !important;
|
| 1045 |
+
margin-right: 0 !important;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
#progress-bar-wrapper>div:last-child {
|
| 1049 |
+
width: 100% !important;
|
| 1050 |
+
justify-content: space-between !important;
|
| 1051 |
+
font-size: 10px !important;
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
/* --- All other grids collapse to single column --- */
|
| 1055 |
+
.grid-cols-3,
|
| 1056 |
+
.grid-cols-2,
|
| 1057 |
+
.lg\:grid-cols-2,
|
| 1058 |
+
.xl\:grid-cols-3 {
|
| 1059 |
+
grid-template-columns: 1fr !important;
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
/* --- Run details tab --- */
|
| 1063 |
+
#run-results-content {
|
| 1064 |
+
grid-template-columns: 1fr !important;
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
#tab-run-details .grid-cols-2,
|
| 1068 |
+
#tab-run-details .grid-cols-3 {
|
| 1069 |
+
grid-template-columns: 1fr !important;
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
/* --- Reports grid --- */
|
| 1073 |
+
#reports-grid,
|
| 1074 |
+
#reports-pending {
|
| 1075 |
+
grid-template-columns: 1fr !important;
|
| 1076 |
+
}
|
| 1077 |
+
|
| 1078 |
+
/* --- About tab grid --- */
|
| 1079 |
+
#tab-about .grid.grid-cols-3 {
|
| 1080 |
+
grid-template-columns: 1fr !important;
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
/* --- Post-process cards --- */
|
| 1084 |
+
#post-process-cards {
|
| 1085 |
+
grid-template-columns: 1fr !important;
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
/* --- Insights panel --- */
|
| 1089 |
+
#insights-panel .grid {
|
| 1090 |
+
grid-template-columns: 1fr !important;
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
/* --- Feedback tab --- */
|
| 1094 |
+
#tab-feedback .grid {
|
| 1095 |
+
grid-template-columns: 1fr !important;
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
/* --- About tab cards --- */
|
| 1099 |
+
#tab-about .bg-black.border.rounded-xl {
|
| 1100 |
+
padding: 20px !important;
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
/* --- Stepper — ensure full tap area --- */
|
| 1104 |
+
.s-stepper button {
|
| 1105 |
+
padding: 10px 14px;
|
| 1106 |
+
min-width: 40px;
|
| 1107 |
+
min-height: 40px;
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
/* --- s-row label text — allow wrap --- */
|
| 1111 |
+
.s-row>div:first-child .text-xs {
|
| 1112 |
+
font-size: 11px;
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
/* --- Help accordion buttons --- */
|
| 1116 |
+
#tab-help button.w-full {
|
| 1117 |
+
min-height: 52px;
|
| 1118 |
+
padding: 14px 16px !important;
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
/* --- Feedback priority chips grid --- */
|
| 1122 |
+
#fb-priorities {
|
| 1123 |
+
grid-template-columns: 1fr !important;
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
/* --- Keyboard shortcut modal --- */
|
| 1127 |
+
#appModal-shortcutsModal>div {
|
| 1128 |
+
max-width: 95% !important;
|
| 1129 |
+
padding: 20px !important;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
/* --- Privacy / Terms modals --- */
|
| 1133 |
+
[id^="appModal-"]>div {
|
| 1134 |
+
max-width: 95% !important;
|
| 1135 |
+
max-height: 80dvh !important;
|
| 1136 |
+
overflow-y: auto !important;
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
#tab-overview>div:last-child {
|
| 1140 |
+
min-height: 300px !important;
|
| 1141 |
+
padding-bottom: 4px !important;
|
| 1142 |
+
margin-bottom: 0 !important;
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
/* --- Vehicle Classification Internal Scroll --- */
|
| 1146 |
+
#tab-overview>div:nth-child(4) {
|
| 1147 |
+
max-height: 380px !important;
|
| 1148 |
+
display: flex !important;
|
| 1149 |
+
flex-direction: column !important;
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
#tab-overview>div:nth-child(4) #class-breakdown {
|
| 1153 |
+
flex: 1 !important;
|
| 1154 |
+
overflow-y: auto !important;
|
| 1155 |
+
min-height: 0 !important;
|
| 1156 |
+
}
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
/* =============================================
|
| 1160 |
+
BOTTOM NAVIGATION BAR — mobile only
|
| 1161 |
+
============================================= */
|
| 1162 |
+
.mobile-bottom-nav {
|
| 1163 |
+
display: none;
|
| 1164 |
+
/* hidden by default, shown on mobile */
|
| 1165 |
+
position: fixed;
|
| 1166 |
+
bottom: 0;
|
| 1167 |
+
left: 0;
|
| 1168 |
+
right: 0;
|
| 1169 |
+
height: 68px;
|
| 1170 |
+
background: #000000;
|
| 1171 |
+
border-top: 1px solid #1a1a1a;
|
| 1172 |
+
z-index: 40;
|
| 1173 |
+
align-items: stretch;
|
| 1174 |
+
}
|
| 1175 |
+
|
| 1176 |
+
.mob-nav-item {
|
| 1177 |
+
flex: 1;
|
| 1178 |
+
display: flex;
|
| 1179 |
+
flex-direction: column;
|
| 1180 |
+
align-items: center;
|
| 1181 |
+
justify-content: center;
|
| 1182 |
+
gap: 3px;
|
| 1183 |
+
cursor: pointer;
|
| 1184 |
+
color: #444444;
|
| 1185 |
+
font-size: 0;
|
| 1186 |
+
font-weight: 700;
|
| 1187 |
+
text-transform: uppercase;
|
| 1188 |
+
letter-spacing: 0.05em;
|
| 1189 |
+
transition: color 0.15s ease;
|
| 1190 |
+
border: none;
|
| 1191 |
+
background: none;
|
| 1192 |
+
padding: 8px 2px;
|
| 1193 |
+
-webkit-tap-highlight-color: transparent;
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
.mob-nav-item i {
|
| 1197 |
+
font-size: 22px;
|
| 1198 |
+
transition: color 0.15s ease;
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
.mob-nav-item.active {
|
| 1202 |
+
color: var(--cocoa-l);
|
| 1203 |
+
}
|
| 1204 |
+
|
| 1205 |
+
.mob-nav-item.active i {
|
| 1206 |
+
color: var(--cocoa-l);
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
.mob-nav-item:active {
|
| 1210 |
+
color: var(--cocoa-xl);
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
/* Show bottom nav only on mobile */
|
| 1214 |
+
@media (max-width: 1023px) {
|
| 1215 |
+
.mobile-bottom-nav {
|
| 1216 |
+
display: flex !important;
|
| 1217 |
+
}
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
/* =============================================
|
| 1221 |
+
MEDIUM TABLET (640px–1023px) adjustments
|
| 1222 |
+
============================================= */
|
| 1223 |
+
@media (min-width: 640px) and (max-width: 1023px) {
|
| 1224 |
+
|
| 1225 |
+
/* 2-column grids on tablet where it fits */
|
| 1226 |
+
#tab-overview>div {
|
| 1227 |
+
min-height: 280px;
|
| 1228 |
+
}
|
| 1229 |
+
|
| 1230 |
+
#reports-grid,
|
| 1231 |
+
#reports-pending {
|
| 1232 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
#fb-priorities {
|
| 1236 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 1237 |
+
}
|
| 1238 |
+
|
| 1239 |
+
#tab-about .grid.grid-cols-3 {
|
| 1240 |
+
grid-template-columns: repeat(2, 1fr) !important;
|
| 1241 |
+
}
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
/* =============================================
|
| 1245 |
+
TOUCH DEVICES — remove hover jank
|
| 1246 |
+
============================================= */
|
| 1247 |
+
@media (hover: none) and (pointer: coarse) {
|
| 1248 |
+
.nav-item-inactive:hover {
|
| 1249 |
+
color: #555555 !important;
|
| 1250 |
+
background-color: transparent !important;
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
.chip:hover {
|
| 1254 |
+
border-color: #333333;
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
.chip.active:hover {
|
| 1258 |
+
background: var(--cocoa-l);
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
.s-stepper button:hover {
|
| 1262 |
+
background: transparent;
|
| 1263 |
+
color: #666666;
|
| 1264 |
+
}
|
| 1265 |
+
|
| 1266 |
+
/* Make all interactive elements minimum 44px tall */
|
| 1267 |
+
button,
|
| 1268 |
+
.fb-emoji-btn,
|
| 1269 |
+
.mob-nav-item {
|
| 1270 |
+
min-height: 44px;
|
| 1271 |
+
}
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
/* ============================================================
|
| 1275 |
+
Custom Select Dropdown (uf-select)
|
| 1276 |
+
Replaces native <select> to prevent OS picker sheet on mobile
|
| 1277 |
+
============================================================ */
|
| 1278 |
+
.uf-select-wrap {
|
| 1279 |
+
position: relative;
|
| 1280 |
+
display: inline-block;
|
| 1281 |
+
min-width: 110px;
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
.uf-select-wrap.w-full {
|
| 1285 |
+
display: block;
|
| 1286 |
+
width: 100%;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
.uf-select-trigger {
|
| 1290 |
+
display: flex;
|
| 1291 |
+
align-items: center;
|
| 1292 |
+
justify-content: space-between;
|
| 1293 |
+
gap: 6px;
|
| 1294 |
+
padding: 5px 10px;
|
| 1295 |
+
background: #111111;
|
| 1296 |
+
border: 1px solid #222222;
|
| 1297 |
+
border-radius: 6px;
|
| 1298 |
+
font-size: 11px;
|
| 1299 |
+
font-weight: 600;
|
| 1300 |
+
color: #ffffff;
|
| 1301 |
+
cursor: pointer;
|
| 1302 |
+
user-select: none;
|
| 1303 |
+
-webkit-tap-highlight-color: transparent;
|
| 1304 |
+
transition: border-color 0.15s;
|
| 1305 |
+
white-space: nowrap;
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
.uf-select-trigger:hover,
|
| 1309 |
+
.uf-select-trigger:active {
|
| 1310 |
+
border-color: #444444;
|
| 1311 |
+
}
|
| 1312 |
+
|
| 1313 |
+
.uf-select-arrow {
|
| 1314 |
+
font-size: 9px;
|
| 1315 |
+
color: #666666;
|
| 1316 |
+
transition: transform 0.2s ease;
|
| 1317 |
+
flex-shrink: 0;
|
| 1318 |
+
}
|
| 1319 |
+
|
| 1320 |
+
.uf-select-arrow-open {
|
| 1321 |
+
transform: rotate(180deg);
|
| 1322 |
+
}
|
| 1323 |
+
|
| 1324 |
+
/* Dropdown panel — opens downward by default */
|
| 1325 |
+
.uf-select-dropdown {
|
| 1326 |
+
position: absolute;
|
| 1327 |
+
top: calc(100% + 4px);
|
| 1328 |
+
left: 0;
|
| 1329 |
+
min-width: 100%;
|
| 1330 |
+
background: #111111;
|
| 1331 |
+
border: 1px solid #2a2a2a;
|
| 1332 |
+
border-radius: 8px;
|
| 1333 |
+
z-index: 9999;
|
| 1334 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.8);
|
| 1335 |
+
overflow: hidden;
|
| 1336 |
+
max-height: 240px;
|
| 1337 |
+
overflow-y: auto;
|
| 1338 |
+
}
|
| 1339 |
+
|
| 1340 |
+
/* Upward variant — anchors above trigger, for bottom-of-screen selects */
|
| 1341 |
+
.uf-select-dropdown-up {
|
| 1342 |
+
top: auto;
|
| 1343 |
+
bottom: calc(100% + 4px);
|
| 1344 |
+
}
|
| 1345 |
+
|
| 1346 |
+
.uf-select-option {
|
| 1347 |
+
padding: 10px 14px;
|
| 1348 |
+
font-size: 11px;
|
| 1349 |
+
font-weight: 600;
|
| 1350 |
+
color: #aaaaaa;
|
| 1351 |
+
cursor: pointer;
|
| 1352 |
+
transition: background 0.1s, color 0.1s;
|
| 1353 |
+
-webkit-tap-highlight-color: transparent;
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
.uf-select-option:hover,
|
| 1357 |
+
.uf-select-option:active {
|
| 1358 |
+
background: #1a1a1a;
|
| 1359 |
+
color: #ffffff;
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
.uf-select-option-active {
|
| 1363 |
+
color: var(--cocoa-l);
|
| 1364 |
+
background: #0a0a0a;
|
| 1365 |
+
}
|
| 1366 |
+
|
| 1367 |
+
/* Hide scrollbar inside dropdown — options fit within max-height */
|
| 1368 |
+
.uf-select-dropdown::-webkit-scrollbar {
|
| 1369 |
+
width: 0;
|
| 1370 |
+
height: 0;
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
/* Desktop: Vehicle Classification thin grey scrollbar (matches reference) */
|
| 1374 |
+
@media (min-width: 1024px) {
|
| 1375 |
+
#class-breakdown::-webkit-scrollbar {
|
| 1376 |
+
width: 4px;
|
| 1377 |
+
}
|
| 1378 |
+
#class-breakdown::-webkit-scrollbar-track {
|
| 1379 |
+
background: #000000;
|
| 1380 |
+
}
|
| 1381 |
+
#class-breakdown::-webkit-scrollbar-thumb {
|
| 1382 |
+
background: #333333;
|
| 1383 |
+
border-radius: 4px;
|
| 1384 |
+
}
|
| 1385 |
+
#class-breakdown::-webkit-scrollbar-thumb:hover {
|
| 1386 |
+
background: #444444;
|
| 1387 |
+
}
|
| 1388 |
+
}
|
| 1389 |
+
|
frontend/initial.html
CHANGED
|
@@ -16,16 +16,22 @@
|
|
| 16 |
}
|
| 17 |
</script>
|
| 18 |
<meta charset="UTF-8">
|
| 19 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 20 |
<title>UrbanFlow</title>
|
| 21 |
-
<link rel="icon" type="image/png" href="assets/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 23 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 24 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 25 |
<link rel="stylesheet" href="css/initial.css">
|
| 26 |
</head>
|
| 27 |
|
| 28 |
-
<body class="bg-black text-white min-h-screen w-full flex flex-col items-center selection:bg-white selection:text-black">
|
| 29 |
|
| 30 |
<header class="mt-16 flex flex-col items-center flex-shrink-0 w-full z-10">
|
| 31 |
<img src="assets/uf_rf.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
|
|
@@ -33,28 +39,31 @@
|
|
| 33 |
|
| 34 |
<main class="flex-1 w-full max-w-[90rem] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20 px-10 py-6 items-center z-10">
|
| 35 |
|
| 36 |
-
<div class="lg:col-span-7 flex flex-col justify-center
|
| 37 |
-
<h1 class="text-
|
| 38 |
-
style="background:linear-gradient(
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
</h1>
|
| 41 |
-
<p class="font-bold mb-8 text-sm uppercase tracking-[0.2em] flex items-center" style="color:#a89f97">
|
| 42 |
<span class="core-badge px-3 py-1 rounded-full text-[10px] mr-3">DEMO</span>
|
| 43 |
Cloud-Native Traffic Intelligence
|
| 44 |
</p>
|
| 45 |
<ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-medium" style="color:#a89f97">
|
| 46 |
-
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> No new hardware — works with your existing cameras</li>
|
| 47 |
-
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Granular vehicle counts across 14 classes</li>
|
| 48 |
-
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Directional flow & congestion insights in minutes</li>
|
| 49 |
-
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Downloadable reports ready for planning & compliance</li>
|
| 50 |
-
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Built for Indian roads — tested on real field conditions</li>
|
| 51 |
</ul>
|
| 52 |
</div>
|
| 53 |
|
| 54 |
-
<div class="lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto
|
| 55 |
|
| 56 |
<!-- STEP: Modules -->
|
| 57 |
-
<div id="step-modules" class="w-full flex flex-col fade-in">
|
| 58 |
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">UrbanFlow</h2>
|
| 59 |
<p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Select an analytical module to continue.</p>
|
| 60 |
<div class="flex justify-center w-full">
|
|
@@ -70,16 +79,16 @@
|
|
| 70 |
</div>
|
| 71 |
|
| 72 |
<!-- STEP: Upload -->
|
| 73 |
-
<div id="step-upload" class="hidden w-full flex flex-col fade-in">
|
| 74 |
<button onclick="showStep('modules')"
|
| 75 |
-
class="text-
|
| 76 |
<i class="fa-solid fa-arrow-left mr-2"></i> Back
|
| 77 |
</button>
|
| 78 |
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Source Media</h2>
|
| 79 |
<p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Submit camera footage to begin traffic analysis.</p>
|
| 80 |
<input id="file-input" type="file" accept="video/*" class="hidden">
|
| 81 |
<div id="dropzone" onclick="document.getElementById('file-input').click()"
|
| 82 |
-
class="border border-dashed border-neutral-700 rounded-[2rem] p-12 flex flex-col items-center justify-center cursor-pointer transition-all duration-300 group">
|
| 83 |
<i class="fa-solid fa-arrow-up-from-bracket text-4xl mb-5 block mx-auto transition" style="color:#a89f97"></i>
|
| 84 |
<span class="font-semibold text-lg mb-2 text-center block" style="color:#f0ece6">Drop or select a video file</span>
|
| 85 |
<span class="text-[10px] font-bold uppercase tracking-widest text-center block" style="color:#a89f97">Any standard video format accepted</span>
|
|
@@ -93,10 +102,10 @@
|
|
| 93 |
<div id="upload-bar" class="h-full w-0 transition-all duration-75 ease-linear rounded-full" style="background:#c89a6c"></div>
|
| 94 |
</div>
|
| 95 |
</div>
|
| 96 |
-
</div>
|
| 97 |
|
| 98 |
<!-- STEP: Draw -->
|
| 99 |
-
<div id="step-draw" class="hidden w-full flex flex-col fade-in">
|
| 100 |
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Spatial Boundary</h2>
|
| 101 |
<p class="text-[11px] font-bold uppercase tracking-widest mb-6 text-center" style="color:#a89f97">Mark two points to define the vehicle counting threshold</p>
|
| 102 |
<div class="relative w-full aspect-video bg-neutral-950 rounded-3xl overflow-hidden cursor-crosshair mb-6">
|
|
@@ -115,7 +124,7 @@
|
|
| 115 |
Continue →
|
| 116 |
</button>
|
| 117 |
<button onclick="resetCanvas()"
|
| 118 |
-
class="text-[10px] font-bold uppercase tracking-widest text-slate-500 hover:text-white transition"
|
| 119 |
style="background:none;border:none;">Reset Boundary</button>
|
| 120 |
</div>
|
| 121 |
</div>
|
|
@@ -123,16 +132,32 @@
|
|
| 123 |
</div>
|
| 124 |
</main>
|
| 125 |
|
| 126 |
-
<footer class="w-full max-w-[90rem] mx-auto px-10
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
<
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
</div>
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
</div>
|
|
|
|
| 136 |
</footer>
|
| 137 |
|
| 138 |
<script src="js/initial.js"></script>
|
|
@@ -161,6 +186,7 @@
|
|
| 161 |
<p style="color:#a89f97;font-size:11px;margin-bottom:20px">We keep this simple and honest.</p>
|
| 162 |
<ul style="color:#a89f97;font-size:11px;line-height:1.9;padding-left:16px;list-style:disc;text-align:left">
|
| 163 |
<li>This is a <strong style="color:#f0ece6">public demo</strong> hosted on Hugging Face Spaces. It is not a production service.</li>
|
|
|
|
| 164 |
<li>Footage you submit is processed in real time and <strong style="color:#f0ece6">discarded immediately</strong> after the session ends. Nothing is stored on our servers.</li>
|
| 165 |
<li>We do not use your footage to train models, sell it, or share it with any third party.</li>
|
| 166 |
<li>Reports and annotated videos are generated temporarily and delivered to your device. We do not retain copies.</li>
|
|
@@ -199,7 +225,8 @@
|
|
| 199 |
<li>Misrepresent demo outputs as certified or regulatory-grade traffic data.</li>
|
| 200 |
</ul>
|
| 201 |
<p style="color:#a89f97;font-size:11px;text-align:left">This platform is provided as-is for <strong style="color:#f0ece6">demonstration and evaluation purposes only</strong>.
|
| 202 |
-
|
|
|
|
| 203 |
<p style="color:#555;font-size:10px;margin-top:16px;text-align:left">Questions: <strong
|
| 204 |
style="color:#c89a6c">support.urbanflow365@gmail.com</strong></p>
|
| 205 |
</div>
|
|
@@ -239,5 +266,12 @@
|
|
| 239 |
</div>
|
| 240 |
</div>
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
</body>
|
| 243 |
</html>
|
|
|
|
| 16 |
}
|
| 17 |
</script>
|
| 18 |
<meta charset="UTF-8">
|
| 19 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 20 |
<title>UrbanFlow</title>
|
| 21 |
+
<link rel="icon" type="image/png" sizes="512x512" href="assets/shuriken.png">
|
| 22 |
+
<link rel="manifest" href="manifest.json">
|
| 23 |
+
<meta name="theme-color" content="#000000">
|
| 24 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 25 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
| 26 |
+
<meta name="apple-mobile-web-app-title" content="UrbanFlow">
|
| 27 |
+
<link rel="apple-touch-icon" sizes="512x512" href="assets/shurkien_b.png">
|
| 28 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 29 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 30 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 31 |
<link rel="stylesheet" href="css/initial.css">
|
| 32 |
</head>
|
| 33 |
|
| 34 |
+
<body class="bg-black text-white min-h-screen w-full flex flex-col items-center selection:bg-white selection:text-black overflow-x-hidden">
|
| 35 |
|
| 36 |
<header class="mt-16 flex flex-col items-center flex-shrink-0 w-full z-10">
|
| 37 |
<img src="assets/uf_rf.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
|
|
|
|
| 39 |
|
| 40 |
<main class="flex-1 w-full max-w-[90rem] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20 px-10 py-6 items-center z-10">
|
| 41 |
|
| 42 |
+
<div class="hero-text-section lg:col-span-7 flex flex-col justify-center pb-6 lg:pb-0">
|
| 43 |
+
<h1 class="hero-title text-center sm:text-left font-extrabold mb-6 sm:mb-4 tracking-tight"
|
| 44 |
+
style="background:linear-gradient(135deg,#f0ece6 0%,#f0ece6 35%,#d4b08a 60%,#c89a6c 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
|
| 45 |
+
<span class="block sm:inline">Automated</span>
|
| 46 |
+
<br class="hidden sm:block">
|
| 47 |
+
<span class="block sm:inline">Vision</span>
|
| 48 |
+
<span class="block sm:inline">Intelligence</span>
|
| 49 |
</h1>
|
| 50 |
+
<p class="font-bold mb-8 text-sm uppercase tracking-[0.2em] flex items-center justify-center sm:justify-start" style="color:#a89f97">
|
| 51 |
<span class="core-badge px-3 py-1 rounded-full text-[10px] mr-3">DEMO</span>
|
| 52 |
Cloud-Native Traffic Intelligence
|
| 53 |
</p>
|
| 54 |
<ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-medium" style="color:#a89f97">
|
| 55 |
+
<li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> No new hardware — works with your existing cameras</li>
|
| 56 |
+
<li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> Granular vehicle counts across 14 classes</li>
|
| 57 |
+
<li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> Directional flow & congestion insights in minutes</li>
|
| 58 |
+
<li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> Downloadable reports ready for planning & compliance</li>
|
| 59 |
+
<li class="flex items-center"><i class="fa-solid fa-check mr-3 md:mr-5 text-xl" style="color:#c89a6c"></i> Built for Indian roads — tested on real field conditions</li>
|
| 60 |
</ul>
|
| 61 |
</div>
|
| 62 |
|
| 63 |
+
<div class="step-card-section lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto h-auto md:h-[480px] lg:h-[520px] mb-12 lg:mb-0">
|
| 64 |
|
| 65 |
<!-- STEP: Modules -->
|
| 66 |
+
<div id="step-modules" class="w-full flex flex-col fade-in overflow-y-auto">
|
| 67 |
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">UrbanFlow</h2>
|
| 68 |
<p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Select an analytical module to continue.</p>
|
| 69 |
<div class="flex justify-center w-full">
|
|
|
|
| 79 |
</div>
|
| 80 |
|
| 81 |
<!-- STEP: Upload -->
|
| 82 |
+
<div id="step-upload" class="hidden w-full flex flex-col fade-in overflow-y-auto">
|
| 83 |
<button onclick="showStep('modules')"
|
| 84 |
+
class="text-[#a89f97] hover:text-white hover:underline transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
|
| 85 |
<i class="fa-solid fa-arrow-left mr-2"></i> Back
|
| 86 |
</button>
|
| 87 |
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Source Media</h2>
|
| 88 |
<p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Submit camera footage to begin traffic analysis.</p>
|
| 89 |
<input id="file-input" type="file" accept="video/*" class="hidden">
|
| 90 |
<div id="dropzone" onclick="document.getElementById('file-input').click()"
|
| 91 |
+
class="border border-dashed border-neutral-700 rounded-[2rem] p-8 md:p-12 flex flex-col items-center justify-center cursor-pointer transition-all duration-300 group">
|
| 92 |
<i class="fa-solid fa-arrow-up-from-bracket text-4xl mb-5 block mx-auto transition" style="color:#a89f97"></i>
|
| 93 |
<span class="font-semibold text-lg mb-2 text-center block" style="color:#f0ece6">Drop or select a video file</span>
|
| 94 |
<span class="text-[10px] font-bold uppercase tracking-widest text-center block" style="color:#a89f97">Any standard video format accepted</span>
|
|
|
|
| 102 |
<div id="upload-bar" class="h-full w-0 transition-all duration-75 ease-linear rounded-full" style="background:#c89a6c"></div>
|
| 103 |
</div>
|
| 104 |
</div>
|
| 105 |
+
</div>
|
| 106 |
|
| 107 |
<!-- STEP: Draw -->
|
| 108 |
+
<div id="step-draw" class="hidden w-full flex flex-col fade-in overflow-y-auto">
|
| 109 |
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Spatial Boundary</h2>
|
| 110 |
<p class="text-[11px] font-bold uppercase tracking-widest mb-6 text-center" style="color:#a89f97">Mark two points to define the vehicle counting threshold</p>
|
| 111 |
<div class="relative w-full aspect-video bg-neutral-950 rounded-3xl overflow-hidden cursor-crosshair mb-6">
|
|
|
|
| 124 |
Continue →
|
| 125 |
</button>
|
| 126 |
<button onclick="resetCanvas()"
|
| 127 |
+
class="text-[10px] font-bold uppercase tracking-widest text-slate-500 hover:text-white transition px-4 py-2 mt-2"
|
| 128 |
style="background:none;border:none;">Reset Boundary</button>
|
| 129 |
</div>
|
| 130 |
</div>
|
|
|
|
| 132 |
</div>
|
| 133 |
</main>
|
| 134 |
|
| 135 |
+
<footer class="w-full max-w-[90rem] mx-auto px-10 mt-auto z-10 text-[11px] font-bold uppercase tracking-[0.2em]" style="color:#777">
|
| 136 |
+
|
| 137 |
+
<!-- Desktop: Privacy Policy left | T&C center | © right — single row -->
|
| 138 |
+
<div class="hidden md:grid md:grid-cols-3 items-center py-6">
|
| 139 |
+
<div class="text-left">
|
| 140 |
+
<button onclick="openAppModal('privacyModal')" class="hover:text-white transition">Privacy Policy</button>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="text-center">
|
| 143 |
+
<button onclick="openAppModal('termsModal')" class="hover:text-white transition">Terms & Conditions</button>
|
| 144 |
+
</div>
|
| 145 |
+
<div class="text-right">
|
| 146 |
+
© 2026 UrbanFlow. All rights reserved.
|
| 147 |
+
</div>
|
| 148 |
</div>
|
| 149 |
+
|
| 150 |
+
<!-- Mobile: Privacy Policy left | T&C right, then © centered below -->
|
| 151 |
+
<div class="md:hidden py-4">
|
| 152 |
+
<div class="flex items-center justify-between mb-2">
|
| 153 |
+
<button onclick="openAppModal('privacyModal')" class="hover:text-white transition">Privacy Policy</button>
|
| 154 |
+
<button onclick="openAppModal('termsModal')" class="hover:text-white transition">Terms & Conditions</button>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="text-center">
|
| 157 |
+
© 2026 UrbanFlow. All rights reserved.
|
| 158 |
+
</div>
|
| 159 |
</div>
|
| 160 |
+
|
| 161 |
</footer>
|
| 162 |
|
| 163 |
<script src="js/initial.js"></script>
|
|
|
|
| 186 |
<p style="color:#a89f97;font-size:11px;margin-bottom:20px">We keep this simple and honest.</p>
|
| 187 |
<ul style="color:#a89f97;font-size:11px;line-height:1.9;padding-left:16px;list-style:disc;text-align:left">
|
| 188 |
<li>This is a <strong style="color:#f0ece6">public demo</strong> hosted on Hugging Face Spaces. It is not a production service.</li>
|
| 189 |
+
<li>UrbanFlow provides an estimated accuracy of ±5–8% on dense mixed-traffic footage. Results may vary slightly across runs due to the nature of real-time frame-by-frame inference.</li>
|
| 190 |
<li>Footage you submit is processed in real time and <strong style="color:#f0ece6">discarded immediately</strong> after the session ends. Nothing is stored on our servers.</li>
|
| 191 |
<li>We do not use your footage to train models, sell it, or share it with any third party.</li>
|
| 192 |
<li>Reports and annotated videos are generated temporarily and delivered to your device. We do not retain copies.</li>
|
|
|
|
| 225 |
<li>Misrepresent demo outputs as certified or regulatory-grade traffic data.</li>
|
| 226 |
</ul>
|
| 227 |
<p style="color:#a89f97;font-size:11px;text-align:left">This platform is provided as-is for <strong style="color:#f0ece6">demonstration and evaluation purposes only</strong>.
|
| 228 |
+
UrbanFlow provides an estimated accuracy of ±5–8% on dense mixed-traffic footage. For research or planning use, we recommend processing the same video 2–3 times and taking the average count.</p>
|
| 229 |
+
<p style="color:#a89f97;font-size:11px;text-align:left">Outputs are not intended for operational, regulatory, or safety-critical use. This is an early-stage research project, not a commercial product.</p>
|
| 230 |
<p style="color:#555;font-size:10px;margin-top:16px;text-align:left">Questions: <strong
|
| 231 |
style="color:#c89a6c">support.urbanflow365@gmail.com</strong></p>
|
| 232 |
</div>
|
|
|
|
| 266 |
</div>
|
| 267 |
</div>
|
| 268 |
|
| 269 |
+
<script>
|
| 270 |
+
if ('serviceWorker' in navigator) {
|
| 271 |
+
window.addEventListener('load', () => {
|
| 272 |
+
navigator.serviceWorker.register('./sw.js');
|
| 273 |
+
});
|
| 274 |
+
}
|
| 275 |
+
</script>
|
| 276 |
</body>
|
| 277 |
</html>
|
frontend/js/initial.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
let runConfig = {};
|
| 3 |
|
|
|
|
| 4 |
function showStep(name) {
|
| 5 |
['modules', 'upload', 'draw'].forEach(s => {
|
| 6 |
const el = document.getElementById('step-' + s);
|
|
@@ -20,7 +26,8 @@ function showStep(name) {
|
|
| 20 |
if (name === 'draw') loadFirstFrame();
|
| 21 |
}
|
| 22 |
|
| 23 |
-
|
|
|
|
| 24 |
const fileInput = document.getElementById('file-input');
|
| 25 |
|
| 26 |
if (fileInput) {
|
|
@@ -30,34 +37,54 @@ if (fileInput) {
|
|
| 30 |
}
|
| 31 |
|
| 32 |
if (dropzone) {
|
|
|
|
| 33 |
dropzone.addEventListener('dragover', e => {
|
| 34 |
e.preventDefault();
|
| 35 |
-
dropzone.classList.add('
|
| 36 |
});
|
| 37 |
dropzone.addEventListener('dragleave', () => {
|
| 38 |
-
dropzone.classList.remove('
|
| 39 |
});
|
| 40 |
dropzone.addEventListener('drop', e => {
|
| 41 |
e.preventDefault();
|
| 42 |
-
dropzone.classList.remove('
|
| 43 |
if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]);
|
| 44 |
});
|
| 45 |
}
|
| 46 |
|
|
|
|
| 47 |
let currentXHR = null;
|
| 48 |
|
| 49 |
function uploadFile(file) {
|
| 50 |
if (currentXHR) currentXHR.abort();
|
| 51 |
|
| 52 |
const dropzoneEl = document.getElementById('dropzone');
|
| 53 |
-
const prog
|
| 54 |
-
const bar
|
| 55 |
-
const pct
|
| 56 |
-
const txt
|
| 57 |
|
| 58 |
if (dropzoneEl) dropzoneEl.classList.add('hidden');
|
| 59 |
if (prog) prog.classList.remove('hidden');
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
const form = new FormData();
|
| 62 |
form.append('file', file);
|
| 63 |
|
|
@@ -65,54 +92,68 @@ function uploadFile(file) {
|
|
| 65 |
currentXHR = xhr;
|
| 66 |
xhr.open('POST', 'upload');
|
| 67 |
|
|
|
|
| 68 |
xhr.upload.onprogress = e => {
|
| 69 |
if (e.lengthComputable) {
|
| 70 |
-
const
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
};
|
| 75 |
|
| 76 |
xhr.onerror = () => {
|
|
|
|
| 77 |
txt.innerText = 'Error: Network failure';
|
| 78 |
txt.classList.add('text-red-500');
|
| 79 |
-
fileInput.value = '';
|
| 80 |
};
|
| 81 |
|
| 82 |
xhr.onload = () => {
|
|
|
|
|
|
|
| 83 |
if (xhr.status !== 200) {
|
| 84 |
txt.innerText = 'Error: ' + xhr.status;
|
| 85 |
txt.classList.add('text-red-500');
|
| 86 |
-
fileInput.value = '';
|
| 87 |
return;
|
| 88 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
const res = JSON.parse(xhr.responseText);
|
| 90 |
videoId = res.video_id;
|
| 91 |
txt.innerText = 'Extracting Metadata...';
|
| 92 |
-
bar.style.width = '100%';
|
| 93 |
-
pct.innerText = '100%';
|
| 94 |
|
| 95 |
fetch('config/' + videoId)
|
| 96 |
.then(r => r.json())
|
| 97 |
.then(cfg => {
|
| 98 |
-
runConfig
|
| 99 |
runConfig.conf = 0.12;
|
| 100 |
runConfig.iou = 0.60;
|
| 101 |
-
txt.innerText
|
| 102 |
-
fileInput.value = '';
|
| 103 |
setTimeout(() => showStep('draw'), 800);
|
| 104 |
})
|
| 105 |
.catch(() => {
|
| 106 |
txt.innerText = 'Metadata Failed';
|
| 107 |
txt.classList.add('text-red-500');
|
| 108 |
-
fileInput.value = '';
|
| 109 |
});
|
| 110 |
};
|
| 111 |
|
| 112 |
xhr.send(form);
|
| 113 |
}
|
| 114 |
|
| 115 |
-
//
|
|
|
|
|
|
|
|
|
|
| 116 |
const canvas = document.getElementById('drawing-canvas');
|
| 117 |
const ctx = canvas ? canvas.getContext('2d') : null;
|
| 118 |
let points = [];
|
|
@@ -125,7 +166,9 @@ function loadFirstFrame() {
|
|
| 125 |
img.onerror = () => {
|
| 126 |
console.error('Failed to load first frame');
|
| 127 |
if (placeholder) {
|
| 128 |
-
placeholder.innerHTML =
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
};
|
| 131 |
|
|
@@ -140,31 +183,69 @@ function loadFirstFrame() {
|
|
| 140 |
}
|
| 141 |
|
| 142 |
function initCanvas() {
|
| 143 |
-
if (canvas)
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
| 147 |
}
|
| 148 |
|
| 149 |
window.addEventListener('resize', initCanvas);
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
if (canvas) {
|
| 152 |
canvas.addEventListener('mousedown', e => {
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
const cx = e.clientX - rect.left;
|
| 156 |
-
const cy = e.clientY - rect.top;
|
| 157 |
-
const rx = (cx / canvas.width) * imgNatW;
|
| 158 |
-
const ry = (cy / canvas.height) * imgNatH;
|
| 159 |
-
points.push({ cx, cy, rx: Math.round(rx), ry: Math.round(ry) });
|
| 160 |
-
drawDot(cx, cy);
|
| 161 |
-
if (points.length === 2) drawLine();
|
| 162 |
});
|
| 163 |
}
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
function drawDot(x, y) {
|
|
|
|
| 166 |
ctx.beginPath();
|
| 167 |
-
ctx.arc(x, y,
|
| 168 |
ctx.fillStyle = '#c89a6c';
|
| 169 |
ctx.fill();
|
| 170 |
ctx.strokeStyle = '#f0ece6';
|
|
@@ -173,6 +254,7 @@ function drawDot(x, y) {
|
|
| 173 |
}
|
| 174 |
|
| 175 |
function drawLine() {
|
|
|
|
| 176 |
ctx.beginPath();
|
| 177 |
ctx.moveTo(points[0].cx, points[0].cy);
|
| 178 |
ctx.lineTo(points[1].cx, points[1].cy);
|
|
@@ -184,6 +266,7 @@ function drawLine() {
|
|
| 184 |
function resetCanvas() {
|
| 185 |
points = [];
|
| 186 |
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
| 187 |
}
|
| 188 |
|
| 189 |
function startRun() {
|
|
@@ -194,26 +277,41 @@ function startRun() {
|
|
| 194 |
line: line,
|
| 195 |
config: runConfig
|
| 196 |
}));
|
|
|
|
| 197 |
window.location.href = './';
|
| 198 |
}
|
| 199 |
|
|
|
|
| 200 |
// Onboarding
|
|
|
|
| 201 |
let _obStep = 0;
|
| 202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
function nextOnboardStep() {
|
| 204 |
_obStep++;
|
| 205 |
if (_obStep >= 3) { closeOnboarding(); return; }
|
| 206 |
document.querySelectorAll('.onboard-step').forEach((s, i) => s.classList.toggle('active', i === _obStep));
|
| 207 |
document.querySelectorAll('.onboard-dot').forEach((d, i) => d.classList.toggle('active', i === _obStep));
|
| 208 |
if (_obStep === 2) document.getElementById('onboard-next').innerText = 'Get Started';
|
|
|
|
| 209 |
}
|
| 210 |
|
| 211 |
function closeOnboarding() {
|
| 212 |
-
document.getElementById('onboard-overlay')
|
|
|
|
| 213 |
}
|
| 214 |
|
| 215 |
-
// Show onboarding on every page load
|
| 216 |
document.addEventListener('DOMContentLoaded', () => {
|
| 217 |
const onboard = document.getElementById('onboard-overlay');
|
| 218 |
-
if (onboard)
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// =============================================
|
| 2 |
+
// UrbanFlow — initial.js (Mobile-First)
|
| 3 |
+
// Added: touch events, mobile upload UX
|
| 4 |
+
// =============================================
|
| 5 |
+
|
| 6 |
+
let videoId = null;
|
| 7 |
let runConfig = {};
|
| 8 |
|
| 9 |
+
// ---- Step navigation ----
|
| 10 |
function showStep(name) {
|
| 11 |
['modules', 'upload', 'draw'].forEach(s => {
|
| 12 |
const el = document.getElementById('step-' + s);
|
|
|
|
| 26 |
if (name === 'draw') loadFirstFrame();
|
| 27 |
}
|
| 28 |
|
| 29 |
+
// ---- File input / dropzone ----
|
| 30 |
+
const dropzone = document.getElementById('dropzone');
|
| 31 |
const fileInput = document.getElementById('file-input');
|
| 32 |
|
| 33 |
if (fileInput) {
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
if (dropzone) {
|
| 40 |
+
// Desktop drag-and-drop
|
| 41 |
dropzone.addEventListener('dragover', e => {
|
| 42 |
e.preventDefault();
|
| 43 |
+
dropzone.classList.add('dz-active');
|
| 44 |
});
|
| 45 |
dropzone.addEventListener('dragleave', () => {
|
| 46 |
+
dropzone.classList.remove('dz-active');
|
| 47 |
});
|
| 48 |
dropzone.addEventListener('drop', e => {
|
| 49 |
e.preventDefault();
|
| 50 |
+
dropzone.classList.remove('dz-active');
|
| 51 |
if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]);
|
| 52 |
});
|
| 53 |
}
|
| 54 |
|
| 55 |
+
// ---- Upload ----
|
| 56 |
let currentXHR = null;
|
| 57 |
|
| 58 |
function uploadFile(file) {
|
| 59 |
if (currentXHR) currentXHR.abort();
|
| 60 |
|
| 61 |
const dropzoneEl = document.getElementById('dropzone');
|
| 62 |
+
const prog = document.getElementById('upload-progress-container');
|
| 63 |
+
const bar = document.getElementById('upload-bar');
|
| 64 |
+
const pct = document.getElementById('upload-percentage');
|
| 65 |
+
const txt = document.getElementById('upload-text');
|
| 66 |
|
| 67 |
if (dropzoneEl) dropzoneEl.classList.add('hidden');
|
| 68 |
if (prog) prog.classList.remove('hidden');
|
| 69 |
|
| 70 |
+
// ---- Simulated progress (proxy-buffer-safe) ----
|
| 71 |
+
// Estimate upload duration: ~1 MB/s conservative, capped between 3s and 60s
|
| 72 |
+
const fileMB = file.size / (1024 * 1024);
|
| 73 |
+
const estDurationMs = Math.min(Math.max(fileMB * 1000, 3000), 60000);
|
| 74 |
+
const targetPct = 100; // stop simulation at 100%, snap to 100% on load
|
| 75 |
+
const tickMs = 200; // update every 200ms
|
| 76 |
+
const totalTicks = estDurationMs / tickMs;
|
| 77 |
+
const stepPerTick = targetPct / totalTicks;
|
| 78 |
+
|
| 79 |
+
let simPct = 0;
|
| 80 |
+
let simInterval = setInterval(() => {
|
| 81 |
+
if (simPct < targetPct) {
|
| 82 |
+
simPct = Math.min(simPct + stepPerTick, targetPct);
|
| 83 |
+
bar.style.width = simPct.toFixed(1) + '%';
|
| 84 |
+
pct.innerText = Math.floor(simPct) + '%';
|
| 85 |
+
}
|
| 86 |
+
}, tickMs);
|
| 87 |
+
|
| 88 |
const form = new FormData();
|
| 89 |
form.append('file', file);
|
| 90 |
|
|
|
|
| 92 |
currentXHR = xhr;
|
| 93 |
xhr.open('POST', 'upload');
|
| 94 |
|
| 95 |
+
// Real progress override — fires if proxy reports actual bytes (rare but handle it)
|
| 96 |
xhr.upload.onprogress = e => {
|
| 97 |
if (e.lengthComputable) {
|
| 98 |
+
const realPct = Math.round(e.loaded / e.total * 100);
|
| 99 |
+
// Only override simulation if real progress is AHEAD of it
|
| 100 |
+
if (realPct > simPct) {
|
| 101 |
+
simPct = realPct;
|
| 102 |
+
bar.style.width = simPct + '%';
|
| 103 |
+
pct.innerText = simPct + '%';
|
| 104 |
+
}
|
| 105 |
}
|
| 106 |
};
|
| 107 |
|
| 108 |
xhr.onerror = () => {
|
| 109 |
+
clearInterval(simInterval);
|
| 110 |
txt.innerText = 'Error: Network failure';
|
| 111 |
txt.classList.add('text-red-500');
|
| 112 |
+
if (fileInput) fileInput.value = '';
|
| 113 |
};
|
| 114 |
|
| 115 |
xhr.onload = () => {
|
| 116 |
+
clearInterval(simInterval);
|
| 117 |
+
|
| 118 |
if (xhr.status !== 200) {
|
| 119 |
txt.innerText = 'Error: ' + xhr.status;
|
| 120 |
txt.classList.add('text-red-500');
|
| 121 |
+
if (fileInput) fileInput.value = '';
|
| 122 |
return;
|
| 123 |
}
|
| 124 |
+
|
| 125 |
+
// Snap to 100% on successful upload response
|
| 126 |
+
bar.style.width = '100%';
|
| 127 |
+
pct.innerText = '100%';
|
| 128 |
+
|
| 129 |
const res = JSON.parse(xhr.responseText);
|
| 130 |
videoId = res.video_id;
|
| 131 |
txt.innerText = 'Extracting Metadata...';
|
|
|
|
|
|
|
| 132 |
|
| 133 |
fetch('config/' + videoId)
|
| 134 |
.then(r => r.json())
|
| 135 |
.then(cfg => {
|
| 136 |
+
runConfig = cfg;
|
| 137 |
runConfig.conf = 0.12;
|
| 138 |
runConfig.iou = 0.60;
|
| 139 |
+
txt.innerText = 'Initialization Complete';
|
| 140 |
+
if (fileInput) fileInput.value = '';
|
| 141 |
setTimeout(() => showStep('draw'), 800);
|
| 142 |
})
|
| 143 |
.catch(() => {
|
| 144 |
txt.innerText = 'Metadata Failed';
|
| 145 |
txt.classList.add('text-red-500');
|
| 146 |
+
if (fileInput) fileInput.value = '';
|
| 147 |
});
|
| 148 |
};
|
| 149 |
|
| 150 |
xhr.send(form);
|
| 151 |
}
|
| 152 |
|
| 153 |
+
// =============================================
|
| 154 |
+
// CANVAS — Spatial Boundary Drawing
|
| 155 |
+
// Supports both mouse (desktop) and touch (mobile)
|
| 156 |
+
// =============================================
|
| 157 |
const canvas = document.getElementById('drawing-canvas');
|
| 158 |
const ctx = canvas ? canvas.getContext('2d') : null;
|
| 159 |
let points = [];
|
|
|
|
| 166 |
img.onerror = () => {
|
| 167 |
console.error('Failed to load first frame');
|
| 168 |
if (placeholder) {
|
| 169 |
+
placeholder.innerHTML =
|
| 170 |
+
'<i class="fa-solid fa-circle-exclamation text-4xl mb-3 opacity-50" style="color:#c89a6c"></i>' +
|
| 171 |
+
'<span class="font-bold text-[10px] uppercase tracking-widest opacity-50 block mt-2">Frame Load Error</span>';
|
| 172 |
}
|
| 173 |
};
|
| 174 |
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
function initCanvas() {
|
| 186 |
+
if (!canvas) return;
|
| 187 |
+
canvas.width = canvas.offsetWidth;
|
| 188 |
+
canvas.height = canvas.offsetHeight;
|
| 189 |
+
// Redraw existing points after resize
|
| 190 |
+
redrawCanvas();
|
| 191 |
}
|
| 192 |
|
| 193 |
window.addEventListener('resize', initCanvas);
|
| 194 |
|
| 195 |
+
// ---- Coordinate helpers ----
|
| 196 |
+
function getCanvasCoords(clientX, clientY) {
|
| 197 |
+
const rect = canvas.getBoundingClientRect();
|
| 198 |
+
const cx = clientX - rect.left;
|
| 199 |
+
const cy = clientY - rect.top;
|
| 200 |
+
const rx = (cx / canvas.width) * imgNatW;
|
| 201 |
+
const ry = (cy / canvas.height) * imgNatH;
|
| 202 |
+
return { cx, cy, rx: Math.round(rx), ry: Math.round(ry) };
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
function addPoint(coords) {
|
| 206 |
+
if (points.length >= 2) return;
|
| 207 |
+
points.push(coords);
|
| 208 |
+
redrawCanvas();
|
| 209 |
+
if (points.length === 2 && canvas) {
|
| 210 |
+
canvas.style.cursor = 'default';
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
function redrawCanvas() {
|
| 215 |
+
if (!ctx) return;
|
| 216 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 217 |
+
points.forEach(p => drawDot(p.cx, p.cy));
|
| 218 |
+
if (points.length === 2) drawLine();
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// ---- Mouse events (desktop) ----
|
| 222 |
if (canvas) {
|
| 223 |
canvas.addEventListener('mousedown', e => {
|
| 224 |
+
e.preventDefault();
|
| 225 |
+
addPoint(getCanvasCoords(e.clientX, e.clientY));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
});
|
| 227 |
}
|
| 228 |
|
| 229 |
+
// ---- Touch events (mobile) ----
|
| 230 |
+
if (canvas) {
|
| 231 |
+
canvas.addEventListener('touchstart', e => {
|
| 232 |
+
e.preventDefault(); // prevent scroll while drawing
|
| 233 |
+
if (e.touches.length === 0) return;
|
| 234 |
+
const touch = e.touches[0];
|
| 235 |
+
addPoint(getCanvasCoords(touch.clientX, touch.clientY));
|
| 236 |
+
}, { passive: false });
|
| 237 |
+
|
| 238 |
+
// touchmove: prevent page scroll when finger is on canvas
|
| 239 |
+
canvas.addEventListener('touchmove', e => {
|
| 240 |
+
e.preventDefault();
|
| 241 |
+
}, { passive: false });
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// ---- Drawing helpers ----
|
| 245 |
function drawDot(x, y) {
|
| 246 |
+
if (!ctx) return;
|
| 247 |
ctx.beginPath();
|
| 248 |
+
ctx.arc(x, y, 6, 0, Math.PI * 2); // slightly larger dot for mobile visibility
|
| 249 |
ctx.fillStyle = '#c89a6c';
|
| 250 |
ctx.fill();
|
| 251 |
ctx.strokeStyle = '#f0ece6';
|
|
|
|
| 254 |
}
|
| 255 |
|
| 256 |
function drawLine() {
|
| 257 |
+
if (!ctx || points.length < 2) return;
|
| 258 |
ctx.beginPath();
|
| 259 |
ctx.moveTo(points[0].cx, points[0].cy);
|
| 260 |
ctx.lineTo(points[1].cx, points[1].cy);
|
|
|
|
| 266 |
function resetCanvas() {
|
| 267 |
points = [];
|
| 268 |
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 269 |
+
if (canvas) canvas.style.cursor = 'crosshair';
|
| 270 |
}
|
| 271 |
|
| 272 |
function startRun() {
|
|
|
|
| 277 |
line: line,
|
| 278 |
config: runConfig
|
| 279 |
}));
|
| 280 |
+
sessionStorage.setItem('uf_active_tab', 'settings');
|
| 281 |
window.location.href = './';
|
| 282 |
}
|
| 283 |
|
| 284 |
+
// =============================================
|
| 285 |
// Onboarding
|
| 286 |
+
// =============================================
|
| 287 |
let _obStep = 0;
|
| 288 |
|
| 289 |
+
function triggerIconPulse() {
|
| 290 |
+
const activeStep = document.querySelector('.onboard-step.active i');
|
| 291 |
+
if (activeStep) {
|
| 292 |
+
activeStep.classList.add('pulse-once');
|
| 293 |
+
setTimeout(() => activeStep.classList.remove('pulse-once'), 400);
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
function nextOnboardStep() {
|
| 298 |
_obStep++;
|
| 299 |
if (_obStep >= 3) { closeOnboarding(); return; }
|
| 300 |
document.querySelectorAll('.onboard-step').forEach((s, i) => s.classList.toggle('active', i === _obStep));
|
| 301 |
document.querySelectorAll('.onboard-dot').forEach((d, i) => d.classList.toggle('active', i === _obStep));
|
| 302 |
if (_obStep === 2) document.getElementById('onboard-next').innerText = 'Get Started';
|
| 303 |
+
triggerIconPulse();
|
| 304 |
}
|
| 305 |
|
| 306 |
function closeOnboarding() {
|
| 307 |
+
const overlay = document.getElementById('onboard-overlay');
|
| 308 |
+
if (overlay) overlay.style.display = 'none';
|
| 309 |
}
|
| 310 |
|
|
|
|
| 311 |
document.addEventListener('DOMContentLoaded', () => {
|
| 312 |
const onboard = document.getElementById('onboard-overlay');
|
| 313 |
+
if (onboard) {
|
| 314 |
+
onboard.style.display = 'flex';
|
| 315 |
+
triggerIconPulse();
|
| 316 |
+
}
|
| 317 |
+
});
|
frontend/js/vehicles.js
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
-
|
| 3 |
function animateValue(obj, start, end, duration) {
|
| 4 |
let startTimestamp = null;
|
| 5 |
const isPct = typeof end === 'string' && end.includes('%');
|
|
@@ -27,72 +92,97 @@
|
|
| 27 |
window.requestAnimationFrame(step);
|
| 28 |
}
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
} else {
|
| 44 |
-
|
|
|
|
| 45 |
}
|
| 46 |
-
|
| 47 |
-
});
|
| 48 |
-
|
| 49 |
-
document.addEventListener('mouseout', e => {
|
| 50 |
-
const wrap = e.target.closest('.info-wrap');
|
| 51 |
-
if (!wrap) return;
|
| 52 |
-
const tip = wrap.querySelector('.info-tip');
|
| 53 |
-
if (tip) tip.style.display = 'none';
|
| 54 |
-
});
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
if (
|
| 61 |
-
|
| 62 |
-
if (!tip) return;
|
| 63 |
-
|
| 64 |
-
const isVisible = tip.style.display === 'block';
|
| 65 |
-
tip.style.display = isVisible ? 'none' : 'block';
|
| 66 |
-
|
| 67 |
-
if (!isVisible) {
|
| 68 |
-
const rect = wrap.getBoundingClientRect();
|
| 69 |
-
const tipH = tip.offsetHeight || 60;
|
| 70 |
-
if (rect.bottom + tipH + 10 > window.innerHeight) {
|
| 71 |
-
tip.style.top = (rect.top - tipH - 6) + 'px';
|
| 72 |
-
} else {
|
| 73 |
-
tip.style.top = (rect.bottom + 6) + 'px';
|
| 74 |
-
}
|
| 75 |
-
tip.style.left = Math.min(rect.left, window.innerWidth - 300) + 'px';
|
| 76 |
}
|
| 77 |
-
});
|
| 78 |
-
|
| 79 |
-
// =========== Tab switching ===========
|
| 80 |
-
function switchTab(tab) {
|
| 81 |
-
['about', 'overview', 'run-details', 'reports', 'settings', 'help', 'feedback'].forEach(t => {
|
| 82 |
-
const el = document.getElementById('tab-' + t);
|
| 83 |
-
const nav = document.getElementById('nav-' + t);
|
| 84 |
-
if (el) el.classList.toggle('hidden', tab !== t);
|
| 85 |
-
if (nav) {
|
| 86 |
-
if (tab === t) {
|
| 87 |
-
nav.classList.add('nav-item-active');
|
| 88 |
-
nav.classList.remove('nav-item-inactive');
|
| 89 |
-
} else {
|
| 90 |
-
nav.classList.remove('nav-item-active');
|
| 91 |
-
nav.classList.add('nav-item-inactive');
|
| 92 |
-
}
|
| 93 |
-
}
|
| 94 |
-
});
|
| 95 |
}
|
|
|
|
|
|
|
| 96 |
|
| 97 |
// =========== Toast System ===========
|
| 98 |
function showToast(message, type) {
|
|
@@ -244,66 +334,72 @@
|
|
| 244 |
const panel = document.getElementById('insights-panel');
|
| 245 |
panel.classList.remove('hidden');
|
| 246 |
|
| 247 |
-
// Speed distribution bars
|
| 248 |
-
const dist = d.speed_distribution || {};
|
| 249 |
-
const bars = document.getElementById('speed-bars');
|
| 250 |
-
const colors = { slow: '#ef4444', normal: '#eab308', fast: '#22c55e' };
|
| 251 |
-
const labels = { slow: 'Slow', normal: 'Normal', fast: 'Fast' };
|
| 252 |
-
bars.innerHTML = ['slow', 'normal', 'fast'].map(cat => {
|
| 253 |
-
const pct = dist[cat] || 0;
|
| 254 |
-
const h = Math.max(8, pct * 1.2);
|
| 255 |
-
return `<div class="flex flex-col items-center gap-1">
|
| 256 |
-
<span class="text-[10px] font-bold" style="color:${colors[cat]}">${pct}%</span>
|
| 257 |
-
<div style="width:36px;height:${h}px;background:${colors[cat]};border-radius:6px;transition:height 0.5s"></div>
|
| 258 |
-
<span class="text-[9px] font-bold text-slate-500 uppercase">${labels[cat]}</span>
|
| 259 |
-
</div>`;
|
| 260 |
-
}).join('');
|
| 261 |
-
|
| 262 |
// Congestion insights
|
| 263 |
const ci = document.getElementById('congestion-insights');
|
| 264 |
const pcu = d.pcu || {};
|
| 265 |
ci.innerHTML = [
|
| 266 |
infoRow('Total PCU', pcu.total_pcu || 0, 'Passenger Car Units (IRC:106-1990). Normalizes mixed traffic.'),
|
| 267 |
infoRow('PCU In / Out', `${pcu.pcu_in || 0} / ${pcu.pcu_out || 0}`, 'Directional PCU split.'),
|
| 268 |
-
infoRow('Speed Profile', `${dist.slow || 0}% slow · ${dist.normal || 0}% normal · ${dist.fast || 0}% fast`, 'Relative speed categories within this video.'),
|
| 269 |
].join('');
|
| 270 |
|
| 271 |
-
// ---- Also populate Stats tab cards ----
|
| 272 |
-
// Speed card in Stats
|
| 273 |
-
const speedCard = document.getElementById('speed-stats-card');
|
| 274 |
-
if (speedCard) {
|
| 275 |
-
speedCard.innerHTML = `<div class="flex gap-5 items-end justify-center w-full">
|
| 276 |
-
${['slow', 'normal', 'fast'].map(cat => {
|
| 277 |
-
const pct = dist[cat] || 0;
|
| 278 |
-
const h = Math.max(12, pct * 1.0);
|
| 279 |
-
return `<div class="flex flex-col items-center gap-1.5 flex-1">
|
| 280 |
-
<span class="text-lg font-black" style="color:${colors[cat]}">${pct}%</span>
|
| 281 |
-
<div style="width:100%;max-width:48px;height:${h}px;background:${colors[cat]};border-radius:8px;transition:height 0.5s"></div>
|
| 282 |
-
<span class="text-[10px] font-bold text-slate-500 uppercase">${labels[cat]}</span>
|
| 283 |
-
</div>`;
|
| 284 |
-
}).join('')}
|
| 285 |
-
</div>`;
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
// PCU card in Stats
|
| 289 |
const pcuCard = document.getElementById('pcu-stats-card');
|
| 290 |
if (pcuCard) {
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
</div>
|
| 301 |
-
<div class="flex
|
| 302 |
-
<
|
| 303 |
-
<
|
| 304 |
</div>
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
}
|
| 308 |
}
|
| 309 |
|
|
@@ -336,9 +432,9 @@
|
|
| 336 |
const res = c.resolution || [0, 0];
|
| 337 |
|
| 338 |
document.getElementById('panel-video').innerHTML =
|
| 339 |
-
detailRow('video_fps', c.video_fps) +
|
| 340 |
detailRow('frames', c.frames) +
|
| 341 |
-
detailRow('duration', c.duration + ' sec') +
|
| 342 |
detailRow('resolution', res[0] + ' <span class="text-slate-400 text-xs">x</span> ' + res[1]) +
|
| 343 |
detailRow('pixels', (c.pixels || 0).toLocaleString());
|
| 344 |
|
|
@@ -394,101 +490,127 @@
|
|
| 394 |
gold: { congestion: '#fbbf24', congestionBg: 'rgba(251,191,36,0.08)', dominance: '#a8a29e', flow: '#f5f5f4', doughIn: '#f5f5f4', doughOut: '#fbbf24' }
|
| 395 |
};
|
| 396 |
|
| 397 |
-
//
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
let currentPalette = runSettings.palette || 'default';
|
| 401 |
-
let activePalette = PALETTES[currentPalette];
|
| 402 |
-
|
| 403 |
-
// =========== Charts ===========
|
| 404 |
-
Chart.defaults.font.family = "'Montserrat', sans-serif";
|
| 405 |
-
Chart.defaults.color = '#888888';
|
| 406 |
-
Chart.defaults.borderColor = '#222222';
|
| 407 |
-
Chart.defaults.plugins.tooltip.backgroundColor = '#0a0a0a';
|
| 408 |
-
Chart.defaults.plugins.tooltip.titleColor = '#ffffff';
|
| 409 |
-
Chart.defaults.plugins.tooltip.bodyColor = '#aaaaaa';
|
| 410 |
-
Chart.defaults.plugins.tooltip.borderColor = '#222222';
|
| 411 |
-
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
| 412 |
-
|
| 413 |
let MODEL_CLASSES = {};
|
| 414 |
let BUSINESS_MAP = {};
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
},
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
},
|
| 452 |
-
options: {
|
| 453 |
-
responsive: true, maintainAspectRatio: false,
|
| 454 |
-
cutout: '68%',
|
| 455 |
-
plugins: {
|
| 456 |
-
legend: { display: true, position: 'bottom', labels: { padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 10, weight: '600' } } }
|
| 457 |
},
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
| 472 |
},
|
| 473 |
-
|
| 474 |
-
}
|
| 475 |
-
plugins: []
|
| 476 |
-
});
|
| 477 |
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
|
|
|
|
|
|
| 487 |
},
|
| 488 |
-
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
// =========== Update functions ===========
|
| 494 |
function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
|
|
@@ -587,6 +709,14 @@
|
|
| 587 |
});
|
| 588 |
const reportRow = document.getElementById('sv-report');
|
| 589 |
if (reportRow) reportRow.closest('.s-row').classList.add('disabled');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
const annotatedRow = document.getElementById('sv-annotated');
|
| 591 |
if (annotatedRow) annotatedRow.closest('.s-row').classList.add('disabled');
|
| 592 |
const wrap = document.getElementById('settings-start-wrap');
|
|
@@ -694,28 +824,6 @@
|
|
| 694 |
// =========== Main ===========
|
| 695 |
let _params = null;
|
| 696 |
|
| 697 |
-
async function init() {
|
| 698 |
-
const raw = sessionStorage.getItem('funky_run');
|
| 699 |
-
if (!raw) { window.location.href = './'; return; }
|
| 700 |
-
|
| 701 |
-
_params = JSON.parse(raw);
|
| 702 |
-
|
| 703 |
-
// SECURITY: Clear session storage so refresh always redirects home
|
| 704 |
-
sessionStorage.removeItem('funky_run');
|
| 705 |
-
|
| 706 |
-
const cRes = await fetch('constants');
|
| 707 |
-
const cData = await cRes.json();
|
| 708 |
-
MODEL_CLASSES = cData.classes;
|
| 709 |
-
BUSINESS_MAP = cData.business_map;
|
| 710 |
-
|
| 711 |
-
populateAndInit(_params);
|
| 712 |
-
|
| 713 |
-
// SECURITY: Clear session storage after populate so refresh triggers redirect
|
| 714 |
-
sessionStorage.removeItem('funky_run');
|
| 715 |
-
|
| 716 |
-
// Show Settings tab first, but also initialized with About implicitly in sidebar hierarchy
|
| 717 |
-
switchTab('settings');
|
| 718 |
-
}
|
| 719 |
|
| 720 |
function populateAndInit(params) {
|
| 721 |
populateRunDetails(params.config);
|
|
@@ -763,16 +871,7 @@
|
|
| 763 |
document.getElementById('proc-label').innerText = 'Processing';
|
| 764 |
|
| 765 |
// Reset Run Tab Results to Awaiting
|
| 766 |
-
const analyzeAgainBtn = document.getElementById('run-analyze-again-btn');
|
| 767 |
-
if (analyzeAgainBtn) {
|
| 768 |
-
analyzeAgainBtn.classList.add('hidden');
|
| 769 |
-
}
|
| 770 |
|
| 771 |
-
const badge = document.getElementById('results-status-badge');
|
| 772 |
-
if (badge) {
|
| 773 |
-
badge.innerText = 'Processing';
|
| 774 |
-
badge.className = 'px-2.5 py-1 bg-slate-800 text-white text-[10px] font-bold rounded-full uppercase tracking-tighter animate-pulse';
|
| 775 |
-
}
|
| 776 |
document.getElementById('run-results-content').innerHTML = `
|
| 777 |
<div class="flex flex-col items-center justify-center p-8 bg-black/40 border border-slate-800 rounded-2xl col-span-3 text-slate-500">
|
| 778 |
<i class="fa-solid fa-spinner fa-spin text-2xl mb-3 text-white"></i>
|
|
@@ -783,15 +882,16 @@
|
|
| 783 |
const repIcon = document.getElementById('reports-pending-icon');
|
| 784 |
if (repIcon) repIcon.className = 'fa-solid fa-circle-notch fa-spin text-[#c89a6c]';
|
| 785 |
const repText = document.getElementById('reports-pending-text');
|
| 786 |
-
if (repText) repText.innerText = '
|
| 787 |
|
| 788 |
|
| 789 |
// Start WebSocket
|
| 790 |
const videoDuration = _params.config.duration || 10;
|
| 791 |
|
| 792 |
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
| 793 |
-
|
| 794 |
-
const
|
|
|
|
| 795 |
|
| 796 |
ws.onopen = () => {
|
| 797 |
ws.send(JSON.stringify({
|
|
@@ -810,10 +910,6 @@
|
|
| 810 |
console.error('WS Error:', e);
|
| 811 |
document.getElementById('proc-label').innerText = 'Connection Error';
|
| 812 |
showToast('Connection error — server may be busy', 'error');
|
| 813 |
-
if (badge) {
|
| 814 |
-
badge.innerText = 'Pipeline Failed';
|
| 815 |
-
badge.className = 'px-2.5 py-1 bg-red-900/40 text-red-100 text-[10px] font-bold rounded-full uppercase tracking-tighter';
|
| 816 |
-
}
|
| 817 |
};
|
| 818 |
|
| 819 |
let processingDone = false;
|
|
@@ -823,11 +919,6 @@
|
|
| 823 |
if (!processingDone) {
|
| 824 |
// Closed before done=True received — show error state
|
| 825 |
document.getElementById('proc-label').innerText = 'Disconnected';
|
| 826 |
-
const badge = document.getElementById('results-status-badge');
|
| 827 |
-
if (badge) {
|
| 828 |
-
badge.innerText = 'Connection Lost';
|
| 829 |
-
badge.className = 'px-2.5 py-1 bg-red-900/40 text-red-300 text-[10px] font-bold rounded-full uppercase tracking-tighter';
|
| 830 |
-
}
|
| 831 |
document.getElementById('run-results-content').innerHTML = `
|
| 832 |
<div class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-red-900/40 rounded-2xl col-span-3 text-slate-400">
|
| 833 |
<i class="fa-solid fa-triangle-exclamation text-2xl mb-3 text-red-400"></i>
|
|
@@ -841,6 +932,8 @@
|
|
| 841 |
};
|
| 842 |
|
| 843 |
let lastUIUpdate = 0;
|
|
|
|
|
|
|
| 844 |
|
| 845 |
ws.onmessage = e => {
|
| 846 |
const d = JSON.parse(e.data);
|
|
@@ -851,11 +944,6 @@
|
|
| 851 |
if (d.error) {
|
| 852 |
processingDone = true;
|
| 853 |
document.getElementById('proc-label').innerText = 'Engine Error';
|
| 854 |
-
const badge = document.getElementById('results-status-badge');
|
| 855 |
-
if (badge) {
|
| 856 |
-
badge.innerText = 'Failed';
|
| 857 |
-
badge.className = 'px-2.5 py-1 bg-red-900/40 text-red-300 text-[10px] font-bold rounded-full uppercase tracking-tighter';
|
| 858 |
-
}
|
| 859 |
console.error('[UrbanFlow] Engine error:', d.detail || d.error);
|
| 860 |
document.getElementById('run-results-content').innerHTML = `
|
| 861 |
<div class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-red-900/40 rounded-2xl col-span-3 text-slate-400">
|
|
@@ -884,22 +972,18 @@
|
|
| 884 |
}
|
| 885 |
}
|
| 886 |
|
| 887 |
-
// Update Run Tab Badge
|
| 888 |
-
const badge = document.getElementById('results-status-badge');
|
| 889 |
-
if (badge) {
|
| 890 |
-
badge.innerText = 'Completed';
|
| 891 |
-
badge.className = 'px-2.5 py-1 bg-white text-black text-[10px] font-bold rounded-full uppercase tracking-tighter';
|
| 892 |
-
}
|
| 893 |
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
|
|
|
| 897 |
}
|
| 898 |
|
|
|
|
| 899 |
document.getElementById('run-results-content').innerHTML =
|
| 900 |
-
detailRow('Inference Time', d.processing_time + ' sec') +
|
| 901 |
-
infoRow('Throughput (FPS)', d.actual_fps, 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
|
| 902 |
-
infoRow('Real-time Ratio', d.speed_vs_realtime + 'x', 'Processing speed relative to video playback rate.');
|
| 903 |
|
| 904 |
if (d.video_id) {
|
| 905 |
loadReports(d.video_id).then(data => {
|
|
@@ -925,6 +1009,12 @@
|
|
| 925 |
if (jsonToggle) {
|
| 926 |
jsonToggle.closest('.s-row').classList.add('disabled');
|
| 927 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 928 |
const csvToggle = document.getElementById('sv-export-csv');
|
| 929 |
if (csvToggle) {
|
| 930 |
csvToggle.closest('.s-row').classList.add('disabled');
|
|
@@ -937,6 +1027,7 @@
|
|
| 937 |
// Toast + Insights
|
| 938 |
showToast('Processing complete — artifacts ready', 'success');
|
| 939 |
renderInsights(d);
|
|
|
|
| 940 |
|
| 941 |
// Store video_id for keyboard shortcut download
|
| 942 |
document.body.setAttribute('data-last-video-id', d.video_id);
|
|
@@ -969,14 +1060,26 @@
|
|
| 969 |
doughChart.data.datasets[0].data = [totalIn, totalOut];
|
| 970 |
doughChart.update();
|
| 971 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
const now = performance.now();
|
| 973 |
if (now - lastUIUpdate < 300) return;
|
| 974 |
lastUIUpdate = now;
|
| 975 |
|
| 976 |
-
updateCongestion(
|
| 977 |
updateBreakdown(d.class_in, d.class_out);
|
| 978 |
updateDominance(d.class_in, d.class_out);
|
| 979 |
-
buildFlowHistogram(
|
| 980 |
};
|
| 981 |
}
|
| 982 |
|
|
@@ -1003,8 +1106,10 @@
|
|
| 1003 |
const data = await res.json();
|
| 1004 |
if (!data.files || !data.files.length) return null;
|
| 1005 |
|
| 1006 |
-
document.getElementById('reports-pending')
|
| 1007 |
-
|
|
|
|
|
|
|
| 1008 |
document.getElementById('post-process-cards').classList.remove('hidden');
|
| 1009 |
const grid = document.getElementById('reports-grid');
|
| 1010 |
grid.classList.remove('hidden');
|
|
@@ -1105,5 +1210,121 @@
|
|
| 1105 |
}
|
| 1106 |
}
|
| 1107 |
|
| 1108 |
-
|
| 1109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// =========== Custom Select Dropdowns ===========
|
| 2 |
+
function ufSelectToggle(id) {
|
| 3 |
+
const wrap = document.getElementById(id + '-wrap') || document.getElementById(id + '-trigger')?.closest('.uf-select-wrap');
|
| 4 |
+
const trigger = document.getElementById(id + '-trigger');
|
| 5 |
+
const dropdown = document.getElementById(id + '-dropdown');
|
| 6 |
+
const arrow = document.getElementById(id + '-arrow');
|
| 7 |
+
if (!dropdown) return;
|
| 8 |
+
|
| 9 |
+
const isOpen = !dropdown.classList.contains('hidden');
|
| 10 |
+
|
| 11 |
+
// Close all open dropdowns first
|
| 12 |
+
document.querySelectorAll('.uf-select-dropdown').forEach(d => {
|
| 13 |
+
d.classList.add('hidden');
|
| 14 |
+
d.classList.remove('uf-select-dropdown-up');
|
| 15 |
+
});
|
| 16 |
+
document.querySelectorAll('.uf-select-arrow').forEach(a => a.classList.remove('uf-select-arrow-open'));
|
| 17 |
+
|
| 18 |
+
if (!isOpen) {
|
| 19 |
+
// Detect direction: open upward only if there's not enough space below
|
| 20 |
+
if (trigger) {
|
| 21 |
+
const rect = trigger.getBoundingClientRect();
|
| 22 |
+
const spaceBelow = window.innerHeight - rect.bottom;
|
| 23 |
+
const spaceAbove = rect.top;
|
| 24 |
+
const dropH = 240; // max-height of dropdown
|
| 25 |
+
if (spaceBelow < dropH && spaceAbove > spaceBelow) {
|
| 26 |
+
dropdown.classList.add('uf-select-dropdown-up');
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
dropdown.classList.remove('hidden');
|
| 30 |
+
if (arrow) arrow.classList.add('uf-select-arrow-open');
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function ufSelectPick(id, value, label) {
|
| 35 |
+
const hidden = document.getElementById(id);
|
| 36 |
+
const labelEl = document.getElementById(id + '-label');
|
| 37 |
+
const dropdown = document.getElementById(id + '-dropdown');
|
| 38 |
+
const arrow = document.getElementById(id + '-arrow');
|
| 39 |
+
|
| 40 |
+
if (hidden) hidden.value = value;
|
| 41 |
+
if (labelEl) {
|
| 42 |
+
labelEl.textContent = label;
|
| 43 |
+
labelEl.style.color = '#ffffff'; // selected = white, placeholder was grey
|
| 44 |
+
}
|
| 45 |
+
if (dropdown) dropdown.classList.add('hidden');
|
| 46 |
+
if (arrow) arrow.classList.remove('uf-select-arrow-open');
|
| 47 |
+
|
| 48 |
+
// Mark active option
|
| 49 |
+
if (dropdown) {
|
| 50 |
+
dropdown.querySelectorAll('.uf-select-option').forEach(opt => {
|
| 51 |
+
opt.classList.toggle('uf-select-option-active', opt.dataset.value === value);
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Fire side-effects by id
|
| 56 |
+
if (id === 'live-palette') applyPalette(value);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Close on outside click
|
| 60 |
+
document.addEventListener('click', function(e) {
|
| 61 |
+
if (!e.target.closest('.uf-select-wrap')) {
|
| 62 |
+
document.querySelectorAll('.uf-select-dropdown').forEach(d => d.classList.add('hidden'));
|
| 63 |
+
document.querySelectorAll('.uf-select-arrow').forEach(a => a.classList.remove('uf-select-arrow-open'));
|
| 64 |
+
}
|
| 65 |
+
});
|
| 66 |
|
| 67 |
+
// =========== Rolling Counter ===========
|
| 68 |
function animateValue(obj, start, end, duration) {
|
| 69 |
let startTimestamp = null;
|
| 70 |
const isPct = typeof end === 'string' && end.includes('%');
|
|
|
|
| 92 |
window.requestAnimationFrame(step);
|
| 93 |
}
|
| 94 |
|
| 95 |
+
// Close all open tooltips
|
| 96 |
+
function closeAllTips() {
|
| 97 |
+
document.querySelectorAll('.info-tip').forEach(tip => {
|
| 98 |
+
tip.style.display = 'none';
|
| 99 |
+
});
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function positionTip(tip, wrap) {
|
| 103 |
+
const rect = wrap.getBoundingClientRect();
|
| 104 |
+
const tipH = tip.offsetHeight || 60;
|
| 105 |
+
if (rect.bottom + tipH + 10 > window.innerHeight) {
|
| 106 |
+
tip.style.top = (rect.top - tipH - 6) + 'px';
|
| 107 |
+
} else {
|
| 108 |
+
tip.style.top = (rect.bottom + 6) + 'px';
|
| 109 |
+
}
|
| 110 |
+
tip.style.left = Math.min(rect.left, window.innerWidth - 260) + 'px';
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// Desktop hover only — never fires on touch
|
| 114 |
+
document.addEventListener('mouseover', e => {
|
| 115 |
+
if (window.matchMedia('(hover: none)').matches) return;
|
| 116 |
+
const wrap = e.target.closest('.info-wrap');
|
| 117 |
+
if (!wrap) return;
|
| 118 |
+
const tip = wrap.querySelector('.info-tip');
|
| 119 |
+
if (!tip) return;
|
| 120 |
+
tip.style.display = 'block';
|
| 121 |
+
positionTip(tip, wrap);
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
document.addEventListener('mouseout', e => {
|
| 125 |
+
if (window.matchMedia('(hover: none)').matches) return;
|
| 126 |
+
const wrap = e.target.closest('.info-wrap');
|
| 127 |
+
if (!wrap) return;
|
| 128 |
+
const tip = wrap.querySelector('.info-tip');
|
| 129 |
+
if (tip) tip.style.display = 'none';
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
// Tap/click toggle — works on both desktop and mobile
|
| 133 |
+
document.addEventListener('click', e => {
|
| 134 |
+
const btn = e.target.closest('.info-btn');
|
| 135 |
+
if (!btn) {
|
| 136 |
+
if (!e.target.closest('.info-tip') && !e.target.closest('.info-wrap')) {
|
| 137 |
+
closeAllTips();
|
| 138 |
+
}
|
| 139 |
+
return;
|
| 140 |
+
}
|
| 141 |
+
e.stopPropagation();
|
| 142 |
+
const wrap = btn.closest('.info-wrap');
|
| 143 |
+
if (!wrap) return;
|
| 144 |
+
const tip = wrap.querySelector('.info-tip');
|
| 145 |
+
if (!tip) return;
|
| 146 |
+
|
| 147 |
+
const isVisible = tip.style.display === 'block';
|
| 148 |
+
closeAllTips();
|
| 149 |
+
if (!isVisible) {
|
| 150 |
+
tip.style.display = 'block';
|
| 151 |
+
positionTip(tip, wrap);
|
| 152 |
+
}
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
// ---- Tab switching — updates both sidebar + mobile bottom nav ----
|
| 156 |
+
function switchTab(tab) {
|
| 157 |
+
const allTabs = ['about', 'overview', 'run-details', 'reports', 'settings', 'help', 'feedback'];
|
| 158 |
+
|
| 159 |
+
allTabs.forEach(t => {
|
| 160 |
+
// Content panels
|
| 161 |
+
const el = document.getElementById('tab-' + t);
|
| 162 |
+
if (el) el.classList.toggle('hidden', tab !== t);
|
| 163 |
+
|
| 164 |
+
// Desktop sidebar nav items
|
| 165 |
+
const nav = document.getElementById('nav-' + t);
|
| 166 |
+
if (nav) {
|
| 167 |
+
if (tab === t) {
|
| 168 |
+
nav.classList.add('nav-item-active');
|
| 169 |
+
nav.classList.remove('nav-item-inactive');
|
| 170 |
} else {
|
| 171 |
+
nav.classList.remove('nav-item-active');
|
| 172 |
+
nav.classList.add('nav-item-inactive');
|
| 173 |
}
|
| 174 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
+
// Mobile bottom nav items
|
| 177 |
+
const mobNav = document.getElementById('mob-nav-' + t);
|
| 178 |
+
if (mobNav) {
|
| 179 |
+
mobNav.classList.toggle('active', tab === t);
|
| 180 |
+
if (tab === 'reports' && t === 'reports') {
|
| 181 |
+
mobNav.classList.remove('notify-glow');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
+
});
|
| 185 |
+
}
|
| 186 |
|
| 187 |
// =========== Toast System ===========
|
| 188 |
function showToast(message, type) {
|
|
|
|
| 334 |
const panel = document.getElementById('insights-panel');
|
| 335 |
panel.classList.remove('hidden');
|
| 336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
// Congestion insights
|
| 338 |
const ci = document.getElementById('congestion-insights');
|
| 339 |
const pcu = d.pcu || {};
|
| 340 |
ci.innerHTML = [
|
| 341 |
infoRow('Total PCU', pcu.total_pcu || 0, 'Passenger Car Units (IRC:106-1990). Normalizes mixed traffic.'),
|
| 342 |
infoRow('PCU In / Out', `${pcu.pcu_in || 0} / ${pcu.pcu_out || 0}`, 'Directional PCU split.'),
|
|
|
|
| 343 |
].join('');
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
// PCU card in Stats
|
| 346 |
const pcuCard = document.getElementById('pcu-stats-card');
|
| 347 |
if (pcuCard) {
|
| 348 |
+
const totalPcu = pcu.total_pcu || 0;
|
| 349 |
+
|
| 350 |
+
// Capacity label based on total PCU volume
|
| 351 |
+
let capLabel, capColor, capNote;
|
| 352 |
+
if (totalPcu < 50) {
|
| 353 |
+
capLabel = 'Very Low Volume'; capColor = '#22c55e';
|
| 354 |
+
capNote = 'Minimal traffic on this segment';
|
| 355 |
+
} else if (totalPcu < 200) {
|
| 356 |
+
capLabel = 'Low Volume'; capColor = '#22c55e';
|
| 357 |
+
capNote = 'Below typical urban arterial demand';
|
| 358 |
+
} else if (totalPcu < 500) {
|
| 359 |
+
capLabel = 'Moderate Volume'; capColor = '#c89a6c';
|
| 360 |
+
capNote = 'Typical mixed urban traffic';
|
| 361 |
+
} else if (totalPcu < 900) {
|
| 362 |
+
capLabel = 'High Volume'; capColor = '#f97316';
|
| 363 |
+
capNote = 'Approaching single-lane capacity (IRC:106)';
|
| 364 |
+
} else {
|
| 365 |
+
capLabel = 'Near/Over Capacity'; capColor = '#ef4444';
|
| 366 |
+
capNote = 'Exceeds IRC single-lane urban reference (1000 PCU)';
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// Dominant class from per_class breakdown
|
| 370 |
+
let domClass = '—', domFactor = '—';
|
| 371 |
+
if (pcu.per_class && Object.keys(pcu.per_class).length > 0) {
|
| 372 |
+
const sorted = Object.entries(pcu.per_class)
|
| 373 |
+
.sort((a, b) => b[1].pcu - a[1].pcu);
|
| 374 |
+
domClass = sorted[0][0];
|
| 375 |
+
domFactor = sorted[0][1].factor;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
pcuCard.innerHTML = `
|
| 379 |
+
<div class="w-full space-y-3">
|
| 380 |
+
<div>
|
| 381 |
+
<span class="text-base font-black uppercase tracking-wide"
|
| 382 |
+
style="color:${capColor}">${capLabel}</span>
|
| 383 |
+
<p class="text-[10px] mt-0.5" style="color:#a89f97">${capNote}</p>
|
| 384 |
+
<p class="text-[10px] mt-1" style="color:#666">
|
| 385 |
+
Dominant: ${domClass} · PCU factor ${domFactor}
|
| 386 |
+
</p>
|
| 387 |
</div>
|
| 388 |
+
<div class="flex justify-between items-center pt-1">
|
| 389 |
+
<span class="text-xs font-medium text-slate-500">Total PCU</span>
|
| 390 |
+
<span class="text-2xl font-black" style="color:#8b5e3c">${totalPcu}</span>
|
| 391 |
</div>
|
| 392 |
+
<div class="flex gap-3">
|
| 393 |
+
<div class="flex-1 bg-green-950 rounded-lg p-2.5 text-center border border-green-900">
|
| 394 |
+
<div class="text-lg font-bold text-green-400">${pcu.pcu_in || 0}</div>
|
| 395 |
+
<div class="text-[9px] font-bold text-green-600 uppercase">PCU In</div>
|
| 396 |
+
</div>
|
| 397 |
+
<div class="flex-1 bg-red-950 rounded-lg p-2.5 text-center border border-red-900">
|
| 398 |
+
<div class="text-lg font-bold text-red-400">${pcu.pcu_out || 0}</div>
|
| 399 |
+
<div class="text-[9px] font-bold text-red-600 uppercase">PCU Out</div>
|
| 400 |
+
</div>
|
| 401 |
+
</div>
|
| 402 |
+
</div>`;
|
| 403 |
}
|
| 404 |
}
|
| 405 |
|
|
|
|
| 432 |
const res = c.resolution || [0, 0];
|
| 433 |
|
| 434 |
document.getElementById('panel-video').innerHTML =
|
| 435 |
+
detailRow('video_fps', (c.video_fps || 0).toFixed(2)) +
|
| 436 |
detailRow('frames', c.frames) +
|
| 437 |
+
detailRow('duration', (c.duration || 0).toFixed(2) + ' sec') +
|
| 438 |
detailRow('resolution', res[0] + ' <span class="text-slate-400 text-xs">x</span> ' + res[1]) +
|
| 439 |
detailRow('pixels', (c.pixels || 0).toLocaleString());
|
| 440 |
|
|
|
|
| 490 |
gold: { congestion: '#fbbf24', congestionBg: 'rgba(251,191,36,0.08)', dominance: '#a8a29e', flow: '#f5f5f4', doughIn: '#f5f5f4', doughOut: '#fbbf24' }
|
| 491 |
};
|
| 492 |
|
| 493 |
+
// =========== Global State (Initialized in initApp) ===========
|
| 494 |
+
let currentPalette = 'default';
|
| 495 |
+
let activePalette = PALETTES.default;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
let MODEL_CLASSES = {};
|
| 497 |
let BUSINESS_MAP = {};
|
| 498 |
+
let congChart, doughChart, domChart, flowChart;
|
| 499 |
+
|
| 500 |
+
async function initApp() {
|
| 501 |
+
// Read settings from sessionStorage
|
| 502 |
+
const rawRun = sessionStorage.getItem('funky_run');
|
| 503 |
+
const runSettings = rawRun ? (JSON.parse(rawRun).settings || {}) : {};
|
| 504 |
+
currentPalette = runSettings.palette || 'default';
|
| 505 |
+
activePalette = PALETTES[currentPalette];
|
| 506 |
+
|
| 507 |
+
// =========== Charts ===========
|
| 508 |
+
Chart.defaults.font.family = "'Montserrat', sans-serif";
|
| 509 |
+
Chart.defaults.color = '#888888';
|
| 510 |
+
Chart.defaults.borderColor = '#222222';
|
| 511 |
+
Chart.defaults.plugins.tooltip.backgroundColor = '#0a0a0a';
|
| 512 |
+
Chart.defaults.plugins.tooltip.titleColor = '#ffffff';
|
| 513 |
+
Chart.defaults.plugins.tooltip.bodyColor = '#aaaaaa';
|
| 514 |
+
Chart.defaults.plugins.tooltip.borderColor = '#222222';
|
| 515 |
+
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
| 516 |
+
|
| 517 |
+
congChart = new Chart(document.getElementById('congestionChart').getContext('2d'), {
|
| 518 |
+
type: 'line',
|
| 519 |
+
data: {
|
| 520 |
+
labels: [], datasets: [{
|
| 521 |
+
data: [],
|
| 522 |
+
borderColor: activePalette.congestion,
|
| 523 |
+
backgroundColor: activePalette.congestionBg,
|
| 524 |
+
fill: true,
|
| 525 |
+
tension: 0.2,
|
| 526 |
+
borderWidth: 1.5,
|
| 527 |
+
pointRadius: 0
|
| 528 |
+
}]
|
| 529 |
},
|
| 530 |
+
options: {
|
| 531 |
+
responsive: true, maintainAspectRatio: false,
|
| 532 |
+
plugins: { legend: { display: false } },
|
| 533 |
+
scales: {
|
| 534 |
+
x: { grid: { display: false }, ticks: { maxTicksLimit: 10, font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Frame Index', font: { size: 10, weight: '700' }, color: '#888888' } },
|
| 535 |
+
y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Active Vehicles', font: { size: 10, weight: '700' }, color: '#888888' } }
|
| 536 |
+
},
|
| 537 |
+
animation: { duration: 0 }
|
| 538 |
+
},
|
| 539 |
+
plugins: []
|
| 540 |
+
});
|
| 541 |
|
| 542 |
+
doughChart = new Chart(document.getElementById('doughnutChart').getContext('2d'), {
|
| 543 |
+
type: 'doughnut',
|
| 544 |
+
data: {
|
| 545 |
+
labels: ['Incoming', 'Outgoing'], datasets: [{
|
| 546 |
+
data: [0, 0],
|
| 547 |
+
backgroundColor: [activePalette.doughIn, activePalette.doughOut],
|
| 548 |
+
borderColor: '#0a0a0a',
|
| 549 |
+
borderWidth: 3,
|
| 550 |
+
hoverOffset: 6
|
| 551 |
+
}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
},
|
| 553 |
+
options: {
|
| 554 |
+
responsive: true, maintainAspectRatio: false,
|
| 555 |
+
cutout: '68%',
|
| 556 |
+
plugins: {
|
| 557 |
+
legend: { display: true, position: 'bottom', labels: { padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 10, weight: '600' } } }
|
| 558 |
+
},
|
| 559 |
+
animation: { duration: 0 }
|
| 560 |
+
},
|
| 561 |
+
plugins: []
|
| 562 |
+
});
|
| 563 |
|
| 564 |
+
domChart = new Chart(document.getElementById('dominanceChart').getContext('2d'), {
|
| 565 |
+
type: 'bar',
|
| 566 |
+
data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.dominance, borderRadius: 2 }] },
|
| 567 |
+
options: {
|
| 568 |
+
responsive: true, maintainAspectRatio: false,
|
| 569 |
+
layout: { padding: { bottom: 12 } },
|
| 570 |
+
plugins: { legend: { display: false } },
|
| 571 |
+
scales: {
|
| 572 |
+
x: { grid: { display: false }, ticks: { font: { size: 9, weight: '500' }, color: '#666666', maxRotation: 45, minRotation: 30, autoSkip: false } },
|
| 573 |
+
y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Total Vehicle Count', font: { size: 10, weight: '700' }, color: '#888888' } }
|
| 574 |
+
},
|
| 575 |
+
animation: { duration: 0 }
|
| 576 |
},
|
| 577 |
+
plugins: []
|
| 578 |
+
});
|
|
|
|
|
|
|
| 579 |
|
| 580 |
+
flowChart = new Chart(document.getElementById('flowChart').getContext('2d'), {
|
| 581 |
+
type: 'bar',
|
| 582 |
+
data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.flow, borderColor: '#0a0a0a', borderWidth: 1.5, barPercentage: 1.0, categoryPercentage: 1.0 }] },
|
| 583 |
+
options: {
|
| 584 |
+
responsive: true, maintainAspectRatio: false,
|
| 585 |
+
plugins: { legend: { display: false } },
|
| 586 |
+
scales: {
|
| 587 |
+
x: { grid: { display: false }, ticks: { maxTicksLimit: 10, font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Time (seconds)', font: { size: 10, weight: '700' }, color: '#888888' } },
|
| 588 |
+
y: { grid: { color: '#1a1a1a' }, beginAtZero: true, ticks: { font: { size: 10 }, color: '#666666' }, title: { display: true, text: 'Vehicles Crossed', font: { size: 10, weight: '700' }, color: '#888888' } }
|
| 589 |
+
},
|
| 590 |
+
animation: { duration: 0 }
|
| 591 |
},
|
| 592 |
+
plugins: []
|
| 593 |
+
});
|
| 594 |
+
|
| 595 |
+
// Original init() logic
|
| 596 |
+
const raw = sessionStorage.getItem('funky_run');
|
| 597 |
+
if (!raw) { window.location.href = './'; return; }
|
| 598 |
+
|
| 599 |
+
_params = JSON.parse(raw);
|
| 600 |
+
sessionStorage.removeItem('funky_run');
|
| 601 |
+
|
| 602 |
+
const cRes = await fetch('constants');
|
| 603 |
+
const cData = await cRes.json();
|
| 604 |
+
MODEL_CLASSES = cData.classes;
|
| 605 |
+
BUSINESS_MAP = cData.business_map;
|
| 606 |
+
|
| 607 |
+
populateAndInit(_params);
|
| 608 |
+
sessionStorage.removeItem('funky_run');
|
| 609 |
+
|
| 610 |
+
// Sync current active tab from session if set
|
| 611 |
+
const activeTab = sessionStorage.getItem('uf_active_tab') || 'settings';
|
| 612 |
+
switchTab(activeTab);
|
| 613 |
+
}
|
| 614 |
|
| 615 |
// =========== Update functions ===========
|
| 616 |
function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
|
|
|
|
| 709 |
});
|
| 710 |
const reportRow = document.getElementById('sv-report');
|
| 711 |
if (reportRow) reportRow.closest('.s-row').classList.add('disabled');
|
| 712 |
+
// Also directly hide the wrap and close dropdown — CSS may be cached
|
| 713 |
+
const svWrap = document.getElementById('sv-report-wrap');
|
| 714 |
+
if (svWrap) {
|
| 715 |
+
svWrap.style.pointerEvents = 'none';
|
| 716 |
+
svWrap.style.opacity = '0.4';
|
| 717 |
+
const dd = document.getElementById('sv-report-dropdown');
|
| 718 |
+
if (dd) dd.classList.add('hidden');
|
| 719 |
+
}
|
| 720 |
const annotatedRow = document.getElementById('sv-annotated');
|
| 721 |
if (annotatedRow) annotatedRow.closest('.s-row').classList.add('disabled');
|
| 722 |
const wrap = document.getElementById('settings-start-wrap');
|
|
|
|
| 824 |
// =========== Main ===========
|
| 825 |
let _params = null;
|
| 826 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
|
| 828 |
function populateAndInit(params) {
|
| 829 |
populateRunDetails(params.config);
|
|
|
|
| 871 |
document.getElementById('proc-label').innerText = 'Processing';
|
| 872 |
|
| 873 |
// Reset Run Tab Results to Awaiting
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
document.getElementById('run-results-content').innerHTML = `
|
| 876 |
<div class="flex flex-col items-center justify-center p-8 bg-black/40 border border-slate-800 rounded-2xl col-span-3 text-slate-500">
|
| 877 |
<i class="fa-solid fa-spinner fa-spin text-2xl mb-3 text-white"></i>
|
|
|
|
| 882 |
const repIcon = document.getElementById('reports-pending-icon');
|
| 883 |
if (repIcon) repIcon.className = 'fa-solid fa-circle-notch fa-spin text-[#c89a6c]';
|
| 884 |
const repText = document.getElementById('reports-pending-text');
|
| 885 |
+
if (repText) repText.innerText = 'Artifacts will be available after processing completes... Please wait';
|
| 886 |
|
| 887 |
|
| 888 |
// Start WebSocket
|
| 889 |
const videoDuration = _params.config.duration || 10;
|
| 890 |
|
| 891 |
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
| 892 |
+
// Get the directory path (e.g., /app/ or /) rather than the full filename
|
| 893 |
+
const dirPath = location.pathname.substring(0, location.pathname.lastIndexOf('/') + 1);
|
| 894 |
+
const ws = new WebSocket(`${proto}://${location.host}${dirPath}ws/run`);
|
| 895 |
|
| 896 |
ws.onopen = () => {
|
| 897 |
ws.send(JSON.stringify({
|
|
|
|
| 910 |
console.error('WS Error:', e);
|
| 911 |
document.getElementById('proc-label').innerText = 'Connection Error';
|
| 912 |
showToast('Connection error — server may be busy', 'error');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 913 |
};
|
| 914 |
|
| 915 |
let processingDone = false;
|
|
|
|
| 919 |
if (!processingDone) {
|
| 920 |
// Closed before done=True received — show error state
|
| 921 |
document.getElementById('proc-label').innerText = 'Disconnected';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 922 |
document.getElementById('run-results-content').innerHTML = `
|
| 923 |
<div class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-red-900/40 rounded-2xl col-span-3 text-slate-400">
|
| 924 |
<i class="fa-solid fa-triangle-exclamation text-2xl mb-3 text-red-400"></i>
|
|
|
|
| 932 |
};
|
| 933 |
|
| 934 |
let lastUIUpdate = 0;
|
| 935 |
+
let liveCongestion = [];
|
| 936 |
+
let liveFlowTimes = [];
|
| 937 |
|
| 938 |
ws.onmessage = e => {
|
| 939 |
const d = JSON.parse(e.data);
|
|
|
|
| 944 |
if (d.error) {
|
| 945 |
processingDone = true;
|
| 946 |
document.getElementById('proc-label').innerText = 'Engine Error';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 947 |
console.error('[UrbanFlow] Engine error:', d.detail || d.error);
|
| 948 |
document.getElementById('run-results-content').innerHTML = `
|
| 949 |
<div class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-red-900/40 rounded-2xl col-span-3 text-slate-400">
|
|
|
|
| 972 |
}
|
| 973 |
}
|
| 974 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 975 |
|
| 976 |
+
// GLOW NOTIFICATION: Let the user know artifacts are ready
|
| 977 |
+
const reportsMob = document.getElementById('mob-nav-reports');
|
| 978 |
+
if (reportsMob) {
|
| 979 |
+
reportsMob.classList.add('notify-glow');
|
| 980 |
}
|
| 981 |
|
| 982 |
+
|
| 983 |
document.getElementById('run-results-content').innerHTML =
|
| 984 |
+
detailRow('Inference Time', (d.processing_time || 0).toFixed(2) + ' sec') +
|
| 985 |
+
infoRow('Throughput (FPS)', (d.actual_fps || 0).toFixed(2), 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
|
| 986 |
+
infoRow('Real-time Ratio', (d.speed_vs_realtime || 0).toFixed(2) + 'x', 'Processing speed relative to video playback rate.');
|
| 987 |
|
| 988 |
if (d.video_id) {
|
| 989 |
loadReports(d.video_id).then(data => {
|
|
|
|
| 1009 |
if (jsonToggle) {
|
| 1010 |
jsonToggle.closest('.s-row').classList.add('disabled');
|
| 1011 |
}
|
| 1012 |
+
|
| 1013 |
+
// NOTIFY USER: Glow the artifacts (reports) icon in mobile nav
|
| 1014 |
+
const reportsNav = document.getElementById('mob-nav-reports');
|
| 1015 |
+
if (reportsNav) {
|
| 1016 |
+
reportsNav.classList.add('notify-glow');
|
| 1017 |
+
}
|
| 1018 |
const csvToggle = document.getElementById('sv-export-csv');
|
| 1019 |
if (csvToggle) {
|
| 1020 |
csvToggle.closest('.s-row').classList.add('disabled');
|
|
|
|
| 1027 |
// Toast + Insights
|
| 1028 |
showToast('Processing complete — artifacts ready', 'success');
|
| 1029 |
renderInsights(d);
|
| 1030 |
+
showRetryBubble();
|
| 1031 |
|
| 1032 |
// Store video_id for keyboard shortcut download
|
| 1033 |
document.body.setAttribute('data-last-video-id', d.video_id);
|
|
|
|
| 1060 |
doughChart.data.datasets[0].data = [totalIn, totalOut];
|
| 1061 |
doughChart.update();
|
| 1062 |
|
| 1063 |
+
// Live history management (Backend sends incremental updates now)
|
| 1064 |
+
if (d.congestion_last !== undefined) {
|
| 1065 |
+
liveCongestion.push(d.congestion_last);
|
| 1066 |
+
}
|
| 1067 |
+
if (d.flow_count !== undefined) {
|
| 1068 |
+
// Approximate timestamps for new crossings based on current frame
|
| 1069 |
+
const videoFps = _params.config.video_fps || 30;
|
| 1070 |
+
while (liveFlowTimes.length < d.flow_count) {
|
| 1071 |
+
liveFlowTimes.push(d.frame_index * stride / videoFps);
|
| 1072 |
+
}
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
const now = performance.now();
|
| 1076 |
if (now - lastUIUpdate < 300) return;
|
| 1077 |
lastUIUpdate = now;
|
| 1078 |
|
| 1079 |
+
updateCongestion(liveCongestion, stride);
|
| 1080 |
updateBreakdown(d.class_in, d.class_out);
|
| 1081 |
updateDominance(d.class_in, d.class_out);
|
| 1082 |
+
buildFlowHistogram(liveFlowTimes, videoDuration);
|
| 1083 |
};
|
| 1084 |
}
|
| 1085 |
|
|
|
|
| 1106 |
const data = await res.json();
|
| 1107 |
if (!data.files || !data.files.length) return null;
|
| 1108 |
|
| 1109 |
+
const rPending = document.getElementById('reports-pending');
|
| 1110 |
+
if (rPending) rPending.classList.add('hidden');
|
| 1111 |
+
const rMsg = document.getElementById('reports-pending-message');
|
| 1112 |
+
if (rMsg) rMsg.classList.add('hidden');
|
| 1113 |
document.getElementById('post-process-cards').classList.remove('hidden');
|
| 1114 |
const grid = document.getElementById('reports-grid');
|
| 1115 |
grid.classList.remove('hidden');
|
|
|
|
| 1210 |
}
|
| 1211 |
}
|
| 1212 |
|
| 1213 |
+
function showRetryBubble() {
|
| 1214 |
+
if (sessionStorage.getItem('uf_retry_shown')) return;
|
| 1215 |
+
sessionStorage.setItem('uf_retry_shown', '1');
|
| 1216 |
+
|
| 1217 |
+
const existing = document.getElementById('retry-bubble');
|
| 1218 |
+
if (existing) existing.remove();
|
| 1219 |
+
|
| 1220 |
+
const isMobile = window.innerWidth < 1024;
|
| 1221 |
+
const bubble = document.createElement('div');
|
| 1222 |
+
bubble.id = 'retry-bubble';
|
| 1223 |
+
|
| 1224 |
+
Object.assign(bubble.style, {
|
| 1225 |
+
position: 'fixed',
|
| 1226 |
+
background: '#111111',
|
| 1227 |
+
border: '1px solid #c89a6c',
|
| 1228 |
+
borderRadius: '12px',
|
| 1229 |
+
padding: '12px 16px',
|
| 1230 |
+
fontFamily: 'Montserrat, sans-serif',
|
| 1231 |
+
color: '#f0ece6',
|
| 1232 |
+
zIndex: '9000',
|
| 1233 |
+
boxShadow: '0 8px 32px rgba(0,0,0,0.8)',
|
| 1234 |
+
maxWidth: '220px',
|
| 1235 |
+
textAlign: 'center',
|
| 1236 |
+
lineHeight: '1.4',
|
| 1237 |
+
});
|
| 1238 |
+
|
| 1239 |
+
bubble.innerHTML = `
|
| 1240 |
+
<p style="color:#c89a6c;font-weight:800;font-size:13px;margin:0 0 5px 0">
|
| 1241 |
+
Want to try another video?
|
| 1242 |
+
</p>
|
| 1243 |
+
<p style="color:#a89f97;font-size:11px;margin:0">
|
| 1244 |
+
Tap <b style="color:#f0ece6">⚙ Settings</b> to reset & re-run
|
| 1245 |
+
</p>
|
| 1246 |
+
<div id="retry-bubble-arrow"></div>
|
| 1247 |
+
`;
|
| 1248 |
+
|
| 1249 |
+
// Append to fixed portal so overflow:hidden on body/main never clips it
|
| 1250 |
+
const portal = document.getElementById('fixed-portal') || document.body;
|
| 1251 |
+
portal.style.pointerEvents = 'none';
|
| 1252 |
+
bubble.style.pointerEvents = 'auto';
|
| 1253 |
+
portal.appendChild(bubble);
|
| 1254 |
+
|
| 1255 |
+
if (isMobile) {
|
| 1256 |
+
// 7 icons in bottom nav. Settings = 5th (index 4).
|
| 1257 |
+
// Center of 5th icon = (4 + 0.5) / 7 = 64.28% of viewport width.
|
| 1258 |
+
const navH = parseInt(
|
| 1259 |
+
getComputedStyle(document.documentElement)
|
| 1260 |
+
.getPropertyValue('--mob-nav-h') || '68', 10
|
| 1261 |
+
);
|
| 1262 |
+
const vpW = window.innerWidth;
|
| 1263 |
+
const settingsCenterX = (4.5 / 7) * vpW;
|
| 1264 |
+
const bubbleW = bubble.offsetWidth || 220;
|
| 1265 |
+
const leftPx = Math.max(8, Math.min(settingsCenterX - bubbleW / 2, vpW - bubbleW - 8));
|
| 1266 |
+
|
| 1267 |
+
Object.assign(bubble.style, {
|
| 1268 |
+
bottom: (navH + 10) + 'px',
|
| 1269 |
+
left: leftPx + 'px',
|
| 1270 |
+
top: 'auto',
|
| 1271 |
+
right: 'auto',
|
| 1272 |
+
transform: 'none',
|
| 1273 |
+
});
|
| 1274 |
+
|
| 1275 |
+
const arrowLeft = settingsCenterX - leftPx;
|
| 1276 |
+
const arrow = bubble.querySelector('#retry-bubble-arrow');
|
| 1277 |
+
Object.assign(arrow.style, {
|
| 1278 |
+
position: 'absolute',
|
| 1279 |
+
bottom: '-8px',
|
| 1280 |
+
left: arrowLeft + 'px',
|
| 1281 |
+
transform: 'translateX(-50%)',
|
| 1282 |
+
width: '0', height: '0',
|
| 1283 |
+
borderLeft: '8px solid transparent',
|
| 1284 |
+
borderRight: '8px solid transparent',
|
| 1285 |
+
borderTop: '8px solid #c89a6c',
|
| 1286 |
+
});
|
| 1287 |
+
|
| 1288 |
+
} else {
|
| 1289 |
+
// Desktop sidebar <aside class="w-60"> = 240px.
|
| 1290 |
+
// Settings is 5th nav link: logo(112px) + 4 items × 44px + 22px = 310px from top.
|
| 1291 |
+
const settingsY = 310;
|
| 1292 |
+
Object.assign(bubble.style, {
|
| 1293 |
+
left: '256px',
|
| 1294 |
+
top: (settingsY - 40) + 'px',
|
| 1295 |
+
bottom: 'auto',
|
| 1296 |
+
right: 'auto',
|
| 1297 |
+
transform: 'none',
|
| 1298 |
+
textAlign: 'left',
|
| 1299 |
+
});
|
| 1300 |
+
|
| 1301 |
+
const arrow = bubble.querySelector('#retry-bubble-arrow');
|
| 1302 |
+
Object.assign(arrow.style, {
|
| 1303 |
+
position: 'absolute',
|
| 1304 |
+
left: '-8px',
|
| 1305 |
+
top: '50%',
|
| 1306 |
+
transform: 'translateY(-50%)',
|
| 1307 |
+
width: '0', height: '0',
|
| 1308 |
+
borderTop: '8px solid transparent',
|
| 1309 |
+
borderBottom: '8px solid transparent',
|
| 1310 |
+
borderRight: '8px solid #c89a6c',
|
| 1311 |
+
});
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
bubble.addEventListener('click', () => bubble.remove());
|
| 1315 |
+
setTimeout(() => {
|
| 1316 |
+
const el = document.getElementById('retry-bubble');
|
| 1317 |
+
if (el) el.remove();
|
| 1318 |
+
}, 6000);
|
| 1319 |
+
}
|
| 1320 |
+
|
| 1321 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1322 |
+
// Phase 1: instant visual — show the shell immediately on first paint
|
| 1323 |
+
const activeTab = sessionStorage.getItem('uf_active_tab') || 'settings';
|
| 1324 |
+
switchTab(activeTab);
|
| 1325 |
+
|
| 1326 |
+
// Phase 2: defer all heavy init until after browser completes first paint
|
| 1327 |
+
requestAnimationFrame(() => {
|
| 1328 |
+
setTimeout(initApp, 0);
|
| 1329 |
+
});
|
| 1330 |
+
});
|
frontend/manifest.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "UrbanFlow: Your Vision Partner",
|
| 3 |
+
"short_name": "UrbanFlow",
|
| 4 |
+
"description": "Premium Traffic Intelligence Application",
|
| 5 |
+
"start_url": "/?pwa=true",
|
| 6 |
+
"display": "standalone",
|
| 7 |
+
"background_color": "#000000",
|
| 8 |
+
"theme_color": "#000000",
|
| 9 |
+
"orientation": "portrait",
|
| 10 |
+
"icons": [
|
| 11 |
+
{
|
| 12 |
+
"src": "assets/shuriken.png",
|
| 13 |
+
"sizes": "512x512",
|
| 14 |
+
"type": "image/png",
|
| 15 |
+
"purpose": "any"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"src": "assets/shurkien_b.png",
|
| 19 |
+
"sizes": "512x512",
|
| 20 |
+
"type": "image/png",
|
| 21 |
+
"purpose": "maskable"
|
| 22 |
+
}
|
| 23 |
+
]
|
| 24 |
+
}
|
frontend/sw.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CACHE_NAME = 'urbanflow-v4';
|
| 2 |
+
const ASSETS = [
|
| 3 |
+
'./css/initial.css',
|
| 4 |
+
'./css/vehicles.css',
|
| 5 |
+
'./assets/shuriken.png',
|
| 6 |
+
'./assets/shurkien_b.png',
|
| 7 |
+
'./assets/uf_rf.png'
|
| 8 |
+
];
|
| 9 |
+
|
| 10 |
+
self.addEventListener('install', (e) => {
|
| 11 |
+
e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(ASSETS)));
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
self.addEventListener('fetch', (e) => {
|
| 15 |
+
const url = new URL(e.request.url);
|
| 16 |
+
|
| 17 |
+
// NEVER cache WebSockets or API calls
|
| 18 |
+
if (url.pathname.includes('/ws/') || url.pathname.includes('/reports/') || url.pathname.includes('/bundle/')) {
|
| 19 |
+
return;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
e.respondWith(
|
| 23 |
+
fetch(e.request).catch(() => caches.match(e.request))
|
| 24 |
+
);
|
| 25 |
+
});
|
frontend/vehicles.html
CHANGED
|
@@ -3,9 +3,15 @@
|
|
| 3 |
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
<title>UrbanFlow</title>
|
| 8 |
<link rel="icon" type="image/png" href="assets/rf.png">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 11 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
@@ -16,7 +22,27 @@
|
|
| 16 |
<link rel="stylesheet" href="css/vehicles.css">
|
| 17 |
</head>
|
| 18 |
|
| 19 |
-
<body class="bg-black text-white h-screen w-screen overflow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
<!-- Sidebar -->
|
| 22 |
<aside class="w-60 bg-white shadow-xl flex flex-col z-20 flex-shrink-0 border-r border-slate-200 relative">
|
|
@@ -96,7 +122,8 @@
|
|
| 96 |
|
| 97 |
<!-- TAB: About -->
|
| 98 |
<div id="tab-about" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 99 |
-
|
|
|
|
| 100 |
<div class="text-center">
|
| 101 |
<p class="text-sm italic font-bold leading-relaxed max-w-3xl mx-auto"
|
| 102 |
style="color:#c89a6c;font-family:'Montserrat',sans-serif">
|
|
@@ -131,7 +158,8 @@
|
|
| 131 |
</p>
|
| 132 |
</div>
|
| 133 |
|
| 134 |
-
<div class="grid grid-cols-
|
|
|
|
| 135 |
<div class="space-y-4">
|
| 136 |
<h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">What You Get
|
| 137 |
</h4>
|
|
@@ -155,52 +183,17 @@
|
|
| 155 |
</ul>
|
| 156 |
</div>
|
| 157 |
<div class="space-y-4">
|
| 158 |
-
<h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">
|
| 159 |
-
|
| 160 |
-
<ul class="text-xs space-y-3 pl-1">
|
| 161 |
-
<li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
|
| 162 |
-
style="color:#c89a6c"></i>
|
| 163 |
-
<span>Upload footage from any existing camera — phone, CCTV, dashcam</span>
|
| 164 |
-
</li>
|
| 165 |
-
<li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
|
| 166 |
-
style="color:#c89a6c"></i>
|
| 167 |
-
<span>Draw a counting boundary on the first frame</span>
|
| 168 |
-
</li>
|
| 169 |
-
<li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
|
| 170 |
-
style="color:#c89a6c"></i>
|
| 171 |
-
<span>Inference runs in the cloud — no local GPU or setup needed</span>
|
| 172 |
-
</li>
|
| 173 |
-
<li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
|
| 174 |
-
style="color:#c89a6c"></i>
|
| 175 |
-
<span>Review live KPIs and download artifacts when complete</span>
|
| 176 |
-
</li>
|
| 177 |
-
</ul>
|
| 178 |
-
</div>
|
| 179 |
-
<div class="space-y-4">
|
| 180 |
-
<h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">What's Coming
|
| 181 |
</h4>
|
| 182 |
-
<
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
</li>
|
| 191 |
-
<li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
|
| 192 |
-
style="color:#8b5e3c"></i>
|
| 193 |
-
<span>Helmet compliance detection — rider safety enforcement</span>
|
| 194 |
-
</li>
|
| 195 |
-
<li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
|
| 196 |
-
style="color:#8b5e3c"></i>
|
| 197 |
-
<span>Traffic rule violation detection — signal jumping, wrong-way driving</span>
|
| 198 |
-
</li>
|
| 199 |
-
<li class="flex items-start gap-3"><i class="fa-solid fa-circle text-[5px] mt-1.5"
|
| 200 |
-
style="color:#8b5e3c"></i>
|
| 201 |
-
<span>Enterprise deployment with dedicated SLAs and onboarding</span>
|
| 202 |
-
</li>
|
| 203 |
-
</ul>
|
| 204 |
</div>
|
| 205 |
</div>
|
| 206 |
|
|
@@ -221,8 +214,8 @@
|
|
| 221 |
|
| 222 |
|
| 223 |
<!-- Progress Bar (shared) -->
|
| 224 |
-
<div
|
| 225 |
-
class="bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
|
| 226 |
<div class="flex items-center space-x-4 flex-1 mr-6">
|
| 227 |
<span class="text-[11px] font-black text-white uppercase tracking-wider whitespace-nowrap"
|
| 228 |
id="proc-label">Waiting</span>
|
|
@@ -234,18 +227,24 @@
|
|
| 234 |
<div class="flex items-center space-x-6 text-xs font-bold text-white whitespace-nowrap">
|
| 235 |
<span id="proc-frames">0 / 0 Frames</span>
|
| 236 |
<span id="proc-pct">0%</span>
|
| 237 |
-
<
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
<
|
| 242 |
-
<
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
</div>
|
| 250 |
</div>
|
| 251 |
|
|
@@ -320,7 +319,8 @@
|
|
| 320 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 321 |
<span class="info-tip">Aggregated vehicle count grouped by dominant category.</span></span>
|
| 322 |
</h3>
|
| 323 |
-
<span
|
|
|
|
| 324 |
Intelligence</span>
|
| 325 |
</div>
|
| 326 |
<div class="flex-1 w-full relative min-h-[220px]">
|
|
@@ -353,31 +353,24 @@
|
|
| 353 |
<div class="space-y-6 w-full max-w-[1400px] mx-auto">
|
| 354 |
|
| 355 |
<!-- HERO: Process Analytics -->
|
| 356 |
-
<div id="run-results-card"
|
| 357 |
-
|
| 358 |
-
<div class="px-6 py-4 border-b flex justify-between items-center
|
| 359 |
-
|
|
|
|
| 360 |
<h3 class="font-bold text-sm" style="color:#f0ece6">Process Analytics</h3>
|
| 361 |
-
<p
|
| 362 |
-
|
| 363 |
Execution Telemetry</p>
|
| 364 |
</div>
|
| 365 |
<div class="flex items-center gap-3">
|
| 366 |
-
<button id="run-analyze-again-btn" onclick="switchTab('settings')"
|
| 367 |
-
class="hidden px-4 py-1.5 bg-[#c89a6c] text-white text-[10px] font-bold rounded-full shadow-sm hover:bg-[#8b5e3c] transition-colors"><i
|
| 368 |
-
class="fa-solid fa-gear mr-1"></i> Configure & Analyze Again</button>
|
| 369 |
-
<div id="results-status-badge"
|
| 370 |
-
class="px-3 py-1.5 bg-amber-900/30 border border-amber-800/50 text-amber-500 text-[10px] font-bold rounded-full uppercase tracking-widest shadow-sm flex items-center gap-2">
|
| 371 |
-
<div class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></div>System Standby
|
| 372 |
-
</div>
|
| 373 |
</div>
|
| 374 |
</div>
|
| 375 |
<div class="p-8">
|
| 376 |
<div id="run-results-content" class="grid grid-cols-3 gap-12">
|
| 377 |
-
<div
|
| 378 |
-
|
| 379 |
-
<
|
| 380 |
-
<span class="text-xs font-semibold">Initiate a run to view performance insights</span>
|
| 381 |
</div>
|
| 382 |
</div>
|
| 383 |
</div>
|
|
@@ -385,25 +378,29 @@
|
|
| 385 |
|
| 386 |
<!-- Technical Context Row -->
|
| 387 |
<div class="grid grid-cols-2 gap-6">
|
| 388 |
-
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
|
|
|
| 389 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 390 |
<h3 class="font-bold text-sm" style="color:#f0ece6">Stream Source Profile</h3>
|
| 391 |
</div>
|
| 392 |
<div class="p-6 space-y-4" id="panel-video"></div>
|
| 393 |
</div>
|
| 394 |
-
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
|
|
|
| 395 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 396 |
<h3 class="font-bold text-sm" style="color:#f0ece6">System Resource Utilization</h3>
|
| 397 |
</div>
|
| 398 |
<div class="p-6 space-y-4" id="panel-perf"></div>
|
| 399 |
</div>
|
| 400 |
-
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
|
|
|
| 401 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 402 |
<h3 class="font-bold text-sm" style="color:#f0ece6">Model Architecture & Logic</h3>
|
| 403 |
</div>
|
| 404 |
<div class="p-6 space-y-4" id="panel-model"></div>
|
| 405 |
</div>
|
| 406 |
-
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
|
|
|
| 407 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 408 |
<h3 class="font-bold text-sm" style="color:#f0ece6">Inference Parameters</h3>
|
| 409 |
</div>
|
|
@@ -413,25 +410,12 @@
|
|
| 413 |
|
| 414 |
<!-- Insights Panel (xAI) -->
|
| 415 |
<div id="insights-panel" class="hidden">
|
| 416 |
-
<div class="
|
| 417 |
-
<div
|
| 418 |
-
|
| 419 |
-
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 420 |
-
<h3 class="font-bold text-sm flex items-center" style="color:#f0ece6">Speed Distribution
|
| 421 |
-
<span class="info-wrap"><span class="info-btn"><i
|
| 422 |
-
class="fa-solid fa-info"></i></span>
|
| 423 |
-
<span class="info-tip">Relative speed categories based on pixel displacement.
|
| 424 |
-
Slow/Normal/Fast are percentile-based within this video.</span></span>
|
| 425 |
-
</h3>
|
| 426 |
-
</div>
|
| 427 |
-
<div class="p-6" id="speed-dist-content">
|
| 428 |
-
<div class="flex gap-4 items-end justify-center h-32" id="speed-bars"></div>
|
| 429 |
-
</div>
|
| 430 |
-
</div>
|
| 431 |
-
<div
|
| 432 |
-
class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden" style="border-color:#2a2a2a">
|
| 433 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 434 |
-
<h3 class="font-bold text-sm flex items-center" style="color:#f0ece6">Congestion
|
|
|
|
| 435 |
<span class="info-wrap"><span class="info-btn"><i
|
| 436 |
class="fa-solid fa-info"></i></span>
|
| 437 |
<span class="info-tip">Automated peak detection and congestion
|
|
@@ -451,70 +435,16 @@
|
|
| 451 |
<!-- TAB: Reports -->
|
| 452 |
<div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 453 |
<div id="reports-pending-message"
|
| 454 |
-
class="mb-4 text-center p-
|
| 455 |
-
<i id="reports-pending-icon" class="fa-solid fa-layer-group text-[#c89a6c]"></i>
|
| 456 |
<span id="reports-pending-text"
|
| 457 |
-
class="text-xs text-[#a89f97] font-medium tracking-wide uppercase
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
<div id="reports-pending" class="grid grid-cols-2 xl:grid-cols-3 gap-4 w-full">
|
| 461 |
-
<!-- Skeleton Loader 1 -->
|
| 462 |
-
<div
|
| 463 |
-
class="bg-[#0a0a0a] rounded-xl border border-[#222] shadow-sm flex flex-col overflow-hidden animate-pulse">
|
| 464 |
-
<div class="flex flex-col items-center justify-center py-12">
|
| 465 |
-
<div class="w-16 h-16 bg-[#1a1a1a] rounded mb-4"></div>
|
| 466 |
-
<div class="w-24 h-2 bg-[#1a1a1a] rounded"></div>
|
| 467 |
-
</div>
|
| 468 |
-
<div class="px-5 py-4 border-t border-[#1a1a1a] bg-[#050505]">
|
| 469 |
-
<div class="w-3/4 h-3 bg-[#1a1a1a] rounded mb-2"></div>
|
| 470 |
-
<div class="w-1/2 h-2 bg-[#111] rounded"></div>
|
| 471 |
-
</div>
|
| 472 |
-
</div>
|
| 473 |
-
<!-- Skeleton Loader 2 -->
|
| 474 |
-
<div class="bg-[#0a0a0a] rounded-xl border border-[#222] shadow-sm flex flex-col overflow-hidden animate-pulse"
|
| 475 |
-
style="animation-delay: 150ms;">
|
| 476 |
-
<div class="flex flex-col items-center justify-center py-12">
|
| 477 |
-
<div class="w-16 h-16 bg-[#1a1a1a] rounded mb-4"></div>
|
| 478 |
-
<div class="w-24 h-2 bg-[#1a1a1a] rounded"></div>
|
| 479 |
-
</div>
|
| 480 |
-
<div class="px-5 py-4 border-t border-[#1a1a1a] bg-[#050505]">
|
| 481 |
-
<div class="w-2/3 h-3 bg-[#1a1a1a] rounded mb-2"></div>
|
| 482 |
-
<div class="w-1/3 h-2 bg-[#111] rounded"></div>
|
| 483 |
-
</div>
|
| 484 |
-
</div>
|
| 485 |
-
<!-- Skeleton Loader 3 -->
|
| 486 |
-
<div class="bg-[#0a0a0a] rounded-xl border border-[#222] shadow-sm flex flex-col overflow-hidden animate-pulse"
|
| 487 |
-
style="animation-delay: 300ms;">
|
| 488 |
-
<div class="flex flex-col items-center justify-center py-12">
|
| 489 |
-
<div class="w-16 h-16 bg-[#1a1a1a] rounded mb-4"></div>
|
| 490 |
-
<div class="w-24 h-2 bg-[#1a1a1a] rounded"></div>
|
| 491 |
-
</div>
|
| 492 |
-
<div class="px-5 py-4 border-t border-[#1a1a1a] bg-[#050505]">
|
| 493 |
-
<div class="w-4/5 h-3 bg-[#1a1a1a] rounded mb-2"></div>
|
| 494 |
-
<div class="w-1/2 h-2 bg-[#111] rounded"></div>
|
| 495 |
-
</div>
|
| 496 |
-
</div>
|
| 497 |
</div>
|
| 498 |
|
| 499 |
<!-- Post-Processing Insights (Populated after completion) -->
|
| 500 |
-
<div id="post-process-cards" class="hidden
|
| 501 |
-
<!-- Speed Distribution -->
|
| 502 |
-
<div class="bg-black rounded-xl p-6 border border-slate-800 shadow-sm flex flex-col min-h-[140px]">
|
| 503 |
-
<div class="flex justify-between items-center mb-4 relative">
|
| 504 |
-
<h3 class="font-bold text-white text-sm flex items-center">Speed Profile
|
| 505 |
-
<span class="info-wrap"><span class="info-btn" style="background:#222;color:#888"><i
|
| 506 |
-
class="fa-solid fa-info"></i></span>
|
| 507 |
-
<span class="info-tip">Relative speed categories based on pixel displacement between
|
| 508 |
-
frames. Slow/Normal/Fast are percentile-based within this video — not
|
| 509 |
-
km/h.</span></span>
|
| 510 |
-
</h3>
|
| 511 |
-
</div>
|
| 512 |
-
<div class="flex-1 flex items-center justify-center" id="speed-stats-card">
|
| 513 |
-
<div class="text-center text-slate-600 text-xs"><i
|
| 514 |
-
class="fa-solid fa-gauge-high text-2xl mb-2"></i><br>Available after processing</div>
|
| 515 |
-
</div>
|
| 516 |
-
</div>
|
| 517 |
-
|
| 518 |
<!-- PCU Summary -->
|
| 519 |
<div class="bg-black rounded-xl p-6 border border-slate-800 shadow-sm flex flex-col min-h-[140px]">
|
| 520 |
<div class="flex justify-between items-center mb-4 relative">
|
|
@@ -539,104 +469,138 @@
|
|
| 539 |
<div id="tab-settings" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 540 |
<div class="grid grid-cols-2 gap-6 w-full">
|
| 541 |
<!-- Processing Parameters -->
|
| 542 |
-
<div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
|
|
|
|
| 543 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 544 |
<h3 class="font-bold text-white text-sm flex items-center">Inference Configuration Profile
|
| 545 |
<span class="info-wrap ml-2">
|
| 546 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 547 |
-
|
|
|
|
|
|
|
| 548 |
</span>
|
| 549 |
</h3>
|
| 550 |
-
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
|
|
|
| 551 |
</div>
|
| 552 |
<div class="px-6 py-2 flex-1" id="settings-params">
|
| 553 |
<div class="s-row" data-param="imgsz">
|
| 554 |
<div>
|
| 555 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Image Size
|
| 556 |
<span class="info-wrap ml-1">
|
| 557 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 558 |
-
|
|
|
|
|
|
|
| 559 |
</span>
|
| 560 |
</div>
|
| 561 |
<div class="text-[10px] text-slate-500">Model input resolution</div>
|
| 562 |
</div>
|
| 563 |
-
<div class="s-stepper"><button onclick="stepParam('imgsz',-32)">‹</button><span
|
|
|
|
|
|
|
| 564 |
</div>
|
| 565 |
<div class="s-row" data-param="conf">
|
| 566 |
<div>
|
| 567 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Confidence
|
| 568 |
<span class="info-wrap ml-1">
|
| 569 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 570 |
-
|
|
|
|
|
|
|
| 571 |
</span>
|
| 572 |
</div>
|
| 573 |
<div class="text-[10px] text-slate-500">Minimum detection threshold</div>
|
| 574 |
</div>
|
| 575 |
-
<div class="s-stepper"><button onclick="stepParam('conf',-0.01)">‹</button><span
|
|
|
|
|
|
|
| 576 |
</div>
|
| 577 |
<div class="s-row" data-param="iou">
|
| 578 |
<div>
|
| 579 |
<div class="text-xs font-semibold text-slate-300 flex items-center">IoU Threshold
|
| 580 |
<span class="info-wrap ml-1">
|
| 581 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 582 |
-
|
|
|
|
|
|
|
| 583 |
</span>
|
| 584 |
</div>
|
| 585 |
<div class="text-[10px] text-slate-500">Non-max suppression overlap</div>
|
| 586 |
</div>
|
| 587 |
-
<div class="s-stepper"><button onclick="stepParam('iou',-0.05)">‹</button><span
|
|
|
|
|
|
|
| 588 |
</div>
|
| 589 |
<div class="s-row" data-param="stride">
|
| 590 |
<div>
|
| 591 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Frame Stride
|
| 592 |
<span class="info-wrap ml-1">
|
| 593 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 594 |
-
|
|
|
|
|
|
|
| 595 |
</span>
|
| 596 |
</div>
|
| 597 |
<div class="text-[10px] text-slate-500">Frames skipped between detections</div>
|
| 598 |
</div>
|
| 599 |
-
<div class="s-stepper"><button onclick="stepParam('stride',-1)">‹</button><span
|
|
|
|
|
|
|
| 600 |
</div>
|
| 601 |
|
| 602 |
<div class="s-row" data-param="smoothing">
|
| 603 |
<div>
|
| 604 |
-
<div class="text-xs font-semibold text-slate-300 flex items-center">
|
| 605 |
-
Congestion Smoothing
|
| 606 |
-
<span class="info-wrap
|
| 607 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
| 609 |
</span>
|
| 610 |
</div>
|
| 611 |
<div class="text-[10px] text-slate-500">EMA Alpha factor for the rolling average</div>
|
| 612 |
</div>
|
| 613 |
-
<div class="s-stepper"><button onclick="stepParam('smoothing',-0.05)">‹</button><span
|
|
|
|
|
|
|
| 614 |
</div>
|
| 615 |
</div>
|
| 616 |
</div>
|
| 617 |
|
| 618 |
<!-- Artifact Generation Settings -->
|
| 619 |
-
<div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
|
|
|
|
| 620 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 621 |
<h3 class="font-bold text-white text-sm">Artifact Settings</h3>
|
| 622 |
-
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
|
|
|
| 623 |
</div>
|
| 624 |
<div class="px-6 py-4 flex-1 flex flex-col">
|
| 625 |
<div class="s-row" data-param="report">
|
| 626 |
<div>
|
| 627 |
-
<div class="text-xs font-semibold text-slate-300">
|
| 628 |
<div class="text-[10px] text-slate-500">Format for separate KPI chart files</div>
|
| 629 |
</div>
|
| 630 |
-
<
|
| 631 |
-
<
|
| 632 |
-
|
| 633 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
</div>
|
| 635 |
-
<div class="s-row flex-col items-stretch" data-param="annotated">
|
| 636 |
-
<div class="flex items-center justify-between w-full">
|
| 637 |
-
<div>
|
| 638 |
-
<div class="text-xs font-semibold text-slate-300">Export Annotated Video</div>
|
| 639 |
-
<div class="text-[10px] text-slate-500">
|
| 640 |
</div>
|
| 641 |
<div class="toggle-track" id="sv-annotated" onclick="toggleExportMaster(this)">
|
| 642 |
<div class="toggle-thumb"></div>
|
|
@@ -663,10 +627,12 @@
|
|
| 663 |
</div>
|
| 664 |
<div class="s-row">
|
| 665 |
<div>
|
| 666 |
-
<div class="text-xs font-semibold text-slate-300 flex items-center">Export Run Details
|
| 667 |
<span class="info-wrap ml-1">
|
| 668 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 669 |
-
|
|
|
|
|
|
|
| 670 |
</span>
|
| 671 |
</div>
|
| 672 |
<div class="text-[10px] text-slate-500">Structurally represented technical details</div>
|
|
@@ -679,7 +645,8 @@
|
|
| 679 |
<div>
|
| 680 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Export CSV Results
|
| 681 |
<span class="info-wrap ml-1">
|
| 682 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
|
|
|
| 683 |
<span class="info-tip">Include frame-by-frame data in raw CSV format.</span>
|
| 684 |
</span>
|
| 685 |
</div>
|
|
@@ -693,8 +660,11 @@
|
|
| 693 |
<div>
|
| 694 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Auto-Download Report
|
| 695 |
<span class="info-wrap ml-1">
|
| 696 |
-
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 697 |
-
|
|
|
|
|
|
|
|
|
|
| 698 |
</span>
|
| 699 |
</div>
|
| 700 |
<div class="text-[10px] text-slate-500">Save reports automatically</div>
|
|
@@ -708,7 +678,8 @@
|
|
| 708 |
<div class="text-xs font-semibold text-slate-300">Interface Mode</div>
|
| 709 |
<div class="text-[10px] text-slate-500">Locked to Professional Dark</div>
|
| 710 |
</div>
|
| 711 |
-
<div class="text-xs font-bold text-white px-3 py-1 bg-slate-800 rounded-full">Dark Mode Only
|
|
|
|
| 712 |
</div>
|
| 713 |
</div>
|
| 714 |
</div>
|
|
@@ -737,10 +708,12 @@
|
|
| 737 |
<div id="tab-help" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 738 |
<div class="space-y-6 w-full">
|
| 739 |
<div class="bg-black border rounded-xl overflow-hidden shadow-sm" style="border-color:#2a2a2a">
|
| 740 |
-
<div class="px-6 py-4 border-b flex justify-between items-center"
|
|
|
|
| 741 |
<div>
|
| 742 |
<h3 class="font-bold text-white text-sm flex items-center">Navigator</h3>
|
| 743 |
-
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
|
|
|
| 744 |
assistance and platform documentation</p>
|
| 745 |
</div>
|
| 746 |
</div>
|
|
@@ -800,10 +773,12 @@
|
|
| 800 |
to the upload interface without requiring a full browser refresh.
|
| 801 |
</div>
|
| 802 |
</div>
|
| 803 |
-
<div
|
|
|
|
| 804 |
<button onclick="toggleHelp('h4')"
|
| 805 |
class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-[#080808] transition">
|
| 806 |
-
<span class="text-sm font-semibold" style="color:#a89f97">How accurate are
|
|
|
|
| 807 |
traffic calculations?</span>
|
| 808 |
<i id="h4-icon" class="fa-solid fa-plus text-xs" style="color:#555"></i>
|
| 809 |
</button>
|
|
@@ -816,10 +791,12 @@
|
|
| 816 |
human-level perception.
|
| 817 |
</div>
|
| 818 |
</div>
|
| 819 |
-
<div
|
|
|
|
| 820 |
<button onclick="toggleHelp('h5')"
|
| 821 |
class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-[#080808] transition">
|
| 822 |
-
<span class="text-sm font-semibold" style="color:#a89f97">What is PCU analysis
|
|
|
|
| 823 |
which speed metrics are calculated?</span>
|
| 824 |
<i id="h5-icon" class="fa-solid fa-plus text-xs" style="color:#555"></i>
|
| 825 |
</button>
|
|
@@ -957,16 +934,18 @@
|
|
| 957 |
|
| 958 |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
|
| 959 |
<!-- Left Side: Experience & Requirements (All Selective Choices) -->
|
| 960 |
-
<div
|
| 961 |
-
|
| 962 |
<div class="px-6 py-4 border-b bg-[#050505]" style="border-color:#1a1a1a">
|
| 963 |
<h3 class="font-bold text-white text-sm flex items-center">Experience & Priorities</h3>
|
| 964 |
-
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
|
|
|
| 965 |
experience</p>
|
| 966 |
</div>
|
| 967 |
<div class="p-6 flex-1 space-y-8">
|
| 968 |
<!-- Overall Experience Centered -->
|
| 969 |
-
<div class="flex flex-col items-center justify-center border-b pb-6"
|
|
|
|
| 970 |
<label class="text-[10px] font-bold uppercase tracking-widest block mb-3 text-center"
|
| 971 |
style="color:#a89f97">Overall Experience</label>
|
| 972 |
<div class="fb-stars" id="fb-stars">
|
|
@@ -986,7 +965,8 @@
|
|
| 986 |
<div class="flex gap-2" id="fb-recommend">
|
| 987 |
<div class="fb-emoji-btn" onclick="setEmoji(this, 'fb-recommend', 'Unlikely')">
|
| 988 |
<i class="fa-solid fa-face-frown text-xl"></i><span
|
| 989 |
-
class="block mt-1 text-[8px] uppercase">Unlikely</span>
|
|
|
|
| 990 |
<div class="fb-emoji-btn" onclick="setEmoji(this, 'fb-recommend', 'Maybe')"><i
|
| 991 |
class="fa-solid fa-face-meh text-xl"></i><span
|
| 992 |
class="block mt-1 text-[8px] uppercase">Maybe</span></div>
|
|
@@ -1094,11 +1074,12 @@
|
|
| 1094 |
</div>
|
| 1095 |
|
| 1096 |
<!-- Right Side: Categorization & Feedback Text -->
|
| 1097 |
-
<div
|
| 1098 |
-
|
| 1099 |
<div class="px-6 py-4 border-b bg-[#050505]" style="border-color:#1a1a1a">
|
| 1100 |
<h3 class="font-bold text-white text-sm flex items-center">Categorization & Feedback</h3>
|
| 1101 |
-
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
|
|
|
| 1102 |
workflow insights</p>
|
| 1103 |
</div>
|
| 1104 |
<div class="p-6 flex-1 flex flex-col space-y-6">
|
|
@@ -1106,25 +1087,37 @@
|
|
| 1106 |
<div>
|
| 1107 |
<label class="text-[10px] font-bold uppercase tracking-widest block mb-3"
|
| 1108 |
style="color:#a89f97">Primary Use Case</label>
|
| 1109 |
-
<
|
| 1110 |
-
<
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
<
|
| 1114 |
-
<
|
| 1115 |
-
|
| 1116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1117 |
</div>
|
| 1118 |
<div>
|
| 1119 |
<label class="text-[10px] font-bold uppercase tracking-widest block mb-3"
|
| 1120 |
style="color:#a89f97">Feedback Category</label>
|
| 1121 |
-
<
|
| 1122 |
-
<
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
<
|
| 1126 |
-
<
|
| 1127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1128 |
</div>
|
| 1129 |
</div>
|
| 1130 |
|
|
@@ -1302,12 +1295,67 @@
|
|
| 1302 |
const el = document.getElementById('appModal-' + id);
|
| 1303 |
if (el) { el.style.display = 'none'; document.body.style.overflow = ''; }
|
| 1304 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
document.addEventListener('keydown', function (e) {
|
| 1306 |
-
if (e.key === 'Escape') {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1307 |
});
|
| 1308 |
|
| 1309 |
-
// Auto-show keyboard shortcuts on every page visit
|
| 1310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1311 |
</script>
|
| 1312 |
</body>
|
| 1313 |
|
|
|
|
| 3 |
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 7 |
<title>UrbanFlow</title>
|
| 8 |
<link rel="icon" type="image/png" href="assets/rf.png">
|
| 9 |
+
<link rel="manifest" href="manifest.json">
|
| 10 |
+
<meta name="theme-color" content="#000000">
|
| 11 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 12 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
| 13 |
+
<meta name="apple-mobile-web-app-title" content="UrbanFlow">
|
| 14 |
+
<link rel="apple-touch-icon" href="assets/icon-192.png">
|
| 15 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 16 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 17 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
| 22 |
<link rel="stylesheet" href="css/vehicles.css">
|
| 23 |
</head>
|
| 24 |
|
| 25 |
+
<body class="bg-black text-white h-screen w-screen flex" style="overflow:hidden">
|
| 26 |
+
<!-- Fixed portal: bubbles, toasts, overlays append here — never clipped -->
|
| 27 |
+
<div id="fixed-portal" style="position:fixed;inset:0;pointer-events:none;z-index:8999"></div>
|
| 28 |
+
<div class="mobile-top-bar" id="mobile-top-bar">
|
| 29 |
+
<img src="assets/uf_rf.png" alt="UrbanFlow" class="h-14 w-auto object-contain">
|
| 30 |
+
|
| 31 |
+
<!-- Mobile Legal Overflow -->
|
| 32 |
+
<div class="absolute right-4 top-1/2 -translate-y-1/2 flex items-center">
|
| 33 |
+
<button onclick="toggleLegalMenu(event)" class="text-[#a89f97] hover:text-white p-2 transition-colors" aria-label="Legal Menu">
|
| 34 |
+
<i class="fa-solid fa-ellipsis-vertical text-lg"></i>
|
| 35 |
+
</button>
|
| 36 |
+
<div id="legal-menu" class="hidden absolute right-0 top-full mt-2 w-48 bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg shadow-2xl z-50 overflow-hidden">
|
| 37 |
+
<button onclick="openAppModal('privacyModal'); toggleLegalMenu(event)" class="w-full text-left px-4 py-3.5 text-[10px] font-bold uppercase tracking-widest text-[#a89f97] hover:text-white hover:bg-[#111] border-b border-[#1a1a1a] transition-all">
|
| 38 |
+
Privacy Policy
|
| 39 |
+
</button>
|
| 40 |
+
<button onclick="openAppModal('termsModal'); toggleLegalMenu(event)" class="w-full text-left px-4 py-3.5 text-[10px] font-bold uppercase tracking-widest text-[#a89f97] hover:text-white hover:bg-[#111] transition-all">
|
| 41 |
+
Terms & Conditions
|
| 42 |
+
</button>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
|
| 47 |
<!-- Sidebar -->
|
| 48 |
<aside class="w-60 bg-white shadow-xl flex flex-col z-20 flex-shrink-0 border-r border-slate-200 relative">
|
|
|
|
| 122 |
|
| 123 |
<!-- TAB: About -->
|
| 124 |
<div id="tab-about" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 125 |
+
<div class="bg-black border rounded-xl p-12 shadow-2xl space-y-8 flex flex-col justify-center"
|
| 126 |
+
style="border-color:#2a2a2a">
|
| 127 |
<div class="text-center">
|
| 128 |
<p class="text-sm italic font-bold leading-relaxed max-w-3xl mx-auto"
|
| 129 |
style="color:#c89a6c;font-family:'Montserrat',sans-serif">
|
|
|
|
| 158 |
</p>
|
| 159 |
</div>
|
| 160 |
|
| 161 |
+
<div class="grid grid-cols-2 gap-12 pt-8 text-left border-t max-w-5xl mx-auto"
|
| 162 |
+
style="border-color:#1a1a1a">
|
| 163 |
<div class="space-y-4">
|
| 164 |
<h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">What You Get
|
| 165 |
</h4>
|
|
|
|
| 183 |
</ul>
|
| 184 |
</div>
|
| 185 |
<div class="space-y-4">
|
| 186 |
+
<h4 class="font-bold text-[13px] uppercase tracking-wider" style="color:#f0ece6">Precision &
|
| 187 |
+
Accuracy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
</h4>
|
| 189 |
+
<p class="text-xs leading-relaxed" style="color:#a89f97">
|
| 190 |
+
UrbanFlow provides an estimated accuracy of ±5–8% on dense mixed-traffic footage. Results
|
| 191 |
+
may vary slightly across runs due to the nature of real-time frame-by-frame inference.
|
| 192 |
+
</p>
|
| 193 |
+
<p class="text-xs leading-relaxed" style="color:#a89f97">
|
| 194 |
+
For research or planning use, we recommend processing the same video 2–3 times and taking
|
| 195 |
+
the average count for maximum reliability.
|
| 196 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
</div>
|
| 198 |
</div>
|
| 199 |
|
|
|
|
| 214 |
|
| 215 |
|
| 216 |
<!-- Progress Bar (shared) -->
|
| 217 |
+
<div id="progress-bar-wrapper"
|
| 218 |
+
class="w-full bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
|
| 219 |
<div class="flex items-center space-x-4 flex-1 mr-6">
|
| 220 |
<span class="text-[11px] font-black text-white uppercase tracking-wider whitespace-nowrap"
|
| 221 |
id="proc-label">Waiting</span>
|
|
|
|
| 227 |
<div class="flex items-center space-x-6 text-xs font-bold text-white whitespace-nowrap">
|
| 228 |
<span id="proc-frames">0 / 0 Frames</span>
|
| 229 |
<span id="proc-pct">0%</span>
|
| 230 |
+
<div class="uf-select-wrap" id="live-palette-wrap">
|
| 231 |
+
<div class="uf-select-trigger" id="live-palette-trigger" onclick="ufSelectToggle('live-palette')">
|
| 232 |
+
<span class="uf-select-label" id="live-palette-label">Default</span>
|
| 233 |
+
<i class="fa-solid fa-chevron-down uf-select-arrow" id="live-palette-arrow"></i>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="uf-select-dropdown hidden" id="live-palette-dropdown">
|
| 236 |
+
<div class="uf-select-option" data-value="default" onclick="ufSelectPick('live-palette','default','Default')">Default</div>
|
| 237 |
+
<div class="uf-select-option" data-value="vibrant" onclick="ufSelectPick('live-palette','vibrant','Vibrant')">Vibrant</div>
|
| 238 |
+
<div class="uf-select-option" data-value="corporate" onclick="ufSelectPick('live-palette','corporate','Corporate')">Corporate</div>
|
| 239 |
+
<div class="uf-select-option" data-value="neon" onclick="ufSelectPick('live-palette','neon','Neon Night')">Neon Night</div>
|
| 240 |
+
<div class="uf-select-option" data-value="earth" onclick="ufSelectPick('live-palette','earth','Earth Tones')">Earth Tones</div>
|
| 241 |
+
<div class="uf-select-option" data-value="ocean" onclick="ufSelectPick('live-palette','ocean','Ocean Breeze')">Ocean Breeze</div>
|
| 242 |
+
<div class="uf-select-option" data-value="sunset" onclick="ufSelectPick('live-palette','sunset','Sunset Glow')">Sunset Glow</div>
|
| 243 |
+
<div class="uf-select-option" data-value="midnight" onclick="ufSelectPick('live-palette','midnight','Midnight Deep')">Midnight Deep</div>
|
| 244 |
+
<div class="uf-select-option" data-value="gold" onclick="ufSelectPick('live-palette','gold','Monochrome Gold')">Monochrome Gold</div>
|
| 245 |
+
</div>
|
| 246 |
+
<input type="hidden" id="live-palette-select" value="default">
|
| 247 |
+
</div>
|
| 248 |
</div>
|
| 249 |
</div>
|
| 250 |
|
|
|
|
| 319 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 320 |
<span class="info-tip">Aggregated vehicle count grouped by dominant category.</span></span>
|
| 321 |
</h3>
|
| 322 |
+
<span
|
| 323 |
+
class="text-[10px] text-slate-500 font-bold uppercase tracking-wider whitespace-nowrap">Dominant
|
| 324 |
Intelligence</span>
|
| 325 |
</div>
|
| 326 |
<div class="flex-1 w-full relative min-h-[220px]">
|
|
|
|
| 353 |
<div class="space-y-6 w-full max-w-[1400px] mx-auto">
|
| 354 |
|
| 355 |
<!-- HERO: Process Analytics -->
|
| 356 |
+
<div id="run-results-card" class="bg-white rounded-xl border shadow-sm overflow-hidden flex flex-col"
|
| 357 |
+
style="border-color:#2a2a2a">
|
| 358 |
+
<div class="px-6 py-4 border-b flex flex-col lg:flex-row justify-between items-center gap-4"
|
| 359 |
+
style="border-color:#1a1a1a;background:#050505">
|
| 360 |
+
<div class="text-center lg:text-left">
|
| 361 |
<h3 class="font-bold text-sm" style="color:#f0ece6">Process Analytics</h3>
|
| 362 |
+
<p class="text-[10px] mt-0.5 uppercase tracking-widest font-medium"
|
| 363 |
+
style="color:#a89f97">
|
| 364 |
Execution Telemetry</p>
|
| 365 |
</div>
|
| 366 |
<div class="flex items-center gap-3">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
</div>
|
| 368 |
</div>
|
| 369 |
<div class="p-8">
|
| 370 |
<div id="run-results-content" class="grid grid-cols-3 gap-12">
|
| 371 |
+
<div class="flex flex-col items-center justify-center p-12 rounded-2xl col-span-3"
|
| 372 |
+
style="color:#555">
|
| 373 |
+
<span class="text-[11px] font-bold uppercase tracking-[0.2em]">Initiate a run to view performance insights</span>
|
|
|
|
| 374 |
</div>
|
| 375 |
</div>
|
| 376 |
</div>
|
|
|
|
| 378 |
|
| 379 |
<!-- Technical Context Row -->
|
| 380 |
<div class="grid grid-cols-2 gap-6">
|
| 381 |
+
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
| 382 |
+
style="border-color:#2a2a2a">
|
| 383 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 384 |
<h3 class="font-bold text-sm" style="color:#f0ece6">Stream Source Profile</h3>
|
| 385 |
</div>
|
| 386 |
<div class="p-6 space-y-4" id="panel-video"></div>
|
| 387 |
</div>
|
| 388 |
+
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
| 389 |
+
style="border-color:#2a2a2a">
|
| 390 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 391 |
<h3 class="font-bold text-sm" style="color:#f0ece6">System Resource Utilization</h3>
|
| 392 |
</div>
|
| 393 |
<div class="p-6 space-y-4" id="panel-perf"></div>
|
| 394 |
</div>
|
| 395 |
+
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
| 396 |
+
style="border-color:#2a2a2a">
|
| 397 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 398 |
<h3 class="font-bold text-sm" style="color:#f0ece6">Model Architecture & Logic</h3>
|
| 399 |
</div>
|
| 400 |
<div class="p-6 space-y-4" id="panel-model"></div>
|
| 401 |
</div>
|
| 402 |
+
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
| 403 |
+
style="border-color:#2a2a2a">
|
| 404 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 405 |
<h3 class="font-bold text-sm" style="color:#f0ece6">Inference Parameters</h3>
|
| 406 |
</div>
|
|
|
|
| 410 |
|
| 411 |
<!-- Insights Panel (xAI) -->
|
| 412 |
<div id="insights-panel" class="hidden">
|
| 413 |
+
<div class="mt-6">
|
| 414 |
+
<div class="bg-white rounded-xl border shadow-sm flex flex-col overflow-hidden"
|
| 415 |
+
style="border-color:#2a2a2a">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 417 |
+
<h3 class="font-bold text-sm flex items-center" style="color:#f0ece6">Congestion
|
| 418 |
+
Insights
|
| 419 |
<span class="info-wrap"><span class="info-btn"><i
|
| 420 |
class="fa-solid fa-info"></i></span>
|
| 421 |
<span class="info-tip">Automated peak detection and congestion
|
|
|
|
| 435 |
<!-- TAB: Reports -->
|
| 436 |
<div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 437 |
<div id="reports-pending-message"
|
| 438 |
+
class="mb-4 text-center p-8 border border-[#222] bg-[#0a0a0a] rounded-xl flex flex-col items-center justify-center gap-4 shadow-sm min-h-[200px]">
|
| 439 |
+
<i id="reports-pending-icon" class="fa-solid fa-layer-group text-3xl text-[#c89a6c] opacity-50"></i>
|
| 440 |
<span id="reports-pending-text"
|
| 441 |
+
class="text-xs text-[#a89f97] font-medium tracking-wide uppercase leading-relaxed max-w-[240px]">
|
| 442 |
+
Please return once processing is 100% complete to access artifacts.
|
| 443 |
+
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
</div>
|
| 445 |
|
| 446 |
<!-- Post-Processing Insights (Populated after completion) -->
|
| 447 |
+
<div id="post-process-cards" class="hidden mb-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
<!-- PCU Summary -->
|
| 449 |
<div class="bg-black rounded-xl p-6 border border-slate-800 shadow-sm flex flex-col min-h-[140px]">
|
| 450 |
<div class="flex justify-between items-center mb-4 relative">
|
|
|
|
| 469 |
<div id="tab-settings" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 470 |
<div class="grid grid-cols-2 gap-6 w-full">
|
| 471 |
<!-- Processing Parameters -->
|
| 472 |
+
<div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
|
| 473 |
+
style="border-color:#2a2a2a">
|
| 474 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 475 |
<h3 class="font-bold text-white text-sm flex items-center">Inference Configuration Profile
|
| 476 |
<span class="info-wrap ml-2">
|
| 477 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 478 |
+
class="fa-solid fa-info"></i></span>
|
| 479 |
+
<span class="info-tip">Figures and numbers below are auto-calculated for best
|
| 480 |
+
performance.</span>
|
| 481 |
</span>
|
| 482 |
</h3>
|
| 483 |
+
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
| 484 |
+
Auto-configured pipeline parameters</p>
|
| 485 |
</div>
|
| 486 |
<div class="px-6 py-2 flex-1" id="settings-params">
|
| 487 |
<div class="s-row" data-param="imgsz">
|
| 488 |
<div>
|
| 489 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Image Size
|
| 490 |
<span class="info-wrap ml-1">
|
| 491 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 492 |
+
class="fa-solid fa-info"></i></span>
|
| 493 |
+
<span class="info-tip">Input resolution fed to the detection model. Optimized
|
| 494 |
+
for the resolution compiled into the OpenVINO weights.</span>
|
| 495 |
</span>
|
| 496 |
</div>
|
| 497 |
<div class="text-[10px] text-slate-500">Model input resolution</div>
|
| 498 |
</div>
|
| 499 |
+
<div class="s-stepper"><button onclick="stepParam('imgsz',-32)">‹</button><span
|
| 500 |
+
class="s-val" id="sv-imgsz">640</span><button
|
| 501 |
+
onclick="stepParam('imgsz',32)">›</button></div>
|
| 502 |
</div>
|
| 503 |
<div class="s-row" data-param="conf">
|
| 504 |
<div>
|
| 505 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Confidence
|
| 506 |
<span class="info-wrap ml-1">
|
| 507 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 508 |
+
class="fa-solid fa-info"></i></span>
|
| 509 |
+
<span class="info-tip">Minimum score a detection must reach to be counted. Lower
|
| 510 |
+
values detect more but may include false positives.</span>
|
| 511 |
</span>
|
| 512 |
</div>
|
| 513 |
<div class="text-[10px] text-slate-500">Minimum detection threshold</div>
|
| 514 |
</div>
|
| 515 |
+
<div class="s-stepper"><button onclick="stepParam('conf',-0.01)">‹</button><span
|
| 516 |
+
class="s-val" id="sv-conf">0.12</span><button
|
| 517 |
+
onclick="stepParam('conf',0.01)">›</button></div>
|
| 518 |
</div>
|
| 519 |
<div class="s-row" data-param="iou">
|
| 520 |
<div>
|
| 521 |
<div class="text-xs font-semibold text-slate-300 flex items-center">IoU Threshold
|
| 522 |
<span class="info-wrap ml-1">
|
| 523 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 524 |
+
class="fa-solid fa-info"></i></span>
|
| 525 |
+
<span class="info-tip">Controls how aggressively overlapping detections are
|
| 526 |
+
merged. Higher values allow more overlap before suppression.</span>
|
| 527 |
</span>
|
| 528 |
</div>
|
| 529 |
<div class="text-[10px] text-slate-500">Non-max suppression overlap</div>
|
| 530 |
</div>
|
| 531 |
+
<div class="s-stepper"><button onclick="stepParam('iou',-0.05)">‹</button><span
|
| 532 |
+
class="s-val" id="sv-iou">0.60</span><button
|
| 533 |
+
onclick="stepParam('iou',0.05)">›</button></div>
|
| 534 |
</div>
|
| 535 |
<div class="s-row" data-param="stride">
|
| 536 |
<div>
|
| 537 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Frame Stride
|
| 538 |
<span class="info-wrap ml-1">
|
| 539 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 540 |
+
class="fa-solid fa-info"></i></span>
|
| 541 |
+
<span class="info-tip">Number of frames skipped between each detection pass.
|
| 542 |
+
Higher values trade accuracy for speed.</span>
|
| 543 |
</span>
|
| 544 |
</div>
|
| 545 |
<div class="text-[10px] text-slate-500">Frames skipped between detections</div>
|
| 546 |
</div>
|
| 547 |
+
<div class="s-stepper"><button onclick="stepParam('stride',-1)">‹</button><span
|
| 548 |
+
class="s-val" id="sv-stride">2</span><button
|
| 549 |
+
onclick="stepParam('stride',1)">›</button></div>
|
| 550 |
</div>
|
| 551 |
|
| 552 |
<div class="s-row" data-param="smoothing">
|
| 553 |
<div>
|
| 554 |
+
<div class="text-xs font-semibold text-slate-300 flex items-center gap-1">
|
| 555 |
+
<span>Congestion Smoothing</span>
|
| 556 |
+
<span class="info-wrap">
|
| 557 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 558 |
+
class="fa-solid fa-info"></i></span>
|
| 559 |
+
<span class="info-tip">Reduces jitter/noise in the line chart. Low values
|
| 560 |
+
(0.05-0.2) create very smooth trends; high values (0.8+) show raw spiky
|
| 561 |
+
data.</span>
|
| 562 |
</span>
|
| 563 |
</div>
|
| 564 |
<div class="text-[10px] text-slate-500">EMA Alpha factor for the rolling average</div>
|
| 565 |
</div>
|
| 566 |
+
<div class="s-stepper"><button onclick="stepParam('smoothing',-0.05)">‹</button><span
|
| 567 |
+
class="s-val" id="sv-smoothing">0.25</span><button
|
| 568 |
+
onclick="stepParam('smoothing',0.05)">›</button></div>
|
| 569 |
</div>
|
| 570 |
</div>
|
| 571 |
</div>
|
| 572 |
|
| 573 |
<!-- Artifact Generation Settings -->
|
| 574 |
+
<div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
|
| 575 |
+
style="border-color:#2a2a2a">
|
| 576 |
<div class="px-6 py-4 border-b" style="border-color:#1a1a1a;background:#050505">
|
| 577 |
<h3 class="font-bold text-white text-sm">Artifact Settings</h3>
|
| 578 |
+
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
| 579 |
+
Configured visual overlay options</p>
|
| 580 |
</div>
|
| 581 |
<div class="px-6 py-4 flex-1 flex flex-col">
|
| 582 |
<div class="s-row" data-param="report">
|
| 583 |
<div>
|
| 584 |
+
<div class="text-xs font-semibold text-slate-300">Chart Format</div>
|
| 585 |
<div class="text-[10px] text-slate-500">Format for separate KPI chart files</div>
|
| 586 |
</div>
|
| 587 |
+
<div class="uf-select-wrap" id="sv-report-wrap">
|
| 588 |
+
<div class="uf-select-trigger" id="sv-report-trigger" onclick="ufSelectToggle('sv-report')">
|
| 589 |
+
<span class="uf-select-label" id="sv-report-label">PNG Image</span>
|
| 590 |
+
<i class="fa-solid fa-chevron-down uf-select-arrow" id="sv-report-arrow"></i>
|
| 591 |
+
</div>
|
| 592 |
+
<div class="uf-select-dropdown hidden" id="sv-report-dropdown">
|
| 593 |
+
<div class="uf-select-option uf-select-option-active" data-value="png" onclick="ufSelectPick('sv-report','png','PNG Image')">PNG Image</div>
|
| 594 |
+
<div class="uf-select-option" data-value="pdf" onclick="ufSelectPick('sv-report','pdf','PDF Document')">PDF Document</div>
|
| 595 |
+
</div>
|
| 596 |
+
<input type="hidden" id="sv-report" value="png">
|
| 597 |
+
</div>
|
| 598 |
</div>
|
| 599 |
+
<div class="s-row flex-col items-stretch !gap-0" data-param="annotated">
|
| 600 |
+
<div class="flex items-center justify-between w-full py-2">
|
| 601 |
+
<div class="flex-1 pr-2">
|
| 602 |
+
<div class="text-xs font-semibold text-slate-300">Export Annotated Video</div>
|
| 603 |
+
<div class="text-[10px] text-slate-500">Diagnostic visual overlays</div>
|
| 604 |
</div>
|
| 605 |
<div class="toggle-track" id="sv-annotated" onclick="toggleExportMaster(this)">
|
| 606 |
<div class="toggle-thumb"></div>
|
|
|
|
| 627 |
</div>
|
| 628 |
<div class="s-row">
|
| 629 |
<div>
|
| 630 |
+
<div class="text-xs font-semibold text-slate-300 flex items-center">Export Run Details
|
| 631 |
<span class="info-wrap ml-1">
|
| 632 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 633 |
+
class="fa-solid fa-info"></i></span>
|
| 634 |
+
<span class="info-tip">Include analysis.json payload with all run parameters and
|
| 635 |
+
data structures.</span>
|
| 636 |
</span>
|
| 637 |
</div>
|
| 638 |
<div class="text-[10px] text-slate-500">Structurally represented technical details</div>
|
|
|
|
| 645 |
<div>
|
| 646 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Export CSV Results
|
| 647 |
<span class="info-wrap ml-1">
|
| 648 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 649 |
+
class="fa-solid fa-info"></i></span>
|
| 650 |
<span class="info-tip">Include frame-by-frame data in raw CSV format.</span>
|
| 651 |
</span>
|
| 652 |
</div>
|
|
|
|
| 660 |
<div>
|
| 661 |
<div class="text-xs font-semibold text-slate-300 flex items-center">Auto-Download Report
|
| 662 |
<span class="info-wrap ml-1">
|
| 663 |
+
<span class="info-btn" style="background:#1a1a1a;color:#888"><i
|
| 664 |
+
class="fa-solid fa-info"></i></span>
|
| 665 |
+
<span class="info-tip">Enable before processing starts. The artifact bundle
|
| 666 |
+
(ZIP) will download automatically once analysis completes. Cannot be changed
|
| 667 |
+
after processing finishes.</span>
|
| 668 |
</span>
|
| 669 |
</div>
|
| 670 |
<div class="text-[10px] text-slate-500">Save reports automatically</div>
|
|
|
|
| 678 |
<div class="text-xs font-semibold text-slate-300">Interface Mode</div>
|
| 679 |
<div class="text-[10px] text-slate-500">Locked to Professional Dark</div>
|
| 680 |
</div>
|
| 681 |
+
<div class="text-xs font-bold text-white px-3 py-1 bg-slate-800 rounded-full">Dark Mode Only
|
| 682 |
+
</div>
|
| 683 |
</div>
|
| 684 |
</div>
|
| 685 |
</div>
|
|
|
|
| 708 |
<div id="tab-help" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 709 |
<div class="space-y-6 w-full">
|
| 710 |
<div class="bg-black border rounded-xl overflow-hidden shadow-sm" style="border-color:#2a2a2a">
|
| 711 |
+
<div class="px-6 py-4 border-b flex justify-between items-center"
|
| 712 |
+
style="border-color:#1a1a1a;background:#050505">
|
| 713 |
<div>
|
| 714 |
<h3 class="font-bold text-white text-sm flex items-center">Navigator</h3>
|
| 715 |
+
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
| 716 |
+
Quick
|
| 717 |
assistance and platform documentation</p>
|
| 718 |
</div>
|
| 719 |
</div>
|
|
|
|
| 773 |
to the upload interface without requiring a full browser refresh.
|
| 774 |
</div>
|
| 775 |
</div>
|
| 776 |
+
<div
|
| 777 |
+
class="bg-[#050505] border border-slate-900 rounded-lg overflow-hidden transition hover:border-[#1a1a1a]">
|
| 778 |
<button onclick="toggleHelp('h4')"
|
| 779 |
class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-[#080808] transition">
|
| 780 |
+
<span class="text-sm font-semibold" style="color:#a89f97">How accurate are
|
| 781 |
+
UrbanFlow's
|
| 782 |
traffic calculations?</span>
|
| 783 |
<i id="h4-icon" class="fa-solid fa-plus text-xs" style="color:#555"></i>
|
| 784 |
</button>
|
|
|
|
| 791 |
human-level perception.
|
| 792 |
</div>
|
| 793 |
</div>
|
| 794 |
+
<div
|
| 795 |
+
class="bg-[#050505] border border-slate-900 rounded-lg overflow-hidden transition hover:border-[#1a1a1a]">
|
| 796 |
<button onclick="toggleHelp('h5')"
|
| 797 |
class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-[#080808] transition">
|
| 798 |
+
<span class="text-sm font-semibold" style="color:#a89f97">What is PCU analysis
|
| 799 |
+
and
|
| 800 |
which speed metrics are calculated?</span>
|
| 801 |
<i id="h5-icon" class="fa-solid fa-plus text-xs" style="color:#555"></i>
|
| 802 |
</button>
|
|
|
|
| 934 |
|
| 935 |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
|
| 936 |
<!-- Left Side: Experience & Requirements (All Selective Choices) -->
|
| 937 |
+
<div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
|
| 938 |
+
style="border-color:#2a2a2a">
|
| 939 |
<div class="px-6 py-4 border-b bg-[#050505]" style="border-color:#1a1a1a">
|
| 940 |
<h3 class="font-bold text-white text-sm flex items-center">Experience & Priorities</h3>
|
| 941 |
+
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
| 942 |
+
Rate your
|
| 943 |
experience</p>
|
| 944 |
</div>
|
| 945 |
<div class="p-6 flex-1 space-y-8">
|
| 946 |
<!-- Overall Experience Centered -->
|
| 947 |
+
<div class="flex flex-col items-center justify-center border-b pb-6"
|
| 948 |
+
style="border-color:#1a1a1a">
|
| 949 |
<label class="text-[10px] font-bold uppercase tracking-widest block mb-3 text-center"
|
| 950 |
style="color:#a89f97">Overall Experience</label>
|
| 951 |
<div class="fb-stars" id="fb-stars">
|
|
|
|
| 965 |
<div class="flex gap-2" id="fb-recommend">
|
| 966 |
<div class="fb-emoji-btn" onclick="setEmoji(this, 'fb-recommend', 'Unlikely')">
|
| 967 |
<i class="fa-solid fa-face-frown text-xl"></i><span
|
| 968 |
+
class="block mt-1 text-[8px] uppercase">Unlikely</span>
|
| 969 |
+
</div>
|
| 970 |
<div class="fb-emoji-btn" onclick="setEmoji(this, 'fb-recommend', 'Maybe')"><i
|
| 971 |
class="fa-solid fa-face-meh text-xl"></i><span
|
| 972 |
class="block mt-1 text-[8px] uppercase">Maybe</span></div>
|
|
|
|
| 1074 |
</div>
|
| 1075 |
|
| 1076 |
<!-- Right Side: Categorization & Feedback Text -->
|
| 1077 |
+
<div class="col-span-1 bg-black rounded-xl border shadow-sm overflow-hidden flex flex-col"
|
| 1078 |
+
style="border-color:#2a2a2a">
|
| 1079 |
<div class="px-6 py-4 border-b bg-[#050505]" style="border-color:#1a1a1a">
|
| 1080 |
<h3 class="font-bold text-white text-sm flex items-center">Categorization & Feedback</h3>
|
| 1081 |
+
<p class="text-[10px] mt-0.1 uppercase tracking-widest font-medium" style="color:#a89f97">
|
| 1082 |
+
Detailed
|
| 1083 |
workflow insights</p>
|
| 1084 |
</div>
|
| 1085 |
<div class="p-6 flex-1 flex flex-col space-y-6">
|
|
|
|
| 1087 |
<div>
|
| 1088 |
<label class="text-[10px] font-bold uppercase tracking-widest block mb-3"
|
| 1089 |
style="color:#a89f97">Primary Use Case</label>
|
| 1090 |
+
<div class="uf-select-wrap w-full" id="fb-usecase-wrap">
|
| 1091 |
+
<div class="uf-select-trigger" id="fb-usecase-trigger" onclick="ufSelectToggle('fb-usecase')">
|
| 1092 |
+
<span class="uf-select-label" id="fb-usecase-label" style="color:#666">Select your use case</span>
|
| 1093 |
+
<i class="fa-solid fa-chevron-down uf-select-arrow" id="fb-usecase-arrow"></i>
|
| 1094 |
+
</div>
|
| 1095 |
+
<div class="uf-select-dropdown hidden" id="fb-usecase-dropdown">
|
| 1096 |
+
<div class="uf-select-option" data-value="research" onclick="ufSelectPick('fb-usecase','research','Academic Research')">Academic Research</div>
|
| 1097 |
+
<div class="uf-select-option" data-value="planning" onclick="ufSelectPick('fb-usecase','planning','Urban Planning')">Urban Planning</div>
|
| 1098 |
+
<div class="uf-select-option" data-value="highway" onclick="ufSelectPick('fb-usecase','highway','Business Modelling')">Business Modelling</div>
|
| 1099 |
+
<div class="uf-select-option" data-value="smartcity" onclick="ufSelectPick('fb-usecase','smartcity','Smart City Investment')">Smart City Investment</div>
|
| 1100 |
+
<div class="uf-select-option" data-value="other" onclick="ufSelectPick('fb-usecase','other','Other...')">Other...</div>
|
| 1101 |
+
</div>
|
| 1102 |
+
<input type="hidden" id="fb-usecase" value="">
|
| 1103 |
+
</div>
|
| 1104 |
</div>
|
| 1105 |
<div>
|
| 1106 |
<label class="text-[10px] font-bold uppercase tracking-widest block mb-3"
|
| 1107 |
style="color:#a89f97">Feedback Category</label>
|
| 1108 |
+
<div class="uf-select-wrap w-full" id="fb-type-wrap">
|
| 1109 |
+
<div class="uf-select-trigger" id="fb-type-trigger" onclick="ufSelectToggle('fb-type')">
|
| 1110 |
+
<span class="uf-select-label" id="fb-type-label" style="color:#666">General Feedback</span>
|
| 1111 |
+
<i class="fa-solid fa-chevron-down uf-select-arrow" id="fb-type-arrow"></i>
|
| 1112 |
+
</div>
|
| 1113 |
+
<div class="uf-select-dropdown hidden" id="fb-type-dropdown">
|
| 1114 |
+
<div class="uf-select-option" data-value="bug" onclick="ufSelectPick('fb-type','bug','Technical Issue / Bug Report')">Technical Issue / Bug Report</div>
|
| 1115 |
+
<div class="uf-select-option" data-value="feature" onclick="ufSelectPick('fb-type','feature','Feature Request')">Feature Request</div>
|
| 1116 |
+
<div class="uf-select-option" data-value="accuracy" onclick="ufSelectPick('fb-type','accuracy','Inference Accuracy Review')">Inference Accuracy Review</div>
|
| 1117 |
+
<div class="uf-select-option" data-value="ux" onclick="ufSelectPick('fb-type','ux','UI / UX')">UI / UX</div>
|
| 1118 |
+
</div>
|
| 1119 |
+
<input type="hidden" id="fb-type" value="">
|
| 1120 |
+
</div>
|
| 1121 |
</div>
|
| 1122 |
</div>
|
| 1123 |
|
|
|
|
| 1295 |
const el = document.getElementById('appModal-' + id);
|
| 1296 |
if (el) { el.style.display = 'none'; document.body.style.overflow = ''; }
|
| 1297 |
}
|
| 1298 |
+
|
| 1299 |
+
// Legal Menu Toggle
|
| 1300 |
+
function toggleLegalMenu(e) {
|
| 1301 |
+
if (e) e.stopPropagation();
|
| 1302 |
+
const menu = document.getElementById('legal-menu');
|
| 1303 |
+
if (menu) menu.classList.toggle('hidden');
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
document.addEventListener('click', function(e) {
|
| 1307 |
+
const menu = document.getElementById('legal-menu');
|
| 1308 |
+
if (menu && !menu.classList.contains('hidden')) {
|
| 1309 |
+
if (!e.target.closest('.mobile-top-bar')) {
|
| 1310 |
+
menu.classList.add('hidden');
|
| 1311 |
+
}
|
| 1312 |
+
}
|
| 1313 |
+
});
|
| 1314 |
+
|
| 1315 |
document.addEventListener('keydown', function (e) {
|
| 1316 |
+
if (e.key === 'Escape') {
|
| 1317 |
+
closeAppModal('privacyModal');
|
| 1318 |
+
closeAppModal('termsModal');
|
| 1319 |
+
closeAppModal('shortcutsModal');
|
| 1320 |
+
const menu = document.getElementById('legal-menu');
|
| 1321 |
+
if (menu) menu.classList.add('hidden');
|
| 1322 |
+
}
|
| 1323 |
});
|
| 1324 |
|
| 1325 |
+
// Auto-show keyboard shortcuts on every page visit (Desktop only)
|
| 1326 |
+
if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
|
| 1327 |
+
setTimeout(function () { openAppModal('shortcutsModal'); }, 800);
|
| 1328 |
+
}
|
| 1329 |
+
</script>
|
| 1330 |
+
<nav class="mobile-bottom-nav" id="mobile-bottom-nav">
|
| 1331 |
+
<button class="mob-nav-item" id="mob-nav-about" onclick="switchTab('about')">
|
| 1332 |
+
<i class="fa-solid fa-circle-info"></i>
|
| 1333 |
+
</button>
|
| 1334 |
+
<button class="mob-nav-item" id="mob-nav-overview" onclick="switchTab('overview')">
|
| 1335 |
+
<i class="fa-solid fa-desktop"></i>
|
| 1336 |
+
</button>
|
| 1337 |
+
<button class="mob-nav-item" id="mob-nav-run-details" onclick="switchTab('run-details')">
|
| 1338 |
+
<i class="fa-solid fa-microchip"></i>
|
| 1339 |
+
</button>
|
| 1340 |
+
<button class="mob-nav-item" id="mob-nav-reports" onclick="switchTab('reports')">
|
| 1341 |
+
<i class="fa-solid fa-file-lines"></i>
|
| 1342 |
+
</button>
|
| 1343 |
+
<button class="mob-nav-item" id="mob-nav-settings" onclick="switchTab('settings')">
|
| 1344 |
+
<i class="fa-solid fa-sliders"></i>
|
| 1345 |
+
</button>
|
| 1346 |
+
<button class="mob-nav-item" id="mob-nav-help" onclick="switchTab('help')">
|
| 1347 |
+
<i class="fa-solid fa-circle-question"></i>
|
| 1348 |
+
</button>
|
| 1349 |
+
<button class="mob-nav-item" id="mob-nav-feedback" onclick="switchTab('feedback')">
|
| 1350 |
+
<i class="fa-solid fa-comment-dots"></i>
|
| 1351 |
+
</button>
|
| 1352 |
+
</nav>
|
| 1353 |
+
<script>
|
| 1354 |
+
if ('serviceWorker' in navigator) {
|
| 1355 |
+
window.addEventListener('load', () => {
|
| 1356 |
+
navigator.serviceWorker.register('./sw.js');
|
| 1357 |
+
});
|
| 1358 |
+
}
|
| 1359 |
</script>
|
| 1360 |
</body>
|
| 1361 |
|
requirements.txt
CHANGED
|
@@ -7,11 +7,7 @@ ultralytics>=8.3.0
|
|
| 7 |
numpy>=1.23.0,<2.0.0
|
| 8 |
python-dotenv==1.0.0
|
| 9 |
websockets==12.0
|
| 10 |
-
onnxruntime
|
| 11 |
openvino>=2024.0.0
|
| 12 |
-
nncf>=2.14.0,<3.0.0
|
| 13 |
-
onnx>=1.12.0
|
| 14 |
-
onnxslim>=0.1.71
|
| 15 |
lap>=0.5.12
|
| 16 |
resend
|
| 17 |
torch==2.1.0+cpu
|
|
|
|
| 7 |
numpy>=1.23.0,<2.0.0
|
| 8 |
python-dotenv==1.0.0
|
| 9 |
websockets==12.0
|
|
|
|
| 10 |
openvino>=2024.0.0
|
|
|
|
|
|
|
|
|
|
| 11 |
lap>=0.5.12
|
| 12 |
resend
|
| 13 |
torch==2.1.0+cpu
|