import { AssistantTr } from '../i18n'; import { StageCopyEntry } from '../stageCopy'; import { VoiceStageState } from './voiceStageState'; /** The voice session's own label for `thinking` — one definition for the session and the status line. */ export declare const VOICE_THINKING_LABEL: StageCopyEntry; /** Reported alongside a stage while the microphone is off. */ export declare const VOICE_MUTED_LABEL: StageCopyEntry; /** * The muted suffix for the status line, or null. * * Muted is reported ALONGSIDE the stage, not instead of it: the assistant may * still be speaking with the microphone off, and saying only "Microphone off" * would misreport that. * * ★ Gated on the STAGE, not the phase. `connecting`, `endpoint-grace` and * `interrupting` are all `phase === 'listening'` underneath, so a phase test * dropped the suffix on exactly those three stages — leaving "Stopped — go * ahead" inviting someone to speak into a muted microphone. * * ★ Not while `listening`: that stage's own label already IS "Microphone off" * when muted, and a suffix would read "Microphone off · Microphone off". * * ★ Not for an empty label (ended, mic-error), which would otherwise render a * bare " · Microphone off". And no `error !== null` arm: the error belongs to * the stage's alert element, and testing it here once printed the whole * sentence TWICE — truncated in the status line, repeated below it, and * announced twice by a screen reader. */ export declare function voiceStatusSuffix(args: { tr: AssistantTr; stage: VoiceStageState; muted: boolean; /** The stage's label, already localised. */ label: string; }): string | null; /** The full status: the stage label, then the suffix when there is one. */ export declare function composeVoiceStatus(label: string, suffix: string | null): string; /** * The status line, split into what is SHOWN and what is ANNOUNCED. * * ★★ Why two strings. The status line is a live region: a screen reader reads * it aloud whenever its text changes. While the model is thinking, a sighted * user is better served by a phrase that moves — a single frozen "Thinking…" * for the length of a turn reads as a hang (see `THINKING_PHRASES`). Putting * that rotation into the live region would announce a new filler word every * `PHRASE_ROTATE_MS`. So the rotation is visual only, and the announced text * changes only when the status genuinely does. * * ★ The first shown phrase is the voice session's OWN label, not the pool's * opening entry: on entering `thinking`, sighted and screen-reader users get * the same words. * * ★★ A pool phrase is shown only when it is in the SAME translation state as * the voice label. The label (`voiceSession.*`) and the pool (`stage.*`) are * separately seeded key families. With the label translated and the pool not, * rotating would alternate the user's language with English — so the line holds * on the translated label instead. When neither is translated, it rotates in * English, like the rest of the untranslated interface. * * ★ A muted microphone stays on screen through the rotation. * * Pure in `elapsedMs` — the caller owns the clock — like `thinkingPhrase`. */ export interface VoiceStatusParts { /** For the live region. Changes only when the status does. */ readonly announced: string; /** * For the eye. Rotates while `rotating` (thinking, with motion allowed); * otherwise identical to `announced`. */ readonly shown: string; } export declare function voiceStatusParts(args: { tr: AssistantTr; /** The meaningful status, exactly as it should be announced. */ status: string; /** Kept on the shown text while it rotates, e.g. "Microphone off". */ suffix: string | null; /** * Whether the shown text may rotate: the assistant is thinking AND motion is * allowed. `VoiceStatusText` passes false under reduced motion (BOFF-7284). */ rotating: boolean; /** Time since rotation began. */ elapsedMs: number; }): VoiceStatusParts; //# sourceMappingURL=voiceStatus.d.ts.map