@alia.onl/sdk 3.1.2 → 3.1.3

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.
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "Alia AI chat SDK — reusable chat UI, voice, and streaming components for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -11,35 +11,40 @@
11
11
  "access": "public"
12
12
  },
13
13
  "dependencies": {
14
- "@tanstack/query-async-storage-persister": "^5.100.0",
15
- "@tanstack/react-query": "^5.100.0",
16
- "@tanstack/react-query-persist-client": "^5.100.0",
17
- "react-native-markdown-display": "^7.0.2-0",
18
- "zustand": ">=4"
14
+ "@radix-ui/react-icons": "^1.3.2",
15
+ "@tanstack/query-async-storage-persister": "^5.101.2",
16
+ "@tanstack/react-query": "^5.101.2",
17
+ "@tanstack/react-query-persist-client": "^5.101.2",
18
+ "react-native-markdown-display": "^7.0.2",
19
+ "zustand": "^5.0.14"
19
20
  },
20
21
  "peerDependencies": {
21
- "react": ">=18",
22
- "react-native": ">=0.73",
23
- "@oxyhq/services": "^10.2.3",
24
- "expo-blur": "*",
25
- "react-native-reanimated": ">=3",
26
- "react-native-gesture-handler": ">=2",
27
- "react-native-safe-area-context": ">=4",
28
- "react-native-keyboard-controller": ">=1",
29
- "react-native-svg": ">=13",
30
- "livekit-client": ">=2.0.0",
31
- "expo-audio": ">=0.1.0",
32
- "nativewind": ">=4",
33
- "clsx": ">=2",
34
- "tailwind-merge": ">=2",
35
- "class-variance-authority": ">=0.7",
36
- "zeego": ">=3",
37
- "expo-image": ">=1",
38
- "expo-image-picker": ">=15",
39
- "expo-document-picker": ">=12",
40
- "expo-clipboard": ">=6",
41
- "lucide-react-native": ">=0.300",
42
- "@radix-ui/react-dropdown-menu": ">=2"
22
+ "react": "^19.2.3",
23
+ "react-native": "^0.86.0",
24
+ "@oxyhq/services": "^20.0.0",
25
+ "@react-native-async-storage/async-storage": "^2.2.0",
26
+ "socket.io-client": "^4.8.3",
27
+ "expo-font": "^57.0.0",
28
+ "expo-blur": "^57.0.0",
29
+ "react-native-reanimated": "^4.5.0",
30
+ "react-native-gesture-handler": "^2.32.0",
31
+ "react-native-safe-area-context": "^5.8.0",
32
+ "react-native-keyboard-controller": "^1.21.9",
33
+ "react-native-svg": "^15.15.4",
34
+ "livekit-client": "^2.20.1",
35
+ "expo-audio": "^57.0.0",
36
+ "nativewind": "^5.0.0-preview.3",
37
+ "clsx": "^2.1.1",
38
+ "tailwind-merge": "^3.6.0",
39
+ "class-variance-authority": "^0.7.1",
40
+ "zeego": "^3.0.6",
41
+ "expo-image": "^57.0.0",
42
+ "expo-image-picker": "^57.0.2",
43
+ "expo-document-picker": "^57.0.0",
44
+ "expo-clipboard": "^57.0.0",
45
+ "lucide-react-native": "^0.562.0",
46
+ "@radix-ui/react-dropdown-menu": "^2.1.20",
47
+ "expo-haptics": "*"
43
48
  },
44
49
  "peerDependenciesMeta": {
45
50
  "livekit-client": {
@@ -83,6 +88,9 @@
83
88
  },
84
89
  "@radix-ui/react-dropdown-menu": {
85
90
  "optional": true
91
+ },
92
+ "expo-haptics": {
93
+ "optional": true
86
94
  }
87
95
  },
