@alia.onl/sdk 3.0.0 → 3.1.1
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 +47 -19
- package/src/components/AliaChatContent.tsx +62 -19
- package/src/components/AliaChatMessageList.tsx +437 -280
- package/src/components/AliaChatScreen.tsx +21 -48
- package/src/components/AliaChatSheet.tsx +19 -31
- package/src/components/AliaFace.tsx +108 -58
- package/src/components/AliaWelcomeMessage.tsx +75 -0
- package/src/components/Markdown.tsx +94 -130
- package/src/components/PlanPreviewCard.tsx +76 -0
- package/src/components/Reasoning.tsx +1 -3
- package/src/components/ResearchProgressCard.tsx +158 -0
- package/src/components/ThinkingIndicator.tsx +6 -21
- package/src/components/voice/AudioWaveVisualizer.tsx +72 -22
- package/src/components/voice/VoiceControls.tsx +25 -42
- package/src/components/voice/VoiceOverlay.tsx +5 -1
- package/src/hooks/useAliaChat.ts +2 -1
- package/src/hooks/useSpeechToText.ts +21 -7
- package/src/hooks/useVoiceRoom.ts +24 -8
- package/src/index.ts +10 -3
- package/src/lib/content-utils.ts +23 -0
- package/src/lib/tool-registry.ts +55 -0
- package/src/theme.ts +2 -41
- package/src/types.ts +20 -7
- package/src/components/AliaChatInput.tsx +0 -294
- package/src/components/AliaChatSuggestions.tsx +0 -70
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { View, Platform, useWindowDimensions } from 'react-native';
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import { View, Platform, useWindowDimensions, useColorScheme } from 'react-native';
|
|
3
3
|
import Svg, { Path } from 'react-native-svg';
|
|
4
4
|
import Animated, {
|
|
5
5
|
useSharedValue,
|
|
@@ -15,6 +15,13 @@ import type { AgentState } from '../../types';
|
|
|
15
15
|
|
|
16
16
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
17
17
|
|
|
18
|
+
/** Cross-platform blur: CSS string on web, RN array syntax on native */
|
|
19
|
+
function blurStyle(radius: number) {
|
|
20
|
+
return Platform.OS === 'web'
|
|
21
|
+
? ({ filter: `blur(${radius}px)`, WebkitFilter: `blur(${radius}px)` } as any)
|
|
22
|
+
: ({ filter: [{ blur: radius }] } as any);
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
const GLOW_HEIGHT = 350;
|
|
19
26
|
const NUM_POINTS = 8;
|
|
20
27
|
|
|
@@ -167,20 +174,79 @@ const BLOB_COLORS: Record<AgentState, string[]> = {
|
|
|
167
174
|
speaking: ['#1e2870', '#6366f1', '#9333ea', '#0ea5e9'],
|
|
168
175
|
};
|
|
169
176
|
|
|
177
|
+
// Derive wave & blob palettes from a hex color
|
|
178
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
179
|
+
const h = hex.replace('#', '');
|
|
180
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function darken(rgb: [number, number, number], factor: number): [number, number, number] {
|
|
184
|
+
return [Math.round(rgb[0] * factor), Math.round(rgb[1] * factor), Math.round(rgb[2] * factor)];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function lighten(rgb: [number, number, number], factor: number): [number, number, number] {
|
|
188
|
+
return [
|
|
189
|
+
Math.min(255, Math.round(rgb[0] + (255 - rgb[0]) * factor)),
|
|
190
|
+
Math.min(255, Math.round(rgb[1] + (255 - rgb[1]) * factor)),
|
|
191
|
+
Math.min(255, Math.round(rgb[2] + (255 - rgb[2]) * factor)),
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function deriveWavePalette(hex: string, isDark: boolean): { waves: string[]; blobs: string[] } {
|
|
196
|
+
const rgb = hexToRgb(hex);
|
|
197
|
+
// Dark mode: lighter/brighter waves visible against dark bg
|
|
198
|
+
// Light mode: darker waves visible against light bg
|
|
199
|
+
const layer0 = isDark ? darken(rgb, 0.55) : darken(rgb, 0.3);
|
|
200
|
+
const layer1 = isDark ? lighten(rgb, 0.15) : darken(rgb, 0.5);
|
|
201
|
+
const layer2 = isDark ? lighten(rgb, 0.4) : darken(rgb, 0.15);
|
|
202
|
+
const layer3 = isDark ? lighten(rgb, 0.6) : rgb;
|
|
203
|
+
return {
|
|
204
|
+
waves: [
|
|
205
|
+
`rgba(${layer0[0]}, ${layer0[1]}, ${layer0[2]}, 0.8)`,
|
|
206
|
+
`rgba(${layer1[0]}, ${layer1[1]}, ${layer1[2]}, 0.45)`,
|
|
207
|
+
`rgba(${layer2[0]}, ${layer2[1]}, ${layer2[2]}, 0.35)`,
|
|
208
|
+
`rgba(${layer3[0]}, ${layer3[1]}, ${layer3[2]}, 0.25)`,
|
|
209
|
+
],
|
|
210
|
+
blobs: [
|
|
211
|
+
`rgb(${layer0[0]}, ${layer0[1]}, ${layer0[2]})`,
|
|
212
|
+
`rgb(${layer1[0]}, ${layer1[1]}, ${layer1[2]})`,
|
|
213
|
+
`rgb(${layer2[0]}, ${layer2[1]}, ${layer2[2]})`,
|
|
214
|
+
`rgb(${layer3[0]}, ${layer3[1]}, ${layer3[2]})`,
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
170
219
|
interface AudioWaveVisualizerProps {
|
|
171
220
|
waveAmplitude: SharedValue<number>;
|
|
172
221
|
agentState: AgentState;
|
|
173
222
|
isConnected: boolean;
|
|
223
|
+
/** Theme primary color hex — derives idle/listening wave palette */
|
|
224
|
+
primaryColor?: string;
|
|
225
|
+
/** Whether the app is in dark mode (affects wave brightness) */
|
|
226
|
+
isDarkMode?: boolean;
|
|
174
227
|
}
|
|
175
228
|
|
|
176
229
|
export function AudioWaveVisualizer({
|
|
177
230
|
waveAmplitude,
|
|
178
231
|
agentState = 'idle',
|
|
179
232
|
isConnected,
|
|
233
|
+
primaryColor,
|
|
234
|
+
isDarkMode,
|
|
180
235
|
}: AudioWaveVisualizerProps) {
|
|
181
236
|
const { width: screenWidth } = useWindowDimensions();
|
|
237
|
+
const scheme = useColorScheme();
|
|
238
|
+
const effectiveIsDark = isDarkMode ?? (scheme === 'dark');
|
|
182
239
|
const state = WAVE_COLORS[agentState] ? agentState : 'idle';
|
|
183
240
|
|
|
241
|
+
// Use theme-derived palette for idle/listening, keep hardcoded for thinking/speaking
|
|
242
|
+
const themed = useMemo(
|
|
243
|
+
() => (primaryColor ? deriveWavePalette(primaryColor, effectiveIsDark) : null),
|
|
244
|
+
[primaryColor, effectiveIsDark],
|
|
245
|
+
);
|
|
246
|
+
const isThemed = (state === 'idle' || state === 'listening') && themed != null;
|
|
247
|
+
const waveColors = isThemed ? themed.waves : WAVE_COLORS[state];
|
|
248
|
+
const blobColors = isThemed ? themed.blobs : BLOB_COLORS[state];
|
|
249
|
+
|
|
184
250
|
return (
|
|
185
251
|
<View
|
|
186
252
|
style={{
|
|
@@ -194,7 +260,7 @@ export function AudioWaveVisualizer({
|
|
|
194
260
|
<GlowBlob
|
|
195
261
|
key={`blob-${i}`}
|
|
196
262
|
blob={blob}
|
|
197
|
-
color={
|
|
263
|
+
color={blobColors[i]}
|
|
198
264
|
waveAmplitude={waveAmplitude}
|
|
199
265
|
isConnected={isConnected}
|
|
200
266
|
screenWidth={screenWidth}
|
|
@@ -206,7 +272,7 @@ export function AudioWaveVisualizer({
|
|
|
206
272
|
<OceanWave
|
|
207
273
|
key={`wave-${i}`}
|
|
208
274
|
layer={layer}
|
|
209
|
-
color={
|
|
275
|
+
color={waveColors[i]}
|
|
210
276
|
waveAmplitude={waveAmplitude}
|
|
211
277
|
isConnected={isConnected}
|
|
212
278
|
screenWidth={screenWidth}
|
|
@@ -279,14 +345,6 @@ function OceanWave({
|
|
|
279
345
|
return { d };
|
|
280
346
|
});
|
|
281
347
|
|
|
282
|
-
const webBlurStyle =
|
|
283
|
-
Platform.OS === 'web'
|
|
284
|
-
? ({
|
|
285
|
-
filter: `blur(${layer.blur}px)`,
|
|
286
|
-
WebkitFilter: `blur(${layer.blur}px)`,
|
|
287
|
-
} as any)
|
|
288
|
-
: {};
|
|
289
|
-
|
|
290
348
|
return (
|
|
291
349
|
<View
|
|
292
350
|
style={{
|
|
@@ -296,7 +354,7 @@ function OceanWave({
|
|
|
296
354
|
right: 0,
|
|
297
355
|
bottom: 0,
|
|
298
356
|
opacity: isConnected ? 1 : 0,
|
|
299
|
-
...
|
|
357
|
+
...blurStyle(layer.blur),
|
|
300
358
|
pointerEvents: 'none',
|
|
301
359
|
}}
|
|
302
360
|
>
|
|
@@ -365,13 +423,5 @@ function GlowBlob({
|
|
|
365
423
|
};
|
|
366
424
|
});
|
|
367
425
|
|
|
368
|
-
|
|
369
|
-
Platform.OS === 'web'
|
|
370
|
-
? ({
|
|
371
|
-
filter: `blur(${blob.blur}px)`,
|
|
372
|
-
WebkitFilter: `blur(${blob.blur}px)`,
|
|
373
|
-
} as any)
|
|
374
|
-
: {};
|
|
375
|
-
|
|
376
|
-
return <Animated.View style={[animatedStyle, webBlurStyle]} />;
|
|
426
|
+
return <Animated.View style={[animatedStyle, blurStyle(blob.blur)]} />;
|
|
377
427
|
}
|
|
@@ -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
|
|
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
|
-
|
|
72
|
+
className="bg-primary/30"
|
|
73
|
+
style={[styles.continueButton, primaryColor ? { backgroundColor: primaryColor + '4D' } : undefined]}
|
|
71
74
|
>
|
|
72
|
-
<Text style={
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
{isMuted ? (
|
|
90
|
+
<MicOff size={24} color="white" />
|
|
91
|
+
) : (
|
|
92
|
+
<Mic size={24} color="white" />
|
|
93
|
+
)}
|
|
89
94
|
</Pressable>
|
|
90
|
-
<Text
|
|
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
|
-
|
|
106
|
+
cohostActive && primaryColor ? { backgroundColor: primaryColor } : undefined,
|
|
101
107
|
]}
|
|
102
108
|
>
|
|
103
|
-
<
|
|
109
|
+
<Users size={24} color="white" />
|
|
104
110
|
</Pressable>
|
|
105
|
-
<Text
|
|
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
|
-
<
|
|
121
|
+
<PhoneOff size={24} color="white" />
|
|
116
122
|
</Pressable>
|
|
117
|
-
<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"
|
|
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>
|
package/src/hooks/useAliaChat.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import { useOxy } from '@oxyhq/services';
|
|
3
3
|
import type { ChatMessage, ToolInvocation } from '../types';
|
|
4
|
+
import { getTextFromContent } from '../lib/content-utils';
|
|
4
5
|
|
|
5
6
|
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
6
7
|
|
|
@@ -188,7 +189,7 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
|
|
|
188
189
|
// Include full conversation history with tool context
|
|
189
190
|
for (const msg of messagesRef.current) {
|
|
190
191
|
if (msg.role === 'system') continue;
|
|
191
|
-
apiMessages.push({ role: msg.role, content: msg.content });
|
|
192
|
+
apiMessages.push({ role: msg.role, content: getTextFromContent(msg.content) });
|
|
192
193
|
// Include tool results so the AI has full context
|
|
193
194
|
if (msg.toolInvocations?.length) {
|
|
194
195
|
for (const tool of msg.toolInvocations) {
|
|
@@ -60,18 +60,32 @@ export function useSpeechToText(options: UseSTTOptions = {}) {
|
|
|
60
60
|
return oxyServices.httpService.getAccessToken();
|
|
61
61
|
}, [options.accessToken, oxyServices]);
|
|
62
62
|
|
|
63
|
-
// Push metering to store while recording
|
|
63
|
+
// Push metering to store while recording (with epsilon guard to avoid thrashing subscribers)
|
|
64
|
+
const lastMeteringRef = useRef(0);
|
|
64
65
|
useEffect(() => {
|
|
65
|
-
if (state === 'recording'
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
if (state === 'recording') {
|
|
67
|
+
let target: number;
|
|
68
|
+
if (recorderState.metering != null) {
|
|
69
|
+
// dBFS: -160 (silence) to 0 (max). Normalize using -60 as practical floor.
|
|
70
|
+
target = Math.min(1, Math.max(0, (recorderState.metering + 60) / 60));
|
|
71
|
+
} else {
|
|
72
|
+
// Metering unavailable — simulate gentle activity
|
|
73
|
+
target = 0.12 + Math.random() * 0.18;
|
|
74
|
+
}
|
|
75
|
+
if (Math.abs(target - lastMeteringRef.current) >= 0.02) {
|
|
76
|
+
lastMeteringRef.current = target;
|
|
77
|
+
sttStore.getState().setMetering(target);
|
|
78
|
+
}
|
|
69
79
|
}
|
|
70
|
-
}, [state, recorderState.metering]);
|
|
80
|
+
}, [state, recorderState.metering, recorderState.durationMillis]);
|
|
71
81
|
|
|
72
|
-
// Sync recording state to store
|
|
82
|
+
// Sync recording state to store + reset metering on stop
|
|
73
83
|
useEffect(() => {
|
|
74
84
|
sttStore.getState().setRecording(state === 'recording');
|
|
85
|
+
if (state !== 'recording') {
|
|
86
|
+
lastMeteringRef.current = 0;
|
|
87
|
+
sttStore.getState().setMetering(0);
|
|
88
|
+
}
|
|
75
89
|
}, [state]);
|
|
76
90
|
|
|
77
91
|
const startRecording = useCallback(async () => {
|
|
@@ -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
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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)
|
|
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.
|
|
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,10 +10,14 @@ 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';
|
|
18
|
+
export { AliaWelcomeMessage } from './components/AliaWelcomeMessage';
|
|
19
|
+
export { ResearchProgressCard } from './components/ResearchProgressCard';
|
|
20
|
+
export { PlanPreviewCard } from './components/PlanPreviewCard';
|
|
17
21
|
|
|
18
22
|
// ── Voice components ──
|
|
19
23
|
export { AudioWaveVisualizer } from './components/voice/AudioWaveVisualizer';
|
|
@@ -39,9 +43,11 @@ export type { SoundName, SoundSources } from './hooks/useSoundEffects';
|
|
|
39
43
|
export type {
|
|
40
44
|
ChatMessage,
|
|
41
45
|
ToolInvocation,
|
|
42
|
-
|
|
46
|
+
WelcomeSuggestion,
|
|
43
47
|
ResearchProgress,
|
|
48
|
+
ResearchSource,
|
|
44
49
|
PendingPlan,
|
|
50
|
+
PlanStep,
|
|
45
51
|
RoomState,
|
|
46
52
|
AgentState,
|
|
47
53
|
VoiceMessage,
|
|
@@ -54,6 +60,8 @@ export type { ButtonProps } from './components/ui/button';
|
|
|
54
60
|
export { Text, TextClassContext } from './components/ui/text';
|
|
55
61
|
export { ChatTextInput } from './components/ui/chat-text-input';
|
|
56
62
|
export { cn, formatFileSize } from './lib/utils';
|
|
63
|
+
export { getToolLabel, getToolActiveLabel, getResearchActiveLabel } from './lib/tool-registry';
|
|
64
|
+
export { getTextFromContent, getImagesFromContent } from './lib/content-utils';
|
|
57
65
|
|
|
58
66
|
// ── PromptInput ──
|
|
59
67
|
export { PromptInput } from './components/ui/prompt-input/prompt-input';
|
|
@@ -78,5 +86,4 @@ export type { DocumentPickerResult } from './hooks/useDocumentPicker';
|
|
|
78
86
|
export { KeyboardAwareScrollView, KeyboardAvoidingView, KeyboardProvider } from './lib/keyboard';
|
|
79
87
|
|
|
80
88
|
// ── Theme ──
|
|
81
|
-
export { useAliaColors } from './theme';
|
|
82
89
|
export type { AliaColors } from './theme';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract text from message content, whether it's a string or multi-part array.
|
|
3
|
+
*/
|
|
4
|
+
export function getTextFromContent(content: string | Array<{ type: string; [key: string]: any }> | any): string {
|
|
5
|
+
if (typeof content === 'string') return content;
|
|
6
|
+
if (Array.isArray(content)) {
|
|
7
|
+
return content
|
|
8
|
+
.filter((p: any) => p.type === 'text')
|
|
9
|
+
.map((p: any) => p.text || '')
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
return String(content || '');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract image URLs from multi-part message content.
|
|
17
|
+
*/
|
|
18
|
+
export function getImagesFromContent(content: string | Array<{ type: string; [key: string]: any }> | any): string[] {
|
|
19
|
+
if (!Array.isArray(content)) return [];
|
|
20
|
+
return content
|
|
21
|
+
.filter((p: any) => p.type === 'image_url' && p.image_url?.url)
|
|
22
|
+
.map((p: any) => p.image_url.url);
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface ToolDefinition {
|
|
2
|
+
label: string;
|
|
3
|
+
category: 'search' | 'communication' | 'utility' | 'memory';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const TOOL_REGISTRY: Record<string, ToolDefinition> = {
|
|
7
|
+
// Search
|
|
8
|
+
webSearch: { label: 'Searching the web', category: 'search' },
|
|
9
|
+
scrapeURL: { label: 'Reading URL', category: 'search' },
|
|
10
|
+
getTimeline: { label: 'Getting timeline', category: 'search' },
|
|
11
|
+
searchKnowledgeBase: { label: 'Searching knowledge base', category: 'search' },
|
|
12
|
+
webScraper: { label: 'Reading web page', category: 'search' },
|
|
13
|
+
browse: { label: 'Browsing the web', category: 'search' },
|
|
14
|
+
|
|
15
|
+
// Communication
|
|
16
|
+
sendWhatsAppMessage: { label: 'Sending WhatsApp message', category: 'communication' },
|
|
17
|
+
getWhatsAppChats: { label: 'Loading WhatsApp chats', category: 'communication' },
|
|
18
|
+
getWhatsAppMessages: { label: 'Reading WhatsApp messages', category: 'communication' },
|
|
19
|
+
sendTelegram: { label: 'Sending Telegram message', category: 'communication' },
|
|
20
|
+
|
|
21
|
+
// Utility
|
|
22
|
+
getCurrentDate: { label: 'Getting current date', category: 'utility' },
|
|
23
|
+
generateFile: { label: 'Generating file', category: 'utility' },
|
|
24
|
+
|
|
25
|
+
// Memory
|
|
26
|
+
saveUserMemory: { label: 'Saving to memory', category: 'memory' },
|
|
27
|
+
updateUserPreferences: { label: 'Updating preferences', category: 'memory' },
|
|
28
|
+
updateUserContext: { label: 'Updating context', category: 'memory' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function getToolLabel(toolName: string): string {
|
|
32
|
+
return TOOL_REGISTRY[toolName]?.label || toolName;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getToolActiveLabel(toolName: string): string | undefined {
|
|
36
|
+
const label = TOOL_REGISTRY[toolName]?.label;
|
|
37
|
+
if (!label) return undefined;
|
|
38
|
+
return label + '...';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Status strings for the ThinkingIndicator during research phases. */
|
|
42
|
+
const RESEARCH_ACTIVE_LABELS: Record<string, string> = {
|
|
43
|
+
decomposing: 'Decomposing query...',
|
|
44
|
+
searching: 'Searching sources...',
|
|
45
|
+
reading: 'Reading articles...',
|
|
46
|
+
synthesizing: 'Synthesizing findings...',
|
|
47
|
+
follow_up: 'Following up...',
|
|
48
|
+
finalizing: 'Finalizing research...',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function getResearchActiveLabel(phase: string): string | undefined {
|
|
52
|
+
return RESEARCH_ACTIVE_LABELS[phase];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { TOOL_REGISTRY };
|
package/src/theme.ts
CHANGED
|
@@ -1,35 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
}
|