sona-react
v0.1.1
Published
React hook and components for real-time speech-to-text dictation — built for SaaS products and product teams.
Maintainers
Readme
sona-react
Real-time speech-to-text dictation for React — built for SaaS products and product teams.
npm install sona-reactQuick start
import { useSona } from 'sona-react';
function SearchBar() {
const { isListening, transcript, startListening, stopListening } = useSona({
apiKey: 'your-api-key', // get one at sona.ai
});
return (
<div>
<input value={transcript} readOnly placeholder="Say something…" />
<button onClick={isListening ? stopListening : startListening}>
{isListening ? 'Stop' : 'Dictate'}
</button>
</div>
);
}Or use the pre-built textarea component:
import { SonaInput } from 'sona-react';
<SonaInput
apiKey="your-api-key"
placeholder="Click the mic to dictate…"
/>How it works
mount → GET /v1/warm_up (pre-allocates server resources)
↓
click → WebSocket /v1/ws (opens a session)
↓ { type: 'auth', token, language, context }
AudioContext (16 kHz) + AudioWorklet
↓ every 100 ms
{ type: 'stream', data, volume, duration }
↓
server → { type: 'transcript', text, is_final: false } → interimTranscript
↓
user stops → { type: 'end' }
↓
server → { type: 'transcript', text, is_final: true } → finalTranscriptYou run the server. sona-react connects to your backend — your backend
handles all speech engine communication, credentials, and billing. The browser
never sees the underlying provider details.
See examples/04-backend-proxy/ for a ready-to-use Node.js proxy server.
API
useSona(options)
Options
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| serverUrl | string | 'https://sona-proxy.sona-test.workers.dev' | Sona backend URL |
| token | string | — | Short-lived session token from your server (recommended in production) |
| apiKey | string | — | Your Sona API key. Only for local dev — never ship in client code |
| language | string[] | ['en'] | BCP-47 language codes |
| context | object | {} | Domain context / vocabulary hints |
| onTranscript | (text, isFinal) => void | — | Called on every transcript event |
| onFinalTranscript | (text) => void | — | Final transcripts only |
| onError | (error) => void | — | Error handler |
| warmUpOnMount | boolean | true | Auto warm-up on mount |
| continuous | boolean | false | Keep session open across utterances |
| debug | boolean | false | Log WebSocket events to console |
Return value
| Field | Type | Description |
|-------|------|-------------|
| status | SonaStatus | 'idle' \| 'warming' \| 'ready' \| 'connecting' \| 'listening' \| 'processing' \| 'error' |
| isListening | boolean | Mic is open and streaming |
| transcript | string | finalTranscript + interimTranscript |
| finalTranscript | string | Confirmed final text |
| interimTranscript | string | Live partial text |
| error | Error \| null | Last error |
| warmUp() | Promise<void> | Trigger warm-up manually |
| startListening() | Promise<void> | Open mic and start streaming |
| stopListening() | void | Send end signal and await final transcript |
| resetTranscript() | void | Clear transcript state |
<SonaInput />
Drop-in textarea with mic button. Accepts all useSona options plus:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | — | Controlled value |
| onChange | (value: string) => void | — | Change handler |
| appendMode | boolean | false | Append transcript instead of replace |
| placeholder | string | 'Start speaking or type here…' | |
| disabled | boolean | false | |
| rows | number | 4 | |
| className | string | — | Outer wrapper class |
| textareaClassName | string | — | Textarea class |
| buttonClassName | string | — | Mic button class |
| renderButton | (props) => ReactNode | — | Custom mic button render prop |
<SonaMicButton />
Standalone mic button — no textarea included. Drives your own state.
import { SonaMicButton } from 'sona-react';
<SonaMicButton
apiKey="your-api-key"
onResult={(text) => appendToNotes(text)}
/>Backend server
sona-react connects to your backend. Your backend can use any speech
provider internally — the browser only ever sees the Sona protocol.
Cloudflare Worker (recommended)
See examples/04-cloudflare-worker/ for the recommended deployment.
A Cloudflare Worker runs at 300+ edge locations worldwide, adding only ~1–3 ms over a direct connection. Setup:
cd examples/04-cloudflare-worker
npm install
# Set secrets (stored in Cloudflare, never in code)
npm run secret:upstream-ws # upstream WebSocket URL
npm run secret:upstream-warmup # upstream warm-up URL
npm run secret:upstream-key # upstream provider API key
npm run secret:sona-key # your Sona API key
# Local development
npm run dev
# Deploy to the edge
npm run deployPoint a custom domain (api.sona.ai) to the Worker by uncommenting the
routes block in wrangler.toml after adding your domain to Cloudflare.
Sona WebSocket protocol
Your server must speak this protocol on ws(s)://<host>/v1/ws:
Client → Server
{ "type": "auth", "token": "<session_token>", "language": ["en"], "context": {} }
{ "type": "stream", "data": "<base64_wav>", "volume": 0.5, "duration": 100 }
{ "type": "end" }Server → Client
{ "type": "ready" }
{ "type": "transcript", "text": "hello world", "is_final": false }
{ "type": "transcript", "text": "hello world", "is_final": true }
{ "type": "error", "message": "..." }Warm-up endpoint
GET /v1/warm_up
Authorization: Bearer <token>
→ 200 { "status": "ok" }Session token endpoint (optional)
POST /v1/session-tokens
Authorization: Bearer <api_key>
→ 200 { "token": "...", "expires_at": "..." }Authentication
Never put your API key in a client-side bundle.
The recommended flow:
Browser Your server Sona backend
| | |
|-- POST /api/session-token -->| |
| |-- POST /v1/session-tokens ->|
| |<-- { token } --------------|
|<-- { token } ----------------| |
| |
|-- WebSocket /v1/ws (token) -------------------------------->|// In your Next.js / Express API route:
const { token } = await fetch(`${SONA_SERVER_URL}/v1/session-tokens`, {
method: 'POST',
headers: { Authorization: `Bearer ${SONA_API_KEY}` }, // stays on server
}).then(r => r.json());
// In the browser:
<SonaInput serverUrl={process.env.NEXT_PUBLIC_SONA_SERVER_URL} token={token} />See examples/03-nextjs/ for a complete Next.js App Router implementation.
Examples
| Example | Shows |
|---------|-------|
| examples/01-basic-hook/ | useSona hook, status badge, start/stop/clear |
| examples/02-drop-in-component/ | SonaInput in 5 patterns: uncontrolled, controlled, append, custom button, full form |
| examples/03-nextjs/ | Next.js App Router with server-side token issuance |
| examples/04-cloudflare-worker/ | Cloudflare Worker proxy — ~1–3 ms overhead, deploys to 300+ edge PoPs |
Continuous mode
Keep the session open and accumulate transcripts across multiple utterances.
const { transcript, startListening, stopListening } = useSona({
serverUrl: 'https://your-backend.com',
token: sessionToken,
continuous: true,
onFinalTranscript: (text) => console.log('Utterance:', text),
});Styling
SonaInput ships with minimal inline styles. Override them with:
// Tailwind
<SonaInput
token={token}
className="relative"
textareaClassName="w-full rounded-lg border border-gray-300 p-3 pr-12 focus:outline-none focus:ring-2 focus:ring-indigo-500"
buttonClassName="absolute bottom-2 right-2 w-8 h-8 rounded-full bg-indigo-500 text-white"
/>
// Or replace the button entirely
<SonaInput
token={token}
renderButton={({ isListening, onClick }) => (
<MyCustomMicButton active={isListening} onClick={onClick} />
)}
/>Browser requirements
getUserMedia— HTTPS required (orlocalhost)AudioWorklet— Chrome 66+, Firefox 76+, Safari 14.1+WebSocket— all modern browsers
License
MIT