88
96
  "devDependencies": {
@@ -2,12 +2,11 @@
2
2
  * Shared chat content component used by both AliaChatSheet and AliaChatScreen.
3
3
  *
4
4
  * Owns all chat + voice state internally. Containers only handle
5
- * layout chrome (sheet modal, full-screen SafeAreaView, headers).
5
+ * layout chrome (sheet modal, full-screen SafeAreaView) and pass a `header`
6
+ * render prop that receives the live mark state.
6
7
  */
7
8
 
8
9
  import React, {
9
- forwardRef,
10
- useImperativeHandle,
11
10
  useState,
12
11
  useCallback,
13
12
  useEffect,
@@ -15,7 +14,7 @@ import React, {
15
14
  useRef,
16
15
  } from 'react';
17
16
  import { View, StyleSheet } from 'react-native';
18
- import { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
17
+ import { useSharedValue } from 'react-native-reanimated';
19
18
  import type { SharedValue } from 'react-native-reanimated';
20
19
  import { Volume2 } from 'lucide-react-native';
21
20
  import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
@@ -23,15 +22,15 @@ import { useVoiceRoom } from '../hooks/useVoiceRoom';
23
22
  import { useAudioLevelMonitor } from '../hooks/useAudioLevelMonitor';
24
23
  import { useAudioLevels } from '../hooks/useAudioLevels';
25
24
  import { useTTS } from '../hooks/useTTS';
26
- import { useSTTStore } from '../hooks/useSpeechToText';
25
+ import { useAmbientWave } from '../hooks/useAmbientWave';
27
26
  import { VoiceOverlay } from './voice/VoiceOverlay';
28
27
  import { VoiceControls } from './voice/VoiceControls';
29
28
  import { AliaChatMessageList } from './AliaChatMessageList';
30
29
  import { AliaWelcomeMessage, type WelcomeSuggestion } from './AliaWelcomeMessage';
31
30
  import { PromptInput } from './ui/prompt-input/prompt-input';
32
31
  import { Button } from './ui/button';
33
- import type { AliaExpression } from './AliaFace';
34
- import type { ChatMessage } from '../types';
32
+ import type { AliaMarkState } from './AliaMark';
33
+ import type { ChatMessage, VoiceMessage } from '../types';
35
34
  import type { Completion } from './ui/prompt-input/context';
36
35
 
37
36
  export interface AliaChatContentProps {
@@ -59,18 +58,16 @@ export interface AliaChatContentProps {
59
58
  onToolResultPress?: (messageId: string) => void;
60
59
  /** Override markdown renderer (app passes CustomMarkdown) */
61
60
  renderMarkdown?: (content: string) => React.ReactNode;
62
- }
63
-
64
- export interface AliaChatContentRef {
65
- messages: ChatMessage[];
66
- isStreaming: boolean;
67
- isVoiceActive: boolean;
68
- faceExpression: AliaExpression;
69
- clear: () => void;
61
+ /** Header bar render prop — receives live mark state, message presence, and the clear handler. */
62
+ header?: (state: { markState: AliaMarkState; hasMessages: boolean; clear: () => void }) => React.ReactNode;
63
+ /** Theme primary color hex — forwarded to the ambient wave overlay palette. */
64
+ primaryColor?: string;
65
+ /** Dark-mode flag — forwarded to the ambient wave overlay. */
66
+ isDarkMode?: boolean;
70
67
  }
71
68
 
72
69
  /** Adapt a voice message into the chat message format. */
73
- function adaptVoiceMessage(vm: any): ChatMessage {
70
+ function adaptVoiceMessage(vm: VoiceMessage): ChatMessage {
74
71
  return {
75
72
  id: vm.id,
76
73
  role: vm.role,
@@ -82,266 +79,231 @@ function adaptVoiceMessage(vm: any): ChatMessage {
82
79
  };
83
80
  }
84
81
 
85
- export const AliaChatContent = forwardRef<AliaChatContentRef, AliaChatContentProps>(
86
- ({
87
- clientContext,
88
- model,
89
- apiUrl,
90
- scrollOffsetY: externalScrollOffsetY,
91
- useSuggestions,
92
- onSuggestionUsed,
93
- onError,
94
- welcomeGreeting,
95
- welcomeSubtitle,
96
- welcomeSuggestions,
97
- onEditMessage,
98
- onThumbsUp,
99
- onThumbsDown,
100
- onApprovePlan,
101
- onRejectPlan,
102
- onToolResultPress,
103
- renderMarkdown,
104
- }, ref) => {
105
- // ── Chat ──
106
- const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
107
- const { messages, setMessages, send, isStreaming, stop, clear } = useAliaChat(chatOptions);
82
+ export function AliaChatContent({
83
+ clientContext,
84
+ model,
85
+ apiUrl,
86
+ scrollOffsetY: externalScrollOffsetY,
87
+ useSuggestions,
88
+ onSuggestionUsed,
89
+ onError,
90
+ welcomeGreeting,
91
+ welcomeSubtitle,
92
+ welcomeSuggestions,
93
+ onEditMessage,
94
+ onThumbsUp,
95
+ onThumbsDown,
96
+ onApprovePlan,
97
+ onRejectPlan,
98
+ onToolResultPress,
99
+ renderMarkdown,
100
+ header,
101
+ primaryColor,
102
+ isDarkMode,
103
+ }: AliaChatContentProps) {
104
+ // ── Chat ──
105
+ const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
106
+ const { messages, setMessages, send, isStreaming, stop, clear } = useAliaChat(chatOptions);
108
107
 
109
- // ── Input state ──
110
- const [inputValue, setInputValue] = useState('');
108
+ // ── Input state ──
109
+ const [inputValue, setInputValue] = useState('');
111
110
 
112
- const handleSubmit = useCallback(() => {
113
- const text = inputValue.trim();
114
- if (!text) return;
115
- send(text);
116
- setInputValue('');
117
- }, [inputValue, send]);
111
+ const handleSubmit = useCallback(() => {
112
+ const text = inputValue.trim();
113
+ if (!text) return;
114
+ send(text);
115
+ setInputValue('');
116
+ }, [inputValue, send]);
118
117
 
119
- // ── Scroll offset (use external if provided, else internal) ──
120
- const internalScrollOffsetY = useSharedValue(0);
121
- const scrollOffsetY = externalScrollOffsetY ?? internalScrollOffsetY;
118
+ // ── Scroll offset (use external if provided, else internal) ──
119
+ const internalScrollOffsetY = useSharedValue(0);
120
+ const scrollOffsetY = externalScrollOffsetY ?? internalScrollOffsetY;
122
121
 
123
- // ── Voice room ──
124
- const voiceRoom = useVoiceRoom({ apiUrl });
125
- const { captureLevel, playbackLevel } = useAudioLevelMonitor(voiceRoom.room, voiceRoom.isConnected);
126
- const { waveAmplitude } = useAudioLevels({
127
- captureLevel,
128
- playbackLevel,
129
- agentState: voiceRoom.agentState,
130
- isConnected: voiceRoom.isConnected,
131
- });
122
+ // ── Voice room ──
123
+ const voiceRoom = useVoiceRoom({ apiUrl });
124
+ const { captureLevel, playbackLevel } = useAudioLevelMonitor(voiceRoom.room, voiceRoom.isConnected);
125
+ const { waveAmplitude } = useAudioLevels({
126
+ captureLevel,
127
+ playbackLevel,
128
+ agentState: voiceRoom.agentState,
129
+ isConnected: voiceRoom.isConnected,
130
+ });
132
131
 
133
- // ── TTS ──
134
- const tts = useTTS({ apiUrl });
132
+ // ── TTS ──
133
+ const tts = useTTS({ apiUrl });
135
134
 
136
- // ── STT wave overlay ──
137
- const sttIsRecording = useSTTStore((s) => s.isRecording);
138
- const sttMetering = useSTTStore((s) => s.metering);
139
- const sttWaveAmplitude = useSharedValue(0);
135
+ // ── Voice mode state ──
136
+ const [isVoiceActive, setIsVoiceActive] = useState(false);
137
+ const textSnapshotRef = useRef<ChatMessage[]>([]);
140
138
 
141
- useEffect(() => {
142
- if (!sttIsRecording) {
143
- sttWaveAmplitude.value = withTiming(0, { duration: 300 });
144
- return;
145
- }
146
- const target = Math.max(0.08, sttMetering);
147
- const duration = target > sttWaveAmplitude.value ? 60 : 200;
148
- sttWaveAmplitude.value = withTiming(target, {
149
- duration,
150
- easing: Easing.bezier(0.33, 1, 0.68, 1),
151
- });
152
- }, [sttIsRecording, sttMetering]);
139
+ // Merge voice messages into chat messages
140
+ useEffect(() => {
141
+ if (!isVoiceActive) return;
142
+ const adapted = voiceRoom.messages.map(adaptVoiceMessage);
143
+ setMessages([...textSnapshotRef.current, ...adapted]);
144
+ }, [voiceRoom.messages, isVoiceActive, setMessages]);
153
145
 
154
- // ── Voice mode state ──
155
- const [isVoiceActive, setIsVoiceActive] = useState(false);
156
- const textSnapshotRef = useRef<ChatMessage[]>([]);
146
+ // Auto-deactivate on error
147
+ useEffect(() => {
148
+ if (!isVoiceActive) return;
149
+ if (voiceRoom.error) {
150
+ deactivateVoice();
151
+ } else if (voiceRoom.roomState === 'error') {
152
+ deactivateVoice();
153
+ }
154
+ }, [voiceRoom.error, voiceRoom.roomState, isVoiceActive]);
157
155
 
158
- // Merge voice messages into chat messages
159
- useEffect(() => {
160
- if (!isVoiceActive) return;
161
- const adapted = voiceRoom.messages.map(adaptVoiceMessage);
162
- setMessages([...textSnapshotRef.current, ...adapted]);
163
- }, [voiceRoom.messages, isVoiceActive, setMessages]);
156
+ // Auto-deactivate on unexpected disconnection
157
+ useEffect(() => {
158
+ if (!isVoiceActive) return;
159
+ if (voiceRoom.roomState === 'disconnected' && textSnapshotRef.current.length > 0) {
160
+ deactivateVoice();
161
+ }
162
+ }, [voiceRoom.roomState, isVoiceActive]);
164
163
 
165
- // Auto-deactivate on error
166
- useEffect(() => {
167
- if (!isVoiceActive) return;
168
- if (voiceRoom.error) {
169
- deactivateVoice();
170
- } else if (voiceRoom.roomState === 'error') {
171
- deactivateVoice();
172
- }
173
- }, [voiceRoom.error, voiceRoom.roomState, isVoiceActive]);
164
+ const activateVoice = useCallback(() => {
165
+ if (isVoiceActive || voiceRoom.roomState === 'connecting') return;
166
+ textSnapshotRef.current = [...messages];
167
+ setIsVoiceActive(true);
168
+ voiceRoom.connect();
169
+ }, [isVoiceActive, voiceRoom, messages]);
174
170
 
175
- // Auto-deactivate on unexpected disconnection
176
- useEffect(() => {
177
- if (!isVoiceActive) return;
178
- if (voiceRoom.roomState === 'disconnected' && textSnapshotRef.current.length > 0) {
179
- deactivateVoice();
180
- }
181
- }, [voiceRoom.roomState, isVoiceActive]);
171
+ const deactivateVoice = useCallback(() => {
172
+ voiceRoom.disconnect();
173
+ setIsVoiceActive(false);
174
+ textSnapshotRef.current = [];
175
+ }, [voiceRoom]);
182
176
 
183
- const activateVoice = useCallback(() => {
184
- if (isVoiceActive || voiceRoom.roomState === 'connecting') return;
185
- textSnapshotRef.current = [...messages];
186
- setIsVoiceActive(true);
187
- voiceRoom.connect();
188
- }, [isVoiceActive, voiceRoom, messages]);
177
+ // ── TTS read aloud handler ──
178
+ const handleReadAloud = useCallback(
179
+ (messageId: string, text: string) => {
180
+ tts.readAloud(messageId, text);
181
+ },
182
+ [tts],
183
+ );
189
184
 
190
- const deactivateVoice = useCallback(() => {
191
- voiceRoom.disconnect();
192
- setIsVoiceActive(false);
193
- textSnapshotRef.current = [];
194
- }, [voiceRoom]);
185
+ // ── Mark state (drives the header brand mark) ──
186
+ const markState = useMemo<AliaMarkState>(() => {
187
+ if (isVoiceActive) {
188
+ if (voiceRoom.agentState === 'speaking') return 'writing';
189
+ if (voiceRoom.agentState === 'thinking') return 'thinking';
190
+ return 'idle';
191
+ }
192
+ if (!isStreaming) return 'idle';
193
+ const lastMsg = messages[messages.length - 1];
194
+ if (lastMsg?.role === 'assistant' && lastMsg.toolInvocations?.some((t) => t.state === 'call'))
195
+ return 'working';
196
+ if (lastMsg?.thinking) return 'thinking';
197
+ return 'writing';
198
+ }, [messages, isStreaming, isVoiceActive, voiceRoom.agentState]);
195
199
 
196
- // ── TTS read aloud handler ──
197
- const handleReadAloud = useCallback(
198
- (messageId: string, text: string) => {
199
- tts.readAloud(messageId, text);
200
- },
201
- [tts],
200
+ const welcomeComponent = useMemo(() => {
201
+ if (!welcomeGreeting || messages.length > 0) return undefined;
202
+ return (
203
+ <AliaWelcomeMessage
204
+ greeting={welcomeGreeting}
205
+ subtitle={welcomeSubtitle}
206
+ suggestions={welcomeSuggestions}
207
+ onSuggestionPress={send}
208
+ />
202
209
  );
210
+ }, [welcomeGreeting, welcomeSubtitle, welcomeSuggestions, send, messages.length]);
203
211
 
204
- // ── Face expression ──
205
- const faceExpression = useMemo<AliaExpression>(() => {
206
- if (isVoiceActive) {
207
- if (voiceRoom.agentState === 'speaking') return 'Writing E';
208
- if (voiceRoom.agentState === 'thinking') return 'Thinking';
209
- return 'Idle A';
210
- }
211
- if (!isStreaming) return 'Idle A';
212
- const lastMsg = messages[messages.length - 1];
213
- if (lastMsg?.role === 'assistant' && lastMsg.toolInvocations?.some((t) => t.state === 'call'))
214
- return 'Searching A';
215
- if (lastMsg?.thinking) return 'Thinking';
216
- return 'Writing E';
217
- }, [messages, isStreaming, isVoiceActive, voiceRoom.agentState]);
218
-
219
- const welcomeComponent = useMemo(() => {
220
- if (!welcomeGreeting || messages.length > 0) return undefined;
221
- return (
222
- <AliaWelcomeMessage
223
- greeting={welcomeGreeting}
224
- subtitle={welcomeSubtitle}
225
- suggestions={welcomeSuggestions}
226
- onSuggestionPress={send}
227
- />
228
- );
229
- }, [welcomeGreeting, welcomeSubtitle, welcomeSuggestions, send, messages.length]);
212
+ // ── Ambient wave — one persistent overlay across idle/voice/TTS/STT ──
213
+ const wave = useAmbientWave({
214
+ voice: {
215
+ isActive: isVoiceActive,
216
+ isConnected: voiceRoom.isConnected,
217
+ agentState: voiceRoom.agentState,
218
+ waveAmplitude,
219
+ },
220
+ isTTSPlaying: tts.playbackState === 'playing',
221
+ ttsWaveAmplitude: tts.ttsWaveAmplitude,
222
+ isGenerating: isStreaming,
223
+ });
230
224
 
231
- // ── Overlay state ──
232
- const showVoiceOverlay = isVoiceActive;
233
- const showTTSOverlay = !isVoiceActive && tts.playbackState === 'playing';
234
- const showSTTOverlay = !isVoiceActive && tts.playbackState !== 'playing' && sttIsRecording;
225
+ // ── Voice activate button for empty submit ──
226
+ const voiceActivateButton = useMemo(
227
+ () => (
228
+ <Button
229
+ size="icon"
230
+ onPress={activateVoice}
231
+ className="h-8 w-8 rounded-full"
232
+ >
233
+ <Volume2 size={16} color="white" />
234
+ </Button>
235
+ ),
236
+ [activateVoice],
237
+ );
235
238
 
236
- // ── Voice activate button for empty submit ──
237
- const voiceActivateButton = useMemo(
238
- () => (
239
- <Button
240
- size="icon"
241
- onPress={activateVoice}
242
- className="h-8 w-8 rounded-full"
243
- >
244
- <Volume2 size={16} color="white" />
245
- </Button>
246
- ),
247
- [activateVoice],
248
- );
239
+ return (
240
+ <View style={styles.container}>
241
+ {/* Header (render prop) */}
242
+ {header?.({ markState, hasMessages: messages.length > 0, clear })}
249
243
 
250
- // ── Expose state to container ──
251
- useImperativeHandle(
252
- ref,
253
- () => ({
254
- messages,
255
- isStreaming,
256
- isVoiceActive,
257
- faceExpression,
258
- clear,
259
- }),
260
- [messages, isStreaming, isVoiceActive, faceExpression, clear],
261
- );
244
+ {/* Persistent ambient wave overlay */}
245
+ <VoiceOverlay
246
+ waveAmplitude={wave.waveAmplitude}
247
+ agentState={wave.agentState}
248
+ intensity={wave.intensity}
249
+ primaryColor={primaryColor}
250
+ isDarkMode={isDarkMode}
251
+ />
262
252
 
263
- return (
264
- <View style={styles.container}>
265
- {/* Voice/TTS/STT wave overlay */}
266
- {showVoiceOverlay && (
267
- <VoiceOverlay
268
- waveAmplitude={waveAmplitude}
269
- agentState={voiceRoom.agentState}
270
- isConnected={voiceRoom.isConnected}
271
- />
272
- )}
273
- {showTTSOverlay && (
274
- <VoiceOverlay
275
- waveAmplitude={tts.ttsWaveAmplitude}
276
- agentState="speaking"
277
- isConnected={true}
278
- />
279
- )}
280
- {showSTTOverlay && (
281
- <VoiceOverlay
282
- waveAmplitude={sttWaveAmplitude}
283
- agentState="listening"
284
- isConnected={true}
285
- />
286
- )}
253
+ {/* Welcome or Messages */}
254
+ <AliaChatMessageList
255
+ messages={messages}
256
+ isStreaming={isStreaming}
257
+ scrollOffsetY={scrollOffsetY}
258
+ onReadAloud={handleReadAloud}
259
+ ttsActiveMessageId={tts.activeMessageId}
260
+ ttsPlaybackState={tts.playbackState}
261
+ onEditMessage={onEditMessage}
262
+ onThumbsUp={onThumbsUp}
263
+ onThumbsDown={onThumbsDown}
264
+ onApprovePlan={onApprovePlan}
265
+ onRejectPlan={onRejectPlan}
266
+ onToolResultPress={onToolResultPress}
267
+ renderMarkdown={renderMarkdown}
268
+ welcomeComponent={welcomeComponent}
269
+ />
287
270
 
288
- {/* Welcome or Messages */}
289
- <AliaChatMessageList
290
- messages={messages}
291
- isStreaming={isStreaming}
292
- scrollOffsetY={scrollOffsetY}
293
- onReadAloud={handleReadAloud}
294
- ttsActiveMessageId={tts.activeMessageId}
295
- ttsPlaybackState={tts.playbackState}
296
- onEditMessage={onEditMessage}
297
- onThumbsUp={onThumbsUp}
298
- onThumbsDown={onThumbsDown}
299
- onApprovePlan={onApprovePlan}
300
- onRejectPlan={onRejectPlan}
301
- onToolResultPress={onToolResultPress}
302
- renderMarkdown={renderMarkdown}
303
- welcomeComponent={welcomeComponent}
271
+ {/* Input or Voice Controls */}
272
+ {isVoiceActive ? (
273
+ <VoiceControls
274
+ roomState={voiceRoom.roomState}
275
+ agentState={voiceRoom.agentState}
276
+ isMuted={voiceRoom.isMuted}
277
+ cohostActive={voiceRoom.cohostActive}
278
+ currentSpeaker={voiceRoom.currentSpeaker}
279
+ roundComplete={voiceRoom.roundComplete}
280
+ onToggleMute={voiceRoom.toggleMute}
281
+ onEnableCohost={voiceRoom.enableCohost}
282
+ onDisableCohost={voiceRoom.disableCohost}
283
+ onContinueCohost={voiceRoom.continueCohost}
284
+ onEnd={deactivateVoice}
304
285
  />
305
-
306
- {/* Input or Voice Controls */}
307
- {isVoiceActive ? (
308
- <VoiceControls
309
- roomState={voiceRoom.roomState}
310
- agentState={voiceRoom.agentState}
311
- isMuted={voiceRoom.isMuted}
312
- cohostActive={voiceRoom.cohostActive}
313
- currentSpeaker={voiceRoom.currentSpeaker}
314
- roundComplete={voiceRoom.roundComplete}
315
- onToggleMute={voiceRoom.toggleMute}
316
- onEnableCohost={voiceRoom.enableCohost}
317
- onDisableCohost={voiceRoom.disableCohost}
318
- onContinueCohost={voiceRoom.continueCohost}
319
- onEnd={deactivateVoice}
286
+ ) : (
287
+ <View style={styles.inputContainer}>
288
+ <PromptInput
289
+ value={inputValue}
290
+ onValueChange={setInputValue}
291
+ onSubmit={handleSubmit}
292
+ isLoading={isStreaming}
293
+ onStop={stop}
294
+ emptyAction={voiceActivateButton}
295
+ placeholder="Ask Alia..."
296
+ apiUrl={apiUrl}
297
+ autocomplete={!!useSuggestions}
298
+ useSuggestions={useSuggestions}
299
+ onSuggestionUsed={onSuggestionUsed}
300
+ onError={onError}
320
301
  />
321
- ) : (
322
- <View style={styles.inputContainer}>
323
- <PromptInput
324
- value={inputValue}
325
- onValueChange={setInputValue}
326
- onSubmit={handleSubmit}
327
- isLoading={isStreaming}
328
- onStop={stop}
329
- emptyAction={voiceActivateButton}
330
- placeholder="Ask Alia..."
331
- apiUrl={apiUrl}
332
- autocomplete={!!useSuggestions}
333
- useSuggestions={useSuggestions}
334
- onSuggestionUsed={onSuggestionUsed}
335
- onError={onError}
336
- />
337
- </View>
338
- )}
339
- </View>
340
- );
341
- },
342
- );
343
-
344
- AliaChatContent.displayName = 'AliaChatContent';
302
+ </View>
303
+ )}
304
+ </View>
305
+ );
306
+ }
345
307
 
346
308
  const styles = StyleSheet.create({
347
309
  container: {
@@ -32,7 +32,12 @@ import { getTextFromContent, getImagesFromContent } from '../lib/content-utils';
32
32
  import type { ChatMessage, ToolInvocation } from '../types';
33
33
 
34
34
  // Resolve expo-image once at module load (optional peer dep)
35
- let ExpoImage: any = null;
35
+ type ExpoImageProps = {
36
+ source: { uri: string };
37
+ className?: string;
38
+ contentFit?: string;
39
+ };
40
+ let ExpoImage: React.ComponentType<ExpoImageProps> | null = null;
36
41
  try { ExpoImage = require('expo-image').Image; } catch {}
37
42
 
38
43
  type PlaybackState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
@@ -433,7 +438,7 @@ export function AliaChatMessageList({
433
438
  useEffect(() => {
434
439
  if (messages.length > 0) {
435
440
  const timer = setTimeout(() => {
436
- (scrollRef.current as any)?.scrollToEnd?.({ animated: true });
441
+ (scrollRef.current as { scrollToEnd?: (opts: { animated: boolean }) => void } | null)?.scrollToEnd?.({ animated: true });
437
442
  }, 100);
438
443
  return () => clearTimeout(timer);
439
444
  }