Recommendation card
An AI recommendation with its rationale, confidence and impact.
Email the ACH trace and remittance to Northwind Freight
Sources2
import { RecommendationCard } from "@oration/canon/components/ai/recommendation-card";import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";import { CheckIcon, SparklesIcon } from "lucide-react";import * as React from "react";export function Hero() { const [state, setState] = React.useState<"idle" | "done" | "dismissed">( "idle", ); if (state === "dismissed") { return ( <div className="flex w-full max-w-md items-center justify-between rounded-xl bg-card px-4 py-3 text-left text-13 text-muted-foreground shadow-border"> Next action dismissed for this conversation. <Button type="button" variant="ghost" size="xs" onClick={() => setState("idle")} > Show again </Button> </div> ); } return ( <RecommendationCard className="w-full max-w-md text-left" icon={<SparklesIcon />} title="Email the ACH trace and remittance to Northwind Freight" rationale="Aisha Bello has called three times since Friday about INV-20931 and asked for the trace number each time. PMT-58213 went out by ACH this morning." confidence={0.86} impact={{ label: "Avoids", value: "a fourth call this week" }} sources={[ { id: "s1", title: "PMT-58213", snippet: "ACH, $20,625.00, sent Monday, Sep 28. Trace 091000019284713.", kind: "record", meta: "Payment record", }, { id: "s2", title: "Call with Aisha Bello", snippet: "Can you just email me the trace? Our bank keeps asking for it.", kind: "transcript", meta: "Call on Sep 25", }, ]} actions={ state === "done" ? ( <span className="flex items-center gap-1.5 text-13 text-success"> <CheckIcon aria-hidden="true" className="size-3.5" /> Done </span> ) : ( <> <Button type="button" variant="outline" size="sm" onClick={() => { setState("done"); toast.add({ type: "success", title: "Trace and remittance sent", description: "Emailed to ap@northwindfreight.example.", actionProps: { children: "Undo", onClick: () => setState("idle"), }, }); }} > Send trace and remittance </Button> <Button type="button" variant="ghost" size="sm" onClick={() => setState("dismissed")} > Not now </Button> </> ) } /> );}Usage#
Recommendation card proposes a next step and shows its working: a title that names the step, the rationale in plain words, a ConfidenceMeter (High confidence 86%), the expected impact in a tint well, optional sources and the actions to take it. It powers the Contact Center's next best action and Copilot's suggestions. Unlike an approval card it changes nothing by itself; the person decides whether and how to act. The common mistake is coloring confidence: it is ink, with a word beside it, never green or indigo.
When to use
- For the next best action in the Contact Center assist panel: Email the ACH trace to Northwind Freight.
- Inside a Copilot answer, when the answer ends in a suggestion the person can follow up on.
- When the suggestion is uncertain enough that how sure the model is, and why, should be visible.
- When there is a concrete impact to weigh, such as a late fee avoided or a repeat call saved.
When not to use
- For an action Copilot will perform once approved, with fields that change. Use Approval card
- For a finding with a metric and a trend, such as calls getting shorter. Use Insight card
- For the sources of an answer on their own. Use Source cards
- For a measured value such as credits used or talk ratio. Use Meter
- For a system notice that needs attention, such as an expired W-9. Use Alert
The Ink Fill Rule
The Label-Beside-Color Rule
The Tint Well Rule
The One Filled Button Rule
actions. In the assist panel and in Copilot, where the view already has a primary action, use outline and ghost.Anatomy#
Explain the discount to Halcyon
Sources1
- Icon tile. Optional 28px Well Gray tile with a 16px Slate Meta icon.
- Title. An
h3at 14px, weight 600: the step, verb first. - Confidence meter. Top right: the band word in 12px Slate Meta, a 48px by 4px ink meter and the percentage in 12px medium tabular figures. Wraps under the title when space is short.
- Rationale. 13px Slate Meta: the evidence behind the step.
- Impact well. A structured impact puts its label at the left in Slate Meta and its value at the right in medium tabular figures. Any other node renders as is in the well.
- Sources. Optional numbered source cards in a sideways-scrolling row.
- Actions. Buttons in a wrapping row with 6px gaps.
Examples#
Confidence
High at 80% and above, medium from 55%, low below. The meter stays ink in every band; the word carries the meaning.
import { ConfidenceMeter } from "@oration/canon/components/ai/recommendation-card";export function Confidence() { return ( <div className="flex flex-col items-start gap-3"> <ConfidenceMeter value={0.92} /> <ConfidenceMeter value={0.68} /> <ConfidenceMeter value={0.41} /> </div> );}Impact
Pass { label, value } for the two-column well, or any node when the impact has more than one figure.
Release Orchard Street's held invoice
Move Northwind Freight to a weekly remittance digest
- Calls avoidedabout 12 a month
- Emails sent4 instead of 18
import { RecommendationCard } from "@oration/canon/components/ai/recommendation-card";import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";export function Impact() { return ( <div className="grid w-full max-w-3xl gap-4 text-left md:grid-cols-2"> <RecommendationCard title="Release Orchard Street's held invoice" rationale="The W-9 arrived this morning and passed TIN matching. OS-4502 can join Friday's payment run." confidence={0.91} impact={{ label: "Pays on time", value: "$12,480.00" }} actions={ <Button type="button" variant="outline" size="sm" onClick={() => toast.add({ title: "Opened OS-4502" })} > Review invoice </Button> } /> <RecommendationCard title="Move Northwind Freight to a weekly remittance digest" rationale="They asked for 14 remittances by phone in September, all for invoices in the same weekly run." confidence={0.58} impact={ <ul className="flex flex-col gap-1"> <li className="flex justify-between gap-3"> <span className="text-muted-foreground"> Calls avoided </span> <span className="font-medium tabular-nums"> about 12 a month </span> </li> <li className="flex justify-between gap-3"> <span className="text-muted-foreground"> Emails sent </span> <span className="font-medium tabular-nums"> 4 instead of 18 </span> </li> </ul> } actions={ <Button type="button" variant="outline" size="sm" onClick={() => toast.add({ title: "Digest drafted for Northwind Freight", }) } > Draft the digest </Button> } /> </div> );}In a Copilot answer
The answer explains, the card proposes, and its one outline action sends the follow-up. Copilot's view already has its filled Send.
import { AssistantMessage } from "@oration/canon/components/ai/message";import { RecommendationCard } from "@oration/canon/components/ai/recommendation-card";import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";export function InCopilot() { return ( <div className="w-full max-w-xl rounded-xl bg-card p-4 text-left shadow-border"> <AssistantMessage> <div className="flex flex-col gap-3"> <p> Wen Zhou Consulting's W-9 expired in August, and two of their invoices are in Friday's run. </p> <RecommendationCard title="Request a new W-9 from Wen Zhou Consulting" rationale="Both invoices will be held on Friday without a current W-9. Wen replied to the last request within a day." confidence={0.81} impact={{ label: "Keeps on schedule", value: "$8,950.00 in 2 invoices", }} actions={ <Button type="button" variant="outline" size="sm" onClick={() => toast.add({ title: "Prompt sent", description: "Request a new W-9 from Wen Zhou Consulting", }) } > Request the W-9 </Button> } /> </div> </AssistantMessage> </div> );}States#
| State | Treatment |
|---|---|
| High confidence | 80% and above. |
| Medium confidence | 55% to 79%. |
| Low confidence | Below 55%. Values outside 0 to 1 are clamped. |
| Acted on | The card has no built-in state. The assist panel swaps its actions for a green check and Done, and offers Undo in the toast. |
| Dismissed | Also the caller's job: the assist panel removes the card on Not now. |
| Source hover and focus | Linked source cards fill Well Gray on hover and draw a 3px Focus Indigo ring at 40% on focus. |
Behavior#
- It's a static
<article>; only the buttons you pass inactionsand linked sources are interactive. confidenceis 0 to 1. The meter rounds it to a whole percentage, andconfidenceLabel(value)picks the band at 0.8 and 0.55.impactas{ label, value }renders the two-column well. Pass any other node for a custom well, or leave it out.sourcesrendersSourceCardsin the row layout, numbered from 1.- The header wraps: on a narrow card the confidence meter drops under the title instead of squeezing it.
Do and don't#
Email the ACH trace to Northwind Freight
Email the ACH trace to Northwind Freight
Content#
- Title: the step, verb first, with the object: Email the ACH trace to Northwind Freight, Offer the early-payment discount explanation.
- Rationale: one or two sentences of evidence: what was said, what the record shows. Name invoices and payments by ID.
- Impact label: a verb or short noun, Avoids, Recovers, Saves. Value: the measured effect, a fourth call this week, $760.00 in disputed discount.
- Actions: the step as a verb phrase, Send trace and remittance, plus a quiet Not now.
Accessibility#
- The card is an
<article>with anh3title, so it's reachable by heading. - The confidence meter is
role="meter"named Confidence, with the value out of 100. The band word and percentage are visible text beside it. - Source cards that link are anchors; external ones say (opens in a new tab) to screen readers.
- Keyboard reaches only the actions and linked sources, in reading order.
Design tokens#
| Token | Used for |
|---|---|
--card | Card surface |
shadow-border | Hairline lift |
--muted | Icon tile, meter track, impact well at 70% |
--foreground | Meter fill at 65%, title, impact value, percentage |
--muted-foreground | Band word, rationale, impact label |
--radius-xl | 12px card corners |
--radius-lg | 10px impact well |
API reference#
RecommendationCard
A proposed next step.
Other props spread onto Nothing. Only the props below are read..
| Prop | Type | Default | Description |
|---|---|---|---|
titleRequired | string | No default | The step, verb first. |
rationaleRequired | React.ReactNode | No default | The evidence. |
confidenceRequired | number | No default | 0 to 1. |
impact | { label: string; value: React.ReactNode } | React.ReactNode | No default | Structured for the two-column well, or any node. |
actions | React.ReactNode | No default | Buttons. At most one filled. |
icon | React.ReactNode | No default | Shown in a 28px tile. |
sources | { id: string; title: string; snippet: string; kind: "doc" | "url" | "record" | "transcript"; href?: string; score?: number; meta?: string }[] | No default | Rendered as a source card row. |
className | string | No default | Merged onto the article. |
ConfidenceMeter
The band word, an ink meter and the percentage. Works on its own.
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | number | No default | 0 to 1, clamped. |
className | string | No default | Merged onto the wrapper. |
confidenceLabel
Returns the band for a value.
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | number | No default | 0 to 1. |
returns | "High confidence" | "Medium confidence" | "Low confidence" | No default | High at 0.8 and above, medium at 0.55 and above. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
The bands are fixed at 0.8 and 0.55; a page can't tune them for a model that runs hot or cold.
There's no built-in done, dismissed or loading state. The assist panel hand-rolls Done and Not now, and Copilot passes a single outline action.
The title is always an h3.
confidenceLabel is exported but not listed in the registry.