@alia.onl/sdk 4.1.0 → 5.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @alia.onl/sdk
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Breaking
6
+
7
+ - **Voice is now an injected capability on `AliaChatScreen` and
8
+ `AliaChatSheet`.** Pass `voiceSession={VoiceSession}` (from
9
+ `@alia.onl/sdk/voice`) to offer voice calls; without it the chat is text-only
10
+ and the microphone button is not rendered.
11
+
12
+ ```tsx
13
+ import { AliaChatScreen } from '@alia.onl/sdk';
14
+ import { VoiceSession } from '@alia.onl/sdk/voice';
15
+
16
+ <AliaChatScreen voiceSession={VoiceSession} />;
17
+ ```
18
+
19
+ `voiceSession` may be a `React.lazy` component — the chat renders it inside
20
+ its own `Suspense` boundary.
21
+
22
+ v4.0.0 moved the voice exports out of the root barrel so that text-chat
23
+ consumers would stop compiling `livekit-client`, but `AliaChatContent` still
24
+ imported `useVoiceRoom` and `useAudioLevelMonitor` directly from their
25
+ modules, and `AliaChatScreen`/`AliaChatSheet` reach it. Since this package
26
+ ships raw source, that put the whole LiveKit client (1,204,825 bytes raw,
27
+ ~250 KiB gzip) back into every text-chat consumer's module graph. The split
28
+ could not be made real while the shell owned voice, so it no longer does.
29
+
30
+ ### Added
31
+
32
+ - `VoiceSession` (`@alia.onl/sdk/voice`) — the LiveKit half of the chat as one
33
+ component. It dials on mount, ends the call on unmount, renders
34
+ `VoiceControls`, and reports room state, agent state, audio amplitude, and the
35
+ transcript back to the chat shell.
36
+ - `VoiceSessionComponent`, `VoiceSessionProps`, and `VoiceSessionState` types,
37
+ exported from both entries.
38
+
39
+ ### Fixed
40
+
41
+ - An unexpected room drop now always ends voice mode. It previously required a
42
+ non-empty pre-call transcript, so a dropped call in a fresh chat left the
43
+ voice UI up with no room behind it.
44
+
45
+ ### Internal
46
+
47
+ - `bun run --filter @alia.onl/sdk check:entries` walks the real import graph and
48
+ fails if the root entry can reach `livekit-client` (or if the voice entry
49
+ cannot). Wired into CI alongside a new SDK typecheck step.
50
+
3
51
  ## 4.1.0
4
52
 
