"use client"; import * as React from "react"; /** * Fake streaming engine for the marketing mocks — purely client-side, nothing is sent anywhere. * Drives one or more scripted texts character by character with a per-stream delay and speed, * and records wall-clock timings so the mock metadata (tok/s, TTFT) is internally consistent. */ export interface FakeStream { text: string; /** characters per tick (~35 ms) */ speed: number; /** ms before the first token */ delay: number; } export interface FakeStreamState { /** Characters revealed per stream. */ progress: number[]; /** ms since the first token of stream i (frozen when it completes). */ elapsedFor: (i: number) => number; /** Time to first token per stream (its scripted delay). */ ttftFor: (i: number) => number; done: (i: number) => boolean; allDone: boolean; } export function useFakeStreams(streams: FakeStream[], active: boolean, replayKey: number, instant: boolean): FakeStreamState { const [progress, setProgress] = React.useState(() => streams.map(() => 0)); const [elapsed, setElapsed] = React.useState(0); const [finishedAt, setFinishedAt] = React.useState<(number | null)[]>(() => streams.map(() => null)); React.useEffect(() => { if (!active) return; if (instant) { // Reduced motion: show the finished state right away. const t = setTimeout(() => { setProgress(streams.map((s) => s.text.length)); setFinishedAt(streams.map((s) => s.delay + 2400)); setElapsed(2400); }, 0); return () => clearTimeout(t); } const started = performance.now(); let cancelled = false; let raf = 0; let last = started; const finished: (number | null)[] = streams.map(() => null); const tick = (now: number) => { if (cancelled) return; const sinceStart = now - started; if (now - last >= 35) { last = now; setElapsed(sinceStart); setProgress((prev) => { let done = true; const next = prev.map((p, i) => { const s = streams[i]; if (sinceStart < s.delay) { done = false; return 0; } if (p >= s.text.length) return p; const jitter = Math.random() < 0.15 ? 0 : s.speed; const n = Math.min(s.text.length, p + jitter); if (n < s.text.length) done = false; else if (finished[i] === null) finished[i] = sinceStart; return n; }); if (done) cancelled = true; return next; }); setFinishedAt([...finished]); } if (!cancelled) raf = requestAnimationFrame(tick); }; const start = setTimeout(() => { setProgress(streams.map(() => 0)); setFinishedAt(streams.map(() => null)); setElapsed(0); raf = requestAnimationFrame(tick); }, 250); return () => { cancelled = true; clearTimeout(start); cancelAnimationFrame(raf); }; // replayKey intentionally restarts the effect; `streams` are module constants. // eslint-disable-next-line react-hooks/exhaustive-deps }, [active, instant, replayKey]); const elapsedFor = (i: number) => Math.max(0, (finishedAt[i] ?? elapsed) - streams[i].delay); const ttftFor = (i: number) => streams[i].delay; const done = (i: number) => progress[i] >= streams[i].text.length; const allDone = streams.every((s, i) => progress[i] >= s.text.length); return { progress, elapsedFor, ttftFor, done, allDone }; } export function approxTokens(chars: number) { return Math.round(chars / 4); } /** Which stream finishes first given the scripted delays and speeds (35 ms per tick). */ export function fastestIndex(streams: FakeStream[]): number { return streams.map((s, i) => [s.delay + (s.text.length / s.speed) * 35, i] as const).sort((a, b) => a[0] - b[0])[0][1]; }