@alia.onl/sdk 10.1.1 → 11.1.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 +67 -0
- package/README.md +24 -0
- package/package.json +4 -6
- package/src/components/AliaAvatar.tsx +66 -0
- package/src/components/AliaChatContent.tsx +236 -112
- package/src/components/AliaChatHeader.tsx +118 -0
- package/src/components/AliaChatMessageList.tsx +197 -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/QuestionCard.tsx +62 -0
- 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 +26 -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/questions.ts +78 -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 +18 -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,44 @@
|
|
|
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';
|
|
36
|
+
import { QuestionCard } from './QuestionCard';
|
|
37
|
+
import { askedQuestions, isQuestionInvocation } from '../lib/questions';
|
|
27
38
|
import { chatMessageEnter } from '../animations';
|
|
28
39
|
import { getToolLabel, getToolActiveLabel, getResearchActiveLabel } from '../lib/tool-registry';
|
|
29
40
|
import { getTextFromContent, getImagesFromContent } from '../lib/content-utils';
|
|
30
|
-
import type { ChatMessage
|
|
41
|
+
import type { ChatMessage } from '../types';
|
|
31
42
|
|
|
32
43
|
// Resolve expo-image once at module load (optional peer dep)
|
|
33
44
|
type ExpoImageProps = {
|
|
@@ -49,7 +60,6 @@ type PlaybackState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
|
|
|
49
60
|
export interface AliaChatMessageListProps {
|
|
50
61
|
messages: ChatMessage[];
|
|
51
62
|
isStreaming: boolean;
|
|
52
|
-
scrollOffsetY: SharedValue<number>;
|
|
53
63
|
onReadAloud?: (messageId: string, text: string) => void;
|
|
54
64
|
ttsActiveMessageId?: string | null;
|
|
55
65
|
ttsPlaybackState?: PlaybackState;
|
|
@@ -59,42 +69,14 @@ export interface AliaChatMessageListProps {
|
|
|
59
69
|
onThumbsDown?: (messageId: string) => void;
|
|
60
70
|
onApprovePlan?: (planId: string) => void;
|
|
61
71
|
onRejectPlan?: (planId: string) => void;
|
|
62
|
-
|
|
72
|
+
/** Sends the answers to Alia's questions as the person's next message. */
|
|
73
|
+
onAnswerQuestions?: (message: string) => void;
|
|
63
74
|
// Welcome
|
|
64
75
|
welcomeComponent?: React.ReactNode;
|
|
65
|
-
conversationLoading?: boolean;
|
|
66
76
|
// Markdown renderer override (app passes CustomMarkdown, SDK uses AliaMarkdown)
|
|
67
77
|
renderMarkdown?: (content: string) => React.ReactNode;
|
|
68
78
|
}
|
|
69
79
|
|
|
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
80
|
// ── User Bubble ──
|
|
99
81
|
|
|
100
82
|
const UserBubble = React.memo(function UserBubble({
|
|
@@ -122,84 +104,73 @@ const UserBubble = React.memo(function UserBubble({
|
|
|
122
104
|
copiedMessageId: string | null;
|
|
123
105
|
showEditButton: boolean;
|
|
124
106
|
}) {
|
|
107
|
+
const labels = useLabels();
|
|
125
108
|
const images = getImagesFromContent(message.content);
|
|
126
109
|
|
|
127
110
|
return (
|
|
128
111
|
<View className="flex-col items-end gap-0.5">
|
|
129
112
|
{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>
|
|
113
|
+
<View className="w-full max-w-[85%] gap-2 sm:max-w-[75%]">
|
|
114
|
+
<Textarea
|
|
115
|
+
value={editedContent}
|
|
116
|
+
onValueChange={onEditedContentChange}
|
|
117
|
+
rows={1}
|
|
118
|
+
autoResize
|
|
119
|
+
autoFocus
|
|
120
|
+
accessibilityLabel={labels.edit}
|
|
121
|
+
/>
|
|
122
|
+
<View className="flex-row justify-end gap-2">
|
|
123
|
+
<Button tone="neutral" appearance="subtle" size="xs" onPress={onCancelEdit}>
|
|
124
|
+
{labels.cancel}
|
|
125
|
+
</Button>
|
|
126
|
+
<Button tone="action" size="xs" onPress={() => onSaveEdit(message.id)}>
|
|
127
|
+
{labels.save}
|
|
128
|
+
</Button>
|
|
154
129
|
</View>
|
|
155
130
|
</View>
|
|
156
131
|
) : (
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
132
|
+
<AiChatUserMessage
|
|
133
|
+
animate={false}
|
|
134
|
+
actions={[
|
|
135
|
+
{
|
|
136
|
+
key: 'copy',
|
|
137
|
+
label: copiedMessageId === message.id ? labels.copied : labels.copy,
|
|
138
|
+
icon: copiedMessageId === message.id ? RiCheckLine : RiFileCopyLine,
|
|
139
|
+
onPress: () => onCopy(text, message.id),
|
|
140
|
+
},
|
|
141
|
+
...(showEditButton
|
|
142
|
+
? [
|
|
143
|
+
{
|
|
144
|
+
key: 'edit',
|
|
145
|
+
label: labels.edit,
|
|
146
|
+
icon: RiPencilLine,
|
|
147
|
+
onPress: () => onStartEdit(message.id, text),
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
: []),
|
|
151
|
+
]}
|
|
152
|
+
>
|
|
153
|
+
{images.length > 0 && ExpoImage ? (
|
|
154
|
+
<AiChatMessageLine block>
|
|
155
|
+
<View className="flex-row flex-wrap gap-2">
|
|
156
|
+
{images.map((imgUrl) => (
|
|
157
|
+
<View
|
|
158
|
+
key={imgUrl}
|
|
159
|
+
className="rounded-xl overflow-hidden"
|
|
160
|
+
style={{ width: 120, height: 120 }}
|
|
161
|
+
>
|
|
162
|
+
<ExpoImage
|
|
163
|
+
source={{ uri: imgUrl }}
|
|
164
|
+
className="w-full h-full"
|
|
165
|
+
contentFit="cover"
|
|
166
|
+
/>
|
|
167
|
+
</View>
|
|
168
|
+
))}
|
|
175
169
|
</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>
|
|
170
|
+
</AiChatMessageLine>
|
|
171
|
+
) : null}
|
|
172
|
+
<AiChatMessageLine>{text}</AiChatMessageLine>
|
|
173
|
+
</AiChatUserMessage>
|
|
203
174
|
)}
|
|
204
175
|
</View>
|
|
205
176
|
);
|
|
@@ -217,12 +188,11 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
217
188
|
ttsActiveMessageId,
|
|
218
189
|
ttsPlaybackState,
|
|
219
190
|
onCopy,
|
|
220
|
-
copiedMessageId,
|
|
221
191
|
onThumbsUp,
|
|
222
192
|
onThumbsDown,
|
|
223
193
|
onApprovePlan,
|
|
224
194
|
onRejectPlan,
|
|
225
|
-
|
|
195
|
+
onAnswerQuestions,
|
|
226
196
|
renderMarkdown,
|
|
227
197
|
}: {
|
|
228
198
|
message: ChatMessage;
|
|
@@ -234,26 +204,49 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
234
204
|
ttsActiveMessageId?: string | null;
|
|
235
205
|
ttsPlaybackState?: PlaybackState;
|
|
236
206
|
onCopy: (text: string, messageId: string) => void;
|
|
237
|
-
copiedMessageId: string | null;
|
|
238
207
|
onThumbsUp?: (messageId: string) => void;
|
|
239
208
|
onThumbsDown?: (messageId: string) => void;
|
|
240
209
|
onApprovePlan?: (planId: string) => void;
|
|
241
210
|
onRejectPlan?: (planId: string) => void;
|
|
242
|
-
|
|
211
|
+
onAnswerQuestions?: (message: string) => void;
|
|
243
212
|
renderMarkdown?: (content: string) => React.ReactNode;
|
|
244
213
|
}) {
|
|
214
|
+
const labels = useLabels();
|
|
245
215
|
const hasContent = messageText.length > 0;
|
|
246
216
|
const hasThinking = !!message.thinking;
|
|
247
217
|
|
|
218
|
+
// Both logs count units and are driven by `revealed`, from the calls that have really
|
|
219
|
+
// arrived — never by their own demo ticker. The task list waits for the turn to end.
|
|
220
|
+
const toolInvocations = message.toolInvocations;
|
|
221
|
+
const researchProgress = message.researchProgress;
|
|
222
|
+
const webLog = useMemo(
|
|
223
|
+
() => webSearchLog(toolInvocations, researchProgress, { t: labels.workLog }),
|
|
224
|
+
[toolInvocations, researchProgress, labels.workLog],
|
|
225
|
+
);
|
|
226
|
+
// The questions Alia asked, drawn as a card. Answerable only on the last message once the
|
|
227
|
+
// turn is over; any later message (typed or sent from the card) is the answer.
|
|
228
|
+
const questions = useMemo(
|
|
229
|
+
() => (toolInvocations ?? []).flatMap((inv) => askedQuestions(inv) ?? []),
|
|
230
|
+
[toolInvocations],
|
|
231
|
+
);
|
|
232
|
+
const taskLog = useMemo(() => {
|
|
233
|
+
const tasks = (toolInvocations ?? []).filter(
|
|
234
|
+
(inv) => !isWebInvocation(inv) && !isQuestionInvocation(inv),
|
|
235
|
+
);
|
|
236
|
+
return tasks.length === 0 || isStreamingThis
|
|
237
|
+
? null
|
|
238
|
+
: taskListLog(tasks, false, { t: labels.workLog });
|
|
239
|
+
}, [toolInvocations, isStreamingThis, labels.workLog]);
|
|
240
|
+
|
|
248
241
|
const isThisPlaying = ttsActiveMessageId === message.id && ttsPlaybackState === 'playing';
|
|
249
242
|
const isThisPaused = ttsActiveMessageId === message.id && ttsPlaybackState === 'paused';
|
|
250
243
|
const isThisLoading = ttsActiveMessageId === message.id && ttsPlaybackState === 'loading';
|
|
251
244
|
|
|
252
|
-
//
|
|
245
|
+
// AgentThinking — context-aware status
|
|
253
246
|
let thinkingIndicator: React.ReactNode = null;
|
|
254
247
|
if (isLoading && isLastMessage && !hasContent) {
|
|
255
248
|
const activeTool = message.toolInvocations?.find(
|
|
256
|
-
t => t.state === 'call' || t.state === 'partial-call',
|
|
249
|
+
(t) => t.state === 'call' || t.state === 'partial-call',
|
|
257
250
|
);
|
|
258
251
|
const rp = message.researchProgress;
|
|
259
252
|
let activeStatus: string | undefined;
|
|
@@ -261,15 +254,16 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
261
254
|
activeStatus = getToolActiveLabel(activeTool.toolName);
|
|
262
255
|
} else if (rp?.phase && rp.phase !== 'complete') {
|
|
263
256
|
activeStatus = getResearchActiveLabel(rp.phase);
|
|
264
|
-
} else if (message.thinking) {
|
|
265
|
-
activeStatus = 'Reasoning...';
|
|
266
257
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
258
|
+
// While it reasons, the reasoning block draws its own indicator: two at once is noise.
|
|
259
|
+
if (activeStatus !== undefined || !hasThinking) {
|
|
260
|
+
thinkingIndicator = (
|
|
261
|
+
<AgentThinking
|
|
262
|
+
variant={(message.toolInvocations?.length ?? 0) > 0 ? 'spin' : 'wave'}
|
|
263
|
+
label={activeStatus?.replace(/\.{3}$/, '')}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
273
267
|
}
|
|
274
268
|
|
|
275
269
|
return (
|
|
@@ -285,52 +279,27 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
285
279
|
/>
|
|
286
280
|
)}
|
|
287
281
|
|
|
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} />
|
|
282
|
+
{/* What the turn did: where it went (searches, pages, research) and everything else. */}
|
|
283
|
+
{taskLog === null ? null : (
|
|
284
|
+
<TaskList
|
|
285
|
+
tasks={taskLog.tasks}
|
|
286
|
+
revealed={taskLog.revealed}
|
|
287
|
+
collapseOnComplete="all"
|
|
288
|
+
working={false}
|
|
289
|
+
/>
|
|
290
|
+
)}
|
|
291
|
+
{webLog.steps.length === 0 ? null : (
|
|
292
|
+
<WebSearch
|
|
293
|
+
steps={webLog.steps}
|
|
294
|
+
revealed={webLog.revealed}
|
|
295
|
+
working={isStreamingThis ? undefined : false}
|
|
296
|
+
/>
|
|
325
297
|
)}
|
|
326
298
|
|
|
327
299
|
{/* Extended Thinking */}
|
|
328
300
|
{hasThinking && (
|
|
329
301
|
<View className="mb-3 w-full">
|
|
330
|
-
<
|
|
331
|
-
<ReasoningTrigger />
|
|
332
|
-
<ReasoningContent>{message.thinking!}</ReasoningContent>
|
|
333
|
-
</Reasoning>
|
|
302
|
+
<ThinkingBlock text={message.thinking!} streaming={isStreamingThis && !hasContent} />
|
|
334
303
|
</View>
|
|
335
304
|
)}
|
|
336
305
|
|
|
@@ -339,18 +308,16 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
339
308
|
<View className="flex-col items-start gap-0.5">
|
|
340
309
|
{/* Agent identity */}
|
|
341
310
|
{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' }}>
|
|
311
|
+
<View className="mb-0.5 flex-row items-center gap-2">
|
|
312
|
+
<Avatar source={message.agentInfo.avatar} size="xs" />
|
|
313
|
+
<Text className="text-xs font-semibold text-text-primary">
|
|
347
314
|
{message.agentInfo.name}
|
|
348
315
|
</Text>
|
|
349
316
|
</View>
|
|
350
317
|
)}
|
|
351
318
|
<View className="w-full">
|
|
352
319
|
{message.source === 'voice' ? (
|
|
353
|
-
<Text className="text-base text-
|
|
320
|
+
<Text className="text-base text-text-primary leading-7">
|
|
354
321
|
{messageText}
|
|
355
322
|
{message.isStreaming ? '\u258C' : ''}
|
|
356
323
|
</Text>
|
|
@@ -360,46 +327,41 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
360
327
|
<AliaMarkdown content={messageText} />
|
|
361
328
|
)}
|
|
362
329
|
</View>
|
|
363
|
-
{/* Action buttons */}
|
|
364
330
|
{!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>
|
|
331
|
+
<AiChatFeedbackRow
|
|
332
|
+
onLike={onThumbsUp ? () => onThumbsUp(message.id) : undefined}
|
|
333
|
+
onDislike={onThumbsDown ? () => onThumbsDown(message.id) : undefined}
|
|
334
|
+
onCopy={() => onCopy(messageText, message.id)}
|
|
335
|
+
actions={
|
|
336
|
+
onReadAloud
|
|
337
|
+
? [
|
|
338
|
+
{
|
|
339
|
+
key: 'read-aloud',
|
|
340
|
+
label:
|
|
341
|
+
isThisPlaying || isThisPaused ? labels.stopReading : labels.readAloud,
|
|
342
|
+
icon: isThisPlaying || isThisPaused ? RiStopFill : RiVolumeUpLine,
|
|
343
|
+
active: isThisPlaying || isThisPaused,
|
|
344
|
+
disabled: isThisLoading,
|
|
345
|
+
onPress: () => onReadAloud(message.id, messageText),
|
|
346
|
+
},
|
|
347
|
+
]
|
|
348
|
+
: []
|
|
349
|
+
}
|
|
350
|
+
/>
|
|
399
351
|
)}
|
|
400
352
|
</View>
|
|
401
353
|
)}
|
|
402
354
|
|
|
355
|
+
{questions.length === 0 ? null : (
|
|
356
|
+
<QuestionCard
|
|
357
|
+
questions={questions}
|
|
358
|
+
onSubmit={
|
|
359
|
+
isLastMessage && !isLoading && onAnswerQuestions !== undefined
|
|
360
|
+
? onAnswerQuestions
|
|
361
|
+
: undefined
|
|
362
|
+
}
|
|
363
|
+
/>
|
|
364
|
+
)}
|
|
403
365
|
{thinkingIndicator}
|
|
404
366
|
</View>
|
|
405
367
|
);
|
|
@@ -410,7 +372,6 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
410
372
|
export function AliaChatMessageList({
|
|
411
373
|
messages,
|
|
412
374
|
isStreaming,
|
|
413
|
-
scrollOffsetY,
|
|
414
375
|
onReadAloud,
|
|
415
376
|
ttsActiveMessageId,
|
|
416
377
|
ttsPlaybackState,
|
|
@@ -419,12 +380,10 @@ export function AliaChatMessageList({
|
|
|
419
380
|
onThumbsDown,
|
|
420
381
|
onApprovePlan,
|
|
421
382
|
onRejectPlan,
|
|
422
|
-
|
|
383
|
+
onAnswerQuestions,
|
|
423
384
|
welcomeComponent,
|
|
424
|
-
conversationLoading,
|
|
425
385
|
renderMarkdown,
|
|
426
386
|
}: AliaChatMessageListProps) {
|
|
427
|
-
const scrollRef = useRef<Animated.ScrollView>(null);
|
|
428
387
|
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
|
429
388
|
const [editedContent, setEditedContent] = useState('');
|
|
430
389
|
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
|
@@ -434,22 +393,6 @@ export function AliaChatMessageList({
|
|
|
434
393
|
prevMessageCountRef.current = messages.length;
|
|
435
394
|
}, [messages.length]);
|
|
436
395
|
|
|
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
396
|
const handleCopy = useCallback(async (content: string, messageId: string) => {
|
|
454
397
|
try {
|
|
455
398
|
if (Platform.OS === 'web') {
|
|
@@ -473,36 +416,34 @@ export function AliaChatMessageList({
|
|
|
473
416
|
const editedContentRef = useRef(editedContent);
|
|
474
417
|
editedContentRef.current = editedContent;
|
|
475
418
|
|
|
476
|
-
const handleSaveEdit = useCallback(
|
|
477
|
-
|
|
478
|
-
onEditMessage
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
419
|
+
const handleSaveEdit = useCallback(
|
|
420
|
+
(messageId: string) => {
|
|
421
|
+
if (onEditMessage && editedContentRef.current.trim()) {
|
|
422
|
+
onEditMessage(messageId, editedContentRef.current);
|
|
423
|
+
}
|
|
424
|
+
setEditingMessageId(null);
|
|
425
|
+
setEditedContent('');
|
|
426
|
+
},
|
|
427
|
+
[onEditMessage],
|
|
428
|
+
);
|
|
483
429
|
|
|
484
430
|
const handleCancelEdit = useCallback(() => {
|
|
485
431
|
setEditingMessageId(null);
|
|
486
432
|
setEditedContent('');
|
|
487
433
|
}, []);
|
|
488
434
|
|
|
489
|
-
const filteredMessages = useMemo(() => messages.filter(m => m.role !== 'system'), [messages]);
|
|
435
|
+
const filteredMessages = useMemo(() => messages.filter((m) => m.role !== 'system'), [messages]);
|
|
490
436
|
|
|
491
437
|
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}>
|
|
438
|
+
// Bloom's thread: follows the newest turn, and reports its scroll to a floating-chrome
|
|
439
|
+
// container so the header and composer fade in over it.
|
|
440
|
+
<AiChatThread>
|
|
441
|
+
<View
|
|
442
|
+
className="max-w-3xl mx-auto w-full"
|
|
443
|
+
style={!filteredMessages.length ? { flex: 1, justifyContent: 'center' } : undefined}
|
|
444
|
+
>
|
|
502
445
|
{/* Welcome or loading */}
|
|
503
|
-
{!filteredMessages.length && (
|
|
504
|
-
welcomeComponent || null
|
|
505
|
-
)}
|
|
446
|
+
{!filteredMessages.length && (welcomeComponent || null)}
|
|
506
447
|
|
|
507
448
|
{/* Messages */}
|
|
508
449
|
<View className="gap-2">
|
|
@@ -513,10 +454,7 @@ export function AliaChatMessageList({
|
|
|
513
454
|
const messageText = getTextFromContent(msg.content);
|
|
514
455
|
|
|
515
456
|
return (
|
|
516
|
-
<Animated.View
|
|
517
|
-
key={msg.id}
|
|
518
|
-
entering={isNewMessage ? chatMessageEnter : undefined}
|
|
519
|
-
>
|
|
457
|
+
<Animated.View key={msg.id} entering={isNewMessage ? chatMessageEnter : undefined}>
|
|
520
458
|
{msg.role === 'user' ? (
|
|
521
459
|
<UserBubble
|
|
522
460
|
message={msg}
|
|
@@ -542,12 +480,11 @@ export function AliaChatMessageList({
|
|
|
542
480
|
ttsActiveMessageId={ttsActiveMessageId}
|
|
543
481
|
ttsPlaybackState={ttsPlaybackState}
|
|
544
482
|
onCopy={handleCopy}
|
|
545
|
-
copiedMessageId={copiedMessageId}
|
|
546
483
|
onThumbsUp={onThumbsUp}
|
|
547
484
|
onThumbsDown={onThumbsDown}
|
|
548
485
|
onApprovePlan={onApprovePlan}
|
|
549
486
|
onRejectPlan={onRejectPlan}
|
|
550
|
-
|
|
487
|
+
onAnswerQuestions={onAnswerQuestions}
|
|
551
488
|
renderMarkdown={renderMarkdown}
|
|
552
489
|
/>
|
|
553
490
|
)}
|
|
@@ -556,6 +493,6 @@ export function AliaChatMessageList({
|
|
|
556
493
|
})}
|
|
557
494
|
</View>
|
|
558
495
|
</View>
|
|
559
|
-
</
|
|
496
|
+
</AiChatThread>
|
|
560
497
|
);
|
|
561
498
|
}
|