5
53
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "Alia AI chat SDK — reusable chat UI, voice, and streaming components for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -20,6 +20,10 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
+ "scripts": {
24
+ "typecheck": "tsc --noEmit",
25
+ "check:entries": "node scripts/check-entry-isolation.mjs"
26
+ },
23
27
  "dependencies": {
24
28
  "@radix-ui/react-dropdown-menu": "^2.1.20",
25
29
  "@radix-ui/react-icons": "^1.3.2",
@@ -37,7 +41,7 @@
37
41
  "peerDependencies": {
38
42
  "react": "^19.2.3",
39
43
  "react-native": "^0.86.0",
40
- "@oxyhq/services": "^22.12.3",
44
+ "@oxyhq/services": "^23.0.1",
41
45
  "@react-native-async-storage/async-storage": "^2.2.0",
42
46
  "socket.io-client": "^4.8.3",
43
47
  "expo-font": "^57.0.0",
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Shared chat content component used by both AliaChatSheet and AliaChatScreen.
3
3
  *
4
- * Owns all chat + voice state internally. Containers only handle
5
- * layout chrome (sheet modal, full-screen SafeAreaView) and pass a `header`
6
- * render prop that receives the live mark state.
4
+ * Owns the text chat; voice is an injected capability (`voiceSession`) so that
5
+ * nothing here reaches `livekit-client` at module-evaluation time. Containers
6
+ * only handle layout chrome (sheet modal, full-screen SafeAreaView) and pass a
7
+ * `header` render prop that receives the live mark state.
7
8
  */
8
9
 
9
10
  import React, {
11
+ Suspense,
10
12
  useState,
11
13
  useCallback,
12
14
  useEffect,
@@ -18,19 +20,20 @@ import { useSharedValue } from 'react-native-reanimated';
18
20
  import type { SharedValue } from 'react-native-reanimated';
19
21
  import Volume2 from 'lucide-react-native/icons/volume-2';
20
22
  import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
21
- import { useVoiceRoom } from '../hooks/useVoiceRoom';
22
- import { useAudioLevelMonitor } from '../hooks/useAudioLevelMonitor';
23
- import { useAudioLevels } from '../hooks/useAudioLevels';
24
23
  import { useTTS } from '../hooks/useTTS';
25
24
  import { useAmbientWave } from '../hooks/useAmbientWave';
26
25
  import { VoiceOverlay } from './voice/VoiceOverlay';
27
- import { VoiceControls } from './voice/VoiceControls';
28
26
  import { AliaChatMessageList } from './AliaChatMessageList';
29
27
  import { AliaWelcomeMessage, type WelcomeSuggestion } from './AliaWelcomeMessage';
30
28
  import { PromptInput } from './ui/prompt-input/prompt-input';
31
29
  import { Button } from './ui/button';
32
30
  import type { AliaMarkState } from './AliaMark';
33
- import type { ChatMessage, VoiceMessage } from '../types';
31
+ import type {
32
+ ChatMessage,
33
+ VoiceMessage,
34
+ VoiceSessionComponent,
35
+ VoiceSessionState,
36
+ } from '../types';
34
37
  import type { Completion } from './ui/prompt-input/context';
35
38
 
36
39
  export interface AliaChatContentProps {
@@ -64,6 +67,12 @@ export interface AliaChatContentProps {
64
67
  primaryColor?: string;
65
68
  /** Dark-mode flag — forwarded to the ambient wave overlay. */
66
69
  isDarkMode?: boolean;
70
+ /**
71
+ * Voice capability — pass `VoiceSession` from `@alia.onl/sdk/voice` to offer
72
+ * voice calls. Omitted, the chat is text-only and no LiveKit code is reachable
73
+ * from this module. May be a `React.lazy` component.
74
+ */
75
+ voiceSession?: VoiceSessionComponent;
67
76
  }
68
77
 
69
78
  /** Adapt a voice message into the chat message format. */
@@ -100,6 +109,7 @@ export function AliaChatContent({
100
109
  header,
101
110
  primaryColor,
102
111
  isDarkMode,
112
+ voiceSession: VoiceSession,
103
113
  }: AliaChatContentProps) {
104
114
  // ── Chat ──
105
115
  const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
@@ -119,60 +129,36 @@ export function AliaChatContent({
119
129
  const internalScrollOffsetY = useSharedValue(0);
120
130
  const scrollOffsetY = externalScrollOffsetY ?? internalScrollOffsetY;
121
131
 
122
- // ── Voice room ──
123
- const voiceRoom = useVoiceRoom({ apiUrl });
124
- const { captureLevel, playbackLevel } = useAudioLevelMonitor(voiceRoom.room, voiceRoom.isConnected);
125
- const { waveAmplitude } = useAudioLevels({
126
- captureLevel,
127
- playbackLevel,
128
- agentState: voiceRoom.agentState,
129
- isConnected: voiceRoom.isConnected,
130
- });
131
-
132
132
  // ── TTS ──
133
133
  const tts = useTTS({ apiUrl });
134
134
 
135
135
  // ── Voice mode state ──
136
+ // `isVoiceActive` mounts the session; `voiceState` is what the session reports
137
+ // back, and stays null until the call is under way.
136
138
  const [isVoiceActive, setIsVoiceActive] = useState(false);
139
+ const [voiceState, setVoiceState] = useState<VoiceSessionState | null>(null);
137
140
  const textSnapshotRef = useRef<ChatMessage[]>([]);
138
141
 
139
142
  // Merge voice messages into chat messages
143
+ const voiceMessages = voiceState?.messages;
140
144
  useEffect(() => {
141
- if (!isVoiceActive) return;
142
- const adapted = voiceRoom.messages.map(adaptVoiceMessage);
143
- setMessages([...textSnapshotRef.current, ...adapted]);
144
- }, [voiceRoom.messages, isVoiceActive, setMessages]);
145
-
146
- // Auto-deactivate on error
147
- useEffect(() => {
148
- if (!isVoiceActive) return;
149
- if (voiceRoom.error) {
150
- deactivateVoice();
151
- } else if (voiceRoom.roomState === 'error') {
152
- deactivateVoice();
153
- }
154
- }, [voiceRoom.error, voiceRoom.roomState, isVoiceActive]);
155
-
156
- // Auto-deactivate on unexpected disconnection
157
- useEffect(() => {
158
- if (!isVoiceActive) return;
159
- if (voiceRoom.roomState === 'disconnected' && textSnapshotRef.current.length > 0) {
160
- deactivateVoice();
161
- }
162
- }, [voiceRoom.roomState, isVoiceActive]);
145
+ if (!voiceMessages) return;
146
+ setMessages([...textSnapshotRef.current, ...voiceMessages.map(adaptVoiceMessage)]);
147
+ }, [voiceMessages, setMessages]);
163
148
 
164
149
  const activateVoice = useCallback(() => {
165
- if (isVoiceActive || voiceRoom.roomState === 'connecting') return;
150
+ if (isVoiceActive) return;
166
151
  textSnapshotRef.current = [...messages];
167
152
  setIsVoiceActive(true);
168
- voiceRoom.connect();
169
- }, [isVoiceActive, voiceRoom, messages]);
153
+ }, [isVoiceActive, messages]);
170
154
 
155
+ // Unmounting the session disconnects the room; the transcript merged above
156
+ // stays in the chat.
171
157
  const deactivateVoice = useCallback(() => {
172
- voiceRoom.disconnect();
173
158
  setIsVoiceActive(false);
159
+ setVoiceState(null);
174
160
  textSnapshotRef.current = [];
175
- }, [voiceRoom]);
161
+ }, []);
176
162
 
177
163
  // ── TTS read aloud handler ──
178
164
  const handleReadAloud = useCallback(
@@ -183,10 +169,11 @@ export function AliaChatContent({
183
169
  );
184
170
 
185
171
  // ── Mark state (drives the header brand mark) ──
172
+ const voiceAgentState = voiceState?.agentState;
186
173
  const markState = useMemo<AliaMarkState>(() => {
187
174
  if (isVoiceActive) {
188
- if (voiceRoom.agentState === 'speaking') return 'writing';
189
- if (voiceRoom.agentState === 'thinking') return 'thinking';
175
+ if (voiceAgentState === 'speaking') return 'writing';
176
+ if (voiceAgentState === 'thinking') return 'thinking';
190
177
  return 'idle';
191
178
  }
192
179
  if (!isStreaming) return 'idle';
@@ -195,7 +182,7 @@ export function AliaChatContent({
195
182
  return 'working';
196
183
  if (lastMsg?.thinking) return 'thinking';
197
184
  return 'writing';
198
- }, [messages, isStreaming, isVoiceActive, voiceRoom.agentState]);
185
+ }, [messages, isStreaming, isVoiceActive, voiceAgentState]);
199
186
 
200
187
  const welcomeComponent = useMemo(() => {
201
188
  if (!welcomeGreeting || messages.length > 0) return undefined;
@@ -211,29 +198,32 @@ export function AliaChatContent({
211
198
 
212
199
  // ── Ambient wave — one persistent overlay across idle/voice/TTS/STT ──
213
200
  const wave = useAmbientWave({
214
- voice: {
215
- isActive: isVoiceActive,
216
- isConnected: voiceRoom.isConnected,
217
- agentState: voiceRoom.agentState,
218
- waveAmplitude,
219
- },
201
+ voice: voiceState
202
+ ? {
203
+ isActive: isVoiceActive,
204
+ isConnected: voiceState.isConnected,
205
+ agentState: voiceState.agentState,
206
+ waveAmplitude: voiceState.waveAmplitude,
207
+ }
208
+ : undefined,
220
209
  isTTSPlaying: tts.playbackState === 'playing',
221
210
  ttsWaveAmplitude: tts.ttsWaveAmplitude,
222
211
  isGenerating: isStreaming,
223
212
  });
224
213
 
225
- // ── Voice activate button for empty submit ──
214
+ // ── Voice activate button for empty submit (only if voice was provided) ──
226
215
  const voiceActivateButton = useMemo(
227
- () => (
228
- <Button
229
- size="icon"
230
- onPress={activateVoice}
231
- className="h-8 w-8 rounded-full"
232
- >
233
- <Volume2 size={16} color="white" />
234
- </Button>
235
- ),
236
- [activateVoice],
216
+ () =>
217
+ VoiceSession ? (
218
+ <Button
219
+ size="icon"
220
+ onPress={activateVoice}
221
+ className="h-8 w-8 rounded-full"
222
+ >
223
+ <Volume2 size={16} color="white" />
224
+ </Button>
225
+ ) : undefined,
226
+ [VoiceSession, activateVoice],
237
227
  );
238
228
 
239
229
  return (
@@ -269,20 +259,15 @@ export function AliaChatContent({
269
259
  />
270
260
 
271
261
  {/* Input or Voice Controls */}
272
- {isVoiceActive ? (
273
- <VoiceControls
274
- roomState={voiceRoom.roomState}
275
- agentState={voiceRoom.agentState}
276
- isMuted={voiceRoom.isMuted}
277
- cohostActive={voiceRoom.cohostActive}
278
- currentSpeaker={voiceRoom.currentSpeaker}
279
- roundComplete={voiceRoom.roundComplete}
280
- onToggleMute={voiceRoom.toggleMute}
281
- onEnableCohost={voiceRoom.enableCohost}
282
- onDisableCohost={voiceRoom.disableCohost}
283
- onContinueCohost={voiceRoom.continueCohost}
284
- onEnd={deactivateVoice}
285
- />
262
+ {VoiceSession && isVoiceActive ? (
263
+ /* Suspense so the session may be handed to us as a React.lazy import. */
264
+ <Suspense fallback={null}>
265
+ <VoiceSession
266
+ apiUrl={apiUrl}
267
+ onStateChange={setVoiceState}
268
+ onEnd={deactivateVoice}
269
+ />
270
+ </Suspense>
286
271
  ) : (
287
272
  <View style={styles.inputContainer}>
288
273
  <PromptInput
@@ -11,6 +11,10 @@
11
11
  * headerLeft={<BackButton />}
12
12
  * />
13
13
  * ```
14
+ *
15
+ * Voice is opt-in — pass `voiceSession={VoiceSession}` from
16
+ * `@alia.onl/sdk/voice` to offer calls. Without it the screen is text-only and
17
+ * `livekit-client` never enters the bundle.
14
18
  */
15
19
 
16
20
  import React from 'react';
@@ -19,6 +23,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
19
23
  import { AliaChatContent } from './AliaChatContent';
20
24
  import { AliaMark } from './AliaMark';
21
25
  import type { WelcomeSuggestion } from './AliaWelcomeMessage';
26
+ import type { VoiceSessionComponent } from '../types';
22
27
 
23
28
  export interface AliaChatScreenProps {
24
29
  /** App context injected into system prompt */
@@ -41,6 +46,8 @@ export interface AliaChatScreenProps {
41
46
  primaryColor?: string;
42
47
  /** Dark-mode flag — forwarded to the ambient wave overlay. */
43
48
  isDarkMode?: boolean;
49
+ /** Voice capability — `VoiceSession` from `@alia.onl/sdk/voice`. */
50
+ voiceSession?: VoiceSessionComponent;
44
51
  }
45
52
 
46
53
  export function AliaChatScreen({
@@ -54,6 +61,7 @@ export function AliaChatScreen({
54
61
  welcomeSuggestions,
55
62
  primaryColor,
56
63
  isDarkMode,
64
+ voiceSession,
57
65
  }: AliaChatScreenProps) {
58
66
  const insets = useSafeAreaInsets();
59
67
 
@@ -68,6 +76,7 @@ export function AliaChatScreen({
68
76
  welcomeSuggestions={welcomeSuggestions}
69
77
  primaryColor={primaryColor}
70
78
  isDarkMode={isDarkMode}
79
+ voiceSession={voiceSession}
71
80
  header={({ markState, hasMessages, clear }) => (
72
81
  <View className="flex-row items-center justify-between px-4 py-3">
73
82
  <View className="flex-row items-center gap-2.5">
@@ -32,6 +32,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
32
32
  import { AliaChatContent } from './AliaChatContent';
33
33
  import { AliaMark } from './AliaMark';
34
34
  import type { WelcomeSuggestion } from './AliaWelcomeMessage';
35
+ import type { VoiceSessionComponent } from '../types';
35
36
 
36
37
  const { height: SCREEN_HEIGHT } = Dimensions.get('window');
37
38
 
@@ -58,6 +59,12 @@ export interface AliaChatSheetProps {
58
59
  primaryColor?: string;
59
60
  /** Dark-mode flag — forwarded to the ambient wave overlay (defaults to the sheet's own color scheme). */
60
61
  isDarkMode?: boolean;
62
+ /**
63
+ * Voice capability — pass `VoiceSession` from `@alia.onl/sdk/voice` to offer
64
+ * voice calls. Omitted, the sheet is text-only and `livekit-client` never
65
+ * enters the bundle.
66
+ */
67
+ voiceSession?: VoiceSessionComponent;
61
68
  }
62
69
 
63
70
  export interface AliaChatSheetRef {
@@ -66,7 +73,7 @@ export interface AliaChatSheetRef {
66
73
  }
67
74
 
68
75
  export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
69
- ({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions, primaryColor, isDarkMode }, ref) => {
76
+ ({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions, primaryColor, isDarkMode, voiceSession }, ref) => {
70
77
  const scheme = useColorScheme();
71
78
  const isDark = scheme === 'dark';
72
79
  const insets = useSafeAreaInsets();
@@ -250,6 +257,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
250
257
  welcomeSuggestions={welcomeSuggestions}
251
258
  primaryColor={primaryColor}
252
259
  isDarkMode={isDarkMode ?? isDark}
260
+ voiceSession={voiceSession}
253
261
  header={({ markState, hasMessages, clear }) => (
254
262
  <View style={styles.header}>
255
263
  <View style={styles.headerLeft}>
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A live LiveKit call, mounted only while voice mode is on.
3
+ *
4
+ * This is the only place the chat UI touches `livekit-client`. The chat shell
5
+ * receives it as the `voiceSession` prop rather than importing it, so a
6
+ * text-only consumer never pulls the LiveKit client into its module graph —
7
+ * which is the whole reason the `@alia.onl/sdk/voice` entry exists.
8
+ *
9
+ * Mounting starts the call; unmounting ends it (`useVoiceRoom` tears the room
10
+ * down on unmount), so the shell only has to decide whether to render it.
11
+ */
12
+
13
+ import { useEffect, useRef } from 'react';
14
+ import { useVoiceRoom } from '../../hooks/useVoiceRoom';
15
+ import { useAudioLevelMonitor } from '../../hooks/useAudioLevelMonitor';
16
+ import { useAudioLevels } from '../../hooks/useAudioLevels';
17
+ import { VoiceControls } from './VoiceControls';
18
+ import type { VoiceSessionProps } from '../../types';
19
+
20
+ export function VoiceSession({ apiUrl, onStateChange, onEnd }: VoiceSessionProps) {
21
+ const {
22
+ room,
23
+ roomState,
24
+ agentState,
25
+ isMuted,
26
+ error,
27
+ messages,
28
+ cohostActive,
29
+ currentSpeaker,
30
+ roundComplete,
31
+ isConnected,
32
+ connect,
33
+ toggleMute,
34
+ enableCohost,
35
+ disableCohost,
36
+ continueCohost,
37
+ } = useVoiceRoom({ apiUrl });
38
+
39
+ const { captureLevel, playbackLevel } = useAudioLevelMonitor(room, isConnected);
40
+ const { waveAmplitude } = useAudioLevels({
41
+ captureLevel,
42
+ playbackLevel,
43
+ agentState,
44
+ isConnected,
45
+ });
46
+
47
+ // Dial out once per mount. The ref keeps a new `connect` identity from
48
+ // placing a second call on top of the live one.
49
+ const dialedRef = useRef(false);
50
+ useEffect(() => {
51
+ if (dialedRef.current) return;
52
+ dialedRef.current = true;
53
+ connect();
54
+ }, [connect]);
55
+
56
+ useEffect(() => {
57
+ onStateChange({ isConnected, agentState, waveAmplitude, messages });
58
+ }, [isConnected, agentState, waveAmplitude, messages, onStateChange]);
59
+
60
+ // End the call on failure, or once a room we actually reached goes away. The
61
+ // "reached" flag is what keeps the pre-connect `disconnected` state — which
62
+ // every session starts in — from ending the call before it begins.
63
+ const reachedRoomRef = useRef(false);
64
+ useEffect(() => {
65
+ if (roomState === 'connected') reachedRoomRef.current = true;
66
+ if (error || roomState === 'error') {
67
+ onEnd();
68
+ return;
69
+ }
70
+ if (roomState === 'disconnected' && reachedRoomRef.current) onEnd();
71
+ }, [error, roomState, onEnd]);
72
+
73
+ return (
74
+ <VoiceControls
75
+ roomState={roomState}
76
+ agentState={agentState}
77
+ isMuted={isMuted}
78
+ cohostActive={cohostActive}
79
+ currentSpeaker={currentSpeaker}
80
+ roundComplete={roundComplete}
81
+ onToggleMute={toggleMute}
82
+ onEnableCohost={enableCohost}
83
+ onDisableCohost={disableCohost}
84
+ onContinueCohost={continueCohost}
85
+ onEnd={onEnd}
86
+ />
87
+ );
88
+ }
package/src/index.ts CHANGED
@@ -40,6 +40,13 @@ export type {
40
40
  PlanStep,
41
41
  } from './types';
42
42
 
43
+ // ── Voice capability contract (the implementation lives in `./voice`) ──
44
+ export type {
45
+ VoiceSessionComponent,
46
+ VoiceSessionProps,
47
+ VoiceSessionState,
48
+ } from './types';
49
+
43
50
  // ── UI Primitives (NativeWind) ──
44
51
  export { Button, buttonVariants, buttonTextVariants } from './components/ui/button';
45
52
  export type { ButtonProps } from './components/ui/button';
package/src/types.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+
1
4
  // ── Welcome / Suggestion Types ──
2
5
 
3
6
  export interface WelcomeSuggestion {
@@ -93,3 +96,32 @@ export interface VoiceMessage {
93
96
  isStreaming: boolean;
94
97
  toolInvocations?: VoiceToolInvocation[];
95
98
  }
99
+
100
+ // ── Voice Session (injected capability) ──
101
+
102
+ /** Everything a live call reports back to the chat shell. */
103
+ export interface VoiceSessionState {
104
+ /** The room is up and the agent is live. */
105
+ isConnected: boolean;
106
+ agentState: AgentState;
107
+ /** Audio amplitude channel that drives the ambient wave overlay. */
108
+ waveAmplitude: SharedValue<number>;
109
+ /** Transcript so far; the shell adapts it into the chat message list. */
110
+ messages: VoiceMessage[];
111
+ }
112
+
113
+ export interface VoiceSessionProps {
114
+ /** Alia API base URL. */
115
+ apiUrl?: string;
116
+ /** Called on every room, agent, or transcript change. */
117
+ onStateChange: (state: VoiceSessionState) => void;
118
+ /** The call is over — the user ended it, it errored, or the room dropped. */
119
+ onEnd: () => void;
120
+ }
121
+
122
+ /**
123
+ * The voice capability handed to `AliaChatScreen` / `AliaChatSheet`. Pass
124
+ * `VoiceSession` from `@alia.onl/sdk/voice` to enable voice calls; leave it out
125
+ * and the chat is text-only, with `livekit-client` absent from the module graph.
126
+ */
127
+ export type VoiceSessionComponent = ComponentType<VoiceSessionProps>;
package/src/voice.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  // into their module graph.
5
5
 
6
6
  // ── Voice components ──
7
+ export { VoiceSession } from './components/voice/VoiceSession';
7
8
  export { AudioWaveVisualizer } from './components/voice/AudioWaveVisualizer';
8
9
  export { VoiceOverlay } from './components/voice/VoiceOverlay';
9
10
  export { VoiceControls } from './components/voice/VoiceControls';
@@ -23,4 +24,6 @@ export type {
23
24
  AgentState,
24
25
  VoiceMessage,
25
26
  VoiceToolInvocation,
27
+ VoiceSessionProps,
28
+ VoiceSessionState,
26
29
  } from './types';