Voice
The assistant bar's voice on every platform — starting, ending and muting a call, every state member, the launcher, the bar and the component views with their props, React Native's voice hooks, and the website engine's messages — with types, defaults and threading.
The bar's voice runs on the in-app engine — how it connects, its transports and recovery are on Voice; drawing your own call UI from these members is on Your own UI. Native calls, which ring on the lock screen, are on Calls.
Start, end, mute
active={false} also ends a live call, and loads the engine idle so a later
start is instant.
Starting asks for the microphone if it is missing (in Flutter through your
requestMicrophone), and a start before the engine is ready is held and placed
when it is. A refused microphone places no call and reports micDenied. Only one
conversation runs at a time: starting the bar during a native call is refused
with an error.
State
| What | React Native (api) | iOS | Android | Flutter | Values |
|---|---|---|---|---|---|
| On a call / placing one | connected, connecting | connected, connecting | connected, connecting | connected, connecting | Bool |
| A call can start now | ready | ready | ready | ready | Bool |
| Muted | muted | muted | muted | muted | Bool |
| One word to draw from | phase | phase (GrainePhase) | phase (CallPhase) | from connected, agentSpeaking | idle · connecting · reconnecting · thinking · speaking · listening |
| Where a connect is | connectStage, slowConnect, callFailed | the same | the same | the same | registering · dialling · retrying · switching; slowConnect after 4 s |
| The line | quality, network, reconnecting | quality, online, reconnecting | quality, online, reconnecting | quality, online, reconnecting | excellent · good · poor · lost · unknown |
| The engine page | connectivity | connectivity | connectivity | connectivity | loading · ready · retrying · failed |
| What was said | captions (12 kept), caption | captions | captions (12 kept, GraineCaption), and onCaption | captions, caption | role, text, live |
| Captions switch | captionsOn, setCaptionsOn, clearCaptions() | captionsOn, setCaptionsOn(_:), clearCaptions() | captionsOn, setCaptionsOn(on), clearCaptions() | captionsOn, setCaptionsOn(on), clearCaptions() (GraineCaptionsSwitch) | On by default; the SDK's bar hides its caption while it is off; what was said is kept |
| Microphone refused | micDenied | via onMicDenied | via onMicDenied | micDenied | |
| A setting must be fixed | error | via onError | error | error |
Value meanings: Connection values.
Kotlin Multiplatform has the same in Graine.voice.state: captionsOn and
captions, with setCaptionsOn(on) and clearCaptions().
The session and the brand's design
| React Native | iOS | Android | Flutter | Kotlin Multiplatform | |
|---|---|---|---|---|---|
| The agent's settings have loaded | useGraineReady() → { ready, error } (also as useGraineInit() → { isInitialized, error }) | session: GraineSessionConfig?; onSessionReady | session: SessionConfig?; onSessionReady | session, sessionListenable | Graine.session: StateFlow<GraineSession?> |
| The brand's design tokens, for UI of your own | useGraineTheme(): ThemeTokens — works outside a provider too | onTheme(_:) | themeTokens(context?, scheme?): ThemeTokens? | tokens, tokensListenable | Graine.theme: StateFlow<GraineThemeTokens?> (each agent's theme) |
| Try a design before it is saved | — | — | previewDesign(design); null goes back to the agent's | — | — |
| The engine is mounted | — | voice.isAttached | voice.attached | voice.attached | — |
| Your own UI showing the voice opened or closed | the launcher's active | voice.viewOpened(), voice.viewClosed() | voice.viewOpened(), voice.viewClosed() | voice.viewOpened(), voice.viewClosed() | — |
ready (or isInitialized) turns true once the agent's settings have been
fetched — its look, its declared actions, how to connect — and error carries the
first thing that went wrong: show a splash until then, or an error state when the
publishable key is wrong. Do not infer it from connected, which stays false on a
voice-only screen until someone taps the microphone. The settings' fields:
Session settings.
The tokens are the colours, type, corners and depth the SDK draws with, for the
device's current light or dark appearance, as plain numbers and #RRGGBB
strings; they change when the device switches. In Flutter, iOS and Android, tell
the voice when a UI of yours that shows it opens and closes (viewOpened(),
viewClosed(); React Native: the launcher's active): a slow or failed connect is
only reported while one is open.
The launcher (React Native)
GraineVoiceLauncher owns the engine page and the call.
| Prop | Type | Default | |
|---|---|---|---|
webView | component | required | WebView from react-native-webview |
active | boolean | true | Mount early with false; true when your UI opens. false ends a live call |
autoStart | boolean | false | Place the call when the page is ready and the launcher is active |
transport | "ws" or "rtc" | the dashboard's | Force a transport, for testing |
timing | object | stallMs 15000 · bootMs 15000 · hardCapMs 120000 · retryDelaysMs 1, 2, 4, 8, 15, 15 s · endWatchdogMs 4000 · slowConnectMs 4000 | |
onCaption | (caption) => void | Each line as it is said | |
onCallState | ({ connected, connecting }) => void | ||
onEnded | () => void | Once per call | |
onMicDenied | (reason) => void | NotAllowedError or NotFoundError | |
onError | (message) => void | Voice cannot work until a setting is fixed | |
onTransport | ({ transport, requested, reason, message }) => void | A fallback, and why | |
children | (api) => ReactNode | The voice api: the state above, start, end, setMuted, retry |
The bar and the launcher button
| Component | Props |
|---|---|
GraineVoiceBar | api (required) · onClose · bottomInset (default 96) · draggable (default true) · placement / onPlacementChange ({ bottomOffset }) · name · accessory |
GraineBarAction | label (required) · icon ("call" or "chat", default "call") · onPress (required) |
GraineLauncherButton | onPress (required) · style · label (default "Talk to" + the heading) · size (default 56) |
GraineAgentBar | name, accent, avatarUrl · bottomInset · hidden · captionsOn · scheme (auto/light/dark) · theme · draggable · placement / onPlacementChange |
GraineLoader | moment (connecting, reconnecting, thinking, loading) · label · kind · size · color |
GraineWidget | Draws the agent's components where you place it |
Multi-agent API: with several agents, the bar
takes the agent it draws — iOS GraineAgentBar(graine: GraineInApp.named("sales")),
Android agentBar.graine = GraineInApp.get("sales"), Flutter
GraineAgentBar(app: GraineInApp.named('sales')), Kotlin Multiplatform
GraineAgentBar(agent = "sales"); in React Native, the nearest provider's. Each
agent has its own voice; one conversation runs at a time across all of them.
React Native hooks for voice
| Hook | Returns |
|---|---|
useGraineConnection() | { quality, online: boolean | null, network: "online" | "offline" | "unknown" } |
useKeyboardLift() | number — pixels to add to a bottom-anchored bar while the keyboard is up |
useDraggablePlacement({ baseBottom, initialOffset, onChange }) | { offset, panHandlers, onLayout } — spread on your own bar |
useGraineVoice(adapter) | { listening, start, stop } — only if you bring your own native audio: 16 kHz mono PCM16 in, with the provider's voice prop |
useGraineLoader() | The loader's look and the accent, for a loader of your own |
The website engine's messages
A page that hosts the engine itself (rather than the loader's widget) drives it with messages — the full list is on Any other platform.
| Out of the page | Payload |
|---|---|
graine:ready | — the page is mounted |
graine:call | { connected, connecting } |
graine:caption | { role, text, live } |
graine:progress | { stage } — registering, dialling, retrying, switching, failed |
graine:quality | { sample: { rtt, jitter, loss, quality } } |
graine:network, graine:reconnecting | { online }, { active } |
graine:mic-denied | { reason } |
graine:transport | { transport, requested, reason, message } |
graine:need-identity, graine:need-ticket | The page needs the visitor's identity, or a ticket from your origin |
graine:ended | — once per call |

