@alia.onl/sdk 1.0.0 → 2.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.
@@ -0,0 +1,233 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import {
3
+ useSharedValue,
4
+ withRepeat,
5
+ withSequence,
6
+ withTiming,
7
+ cancelAnimation,
8
+ Easing,
9
+ } from 'react-native-reanimated';
10
+ import { useOxy } from '@oxyhq/services';
11
+ import { create } from 'zustand';
12
+
13
+ const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
14
+
15
+ // ============== OPTIONS ==============
16
+
17
+ export interface UseTTSOptions {
18
+ apiUrl?: string;
19
+ accessToken?: string;
20
+ voice?: 'male' | 'female';
21
+ tone?: 'brief' | 'chill' | 'default';
22
+ }
23
+
24
+ // ============== INLINE TTS STORE ==============
25
+
26
+ type PlaybackState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
27
+
28
+ interface TTSStore {
29
+ activeMessageId: string | null;
30
+ playbackState: PlaybackState;
31
+ error: string | null;
32
+ setActiveMessage: (id: string | null) => void;
33
+ setPlaybackState: (state: PlaybackState) => void;
34
+ setError: (error: string | null) => void;
35
+ reset: () => void;
36
+ }
37
+
38
+ const useTTSStore = create<TTSStore>((set) => ({
39
+ activeMessageId: null,
40
+ playbackState: 'idle',
41
+ error: null,
42
+ setActiveMessage: (id) => set({ activeMessageId: id }),
43
+ setPlaybackState: (playbackState) => set({ playbackState }),
44
+ setError: (error) => set({ error, playbackState: error ? 'error' : 'idle' }),
45
+ reset: () => set({ activeMessageId: null, playbackState: 'idle', error: null }),
46
+ }));
47
+
48
+ // ============== HOOK ==============
49
+
50
+ export function useTTS(options: UseTTSOptions = {}) {
51
+ const apiUrl = options.apiUrl || API_URL;
52
+ const voicePref = options.voice ?? 'female';
53
+ const tonePref = options.tone ?? 'default';
54
+
55
+ const { oxyServices } = useOxy();
56
+ const {
57
+ activeMessageId,
58
+ playbackState,
59
+ error,
60
+ setActiveMessage,
61
+ setPlaybackState,
62
+ setError,
63
+ reset,
64
+ } = useTTSStore();
65
+
66
+ const playerRef = useRef<any>(null);
67
+
68
+ // Simulated wave amplitude for visualization
69
+ const ttsWaveAmplitude = useSharedValue(0);
70
+
71
+ // ============== AUTH ==============
72
+
73
+ const getToken = useCallback((): string | null => {
74
+ if (options.accessToken) return options.accessToken;
75
+ return oxyServices.httpService.getAccessToken();
76
+ }, [options.accessToken, oxyServices]);
77
+
78
+ // Animate wave when playing
79
+ useEffect(() => {
80
+ if (playbackState === 'playing') {
81
+ ttsWaveAmplitude.value = withRepeat(
82
+ withSequence(
83
+ withTiming(0.5, { duration: 800, easing: Easing.inOut(Easing.sin) }),
84
+ withTiming(0.2, { duration: 600, easing: Easing.inOut(Easing.sin) }),
85
+ withTiming(0.6, { duration: 700, easing: Easing.inOut(Easing.sin) }),
86
+ withTiming(0.15, { duration: 500, easing: Easing.inOut(Easing.sin) }),
87
+ ),
88
+ -1,
89
+ );
90
+ } else {
91
+ cancelAnimation(ttsWaveAmplitude);
92
+ ttsWaveAmplitude.value = withTiming(0, { duration: 300 });
93
+ }
94
+ }, [playbackState]);
95
+
96
+ const getTTSVoice = useCallback(() => {
97
+ return voicePref === 'male' ? 'echo' : 'nova';
98
+ }, [voicePref]);
99
+
100
+ const getTTSSpeed = useCallback(() => {
101
+ if (tonePref === 'brief') return 1.15;
102
+ if (tonePref === 'chill') return 0.9;
103
+ return 1.0;
104
+ }, [tonePref]);
105
+
106
+ const releasePlayer = useCallback(() => {
107
+ try {
108
+ playerRef.current?.remove();
109
+ } catch {}
110
+ playerRef.current = null;
111
+ }, []);
112
+
113
+ const stop = useCallback(() => {
114
+ releasePlayer();
115
+ reset();
116
+ }, [reset, releasePlayer]);
117
+
118
+ const playFromUrl = useCallback((audioUrl: string, _messageId: string) => {
119
+ releasePlayer();
120
+
121
+ (async () => {
122
+ try {
123
+ const { createAudioPlayer } = await import('expo-audio');
124
+ const player = createAudioPlayer({ uri: audioUrl });
125
+ playerRef.current = player;
126
+
127
+ player.addListener('playbackStatusUpdate', (status: any) => {
128
+ if (status.didJustFinish) {
129
+ releasePlayer();
130
+ reset();
131
+ }
132
+ });
133
+
134
+ player.play();
135
+ setPlaybackState('playing');
136
+ } catch {
137
+ setError('Audio playback not available');
138
+ }
139
+ })();
140
+ }, [releasePlayer, reset, setPlaybackState, setError]);
141
+
142
+ const readAloud = useCallback(async (
143
+ messageId: string,
144
+ text: string,
145
+ conversationId?: string,
146
+ audioUrl?: string,
147
+ ) => {
148
+ // If same message is playing, toggle pause/play
149
+ if (activeMessageId === messageId) {
150
+ if (playbackState === 'playing') {
151
+ playerRef.current?.pause();
152
+ setPlaybackState('paused');
153
+ return;
154
+ }
155
+ if (playbackState === 'paused') {
156
+ playerRef.current?.play();
157
+ setPlaybackState('playing');
158
+ return;
159
+ }
160
+ }
161
+
162
+ // Stop any current playback
163
+ if (activeMessageId) {
164
+ stop();
165
+ }
166
+
167
+ try {
168
+ setActiveMessage(messageId);
169
+ setPlaybackState('loading');
170
+
171
+ // If cached audioUrl exists, play directly
172
+ if (audioUrl) {
173
+ playFromUrl(audioUrl, messageId);
174
+ return;
175
+ }
176
+
177
+ // Call backend TTS API
178
+ const token = getToken();
179
+ if (!token) {
180
+ throw new Error('Not authenticated');
181
+ }
182
+
183
+ const response = await fetch(`${apiUrl}/v1/audio/speech`, {
184
+ method: 'POST',
185
+ headers: {
186
+ 'Content-Type': 'application/json',
187
+ Authorization: `Bearer ${token}`,
188
+ },
189
+ body: JSON.stringify({
190
+ model: 'alia-v1-voice',
191
+ input: text,
192
+ voice: getTTSVoice(),
193
+ speed: getTTSSpeed(),
194
+ conversationId,
195
+ messageId,
196
+ }),
197
+ });
198
+
199
+ if (!response.ok) {
200
+ if (response.status === 504) {
201
+ throw new Error('Request timed out — please try again');
202
+ }
203
+ const errData = await response.json().catch(() => ({}));
204
+ throw new Error(errData.error?.message || errData.error || 'TTS failed');
205
+ }
206
+
207
+ const data = await response.json();
208
+ playFromUrl(data.audioUrl, messageId);
209
+ } catch (e: any) {
210
+ console.error('[TTS] Error:', e);
211
+ setError(e.message || 'Failed to read aloud');
212
+ }
213
+ }, [activeMessageId, playbackState, getToken, apiUrl, getTTSVoice, getTTSSpeed, stop, playFromUrl, setActiveMessage, setPlaybackState, setError]);
214
+
215
+ // Cleanup on unmount
216
+ useEffect(() => {
217
+ return () => {
218
+ releasePlayer();
219
+ };
220
+ }, []);
221
+
222
+ return {
223
+ readAloud,
224
+ stop,
225
+ activeMessageId,
226
+ playbackState,
227
+ error,
228
+ ttsWaveAmplitude,
229
+ isPlaying: playbackState === 'playing',
230
+ isPaused: playbackState === 'paused',
231
+ isLoading: playbackState === 'loading',
232
+ };
233
+ }
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Hook for real-time voice conversations via LiveKit rooms.
3
+ *
4
+ * Connects to a LiveKit room (created by POST /v1/voice/token),
5
+ * publishes the user's microphone, and receives agent audio + data
6
+ * messages (transcripts, state, cohost events) over WebRTC.
7
+ */
8
+
9
+ import { useState, useCallback, useRef, useEffect } from 'react';
10
+ import {
11
+ Room,
12
+ RoomEvent,
13
+ Track,
14
+ DisconnectReason,
15
+ type RemoteTrackPublication,
16
+ type RemoteParticipant,
17
+ type DataPublishOptions,
18
+ } from 'livekit-client';
19
+ import { useOxy } from '@oxyhq/services';
20
+ import type { RoomState, AgentState, VoiceMessage, VoiceToolInvocation } from '../types';
21
+
22
+ const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
23
+
24
+ // ============== OPTIONS ==============
25
+
26
+ export interface UseVoiceRoomOptions {
27
+ apiUrl?: string;
28
+ voicePreference?: 'male' | 'female';
29
+ accessToken?: string;
30
+ }
31
+
32
+ // ============== TITLE TAG UTILITIES ==============
33
+
34
+ const TAG = String.raw`ALIA_TITLE|TITLE|TÍTULO|TITRE|TITOLO|TITEL|ЗАГОЛОВОК`;
35
+
36
+ const TITLE_STRIP_RE = new RegExp(
37
+ String.raw`\[(${TAG})\].*?\[\/\1\]|<(${TAG})>.*?<\/\2>`, 'gi',
38
+ );
39
+
40
+ const TITLE_PARTIAL_RE = new RegExp(
41
+ String.raw`\[(${TAG})\].*?(\[\/\1\])?$|<(${TAG})>.*?(<\/\3>)?$`, 'si',
42
+ );
43
+
44
+ function stripTitleTags(content: string): string {
45
+ return content.replace(TITLE_STRIP_RE, '').trim();
46
+ }
47
+
48
+ function stripTitleTagsPartial(content: string): string {
49
+ return content.replace(TITLE_STRIP_RE, '').replace(TITLE_PARTIAL_RE, '').trim();
50
+ }
51
+
52
+ // ============== INTERNAL DATA MESSAGE TYPES ==============
53
+
54
+ interface AgentStateMsg { type: 'agent.state'; state: 'listening' | 'thinking' | 'speaking'; speaker: 'primary' | 'cohost' }
55
+ interface TranscriptDeltaMsg { type: 'transcript.delta'; delta: string; speaker: 'primary' | 'cohost' }
56
+ interface TranscriptDoneMsg { type: 'transcript.done'; transcript: string; speaker: 'primary' | 'cohost' }
57
+ interface TranscriptUserMsg { type: 'transcript.user'; transcript: string }
58
+ interface CohostEnabledMsg { type: 'cohost.enabled' }
59
+ interface CohostDisabledMsg { type: 'cohost.disabled' }
60
+ interface CohostTurnMsg { type: 'cohost.turn_changed'; speaker: 'primary' | 'cohost' | 'user' }
61
+ interface CohostRoundMsg { type: 'cohost.round_complete'; turns: number }
62
+ interface ToolCallMsg { type: 'tool.call'; toolName: string; callId: string; args?: any; speaker: 'primary' | 'cohost' }
63
+ interface ToolResultMsg { type: 'tool.result'; callId: string; speaker: 'primary' | 'cohost' }
64
+ interface SessionEndedMsg { type: 'session.ended'; reason: string }
65
+ interface ErrorMsg { type: 'error'; code: string; message: string }
66
+
67
+ type AgentDataMessage =
68
+ | AgentStateMsg | TranscriptDeltaMsg | TranscriptDoneMsg | TranscriptUserMsg
69
+ | CohostEnabledMsg | CohostDisabledMsg | CohostTurnMsg | CohostRoundMsg
70
+ | ToolCallMsg | ToolResultMsg | SessionEndedMsg | ErrorMsg;
71
+
72
+ // ============== HOOK ==============
73
+
74
+ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
75
+ const apiUrl = options.apiUrl || API_URL;
76
+ const voicePref = options.voicePreference ?? 'female';
77
+
78
+ const [roomState, setRoomState] = useState<RoomState>('disconnected');
79
+ const [agentState, setAgentState] = useState<AgentState>('idle');
80
+ const [isMuted, setIsMuted] = useState(false);
81
+ const [error, setError] = useState<string | null>(null);
82
+ const [messages, setMessages] = useState<VoiceMessage[]>([]);
83
+ const [cohostActive, setCohostActive] = useState(false);
84
+ const [currentSpeaker, setCurrentSpeaker] = useState<'primary' | 'cohost' | 'user' | null>(null);
85
+ const [roundComplete, setRoundComplete] = useState(false);
86
+
87
+ const roomRef = useRef<Room | null>(null);
88
+ const sessionIdRef = useRef<string | null>(null);
89
+ const msgIdRef = useRef(0);
90
+ const sessionPrefixRef = useRef(`vm-${Date.now().toString(36)}`);
91
+
92
+ // Per-speaker streaming transcript refs
93
+ const primaryTextRef = useRef('');
94
+ const cohostTextRef = useRef('');
95
+
96
+ const { oxyServices } = useOxy();
97
+
98
+ // ============== AUTH ==============
99
+
100
+ const getToken = useCallback((): string | null => {
101
+ if (options.accessToken) return options.accessToken;
102
+ return oxyServices.httpService.getAccessToken();
103
+ }, [options.accessToken, oxyServices]);
104
+
105
+ // ============== CLEANUP ==============
106
+
107
+ const cleanup = useCallback(() => {
108
+ if (roomRef.current) {
109
+ // Detach all remote audio tracks before disconnecting
110
+ roomRef.current.remoteParticipants.forEach((p) => {
111
+ p.trackPublications.forEach((pub) => {
112
+ pub.track?.detach();
113
+ });
114
+ });
115
+ roomRef.current.disconnect();
116
+ roomRef.current = null;
117
+ }
118
+ sessionIdRef.current = null;
119
+ primaryTextRef.current = '';
120
+ cohostTextRef.current = '';
121
+ }, []);
122
+
123
+ // ============== DATA MESSAGE HANDLER ==============
124
+
125
+ const handleDataMessage = useCallback((payload: Uint8Array) => {
126
+ try {
127
+ const msg: AgentDataMessage = JSON.parse(new TextDecoder().decode(payload));
128
+
129
+ switch (msg.type) {
130
+ case 'agent.state': {
131
+ // Always update agentState so both primary and cohost drive wave animation
132
+ setAgentState(msg.state);
133
+ setCurrentSpeaker(msg.speaker);
134
+ break;
135
+ }
136
+
137
+ case 'transcript.delta': {
138
+ const textRef = msg.speaker === 'cohost' ? cohostTextRef : primaryTextRef;
139
+ textRef.current += msg.delta;
140
+ const text = stripTitleTagsPartial(textRef.current);
141
+ setMessages(prev => {
142
+ const last = prev[prev.length - 1];
143
+ if (last && last.role === 'assistant' && last.isStreaming && last.speaker === msg.speaker) {
144
+ return [...prev.slice(0, -1), { ...last, content: text }];
145
+ }
146
+ msgIdRef.current++;
147
+ return [...prev, {
148
+ id: `${sessionPrefixRef.current}-${msgIdRef.current}`,
149
+ role: 'assistant',
150
+ speaker: msg.speaker,
151
+ content: text,
152
+ timestamp: Date.now(),
153
+ isStreaming: true,
154
+ }];
155
+ });
156
+ break;
157
+ }
158
+
159
+ case 'transcript.done': {
160
+ const textRef = msg.speaker === 'cohost' ? cohostTextRef : primaryTextRef;
161
+ textRef.current = '';
162
+ const clean = stripTitleTags(msg.transcript);
163
+ setMessages(prev => {
164
+ const last = prev[prev.length - 1];
165
+ if (last && last.role === 'assistant' && last.isStreaming && last.speaker === msg.speaker) {
166
+ return [...prev.slice(0, -1), { ...last, content: clean, isStreaming: false }];
167
+ }
168
+ return prev;
169
+ });
170
+ break;
171
+ }
172
+
173
+ case 'transcript.user': {
174
+ if (!msg.transcript.trim()) break;
175
+ const transcript = msg.transcript.trim();
176
+ setMessages(prev => {
177
+ const last = prev[prev.length - 1];
178
+ // Update existing user message instead of creating duplicates
179
+ if (last && last.role === 'user') {
180
+ return [...prev.slice(0, -1), { ...last, content: transcript }];
181
+ }
182
+ msgIdRef.current++;
183
+ return [...prev, {
184
+ id: `${sessionPrefixRef.current}-${msgIdRef.current}`,
185
+ role: 'user',
186
+ content: transcript,
187
+ timestamp: Date.now(),
188
+ isStreaming: false,
189
+ }];
190
+ });
191
+ break;
192
+ }
193
+
194
+ case 'cohost.enabled':
195
+ setCohostActive(true);
196
+ break;
197
+
198
+ case 'cohost.disabled':
199
+ setCohostActive(false);
200
+ setCurrentSpeaker(null);
201
+ break;
202
+
203
+ case 'cohost.turn_changed':
204
+ setCurrentSpeaker(msg.speaker);
205
+ break;
206
+
207
+ case 'cohost.round_complete':
208
+ setRoundComplete(true);
209
+ break;
210
+
211
+ case 'tool.call': {
212
+ const tool: VoiceToolInvocation = { toolCallId: msg.callId, toolName: msg.toolName, state: 'call', args: msg.args };
213
+ setMessages(prev => {
214
+ const last = prev[prev.length - 1];
215
+ if (last && last.role === 'assistant' && last.speaker === msg.speaker) {
216
+ return [...prev.slice(0, -1), {
217
+ ...last,
218
+ toolInvocations: [...(last.toolInvocations || []), tool],
219
+ }];
220
+ }
221
+ msgIdRef.current++;
222
+ return [...prev, {
223
+ id: `${sessionPrefixRef.current}-${msgIdRef.current}`,
224
+ role: 'assistant' as const,
225
+ speaker: msg.speaker,
226
+ content: '',
227
+ timestamp: Date.now(),
228
+ isStreaming: true,
229
+ toolInvocations: [tool],
230
+ }];
231
+ });
232
+ break;
233
+ }
234
+
235
+ case 'tool.result': {
236
+ setMessages(prev => prev.map(m => {
237
+ if (!m.toolInvocations) return m;
238
+ const updated = m.toolInvocations.map(t =>
239
+ t.toolCallId === msg.callId ? { ...t, state: 'result' as const } : t
240
+ );
241
+ return { ...m, toolInvocations: updated };
242
+ }));
243
+ break;
244
+ }
245
+
246
+ case 'session.ended': {
247
+ cleanup();
248
+ setRoomState('disconnected');
249
+ setAgentState('idle');
250
+ const reason = msg.reason;
251
+ setError(
252
+ reason === 'user_silent' ? 'Call ended due to inactivity'
253
+ : reason === 'user_unresponsive' ? 'Call ended — no response'
254
+ : reason === 'max_duration_exceeded' ? 'Voice minutes limit reached. Upgrade for more.'
255
+ : reason === 'credits_exhausted' ? 'Not enough credits to continue. Add more or upgrade your plan.'
256
+ : 'Voice session ended'
257
+ );
258
+ break;
259
+ }
260
+
261
+ case 'error':
262
+ setError(msg.message);
263
+ break;
264
+ }
265
+ } catch {
266
+ // Ignore non-JSON data
267
+ }
268
+ }, [cleanup]);
269
+
270
+ // ============== CONNECT ==============
271
+
272
+ const connect = useCallback(async () => {
273
+ try {
274
+ setError(null);
275
+ setRoomState('connecting');
276
+
277
+ const token = getToken();
278
+ if (!token) {
279
+ setError('Not authenticated');
280
+ setRoomState('error');
281
+ return;
282
+ }
283
+
284
+ // Request session creation from backend
285
+ const resp = await fetch(`${apiUrl}/v1/voice/token`, {
286
+ method: 'POST',
287
+ headers: {
288
+ 'Content-Type': 'application/json',
289
+ Authorization: `Bearer ${token}`,
290
+ },
291
+ body: JSON.stringify({
292
+ model: 'alia-v1-voice',
293
+ voice: voicePref === 'male' ? 'echo' : 'nova',
294
+ }),
295
+ });
296
+
297
+ if (!resp.ok) {
298
+ const body = await resp.json().catch(() => ({}));
299
+ const errMsg = body?.error?.message || body?.error || `Voice session failed (${resp.status})`;
300
+ setError(errMsg);
301
+ setRoomState('error');
302
+ return;
303
+ }
304
+
305
+ const { token: livekitToken, url, roomName, sessionId } = await resp.json();
306
+ sessionIdRef.current = sessionId;
307
+
308
+ // Create LiveKit room and connect
309
+ const room = new Room();
310
+ roomRef.current = room;
311
+
312
+ // Event: remote audio tracks (agent speaking) — must attach to play
313
+ room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
314
+ if (track.kind === Track.Kind.Audio) {
315
+ track.attach();
316
+ }
317
+ });
318
+
319
+ room.on(RoomEvent.TrackUnsubscribed, (track) => {
320
+ track.detach();
321
+ });
322
+
323
+ // Event: data messages from agent
324
+ room.on(RoomEvent.DataReceived, (payload: Uint8Array) => {
325
+ handleDataMessage(payload);
326
+ });
327
+
328
+ // Event: disconnected
329
+ room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {
330
+ setRoomState('disconnected');
331
+ setAgentState('idle');
332
+ setCohostActive(false);
333
+ });
334
+
335
+ // Connect to the room
336
+ await room.connect(url, livekitToken, { autoSubscribe: true });
337
+
338
+ // Enable microphone — audio goes to LiveKit, agent picks it up server-side
339
+ await room.localParticipant.setMicrophoneEnabled(true);
340
+
341
+ setRoomState('connected');
342
+ setAgentState('listening');
343
+
344
+ } catch (e: any) {
345
+ console.error('[useVoiceRoom] Connection error:', e);
346
+ setError(e.message || 'Failed to connect');
347
+ setRoomState('error');
348
+ cleanup();
349
+ }
350
+ }, [getToken, cleanup, handleDataMessage, apiUrl, voicePref]);
351
+
352
+ // ============== DISCONNECT ==============
353
+
354
+ const disconnect = useCallback(() => {
355
+ cleanup();
356
+ setRoomState('disconnected');
357
+ setAgentState('idle');
358
+ setError(null);
359
+ setIsMuted(false);
360
+ setMessages([]);
361
+ setCohostActive(false);
362
+ setCurrentSpeaker(null);
363
+ setRoundComplete(false);
364
+ }, [cleanup]);
365
+
366
+ // ============== MUTE ==============
367
+
368
+ const toggleMute = useCallback(() => {
369
+ const room = roomRef.current;
370
+ if (!room) return;
371
+ const next = !isMuted;
372
+ room.localParticipant.setMicrophoneEnabled(!next);
373
+ setIsMuted(next);
374
+ }, [isMuted]);
375
+
376
+ // ============== COHOST CONTROLS ==============
377
+
378
+ const sendClientData = useCallback((data: object) => {
379
+ const room = roomRef.current;
380
+ if (!room?.localParticipant) return;
381
+ const encoded = new TextEncoder().encode(JSON.stringify(data));
382
+ room.localParticipant.publishData(encoded, { reliable: true } as DataPublishOptions);
383
+ }, []);
384
+
385
+ const enableCohost = useCallback(() => {
386
+ sendClientData({ type: 'cohost.enable' });
387
+ }, [sendClientData]);
388
+
389
+ const disableCohost = useCallback(() => {
390
+ sendClientData({ type: 'cohost.disable' });
391
+ }, [sendClientData]);
392
+
393
+ const continueCohost = useCallback(() => {
394
+ setRoundComplete(false);
395
+ sendClientData({ type: 'cohost.continue' });
396
+ }, [sendClientData]);
397
+
398
+ // ============== CLEANUP ON UNMOUNT ==============
399
+
400
+ useEffect(() => cleanup, [cleanup]);
401
+
402
+ return {
403
+ room: roomRef.current,
404
+ roomState,
405
+ agentState,
406
+ isMuted,
407
+ error,
408
+ messages,
409
+ cohostActive,
410
+ currentSpeaker,
411
+ roundComplete,
412
+ connect,
413
+ disconnect,
414
+ toggleMute,
415
+ enableCohost,
416
+ disableCohost,
417
+ continueCohost,
418
+ isConnected: roomState === 'connected',
419
+ };
420
+ }
package/src/index.ts CHANGED
@@ -1,17 +1,51 @@
1
+ /// <reference path="./optional-deps.d.ts" />
1
2
  // @alia.onl/sdk — Alia AI Chat SDK
