"use client"; import { ArrowLeft, ArrowRight, Check, CircleHelp, LoaderCircle, MessageSquareText, X, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useCallback, useEffect, useRef, useState } from "react"; import { AgentDisclosure } from "@/components/agents/agent-disclosure"; import { ActionSwapRollText } from "@/components/motion/action-swap-roll"; import { Button } from "@/components/motion/button"; import { Checkbox } from "@/components/motion/checkbox"; import { Input } from "@/components/motion/input"; import { RadioGroup, RadioGroupItem } from "@/components/motion/radio"; import { EASE_OUT, SPRING_SWAP } from "@/lib/ease"; import { cn } from "@/lib/utils"; import type { ApprovalCardAnswer, ApprovalCardAnswers, ApprovalCardProps, ApprovalCardQuestion, ApprovalCardStatus, } from "./types"; export type { ApprovalCardAnswer, ApprovalCardAnswers, ApprovalCardOption, ApprovalCardProps, ApprovalCardQuestion, ApprovalCardStatus, } from "./types"; const EMPTY_ANSWER: ApprovalCardAnswer = { selected: [], custom: "" }; function getStatusLabel(status: ApprovalCardStatus) { if (status === "submitting") return "Submitting"; if (status === "approved") return "Approved"; if (status === "rejected") return "Rejected"; if (status === "changes-requested") return "Changes requested"; if (status === "answered") return "Response submitted"; return "Input required"; } function getStatusClass(status: ApprovalCardStatus) { if (status === "approved" || status === "answered") { return "text-emerald-600 dark:text-emerald-400"; } if (status === "rejected") return "text-rose-600 dark:text-rose-400"; if (status === "changes-requested") { return "text-amber-600 dark:text-amber-400"; } return "text-muted-foreground"; } function getStatusBadgeClass(status: ApprovalCardStatus) { if (status === "pending" || status === "changes-requested") { return "border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400"; } if (status === "submitting") { return "border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400"; } if (status === "approved" || status === "answered") { return "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"; } return "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400"; } function isAnswered(answer: ApprovalCardAnswer) { return answer.selected.length > 0 || Boolean(answer.custom?.trim()); } function QuestionOptions({ question, answer, disabled, onChange, onSingleSelect, }: { question: ApprovalCardQuestion; answer: ApprovalCardAnswer; disabled: boolean; onChange: (answer: ApprovalCardAnswer) => void; onSingleSelect?: () => void; }) { const custom = answer.custom ?? ""; return (
{question.options?.length ? ( question.multiple ? (
{question.options.map((option) => ( onChange({ ...answer, selected: checked ? [...answer.selected, option.value] : answer.selected.filter((value) => value !== option.value), }) } className="min-h-9 rounded-lg px-1.5 py-1" /> ))}
) : ( { onChange({ selected: [value], custom: "" }); onSingleSelect?.(); }} className="gap-0.5" > {question.options.map((option) => ( ))} ) ) : null} {question.allowCustom ? ( onChange({ selected: question.multiple ? answer.selected : [], custom: value, }) } className={cn("p-0.5", question.options?.length && "mt-1.5")} classNames={{ field: "h-10 rounded-xl border-0 bg-background/70 focus-within:bg-background", input: "px-3 text-sm", }} /> ) : null}
); } function ProgressDots({ current, ids }: { current: number; ids: string[] }) { return ( Question {current + 1} of {ids.length} {ids.map((id, index) => ( ); } export function ApprovalCard({ title = "Approval required", description, children, questions = [], status = "pending", answers, defaultAnswers = {}, onAnswersChange, step, defaultStep = 0, onStepChange, onSubmit, onApprove, onReject, onRequestChanges, onDismiss, approveLabel = "Approve", submitLabel = "Submit response", result, className, }: ApprovalCardProps) { const reduce = useReducedMotion() ?? false; const [internalAnswers, setInternalAnswers] = useState(defaultAnswers); const [internalStep, setInternalStep] = useState(defaultStep); const autoAdvanceTimer = useRef(undefined); const currentAnswers = answers ?? internalAnswers; const currentStep = Math.min( Math.max(0, step ?? internalStep), Math.max(0, questions.length - 1), ); const question = questions[currentStep]; const questionMode = questions.length > 0; const pending = status === "pending"; const busy = status === "submitting"; const interactive = pending || busy; const currentAnswer = question ? (currentAnswers[question.id] ?? EMPTY_ANSWER) : EMPTY_ANSWER; const displayTitle = question?.title ?? title; const titleKey = question?.id ?? String(status); const statusLabel = getStatusLabel(status); const clearAutoAdvance = useCallback(() => { if (autoAdvanceTimer.current === undefined) return; window.clearTimeout(autoAdvanceTimer.current); autoAdvanceTimer.current = undefined; }, []); useEffect(() => clearAutoAdvance, [clearAutoAdvance]); const setAnswers = useCallback( (next: ApprovalCardAnswers) => { if (answers === undefined) setInternalAnswers(next); onAnswersChange?.(next); }, [answers, onAnswersChange], ); const setStep = (next: number) => { clearAutoAdvance(); if (step === undefined) setInternalStep(next); onStepChange?.(next); }; const updateCurrentAnswer = (next: ApprovalCardAnswer) => { if (!question) return; setAnswers({ ...currentAnswers, [question.id]: next }); }; const continueQuestion = () => { if (currentStep < questions.length - 1) { setStep(currentStep + 1); return; } onSubmit?.(currentAnswers); }; const queueAutoAdvance = () => { if ( !question || question.multiple || question.autoAdvance === false || currentStep >= questions.length - 1 || busy ) { return; } clearAutoAdvance(); autoAdvanceTimer.current = window.setTimeout(() => { setStep(currentStep + 1); }, 240); }; return (

{displayTitle}

{questionMode && interactive ? ( {currentStep + 1}/{questions.length} ) : ( {statusLabel} )} {onDismiss ? ( ) : null}
{questionMode && question ? ( {question.description ? (

{question.description}

) : null}
) : (
{description ? (

{description}

) : null} {children ?
{children}
: null}
)} {questionMode ? (
item.id)} />
) : (
{onRequestChanges ? ( ) : null} {onReject ? ( ) : null}
)}
{!interactive ? (

{result ?? statusLabel}

) : null}
); }