@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.
@@ -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={BLOB_COLORS[state][i]}
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={WAVE_COLORS[state][i]}
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
- ...webBlurStyle,
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
- const webBlurStyle =
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 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>
@@ -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' && recorderState.metering != null) {
66
- // dBFS: -160 (silence) to 0 (max). Normalize using -60 as practical floor.
67
- const normalized = Math.min(1, Math.max(0, (recorderState.metering + 60) / 60));
68
- sttStore.getState().setMetering(normalized);
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
- // 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,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
- AliaChatSuggestion,
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
- 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
- }