@valora-ai/react
A React UI kit for voice-AI calls — orb, transcript rail, controls — built on a headless primitive layer you can re-skin.
A particle orb that listens and speaks, a chat rail with streaming
transcription and tool cards, and a control bar — all driven by props you own.
Bring your own voice backend; @valora-ai/react renders the call.
npm install @valora-ai/react
# react & react-dom are peer dependencies (>=18)Three layers, one behavior
| Import | Layer | Use it when |
|---|---|---|
@valora-ai/react | Teams skin — themed, batteries-included components (VoiceCall, Orb, ChatRail, …) | you want the meeting-app look out of the box |
@valora-ai/react/headless | Headless primitives — unstyled behavior + a11y | you want full control of the markup/styling |
@valora-ai/react/shadcn | shadcn/ui skin — Tailwind components on the headless layer | your app already uses shadcn/Tailwind |
The Teams and shadcn layers are two skins over the same headless primitives — build a third skin the same way.
Quick start
VoiceCall is fully presentational — render it with state and handlers and you
have a working call UI on screen. No server, no tokens.
import { useState } from 'react';
import { VoiceCall } from '@valora-ai/react';
import '@valora-ai/react/styles.css';
export default function App() {
const [muted, setMuted] = useState(false);
return (
<VoiceCall
state="listening" // idle | listening | thinking | speaking
level={0.4} // mic amplitude 0–1, drives the orb
messages={[{ id: '1', role: 'eva', text: 'Hi — what can I help with?' }]}
muted={muted}
onMic={() => setMuted((m) => !m)}
onSend={(text) => console.log('user said:', text)}
onLeave={() => console.log('hang up')}
/>
);
}You own the state, @valora-ai/react draws it. Wire state/level/messages
to your voice backend and the orb, transcript, and controls react automatically.
Driving it from a voice agent
Wrap your tree in VoiceRoom and read the agent snapshot through hooks instead
of hand-feeding props:
import { VoiceRoom, useVoiceRoom } from '@valora-ai/react';
import { createVoiceAgent } from '@valora-ai/voice';
const agent = createVoiceAgent({ vad, stt, llm, tts, player });
<VoiceRoom agent={agent}>
<MyCallUI />
</VoiceRoom>;VoiceRoom takes a @valora-ai/voice agent directly. With
manageLifecycle (default on) it calls agent.start() on mount and
agent.stop() on unmount.
Local realtime hook
For apps that already own a VoiceAgent, @valora-ai/react/local exposes
useLocalRealtime(agent): a useSyncExternalStore hook over
createLocalRealtimeSession(agent).
import { useLocalRealtime } from '@valora-ai/react/local';
function LocalRealtimePanel({ agent }) {
const rt = useLocalRealtime(agent);
return (
<div>
<p>{rt.status} / {rt.state}</p>
{rt.messages.map((m) => <p key={m.id}>{m.role}: {m.text}</p>)}
<button onClick={() => rt.connect()}>Connect</button>
<button onClick={() => rt.interrupt()}>Interrupt</button>
</div>
);
}This is session-shaped local realtime: connect() starts the local agent,
disconnect() stops it, and messages mirrors transcript segments. It does not
create a remote room or request a token.
Local voice agent, one hook
For a fully local, in-browser pipeline (mic → VAD → STT → LLM → TTS, no server), configure the model providers you want to ship and let one React provider plus one hook handle the wiring:
npm install @valora-ai/react
npm install @valora-ai/lfm2 @valora-ai/moonshine-stt @valora-ai/kokoro @valora-ai/silero-vadimport { useState } from 'react';
import { LocalVoiceAgentProvider, useLocalVoiceAgent } from '@valora-ai/react';
import { createValoraRuntime } from '@valora-ai/react/local';
import { kokoroProvider } from '@valora-ai/kokoro/provider';
import { createLfm2Provider } from '@valora-ai/lfm2/provider';
import { moonshineProvider } from '@valora-ai/moonshine-stt/provider';
import { sileroProvider } from '@valora-ai/silero-vad/provider';
// pick the models this quickstart offers
const runtime = createValoraRuntime({
languageModels: [createLfm2Provider()],
registryProviders: [moonshineProvider, kokoroProvider, sileroProvider],
});
function VoiceAgentPanel() {
const { status, mic, messages, send } = useLocalVoiceAgent();
const [input, setInput] = useState('');
return (
<div>
<p>{status.error || (status.active ? status.phase : 'Ready')}</p>
<button onClick={mic.toggle}>{mic.enabled ? 'Mute' : 'Unmute'}</button>
{messages.map((m) => <p key={m.id}>{m.role}: {m.text}</p>)}
<form onSubmit={(e) => { e.preventDefault(); send(input); setInput(''); }}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
</div>
);
}
export default function App() {
return (
<LocalVoiceAgentProvider runtime={runtime}>
<VoiceAgentPanel />
</LocalVoiceAgentProvider>
);
}useLocalVoiceAgent() returns { state, level, messages, send, mic, screen, status, applyModel } — model selection defaults to sane picks and can be
overridden per-kind (llm/stt/tts) via applyModel or the provider's
selection prop. If you already have a ModelRegistry, the older
registry={registry} prop still works; prefer runtime={runtime} when text
hooks and voice should share configured providers.
LocalVoiceAgentProvider and useLocalVoiceAgent ship from @valora-ai/react.
The model providers are separate packages, so install only the providers you pass
to createValoraRuntime. WebGPU is still required in the browser. See
voice-quickstart.tsx
for the full ~40-line example.
Components
| Export | What it is |
|---|---|
VoiceCall | The whole call shell — orb, title bar, chat rail, control bar. Start here. |
Orb | Three.js 9,000-point particle sphere; reacts to level, listening, speaking. |
DotsOrb | Lightweight canvas-2D orb (no WebGL); same states, tiny footprint. |
ChatRail | Scrollable transcript panel with composer, reactions, thinking indicator. |
VoiceControlBar | Mic / camera / chat / leave controls. |
VoiceToast, InviteModal, CallEnded, MicPermissionModal | Overlays. |
VoiceRoom, useVoiceRoom | Agent context for a @valora-ai/voice agent. |
It also exports an Valora-styled surface that mirrors a familiar
components-react API (AudioConference, ControlBar, useVoiceAssistant,
useTracks, …) — the conventional room/participant names and prop shapes, bound
to a local agent instead of a remote room.
Theming
import '@valora-ai/react/styles.css';theme="dark" (default) or theme="light". All colors are CSS custom
properties on :root, so you can re-skin without touching the components:
:root {
--eva-accent: #7c83e8; /* indigo — orb, bubbles, active controls */
--eva-stage: #0e0e11; /* call backdrop */
--eva-green: #3fd18a; /* "live" / success */
--eva-leave: #c4314b; /* hang-up red */
}See the headless primitives, the shadcn/ui skin, and the @valora-ai/react API reference.