npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sona-react

v0.1.1

Published

React hook and components for real-time speech-to-text dictation — built for SaaS products and product teams.

Readme

sona-react

Real-time speech-to-text dictation for React — built for SaaS products and product teams.

npm install sona-react

Quick 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 }   →  finalTranscript

You 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 deploy

Point 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 (or localhost)
  • AudioWorklet — Chrome 66+, Firefox 76+, Safari 14.1+
  • WebSocket — all modern browsers

License

MIT