@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.
Files changed (44) hide show
  1. package/package.json +37 -3
  2. package/src/components/AliaChatContent.tsx +313 -0
  3. package/src/components/AliaChatInput.tsx +174 -27
  4. package/src/components/AliaChatMessageList.tsx +220 -34
  5. package/src/components/AliaChatScreen.tsx +132 -0
  6. package/src/components/AliaChatSheet.tsx +36 -49
  7. package/src/components/AliaChatSuggestions.tsx +1 -1
  8. package/src/components/AliaFace.tsx +402 -0
  9. package/src/components/Markdown.tsx +288 -0
  10. package/src/components/Reasoning.tsx +154 -0
  11. package/src/components/ThinkingIndicator.tsx +134 -0
  12. package/src/components/ui/button.tsx +104 -0
  13. package/src/components/ui/chat-text-input.tsx +164 -0
  14. package/src/components/ui/dropdown-menu.tsx +2 -0
  15. package/src/components/ui/dropdown-menu.web.tsx +355 -0
  16. package/src/components/ui/prompt-input/actions.tsx +28 -0
  17. package/src/components/ui/prompt-input/add-menu.tsx +83 -0
  18. package/src/components/ui/prompt-input/attachments.tsx +183 -0
  19. package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
  20. package/src/components/ui/prompt-input/context.tsx +71 -0
  21. package/src/components/ui/prompt-input/mic-button.tsx +53 -0
  22. package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
  23. package/src/components/ui/prompt-input/submit-button.tsx +49 -0
  24. package/src/components/ui/prompt-input/textarea.tsx +64 -0
  25. package/src/components/ui/text.tsx +29 -0
  26. package/src/components/voice/AudioWaveVisualizer.tsx +377 -0
  27. package/src/components/voice/VoiceControls.tsx +180 -0
  28. package/src/components/voice/VoiceOverlay.tsx +43 -0
  29. package/src/hooks/useAliaChat.ts +278 -91
  30. package/src/hooks/useAudioLevelMonitor.ts +142 -0
  31. package/src/hooks/useAudioLevels.ts +51 -0
  32. package/src/hooks/useDocumentPicker.ts +37 -0
  33. package/src/hooks/useImagePicker.ts +38 -0
  34. package/src/hooks/useSoundEffects.ts +128 -0
  35. package/src/hooks/useSpeechToText.ts +182 -0
  36. package/src/hooks/useTTS.ts +233 -0
  37. package/src/hooks/useVoiceRoom.ts +420 -0
  38. package/src/index.ts +71 -6
  39. package/src/lib/keyboard.native.tsx +6 -0
  40. package/src/lib/keyboard.tsx +25 -0
  41. package/src/lib/utils.ts +14 -0
  42. package/src/optional-deps.d.ts +86 -0
  43. package/src/theme.ts +25 -2
  44. 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 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 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
- // Hook (for custom UIs)
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
- // Types
13
- export type { ChatMessage, ToolInvocation, AliaChatSuggestion } from './types';
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,6 @@
1
+ // Native: re-export react-native-keyboard-controller components
2
+ export {
3
+ KeyboardAwareScrollView,
4
+ KeyboardAvoidingView,
5
+ KeyboardProvider,
6
+ } from 'react-native-keyboard-controller';
@@ -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
+ }
@@ -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 type AliaColors = (typeof COLORS)['light'];
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
- return useMemo(() => COLORS[scheme ?? 'light'], [scheme]);
50
+ const key = scheme === 'dark' ? 'dark' : 'light';
51
+ return useMemo(() => ({
52
+ ...COLORS[key],
53
+ isDark: key === 'dark',
54
+ }), [key]);
32
55
  }