@alia.onl/sdk 3.1.2 → 3.1.4
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 +40 -28
- package/src/components/AliaChatContent.tsx +215 -253
- package/src/components/AliaChatMessageList.tsx +7 -2
- package/src/components/AliaChatScreen.tsx +28 -42
- package/src/components/AliaChatSheet.tsx +40 -55
- package/src/components/AliaMark.tsx +144 -0
- package/src/components/AliaWelcomeMessage.tsx +5 -5
- package/src/components/Markdown.tsx +18 -18
- package/src/components/ThinkingIndicator.tsx +6 -27
- package/src/components/ui/chat-text-input.tsx +4 -3
- package/src/components/ui/dropdown-menu.web.tsx +7 -6
- package/src/components/ui/prompt-input/autocomplete.tsx +1 -1
- package/src/components/ui/prompt-input/context.tsx +1 -1
- package/src/components/ui/prompt-input/prompt-input.tsx +3 -1
- package/src/components/voice/AudioWaveVisualizer.tsx +27 -10
- package/src/components/voice/VoiceOverlay.tsx +37 -9
- package/src/hooks/useAliaChat.ts +6 -6
- package/src/hooks/useAmbientWave.ts +142 -0
- package/src/hooks/useAudioLevelMonitor.ts +6 -6
- package/src/hooks/useDocumentPicker.ts +1 -1
- package/src/hooks/useImagePicker.ts +1 -1
- package/src/hooks/useSoundEffects.ts +6 -5
- package/src/hooks/useSpeechToText.ts +21 -7
- package/src/hooks/useTTS.ts +6 -4
- package/src/hooks/useVoiceRoom.ts +52 -7
- package/src/index.ts +4 -2
- package/src/lib/content-utils.ts +18 -6
- package/src/lib/utils.ts +11 -0
- package/src/optional-deps.d.ts +76 -15
- package/src/types.ts +4 -4
- package/src/components/AliaFace.tsx +0 -452
package/package.json
CHANGED
|
@@ -1,45 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia.onl/sdk",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.4",
|
|
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",
|
|
7
7
|
"source": "src/index.ts",
|
|
8
8
|
"types": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"tsconfig.json"
|
|
12
|
+
],
|
|
9
13
|
"private": false,
|
|
10
14
|
"publishConfig": {
|
|
11
15
|
"access": "public"
|
|
12
16
|
},
|
|
13
17
|
"dependencies": {
|
|
14
|
-
"@
|
|
15
|
-
"@tanstack/
|
|
16
|
-
"@tanstack/react-query
|
|
17
|
-
"react-
|
|
18
|
-
"
|
|
18
|
+
"@radix-ui/react-icons": "^1.3.2",
|
|
19
|
+
"@tanstack/query-async-storage-persister": "^5.101.2",
|
|
20
|
+
"@tanstack/react-query": "^5.101.2",
|
|
21
|
+
"@tanstack/react-query-persist-client": "^5.101.2",
|
|
22
|
+
"react-native-markdown-display": "^7.0.2",
|
|
23
|
+
"zustand": "^5.0.14"
|
|
19
24
|
},
|
|
20
25
|
"peerDependencies": {
|
|
21
|
-
"react": "
|
|
22
|
-
"react-native": "
|
|
23
|
-
"@oxyhq/services": "^
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"react-native-
|
|
29
|
-
"react-native-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"expo-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
26
|
+
"react": "^19.2.3",
|
|
27
|
+
"react-native": "^0.86.0",
|
|
28
|
+
"@oxyhq/services": "^20.0.0",
|
|
29
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
30
|
+
"socket.io-client": "^4.8.3",
|
|
31
|
+
"expo-font": "^57.0.0",
|
|
32
|
+
"expo-blur": "^57.0.0",
|
|
33
|
+
"react-native-reanimated": "^4.5.0",
|
|
34
|
+
"react-native-gesture-handler": "^2.32.0",
|
|
35
|
+
"react-native-safe-area-context": "^5.8.0",
|
|
36
|
+
"react-native-keyboard-controller": "^1.21.9",
|
|
37
|
+
"react-native-svg": "^15.15.4",
|
|
38
|
+
"livekit-client": "^2.20.1",
|
|
39
|
+
"expo-audio": "^57.0.0",
|
|
40
|
+
"nativewind": "^5.0.0-preview.3",
|
|
41
|
+
"clsx": "^2.1.1",
|
|
42
|
+
"tailwind-merge": "^3.6.0",
|
|
43
|
+
"class-variance-authority": "^0.7.1",
|
|
44
|
+
"zeego": "^3.0.6",
|
|
45
|
+
"expo-image": "^57.0.0",
|
|
46
|
+
"expo-image-picker": "^57.0.2",
|
|
47
|
+
"expo-document-picker": "^57.0.0",
|
|
48
|
+
"expo-clipboard": "^57.0.0",
|
|
49
|
+
"lucide-react-native": "^0.562.0",
|
|
50
|
+
"@radix-ui/react-dropdown-menu": "^2.1.20",
|
|
51
|
+
"expo-haptics": "*"
|
|
43
52
|
},
|
|
44
53
|
"peerDependenciesMeta": {
|
|
45
54
|
"livekit-client": {
|
|
@@ -83,6 +92,9 @@
|
|
|
83
92
|
},
|
|
84
93
|
"@radix-ui/react-dropdown-menu": {
|
|
85
94
|
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"expo-haptics": {
|
|
97
|
+
"optional": true
|
|
86
98
|
}
|
|
87
99
|
},
|
|
88
100
|
"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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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:
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
108
|
+
// ── Input state ──
|
|
109
|
+
const [inputValue, setInputValue] = useState('');
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
const handleSubmit = useCallback(() => {
|
|
112
|
+
const text = inputValue.trim();
|
|
113
|
+
if (!text) return;
|
|
114
|
+
send(text);
|
|
115
|
+
setInputValue('');
|
|
116
|
+
}, [inputValue, send]);
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
// ── Scroll offset (use external if provided, else internal) ──
|
|
119
|
+
const internalScrollOffsetY = useSharedValue(0);
|
|
120
|
+
const scrollOffsetY = externalScrollOffsetY ?? internalScrollOffsetY;
|
|
122
121
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
132
|
+
// ── TTS ──
|
|
133
|
+
const tts = useTTS({ apiUrl });
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const sttWaveAmplitude = useSharedValue(0);
|
|
135
|
+
// ── Voice mode state ──
|
|
136
|
+
const [isVoiceActive, setIsVoiceActive] = useState(false);
|
|
137
|
+
const textSnapshotRef = useRef<ChatMessage[]>([]);
|
|
140
138
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
}, [voiceRoom.roomState, isVoiceActive]);
|
|
171
|
+
const deactivateVoice = useCallback(() => {
|
|
172
|
+
voiceRoom.disconnect();
|
|
173
|
+
setIsVoiceActive(false);
|
|
174
|
+
textSnapshotRef.current = [];
|
|
175
|
+
}, [voiceRoom]);
|
|
182
176
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
()
|
|
239
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
264
|
-
<
|
|
265
|
-
{
|
|
266
|
-
{
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
{
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
{
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
|
441
|
+
(scrollRef.current as { scrollToEnd?: (opts: { animated: boolean }) => void } | null)?.scrollToEnd?.({ animated: true });
|
|
437
442
|
}, 100);
|
|
438
443
|
return () => clearTimeout(timer);
|
|
439
444
|
}
|