"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
type AgentStep,
type ApprovalDecision,
ApprovalSteps,
} from "@/components/ui/approval-steps";
const settings = {
amount: "$129.00",
/** Off: the run starts at the step that asks instead of the first one. */
autoplay: true,
card: "Visa •••• 4242",
/** How long each step of the agent takes, in milliseconds. */
stepMs: 1100,
};
/** The agent's plan for a support ticket; the refund needs a person's go-ahead. */
const PLAN = [
{
id: "read",
ms: 640,
result: "“The mug arrived in pieces.” Two photos attached.",
title: "Read the ticket",
tool: "tickets.get",
},
{
id: "order",
ms: 910,
result: "Order 4821 · delivered Oct 14 · paid by card",
title: "Look up the order",
tool: "orders.get",
},
{
id: "policy",
ms: 1180,
result: "Damaged on arrival: full refund within 30 days.",
title: "Check the refund policy",
tool: "policy.search",
},
{
asks: true,
id: "refund",
ms: 1420,
result: "Refund issued, it lands in 5–10 days.",
title: "Refund the payment",
tool: "payments.refund",
},
{
id: "reply",
ms: 760,
result: "Sent: “Your refund is on its way.”",
title: "Reply to the customer",
tool: "email.send",
},
{
id: "close",
ms: 300,
result: "Ticket 1043 solved.",
title: "Close the ticket",
tool: "tickets.close",
},
];
const GATE = PLAN.findIndex((step) => step.asks);
/**
* Where the run is: the step in progress, and whether it runs, waits for you or was stopped.
* `round` grows with every new run, so a decision sent in an earlier one changes nothing.
*/
const START = { cleared: -1, cursor: 0, note: "", phase: "run", round: 0 };
const PAUSED = { ...START, cursor: GATE, phase: "wait" };
const wait = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(resolve, ms);
});
/** An agent that works through a ticket by itself and stops to ask before it refunds. */
export default function Demo(props: Partial<typeof settings>) {
const { amount, autoplay, card, stepMs } = { ...settings, ...props };
const [run, setRun] = useState(autoplay ? START : PAUSED);
const [lastAutoplay, setLastAutoplay] = useState(autoplay);
if (lastAutoplay !== autoplay) {
setLastAutoplay(autoplay);
setRun((current) => ({
...(autoplay ? START : PAUSED),
round: current.round + 1,
}));
}
const { cleared, cursor, note, phase } = run;
const round = useRef(run.round);
useEffect(() => {
round.current = run.round;
}, [run.round]);
useEffect(() => {
if (phase !== "run" || cursor >= PLAN.length) {
return;
}
const asks = cursor === GATE && cleared !== GATE;
const id = setTimeout(
() =>
setRun((current) =>
asks
? { ...current, phase: "wait" }
: { ...current, cursor: current.cursor + 1 }
),
asks ? stepMs * 0.6 : stepMs
);
return () => clearTimeout(id);
}, [cleared, cursor, phase, stepMs]);
const decide = useCallback(async (decision: ApprovalDecision) => {
const asked = round.current;
await wait(450);
setRun((current) => {
// A new run started meanwhile, or this one has moved on: the decision is for nothing here.
if (
current.round !== asked ||
current.phase !== "wait" ||
current.cursor !== GATE
) {
return current;
}
return decision.decision === "approve"
? { ...current, cleared: GATE, phase: "run" }
: { ...current, note: decision.note, phase: "stopped" };
});
}, []);
const replay = useCallback(
() => setRun((current) => ({ ...START, round: current.round + 1 })),
[]
);
const steps = PLAN.map((step, index): AgentStep => {
const base = { id: step.id, title: step.title, tool: step.tool };
if (index < cursor) {
return {
...base,
durationMs: step.ms,
result: step.result,
status: "done",
};
}
if (index > cursor) {
return { ...base, status: phase === "stopped" ? "skipped" : "queued" };
}
if (phase === "stopped") {
return { ...base, note, status: "rejected" };
}
if (phase === "wait") {
return {
...base,
approval: {
action: `Refund ${amount} to ${card}`,
params: [
{ label: "amount", value: amount },
{ label: "method", value: card },
{ label: "reason", value: "damaged_on_arrival" },
],
risk: "medium",
},
status: "approval",
};
}
return { ...base, status: "running" };
});
const isOver = phase === "stopped" || cursor >= PLAN.length;
return (
<div className="flex min-h-[640px] w-full flex-col items-center justify-center gap-4 bg-background p-6">
<ApprovalSteps
agent="Support agent"
onDecide={decide}
steps={steps}
title="Refund a damaged order"
/>
<div className="flex h-11 items-center">
{isOver ? (
<button
className="h-11 rounded-lg px-4 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={replay}
type="button"
>
Run it again
</button>
) : null}
</div>
</div>
);
}