2
- // Reusable bottom sheet chat component for any Oxy ecosystem app
3
+ // Reusable chat UI, voice, and streaming components for Oxy apps
3
4
 
4
- // Main component
5
+ // ── Main component ──
5
6
  export { AliaChatSheet } from './components/AliaChatSheet';
6
7
  export type { AliaChatSheetProps, AliaChatSheetRef } from './components/AliaChatSheet';
7
8
 
8
- // Hook (for custom UIs)
9
+ // ── Chat UI components ──
10
+ export { AliaFace } from './components/AliaFace';
11
+ export type { AliaExpression, AliaFaceProps } from './components/AliaFace';
12
+ export { ThinkingIndicator } from './components/ThinkingIndicator';
13
+ export { AliaMarkdown } from './components/Markdown';
14
+ export { Reasoning, ReasoningTrigger, ReasoningContent } from './components/Reasoning';
15
+
16
+ // ── Voice components ──
17
+ export { AudioWaveVisualizer } from './components/voice/AudioWaveVisualizer';
18
+ export { VoiceOverlay } from './components/voice/VoiceOverlay';
19
+ export { VoiceControls } from './components/voice/VoiceControls';
20
+
21
+ // ── Chat hook ──
9
22
  export { useAliaChat } from './hooks/useAliaChat';
