orb-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/dist/adapters.d.ts +146 -0
- package/dist/adapters.js +149 -0
- package/dist/orb-ui.d.ts +52 -0
- package/dist/orb-ui.js +397 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# orb-ui
|
|
2
|
+
|
|
3
|
+
The simplest way to add voice UI to your React app. One install, one component, works with Vapi and ElevenLabs out of the box.
|
|
4
|
+
|
|
5
|
+
```jsx
|
|
6
|
+
import { VoiceOrb } from 'orb-ui'
|
|
7
|
+
import { createVapiAdapter } from 'orb-ui/adapters'
|
|
8
|
+
|
|
9
|
+
<VoiceOrb adapter={createVapiAdapter(vapi, { assistantId: 'your-id' })} theme="circle" />
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install orb-ui
|
|
16
|
+
# or
|
|
17
|
+
yarn add orb-ui
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### With Vapi
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
import Vapi from '@vapi-ai/web'
|
|
26
|
+
import { VoiceOrb } from 'orb-ui'
|
|
27
|
+
import { createVapiAdapter } from 'orb-ui/adapters'
|
|
28
|
+
|
|
29
|
+
const vapi = new Vapi('your-public-key')
|
|
30
|
+
const adapter = createVapiAdapter(vapi, { assistantId: 'your-assistant-id' })
|
|
31
|
+
|
|
32
|
+
function App() {
|
|
33
|
+
return <VoiceOrb adapter={adapter} theme="circle" />
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With ElevenLabs
|
|
38
|
+
|
|
39
|
+
```jsx
|
|
40
|
+
import { Conversation } from '@elevenlabs/client'
|
|
41
|
+
import { VoiceOrb } from 'orb-ui'
|
|
42
|
+
import { createElevenLabsAdapter } from 'orb-ui/adapters'
|
|
43
|
+
|
|
44
|
+
const adapter = createElevenLabsAdapter(Conversation, { agentId: 'your-agent-id' })
|
|
45
|
+
|
|
46
|
+
function App() {
|
|
47
|
+
return <VoiceOrb adapter={adapter} theme="circle" />
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Controlled mode (custom integration)
|
|
52
|
+
|
|
53
|
+
```jsx
|
|
54
|
+
import { VoiceOrb } from 'orb-ui'
|
|
55
|
+
import { useState } from 'react'
|
|
56
|
+
|
|
57
|
+
function App() {
|
|
58
|
+
const [state, setState] = useState('idle')
|
|
59
|
+
const [volume, setVolume] = useState(0)
|
|
60
|
+
|
|
61
|
+
return <VoiceOrb state={state} volume={volume} theme="circle" />
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Themes
|
|
66
|
+
|
|
67
|
+
| Theme | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `debug` | State + volume display with start/stop. Use to verify your integration works. |
|
|
70
|
+
| `circle` | Pulsing circle that reacts to volume. |
|
|
71
|
+
| `bars` | Three bars that animate with voice. |
|
|
72
|
+
|
|
73
|
+
## Props
|
|
74
|
+
|
|
75
|
+
| Prop | Type | Default | Description |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `theme` | `'debug' \| 'circle' \| 'bars'` | `'debug'` | Visual theme |
|
|
78
|
+
| `state` | `OrbState` | `'idle'` | Conversation state (controlled mode) |
|
|
79
|
+
| `volume` | `number` | `0` | Audio volume, 0–1 (controlled mode) |
|
|
80
|
+
| `adapter` | `OrbAdapter` | — | Provider adapter (manages state + volume automatically) |
|
|
81
|
+
| `size` | `number` | `200` | Size in pixels |
|
|
82
|
+
| `onStart` | `() => void` | — | Custom start handler (overrides adapter.start()) |
|
|
83
|
+
| `onStop` | `() => void` | — | Custom stop handler (overrides adapter.stop()) |
|
|
84
|
+
|
|
85
|
+
## States
|
|
86
|
+
|
|
87
|
+
`idle` · `connecting` · `listening` · `thinking` · `speaking` · `error` · `disconnected`
|
|
88
|
+
|
|
89
|
+
## Supported Providers
|
|
90
|
+
|
|
91
|
+
| Provider | Adapter |
|
|
92
|
+
|---|---|
|
|
93
|
+
| [Vapi](https://vapi.ai) | `createVapiAdapter` from `orb-ui/adapters` |
|
|
94
|
+
| [ElevenLabs](https://elevenlabs.io/conversational-ai) | `createElevenLabsAdapter` from `orb-ui/adapters` |
|
|
95
|
+
| Custom | Use controlled mode — pass `state` and `volume` directly |
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git clone https://github.com/alexanderqchen/orb-ui.git
|
|
101
|
+
cd orb-ui
|
|
102
|
+
yarn install
|
|
103
|
+
cd demo && yarn install && cd ..
|
|
104
|
+
|
|
105
|
+
# Build the library
|
|
106
|
+
yarn build
|
|
107
|
+
|
|
108
|
+
# Run demo locally
|
|
109
|
+
cd demo && yarn dev
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT © [Alexander Chen](https://github.com/alexanderqchen)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export declare interface AdapterCallbacks {
|
|
2
|
+
onStateChange: (state: OrbState) => void;
|
|
3
|
+
onVolumeChange: (volume: number) => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates an OrbAdapter for ElevenLabs Conversational AI.
|
|
8
|
+
*
|
|
9
|
+
* Unlike createVapiAdapter, this adapter owns the session lifecycle because
|
|
10
|
+
* ElevenLabs requires callbacks to be injected at Conversation.startSession()
|
|
11
|
+
* time. Use the returned start() and stop() methods with VoiceOrb's props.
|
|
12
|
+
*
|
|
13
|
+
* @param ConversationClass - The Conversation class from @elevenlabs/client
|
|
14
|
+
* @param config - agentId / signedUrl + any other startSession options
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* import { Conversation } from '@elevenlabs/client'
|
|
18
|
+
* import { VoiceOrb } from 'orb-ui'
|
|
19
|
+
* import { createElevenLabsAdapter } from 'orb-ui/adapters'
|
|
20
|
+
*
|
|
21
|
+
* const adapter = createElevenLabsAdapter(Conversation, {
|
|
22
|
+
* agentId: 'your-agent-id',
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* function App() {
|
|
26
|
+
* return (
|
|
27
|
+
* <VoiceOrb
|
|
28
|
+
* adapter={adapter}
|
|
29
|
+
* theme="circle"
|
|
30
|
+
* onStart={() => adapter.start()}
|
|
31
|
+
* onStop={() => adapter.stop()}
|
|
32
|
+
* />
|
|
33
|
+
* )
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
export declare function createElevenLabsAdapter(ConversationClass: ElevenLabsConversationClass, config: ElevenLabsConfig): ElevenLabsOrbAdapter;
|
|
37
|
+
|
|
38
|
+
export declare function createVapiAdapter(client: VapiClient, options?: VapiAdapterOptions): OrbAdapter;
|
|
39
|
+
|
|
40
|
+
declare interface ElevenLabsCallbacks {
|
|
41
|
+
onConnect?: (props: {
|
|
42
|
+
conversationId: string;
|
|
43
|
+
}) => void;
|
|
44
|
+
onDisconnect?: (details: unknown) => void;
|
|
45
|
+
onError?: (message: string, context?: unknown) => void;
|
|
46
|
+
onModeChange?: (prop: {
|
|
47
|
+
mode: ElevenLabsMode;
|
|
48
|
+
}) => void;
|
|
49
|
+
onStatusChange?: (prop: {
|
|
50
|
+
status: ElevenLabsStatus;
|
|
51
|
+
}) => void;
|
|
52
|
+
onVadScore?: (props: {
|
|
53
|
+
vadScore: number;
|
|
54
|
+
}) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare type ElevenLabsConfig = {
|
|
58
|
+
agentId?: string;
|
|
59
|
+
signedUrl?: string;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
declare interface ElevenLabsConversation {
|
|
64
|
+
endSession(): Promise<void>;
|
|
65
|
+
getInputVolume(): number;
|
|
66
|
+
getOutputVolume(): number;
|
|
67
|
+
getInputByteFrequencyData(): Uint8Array;
|
|
68
|
+
getOutputByteFrequencyData(): Uint8Array;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare interface ElevenLabsConversationClass {
|
|
72
|
+
startSession(options: ElevenLabsConfig & ElevenLabsCallbacks): Promise<ElevenLabsConversation>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
declare type ElevenLabsMode = 'speaking' | 'listening';
|
|
76
|
+
|
|
77
|
+
export declare interface ElevenLabsOrbAdapter extends OrbAdapter {
|
|
78
|
+
/** Call this from VoiceOrb's onStart prop to begin a conversation. */
|
|
79
|
+
start(): Promise<void>;
|
|
80
|
+
/** Call this from VoiceOrb's onStop prop to end the current conversation. */
|
|
81
|
+
stop(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
declare type ElevenLabsStatus = 'disconnected' | 'connecting' | 'connected' | 'disconnecting';
|
|
85
|
+
|
|
86
|
+
export declare interface OrbAdapter {
|
|
87
|
+
/**
|
|
88
|
+
* Subscribe to state and volume changes from a voice provider.
|
|
89
|
+
* Returns an unsubscribe function to clean up listeners.
|
|
90
|
+
*/
|
|
91
|
+
subscribe(callbacks: {
|
|
92
|
+
onStateChange: (state: OrbState) => void;
|
|
93
|
+
onVolumeChange: (volume: number) => void;
|
|
94
|
+
}): () => void;
|
|
95
|
+
/** Start the voice session. Called internally by VoiceOrb on click. */
|
|
96
|
+
start?: () => void | Promise<void>;
|
|
97
|
+
/** Stop the voice session. Called internally by VoiceOrb on click. */
|
|
98
|
+
stop?: () => void | Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export declare type OrbState = 'idle' | 'connecting' | 'listening' | 'thinking' | 'speaking' | 'error' | 'disconnected';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates an OrbAdapter for Vapi voice agents.
|
|
105
|
+
*
|
|
106
|
+
* State mapping:
|
|
107
|
+
* vapi.start() called (intercepted) → 'connecting'
|
|
108
|
+
* call-start → 'listening'
|
|
109
|
+
* message (final user transcript) → 'thinking'
|
|
110
|
+
* speech-start → 'speaking'
|
|
111
|
+
* speech-end → 'listening' (debounced 350 ms)
|
|
112
|
+
* call-end → 'disconnected'
|
|
113
|
+
* error → 'error'
|
|
114
|
+
*
|
|
115
|
+
* Volume: raw Vapi values are normalized (noise gate + EMA) before being
|
|
116
|
+
* passed to onVolumeChange, so themes receive a clean 0–1 signal.
|
|
117
|
+
*
|
|
118
|
+
* @param client - A Vapi instance from @vapi-ai/web
|
|
119
|
+
* @param options - Optional config (e.g. assistantId to pass to vapi.start())
|
|
120
|
+
*/
|
|
121
|
+
declare interface VapiAdapterOptions {
|
|
122
|
+
/** Assistant ID passed to vapi.start() when the orb is clicked. */
|
|
123
|
+
assistantId?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
declare interface VapiClient {
|
|
127
|
+
on(event: 'call-start', listener: () => void): void;
|
|
128
|
+
on(event: 'call-end', listener: () => void): void;
|
|
129
|
+
on(event: 'speech-start', listener: () => void): void;
|
|
130
|
+
on(event: 'speech-end', listener: () => void): void;
|
|
131
|
+
on(event: 'volume-level', listener: (volume: number) => void): void;
|
|
132
|
+
on(event: 'message', listener: (message: VapiMessage) => void): void;
|
|
133
|
+
on(event: 'error', listener: (error: unknown) => void): void;
|
|
134
|
+
removeListener(event: string, listener: (...args: unknown[]) => void): void;
|
|
135
|
+
start(...args: any[]): Promise<unknown>;
|
|
136
|
+
stop(): void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
declare interface VapiMessage {
|
|
140
|
+
type: string;
|
|
141
|
+
role?: string;
|
|
142
|
+
transcriptType?: 'partial' | 'final';
|
|
143
|
+
transcript?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { }
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
function h(e) {
|
|
2
|
+
let l = "idle", t = null;
|
|
3
|
+
function s(i) {
|
|
4
|
+
if (t && (clearTimeout(t), t = null), l === "speaking" && i === "listening") {
|
|
5
|
+
t = setTimeout(() => {
|
|
6
|
+
l = i, e(i), t = null;
|
|
7
|
+
}, 350);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
l = i, e(i);
|
|
11
|
+
}
|
|
12
|
+
function c() {
|
|
13
|
+
t && (clearTimeout(t), t = null);
|
|
14
|
+
}
|
|
15
|
+
return { emitState: s, clearTimer: c };
|
|
16
|
+
}
|
|
17
|
+
function I(e, l) {
|
|
18
|
+
var m;
|
|
19
|
+
let t = 0;
|
|
20
|
+
function s(a) {
|
|
21
|
+
const n = a < 0.12 ? 0 : (a - 0.12) / 0.88, o = n > t ? 0.65 : 0.12;
|
|
22
|
+
return t = t + (n - t) * o, t;
|
|
23
|
+
}
|
|
24
|
+
let c = null;
|
|
25
|
+
if (typeof navigator < "u" && ((m = navigator.mediaDevices) != null && m.getUserMedia)) {
|
|
26
|
+
const a = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
27
|
+
navigator.mediaDevices.getUserMedia = async (n) => {
|
|
28
|
+
const o = await a(n);
|
|
29
|
+
return n != null && n.audio && (c = o), o;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function i() {
|
|
33
|
+
c == null || c.getTracks().forEach((a) => {
|
|
34
|
+
a.readyState === "live" && a.stop();
|
|
35
|
+
}), c = null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
async start() {
|
|
39
|
+
await e.start(l == null ? void 0 : l.assistantId);
|
|
40
|
+
},
|
|
41
|
+
stop() {
|
|
42
|
+
e.stop();
|
|
43
|
+
},
|
|
44
|
+
subscribe({ onStateChange: a, onVolumeChange: n }) {
|
|
45
|
+
const { emitState: o, clearTimer: g } = h(a), d = () => o("listening"), v = () => {
|
|
46
|
+
o("disconnected"), n(0), t = 0, i();
|
|
47
|
+
}, r = () => o("speaking"), f = () => {
|
|
48
|
+
o("listening"), n(0);
|
|
49
|
+
}, E = (u) => {
|
|
50
|
+
n(s(u));
|
|
51
|
+
}, S = (u) => {
|
|
52
|
+
u.type === "transcript" && u.transcriptType === "final" && u.role === "user" && (o("thinking"), n(0));
|
|
53
|
+
}, O = (u) => {
|
|
54
|
+
console.error("[orb-ui/vapi] Error:", u), o("error"), n(0), t = 0, i();
|
|
55
|
+
};
|
|
56
|
+
e.on("call-start", d), e.on("call-end", v), e.on("speech-start", r), e.on("speech-end", f), e.on("volume-level", E), e.on("message", S), e.on("error", O);
|
|
57
|
+
const b = e.start.bind(e);
|
|
58
|
+
return e.start = async (...u) => (o("connecting"), b(...u)), () => {
|
|
59
|
+
g(), e.removeListener("call-start", d), e.removeListener("call-end", v), e.removeListener("speech-start", r), e.removeListener("speech-end", f), e.removeListener("volume-level", E), e.removeListener("message", S), e.removeListener("error", O), e.start = b, i();
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const y = 2.7, p = 0.05, M = 0.5, T = 0.15;
|
|
65
|
+
function L() {
|
|
66
|
+
let e = 0;
|
|
67
|
+
return function(t) {
|
|
68
|
+
const s = t > e ? M : T;
|
|
69
|
+
return e = e + (t - e) * s, e;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function _(e, l) {
|
|
73
|
+
let t = null, s = null, c = "listening";
|
|
74
|
+
const i = L(), m = L(), a = /* @__PURE__ */ new Set();
|
|
75
|
+
function n(r) {
|
|
76
|
+
a.forEach((f) => f.onStateChange(r));
|
|
77
|
+
}
|
|
78
|
+
function o(r) {
|
|
79
|
+
a.forEach((f) => f.onVolumeChange(r));
|
|
80
|
+
}
|
|
81
|
+
function g() {
|
|
82
|
+
s || (s = setInterval(() => {
|
|
83
|
+
if (!t) return;
|
|
84
|
+
const r = t.getOutputVolume();
|
|
85
|
+
o(i(Math.min(1, r * y)));
|
|
86
|
+
}, 33));
|
|
87
|
+
}
|
|
88
|
+
function d() {
|
|
89
|
+
s && (clearInterval(s), s = null), o(0);
|
|
90
|
+
}
|
|
91
|
+
const v = {
|
|
92
|
+
onStatusChange: ({ status: r }) => {
|
|
93
|
+
r === "connecting" && n("connecting");
|
|
94
|
+
},
|
|
95
|
+
onConnect: () => {
|
|
96
|
+
n("listening");
|
|
97
|
+
},
|
|
98
|
+
onModeChange: ({ mode: r }) => {
|
|
99
|
+
c = r, r === "speaking" ? (n("speaking"), g()) : (n("listening"), d());
|
|
100
|
+
},
|
|
101
|
+
onVadScore: ({ vadScore: r }) => {
|
|
102
|
+
if (c === "listening") {
|
|
103
|
+
const f = r < p ? 0 : (r - p) / (1 - p);
|
|
104
|
+
o(m(f));
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
onDisconnect: () => {
|
|
108
|
+
d(), n("disconnected"), o(0), t = null;
|
|
109
|
+
},
|
|
110
|
+
onError: (r) => {
|
|
111
|
+
console.error("[orb-ui/elevenlabs] Error:", r), d(), n("error"), o(0), t = null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
// ── OrbAdapter.subscribe ────────────────────────────────────────────────
|
|
116
|
+
subscribe(r) {
|
|
117
|
+
return a.add(r), () => {
|
|
118
|
+
a.delete(r), a.size === 0 && d();
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
122
|
+
async start() {
|
|
123
|
+
if (!t)
|
|
124
|
+
try {
|
|
125
|
+
t = await e.startSession({
|
|
126
|
+
...l,
|
|
127
|
+
...v
|
|
128
|
+
});
|
|
129
|
+
} catch (r) {
|
|
130
|
+
console.error("[orb-ui/elevenlabs] startSession failed:", r), n("error"), o(0);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
async stop() {
|
|
134
|
+
if (t) {
|
|
135
|
+
d();
|
|
136
|
+
try {
|
|
137
|
+
await t.endSession();
|
|
138
|
+
} catch (r) {
|
|
139
|
+
console.error("[orb-ui/elevenlabs] endSession failed:", r);
|
|
140
|
+
}
|
|
141
|
+
t = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export {
|
|
147
|
+
_ as createElevenLabsAdapter,
|
|
148
|
+
I as createVapiAdapter
|
|
149
|
+
};
|
package/dist/orb-ui.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
export declare interface OrbAdapter {
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe to state and volume changes from a voice provider.
|
|
6
|
+
* Returns an unsubscribe function to clean up listeners.
|
|
7
|
+
*/
|
|
8
|
+
subscribe(callbacks: {
|
|
9
|
+
onStateChange: (state: OrbState) => void;
|
|
10
|
+
onVolumeChange: (volume: number) => void;
|
|
11
|
+
}): () => void;
|
|
12
|
+
/** Start the voice session. Called internally by VoiceOrb on click. */
|
|
13
|
+
start?: () => void | Promise<void>;
|
|
14
|
+
/** Stop the voice session. Called internally by VoiceOrb on click. */
|
|
15
|
+
stop?: () => void | Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare type OrbState = 'idle' | 'connecting' | 'listening' | 'thinking' | 'speaking' | 'error' | 'disconnected';
|
|
19
|
+
|
|
20
|
+
export declare type OrbTheme = 'debug' | 'circle' | 'bars';
|
|
21
|
+
|
|
22
|
+
export declare function VoiceOrb({ state: stateProp, volume: volumeProp, adapter, theme, size, className, style, onStart, onStop, }: VoiceOrbProps): JSX_2.Element;
|
|
23
|
+
|
|
24
|
+
export declare interface VoiceOrbProps {
|
|
25
|
+
/**
|
|
26
|
+
* Current conversation state. Required in controlled mode (no adapter).
|
|
27
|
+
* Overrides adapter state if both are provided.
|
|
28
|
+
*/
|
|
29
|
+
state?: OrbState;
|
|
30
|
+
/**
|
|
31
|
+
* Current audio volume, normalized 0–1.
|
|
32
|
+
* Overrides adapter volume if both are provided.
|
|
33
|
+
*/
|
|
34
|
+
volume?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Provider adapter (Vapi, ElevenLabs, etc.).
|
|
37
|
+
* Handles state and volume automatically from the SDK.
|
|
38
|
+
*/
|
|
39
|
+
adapter?: OrbAdapter;
|
|
40
|
+
/** Visual theme. Defaults to 'debug'. */
|
|
41
|
+
theme?: OrbTheme;
|
|
42
|
+
/** Size in pixels. Defaults to 200. */
|
|
43
|
+
size?: number;
|
|
44
|
+
className?: string;
|
|
45
|
+
style?: React.CSSProperties;
|
|
46
|
+
/** Called when the Start button is clicked (debug theme only). */
|
|
47
|
+
onStart?: () => void;
|
|
48
|
+
/** Called when the Stop button is clicked (debug theme only). */
|
|
49
|
+
onStop?: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { }
|
package/dist/orb-ui.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { jsxs as C, jsx as i } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as y, useLayoutEffect as q, useEffect as T, useState as O, useCallback as G } from "react";
|
|
3
|
+
const N = [
|
|
4
|
+
"idle",
|
|
5
|
+
"connecting",
|
|
6
|
+
"listening",
|
|
7
|
+
"thinking",
|
|
8
|
+
"speaking",
|
|
9
|
+
"error",
|
|
10
|
+
"disconnected"
|
|
11
|
+
], I = {
|
|
12
|
+
idle: "#888",
|
|
13
|
+
connecting: "#f0c040",
|
|
14
|
+
listening: "#40c0f0",
|
|
15
|
+
thinking: "#c040f0",
|
|
16
|
+
speaking: "#40f080",
|
|
17
|
+
error: "#f04040",
|
|
18
|
+
disconnected: "#555"
|
|
19
|
+
};
|
|
20
|
+
function V({
|
|
21
|
+
state: e,
|
|
22
|
+
volume: u,
|
|
23
|
+
size: n,
|
|
24
|
+
className: x,
|
|
25
|
+
style: S,
|
|
26
|
+
onStart: g,
|
|
27
|
+
onStop: a
|
|
28
|
+
}) {
|
|
29
|
+
return /* @__PURE__ */ C(
|
|
30
|
+
"div",
|
|
31
|
+
{
|
|
32
|
+
className: x,
|
|
33
|
+
style: {
|
|
34
|
+
width: n,
|
|
35
|
+
fontFamily: "monospace",
|
|
36
|
+
fontSize: 12,
|
|
37
|
+
background: "#111",
|
|
38
|
+
color: "#ccc",
|
|
39
|
+
border: "1px solid #333",
|
|
40
|
+
borderRadius: 8,
|
|
41
|
+
padding: 12,
|
|
42
|
+
boxSizing: "border-box",
|
|
43
|
+
userSelect: "none",
|
|
44
|
+
...S
|
|
45
|
+
},
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ i("div", { style: { color: "#555", marginBottom: 10, fontSize: 10, letterSpacing: 1 }, children: "ORB DEBUG" }),
|
|
48
|
+
/* @__PURE__ */ C("div", { style: { marginBottom: 8 }, children: [
|
|
49
|
+
/* @__PURE__ */ i("span", { style: { color: "#555" }, children: "state " }),
|
|
50
|
+
/* @__PURE__ */ i("span", { style: { color: I[e], fontWeight: "bold" }, children: e })
|
|
51
|
+
] }),
|
|
52
|
+
/* @__PURE__ */ C("div", { style: { marginBottom: 10 }, children: [
|
|
53
|
+
/* @__PURE__ */ i("span", { style: { color: "#555" }, children: "volume " }),
|
|
54
|
+
/* @__PURE__ */ i("span", { style: { color: "#ccc" }, children: u.toFixed(2) }),
|
|
55
|
+
/* @__PURE__ */ i(
|
|
56
|
+
"div",
|
|
57
|
+
{
|
|
58
|
+
style: {
|
|
59
|
+
marginTop: 4,
|
|
60
|
+
height: 4,
|
|
61
|
+
background: "#222",
|
|
62
|
+
borderRadius: 2,
|
|
63
|
+
overflow: "hidden"
|
|
64
|
+
},
|
|
65
|
+
children: /* @__PURE__ */ i(
|
|
66
|
+
"div",
|
|
67
|
+
{
|
|
68
|
+
style: {
|
|
69
|
+
height: "100%",
|
|
70
|
+
width: `${u * 100}%`,
|
|
71
|
+
background: I[e],
|
|
72
|
+
borderRadius: 2,
|
|
73
|
+
transition: "width 50ms linear"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
] }),
|
|
80
|
+
/* @__PURE__ */ C("div", { style: { marginBottom: 10 }, children: [
|
|
81
|
+
/* @__PURE__ */ i("div", { style: { color: "#555", marginBottom: 4, fontSize: 10 }, children: "force state" }),
|
|
82
|
+
/* @__PURE__ */ i("div", { style: { display: "flex", flexWrap: "wrap", gap: 4 }, children: N.map((r) => /* @__PURE__ */ i(
|
|
83
|
+
"button",
|
|
84
|
+
{
|
|
85
|
+
style: {
|
|
86
|
+
fontSize: 10,
|
|
87
|
+
padding: "2px 6px",
|
|
88
|
+
background: e === r ? I[r] : "#222",
|
|
89
|
+
color: e === r ? "#000" : "#888",
|
|
90
|
+
border: `1px solid ${e === r ? I[r] : "#333"}`,
|
|
91
|
+
borderRadius: 3,
|
|
92
|
+
cursor: "pointer"
|
|
93
|
+
},
|
|
94
|
+
onClick: () => {
|
|
95
|
+
console.warn(
|
|
96
|
+
`[orb-ui debug] To force state '${r}', use controlled mode: <VoiceOrb state="${r}" />`
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
children: r
|
|
100
|
+
},
|
|
101
|
+
r
|
|
102
|
+
)) })
|
|
103
|
+
] }),
|
|
104
|
+
/* @__PURE__ */ C("div", { style: { display: "flex", gap: 6 }, children: [
|
|
105
|
+
/* @__PURE__ */ i(
|
|
106
|
+
"button",
|
|
107
|
+
{
|
|
108
|
+
onClick: g,
|
|
109
|
+
style: {
|
|
110
|
+
flex: 1,
|
|
111
|
+
padding: "4px 0",
|
|
112
|
+
background: "#1a3a1a",
|
|
113
|
+
color: "#40f080",
|
|
114
|
+
border: "1px solid #40f080",
|
|
115
|
+
borderRadius: 4,
|
|
116
|
+
cursor: "pointer",
|
|
117
|
+
fontSize: 11
|
|
118
|
+
},
|
|
119
|
+
children: "Start"
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
/* @__PURE__ */ i(
|
|
123
|
+
"button",
|
|
124
|
+
{
|
|
125
|
+
onClick: a,
|
|
126
|
+
style: {
|
|
127
|
+
flex: 1,
|
|
128
|
+
padding: "4px 0",
|
|
129
|
+
background: "#3a1a1a",
|
|
130
|
+
color: "#f04040",
|
|
131
|
+
border: "1px solid #f04040",
|
|
132
|
+
borderRadius: 4,
|
|
133
|
+
cursor: "pointer",
|
|
134
|
+
fontSize: 11
|
|
135
|
+
},
|
|
136
|
+
children: "Stop"
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
] })
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function L(e) {
|
|
145
|
+
return [
|
|
146
|
+
parseInt(e.slice(1, 3), 16),
|
|
147
|
+
parseInt(e.slice(3, 5), 16),
|
|
148
|
+
parseInt(e.slice(5, 7), 16)
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
const v = {
|
|
152
|
+
idle: "#cccccc",
|
|
153
|
+
connecting: "#cccccc",
|
|
154
|
+
listening: "#60a5fa",
|
|
155
|
+
speaking: "#a3e635",
|
|
156
|
+
thinking: "#fbbf24",
|
|
157
|
+
error: "#f87171",
|
|
158
|
+
disconnected: "#444444"
|
|
159
|
+
}, W = `
|
|
160
|
+
@keyframes orb-circle-idle-pulse {
|
|
161
|
+
from { transform: scale(1); }
|
|
162
|
+
to { transform: scale(1.06); }
|
|
163
|
+
}
|
|
164
|
+
@keyframes orb-circle-connecting-pulse {
|
|
165
|
+
from { transform: scale(1); }
|
|
166
|
+
to { transform: scale(1.06); }
|
|
167
|
+
}
|
|
168
|
+
@keyframes orb-circle-thinking-spin {
|
|
169
|
+
from { transform: rotate(0deg); }
|
|
170
|
+
to { transform: rotate(360deg); }
|
|
171
|
+
}
|
|
172
|
+
`, M = 0.88, K = 0.22, j = 0.9, D = 0.15, U = 16, Q = 10, H = 0.55;
|
|
173
|
+
function Y({ state: e, volume: u, size: n, className: x, style: S }) {
|
|
174
|
+
const g = y(null), a = y(0), r = y(u);
|
|
175
|
+
q(() => {
|
|
176
|
+
r.current = u;
|
|
177
|
+
}, [u]);
|
|
178
|
+
const f = y(1), p = y(0), A = y(L(v.idle));
|
|
179
|
+
T(() => {
|
|
180
|
+
const s = "orb-circle-keyframes";
|
|
181
|
+
if (!document.getElementById(s)) {
|
|
182
|
+
const m = document.createElement("style");
|
|
183
|
+
m.id = s, m.textContent = W, document.head.appendChild(m);
|
|
184
|
+
}
|
|
185
|
+
}, []), T(() => {
|
|
186
|
+
const s = g.current;
|
|
187
|
+
if (s)
|
|
188
|
+
if (e === "listening" || e === "speaking") {
|
|
189
|
+
const m = e === "speaking" ? M : j, $ = e === "speaking" ? K : D, h = e === "speaking" ? U : Q, c = () => {
|
|
190
|
+
const b = r.current, R = m + b * $, l = b * h;
|
|
191
|
+
f.current += (R - f.current) * H, p.current += (l - p.current) * H;
|
|
192
|
+
const t = L(v[e]), [o, d, k] = A.current;
|
|
193
|
+
A.current = [
|
|
194
|
+
o + (t[0] - o) * 0.05,
|
|
195
|
+
d + (t[1] - d) * 0.05,
|
|
196
|
+
k + (t[2] - k) * 0.05
|
|
197
|
+
];
|
|
198
|
+
const [_, F, B] = A.current.map(Math.round);
|
|
199
|
+
s.style.transform = `scale(${f.current})`, s.style.background = `rgb(${_},${F},${B})`, s.style.boxShadow = `0 0 ${p.current}px ${p.current * 0.25}px rgb(${_},${F},${B})`, s.style.animation = "none", a.current = requestAnimationFrame(c);
|
|
200
|
+
};
|
|
201
|
+
return a.current = requestAnimationFrame(c), () => {
|
|
202
|
+
cancelAnimationFrame(a.current);
|
|
203
|
+
};
|
|
204
|
+
} else
|
|
205
|
+
cancelAnimationFrame(a.current), f.current = 1, p.current = 0, A.current = L(v[e] ?? v.idle), s.style.transform = "", s.style.boxShadow = "", s.style.background = v[e] ?? v.idle, e === "idle" ? s.style.animation = "orb-circle-idle-pulse 3s ease-in-out infinite alternate" : e === "connecting" ? s.style.animation = "orb-circle-connecting-pulse 1.2s ease-in-out infinite alternate" : s.style.animation = "none";
|
|
206
|
+
}, [e]);
|
|
207
|
+
const w = n * 0.55;
|
|
208
|
+
return /* @__PURE__ */ C(
|
|
209
|
+
"div",
|
|
210
|
+
{
|
|
211
|
+
className: x,
|
|
212
|
+
style: {
|
|
213
|
+
width: n,
|
|
214
|
+
height: n,
|
|
215
|
+
display: "flex",
|
|
216
|
+
alignItems: "center",
|
|
217
|
+
justifyContent: "center",
|
|
218
|
+
position: "relative",
|
|
219
|
+
...S
|
|
220
|
+
},
|
|
221
|
+
children: [
|
|
222
|
+
/* @__PURE__ */ i(
|
|
223
|
+
"div",
|
|
224
|
+
{
|
|
225
|
+
ref: g,
|
|
226
|
+
style: {
|
|
227
|
+
width: w,
|
|
228
|
+
height: w,
|
|
229
|
+
borderRadius: "50%",
|
|
230
|
+
// Initial color — rAF overwrites this immediately on first frame
|
|
231
|
+
background: v[e]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
),
|
|
235
|
+
e === "thinking" && /* @__PURE__ */ i("div", { style: {
|
|
236
|
+
position: "absolute",
|
|
237
|
+
width: n * 0.68,
|
|
238
|
+
height: n * 0.68,
|
|
239
|
+
border: "2px dashed #fbbf24",
|
|
240
|
+
borderRadius: "50%",
|
|
241
|
+
animation: "orb-circle-thinking-spin 1.5s linear infinite",
|
|
242
|
+
pointerEvents: "none"
|
|
243
|
+
} })
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const E = 5, J = 1.4, X = Math.PI * 2 / E, P = {
|
|
249
|
+
idle: "#cccccc",
|
|
250
|
+
connecting: "#cccccc",
|
|
251
|
+
listening: "#60a5fa",
|
|
252
|
+
speaking: "#a3e635",
|
|
253
|
+
thinking: "#fbbf24",
|
|
254
|
+
error: "#f87171",
|
|
255
|
+
disconnected: "#444444"
|
|
256
|
+
};
|
|
257
|
+
function Z(e) {
|
|
258
|
+
const u = e * 0.55, n = e * 0.06;
|
|
259
|
+
return `
|
|
260
|
+
@keyframes orb-bars-wave {
|
|
261
|
+
0%, 100% { height: ${n}px; }
|
|
262
|
+
50% { height: ${u * 0.5}px; }
|
|
263
|
+
}
|
|
264
|
+
@keyframes orb-bars-wave-fast {
|
|
265
|
+
0%, 100% { height: ${n}px; }
|
|
266
|
+
50% { height: ${u * 0.5}px; }
|
|
267
|
+
}
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
function z({ state: e, volume: u, size: n, className: x, style: S }) {
|
|
271
|
+
const g = y([]), a = y(0), r = y(new Array(E).fill(0)), f = y(null), p = y(u);
|
|
272
|
+
T(() => {
|
|
273
|
+
p.current = u;
|
|
274
|
+
}, [u]), T(() => {
|
|
275
|
+
const h = "orb-bars-keyframes";
|
|
276
|
+
let c = document.getElementById(h);
|
|
277
|
+
c || (c = document.createElement("style"), c.id = h, document.head.appendChild(c)), c.textContent = Z(n), f.current = c;
|
|
278
|
+
}, [n]), T(() => {
|
|
279
|
+
const h = n * 0.55, c = n * 0.06, b = P[e], R = (l, t) => {
|
|
280
|
+
for (let o = 0; o < E; o++) {
|
|
281
|
+
const d = g.current[o];
|
|
282
|
+
d && (d.style.height = `${l[o]}px`, d.style.background = t, d.style.animation = "none");
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
if (e === "listening" || e === "speaking") {
|
|
286
|
+
const l = e === "speaking" ? 1 : 0.4, t = () => {
|
|
287
|
+
const o = p.current, d = Date.now() / 1e3;
|
|
288
|
+
for (let k = 0; k < E; k++) {
|
|
289
|
+
const _ = 0.5 + 0.15 * Math.sin(d * J * l * Math.PI * 2 + k * X), F = c + (h - c) * o * _, B = F > r.current[k] ? 0.3 : 0.2;
|
|
290
|
+
r.current[k] += (F - r.current[k]) * B;
|
|
291
|
+
}
|
|
292
|
+
R(r.current, b), a.current = requestAnimationFrame(t);
|
|
293
|
+
};
|
|
294
|
+
return a.current = requestAnimationFrame(t), () => cancelAnimationFrame(a.current);
|
|
295
|
+
}
|
|
296
|
+
if (e === "thinking") {
|
|
297
|
+
const l = () => {
|
|
298
|
+
const t = Date.now(), o = [];
|
|
299
|
+
for (let d = 0; d < E; d++)
|
|
300
|
+
o.push(c + (h - c) * 0.5 * (1 + Math.sin(t / 500 + d * 0.8)));
|
|
301
|
+
R(o, b), a.current = requestAnimationFrame(l);
|
|
302
|
+
};
|
|
303
|
+
return a.current = requestAnimationFrame(l), () => cancelAnimationFrame(a.current);
|
|
304
|
+
}
|
|
305
|
+
if (cancelAnimationFrame(a.current), e === "idle" || e === "connecting") {
|
|
306
|
+
const l = e === "idle" ? "1.8s" : "1s";
|
|
307
|
+
for (let t = 0; t < E; t++) {
|
|
308
|
+
const o = g.current[t];
|
|
309
|
+
if (!o) continue;
|
|
310
|
+
o.style.background = b, o.style.height = "";
|
|
311
|
+
const d = e === "idle" ? "orb-bars-wave" : "orb-bars-wave-fast";
|
|
312
|
+
o.style.animation = `${d} ${l} ease-in-out ${t * 0.15}s infinite`;
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
for (let l = 0; l < E; l++) {
|
|
317
|
+
const t = g.current[l];
|
|
318
|
+
t && (t.style.height = `${c}px`, t.style.background = b, t.style.animation = "none");
|
|
319
|
+
}
|
|
320
|
+
}, [e, n]);
|
|
321
|
+
const A = n * 0.055, w = n * 0.035, s = n * 0.03, m = n * 0.55, $ = n * 0.06;
|
|
322
|
+
return /* @__PURE__ */ i(
|
|
323
|
+
"div",
|
|
324
|
+
{
|
|
325
|
+
className: x,
|
|
326
|
+
style: {
|
|
327
|
+
width: n,
|
|
328
|
+
height: n,
|
|
329
|
+
display: "flex",
|
|
330
|
+
alignItems: "center",
|
|
331
|
+
justifyContent: "center",
|
|
332
|
+
gap: w,
|
|
333
|
+
...S
|
|
334
|
+
},
|
|
335
|
+
children: Array.from({ length: E }, (h, c) => /* @__PURE__ */ i(
|
|
336
|
+
"div",
|
|
337
|
+
{
|
|
338
|
+
ref: (b) => {
|
|
339
|
+
g.current[c] = b;
|
|
340
|
+
},
|
|
341
|
+
style: {
|
|
342
|
+
width: A,
|
|
343
|
+
minHeight: $,
|
|
344
|
+
maxHeight: m,
|
|
345
|
+
height: $,
|
|
346
|
+
borderRadius: s,
|
|
347
|
+
background: P[e],
|
|
348
|
+
transition: "background 0.3s ease"
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
c
|
|
352
|
+
))
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
function te({
|
|
357
|
+
state: e,
|
|
358
|
+
volume: u,
|
|
359
|
+
adapter: n,
|
|
360
|
+
theme: x = "debug",
|
|
361
|
+
size: S = 200,
|
|
362
|
+
className: g,
|
|
363
|
+
style: a,
|
|
364
|
+
onStart: r,
|
|
365
|
+
onStop: f
|
|
366
|
+
}) {
|
|
367
|
+
const [p, A] = O("idle"), [w, s] = O(0);
|
|
368
|
+
T(() => n ? n.subscribe({
|
|
369
|
+
onStateChange: A,
|
|
370
|
+
onVolumeChange: s
|
|
371
|
+
}) : void 0, [n]);
|
|
372
|
+
const m = e ?? p, $ = u ?? w, h = m !== "idle" && m !== "disconnected" && m !== "error", c = G(() => {
|
|
373
|
+
var t, o;
|
|
374
|
+
h ? f ? f() : (t = n == null ? void 0 : n.stop) == null || t.call(n) : r ? r() : (o = n == null ? void 0 : n.start) == null || o.call(n);
|
|
375
|
+
}, [n, h, r, f]), b = !!(n != null && n.start || r || f), R = { state: m, volume: $, size: S, className: g, style: a }, l = (() => {
|
|
376
|
+
switch (x) {
|
|
377
|
+
case "circle":
|
|
378
|
+
return /* @__PURE__ */ i(Y, { ...R });
|
|
379
|
+
case "bars":
|
|
380
|
+
return /* @__PURE__ */ i(z, { ...R });
|
|
381
|
+
case "debug":
|
|
382
|
+
default:
|
|
383
|
+
return /* @__PURE__ */ i(V, { ...R, onStart: r, onStop: f });
|
|
384
|
+
}
|
|
385
|
+
})();
|
|
386
|
+
return b && x !== "debug" ? /* @__PURE__ */ i(
|
|
387
|
+
"div",
|
|
388
|
+
{
|
|
389
|
+
onClick: c,
|
|
390
|
+
style: { cursor: "pointer", display: "inline-flex" },
|
|
391
|
+
children: l
|
|
392
|
+
}
|
|
393
|
+
) : l;
|
|
394
|
+
}
|
|
395
|
+
export {
|
|
396
|
+
te as VoiceOrb
|
|
397
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orb-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The simplest voice AI UI component library for React. Works with Vapi and ElevenLabs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "./dist/orb-ui.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/orb-ui.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./adapters": {
|
|
14
|
+
"import": "./dist/adapters.js",
|
|
15
|
+
"types": "./dist/adapters/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc && vite build",
|
|
23
|
+
"dev": "vite build --watch",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"lint": "eslint src",
|
|
26
|
+
"test": "vitest"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18.0.0",
|
|
30
|
+
"react-dom": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^18.3.0",
|
|
34
|
+
"@types/react-dom": "^18.3.0",
|
|
35
|
+
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
37
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
38
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
39
|
+
"eslint": "^8.57.0",
|
|
40
|
+
"typescript": "^5.4.0",
|
|
41
|
+
"vite": "^5.3.0",
|
|
42
|
+
"vite-plugin-dts": "^3.9.0",
|
|
43
|
+
"vitest": "^1.6.0"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"voice",
|
|
47
|
+
"ai",
|
|
48
|
+
"react",
|
|
49
|
+
"animation",
|
|
50
|
+
"component",
|
|
51
|
+
"vapi",
|
|
52
|
+
"elevenlabs"
|
|
53
|
+
],
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "https://github.com/alexanderqchen/orb-ui.git"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {}
|
|
60
|
+
}
|