Skip to content
Tier

Proposal

Pro

One action an agent proposes, with the alternatives it weighed beside it. Each option shows how sure the agent is through ConfidenceMark, with the basis on hover and for screen readers, and unknown when the agent didn't say. The proposal is marked, but nothing is chosen for the person: Accept (and Edit) say they need a choice until the person picks one, the proposal or an alternative, and Accept then calls back with that option. With onEdit, Edit hands the chosen option to the app to change before it is accepted; with onDecline, "None of these" turns them all down. Once accepted or declined, the options stay on screen with the chosen one marked, and a polite status line says what the person did.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0 and 2.9.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/proposal

Brings in: ai-content-label, bidi, button, confidence-mark, heading-level, heading-scope, know, locale, locked, radio-group, tokens, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { Proposal, type ProposalOption } from "@/components/candor/proposal"

const proposed: ProposalOption = {
  id: "refund-duplicate",
  label: "Refund the duplicate charge",
  detail: "Returns $84.20 to the card the second payment came from. The first payment stands.",
  reason: "Both charges have the same amount, card and basket, 40 seconds apart.",
  confidence: { value: "high", basis: "The payment service lists both charges with one basket id" },
}

const alternatives: ProposalOption[] = [
  {
    id: "credit",
    label: "Add $84.20 as store credit",
    detail: "Nothing goes back to the card; the customer can spend it on the next order.",
    reason: "Faster to settle, but the customer asked for their money back.",
    confidence: { value: "low", basis: "Nothing in the conversation says the customer would accept credit" },
  },
  {
    id: "ask-bank",
    label: "Ask the customer for a bank statement first",
    reason: "Would confirm the second charge settled, at the cost of a day or two.",
  },
]

export default function Example() {
  const [log, setLog] = React.useState("")
  return (
    <div className="flex max-w-xl flex-col gap-2">
      {/* Nothing is chosen until the person chooses: Accept says so until they pick the proposal or an alternative. */}
      <Proposal
        agent="Billing agent"
        title="Order #4471 was charged twice"
        proposed={proposed}
        alternatives={alternatives}
        onAccept={(o) => setLog(`Accepted: ${o.id}`)}
        onEdit={(o) => setLog(`Editing: ${o.id}`)}
        onDecline={() => setLog("Declined")}
      />
      <p className="text-xs text-muted-foreground">{log || "Nothing decided yet."}</p>
    </div>
  )
}

<Proposal>

PropTypeDefaultDescription
proposedrequiredProposalOption—The action the agent proposes. It is marked, never pre-selected.
alternativesrequiredProposalOption[]—The other actions the agent weighed, in the order it ranked them.
onAcceptrequired(option: ProposalOption) => void—Called with the option the person chose when they press Accept. Carry it out, or send it to the agent; the component changes nothing itself.
onEdit((option: ProposalOption) => void) | undefined—Adds Edit, when the app lets the person change an option before accepting it. Called with the chosen option; open it in your editor.
onDecline(() => void) | undefined—Adds "None of these"; called when the person turns every option down.
agentstring | undefined—The agent that proposes it, named by the app ("Billing agent"). Never model text.
titlestring | undefined—What the decision is about, in the app's words ("Order #4471 was charged twice"). Defaults to "Proposed action".
lockedstring | null | undefined—Why the person can't decide (they can view but not act, say). Accept, Edit and "None of these" stay visible, locked, and say so: declining is a decision too. `null` or left out: unlocked.
headingLevelHeadingLevel | undefined—The heading's level. Defaults to one below the nearest `HeadingScope`, else 2.
classNamestring | undefined—Extra classes for the outer element.

Strings

The fixed words, in English by default. Change any of them under proposal in <LocaleProvider strings={{ … }}>; the table is exported as proposalStrings.

KeyEnglish
headingProposed action
proposes{agent} proposes
proposesUnnamedThe agent proposes
proposedProposed
alternativesAlternatives it weighed
chooseChoose an action
chooseFirstChoose one of the options first.
reasonLabelIts reasoning, not checked:
noBasisThe agent didn’t say how sure it is.
acceptAccept
editEdit
declineNone of these
untouchedNothing is chosen until you choose.
chosenChosen
acceptedYou accepted “{label}”.
acceptedAlternativeYou accepted “{label}”, an alternative to the proposal.
declinedYou declined every option.
editing“{label}” is open for you to edit.

Motion

MomentFamilyWhat it does
tinttintAn option's border and background change colour when the pointer is over it and when the person chooses it, and the radio's ring fades in on keyboard focus.

Set with --cd-motion-proposal-<moment>-* in your brand file; the families are in the design reference.

Types

ProposalOption
One action the agent weighed. Model output: every word is shown as text, never as HTML or a link.
{ /** Stable for this option; Accept and Edit call back with the option. */ id: string; /** The action in plain words: "Refund the order in full". */ label: string; /** What it would do, in a sentence: "Returns $84.20 to the card it was paid with." */ detail?: string | undefined; /** Why the agent ranks it where it does, in its own words. It is the agent's account, not a checked fact, and is shown after "Its reasoning, not checked:". */ reason?: string | undefined; /** How sure the agent is that this is the right action, and what that rests on. Left out, it shows as unknown, with the agent giving no basis. */ confidence?: { value: Confidence; basis: string; } | undefined; }

Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

HeadingLevel from @/lib/candor/heading-level
A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6
Confidence from @/lib/candor/know
"high" | "medium" | "low" | "unknown"