import { useEffect, useMemo, useState } from "react"; import { Pause, Play, RotateCcw, SkipForward } from "lucide-react"; import { FLOW_ORDER, FRAMES, LEARNING_ORDER, PLANES, RUNTIME_ORDER, SAFETY_ORDER, STAGES, type StageId, } from "../../data/autonomy"; import { FlowArrow, StageCard } from "./StageCard"; import { StageDetail } from "./StageDetail"; type Props = { openStage: StageId | null; onOpenStage: (id: StageId | null) => void; }; const PHASES: { key: string; label: string; stages: StageId[] }[] = [ { key: "sense", label: "Sense", stages: ["sensors", "fusion"] }, { key: "understand", label: "Understand", stages: ["perception"] }, { key: "predict", label: "Predict", stages: ["prediction"] }, { key: "decide", label: "Decide", stages: ["behavior", "motion"] }, { key: "act", label: "Act", stages: ["control", "actuators"] }, { key: "verify", label: "Verify", stages: ["supervisor"] }, { key: "learn", label: "Learn", stages: ["capture"] }, ]; const TICK_MS = 1500; const CYCLE: StageId[] = [...RUNTIME_ORDER, "world"]; export function AutonomyStack({ openStage, onOpenStage }: Props) { const [playing, setPlaying] = useState(true); const [tick, setTick] = useState(0); useEffect(() => { if (!playing) return; const id = window.setInterval(() => setTick((t) => t + 1), TICK_MS); return () => window.clearInterval(id); }, [playing]); const cycle = Math.floor(tick / CYCLE.length); const activeRuntime = CYCLE[tick % CYCLE.length]!; const frame = FRAMES[cycle % FRAMES.length]!; const activeLearning = LEARNING_ORDER[Math.floor(tick / 3) % LEARNING_ORDER.length]!; const overriding = frame.safety === "override"; const gating = activeRuntime === "control" || activeRuntime === "actuators"; const phase = useMemo( () => PHASES.find((item) => item.stages.includes(activeRuntime))?.key ?? "sense", [activeRuntime], ); const activeIds = useMemo(() => { const set = new Set([activeRuntime, activeLearning]); if (gating || overriding) set.add("supervisor"); if (overriding) set.add("fallback"); return set; }, [activeLearning, activeRuntime, gating, overriding]); const step = () => { setPlaying(false); setTick((t) => t + 1); }; const detail = openStage ? STAGES[openStage] : null; const detailIndex = openStage ? FLOW_ORDER.indexOf(openStage) : -1; return (

Closed loop

    {PHASES.map((item, index) => (
  1. {item.label} {index < PHASES.length - 1 ? ( → ) : null}
  2. ))}

An autonomy stack is not a pipeline that ends. Every action changes the world, the sensors measure the change, and the loop runs again — typically ten to twenty times a second.

Scenario · {frame.label} · cycle {cycle + 1}

{frame.headline}

{overriding ? "Safety override" : "Nominal"}

Autonomy runtime

onOpenStage("mission")} /> {RUNTIME_ORDER.map((id, index) => (
onOpenStage(id)} /> {index === RUNTIME_ORDER.length - 1 ? null : ( )}
))} onOpenStage("world")} />

↺ The world feeds the sensor layer again. This edge is what turns a perception model into a control system.

Safety & supervision

Sits between the planner and the actuators with independent authority to say no.

{SAFETY_ORDER.map((id) => ( onOpenStage(id)} /> ))}

Data & learning plane

Runs offline, on fleet time rather than frame time. This is the autonomy development stack.

{frame.capture ? (

event captured · {frame.capture}

) : null}
{LEARNING_ORDER.map((id) => ( onOpenStage(id)} /> ))}

↺ Release feeds the runtime. The autonomy stack drives the car; this loop is what makes next month's car better.

Six interacting planes

{(Object.keys(PLANES) as (keyof typeof PLANES)[]).map((key) => (

{PLANES[key].label}

{PLANES[key].blurb}

))}

There is no single "AI" box. Learning shows up in perception, fusion, the world model, prediction, behavior, motion costs, simulation, and scenario mining — while safety stays deliberately deterministic around all of it.

{detail ? ( onOpenStage(null)} /> ) : null}
); }