"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 (
{busy ? (
) : interactive ? (
questionMode ? (
) : (
)
) : status === "rejected" ? (
) : (
)}
{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}
);
}