Browser voice page
Run the smallest complete browser app — RealtimeProvider, a live transcript, mic control, and one client tool — from the realtime-page example.
realtime-page is a Vite + React 19 page that puts every core React-SDK piece on one screen: a manually constructed RealtimeClient wrapped in RealtimeProvider, RealtimeAudio for playback, MicToggle for mute control, useTranscript() rendering turn-by-turn speech, and useToolCalls() showing the agent's tool activity. It declares one client tool — get_local_time — so you can watch a tool call round-trip without writing a backend.
Full source: examples/typescript/realtime-page in the examples repo. To build this app from an empty directory instead of running it, follow Build a voice React app, which uses this example as its base code.
Prerequisites
- Node 18+
- A workspace API key with the
realtime:startscope (API keys) - A browser that can grant microphone access
Run it
-
Clone the examples repo and install:
git clone https://github.com/socratic-ai/cosmo-ai cd cosmo-ai/examples/typescript/realtime-page npm installThe
cosmo-aidependency is the published npm package, so a fresh clone needs nothing beyondnpm install. -
Start the dev server:
npm run dev -
Open
http://localhost:5173, paste your API key, and select Start Session. Allow microphone access when the browser asks.
Pasting a key into a local page is fine for development. An app you deploy must never contain a workspace key — mint short-lived end-user tokens on a backend instead. See End-user credentials.
How it works
The app is one file, src/App.tsx. Session state lives entirely in the SDK — the component tree just renders hooks:
function SessionView() {
const session = useRealtimeSessionContext();
const transport = useTransportState();
const transcript = useTranscript();
const toolCalls = useToolCalls();
// render transport badge, MicToggle, RealtimeAudio, transcript bubbles…
}The client tool is declared with the typed tool() helper and a zod schema, and runs in the page when the agent calls it:
import { clientTool } from 'cosmo-ai/tool';
import { zodInput } from 'cosmo-ai/tool/zod';
import { z } from 'zod/v4';
const getLocalTime = clientTool({
name: 'get_local_time',
description: 'Returns the local wall-clock time.',
input: zodInput(
z.object({
locale: z.string().describe('BCP 47 locale tag, e.g. "en-US"').optional(),
}),
),
handler: async ({ locale }) => ({
time: new Date().toLocaleTimeString(locale),
}),
});
await client.agent({ tools: [getLocalTime] }).start();One browser-specific detail worth copying: on Node the SDK reads COSMO_BASE_URL, but a browser has no environment, so the page names its backend through a <meta name="cosmo-base-url"> tag it writes before constructing the client. The default is the production origin; the form's Base URL field only matters when you point the page at a non-production backend.
What you should see
The form is replaced by a status badge that moves through the transport states to ready, and the agent greets you out loud. Speak, and your words appear as right-aligned transcript bubbles — dimmed while partial, solid when the turn is final — with the agent's replies on the left. Ask "what time is it?" and a Tool Calls panel appears showing get_local_time with its status, while the agent speaks the answer.
Troubleshooting
Issue: the status badge never reaches ready and no greeting plays.
Cause: the key was rejected, or it belongs to a different backend than the Base URL field names.
Solution: check the browser console for a 401, and confirm the key's workspace matches the backend you're pointing at.
Next steps
- Build a voice React app — the same app built step by step, then extended
- Share your app — replace the pasted key with minted end-user tokens
- Handle tool calls — richer client-tool patterns