Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/voice-orb-guides.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

Clarify voice orb discovery on the homepage and expand the ElevenLabs and React animation guides
with an interactive theme preview, customization examples, and session lifecycle guidance.
5 changes: 5 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ HTML routes to their maintained equivalents, and links directly from the homepag
use-case guides. Documentation pages use one descriptive page heading and implementation-focused
content that stays aligned with the public API.

Voice-orb discovery now builds on those routes: the homepage introduces the React voice orb while
retaining its broader voice agent UI positioning, the ElevenLabs guide explains visual choices
and includes a complete integration, and both provider and animation guides embed an interactive
theme/state preview. The animation example remains a secondary homepage documentation link.

## Experience

### More impressive themes
Expand Down
14 changes: 14 additions & 0 deletions demo/demos/voice-orb/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex,follow" />
<meta name="color-scheme" content="dark" />
<title>Voice orb animation preview — orb-ui</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/voice-orb-preview.tsx"></script>
</body>
</html>
12 changes: 6 additions & 6 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>orb-ui: React Voice Agent UI Components</title>
<title>orb-ui — React Voice Orb &amp; Voice Agent UI Components</title>
<meta
name="description"
content="React components for voice agent UIs. Add animated voice orbs, audio-reactive themes, and provider adapters for Vapi, ElevenLabs, and custom realtime voice AI apps."
content="Build a voice orb in React with orb-ui. Customize audio-reactive themes and connect ElevenLabs, Vapi, and other providers to your voice agent UI. Open source."
/>
<link rel="canonical" href="https://orb-ui.com/" />
<meta name="theme-color" content="#0a0a0a" />
Expand All @@ -19,10 +19,10 @@
<meta property="og:type" content="website" />
<meta property="og:site_name" content="orb-ui" />
<meta property="og:url" content="https://orb-ui.com/" />
<meta property="og:title" content="orb-ui: React Voice Agent UI Components" />
<meta property="og:title" content="orb-ui — React Voice Orb &amp; Voice Agent UI Components" />
<meta
property="og:description"
content="React components for voice agent UIs. Add animated voice orbs, audio-reactive themes, and provider adapters for Vapi, ElevenLabs, and custom realtime voice AI apps."
content="Build a voice orb in React with orb-ui. Customize audio-reactive themes and connect ElevenLabs, Vapi, and other providers to your voice agent UI. Open source."
/>
<meta property="og:image" content="https://orb-ui.com/og-image-v3.png" />
<meta property="og:image:type" content="image/png" />
Expand All @@ -35,10 +35,10 @@

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="orb-ui: React Voice Agent UI Components" />
<meta name="twitter:title" content="orb-ui — React Voice Orb &amp; Voice Agent UI Components" />
<meta
name="twitter:description"
content="React components for voice agent UIs. Add animated voice orbs, audio-reactive themes, and provider adapters for Vapi, ElevenLabs, and custom realtime voice AI apps."
content="Build a voice orb in React with orb-ui. Customize audio-reactive themes and connect ElevenLabs, Vapi, and other providers to your voice agent UI. Open source."
/>
<meta name="twitter:image" content="https://orb-ui.com/og-image-v3.png" />
<meta
Expand Down
8 changes: 4 additions & 4 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,8 @@ const SEO_SECTIONS = [
},
{
id: 'themes',
title: 'Themes and voice states',
copy: 'Map listening, speaking, idle, and error states into polished visual themes.',
title: 'Voice orb animation',
copy: 'Try the interactive React example, tune audio response, and choose a visual theme.',
link: '/docs/examples/voice-orb-ui',
linkLabel: 'View example',
},
Expand Down Expand Up @@ -1753,8 +1753,8 @@ export default function App() {
Voice agent UI that feels <span>alive.</span>
</h1>
<p className="hero-copy__lede">
Give realtime voice agents a visible presence with expressive themes, clear session
states, and provider adapters that all end at one React component.
Give your voice agent a voice orb that feels like your product. One React component,
expressive audio-reactive themes, and adapters for ElevenLabs, Vapi, and more.
</p>

<div className="hero-copy__actions">
Expand Down
141 changes: 141 additions & 0 deletions demo/src/voice-orb-preview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
color: #edf2ff;
background: #10131b;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
.orb-preview {
max-width: 760px;
margin: auto;
padding: 24px;
}
h1 {
font-size: 20px;
letter-spacing: -0.025em;
margin: 0 0 6px;
}
p {
margin: 0;
color: #a9b5cb;
font-size: 13px;
}
.orb-preview__surface {
min-height: 260px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
text-align: center;
}
.orb-preview__surface p {
min-height: 36px;
}
.orb-preview__choices,
.orb-preview__levels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
label,
legend {
font-size: 12px;
color: #b9c6dd;
}
label {
display: block;
min-width: 0;
}
select {
display: block;
width: 100%;
margin-top: 7px;
}
select,
button {
background: #1b2230;
color: #edf2ff;
border: 1px solid #364156;
border-radius: 7px;
padding: 9px 10px;
font: inherit;
text-transform: capitalize;
cursor: pointer;
}
fieldset {
border: 0;
padding: 0;
margin: 20px 0;
min-width: 0;
}
legend {
padding: 0 0 8px;
}
.orb-preview__states {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.orb-preview__states button {
font-size: 12px;
}
button[aria-pressed='true'] {
background: #243d64;
border-color: #91baff;
color: #fff;
}
button:hover {
border-color: #91baff;
}
:focus-visible {
outline: 2px solid #91baff;
outline-offset: 3px;
}
input[type='range'] {
display: block;
accent-color: #91baff;
width: 100%;
margin: 12px 0 0;
}
label span {
float: right;
font-variant-numeric: tabular-nums;
}
details {
margin-top: 22px;
border-top: 1px solid #2d3547;
padding-top: 16px;
}
summary {
cursor: pointer;
font-size: 12px;
color: #b9c6dd;
}
pre {
overflow: auto;
font-size: 11px;
line-height: 1.6;
padding: 12px;
background: #0a0d14;
border-radius: 8px;
}
@media (min-width: 600px) {
.orb-preview__states {
grid-template-columns: repeat(6, 1fr);
}
}
@media (max-width: 360px) {
.orb-preview {
padding: 16px;
}
.orb-preview__choices,
.orb-preview__levels {
gap: 12px;
}
}
128 changes: 128 additions & 0 deletions demo/src/voice-orb-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { StrictMode, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { Orb } from 'orb-ui'
import type { OrbSignal, OrbState, OrbThemeName, OrbThemePreset } from 'orb-ui'
import './voice-orb-preview.css'

const themes: OrbThemeName[] = ['circle', 'cloud', 'radial', 'bars']
const presets: OrbThemePreset[] = ['balanced', 'calm', 'expressive']
const states: OrbState[] = ['idle', 'connecting', 'listening', 'thinking', 'speaking', 'error']
const descriptions: Record<OrbState, string> = {
idle: 'Ready to start',
connecting: 'Connecting to the agent',
listening: 'Listening to you · input level drives the motion',
thinking: 'Preparing a response',
speaking: 'Agent speaking · output level drives the motion',
error: 'Connection failed',
}

function VoiceOrbPreview() {
const [theme, setTheme] = useState<OrbThemeName>('circle')
const [preset, setPreset] = useState<OrbThemePreset>('balanced')
const [state, setState] = useState<OrbState>('listening')
const [inputVolume, setInputVolume] = useState(0.45)
const [outputVolume, setOutputVolume] = useState(0.7)
const signal: OrbSignal = { state, inputVolume, outputVolume }
const code = `<Orb
theme={{ name: '${theme}', preset: '${preset}' }}
signal={{ state: '${state}', inputVolume: ${inputVolume}, outputVolume: ${outputVolume} }}
interactive={false}
/>`

return (
<main className="orb-preview">
<header>
<h1>Make the voice orb your own.</h1>
<p>Preview only. No microphone or live call.</p>
</header>
<div className="orb-preview__surface" aria-label="Voice orb preview">
<Orb
theme={{ name: theme, preset }}
signal={signal}
interactive={false}
size={200}
style={{ '--orb-ui-radial-control-surround': '#10131b' }}
/>
<p role="status">{descriptions[state]}</p>
</div>
<div className="orb-preview__choices">
<label>
Theme
<select value={theme} onChange={(event) => setTheme(event.target.value as OrbThemeName)}>
{themes.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</label>
<label>
Motion preset
<select
value={preset}
onChange={(event) => setPreset(event.target.value as OrbThemePreset)}
>
{presets.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</label>
</div>
<fieldset>
<legend>Voice state</legend>
<div className="orb-preview__states">
{states.map((name) => (
<button
key={name}
type="button"
aria-pressed={state === name}
onClick={() => setState(name)}
>
{name}
</button>
))}
</div>
</fieldset>
<div className="orb-preview__levels">
<label>
Input level <span>{inputVolume.toFixed(2)}</span>
<input
aria-label="Input level"
type="range"
min="0"
max="1"
step="0.01"
value={inputVolume}
onChange={(event) => setInputVolume(Number(event.target.value))}
/>
</label>
<label>
Output level <span>{outputVolume.toFixed(2)}</span>
<input
aria-label="Output level"
type="range"
min="0"
max="1"
step="0.01"
value={outputVolume}
onChange={(event) => setOutputVolume(Number(event.target.value))}
/>
</label>
</div>
<details>
<summary>React code for this preview</summary>
<pre>
<code>{code}</code>
</pre>
</details>
</main>
)
}

createRoot(document.getElementById('root')!).render(
<StrictMode>
<VoiceOrbPreview />
</StrictMode>,
)
8 changes: 8 additions & 0 deletions demo/vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@
}
],
"rewrites": [
{
"source": "/demos/voice-orb",
"destination": "/demos/voice-orb/index.html"
},
{
"source": "/demos/voice-orb/",
"destination": "/demos/voice-orb/index.html"
},
{
"source": "/playground",
"destination": "/playground/index.html"
Expand Down
5 changes: 5 additions & 0 deletions demo/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ function playgroundRoutePlugin(): Plugin {
const url = request.url ?? ''
const [pathname, query] = url.split('?')

if (pathname === '/demos/voice-orb') {
request.url = `/demos/voice-orb/${query ? `?${query}` : ''}`
}

if (
pathname === '/playground' ||
pathname === '/provider-playground' ||
Expand All @@ -36,6 +40,7 @@ export default defineConfig({
input: {
main: resolveInput('./index.html'),
playground: resolveInput('./playground/index.html'),
voiceOrb: resolveInput('./demos/voice-orb/index.html'),
},
},
},
Expand Down
Loading
Loading