Elements

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>
  );
}