import { type HTMLAttributes, type ReactNode } from 'react';
import { type AiCreditsInfo, type AiCreditsNoticeProps } from '../ai-credits-notice';
import { type AiConsentBannerHeadingLevel, type AiConsentGate } from '../ai-permission-notice';
import { type KbdNamedKey } from '../kbd';
/** Where the host's voice session is. */
export type VoiceControlStatus = 'idle' | 'starting' | 'listening' | 'paused' | 'error';
/** How a message reads: the tone decides the colour and the icon. */
export type VoiceControlTone = 'info' | 'success' | 'warning' | 'error';
/** Generic failures the kit words itself, translated. */
export type VoiceControlError = 'microphoneDenied' | 'connectionLost' | 'startFailed' | 'recordingFailed' | 'unsupported';
/** A host message, already translated by the host. */
export interface VoiceControlMessage {
tone: VoiceControlTone;
text: string;
/**
* Change it to announce the same words again (the live region only speaks
* when the message changes), e.g. a counter for "Not recognised" twice.
*/
id?: string | number;
}
/** What the speech service heard. `partial` may still change. */
export interface VoiceControlTranscript {
final?: string;
partial?: string;
}
/** One dictation language the host's speech service accepts. */
export interface VoiceControlLanguage {
value: string;
label: string;
}
/** One match the user can pick by voice ("option 1") or by hand. */
export interface VoiceControlCandidate {
id: string;
label: string;
detail?: string;
}
/** One voice command shown in the commands disclosure. */
export interface VoiceControlCommand {
phrase: ReactNode;
description: ReactNode;
}
/** What a press asks the host to do: start (or resume), or stop (or cancel
* a start). */
export type VoiceControlToggle = 'start' | 'stop';
/** Inline in the page flow, or a floating pill that opens the panel. */
export type VoiceControlLayout = 'inline' | 'floating';
/** The viewport corner of the floating pill. Logical: mirrors in RTL. */
export type VoiceControlPosition = 'bottom-end' | 'bottom-start';
/**
* The practice's AI credits, as the platform keeps them (a yearly allowance
* that expires, then welcome and bought credits that do not), plus whether
* they have run out. See `AiCreditsInfo` for the allowance, the dates and
* the next steps the host can offer.
*/
export interface VoiceControlAiCredits extends AiCreditsInfo {
/**
* Out of AI credits. Every way of starting shows the out-of-credits
* notice in the panel instead of calling `onToggle`; stopping is never
* gated. Set while a session runs or is paused, the panel stops it
* (`onToggle('stop')`, once) and shows the notice on its own.
*/
exhausted?: boolean;
}
/** Curated imperative handle for agent / external automation. */
export interface VoiceControlPanelHandle {
getStatus: () => VoiceControlStatus;
/** Ask the host to start (`onToggle('start')`) unless already starting
* or listening, or disabled. Gated by `aiConsent` like a press. */
start: () => void;
/** Ask the host to stop, cancel a start or end a paused session
* (`onToggle('stop')`); a no-op when idle or stopped. Never gated. */
stop: () => void;
getCandidateIds: () => string[];
pickCandidate: (id: string) => void;
isCommandsOpen: () => boolean;
openCommands: () => void;
closeCommands: () => void;
/** Floating layout: whether the panel above the pill is open. Always
* false inline. */
isPanelOpen: () => boolean;
/** Floating layout: open the panel (no focus move). A no-op inline. */
openPanel: () => void;
/** Floating layout: close the panel; listening carries on. A no-op inline. */
closePanel: () => void;
/** Whether `aiCredits.exhausted` is gating every start. */
isCreditsExhausted: () => boolean;
/** Whether the out-of-credits notice is showing. */
isCreditsNoticeOpen: () => boolean;
/** Close the out-of-credits notice, back to the controls. */
closeCreditsNotice: () => void;
}
export interface VoiceControlPanelProps extends Omit, 'title' | 'children' | 'onToggle'> {
/** Where the host's voice session is. Drives the button, dot and status. */
status: VoiceControlStatus;
/**
* The start/stop button, the pill's voice segment, the shortcut and the
* agent call this, saying which way: `'start'` when idle, paused or
* stopped, `'stop'` when starting or listening (stop, or cancel a start).
* Every stop is passed on, so act on the argument rather than flip your
* own state. A host that ignores the argument keeps working.
*/
onToggle: (next: VoiceControlToggle) => void;
/**
* Stops a session from starting: disables the button, the shortcut, the
* settings and the matches. A session already starting or listening can
* still be stopped, so the microphone is never trapped on.
*/
disabled?: boolean;
/** Panel title. Default: the kit's "Voice control". */
title?: ReactNode;
/** Shows a "Beta" badge beside the title. */
beta?: boolean;
/** A host notice under the title, e.g. "Free during the beta." */
betaNotice?: ReactNode;
/** The current dictation language (a `languages` value). */
language?: string;
/** Dictation languages. The select shows only with two or more. */
languages?: VoiceControlLanguage[];
onLanguageChange?: (value: string) => void;
/** Extra settings beside the language, e.g. the periodontal route select. */
extraControls?: ReactNode;
/** The bold "where am I" line: the field or elements voice acts on. */
target?: ReactNode;
/** What the speech service heard: `final` reads normal, `partial` muted. */
transcript?: VoiceControlTranscript;
/** A host message. The tone sets colour and icon; not every message is red. */
message?: VoiceControlMessage | null;
/** A generic failure, worded and translated by the kit (tone: error). */
error?: VoiceControlError | null;
/** Several matches to choose from, numbered as they are spoken. */
candidates?: VoiceControlCandidate[];
/** Host label above the matches, e.g. "Several matches: say 'option 1'…". */
candidatesLabel?: string;
onCandidatePick?: (id: string) => void;
/** Voice commands listed in the disclosure under the panel. */
commands?: VoiceControlCommand[];
/** Start with the commands disclosure open. */
defaultCommandsOpen?: boolean;
/**
* Keyboard shortcut the kit binds document-wide to toggle, e.g. `"Alt+V"`
* (default). Modifiers: Alt, Control, Shift, Meta. `false` turns it off.
*/
shortcut?: string | false;
/** Opaque instance id: `data-component-id` and agent registration. */
id?: string;
/**
* `inline` (default): the panel sits in the page flow, above the chart.
* `floating`: the RichTextEditor's AI pill (mic | panel) fixed to a
* viewport corner; the second segment opens this panel above it, non-modal.
*/
layout?: VoiceControlLayout;
/**
* Floating only: the viewport corner. Default `bottom-end`; `bottom-start`
* moves the pill away from another floating widget (a chat bubble).
*/
position?: VoiceControlPosition;
/** Floating only: whether the panel above the pill is open (controlled). */
open?: boolean;
/** Floating only: the open state when uncontrolled. Default `false`. */
defaultOpen?: boolean;
/**
* Floating only: called when the panel opens or closes, by the pill, the
* close button, Escape, or on its own for an error or a set of matches.
*/
onOpenChange?: (open: boolean) => void;
/**
* Floating only: the pill's sparkle, as on the RichTextEditor's pill. It
* plays once when the pill first shows and again when a success message
* arrives (a write landed); never in a loop while listening. Default `true`.
*/
aiSparkle?: boolean;
/**
* Patient AI-consent gate: voice sends patient audio to the host's speech
* service. Same contract as RichTextEditor's `aiConsent`. While present and
* not granted, every way of starting shows the `AiConsentBanner` instead of
* calling `onToggle`; stopping is never gated. Omit for ungated behaviour.
*/
aiConsent?: AiConsentGate;
/**
* Heading level for the `AiConsentBanner` title and the out-of-credits
* notice title, which show in the same place. Default 5.
*/
aiConsentHeadingLevel?: AiConsentBannerHeadingLevel;
/**
* The practice's AI credits. With `exhausted`, every way of starting (the
* Start button, the pill's mic, the shortcut, the agent's
* `start_listening`) shows the out-of-credits notice instead of calling
* `onToggle`, after the consent check; stopping is never gated. Running
* low (80% used, or `runningLow`) and `expiring` show as quiet lines in
* the panel and never block. Omit for ungated behaviour.
*/
aiCredits?: VoiceControlAiCredits;
/**
* Floating only: the `AiCreditsNotice` hover card on the pill's mic, as on
* the RichTextEditor's AI triggers. It never takes the click: the mic
* still starts, stops or opens a gate exactly as without it.
*/
aiCreditsNotice?: AiCreditsNoticeProps;
}
type ShortcutModifier = Extract;
interface ParsedShortcut {
modifiers: Set;
key: string;
/** `aria-keyshortcuts` value, e.g. `Alt+V`. */
aria: string;
/** Keys for the kit Kbd, e.g. `['Alt', 'V']`. */
keys: string[];
}
/** Parse `"Alt+V"`-style shortcuts. Returns null for an unusable string. */
export declare function parseVoiceShortcut(shortcut: string): ParsedShortcut | null;
/**
* VoiceControlPanel: the kit UI for a host-driven voice control session.
* The host keeps the microphone and the speech service; the panel shows the
* state, what was heard, where the voice acts, messages and matches, and
* calls `onToggle` from its button and its keyboard shortcut.
*/
export declare const VoiceControlPanel: import("react").ForwardRefExoticComponent>;
export {};
//# sourceMappingURL=voice-control-panel.d.ts.map