@alia.onl/sdk 3.1.0 → 3.1.2

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.
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import { View, Text, Pressable, ActivityIndicator, StyleSheet } from 'react-native';
7
+ import { Mic, MicOff, PhoneOff, Users } from 'lucide-react-native';
7
8
  import type { RoomState, AgentState } from '../../types';
8
- import { useAliaColors } from '../../theme';
9
9
 
10
10
  interface VoiceControlsProps {
11
11
  roomState: RoomState;
@@ -19,6 +19,8 @@ interface VoiceControlsProps {
19
19
  onDisableCohost: () => void;
20
20
  onContinueCohost: () => void;
21
21
  onEnd: () => void;
22
+ /** Override theme primary color (resolved hex/hsl value, not CSS var) */
23
+ primaryColor?: string;
22
24
  }
23
25
 
24
26
  function getStatusText(
@@ -52,14 +54,14 @@ export function VoiceControls({
52
54
  onDisableCohost,
53
55
  onContinueCohost,
54
56
  onEnd,
57
+ primaryColor,
55
58
  }: VoiceControlsProps) {
56
- const colors = useAliaColors();
57
59
  const statusText = getStatusText(roomState, agentState, isMuted, cohostActive, currentSpeaker);
58
60
 
59
61
  return (
60
62
  <View style={styles.container}>
61
63
  {statusText ? (
62
- <Text style={[styles.statusText, { color: colors.text }]}>
64
+ <Text className="text-lg font-medium mb-4 text-foreground">
63
65
  {statusText}
64
66
  </Text>
65
67
  ) : null}
@@ -67,9 +69,12 @@ export function VoiceControls({
67
69
  {roundComplete && (
68
70
  <Pressable
69
71
  onPress={onContinueCohost}
70
- style={styles.continueButton}
72
+ className="bg-primary/30"
73
+ style={[styles.continueButton, primaryColor ? { backgroundColor: primaryColor + '4D' } : undefined]}
71
74
  >
72
- <Text style={styles.continueText}>Continue conversation</Text>
75
+ <Text className="text-sm font-medium text-primary" style={primaryColor ? { color: primaryColor } : undefined}>
76
+ Continue conversation
77
+ </Text>
73
78
  </Pressable>
74
79
  )}
75
80
 
@@ -78,16 +83,16 @@ export function VoiceControls({
78
83
  <View style={styles.buttonWrapper}>
79
84
  <Pressable
80
85
  onPress={onToggleMute}
81
- style={[
82
- styles.button,
83
- { backgroundColor: isMuted ? '#ef4444' : 'rgba(255,255,255,0.15)' },
84
- ]}
86
+ className={isMuted ? undefined : 'bg-muted'}
87
+ style={[styles.button, isMuted ? { backgroundColor: '#ef4444' } : undefined]}
85
88
  >
86
- <Text style={styles.buttonIcon}>
87
- {isMuted ? '\u{1F507}' : '\u{1F3A4}'}
88
- </Text>
89
+ {isMuted ? (
90
+ <MicOff size={24} color="white" />
91
+ ) : (
92
+ <Mic size={24} color="white" />
93
+ )}
89
94
  </Pressable>
90
- <Text style={styles.buttonLabel}>
95
+ <Text className="text-xs text-muted-foreground">
91
96
  {isMuted ? 'Unmute' : 'Mute'}
92
97
  </Text>
93
98
  </View>
@@ -95,14 +100,15 @@ export function VoiceControls({
95
100
  <View style={styles.buttonWrapper}>
96
101
  <Pressable
97
102
  onPress={cohostActive ? onDisableCohost : onEnableCohost}
103
+ className={cohostActive ? 'bg-primary' : 'bg-muted'}
98
104
  style={[
99
105
  styles.button,
100
- { backgroundColor: cohostActive ? '#8b5cf6' : 'rgba(255,255,255,0.15)' },
106
+ cohostActive && primaryColor ? { backgroundColor: primaryColor } : undefined,
101
107
  ]}
102
108
  >
103
- <Text style={styles.buttonIcon}>{'\u{1F465}'}</Text>
109
+ <Users size={24} color="white" />
104
110
  </Pressable>
105
- <Text style={styles.buttonLabel}>
111
+ <Text className="text-xs text-muted-foreground">
106
112
  {cohostActive ? 'Solo' : 'Cohost'}
107
113
  </Text>
108
114
  </View>
@@ -112,15 +118,15 @@ export function VoiceControls({
112
118
  onPress={onEnd}
113
119
  style={[styles.button, { backgroundColor: '#ef4444' }]}
114
120
  >
115
- <Text style={styles.endIcon}>{'\u2715'}</Text>
121
+ <PhoneOff size={24} color="white" />
116
122
  </Pressable>
117
- <Text style={styles.buttonLabel}>End</Text>
123
+ <Text className="text-xs text-muted-foreground">End</Text>
118
124
  </View>
119
125
  </View>
120
126
  )}
121
127
 
122
128
  {roomState === 'connecting' && (
123
- <ActivityIndicator size="large" color="#38bdf8" />
129
+ <ActivityIndicator size="large" className="text-muted-foreground" />
124
130
  )}
125
131
  </View>
126
132
  );
@@ -132,11 +138,6 @@ const styles = StyleSheet.create({
132
138
  paddingBottom: 24,
133
139
  paddingTop: 16,
134
140
  },
135
- statusText: {
136
- fontSize: 18,
137
- fontWeight: '500',
138
- marginBottom: 16,
139
- },
140
141
  buttonRow: {
141
142
  flexDirection: 'row',
142
143
  alignItems: 'center',
@@ -153,28 +154,10 @@ const styles = StyleSheet.create({
153
154
  alignItems: 'center',
154
155
  justifyContent: 'center',
155
156
  },
156
- buttonIcon: {
157
- fontSize: 24,
158
- },
159
- endIcon: {
160
- fontSize: 24,
161
- color: '#ffffff',
162
- fontWeight: '700',
163
- },
164
- buttonLabel: {
165
- color: '#8E8E93',
166
- fontSize: 12,
167
- },
168
157
  continueButton: {
169
158
  marginBottom: 16,
170
159
  paddingHorizontal: 20,
171
160
  paddingVertical: 8,
172
161
  borderRadius: 24,
173
- backgroundColor: 'rgba(139, 92, 246, 0.3)',
174
- },
175
- continueText: {
176
- color: '#a78bfa',
177
- fontSize: 14,
178
- fontWeight: '500',
179
162
  },
180
163
  });
@@ -14,9 +14,11 @@ interface VoiceOverlayProps {
14
14
  waveAmplitude: SharedValue<number>;
15
15
  agentState: AgentState;
16
16
  isConnected: boolean;
17
+ primaryColor?: string;
18
+ isDarkMode?: boolean;
17
19
  }
18
20
 
19
- export function VoiceOverlay({ waveAmplitude, agentState, isConnected }: VoiceOverlayProps) {
21
+ export function VoiceOverlay({ waveAmplitude, agentState, isConnected, primaryColor, isDarkMode }: VoiceOverlayProps) {
20
22
  return (
21
23
  <Animated.View
22
24
  entering={FadeIn.duration(400)}
@@ -36,6 +38,8 @@ export function VoiceOverlay({ waveAmplitude, agentState, isConnected }: VoiceOv
36
38
  waveAmplitude={waveAmplitude}
37
39
  agentState={agentState}
38
40
  isConnected={isConnected}
41
+ primaryColor={primaryColor}
42
+ isDarkMode={isDarkMode}
39
43
  />
40
44
  </View>
41
45
  </Animated.View>
@@ -21,6 +21,10 @@ import type { RoomState, AgentState, VoiceMessage, VoiceToolInvocation } from '.
21
21
 
22
22
  const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
23
23
 
24
+ /** On web, audio tracks must be attached to a DOM element to play.
25
+ * On native (React Native), the WebRTC layer plays them automatically. */
26
+ const hasDOM = typeof document !== 'undefined';
27
+
24
28
  // ============== OPTIONS ==============
25
29
 
26
30
  export interface UseVoiceRoomOptions {
@@ -106,12 +110,14 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
106
110
 
107
111
  const cleanup = useCallback(() => {
108
112
  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
+ // On web, detach all remote audio tracks before disconnecting
114
+ if (hasDOM) {
115
+ roomRef.current.remoteParticipants.forEach((p) => {
116
+ p.trackPublications.forEach((pub) => {
117
+ pub.track?.detach();
118
+ });
113
119
  });
114
- });
120
+ }
115
121
  roomRef.current.disconnect();
116
122
  roomRef.current = null;
117
123
  }
@@ -274,6 +280,13 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
274
280
  setError(null);
275
281
  setRoomState('connecting');
276
282
 
283
+ // Guard: WebRTC must be available (requires registerGlobals on RN)
284
+ if (typeof globalThis.RTCPeerConnection === 'undefined') {
285
+ setError('Voice is not available on this device');
286
+ setRoomState('error');
287
+ return;
288
+ }
289
+
277
290
  const token = getToken();
278
291
  if (!token) {
279
292
  setError('Not authenticated');
@@ -309,15 +322,18 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
309
322
  const room = new Room();
310
323
  roomRef.current = room;
311
324
 
312
- // Event: remote audio tracks (agent speaking) — must attach to play
325
+ // Event: remote audio tracks (agent speaking)
326
+ // On web, must attach to a DOM element to play. On native, WebRTC plays audio automatically.
313
327
  room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
314
- if (track.kind === Track.Kind.Audio) {
328
+ if (track.kind === Track.Kind.Audio && hasDOM) {
315
329
  track.attach();
316
330
  }
317
331
  });
318
332
 
319
333
  room.on(RoomEvent.TrackUnsubscribed, (track) => {
320
- track.detach();
334
+ if (hasDOM && track.kind === Track.Kind.Audio) {
335
+ track.detach();
336
+ }
321
337
  });
322
338
 
323
339
  // Event: data messages from agent
package/src/index.ts CHANGED
@@ -10,9 +10,10 @@ export type { AliaChatScreenProps } from './components/AliaChatScreen';
10
10
 
11
11
  // ── Chat UI components ──
12
12
  export { AliaFace } from './components/AliaFace';
13
- export type { AliaExpression, AliaFaceProps } from './components/AliaFace';
13
+ export type { AliaAccessory, AliaExpression, AliaFaceProps } from './components/AliaFace';
14
14
  export { ThinkingIndicator } from './components/ThinkingIndicator';
15
15
  export { AliaMarkdown } from './components/Markdown';
16
+ export type { AliaMarkdownProps } from './components/Markdown';
16
17
  export { Reasoning, ReasoningTrigger, ReasoningContent } from './components/Reasoning';
17
18
  export { AliaWelcomeMessage } from './components/AliaWelcomeMessage';
18
19
  export { ResearchProgressCard } from './components/ResearchProgressCard';
@@ -85,5 +86,4 @@ export type { DocumentPickerResult } from './hooks/useDocumentPicker';
85
86
  export { KeyboardAwareScrollView, KeyboardAvoidingView, KeyboardProvider } from './lib/keyboard';
86
87
 
87
88
  // ── Theme ──
88
- export { useAliaColors } from './theme';
89
89
  export type { AliaColors } from './theme';
package/src/theme.ts CHANGED
@@ -1,35 +1,4 @@
1
- import { useMemo } from 'react';
2
- import { useColorScheme } from 'react-native';
3
-
4
- const COLORS = {
5
- light: {
6
- text: '#11181C',
7
- background: '#fff',
8
- card: '#F2F2F7',
9
- inputBackground: '#F5F5F5',
10
- border: '#E5E5EA',
11
- secondaryText: '#8E8E93',
12
- icon: '#687076',
13
- tint: '#0a7ea4',
14
- muted: '#F4F4F5',
15
- mutedForeground: '#71717A',
16
- primary: '#0a7ea4',
17
- },
18
- dark: {
19
- text: '#ECEDEE',
20
- background: '#000000',
21
- card: '#1C1C1E',
22
- inputBackground: '#333333',
23
- border: '#2C2C2E',
24
- secondaryText: '#8E8E93',
25
- icon: '#9BA1A6',
26
- tint: '#fff',
27
- muted: '#27272A',
28
- mutedForeground: '#A1A1AA',
29
- primary: '#38BDF8',
30
- },
31
- } as const;
32
-
1
+ /** Color interface used by SDK components that need resolved color values (e.g., Markdown renderer). */
33
2
  export interface AliaColors {
34
3
  text: string;
35
4
  background: string;
@@ -42,14 +11,6 @@ export interface AliaColors {
42
11
  muted: string;
43
12
  mutedForeground: string;
44
13
  primary: string;
14
+ primary10: string;
45
15
  isDark: boolean;
46
16
  }
47
-
48
- export function useAliaColors(): AliaColors {
49
- const scheme = useColorScheme();
50
- const key = scheme === 'dark' ? 'dark' : 'light';
51
- return useMemo(() => ({
52
- ...COLORS[key],
53
- isDark: key === 'dark',
54
- }), [key]);
55
- }
@@ -1,294 +0,0 @@
1
- import React, { useState, useCallback } from 'react';
2
- import {
3
- View,
4
- TextInput,
5
- Pressable,
6
- StyleSheet,
7
- ActivityIndicator,
8
- Platform,
9
- } from 'react-native';
10
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
11
- import { useSpeechToText } from '../hooks/useSpeechToText';
12
- import { useAliaColors } from '../theme';
13
-
14
- interface AliaChatInputProps {
15
- onSend: (text: string) => void;
16
- isStreaming: boolean;
17
- onStop: () => void;
18
- onVoiceActivate: () => void;
19
- apiUrl?: string;
20
- }
21
-
22
- export function AliaChatInput({
23
- onSend,
24
- isStreaming,
25
- onStop,
26
- onVoiceActivate,
27
- apiUrl,
28
- }: AliaChatInputProps) {
29
- const [input, setInput] = useState('');
30
- const colors = useAliaColors();
31
- const insets = useSafeAreaInsets();
32
- const isDark = colors.isDark;
33
- const stt = useSpeechToText({ apiUrl });
34
-
35
- const hasText = input.trim().length > 0;
36
- const canSend = hasText && !isStreaming;
37
-
38
- const handleSend = useCallback(() => {
39
- if (!canSend) return;
40
- onSend(input.trim());
41
- setInput('');
42
- }, [canSend, input, onSend]);
43
-
44
- const handleMicPress = useCallback(async () => {
45
- if (stt.isRecording) {
46
- const text = await stt.stopAndTranscribe();
47
- if (text) {
48
- setInput((prev) => (prev ? `${prev} ${text}` : text));
49
- }
50
- } else if (!stt.isTranscribing) {
51
- stt.startRecording();
52
- }
53
- }, [stt]);
54
-
55
- return (
56
- <View
57
- style={[
58
- styles.container,
59
- {
60
- borderTopColor: colors.border,
61
- paddingBottom: Math.max(insets.bottom, 12),
62
- },
63
- ]}
64
- >
65
- <TextInput
66
- style={[
67
- styles.input,
68
- {
69
- color: colors.text,
70
- backgroundColor: isDark ? colors.inputBackground : colors.card,
71
- },
72
- ]}
73
- value={input}
74
- onChangeText={setInput}
75
- placeholder="Ask Alia..."
76
- placeholderTextColor={colors.secondaryText}
77
- multiline
78
- maxLength={2000}
79
- onSubmitEditing={handleSend}
80
- blurOnSubmit={false}
81
- />
82
-
83
- {/* STT mic button */}
84
- <Pressable
85
- onPress={handleMicPress}
86
- disabled={stt.isTranscribing}
87
- style={styles.micButton}
88
- >
89
- {stt.isTranscribing ? (
90
- <ActivityIndicator size="small" color="#6366f1" />
91
- ) : stt.isRecording ? (
92
- <MicOffIcon />
93
- ) : (
94
- <MicIcon color={colors.secondaryText} />
95
- )}
96
- </Pressable>
97
-
98
- {/* Three-state submit button */}
99
- {isStreaming ? (
100
- <Pressable
101
- style={[styles.actionButton, { backgroundColor: colors.tint }]}
102
- onPress={onStop}
103
- >
104
- <StopIcon />
105
- </Pressable>
106
- ) : hasText ? (
107
- <Pressable
108
- style={[
109
- styles.actionButton,
110
- { backgroundColor: colors.tint },
111
- !canSend && { opacity: 0.4 },
112
- ]}
113
- onPress={handleSend}
114
- disabled={!canSend}
115
- >
116
- <ArrowUpIcon />
117
- </Pressable>
118
- ) : (
119
- <Pressable
120
- style={[styles.actionButton, { backgroundColor: colors.tint }]}
121
- onPress={onVoiceActivate}
122
- >
123
- <SoundIcon />
124
- </Pressable>
125
- )}
126
- </View>
127
- );
128
- }
129
-
130
- // ── Icons (inline SVG on web, View-based on native) ──
131
-
132
- function ArrowUpIcon() {
133
- return (
134
- <View style={styles.iconCenter}>
135
- {Platform.OS === 'web' ? (
136
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
137
- <path d="M12 19V5M5 12l7-7 7 7" />
138
- </svg>
139
- ) : (
140
- <View style={styles.arrowIconInner}>
141
- <View style={[styles.arrowStem, { backgroundColor: '#FFFFFF' }]} />
142
- <View style={[styles.arrowHead, { borderColor: '#FFFFFF' }]} />
143
- </View>
144
- )}
145
- </View>
146
- );
147
- }
148
-
149
- function StopIcon() {
150
- return (
151
- <View style={styles.iconCenter}>
152
- {Platform.OS === 'web' ? (
153
- <svg width="14" height="14" viewBox="0 0 24 24" fill="white">
154
- <rect x="4" y="4" width="16" height="16" rx="2" />
155
- </svg>
156
- ) : (
157
- <View style={styles.stopSquare} />
158
- )}
159
- </View>
160
- );
161
- }
162
-
163
- function SoundIcon() {
164
- return (
165
- <View style={styles.iconCenter}>
166
- {Platform.OS === 'web' ? (
167
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
168
- <path d="M11 5L6 9H2v6h4l5 4V5z" />
169
- <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
170
- <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
171
- </svg>
172
- ) : (
173
- <View style={styles.soundBars}>
174
- <View style={[styles.soundBar, styles.soundBarShort]} />
175
- <View style={[styles.soundBar, styles.soundBarMed]} />
176
- <View style={[styles.soundBar, styles.soundBarTall]} />
177
- </View>
178
- )}
179
- </View>
180
- );
181
- }
182
-
183
- function MicIcon({ color }: { color: string }) {
184
- return (
185
- <View style={styles.iconCenter}>
186
- {Platform.OS === 'web' ? (
187
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
188
- <rect x="9" y="1" width="6" height="12" rx="3" />
189
- <path d="M5 10a7 7 0 0 0 14 0" />
190
- <line x1="12" y1="17" x2="12" y2="21" />
191
- <line x1="8" y1="21" x2="16" y2="21" />
192
- </svg>
193
- ) : (
194
- <View style={[styles.micDot, { backgroundColor: color }]} />
195
- )}
196
- </View>
197
- );
198
- }
199
-
200
- function MicOffIcon() {
201
- return (
202
- <View style={styles.iconCenter}>
203
- {Platform.OS === 'web' ? (
204
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
205
- <rect x="9" y="1" width="6" height="12" rx="3" />
206
- <path d="M5 10a7 7 0 0 0 14 0" />
207
- <line x1="12" y1="17" x2="12" y2="21" />
208
- <line x1="1" y1="1" x2="23" y2="23" />
209
- </svg>
210
- ) : (
211
- <View style={[styles.micDot, { backgroundColor: '#ef4444' }]} />
212
- )}
213
- </View>
214
- );
215
- }
216
-
217
- const styles = StyleSheet.create({
218
- container: {
219
- flexDirection: 'row',
220
- alignItems: 'flex-end',
221
- paddingHorizontal: 12,
222
- paddingTop: 8,
223
- gap: 8,
224
- borderTopWidth: StyleSheet.hairlineWidth,
225
- },
226
- input: {
227
- flex: 1,
228
- fontSize: 15,
229
- paddingHorizontal: 14,
230
- paddingVertical: 10,
231
- borderRadius: 20,
232
- maxHeight: 100,
233
- },
234
- micButton: {
235
- width: 32,
236
- height: 40,
237
- alignItems: 'center',
238
- justifyContent: 'center',
239
- },
240
- actionButton: {
241
- width: 40,
242
- height: 40,
243
- borderRadius: 20,
244
- alignItems: 'center',
245
- justifyContent: 'center',
246
- },
247
- iconCenter: {
248
- width: 18,
249
- height: 18,
250
- alignItems: 'center',
251
- justifyContent: 'center',
252
- },
253
- arrowIconInner: {
254
- alignItems: 'center',
255
- },
256
- arrowStem: {
257
- width: 2,
258
- height: 12,
259
- borderRadius: 1,
260
- },
261
- arrowHead: {
262
- position: 'absolute',
263
- top: -1,
264
- width: 10,
265
- height: 10,
266
- borderTopWidth: 2.5,
267
- borderLeftWidth: 2.5,
268
- transform: [{ rotate: '45deg' }],
269
- },
270
- stopSquare: {
271
- width: 12,
272
- height: 12,
273
- borderRadius: 2,
274
- backgroundColor: '#FFFFFF',
275
- },
276
- soundBars: {
277
- flexDirection: 'row',
278
- alignItems: 'flex-end',
279
- gap: 2,
280
- },
281
- soundBar: {
282
- width: 3,
283
- backgroundColor: '#FFFFFF',
284
- borderRadius: 1.5,
285
- },
286
- soundBarShort: { height: 6 },
287
- soundBarMed: { height: 10 },
288
- soundBarTall: { height: 14 },
289
- micDot: {
290
- width: 12,
291
- height: 12,
292
- borderRadius: 6,
293
- },
294
- });