10
23
  export type { UseAliaChatOptions, UseAliaChatReturn } from './hooks/useAliaChat';
11
24
 
12
- // Types
13
- export type { ChatMessage, ToolInvocation, AliaChatSuggestion } from './types';
25
+ // ── Voice hooks ──
26
+ export { useVoiceRoom } from './hooks/useVoiceRoom';
27
+ export { useAudioLevelMonitor } from './hooks/useAudioLevelMonitor';
28
+ export { useAudioLevels } from './hooks/useAudioLevels';
29
+ export { useTTS } from './hooks/useTTS';
30
+ export type { UseTTSOptions } from './hooks/useTTS';
31
+ export { useSpeechToText } from './hooks/useSpeechToText';
32
+ export type { UseSTTOptions } from './hooks/useSpeechToText';
33
+ export { useSoundEffects, useVoiceSoundEffects } from './hooks/useSoundEffects';
34
+ export type { SoundName, SoundSources } from './hooks/useSoundEffects';
35
+
36
+ // ── Types ──
37
+ export type {
38
+ ChatMessage,
39
+ ToolInvocation,
40
+ AliaChatSuggestion,
41
+ ResearchProgress,
42
+ PendingPlan,
43
+ RoomState,
44
+ AgentState,
45
+ VoiceMessage,
46
+ VoiceToolInvocation,
47
+ } from './types';
14
48
 
15
- // Theme
49
+ // ── Theme ──
16
50
  export { useAliaColors } from './theme';
17
51
  export type { AliaColors } from './theme';