@lofcz/orb-ui
v0.16.0
Published
React voice agent UI components with audio-reactive adapters for Vapi, ElevenLabs, LiveKit, Pipecat, OpenAI Realtime, Gemini Live, and custom voice AI apps.
Maintainers
Readme
orb-ui
Voice agent UI that feels alive.
Expressive, accessible React components for realtime voice agents. Connect Vapi, ElevenLabs, LiveKit, Pipecat, OpenAI Realtime, Gemini Live, or your own voice stack through one consistent UI layer.
import Vapi from '@vapi-ai/web'
import { Orb } from '@lofcz/orb-ui'
import { createVapiAdapter } from '@lofcz/orb-ui/adapters'
const vapi = new Vapi('your-public-key')
const adapter = createVapiAdapter(vapi, { assistantId: 'your-assistant-id' })
export function VoiceOrb() {
return <Orb adapter={adapter} theme="circle" aria-label="Start voice assistant" />
}Install
Install the component package:
bun add @lofcz/orb-uiProvider adapters are lightweight wrappers around provider SDKs. Install the SDK for the provider you use:
# Vapi
bun add @lofcz/orb-ui @vapi-ai/web
# ElevenLabs Conversational AI
bun add @lofcz/orb-ui @elevenlabs/client
# LiveKit Agents
bun add @lofcz/orb-ui livekit-client
# Pipecat (choose the transport used by your agent)
bun add @lofcz/orb-ui @pipecat-ai/client-js @pipecat-ai/small-webrtc-transport
# OpenAI Realtime uses browser WebRTC and has no additional client SDK
bun add @lofcz/orb-ui
# Gemini Live
bun add @lofcz/orb-ui @google/genaiNote: Orb uses React hooks internally — in Next.js App Router, use it in a
'use client'component.
How provider adapters are created
Every provider ends at the same React API:
<Orb adapter={adapter} theme="circle" aria-label="Start voice assistant" />The only difference is how the adapter obtains a provider session:
| Provider | Required browser setup |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Vapi guide | Pass a configured Vapi client plus assistantId |
| ElevenLabs guide | Pass Conversation plus an agentId, signed URL, or conversation token |
| LiveKit guide | Provide a token endpoint and optional agent name |
| Pipecat guide | Pass a configured PipecatClient plus its connect callback |
| OpenAI Realtime guide | Return a fresh short-lived client secret from getClientSecret |
| Gemini Live guide | Open the official Google Live session in connect |
The adapter owns provider event mapping and emits one consistent OrbSignal. OpenAI and Gemini
standard API keys, and LiveKit participant-token signing, stay on your server. See the
adapter overview for the responsibility boundary and
advanced setup shapes.
For an app-owned WebRTC, WebSocket, telephony, or speech runtime, follow the custom integration guide.
Quick Start
Use orb-ui as a React voice AI component when you need a first-party provider voice UI or a custom animated voice orb for another realtime voice agent stack.
With Vapi
import Vapi from '@vapi-ai/web'
import { Orb } from '@lofcz/orb-ui'
import { createVapiAdapter } from '@lofcz/orb-ui/adapters'
const vapi = new Vapi('your-public-key')
const adapter = createVapiAdapter(vapi, { assistantId: 'your-assistant-id' })
function App() {
return <Orb adapter={adapter} theme="circle" aria-label="Start Vapi assistant" />
}With ElevenLabs
import { Conversation } from '@elevenlabs/client'
import { Orb } from '@lofcz/orb-ui'
import { createElevenLabsAdapter } from '@lofcz/orb-ui/adapters'
const adapter = createElevenLabsAdapter(Conversation, { agentId: 'your-agent-id' })
function App() {
return <Orb adapter={adapter} theme="circle" aria-label="Start ElevenLabs assistant" />
}With LiveKit
import { Orb } from '@lofcz/orb-ui'
import { createLiveKitAdapter } from '@lofcz/orb-ui/adapters/livekit'
const adapter = createLiveKitAdapter({
tokenEndpoint: '/api/livekit-token',
agentName: 'your-agent-name',
})
function App() {
return <Orb adapter={adapter} theme="circle" aria-label="Start LiveKit assistant" />
}The LiveKit entrypoint creates the room and token source, assigns a fresh room name, and meters both
sides of the conversation with speech-oriented analyser and smoothing defaults. Existing-room and
custom-runtime modes remain available from the advanced @lofcz/orb-ui/adapters entrypoint.
With Pipecat
import { PipecatClient } from '@pipecat-ai/client-js'
import { SmallWebRTCTransport } from '@pipecat-ai/small-webrtc-transport'
import { Orb } from '@lofcz/orb-ui'
import { createPipecatAdapter } from '@lofcz/orb-ui/adapters'
const client = new PipecatClient({ transport: new SmallWebRTCTransport(), enableMic: true })
const adapter = createPipecatAdapter(client, {
connect: () => client.connect({ webrtcUrl: 'https://agent.example.com/api/offer' }),
})
function App() {
return <Orb adapter={adapter} theme="circle" aria-label="Start Pipecat assistant" />
}The Pipecat adapter consumes the standard RTVI event surface and meters the client media tracks as a browser fallback, so it works with Pipecat Cloud, Daily, SmallWebRTC, and transports that emit sparse audio-level events. See the Pipecat guide.
With OpenAI Realtime
import { Orb } from '@lofcz/orb-ui'
import { createOpenAIRealtimeAdapter } from '@lofcz/orb-ui/adapters'
const adapter = createOpenAIRealtimeAdapter({
getClientSecret: async () => {
const response = await fetch('/api/openai-realtime-token', { method: 'POST' })
return (await response.json()).value
},
})Create client secrets with a standard OpenAI API key on your server. See the OpenAI Realtime guide.
With Gemini Live
import { GoogleGenAI } from '@google/genai'
import { Orb } from '@lofcz/orb-ui'
import { createGeminiLiveAdapter } from '@lofcz/orb-ui/adapters'
const adapter = createGeminiLiveAdapter({
connect: async (callbacks) => {
const token = await fetch('/api/gemini-live-token', { method: 'POST' }).then((res) =>
res.json(),
)
const client = new GoogleGenAI({
apiKey: token.value,
httpOptions: { apiVersion: 'v1alpha' },
})
return client.live.connect({ model: token.model, config: token.config, callbacks })
},
})Mint one-use Gemini Live tokens on your server. See the Gemini Live guide for the matching server config that disables automatic activity detection. The adapter handles client-side turn detection by default.
The examples above show the intended happy paths. Transport overrides, custom browser runtimes, existing-session modes, and audio calibration hooks are optional and documented in the individual adapter guides.
Controlled mode (custom integration)
import { Orb } from '@lofcz/orb-ui'
import { useState } from 'react'
function App() {
const [state, setState] = useState('idle')
const [volume, setVolume] = useState(0)
return <Orb state={state} volume={volume} theme="circle" />
}Use signal when your integration has separate input and output levels:
import { Orb } from '@lofcz/orb-ui'
function App() {
return <Orb signal={{ state: 'speaking', outputVolume: 0.7 }} theme="circle" />
}External session controls
Keep the orb as a passive visual when start and stop controls belong elsewhere in your layout. Provider adapters already expose the matching lifecycle methods.
function VoiceExperience({ adapter }) {
return (
<>
<Orb adapter={adapter} theme="cloud" interactive={false} />
<button onClick={() => void adapter.start?.()}>Start conversation</button>
<button onClick={() => void adapter.stop?.()}>End conversation</button>
</>
)
}Themes
| Theme | Description |
| -------- | ----------------------------------------------------------------------------- |
| radial | Four-lobe radial field with input-reactive rim and output-reactive twisting. |
| cloud | Atmospheric sphere with inverse listening scale and faster speaking motion. |
| debug | State + volume display with start/stop. Use to verify your integration works. |
| circle | Pulsing circle that reacts to volume. |
| bars | Five bars that animate with voice. |
When an adapter or onStart/onStop handler is provided, visual themes include keyboard-accessible <button type="button"> controls. radial places its phone control below the artwork while the other clickable themes use the visual itself. Pass interactive={false} to keep the theme passive and call adapter.start() or adapter.stop() from external controls instead.
The radial phone control uses a small cutout that defaults to white. Match it to the surface behind the orb with the typed style variable:
<Orb adapter={adapter} theme="radial" style={{ '--orb-ui-radial-control-surround': '#101010' }} />Custom renderer
For an app-owned voice stack (Gemini Live, custom Web Audio), keep session control outside the orb and draw your own artwork. Orb still normalizes state, input/output volume, and activity:
import { Orb } from '@lofcz/orb-ui'
;<Orb
signal={{ state: 'listening', inputVolume: 0.4, outputVolume: 0 }}
interactive={false}
renderTheme={({ rootProps, state, activity, inputVolume, outputVolume }) => (
<div {...rootProps} data-voice-orb={state}>
{/* canvas / svg / css — activity is 0–1 for the current state */}
<span>{activity.toFixed(2)}</span>
<span>{inputVolume.toFixed(2)}</span>
<span>{outputVolume.toFixed(2)}</span>
</div>
)}
/>theme is ignored when renderTheme is set.
Props
| Prop | Type | Default | Description |
| ------------- | ----------------------------------------------------------------- | --------- | ------------------------------------------------------- |
| theme | 'debug' \| 'circle' \| 'bars' \| 'cloud' \| 'radial' \| 'ocean' | 'debug' | Visual theme |
| scheme | 'light' \| 'dark' | 'light' | Preset selector for palette-aware themes (ocean) |
| palette | OrbPalette | — | Hex color overrides for palette-aware themes (ocean) |
| signal | OrbSignal | — | Rich controlled signal with state/input/output volume |
| state | OrbState | 'idle' | Conversation state (controlled mode) |
| volume | number | 0 | Audio volume, 0–1. Overrides signal/adapter volume. |
| adapter | OrbAdapter | — | Provider adapter (manages signal updates automatically) |
| size | number | 200 | Size in pixels |
| className | string | — | Optional class name for the rendered theme |
| style | OrbStyle | — | Inline styles, including radial control surround color |
| disabled | boolean | false | Disables clickable themes and debug start/stop controls |
| interactive | boolean | true | Allows the theme control to start and stop a session |
| aria-label | string | generated | Accessible label for clickable visual themes |
| onStart | () => void | — | Custom start handler (overrides adapter.start()) |
| onStop | () => void | — | Custom stop handler (overrides adapter.stop()) |
| renderTheme | OrbThemeRenderer | — | Custom artwork; Orb still owns state and volume |
States
idle · connecting · listening · thinking · speaking · error
Supported Providers
| Provider | Adapter |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Vapi | createVapiAdapter from @lofcz/orb-ui/adapters |
| ElevenLabs | createElevenLabsAdapter from @lofcz/orb-ui/adapters |
| LiveKit | createLiveKitAdapter from @lofcz/orb-ui/adapters |
| Pipecat | createPipecatAdapter from @lofcz/orb-ui/adapters |
| OpenAI Realtime | createOpenAIRealtimeAdapter from @lofcz/orb-ui/adapters |
| Gemini Live | createGeminiLiveAdapter from @lofcz/orb-ui/adapters |
| Custom / Gemini Live (app-owned session) | Controlled mode — pass signal or state + volume, plus renderTheme |
Development
git clone https://github.com/lofcz/orb-ui.git
cd orb-ui
bun install
# Build the library
bun run build
# Run demo locally
bun run dev:demoUseful maintenance commands:
bun run check # format check, lint, typechecks, tests, library build, demo build
bun run format # format repo filesReleases are published as @lofcz/orb-ui from GitHub Actions (release.yml)
using npm trusted publishing (OIDC). Run Release and Publish to npm on main.
License
MIT © Alexander Chen
