@alia.onl/sdk 10.1.1 → 11.0.0
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/CHANGELOG.md +53 -0
- package/README.md +24 -0
- package/package.json +4 -6
- package/src/components/AliaAvatar.tsx +66 -0
- package/src/components/AliaChatContent.tsx +235 -112
- package/src/components/AliaChatHeader.tsx +118 -0
- package/src/components/AliaChatMessageList.tsx +171 -260
- package/src/components/AliaChatScreen.tsx +23 -24
- package/src/components/AliaChatSheet.tsx +70 -316
- package/src/components/AliaWelcomeMessage.tsx +47 -50
- package/src/components/AmbientField.tsx +577 -0
- package/src/components/IdentityMark.tsx +10 -2
- package/src/components/Markdown.tsx +116 -40
- package/src/components/PlanPreviewCard.tsx +71 -55
- package/src/components/ThinkingBlock.tsx +47 -0
- package/src/components/VoiceModeIcon.tsx +10 -0
- package/src/components/voice/VoiceControls.tsx +22 -73
- package/src/components/voice/VoiceSession.tsx +2 -1
- package/src/hooks/useAliaChat.ts +22 -15
- package/src/hooks/useAmbientWave.ts +35 -5
- package/src/hooks/useSoundEffects.ts +14 -11
- package/src/hooks/useSpeechToText.ts +9 -2
- package/src/hooks/useTTS.ts +163 -156
- package/src/hooks/useVoiceRoom.ts +228 -170
- package/src/index.ts +18 -6
- package/src/lib/catalogue.ts +17 -4
- package/src/lib/chat-stream.ts +25 -7
- package/src/lib/chat-transport.ts +2 -1
- package/src/lib/citations.ts +63 -0
- package/src/lib/content-utils.ts +3 -2
- package/src/lib/image-data-url.ts +15 -0
- package/src/lib/keyboard.tsx +1 -1
- package/src/lib/labels.tsx +81 -0
- package/src/lib/speech-recognition.native.ts +15 -4
- package/src/lib/speech-recognition.ts +27 -7
- package/src/lib/task-labels.ts +67 -0
- package/src/lib/tool-call-status.ts +28 -0
- package/src/lib/tool-registry.ts +14 -14
- package/src/lib/voice-text.ts +12 -6
- package/src/lib/voice-turn.ts +4 -1
- package/src/lib/work-log-copy.ts +61 -0
- package/src/lib/work-log.ts +378 -0
- package/src/tools.ts +11 -0
- package/src/types.ts +5 -2
- package/src/voice.ts +12 -4
- package/src/components/Reasoning.tsx +0 -152
- package/src/components/ResearchProgressCard.tsx +0 -159
- package/src/components/ThinkingIndicator.tsx +0 -98
- package/src/components/ui/button.tsx +0 -104
- package/src/components/ui/chat-text-input.tsx +0 -184
- package/src/components/ui/dropdown-menu.tsx +0 -2
- package/src/components/ui/dropdown-menu.web.tsx +0 -383
- package/src/components/ui/prompt-input/actions.tsx +0 -28
- package/src/components/ui/prompt-input/add-menu.tsx +0 -83
- package/src/components/ui/prompt-input/attachments.tsx +0 -181
- package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
- package/src/components/ui/prompt-input/context.tsx +0 -71
- package/src/components/ui/prompt-input/mic-button.tsx +0 -53
- package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
- package/src/components/ui/prompt-input/submit-button.tsx +0 -50
- package/src/components/ui/prompt-input/textarea.tsx +0 -80
- package/src/components/ui/text.tsx +0 -29
- package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
- package/src/components/voice/VoiceOverlay.tsx +0 -75
|
@@ -1,33 +1,42 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
-
import { View, Pressable,
|
|
3
|
-
import { BlurView } from 'expo-blur';
|
|
2
|
+
import { View, Pressable, Platform } from 'react-native';
|
|
4
3
|
import Animated, {
|
|
5
|
-
useAnimatedScrollHandler,
|
|
6
4
|
useSharedValue,
|
|
7
5
|
useAnimatedStyle,
|
|
8
6
|
withRepeat,
|
|
9
7
|
withTiming,
|
|
10
8
|
withSequence,
|
|
11
9
|
cancelAnimation,
|
|
12
|
-
type SharedValue,
|
|
13
10
|
} from 'react-native-reanimated';
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import ThumbsUp from 'lucide-react-native/icons/thumbs-up';
|
|
19
|
-
import ThumbsDown from 'lucide-react-native/icons/thumbs-down';
|
|
20
|
-
import Pencil from 'lucide-react-native/icons/pencil';
|
|
21
|
-
import { Text } from './ui/text';
|
|
11
|
+
import { Textarea } from '@oxy.so/bloom/textarea';
|
|
12
|
+
import { TaskList } from '@oxy.so/bloom/task-list';
|
|
13
|
+
import { Text } from '@oxy.so/bloom/typography';
|
|
14
|
+
import { WebSearch } from '@oxy.so/bloom/web-search';
|
|
22
15
|
import { AliaMarkdown } from './Markdown';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
16
|
+
import { AgentThinking } from '@oxy.so/bloom/agent-thinking';
|
|
17
|
+
import {
|
|
18
|
+
AiChatFeedbackRow,
|
|
19
|
+
AiChatMessageLine,
|
|
20
|
+
AiChatThread,
|
|
21
|
+
AiChatUserMessage,
|
|
22
|
+
} from '@oxy.so/bloom/ai-chat';
|
|
23
|
+
import { Avatar } from '@oxy.so/bloom/avatar';
|
|
24
|
+
import { Button } from '@oxy.so/bloom/button';
|
|
25
|
+
import { useLabels } from '../lib/labels';
|
|
26
|
+
import { isWebInvocation, taskListLog, webSearchLog } from '../lib/work-log';
|
|
27
|
+
import {
|
|
28
|
+
RiCheckLine,
|
|
29
|
+
RiFileCopyLine,
|
|
30
|
+
RiPencilLine,
|
|
31
|
+
RiStopFill,
|
|
32
|
+
RiVolumeUpLine,
|
|
33
|
+
} from '@oxy.so/bloom/icons';
|
|
34
|
+
import { ThinkingBlock } from './ThinkingBlock';
|
|
26
35
|
import { PlanPreviewCard } from './PlanPreviewCard';
|
|
27
36
|
import { chatMessageEnter } from '../animations';
|
|
28
37
|
import { getToolLabel, getToolActiveLabel, getResearchActiveLabel } from '../lib/tool-registry';
|
|
29
38
|
import { getTextFromContent, getImagesFromContent } from '../lib/content-utils';
|
|
30
|
-
import type { ChatMessage
|
|
39
|
+
import type { ChatMessage } from '../types';
|
|
31
40
|
|
|
32
41
|
// Resolve expo-image once at module load (optional peer dep)
|
|
33
42
|
type ExpoImageProps = {
|
|
@@ -49,7 +58,6 @@ type PlaybackState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
|
|
|
49
58
|
export interface AliaChatMessageListProps {
|
|
50
59
|
messages: ChatMessage[];
|
|
51
60
|
isStreaming: boolean;
|
|
52
|
-
scrollOffsetY: SharedValue<number>;
|
|
53
61
|
onReadAloud?: (messageId: string, text: string) => void;
|
|
54
62
|
ttsActiveMessageId?: string | null;
|
|
55
63
|
ttsPlaybackState?: PlaybackState;
|
|
@@ -59,42 +67,12 @@ export interface AliaChatMessageListProps {
|
|
|
59
67
|
onThumbsDown?: (messageId: string) => void;
|
|
60
68
|
onApprovePlan?: (planId: string) => void;
|
|
61
69
|
onRejectPlan?: (planId: string) => void;
|
|
62
|
-
onToolResultPress?: (messageId: string) => void;
|
|
63
70
|
// Welcome
|
|
64
71
|
welcomeComponent?: React.ReactNode;
|
|
65
|
-
conversationLoading?: boolean;
|
|
66
72
|
// Markdown renderer override (app passes CustomMarkdown, SDK uses AliaMarkdown)
|
|
67
73
|
renderMarkdown?: (content: string) => React.ReactNode;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
// ── Tool Bullet ──
|
|
71
|
-
|
|
72
|
-
function ToolBullet({ isRunning }: { isRunning: boolean }) {
|
|
73
|
-
const opacity = useSharedValue(1);
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (isRunning) {
|
|
76
|
-
opacity.value = withRepeat(
|
|
77
|
-
withSequence(
|
|
78
|
-
withTiming(0.3, { duration: 500 }),
|
|
79
|
-
withTiming(1, { duration: 500 }),
|
|
80
|
-
),
|
|
81
|
-
-1,
|
|
82
|
-
);
|
|
83
|
-
} else {
|
|
84
|
-
opacity.value = 1;
|
|
85
|
-
}
|
|
86
|
-
return () => cancelAnimation(opacity);
|
|
87
|
-
}, [isRunning]);
|
|
88
|
-
const style = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
89
|
-
return (
|
|
90
|
-
<Animated.View style={style}>
|
|
91
|
-
<Text style={{ color: isRunning ? '#eab308' : '#22c55e', fontSize: 10 }}>
|
|
92
|
-
{'\u25CF'}
|
|
93
|
-
</Text>
|
|
94
|
-
</Animated.View>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
76
|
// ── User Bubble ──
|
|
99
77
|
|
|
100
78
|
const UserBubble = React.memo(function UserBubble({
|
|
@@ -122,84 +100,73 @@ const UserBubble = React.memo(function UserBubble({
|
|
|
122
100
|
copiedMessageId: string | null;
|
|
123
101
|
showEditButton: boolean;
|
|
124
102
|
}) {
|
|
103
|
+
const labels = useLabels();
|
|
125
104
|
const images = getImagesFromContent(message.content);
|
|
126
105
|
|
|
127
106
|
return (
|
|
128
107
|
<View className="flex-col items-end gap-0.5">
|
|
129
108
|
{isEditing ? (
|
|
130
|
-
<View className="max-w-[85%] sm:max-w-[75%]
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</Pressable>
|
|
147
|
-
<Pressable
|
|
148
|
-
className="px-3 py-1.5 rounded-lg bg-muted-foreground"
|
|
149
|
-
onPress={onCancelEdit}
|
|
150
|
-
>
|
|
151
|
-
<Text className="text-xs text-background">Cancel</Text>
|
|
152
|
-
</Pressable>
|
|
153
|
-
</View>
|
|
109
|
+
<View className="w-full max-w-[85%] gap-2 sm:max-w-[75%]">
|
|
110
|
+
<Textarea
|
|
111
|
+
value={editedContent}
|
|
112
|
+
onValueChange={onEditedContentChange}
|
|
113
|
+
rows={1}
|
|
114
|
+
autoResize
|
|
115
|
+
autoFocus
|
|
116
|
+
accessibilityLabel={labels.edit}
|
|
117
|
+
/>
|
|
118
|
+
<View className="flex-row justify-end gap-2">
|
|
119
|
+
<Button tone="neutral" appearance="subtle" size="xs" onPress={onCancelEdit}>
|
|
120
|
+
{labels.cancel}
|
|
121
|
+
</Button>
|
|
122
|
+
<Button tone="action" size="xs" onPress={() => onSaveEdit(message.id)}>
|
|
123
|
+
{labels.save}
|
|
124
|
+
</Button>
|
|
154
125
|
</View>
|
|
155
126
|
</View>
|
|
156
127
|
) : (
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
128
|
+
<AiChatUserMessage
|
|
129
|
+
animate={false}
|
|
130
|
+
actions={[
|
|
131
|
+
{
|
|
132
|
+
key: 'copy',
|
|
133
|
+
label: copiedMessageId === message.id ? labels.copied : labels.copy,
|
|
134
|
+
icon: copiedMessageId === message.id ? RiCheckLine : RiFileCopyLine,
|
|
135
|
+
onPress: () => onCopy(text, message.id),
|
|
136
|
+
},
|
|
137
|
+
...(showEditButton
|
|
138
|
+
? [
|
|
139
|
+
{
|
|
140
|
+
key: 'edit',
|
|
141
|
+
label: labels.edit,
|
|
142
|
+
icon: RiPencilLine,
|
|
143
|
+
onPress: () => onStartEdit(message.id, text),
|
|
144
|
+
},
|
|
145
|
+
]
|
|
146
|
+
: []),
|
|
147
|
+
]}
|
|
148
|
+
>
|
|
149
|
+
{images.length > 0 && ExpoImage ? (
|
|
150
|
+
<AiChatMessageLine block>
|
|
151
|
+
<View className="flex-row flex-wrap gap-2">
|
|
152
|
+
{images.map((imgUrl) => (
|
|
153
|
+
<View
|
|
154
|
+
key={imgUrl}
|
|
155
|
+
className="rounded-xl overflow-hidden"
|
|
156
|
+
style={{ width: 120, height: 120 }}
|
|
157
|
+
>
|
|
158
|
+
<ExpoImage
|
|
159
|
+
source={{ uri: imgUrl }}
|
|
160
|
+
className="w-full h-full"
|
|
161
|
+
contentFit="cover"
|
|
162
|
+
/>
|
|
163
|
+
</View>
|
|
164
|
+
))}
|
|
175
165
|
</View>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
</
|
|
179
|
-
</
|
|
180
|
-
)}
|
|
181
|
-
{/* User message actions */}
|
|
182
|
-
{!isEditing && (
|
|
183
|
-
<View className="flex-row gap-1">
|
|
184
|
-
<Pressable
|
|
185
|
-
className="p-1.5 rounded-lg active:bg-muted"
|
|
186
|
-
onPress={() => onCopy(text, message.id)}
|
|
187
|
-
>
|
|
188
|
-
{copiedMessageId === message.id ? (
|
|
189
|
-
<Check size={14} className="text-green-500" />
|
|
190
|
-
) : (
|
|
191
|
-
<Copy size={14} className="text-muted-foreground" />
|
|
192
|
-
)}
|
|
193
|
-
</Pressable>
|
|
194
|
-
{showEditButton && (
|
|
195
|
-
<Pressable
|
|
196
|
-
className="p-1.5 rounded-lg active:bg-muted"
|
|
197
|
-
onPress={() => onStartEdit(message.id, text)}
|
|
198
|
-
>
|
|
199
|
-
<Pencil size={14} className="text-muted-foreground" />
|
|
200
|
-
</Pressable>
|
|
201
|
-
)}
|
|
202
|
-
</View>
|
|
166
|
+
</AiChatMessageLine>
|
|
167
|
+
) : null}
|
|
168
|
+
<AiChatMessageLine>{text}</AiChatMessageLine>
|
|
169
|
+
</AiChatUserMessage>
|
|
203
170
|
)}
|
|
204
171
|
</View>
|
|
205
172
|
);
|
|
@@ -217,12 +184,10 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
217
184
|
ttsActiveMessageId,
|
|
218
185
|
ttsPlaybackState,
|
|
219
186
|
onCopy,
|
|
220
|
-
copiedMessageId,
|
|
221
187
|
onThumbsUp,
|
|
222
188
|
onThumbsDown,
|
|
223
189
|
onApprovePlan,
|
|
224
190
|
onRejectPlan,
|
|
225
|
-
onToolResultPress,
|
|
226
191
|
renderMarkdown,
|
|
227
192
|
}: {
|
|
228
193
|
message: ChatMessage;
|
|
@@ -234,26 +199,40 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
234
199
|
ttsActiveMessageId?: string | null;
|
|
235
200
|
ttsPlaybackState?: PlaybackState;
|
|
236
201
|
onCopy: (text: string, messageId: string) => void;
|
|
237
|
-
copiedMessageId: string | null;
|
|
238
202
|
onThumbsUp?: (messageId: string) => void;
|
|
239
203
|
onThumbsDown?: (messageId: string) => void;
|
|
240
204
|
onApprovePlan?: (planId: string) => void;
|
|
241
205
|
onRejectPlan?: (planId: string) => void;
|
|
242
|
-
onToolResultPress?: (messageId: string) => void;
|
|
243
206
|
renderMarkdown?: (content: string) => React.ReactNode;
|
|
244
207
|
}) {
|
|
208
|
+
const labels = useLabels();
|
|
245
209
|
const hasContent = messageText.length > 0;
|
|
246
210
|
const hasThinking = !!message.thinking;
|
|
247
211
|
|
|
212
|
+
// Both logs count units and are driven by `revealed`, from the calls that have really
|
|
213
|
+
// arrived — never by their own demo ticker. The task list waits for the turn to end.
|
|
214
|
+
const toolInvocations = message.toolInvocations;
|
|
215
|
+
const researchProgress = message.researchProgress;
|
|
216
|
+
const webLog = useMemo(
|
|
217
|
+
() => webSearchLog(toolInvocations, researchProgress, { t: labels.workLog }),
|
|
218
|
+
[toolInvocations, researchProgress, labels.workLog],
|
|
219
|
+
);
|
|
220
|
+
const taskLog = useMemo(() => {
|
|
221
|
+
const tasks = (toolInvocations ?? []).filter((inv) => !isWebInvocation(inv));
|
|
222
|
+
return tasks.length === 0 || isStreamingThis
|
|
223
|
+
? null
|
|
224
|
+
: taskListLog(tasks, false, { t: labels.workLog });
|
|
225
|
+
}, [toolInvocations, isStreamingThis, labels.workLog]);
|
|
226
|
+
|
|
248
227
|
const isThisPlaying = ttsActiveMessageId === message.id && ttsPlaybackState === 'playing';
|
|
249
228
|
const isThisPaused = ttsActiveMessageId === message.id && ttsPlaybackState === 'paused';
|
|
250
229
|
const isThisLoading = ttsActiveMessageId === message.id && ttsPlaybackState === 'loading';
|
|
251
230
|
|
|
252
|
-
//
|
|
231
|
+
// AgentThinking — context-aware status
|
|
253
232
|
let thinkingIndicator: React.ReactNode = null;
|
|
254
233
|
if (isLoading && isLastMessage && !hasContent) {
|
|
255
234
|
const activeTool = message.toolInvocations?.find(
|
|
256
|
-
t => t.state === 'call' || t.state === 'partial-call',
|
|
235
|
+
(t) => t.state === 'call' || t.state === 'partial-call',
|
|
257
236
|
);
|
|
258
237
|
const rp = message.researchProgress;
|
|
259
238
|
let activeStatus: string | undefined;
|
|
@@ -261,15 +240,16 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
261
240
|
activeStatus = getToolActiveLabel(activeTool.toolName);
|
|
262
241
|
} else if (rp?.phase && rp.phase !== 'complete') {
|
|
263
242
|
activeStatus = getResearchActiveLabel(rp.phase);
|
|
264
|
-
} else if (message.thinking) {
|
|
265
|
-
activeStatus = 'Reasoning...';
|
|
266
243
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
244
|
+
// While it reasons, the reasoning block draws its own indicator: two at once is noise.
|
|
245
|
+
if (activeStatus !== undefined || !hasThinking) {
|
|
246
|
+
thinkingIndicator = (
|
|
247
|
+
<AgentThinking
|
|
248
|
+
variant={(message.toolInvocations?.length ?? 0) > 0 ? 'spin' : 'wave'}
|
|
249
|
+
label={activeStatus?.replace(/\.{3}$/, '')}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
273
253
|
}
|
|
274
254
|
|
|
275
255
|
return (
|
|
@@ -285,52 +265,27 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
285
265
|
/>
|
|
286
266
|
)}
|
|
287
267
|
|
|
288
|
-
{/*
|
|
289
|
-
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<Pressable
|
|
306
|
-
key={key}
|
|
307
|
-
className="flex-row items-center gap-2 py-1 active:opacity-70"
|
|
308
|
-
onPress={isDone && onToolResultPress ? () => onToolResultPress(message.id) : undefined}
|
|
309
|
-
disabled={!isDone || !onToolResultPress}
|
|
310
|
-
>
|
|
311
|
-
<ToolBullet isRunning={isRunning} />
|
|
312
|
-
<Text className="text-sm text-foreground flex-1 flex-shrink">
|
|
313
|
-
<Text className="font-bold">{toolLabel}</Text>
|
|
314
|
-
{description ? (
|
|
315
|
-
<Text className="text-muted-foreground"> {description}</Text>
|
|
316
|
-
) : null}
|
|
317
|
-
</Text>
|
|
318
|
-
</Pressable>
|
|
319
|
-
);
|
|
320
|
-
})}
|
|
321
|
-
|
|
322
|
-
{/* Research Progress */}
|
|
323
|
-
{message.researchProgress && (
|
|
324
|
-
<ResearchProgressCard progress={message.researchProgress} />
|
|
268
|
+
{/* What the turn did: where it went (searches, pages, research) and everything else. */}
|
|
269
|
+
{taskLog === null ? null : (
|
|
270
|
+
<TaskList
|
|
271
|
+
tasks={taskLog.tasks}
|
|
272
|
+
revealed={taskLog.revealed}
|
|
273
|
+
collapseOnComplete="all"
|
|
274
|
+
working={false}
|
|
275
|
+
/>
|
|
276
|
+
)}
|
|
277
|
+
{webLog.steps.length === 0 ? null : (
|
|
278
|
+
<WebSearch
|
|
279
|
+
steps={webLog.steps}
|
|
280
|
+
revealed={webLog.revealed}
|
|
281
|
+
working={isStreamingThis ? undefined : false}
|
|
282
|
+
/>
|
|
325
283
|
)}
|
|
326
284
|
|
|
327
285
|
{/* Extended Thinking */}
|
|
328
286
|
{hasThinking && (
|
|
329
287
|
<View className="mb-3 w-full">
|
|
330
|
-
<
|
|
331
|
-
<ReasoningTrigger />
|
|
332
|
-
<ReasoningContent>{message.thinking!}</ReasoningContent>
|
|
333
|
-
</Reasoning>
|
|
288
|
+
<ThinkingBlock text={message.thinking!} streaming={isStreamingThis && !hasContent} />
|
|
334
289
|
</View>
|
|
335
290
|
)}
|
|
336
291
|
|
|
@@ -339,18 +294,16 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
339
294
|
<View className="flex-col items-start gap-0.5">
|
|
340
295
|
{/* Agent identity */}
|
|
341
296
|
{message.agentInfo && (
|
|
342
|
-
<View className="flex-row items-center gap-2
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
</View>
|
|
346
|
-
<Text className="text-xs font-semibold" style={{ color: '#f97316' }}>
|
|
297
|
+
<View className="mb-0.5 flex-row items-center gap-2">
|
|
298
|
+
<Avatar source={message.agentInfo.avatar} size="xs" />
|
|
299
|
+
<Text className="text-xs font-semibold text-text-primary">
|
|
347
300
|
{message.agentInfo.name}
|
|
348
301
|
</Text>
|
|
349
302
|
</View>
|
|
350
303
|
)}
|
|
351
304
|
<View className="w-full">
|
|
352
305
|
{message.source === 'voice' ? (
|
|
353
|
-
<Text className="text-base text-
|
|
306
|
+
<Text className="text-base text-text-primary leading-7">
|
|
354
307
|
{messageText}
|
|
355
308
|
{message.isStreaming ? '\u258C' : ''}
|
|
356
309
|
</Text>
|
|
@@ -360,42 +313,27 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
360
313
|
<AliaMarkdown content={messageText} />
|
|
361
314
|
)}
|
|
362
315
|
</View>
|
|
363
|
-
{/* Action buttons */}
|
|
364
316
|
{!isStreamingThis && hasContent && (
|
|
365
|
-
<
|
|
366
|
-
{
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
<Copy size={14} className="text-muted-foreground" />
|
|
386
|
-
)}
|
|
387
|
-
</Pressable>
|
|
388
|
-
{onThumbsUp && (
|
|
389
|
-
<Pressable className="p-1.5 rounded-lg active:bg-muted" onPress={() => onThumbsUp(message.id)}>
|
|
390
|
-
<ThumbsUp size={14} className="text-muted-foreground" />
|
|
391
|
-
</Pressable>
|
|
392
|
-
)}
|
|
393
|
-
{onThumbsDown && (
|
|
394
|
-
<Pressable className="p-1.5 rounded-lg active:bg-muted" onPress={() => onThumbsDown(message.id)}>
|
|
395
|
-
<ThumbsDown size={14} className="text-muted-foreground" />
|
|
396
|
-
</Pressable>
|
|
397
|
-
)}
|
|
398
|
-
</View>
|
|
317
|
+
<AiChatFeedbackRow
|
|
318
|
+
onLike={onThumbsUp ? () => onThumbsUp(message.id) : undefined}
|
|
319
|
+
onDislike={onThumbsDown ? () => onThumbsDown(message.id) : undefined}
|
|
320
|
+
onCopy={() => onCopy(messageText, message.id)}
|
|
321
|
+
actions={
|
|
322
|
+
onReadAloud
|
|
323
|
+
? [
|
|
324
|
+
{
|
|
325
|
+
key: 'read-aloud',
|
|
326
|
+
label:
|
|
327
|
+
isThisPlaying || isThisPaused ? labels.stopReading : labels.readAloud,
|
|
328
|
+
icon: isThisPlaying || isThisPaused ? RiStopFill : RiVolumeUpLine,
|
|
329
|
+
active: isThisPlaying || isThisPaused,
|
|
330
|
+
disabled: isThisLoading,
|
|
331
|
+
onPress: () => onReadAloud(message.id, messageText),
|
|
332
|
+
},
|
|
333
|
+
]
|
|
334
|
+
: []
|
|
335
|
+
}
|
|
336
|
+
/>
|
|
399
337
|
)}
|
|
400
338
|
</View>
|
|
401
339
|
)}
|
|
@@ -410,7 +348,6 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
410
348
|
export function AliaChatMessageList({
|
|
411
349
|
messages,
|
|
412
350
|
isStreaming,
|
|
413
|
-
scrollOffsetY,
|
|
414
351
|
onReadAloud,
|
|
415
352
|
ttsActiveMessageId,
|
|
416
353
|
ttsPlaybackState,
|
|
@@ -419,12 +356,9 @@ export function AliaChatMessageList({
|
|
|
419
356
|
onThumbsDown,
|
|
420
357
|
onApprovePlan,
|
|
421
358
|
onRejectPlan,
|
|
422
|
-
onToolResultPress,
|
|
423
359
|
welcomeComponent,
|
|
424
|
-
conversationLoading,
|
|
425
360
|
renderMarkdown,
|
|
426
361
|
}: AliaChatMessageListProps) {
|
|
427
|
-
const scrollRef = useRef<Animated.ScrollView>(null);
|
|
428
362
|
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
|
429
363
|
const [editedContent, setEditedContent] = useState('');
|
|
430
364
|
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
|
@@ -434,22 +368,6 @@ export function AliaChatMessageList({
|
|
|
434
368
|
prevMessageCountRef.current = messages.length;
|
|
435
369
|
}, [messages.length]);
|
|
436
370
|
|
|
437
|
-
// Auto-scroll
|
|
438
|
-
useEffect(() => {
|
|
439
|
-
if (messages.length > 0) {
|
|
440
|
-
const timer = setTimeout(() => {
|
|
441
|
-
(scrollRef.current as { scrollToEnd?: (opts: { animated: boolean }) => void } | null)?.scrollToEnd?.({ animated: true });
|
|
442
|
-
}, 100);
|
|
443
|
-
return () => clearTimeout(timer);
|
|
444
|
-
}
|
|
445
|
-
}, [messages.length, isStreaming]);
|
|
446
|
-
|
|
447
|
-
const scrollHandler = useAnimatedScrollHandler({
|
|
448
|
-
onScroll: (event) => {
|
|
449
|
-
scrollOffsetY.value = event.contentOffset.y;
|
|
450
|
-
},
|
|
451
|
-
});
|
|
452
|
-
|
|
453
371
|
const handleCopy = useCallback(async (content: string, messageId: string) => {
|
|
454
372
|
try {
|
|
455
373
|
if (Platform.OS === 'web') {
|
|
@@ -473,36 +391,34 @@ export function AliaChatMessageList({
|
|
|
473
391
|
const editedContentRef = useRef(editedContent);
|
|
474
392
|
editedContentRef.current = editedContent;
|
|
475
393
|
|
|
476
|
-
const handleSaveEdit = useCallback(
|
|
477
|
-
|
|
478
|
-
onEditMessage
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
394
|
+
const handleSaveEdit = useCallback(
|
|
395
|
+
(messageId: string) => {
|
|
396
|
+
if (onEditMessage && editedContentRef.current.trim()) {
|
|
397
|
+
onEditMessage(messageId, editedContentRef.current);
|
|
398
|
+
}
|
|
399
|
+
setEditingMessageId(null);
|
|
400
|
+
setEditedContent('');
|
|
401
|
+
},
|
|
402
|
+
[onEditMessage],
|
|
403
|
+
);
|
|
483
404
|
|
|
484
405
|
const handleCancelEdit = useCallback(() => {
|
|
485
406
|
setEditingMessageId(null);
|
|
486
407
|
setEditedContent('');
|
|
487
408
|
}, []);
|
|
488
409
|
|
|
489
|
-
const filteredMessages = useMemo(() => messages.filter(m => m.role !== 'system'), [messages]);
|
|
410
|
+
const filteredMessages = useMemo(() => messages.filter((m) => m.role !== 'system'), [messages]);
|
|
490
411
|
|
|
491
412
|
return (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
scrollEventThrottle={16}
|
|
500
|
-
>
|
|
501
|
-
<View className="max-w-3xl mx-auto w-full" style={!filteredMessages.length ? { flex: 1, justifyContent: 'center' } : undefined}>
|
|
413
|
+
// Bloom's thread: follows the newest turn, and reports its scroll to a floating-chrome
|
|
414
|
+
// container so the header and composer fade in over it.
|
|
415
|
+
<AiChatThread>
|
|
416
|
+
<View
|
|
417
|
+
className="max-w-3xl mx-auto w-full"
|
|
418
|
+
style={!filteredMessages.length ? { flex: 1, justifyContent: 'center' } : undefined}
|
|
419
|
+
>
|
|
502
420
|
{/* Welcome or loading */}
|
|
503
|
-
{!filteredMessages.length && (
|
|
504
|
-
welcomeComponent || null
|
|
505
|
-
)}
|
|
421
|
+
{!filteredMessages.length && (welcomeComponent || null)}
|
|
506
422
|
|
|
507
423
|
{/* Messages */}
|
|
508
424
|
<View className="gap-2">
|
|
@@ -513,10 +429,7 @@ export function AliaChatMessageList({
|
|
|
513
429
|
const messageText = getTextFromContent(msg.content);
|
|
514
430
|
|
|
515
431
|
return (
|
|
516
|
-
<Animated.View
|
|
517
|
-
key={msg.id}
|
|
518
|
-
entering={isNewMessage ? chatMessageEnter : undefined}
|
|
519
|
-
>
|
|
432
|
+
<Animated.View key={msg.id} entering={isNewMessage ? chatMessageEnter : undefined}>
|
|
520
433
|
{msg.role === 'user' ? (
|
|
521
434
|
<UserBubble
|
|
522
435
|
message={msg}
|
|
@@ -542,12 +455,10 @@ export function AliaChatMessageList({
|
|
|
542
455
|
ttsActiveMessageId={ttsActiveMessageId}
|
|
543
456
|
ttsPlaybackState={ttsPlaybackState}
|
|
544
457
|
onCopy={handleCopy}
|
|
545
|
-
copiedMessageId={copiedMessageId}
|
|
546
458
|
onThumbsUp={onThumbsUp}
|
|
547
459
|
onThumbsDown={onThumbsDown}
|
|
548
460
|
onApprovePlan={onApprovePlan}
|
|
549
461
|
onRejectPlan={onRejectPlan}
|
|
550
|
-
onToolResultPress={onToolResultPress}
|
|
551
462
|
renderMarkdown={renderMarkdown}
|
|
552
463
|
/>
|
|
553
464
|
)}
|
|
@@ -556,6 +467,6 @@ export function AliaChatMessageList({
|
|
|
556
467
|
})}
|
|
557
468
|
</View>
|
|
558
469
|
</View>
|
|
559
|
-
</
|
|
470
|
+
</AiChatThread>
|
|
560
471
|
);
|
|
561
472
|
}
|