"use client"; import { CheckIcon, CornerDownLeftIcon, HandIcon, MessageSquareIcon, PlayIcon, XIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type FormEvent, useEffect, useId, useReducer, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export interface BackseatStep { id: string; title: string; detail?: string; } export interface BackseatProps { steps: BackseatStep[]; agent?: string; goal?: string; autoNote?: string; loop?: boolean; className?: string; } interface Item { id: string; kind: "step" | "note"; title: string; detail?: string; } interface State { items: Item[]; active: number; typed: number; hold: number; queue: { id: string; text: string }[]; paused: boolean; finished: boolean; run: number; } type Action = | { type: "tick" } | { type: "queue"; text: string } | { type: "unqueue"; id: string } | { type: "interrupt" } | { type: "resume" } | { type: "reset"; steps: BackseatStep[] }; let noteSeq = 0; const press = "transition-[scale,box-shadow,background-color] duration-150 ease-out active:scale-[0.97]"; function init(steps: BackseatStep[], run = 0): State { return { items: steps.map((s) => ({ id: s.id, kind: "step", title: s.title, detail: s.detail })), active: 0, typed: 0, hold: 0, queue: [], paused: false, finished: false, run, }; } function applyQueue(state: State, at: number): Item[] { const notes: Item[] = state.queue.map((q) => ({ id: q.id, kind: "note", title: q.text })); const items = [...state.items]; items.splice(at, 0, ...notes); return items; } function useReducedMotionAfterMount() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && !!prefers; } function reducer(state: State, action: Action): State { switch (action.type) { case "tick": { if (state.paused || state.finished) return state; const current = state.items[state.active]; if (!current) return { ...state, finished: true }; const length = current.detail?.length ?? 0; if (state.typed < length) return { ...state, typed: Math.min(length, state.typed + 1) }; if (state.hold < 22) return { ...state, hold: state.hold + 1 }; const items = state.queue.length ? applyQueue(state, state.active + 1) : state.items; let next = state.active + 1 + state.queue.length; while (next < items.length && items[next].kind === "note") next++; return { ...state, items, queue: [], active: next, typed: 0, hold: 0, finished: next >= items.length, }; } case "queue": return { ...state, queue: [...state.queue, { id: `note-${state.run}-${++noteSeq}`, text: action.text }] }; case "unqueue": return { ...state, queue: state.queue.filter((q) => q.id !== action.id) }; case "interrupt": return state.finished ? state : { ...state, paused: true }; case "resume": { if (!state.queue.length) return { ...state, paused: false }; const items = applyQueue(state, state.active); return { ...state, items, queue: [], paused: false, active: state.active + state.queue.length, typed: 0, hold: 0 }; } case "reset": return init(action.steps, state.run + 1); } } export function Backseat({ steps, agent = "Agent", goal, autoNote, loop = true, className, }: BackseatProps) { const reduce = useReducedMotionAfterMount(); const [state, dispatch] = useReducer(reducer, steps, (s) => init(s)); const [draft, setDraft] = useState(""); const [focused, setFocused] = useState(false); const inputId = useId(); const touched = useRef(false); const autoRun = useRef(-1); useEffect(() => { const id = setInterval(() => dispatch({ type: "tick" }), 40); return () => clearInterval(id); }, []); useEffect(() => { if (!state.finished || !loop) return; const id = setTimeout(() => dispatch({ type: "reset", steps }), 4500); return () => clearTimeout(id); }, [state.finished, loop, steps]); const timers = useRef[]>([]); const focusedRef = useRef(false); useEffect(() => { focusedRef.current = focused; }, [focused]); useEffect(() => () => timers.current.forEach(clearTimeout), []); useEffect(() => { if (!autoNote || touched.current || state.active !== 1 || autoRun.current === state.run) return; autoRun.current = state.run; const gap = reduce ? 0 : 35; for (let i = 1; i <= autoNote.length; i++) { timers.current.push( setTimeout(() => { if (touched.current || focusedRef.current) return; setDraft(autoNote.slice(0, i)); }, 400 + i * gap), ); } timers.current.push( setTimeout(() => { if (touched.current || focusedRef.current) return; dispatch({ type: "queue", text: autoNote }); setDraft(""); }, 800 + autoNote.length * gap), ); }, [autoNote, state.active, state.run, reduce]); const submit = (e: FormEvent) => { e.preventDefault(); const text = draft.trim(); if (!text) return; touched.current = true; dispatch({ type: "queue", text }); setDraft(""); }; const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 340, damping: 30 }; const statusLabel = state.finished ? "Done" : state.paused ? "Interrupted" : "Working"; return (
{agent.slice(0, 1)}

{agent} {statusLabel}

{goal &&

{goal}

}
{state.paused ? ( ) : ( )}
    {state.items.map((item, i) => { const status = i < state.active ? "done" : i === state.active && !state.finished ? "active" : "todo"; if (item.kind === "note") { return (
    {item.title} Applied
    ); } const shown = status === "active" ? item.detail?.slice(0, state.typed) : item.detail; return (
  1. {item.title}

    {status === "active" && item.detail && ( {shown} {state.paused ? ( paused ) : ( )} )}
  2. ); })}
{state.queue.length > 0 && (

{state.paused ? "Sends when you resume" : "Joins the plan when the current step ends"}

    {state.queue.map((q) => (
    {q.text}
    ))}
)}
setFocused(false)} onChange={(e) => { touched.current = true; setDraft(e.target.value); }} onFocus={() => setFocused(true)} placeholder={state.finished ? "Plan finished" : `Steer ${agent}…`} value={draft} />
); } function StepIcon({ status, paused, reduce }: { status: "done" | "active" | "todo"; paused: boolean; reduce: boolean }) { return ( {status === "done" && ( )} {status === "active" && paused && } {status === "active" && !paused && ( )} {status === "todo" && } ); }