UrbanFlow / frontend /initial.html
Subh775's picture
onboaridng popup
92dde2d
raw
history blame
21.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<!-- SPA Bootstrap: load dashboard without changing URL -->
<script>
if (sessionStorage.getItem('funky_run')) {
document.documentElement.style.display = 'none';
fetch('/vehicles.html')
.then(function (r) { return r.text(); })
.then(function (html) {
document.open();
document.write(html);
document.close();
});
}
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UrbanFlow</title>
<link rel="icon" type="image/svg+xml" href="rf.png">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<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">
<style>
:root {
--cocoa: #8b5e3c;
--cocoa-l: #c89a6c;
--cocoa-xl: #d4b08a;
--t1: #f0ece6;
--t2: #a89f97;
--border: #2a2a2a;
}
body {
font-family: 'Montserrat', sans-serif;
background-color: #000000;
color: var(--t1);
}
.fade-in {
animation: fadeIn 0.4s ease-in-out forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Executive Overrides */
.traffic-dynamics-card {
background-color: #0a0a0a !important;
border: 2px solid var(--cocoa) !important;
}
.traffic-dynamics-card:hover {
border-color: var(--cocoa-l) !important;
}
#dropzone {
transition: all 0.2s ease;
border-color: #2a2a2a;
}
#dropzone:hover {
border-color: var(--cocoa-l) !important;
background-color: #0a0a0a !important;
}
.core-badge {
background-color: var(--cocoa) !important;
color: var(--t1) !important;
}
/* Onboarding */
.onboard-overlay {
position: fixed; inset: 0; z-index: 9999;
background: rgba(0,0,0,0.92);
display: flex; align-items: center; justify-content: center;
}
.onboard-card {
background: #0a0a0a; border: 1px solid #2a2a2a;
border-radius: 16px; max-width: 440px; width: 90%;
padding: 40px 32px; text-align: center;
}
.onboard-step { display: none; }
.onboard-step.active { display: block; }
.onboard-dots { display: flex; gap: 6px; justify-content: center; margin-top: 20px; }
.onboard-dot {
width: 8px; height: 8px; border-radius: 50%;
background: #333; transition: background 0.2s;
}
.onboard-dot.active { background: var(--cocoa-l); }
/* Mobile responsive */
@media (max-width: 768px) {
main { grid-template-columns: 1fr !important; padding: 16px !important; }
h1 { font-size: 2.2rem !important; }
}
</style>
</head>
<body
class="bg-black text-white min-h-screen w-full flex flex-col items-center selection:bg-white selection:text-black">
<header class="mt-16 flex flex-col items-center flex-shrink-0 w-full z-10">
<img src="uf_rf.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
</header>
<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">
<div class="lg:col-span-7 flex flex-col justify-center xl:pl-10 pb-10 lg:pb-0">
<h1 class="text-5xl xl:text-[4.5rem] font-extrabold mb-4 leading-[1.1] tracking-tight" style="background:linear-gradient(110deg,#f0ece6 0%,#f0ece6 35%,#c89a6c 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
Automated <br>Vision Intelligence
</h1>
<p class="font-bold mb-8 text-sm uppercase tracking-[0.2em] flex items-center" style="color:#a89f97">
<span class="core-badge px-3 py-1 rounded-full text-[10px] mr-3">DEMO</span>
Cloud-Native Traffic Intelligence
</p>
<ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-medium" style="color:#a89f97">
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> No new hardware &mdash; works with your existing cameras</li>
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Granular vehicle counts across 14 Indian road classes</li>
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Directional flow &amp; congestion insights in minutes</li>
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Downloadable reports ready for planning &amp; compliance</li>
<li class="flex items-center"><i class="fa-solid fa-check mr-5 text-xl" style="color:#c89a6c"></i> Built for Indian roads &mdash; tested on real field conditions</li>
</ul>
</div>
<div
class="lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto min-h-[450px] mb-12 lg:mb-0">
<!-- STEP: Modules -->
<div id="step-modules" class="w-full flex flex-col fade-in">
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">UrbanFlow</h2>
<p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Select an analytical module to continue.</p>
<div class="flex justify-center w-full">
<div onclick="showStep('upload')"
class="group relative border-2 rounded-[2rem] p-8 cursor-pointer hover:-translate-y-1 transition-all duration-300 text-center max-w-sm w-full traffic-dynamics-card">
<div
class="absolute top-4 right-6 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider"
style="background:#c89a6c;color:#000">
DEMO</div>
<i class="fa-solid fa-car-side text-4xl mb-4 block mx-auto" style="color:#c89a6c"></i>
<h3 class="font-bold text-lg mb-2 leading-tight" style="color:#f0ece6">Traffic <br>Dynamics</h3>
<p class="text-[10px] font-medium leading-relaxed" style="color:#a89f97">Vehicle counting, classification, and flow analysis for Indian roads.</p>
</div>
</div>
</div>
<!-- STEP: Upload -->
<div id="step-upload" class="hidden w-full flex flex-col fade-in">
<button onclick="showStep('modules')"
class="text-neutral-500 hover:text-white transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
<i class="fa-solid fa-arrow-left mr-2"></i> Back
</button>
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Source Media</h2>
<p class="text-[13px] font-medium mb-8 text-center" style="color:#a89f97">Submit camera footage to begin traffic analysis.</p>
<input id="file-input" type="file" accept="video/*" class="hidden">
<div id="dropzone" onclick="document.getElementById('file-input').click()"
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">
<i
class="fa-solid fa-arrow-up-from-bracket text-4xl mb-5 block mx-auto transition" style="color:#a89f97"></i>
<span class="font-semibold text-lg mb-2 text-center block" style="color:#f0ece6">Drop or select a video file</span>
<span class="text-[10px] font-bold uppercase tracking-widest text-center block" style="color:#a89f97">Any standard video format accepted</span>
</div>
<div id="upload-progress-container" class="hidden mt-10 w-full">
<div class="flex justify-between text-[11px] font-bold uppercase tracking-widest mb-3" style="color:#f0ece6">
<span id="upload-text">Uploading...</span>
<span id="upload-percentage">0%</span>
</div>
<div class="w-full h-1 bg-neutral-900 rounded-full overflow-hidden">
<div id="upload-bar"
class="h-full w-0 transition-all duration-75 ease-linear rounded-full" style="background:#c89a6c"></div>
</div>
</div>
</div>
<!-- STEP: Draw -->
<div id="step-draw" class="hidden w-full flex flex-col fade-in">
<h2 class="text-3xl font-bold mb-2 text-center" style="color:#f0ece6">Spatial Boundary</h2>
<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>
<div
class="relative w-full aspect-video bg-neutral-950 rounded-3xl overflow-hidden cursor-crosshair mb-6">
<img id="frame-img" class="absolute inset-0 w-full h-full object-contain" style="display:none;">
<div id="frame-placeholder"
class="absolute inset-0 flex flex-col items-center justify-center text-neutral-800 pointer-events-none">
<i class="fa-solid fa-video text-4xl mb-3 opacity-30"></i>
<span class="font-bold text-[10px] uppercase tracking-widest opacity-50">Media Frame
Preview</span>
</div>
<canvas id="drawing-canvas" class="absolute inset-0 w-full h-full"></canvas>
</div>
<div class="flex flex-col items-center gap-3">
<button id="btn-proceed" onclick="startRun()"
class="w-fit px-16 py-3.5 rounded-full font-bold transition-all text-center text-sm shadow-lg hover:scale-105 active:scale-95" style="background:#c89a6c;color:#000">
Continue &nbsp;&rarr;
</button>
<button onclick="resetCanvas()"
class="text-[10px] font-bold uppercase tracking-widest text-slate-500 hover:text-white transition" style="background:none;border:none;">Reset Boundary</button>
</div>
</div>
</div>
</main>
<script>
let videoId = null;
let runConfig = {};
function showStep(name) {
['modules', 'upload', 'draw'].forEach(s => {
const el = document.getElementById('step-' + s);
if (el) el.classList.add('hidden');
});
const target = document.getElementById('step-' + name);
if (target) target.classList.remove('hidden');
if (name === 'upload') {
document.getElementById('upload-progress-container').classList.add('hidden');
document.getElementById('dropzone').classList.remove('hidden');
// Reset Progress Bar state for new uploads
document.getElementById('upload-bar').style.width = '0%';
document.getElementById('upload-percentage').innerText = '0%';
document.getElementById('upload-text').innerText = 'Uploading...';
document.getElementById('upload-text').classList.remove('text-red-500');
}
if (name === 'draw') loadFirstFrame();
}
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('file-input');
if (fileInput) {
fileInput.addEventListener('change', () => {
if (fileInput.files.length) uploadFile(fileInput.files[0]);
});
}
if (dropzone) {
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('border-white', 'bg-neutral-950'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('border-white', 'bg-neutral-950'));
dropzone.addEventListener('drop', e => {
e.preventDefault();
dropzone.classList.remove('border-white', 'bg-neutral-950');
if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]);
});
}
let currentXHR = null;
function uploadFile(file) {
// Abort previous upload if it exists to prevent jitter/multiple requests
if (currentXHR) currentXHR.abort();
const dropzoneEl = document.getElementById('dropzone');
const prog = document.getElementById('upload-progress-container');
const bar = document.getElementById('upload-bar');
const pct = document.getElementById('upload-percentage');
const txt = document.getElementById('upload-text');
if (dropzoneEl) dropzoneEl.classList.add('hidden');
if (prog) prog.classList.remove('hidden');
const form = new FormData();
form.append('file', file);
const xhr = new XMLHttpRequest();
currentXHR = xhr;
xhr.open('POST', '/upload');
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
const p = Math.round(e.loaded / e.total * 100);
bar.style.width = p + '%';
pct.innerText = p + '%';
}
};
xhr.onerror = () => {
txt.innerText = 'Error: Network failure';
txt.classList.add('text-red-500');
fileInput.value = '';
};
xhr.onload = () => {
if (xhr.status !== 200) {
txt.innerText = 'Error: ' + xhr.status;
txt.classList.add('text-red-500');
fileInput.value = '';
return;
}
const res = JSON.parse(xhr.responseText);
videoId = res.video_id;
txt.innerText = 'Extracting Metadata...';
bar.style.width = '100%';
pct.innerText = '100%';
fetch('/config/' + videoId)
.then(r => r.json())
.then(cfg => {
runConfig = cfg;
runConfig.conf = 0.12;
runConfig.iou = 0.60;
txt.innerText = 'Initialization Complete';
fileInput.value = '';
setTimeout(() => showStep('draw'), 800);
})
.catch(e => {
txt.innerText = 'Metadata Failed';
txt.classList.add('text-red-500');
fileInput.value = '';
});
};
xhr.send(form);
}
// Draw Canvas Logic
const canvas = document.getElementById('drawing-canvas');
const ctx = canvas.getContext('2d');
let points = [];
let imgNatW = 0, imgNatH = 0;
function loadFirstFrame() {
const img = document.getElementById('frame-img');
img.src = '/first-frame/' + videoId;
img.onload = () => {
imgNatW = img.naturalWidth;
imgNatH = img.naturalHeight;
img.style.display = 'block';
document.getElementById('frame-placeholder').style.display = 'none';
initCanvas();
};
}
function initCanvas() {
if (canvas) {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
}
window.addEventListener('resize', initCanvas);
if (canvas) {
canvas.addEventListener('mousedown', e => {
if (points.length >= 2) return;
const rect = canvas.getBoundingClientRect();
const cx = e.clientX - rect.left;
const cy = e.clientY - rect.top;
const rx = (cx / canvas.width) * imgNatW;
const ry = (cy / canvas.height) * imgNatH;
points.push({ cx, cy, rx: Math.round(rx), ry: Math.round(ry) });
drawDot(cx, cy);
if (points.length === 2) drawLine();
});
}
function drawDot(x, y) {
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#c89a6c';
ctx.fill();
ctx.strokeStyle = '#f0ece6';
ctx.lineWidth = 2;
ctx.stroke();
}
function drawLine() {
ctx.beginPath();
ctx.moveTo(points[0].cx, points[0].cy);
ctx.lineTo(points[1].cx, points[1].cy);
ctx.strokeStyle = '#c89a6c';
ctx.lineWidth = 3;
ctx.stroke();
}
function resetCanvas() {
points = [];
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function startRun() {
if (points.length < 2) return;
const line = [[points[0].rx, points[0].ry], [points[1].rx, points[1].ry]];
sessionStorage.setItem('funky_run', JSON.stringify({
video_id: videoId,
line: line,
config: runConfig
}));
window.location.href = '/';
}
</script>
<!-- Onboarding Walkthrough -->
<div id="onboard-overlay" class="onboard-overlay" style="display:none">
<div class="onboard-card">
<div class="onboard-step active" data-step="0">
<i class="fa-solid fa-cloud-arrow-up text-4xl mb-4" style="color:var(--cocoa-l)"></i>
<h3 class="text-lg font-bold mb-2" style="color:#f0ece6">Upload a Traffic Video</h3>
<p class="text-xs" style="color:#777;line-height:1.7">Drag & drop or select a video file recorded from any traffic camera. MP4, MOV, AVI formats supported.</p>
</div>
<div class="onboard-step" data-step="1">
<i class="fa-solid fa-draw-polygon text-4xl mb-4" style="color:var(--cocoa-l)"></i>
<h3 class="text-lg font-bold mb-2" style="color:#f0ece6">Draw a Counting Boundary</h3>
<p class="text-xs" style="color:#777;line-height:1.7">Click two points on the first frame to define a spatial boundary. Vehicles crossing this line will be counted and classified.</p>
</div>
<div class="onboard-step" data-step="2">
<i class="fa-solid fa-chart-line text-4xl mb-4" style="color:var(--cocoa-l)"></i>
<h3 class="text-lg font-bold mb-2" style="color:#f0ece6">Review Analytics & Export</h3>
<p class="text-xs" style="color:#777;line-height:1.7">Watch real-time charts populate as inference runs. Download annotated video, reports, and structured JSON when complete.</p>
</div>
<div class="onboard-dots">
<span class="onboard-dot active"></span>
<span class="onboard-dot"></span>
<span class="onboard-dot"></span>
</div>
<div class="flex gap-3 justify-center mt-6">
<button onclick="closeOnboarding()" class="text-[10px] font-bold uppercase tracking-widest px-4 py-2 rounded-full" style="color:#555;border:1px solid #222">Skip</button>
<button id="onboard-next" onclick="nextOnboardStep()" class="text-[10px] font-bold uppercase tracking-widest px-6 py-2 rounded-full" style="background:var(--cocoa);color:#f0ece6">Next</button>
</div>
</div>
</div>
<script>
let _obStep = 0;
function nextOnboardStep() {
_obStep++;
if (_obStep >= 3) { closeOnboarding(); return; }
document.querySelectorAll('.onboard-step').forEach((s, i) => s.classList.toggle('active', i === _obStep));
document.querySelectorAll('.onboard-dot').forEach((d, i) => d.classList.toggle('active', i === _obStep));
if (_obStep === 2) document.getElementById('onboard-next').innerText = 'Get Started';
}
function closeOnboarding() {
document.getElementById('onboard-overlay').style.display = 'none';
}
// Show onboarding on every page load
document.getElementById('onboard-overlay').style.display = 'flex';
</script>
</body>
</html>