/* ============================================================
NeuralScout — Processing Timeline (How It Works)
5 steps, horizontal, with active-step animation
============================================================ */
function Timeline() {
const [active, setActive] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => setActive(a => (a + 1) % 5), 3400);
return () => clearInterval(id);
}, []);
const steps = [
{ n: '01', title: 'RTSP feed', sub: 'Paste your stream URL — we connect in seconds.', render: () => },
{ n: '02', title: 'Draw zones of interest', sub: 'Sketch polygons on any frame: lines, regions, counters.', render: () => },
{ n: '03', title: 'Vision AI processes Stream', sub: 'Streams are processed at 30 fps with sub-100ms latency with 95% accuracy.', render: () => },
{ n: '04', title: 'See it on the dashboard', sub: 'Charts, filters, alerts — grouped by camera, zone, time.', render: () => },
{ n: '05', title: 'Ask the VLM', sub: 'Type a question. The model applies it to every frame.', render: () => },
];
return (
{/* Connecting line */}
{steps.map((s, i) => (
))}
{steps.map((s, i) => (
{s.render()}
STEP_{s.n}
{s.title}
{s.sub}
))}
);
}
/* ---- step vizes ---- */
function StepRTSP({ active }) {
const now = useTick();
const t = (now % 2200) / 2200;
const text = 'rtsp://10.0.1.44:554/stream1';
const chars = active ? Math.floor(t * text.length * 1.3) : text.length;
return (
CAMERA URL
{text.slice(0, chars)}
{active && chars < text.length && }
● CONNECTED
30 FPS · 1080p
);
}
function StepZones({ active }) {
const now = useTick();
const t = active ? (now % 3000) / 3000 : 1;
const pts1 = [[15, 45], [65, 38], [72, 82], [18, 88]];
const pts2 = [[70, 20], [130, 25], [135, 62], [75, 60]];
const drawLen1 = Math.min(1, t * 1.4);
const drawLen2 = active ? Math.max(0, Math.min(1, (t - 0.35) * 1.4)) : 1;
const makePath = (pts, frac) => {
if (frac <= 0) return '';
const n = pts.length;
const total = n;
const stop = frac * total;
let d = `M ${pts[0][0]} ${pts[0][1]}`;
for (let i = 1; i <= stop && i <= n; i++) {
const idx = i % n;
d += ` L ${pts[idx][0]} ${pts[idx][1]}`;
}
return d;
};
return (
{/* mock feed */}
);
}
function StepQuery({ active }) {
const now = useTick();
const q = 'flag anyone not wearing a hi-vis vest';
const t = active ? (now % 4000) / 4000 : 1;
const chars = active ? Math.floor(t * q.length * 1.6) : q.length;
return (
VLM · PROMPT
› {q.slice(0, chars)}
{active && chars < q.length && }
{chars >= q.length && (
3 MATCHES
active now
)}
);
}
function StepStream({ active }) {
const now = useTick();
const t = (now % 1800) / 1800;
return (
{/* Source */}
{/* Packets flowing */}
{active && [0, 0.33, 0.66].map((offset, i) => {
const p = ((t + offset) % 1);
return (
{Math.floor(p * 30)}ms
);
})}
{/* Server */}
);
}
function StepDash({ active }) {
const now = useTick();
const t = (now % 2500) / 2500;
const bars = [0.3, 0.45, 0.6, 0.75, 0.85, 0.95, 0.7];
return (
{bars.map((b, i) => (
))}
);
}
const tlStyles = {
wrap: { display: 'flex', flexDirection: 'column', gap: 32 },
track: {
position: 'relative',
display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)',
padding: '0 20px',
},
line: {
position: 'absolute', top: 14, left: 'calc(10% + 14px)', right: 'calc(10% + 14px)',
height: 2, background: 'var(--ns-line)', borderRadius: 99, overflow: 'hidden',
},
lineFill: {
height: '100%', background: 'var(--ns-accent)',
transition: 'width 0.6s var(--ns-ease)',
boxShadow: '0 0 12px var(--ns-accent)',
},
step: { display: 'flex', justifyContent: 'center', position: 'relative', zIndex: 1 },
dot: {
width: 30, height: 30, borderRadius: 99,
border: '2px solid var(--ns-line-2)',
background: 'var(--ns-bg-3)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 0.4s var(--ns-ease)',
},
grid: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 14 },
card: {
border: '1px solid var(--ns-line)',
background: 'var(--ns-bg-1)',
borderRadius: 'var(--ns-r-3)',
overflow: 'hidden',
transition: 'all 0.4s var(--ns-ease)',
},
cardViz: {
aspectRatio: '16/7',
borderBottom: '1px solid var(--ns-line)',
overflow: 'hidden',
position: 'relative',
},
cardTitle: {
fontSize: 17, margin: '2px 0 4px', fontWeight: 500,
letterSpacing: '-0.01em',
},
cardSub: {
fontSize: 13, color: 'var(--ns-fg-dim)', margin: 0, lineHeight: 1.5,
textWrap: 'pretty',
},
};
window.Timeline = Timeline;