# Inline suggestion

An agent's proposed value beside a normal field: the field keeps its own value, and the suggestion sits under it, labelled as AI-generated, with Accept, Edit and Reject. Nothing is accepted for the person: the field changes only when they press Accept (the value goes in as it is) or Edit (the value goes in and focus moves to the field, for them to change before they save). Reject leaves the field as it was. After Accept focus moves to the field too, and after Reject back to the control that had it before the suggestion's buttons (the field when there was none), so the keyboard isn't left on the page. Each outcome is said in a polite status line, and the suggestion leaves once it is decided; a suggestion with a new `id` shows again.

Wrap an `Input` or `Textarea` directly: InlineSuggestion adds `aria-describedby` to it. A `Select` renders no element of its own, so pass a function as `children` and spread what it gets onto the `SelectTrigger`. The app writes the value into its own state in `onDecide`; this component never sets the field.

- Registry name: `inline-suggestion` (`@candor/inline-suggestion`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/inline-suggestion

## Install

```bash
npx candor-ui add inline-suggestion
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/inline-suggestion`.
It brings in: ai-content-label, button, locale, locked, motion, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { InlineSuggestion, type FieldSuggestion, type SuggestionDecision } from "@/components/candor/inline-suggestion"
import { Input } from "@/components/candor/ui/input"
import { Textarea } from "@/components/candor/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/candor/ui/select"

const company: FieldSuggestion = { id: "s-company", value: "Northwind Traders Ltd", reason: "From the signed order form, page 1." }
const notes: FieldSuggestion = { id: "s-notes", value: "Delivery to the loading bay at the rear.\nCall 30 minutes ahead." }
const region: FieldSuggestion = { id: "s-region", value: "eu-west", display: "Europe (West)", reason: "The billing address is in Dublin." }

export default function Example() {
  // The fields start with what the person has; the agent's values wait under them until they decide.
  const [name, setName] = React.useState("Northwind")
  const [note, setNote] = React.useState("")
  const [area, setArea] = React.useState("")
  const take = (set: (v: string) => void) => (decision: SuggestionDecision, s: FieldSuggestion) => {
    if (decision !== "rejected") set(s.value)
  }
  return (
    <div className="flex max-w-md flex-col gap-5">
      <div className="flex flex-col gap-1.5">
        <label htmlFor="example-company" className="text-sm font-medium">
          Company name
        </label>
        <InlineSuggestion suggestion={company} agent="Intake agent" current={name} onDecide={take(setName)}>
          <Input id="example-company" value={name} onChange={(e) => setName(e.target.value)} />
        </InlineSuggestion>
      </div>
      <div className="flex flex-col gap-1.5">
        <label htmlFor="example-notes" className="text-sm font-medium">
          Delivery notes
        </label>
        <InlineSuggestion suggestion={notes} agent="Intake agent" current={note} onDecide={take(setNote)}>
          <Textarea id="example-notes" value={note} onChange={(e) => setNote(e.target.value)} />
        </InlineSuggestion>
      </div>
      <div className="flex flex-col gap-1.5">
        <span id="example-region" className="text-sm font-medium">
          Data region
        </span>
        <InlineSuggestion suggestion={region} agent="Intake agent" current={area} onDecide={take(setArea)} locked="Only an admin can set the data region.">
          {(field) => (
            <Select value={area} onValueChange={setArea}>
              <SelectTrigger aria-labelledby="example-region" className="w-56" {...field}>
                <SelectValue placeholder="Choose a region" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="eu-west">Europe (West)</SelectItem>
                <SelectItem value="us-east">United States (East)</SelectItem>
              </SelectContent>
            </Select>
          )}
        </InlineSuggestion>
      </div>
    </div>
  )
}
```

## <InlineSuggestion>

Props:

- `suggestion?: FieldSuggestion | undefined`: The proposed value. Leave it out when there's nothing to suggest: the field renders alone.
- `onDecide: (decision: SuggestionDecision, suggestion: FieldSuggestion) => void`: Called when the person decides: `"accepted"` and `"edited"` with the value to put in the field (for `"edited"`, focus moves to the field once you have), `"rejected"` with the field left as it is. Record the decision; the component never changes the field itself.
- `agent?: string | undefined`: The agent that proposed it, named by the app ("Intake agent"). Never model text. Left out, the line says "Suggested".
- `current?: string | undefined`: What the field holds now. When it already equals the suggestion, Accept and Edit are left out and the line says so.
- `locked?: string | undefined`: Why the person can't take the value (the field is read-only to them, say). Accept and Edit stay visible and say so; Reject still works.
- `children: React.ReactElement<{ "aria-describedby"?: string | undefined }> | ((field: SuggestionFieldProps) => React.ReactNode)`: The field: an `Input` or `Textarea` element, or a function that gets the props to spread on a control (a `SelectTrigger`).
- `className?: string | undefined`

## Strings (`inlineSuggestion`)

The fixed words, English by default. Override any of them under `inlineSuggestion` in `<LocaleProvider strings={{ inlineSuggestion: { … } }}>`; the table is exported as `inlineSuggestionStrings`.

- `region`: Suggested value
- `suggests`: {agent} suggests
- `suggestsUnnamed`: Suggested
- `emptyValue`: Leave this field empty
- `same`: The field already holds this value.
- `accept`: Accept
- `edit`: Edit
- `reject`: Reject
- `untouched`: The field keeps its own value until you accept.
- `accepted`: Accepted. The field now holds the suggested value.
- `edited`: The suggested value is in the field for you to change.
- `rejected`: Rejected. The field is unchanged.

## Types

### FieldSuggestion

An agent's proposed value for one field. Model output: it is shown as text, never as HTML or a link.

```ts
{
  /** Stable for this suggestion; a new id is a new suggestion, shown again even after the last one was decided. */
  id: string;
  /** The value the agent proposes, as the field would hold it. For a select, the option's value. */
  value: string;
  /** How to show `value`, when that differs from the value itself: a select option's label, a formatted date. */
  display?: string | undefined;
  /** Why the agent proposes it, in its own words or the app's ("From the signed order form"). Shown as plain text. */
  reason?: string | undefined;
}
```

### SuggestionDecision

What the person did with a suggestion.

```ts
"accepted" | "edited" | "rejected"
```

### SuggestionFieldProps

Props for the field InlineSuggestion wraps, when `children` is a function: spread them onto the control.

```ts
{
  /** Points the control at the suggestion while one is waiting, so it is read with the field. */
  "aria-describedby"?: string | undefined;
}
```

## Motion

- **suggestion** (arrive, rise): A suggestion that comes in after the field was on screen fades and rises into place under the field. One already there when the field first draws is still, and nothing moves while the person types.

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

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
