@alia.onl/sdk 1.0.0 → 3.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/package.json +37 -3
- package/src/components/AliaChatContent.tsx +313 -0
- package/src/components/AliaChatInput.tsx +174 -27
- package/src/components/AliaChatMessageList.tsx +220 -34
- package/src/components/AliaChatScreen.tsx +132 -0
- package/src/components/AliaChatSheet.tsx +36 -49
- package/src/components/AliaChatSuggestions.tsx +1 -1
- package/src/components/AliaFace.tsx +402 -0
- package/src/components/Markdown.tsx +288 -0
- package/src/components/Reasoning.tsx +154 -0
- package/src/components/ThinkingIndicator.tsx +134 -0
- package/src/components/ui/button.tsx +104 -0
- package/src/components/ui/chat-text-input.tsx +164 -0
- package/src/components/ui/dropdown-menu.tsx +2 -0
- package/src/components/ui/dropdown-menu.web.tsx +355 -0
- package/src/components/ui/prompt-input/actions.tsx +28 -0
- package/src/components/ui/prompt-input/add-menu.tsx +83 -0
- package/src/components/ui/prompt-input/attachments.tsx +183 -0
- package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
- package/src/components/ui/prompt-input/context.tsx +71 -0
- package/src/components/ui/prompt-input/mic-button.tsx +53 -0
- package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
- package/src/components/ui/prompt-input/submit-button.tsx +49 -0
- package/src/components/ui/prompt-input/textarea.tsx +64 -0
- package/src/components/ui/text.tsx +29 -0
- package/src/components/voice/AudioWaveVisualizer.tsx +377 -0
- package/src/components/voice/VoiceControls.tsx +180 -0
- package/src/components/voice/VoiceOverlay.tsx +43 -0
- package/src/hooks/useAliaChat.ts +278 -91
- package/src/hooks/useAudioLevelMonitor.ts +142 -0
- package/src/hooks/useAudioLevels.ts +51 -0
- package/src/hooks/useDocumentPicker.ts +37 -0
- package/src/hooks/useImagePicker.ts +38 -0
- package/src/hooks/useSoundEffects.ts +128 -0
- package/src/hooks/useSpeechToText.ts +182 -0
- package/src/hooks/useTTS.ts +233 -0
- package/src/hooks/useVoiceRoom.ts +420 -0
- package/src/index.ts +71 -6
- package/src/lib/keyboard.native.tsx +6 -0
- package/src/lib/keyboard.tsx +25 -0
- package/src/lib/utils.ts +14 -0
- package/src/optional-deps.d.ts +86 -0
- package/src/theme.ts +25 -2
- package/src/types.ts +63 -1
|
@@ -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,82 @@
|
|
|
1
|
+
/// <reference path="./optional-deps.d.ts" />
|
|
1
2
|
// @alia.onl/sdk — Alia AI Chat SDK
|
|
2
|
-
// Reusable
|
|
3
|
+
// Reusable chat UI, voice, and streaming components for Oxy apps
|
|
3
4
|
|
|
4
|
-
// Main
|
|
5
|
+
// ── Main components ──
|
|
5
6
|
export { AliaChatSheet } from './components/AliaChatSheet';
|
|
6
7
|
export type { AliaChatSheetProps, AliaChatSheetRef } from './components/AliaChatSheet';
|
|
8
|
+
export { AliaChatScreen } from './components/AliaChatScreen';
|
|
9
|
+
export type { AliaChatScreenProps } from './components/AliaChatScreen';
|
|
7
10
|
|
|
8
|
-
//
|
|
11
|
+
// ── Chat UI components ──
|
|
12
|
+
export { AliaFace } from './components/AliaFace';
|
|
13
|
+
export type { AliaExpression, AliaFaceProps } from './components/AliaFace';
|
|
14
|
+
export { ThinkingIndicator } from './components/ThinkingIndicator';
|
|
15
|
+
export { AliaMarkdown } from './components/Markdown';
|
|
16
|
+
export { Reasoning, ReasoningTrigger, ReasoningContent } from './components/Reasoning';
|
|
17
|
+
|
|
18
|
+
// ── Voice components ──
|
|
19
|
+
export { AudioWaveVisualizer } from './components/voice/AudioWaveVisualizer';
|
|
20
|
+
export { VoiceOverlay } from './components/voice/VoiceOverlay';
|
|
21
|
+
export { VoiceControls } from './components/voice/VoiceControls';
|
|
22
|
+
|
|
23
|
+
// ── Chat hook ──
|
|
9
24
|
export { useAliaChat } from './hooks/useAliaChat';
|
|
10
25
|
export type { UseAliaChatOptions, UseAliaChatReturn } from './hooks/useAliaChat';
|
|
11
26
|
|
|
12
|
-
//
|
|
13
|
-
export
|
|
27
|
+
// ── Voice hooks ──
|
|
28
|
+
export { useVoiceRoom } from './hooks/useVoiceRoom';
|
|
29
|
+
export { useAudioLevelMonitor } from './hooks/useAudioLevelMonitor';
|
|
30
|
+
export { useAudioLevels } from './hooks/useAudioLevels';
|
|
31
|
+
export { useTTS } from './hooks/useTTS';
|
|
32
|
+
export type { UseTTSOptions } from './hooks/useTTS';
|
|
33
|
+
export { useSpeechToText, useSTTStore } from './hooks/useSpeechToText';
|
|
34
|
+
export type { UseSTTOptions } from './hooks/useSpeechToText';
|
|
35
|
+
export { useSoundEffects, useVoiceSoundEffects } from './hooks/useSoundEffects';
|
|
36
|
+
export type { SoundName, SoundSources } from './hooks/useSoundEffects';
|
|
37
|
+
|
|
38
|
+
// ── Types ──
|
|
39
|
+
export type {
|
|
40
|
+
ChatMessage,
|
|
41
|
+
ToolInvocation,
|
|
42
|
+
AliaChatSuggestion,
|
|
43
|
+
ResearchProgress,
|
|
44
|
+
PendingPlan,
|
|
45
|
+
RoomState,
|
|
46
|
+
AgentState,
|
|
47
|
+
VoiceMessage,
|
|
48
|
+
VoiceToolInvocation,
|
|
49
|
+
} from './types';
|
|
50
|
+
|
|
51
|
+
// ── UI Primitives (NativeWind) ──
|
|
52
|
+
export { Button, buttonVariants, buttonTextVariants } from './components/ui/button';
|
|
53
|
+
export type { ButtonProps } from './components/ui/button';
|
|
54
|
+
export { Text, TextClassContext } from './components/ui/text';
|
|
55
|
+
export { ChatTextInput } from './components/ui/chat-text-input';
|
|
56
|
+
export { cn, formatFileSize } from './lib/utils';
|
|
57
|
+
|
|
58
|
+
// ── PromptInput ──
|
|
59
|
+
export { PromptInput } from './components/ui/prompt-input/prompt-input';
|
|
60
|
+
export type { PromptInputProps } from './components/ui/prompt-input/prompt-input';
|
|
61
|
+
export { usePromptInput, useIsFullscreen, PromptInputContext } from './components/ui/prompt-input/context';
|
|
62
|
+
export type { Attachment, Completion, PromptInputContextType } from './components/ui/prompt-input/context';
|
|
63
|
+
export { PromptInputTextarea } from './components/ui/prompt-input/textarea';
|
|
64
|
+
export { PromptInputActions } from './components/ui/prompt-input/actions';
|
|
65
|
+
export { PromptInputSubmitButton } from './components/ui/prompt-input/submit-button';
|
|
66
|
+
export { PromptInputMicButton } from './components/ui/prompt-input/mic-button';
|
|
67
|
+
export { PromptInputAddMenu } from './components/ui/prompt-input/add-menu';
|
|
68
|
+
export { PromptInputAttachments } from './components/ui/prompt-input/attachments';
|
|
69
|
+
export { PromptInputAutocomplete } from './components/ui/prompt-input/autocomplete';
|
|
70
|
+
|
|
71
|
+
// ── Picker hooks ──
|
|
72
|
+
export { useImagePicker } from './hooks/useImagePicker';
|
|
73
|
+
export type { ImagePickerAsset } from './hooks/useImagePicker';
|
|
74
|
+
export { useDocumentPicker } from './hooks/useDocumentPicker';
|
|
75
|
+
export type { DocumentPickerResult } from './hooks/useDocumentPicker';
|
|
76
|
+
|
|
77
|
+
// ── Keyboard ──
|
|
78
|
+
export { KeyboardAwareScrollView, KeyboardAvoidingView, KeyboardProvider } from './lib/keyboard';
|
|
14
79
|
|
|
15
|
-
// Theme
|
|
80
|
+
// ── Theme ──
|
|
16
81
|
export { useAliaColors } from './theme';
|
|
17
82
|
export type { AliaColors } from './theme';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Web: re-export React Native built-in components as keyboard-controller substitutes
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ScrollView, type ScrollViewProps, KeyboardAvoidingView } from 'react-native';
|
|
4
|
+
|
|
5
|
+
// Accept native-only props so shared components don't cause TS errors
|
|
6
|
+
type KeyboardAwareScrollViewProps = ScrollViewProps & {
|
|
7
|
+
bottomOffset?: number;
|
|
8
|
+
disableScrollOnKeyboardHide?: boolean;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
extraKeyboardSpace?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const KeyboardAwareScrollView = React.forwardRef<ScrollView, KeyboardAwareScrollViewProps>(
|
|
14
|
+
({ bottomOffset, disableScrollOnKeyboardHide, enabled, extraKeyboardSpace, ...props }, ref) => (
|
|
15
|
+
<ScrollView ref={ref} {...props} />
|
|
16
|
+
)
|
|
17
|
+
);
|
|
18
|
+
KeyboardAwareScrollView.displayName = 'KeyboardAwareScrollView';
|
|
19
|
+
|
|
20
|
+
export { KeyboardAwareScrollView, KeyboardAvoidingView };
|
|
21
|
+
|
|
22
|
+
// No-op provider on web — keyboard-controller is native-only
|
|
23
|
+
export function KeyboardProvider({ children }: { children: React.ReactNode }) {
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatFileSize(bytes: number): string {
|
|
9
|
+
if (bytes === 0) return '0 B';
|
|
10
|
+
const k = 1024;
|
|
11
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
12
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
13
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
14
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// NativeWind types — extends RN components with className prop
|
|
2
|
+
/// <reference types="nativewind/types" />
|
|
3
|
+
|
|
4
|
+
// Type declarations for optional peer dependencies.
|
|
5
|
+
// These prevent TypeScript errors when the deps aren't installed.
|
|
6
|
+
// At runtime, the host app provides the actual packages.
|
|
7
|
+
//
|
|
8
|
+
// NOTE: Only declare modules for packages NOT installed in the monorepo.
|
|
9
|
+
// Installed packages (lucide-react-native, zeego, expo-image, etc.)
|
|
10
|
+
// have their own real types — ambient declarations here would shadow them.
|
|
11
|
+
|
|
12
|
+
declare module 'livekit-client' {
|
|
13
|
+
export class Room {
|
|
14
|
+
on(event: any, listener: (...args: any[]) => void): this;
|
|
15
|
+
off(event: any, listener: (...args: any[]) => void): this;
|
|
16
|
+
localParticipant: LocalParticipant;
|
|
17
|
+
remoteParticipants: Map<string, RemoteParticipant>;
|
|
18
|
+
connect(url: string, token: string, options?: any): Promise<void>;
|
|
19
|
+
disconnect(): Promise<void>;
|
|
20
|
+
state: string;
|
|
21
|
+
}
|
|
22
|
+
export class LocalParticipant {
|
|
23
|
+
publishData(data: Uint8Array, options?: DataPublishOptions): Promise<void>;
|
|
24
|
+
getTrackPublications(): Map<string, any>;
|
|
25
|
+
getTrackPublication(source: string): any;
|
|
26
|
+
setMicrophoneEnabled(enabled: boolean): Promise<void>;
|
|
27
|
+
audioTrackPublications: Map<string, any>;
|
|
28
|
+
trackPublications: Map<string, any>;
|
|
29
|
+
}
|
|
30
|
+
export class RemoteParticipant {
|
|
31
|
+
getTrackPublications(): Map<string, RemoteTrackPublication>;
|
|
32
|
+
audioTrackPublications: Map<string, RemoteTrackPublication>;
|
|
33
|
+
trackPublications: Map<string, RemoteTrackPublication>;
|
|
34
|
+
identity: string;
|
|
35
|
+
}
|
|
36
|
+
export class RemoteTrackPublication {
|
|
37
|
+
track: any;
|
|
38
|
+
source: string;
|
|
39
|
+
kind: string;
|
|
40
|
+
isSubscribed: boolean;
|
|
41
|
+
trackSid: string;
|
|
42
|
+
}
|
|
43
|
+
export interface DataPublishOptions {
|
|
44
|
+
reliable?: boolean;
|
|
45
|
+
topic?: string;
|
|
46
|
+
}
|
|
47
|
+
export const RoomEvent: Record<string, string>;
|
|
48
|
+
export const Track: { Source: Record<string, string>; Kind: Record<string, string> };
|
|
49
|
+
export const DataPacket_Kind: Record<string, number>;
|
|
50
|
+
export enum DisconnectReason {
|
|
51
|
+
UNKNOWN_REASON = 0,
|
|
52
|
+
CLIENT_INITIATED = 1,
|
|
53
|
+
DUPLICATE_IDENTITY = 2,
|
|
54
|
+
SERVER_SHUTDOWN = 3,
|
|
55
|
+
PARTICIPANT_REMOVED = 4,
|
|
56
|
+
ROOM_DELETED = 5,
|
|
57
|
+
STATE_MISMATCH = 6,
|
|
58
|
+
JOIN_FAILURE = 7,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
declare module 'expo-audio' {
|
|
63
|
+
export function createAudioPlayer(source: any): any;
|
|
64
|
+
export function useAudioRecorder(preset: any): any;
|
|
65
|
+
export function useAudioRecorderState(recorder: any, interval: number): any;
|
|
66
|
+
export function requestRecordingPermissionsAsync(): Promise<{ granted: boolean }>;
|
|
67
|
+
export function setAudioModeAsync(mode: any): Promise<void>;
|
|
68
|
+
export const RecordingPresets: { HIGH_QUALITY: any };
|
|
69
|
+
export class AudioRecorder {
|
|
70
|
+
constructor(preset: any);
|
|
71
|
+
prepareToRecordAsync(): Promise<void>;
|
|
72
|
+
record(): void;
|
|
73
|
+
stop(): Promise<void>;
|
|
74
|
+
uri: string | null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare module 'expo-clipboard' {
|
|
79
|
+
export function setStringAsync(text: string): Promise<boolean>;
|
|
80
|
+
export function getStringAsync(): Promise<string>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
declare module 'expo-image-picker' {
|
|
84
|
+
export function launchImageLibraryAsync(options?: any): Promise<any>;
|
|
85
|
+
export const MediaTypeOptions: { Images: string; Videos: string; All: string };
|
|
86
|
+
}
|
package/src/theme.ts
CHANGED
|
@@ -11,6 +11,9 @@ const COLORS = {
|
|
|
11
11
|
secondaryText: '#8E8E93',
|
|
12
12
|
icon: '#687076',
|
|
13
13
|
tint: '#0a7ea4',
|
|
14
|
+
muted: '#F4F4F5',
|
|
15
|
+
mutedForeground: '#71717A',
|
|
16
|
+
primary: '#0a7ea4',
|
|
14
17
|
},
|
|
15
18
|
dark: {
|
|
16
19
|
text: '#ECEDEE',
|
|
@@ -21,12 +24,32 @@ const COLORS = {
|
|
|
21
24
|
secondaryText: '#8E8E93',
|
|
22
25
|
icon: '#9BA1A6',
|
|
23
26
|
tint: '#fff',
|
|
27
|
+
muted: '#27272A',
|
|
28
|
+
mutedForeground: '#A1A1AA',
|
|
29
|
+
primary: '#38BDF8',
|
|
24
30
|
},
|
|
25
31
|
} as const;
|
|
26
32
|
|
|
27
|
-
export
|
|
33
|
+
export interface AliaColors {
|
|
34
|
+
text: string;
|
|
35
|
+
background: string;
|
|
36
|
+
card: string;
|
|
37
|
+
inputBackground: string;
|
|
38
|
+
border: string;
|
|
39
|
+
secondaryText: string;
|
|
40
|
+
icon: string;
|
|
41
|
+
tint: string;
|
|
42
|
+
muted: string;
|
|
43
|
+
mutedForeground: string;
|
|
44
|
+
primary: string;
|
|
45
|
+
isDark: boolean;
|
|
46
|
+
}
|
|
28
47
|
|
|
29
48
|
export function useAliaColors(): AliaColors {
|
|
30
49
|
const scheme = useColorScheme();
|
|
31
|
-
|
|
50
|
+
const key = scheme === 'dark' ? 'dark' : 'light';
|
|
51
|
+
return useMemo(() => ({
|
|
52
|
+
...COLORS[key],
|
|
53
|
+
isDark: key === 'dark',
|
|
54
|
+
}), [key]);
|
|
32
55
|
}
|