25 / 59 · Agents
Recommendation card
The agent proposes a change with its confidence, and waits for a yes.
Enable draft autosave?
Threads lose their draft on switch. Wiring runtime.drafts fixes it with 3 lines and no migration.
high confidence
Installation
npx shadcn@latest add "@assistant-ui/elements-recommendation-card"
Usage
import { RecommendationCard } from "@/components/elements/recommendation-card";
<RecommendationCard
state="idle"
question="Enable draft autosave?"
confidenceLabel="high confidence"
acceptedLabel="Enabled for every thread"
onAccept={() => accept()}
>
Wire{" "}
<span className="bg-foreground/[0.06] text-foreground/70 rounded-md px-1.5 py-0.5 font-mono text-[11px]">
runtime.drafts
</span>{" "}
with three lines.
</RecommendationCard>Props
state*RecommendationStateidle shows accept controls; accepted swaps in a confirmation row.
question*stringProposal headline the agent is asking the user about.
children*ReactNodeSupporting body explaining the recommendation.
confidenceLabel*stringConfidence copy shown next to the bar meter while idle.
acceptedLabel*stringConfirmation copy shown once the proposal is accepted.
onAccept() => voidCalled when the user clicks Accept.
onAlternatives() => voidCalled when the user clicks Alternatives.
classNamestringExtra classes merged onto the root.
Source
recommendation-card.tsx"use client";
import type { ReactNode } from "react";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { inkButton, mono, paper } from "./surfaces";
export type RecommendationState = "idle" | "accepted";
const CONFIDENCE_BARS = [0, 1, 2];
export function RecommendationCard({
state,
question,
children,
confidenceLabel,
acceptedLabel,
onAccept,
onAlternatives,
className,
}: {
state: RecommendationState;
question: string;
children: ReactNode;
confidenceLabel: string;
acceptedLabel: string;
onAccept?: () => void;
onAlternatives?: () => void;
className?: string;
}) {
return (
<div
className={cn(
paper,
"flex w-full max-w-sm flex-col gap-3 rounded-[20px] p-4",
className,
)}
>
<p className="text-sm font-medium">{question}</p>
<p className="text-foreground/55 text-[13px] leading-relaxed">
{children}
</p>
<div className="flex h-8 items-center justify-between">
{state === "idle" ? (
<>
<div className="flex items-center gap-2">
<span className="flex items-end gap-0.5" aria-hidden>
{CONFIDENCE_BARS.map((bar) => (
<span
key={bar}
className="w-1 rounded-full bg-emerald-500/70"
style={{ height: 6 + bar * 3 }}
/>
))}
</span>
<span className={cn(mono, "text-foreground/40")}>
{confidenceLabel}
</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onAlternatives}
className="text-foreground/55 hover:bg-foreground/[0.06] hover:text-foreground/90 h-8 rounded-full px-3.5 text-xs font-medium transition-[background-color,color,scale] duration-150 active:scale-[0.96]"
>
Alternatives
</button>
<button
type="button"
onClick={onAccept}
className={cn(
inkButton,
"flex h-8 items-center rounded-full px-3.5 text-xs font-medium",
)}
>
Accept
</button>
</div>
</>
) : (
<div
key="accepted"
className="fade-in animate-in text-foreground/55 flex items-center gap-2 text-xs duration-300"
>
<CheckIcon className="size-3.5 text-emerald-500" />
{acceptedLabel}
</div>
)}
</div>
</div>
);
}