# Dictation

A microphone button for the composer's `tools` slot: press it, speak, press it again. Words land in the box as they're recognised and stay editable; nothing sends until the person sends it. Its state is shown beside it in words (starting, listening, transcribing) and announced to screen readers, and a problem (a blocked microphone, nothing heard) says what happened.

Opt-in, and private by default: without `transcribe` it uses the browser's speech recognition only where that runs on the device, unless you set `allowBrowserService`. Where dictation can't run, the button is hidden, or says why with `whenUnsupported="explain"`.

<Composer {...composer} tools={<DictationButton value={composer.value} onChange={composer.onChange} />} />

- Registry name: `dictation` (`@candor/dictation`)
- 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/dictation

## Install

```bash
npx candor-ui add dictation
```

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

## Example

```tsx
"use client"

import * as React from "react"
import { Composer } from "@/components/composer"
import { DictationButton } from "@/components/dictation"

export default function Example() {
  const [value, setValue] = React.useState("")
  return (
    <Composer
      className="max-w-2xl"
      value={value}
      onChange={setValue}
      onSubmit={() => setValue("")}
      tools={
        // On-device speech recognition where the browser has it; where it doesn't, the button says why. To use your own
        // speech-to-text instead, pass transcribe={async (audio, { signal }) =>
        //   (await fetch("/api/transcribe", { method: "POST", body: audio, signal })).text()}
        <DictationButton value={value} onChange={setValue} whenUnsupported="explain" />
      }
    />
  )
}
```

## <DictationButton>

Props:

- `value: string`: The text the words go into, usually the composer's `value`.
- `onChange: (value: string) => void`: Called as words arrive, usually the composer's `onChange`.
- `lang?: string | undefined`: A BCP 47 language such as "en-GB". Left out, LocaleProvider's locale, else the page's `lang`, else the browser's.
- `transcribe?: Transcribe | undefined`: Your own speech-to-text. With it, the browser's speech recognition isn't used; the recording goes only to this.
- `allowBrowserService?: boolean | undefined`: Let the browser's own speech service hear the audio. Some browsers (Chrome among them) send it to the browser maker's servers. Off by default: without `transcribe`, dictation runs only where the browser recognises speech on the device, and is unavailable elsewhere.
- `maxSeconds?: number | undefined`: Stops listening after this many seconds of listening (not counting the permission prompt); 120 by default.
- `whenUnsupported?: "hide" | "explain" | undefined` (default `"hide"`): Where dictation can't run: "hide" the button (the default), or "explain" with a locked button that says why.
- `disabled?: boolean | undefined` (default `false`): Off while the composer can't take input, e.g. when it's locked or a reply is streaming.
- `className?: string | undefined`

## Functions

### useDictation

```ts
useDictation({ value, onChange, lang: langProp, transcribe, allowBrowserService = false, maxSeconds = 120 }: DictationOptions)
```

Speech to text into a field you own. Words appear in the field as they're recognised (interim words are replaced as the recogniser settles on them) and stay plain, editable text: nothing is sent. Typing in the field while it listens keeps your edit and carries on after it. Emptying the field (sending the message) stops it and drops whatever it hadn't written yet, so nothing lands in the next message.

Audio goes only where you choose: to your `transcribe`, or to on-device recognition, or, with `allowBrowserService`, to whatever service the browser uses.

## Strings (`dictation`)

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

- `dictate`: Dictate
- `stop`: Stop dictating
- `starting`: Starting the microphone…
- `listening`: Listening. Stop when you’re done, then check the text before you send.
- `transcribing`: Turning your recording into text…
- `stopped`: Stopped dictating. You can edit the text before you send it.
- `unsupported`: Dictation isn’t available in this browser.
- `notOnDevice`: This browser can’t turn speech into text on this device, so dictation is off.
- `denied`: The microphone is blocked. Allow it in the browser’s site settings to dictate.
- `noMicrophone`: No microphone was found.
- `noSpeech`: Nothing was heard. Try again, closer to the microphone.
- `network`: Speech recognition couldn’t reach its service. Try again.
- `language`: Dictation doesn’t support this language here.
- `failed`: Dictation stopped because of an error. Try again.
- `transcribeFailed`: Your recording couldn’t be turned into text. Try again.

## Types

### Transcribe

Your own speech-to-text: given the recording, return its text. Dictation then records with the browser's MediaRecorder and hands you the audio; where it goes from there is your choice.

```ts
(audio: Blob, options: { lang?: string | undefined; signal: AbortSignal }) => Promise<string>
```

### DictationStatus

```ts
| "checking" | "unsupported" | "idle" /** Asking for the microphone, or readying on-device recognition. */ | "starting" | "listening" /** `transcribe` is working on the recording. */ | "transcribing"
```

### DictationProblem

Why dictation stopped or can't run; each is a key in `dictationStrings`.

```ts
"unsupported" | "notOnDevice" | "denied" | "noMicrophone" | "noSpeech" | "network" | "language" | "failed" | "transcribeFailed"
```

### DictationOptions

```ts
{ /** The text the words go into, usually the composer's `value`. */ value: string /** Called as words arrive, usually the composer's `onChange`. */ onChange: (value: string) => void /** A BCP 47 language such as "en-GB". Left out, LocaleProvider's locale, else the page's `lang`, else the browser's. */ lang?: string | undefined /** Your own speech-to-text. With it, the browser's speech recognition
```

### Dictation

```ts
{ status: DictationStatus /** Why it stopped, or why it can't run (with status "unsupported"). Cleared when it starts again. */ problem?: DictationProblem | undefined /** It stopped without a problem, so the words are in the field to check. Cleared when it starts again. */ done: boolean start: () => void stop: () => void toggle: () => void }
```

## 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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
