Valora

@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

ImportLayerUse it when
@valora-ai/reactTeams skin — themed, batteries-included components (VoiceCall, Orb, ChatRail, …)you want the meeting-app look out of the box
@valora-ai/react/headlessHeadless primitives — unstyled behavior + a11yyou want full control of the markup/styling
@valora-ai/react/shadcnshadcn/ui skin — Tailwind components on the headless layeryour 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-vad
import { 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

ExportWhat it is
VoiceCallThe whole call shell — orb, title bar, chat rail, control bar. Start here.
OrbThree.js 9,000-point particle sphere; reacts to level, listening, speaking.
DotsOrbLightweight canvas-2D orb (no WebGL); same states, tiny footprint.
ChatRailScrollable transcript panel with composer, reactions, thinking indicator.
VoiceControlBarMic / camera / chat / leave controls.
VoiceToast, InviteModal, CallEnded, MicPermissionModalOverlays.
VoiceRoom, useVoiceRoomAgent 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.

On this page

Valora is local-first

No API key, no server — everything in this doc runs on-device.

Star on GitHub