import { useEffect, useId, useLayoutEffect, useRef, useState, type Ref } from "react"; import { createPortal } from "react-dom"; import { Check, Copy, Terminal, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Textarea } from "@/components/ui/textarea"; import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion"; import { copyTextToClipboard } from "@/lib/clipboard"; import { motionMilliseconds, motionNumber } from "@/lib/onboarding-motion-tokens"; import { cn } from "@/lib/utils"; export interface AgentSetupPromptProps { /** The complete text to preview and copy; never truncated on the clipboard. */ prompt: string; title?: string; description?: string; label?: string; variant?: "outline" | "ghost"; side?: "top" | "bottom" | "left" | "right"; align?: "start" | "center" | "end"; className?: string; /** Carry over the clipboard result when generating a prompt already copied it. */ initialCopyStatus?: "idle" | "copied" | "failed"; onCopied?: () => void; /** Brand a handoff for one recipient; omit for the general agent logo stack. */ agent?: { name: string; src: string; className?: string }; } const defaultAgents = [ { name: "Codex", src: "/brands/codex-color.svg", className: "" }, { name: "ChatGPT", src: "/brands/apps/openai.svg", className: "dark:invert" }, { name: "Claude", src: "/brands/claude-color.svg", className: "" }, ]; function AgentLogos({ ref, hidden, agents }: { ref: Ref; hidden: boolean; agents: NonNullable[] }) { return ( ); } function readLogoPositions(group: HTMLSpanElement | null) { return Array.from(group?.querySelectorAll(".agent-setup-logo") ?? []).map((logo) => { const bounds = logo.getBoundingClientRect(); const transform = getComputedStyle(logo).transform; const matrix = transform && transform !== "none" ? new DOMMatrixReadOnly(transform) : null; return { // A rotated bounding box is larger than the logo. Anchor its unscaled // box at the same center, preserving the current hover angle separately. left: bounds.left + (bounds.width - logo.offsetWidth) / 2, top: bounds.top + (bounds.height - logo.offsetHeight) / 2, width: logo.offsetWidth, height: logo.offsetHeight, transform: `rotate(${matrix ? Math.atan2(matrix.b, matrix.a) * 180 / Math.PI : 0}deg)`, }; }); } /** A small handoff to the user's agent, with an inspectable prompt and local copy feedback. */ export function AgentSetupPrompt({ prompt, title = "Agent setup", description = "Paste this into your agent to take care of setup.", label = "Set up with an agent", variant = "outline", side = "top", align = "start", className, initialCopyStatus = "idle", onCopied, agent, }: AgentSetupPromptProps) { const agents = agent ? [agent] : defaultAgents; const recipient = agent ? `your ${agent.name}` : "your agent"; const id = useId(); const reducedMotion = usePrefersReducedMotion(); const [open, setOpen] = useState(false); const [status, setStatus] = useState<"idle" | "copying" | "copied" | "failed">(initialCopyStatus); const [flight, setFlight] = useState<{ origins: ReturnType; toPreview: boolean } | null>(null); const triggerLogos = useRef(null); const previewLogos = useRef(null); const flyingLogos = useRef(null); const activationOrigins = useRef | null>(null); const copyAttempt = useRef(0); const hasUserCopyAttempt = useRef(false); const copying = useRef(false); const copyButton = useRef(null); useEffect(() => { setStatus("idle"); copying.current = false; hasUserCopyAttempt.current = false; // A pending clipboard result must not label a changed prompt as copied. return () => { copyAttempt.current += 1; }; }, [prompt]); useEffect(() => { // A delayed generation-time copy must not replace the user's newer result. if (!hasUserCopyAttempt.current) setStatus(initialCopyStatus); }, [prompt, initialCopyStatus]); useEffect(() => { if (status !== "copied") return; const hold = motionMilliseconds("--agent-setup-copied-hold"); if (hold <= 0) return; const timeout = window.setTimeout(() => setStatus("idle"), hold); return () => window.clearTimeout(timeout); }, [status]); useLayoutEffect(() => { if (!flight) return; if (reducedMotion || typeof Element.prototype.animate !== "function") { setFlight(null); return; } let frame: number; let cancelled = false; let animations: Animation[] = []; const finish = () => { if (!cancelled) setFlight(null); }; const start = () => { const destination = flight.toPreview ? previewLogos.current : triggerLogos.current; // Radix publishes this measurement only after positioning its portal. // Until then the full-size flight logos remain at the captured origin. if (!destination || (flight.toPreview && !getComputedStyle(destination).getPropertyValue("--radix-popover-content-transform-origin").trim())) { frame = requestAnimationFrame(start); return; } const targets = readLogoPositions(destination); const logos = flyingLogos.current?.querySelectorAll(".agent-setup-logo"); const easing = getComputedStyle(document.documentElement).getPropertyValue("--motion-ease-out-expo").trim(); animations = Array.from(logos ?? []).map((logo, index) => logo.animate( [flight.origins[index], targets[index]].map(({ left, top, transform }) => ({ left: `${left}px`, top: `${top}px`, transform })), { duration: motionMilliseconds("--agent-setup-flight-duration"), easing, fill: "both" }, )); void Promise.all(animations.map((animation) => animation.finished)).then(finish, () => {}); }; frame = requestAnimationFrame(start); // If the viewport moves mid-flight, settle at its live destination. window.addEventListener("resize", finish); document.addEventListener("scroll", finish, true); return () => { cancelled = true; cancelAnimationFrame(frame); animations.forEach((animation) => animation.cancel()); window.removeEventListener("resize", finish); document.removeEventListener("scroll", finish, true); }; }, [flight, reducedMotion]); function changeOpen(nextOpen: boolean) { const origins = activationOrigins.current ?? readLogoPositions(flyingLogos.current ?? (open ? previewLogos.current : triggerLogos.current)); activationOrigins.current = null; setFlight(!reducedMotion && origins.length === agents.length ? { origins, toPreview: nextOpen } : null); setOpen(nextOpen); } async function copyPrompt() { if (copying.current || !prompt.trim()) return; hasUserCopyAttempt.current = true; copying.current = true; const attempt = ++copyAttempt.current; setStatus("copying"); try { await copyTextToClipboard(prompt); if (attempt === copyAttempt.current) { setStatus("copied"); onCopied?.(); } } catch { if (attempt === copyAttempt.current) setStatus("failed"); } finally { if (attempt === copyAttempt.current) copying.current = false; } } return ( <> { event.preventDefault(); copyButton.current?.focus(); }} onFocusOutside={(event) => { // The legacy clipboard fallback briefly focuses a textarea in // document.body. Keep the preview open so its result stays visible. if (copying.current) event.preventDefault(); }} >

{title}

{description}

{status === "failed" ? (