@alia.onl/sdk 11.1.0 → 13.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 +134 -0
- package/package.json +5 -4
- package/src/components/AliaAvatar.tsx +16 -20
- package/src/components/AliaChatContent.tsx +234 -96
- package/src/components/AliaChatHeader.tsx +102 -76
- package/src/components/AliaChatMessageList.tsx +174 -134
- package/src/components/AliaChatScreen.tsx +45 -23
- package/src/components/AliaChatSheet.tsx +151 -50
- package/src/components/AliaWelcomeMessage.tsx +26 -53
- package/src/components/AmbientField.tsx +19 -16
- package/src/components/ComposerSuggestions.tsx +45 -0
- package/src/components/IdentityMark.tsx +3 -1
- package/src/components/Markdown.tsx +18 -24
- package/src/components/QuestionCard.tsx +35 -33
- package/src/components/ThinkingBlock.tsx +9 -13
- package/src/components/VoiceModeIcon.tsx +15 -7
- package/src/components/voice/VoiceControls.tsx +17 -9
- package/src/hooks/useAliaChat.ts +124 -5
- package/src/hooks/useComposerSuggestions.ts +84 -0
- package/src/index.ts +10 -10
- package/src/lib/agent-character.ts +28 -0
- package/src/lib/chat-stream.ts +25 -3
- package/src/lib/citations.ts +62 -10
- package/src/lib/delegation.ts +34 -0
- package/src/lib/domain.ts +12 -0
- package/src/lib/flights.ts +24 -0
- package/src/lib/greeting.ts +15 -0
- package/src/lib/image-data-url.ts +26 -5
- package/src/lib/labels.tsx +10 -2
- package/src/lib/questions.ts +15 -1
- package/src/lib/suggestions.ts +53 -0
- package/src/lib/task-labels.ts +7 -39
- package/src/lib/use-after-delay.ts +19 -0
- package/src/lib/work-log.ts +3 -9
- package/src/tools.ts +8 -1
- package/src/types.ts +6 -5
|
@@ -1,113 +1,139 @@
|
|
|
1
1
|
import { ButtonGroup, ButtonGroupItem } from '@oxy.so/bloom/button-group';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DropdownMenu,
|
|
4
|
+
DropdownMenuContent,
|
|
5
|
+
DropdownMenuItem,
|
|
6
|
+
DropdownMenuTrigger,
|
|
7
|
+
} from '@oxy.so/bloom/dropdown-menu';
|
|
8
|
+
import { RiArrowLeftLine, RiDeleteBin6Line, RiMoreFill, RiShare2Line } from '@oxy.so/bloom/icons';
|
|
3
9
|
import { PageHeader } from '@oxy.so/bloom/page-header';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { Pressable, StyleSheet, View } from 'react-native';
|
|
10
|
+
import { AgentProfile } from '@oxy.so/bloom/agent-reply';
|
|
11
|
+
import { memo, type ReactNode, useMemo } from 'react';
|
|
12
|
+
import { readAgentCharacter } from '../lib/agent-character';
|
|
8
13
|
import { useLabels } from '../lib/labels';
|
|
9
|
-
import {
|
|
14
|
+
import { ALIA_ANCHORS, useAliaFlights } from '../lib/flights';
|
|
15
|
+
import { ALIA_AVATAR } from './AliaAvatar';
|
|
16
|
+
import type { AliaChatAgent } from './AliaChatContent';
|
|
10
17
|
|
|
11
18
|
export interface AliaChatHeaderProps {
|
|
12
|
-
/** The chat has messages to clear. */
|
|
19
|
+
/** The chat has messages: there is something to share and to clear. */
|
|
13
20
|
hasMessages: boolean;
|
|
21
|
+
/** Delegated agents that have answered: they join Alia in the profile, up to three in all. */
|
|
22
|
+
agents?: AliaChatAgent[];
|
|
23
|
+
/** Clears the conversation, from the menu. */
|
|
14
24
|
onClear: () => void;
|
|
15
|
-
/** Draws the
|
|
16
|
-
|
|
17
|
-
/**
|
|
25
|
+
/** Draws the back button at the start of the bar; omit where there is nowhere to go back to. */
|
|
26
|
+
onBack?: () => void;
|
|
27
|
+
/** Draws the share button, once there is a conversation; omit where the host cannot share. */
|
|
28
|
+
onShare?: () => void;
|
|
29
|
+
/** More rows for the menu (`DropdownMenuItem`s), before Clear. */
|
|
30
|
+
menu?: ReactNode;
|
|
31
|
+
/** The host's own start content instead of the back button, e.g. a navigation drawer's menu. */
|
|
18
32
|
leading?: ReactNode;
|
|
19
|
-
/** The host's own actions,
|
|
33
|
+
/** The host's own actions, before share and the menu. */
|
|
20
34
|
trailing?: ReactNode;
|
|
21
35
|
/** Pressing Alia's avatar or name. Omitted, the avatar waves. */
|
|
22
36
|
onProfilePress?: () => void;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
/**
|
|
26
|
-
* Alia's avatar above a name capsule,
|
|
27
|
-
*
|
|
28
|
-
* TODO(bloom): `ChatAgentProfile` draws exactly this (a 56px avatar over a 28px capsule
|
|
29
|
-
* of radius 14, `backgroundSecondary` with a hairline border) but is not exported, so
|
|
30
|
-
* this repeats its geometry. Use Bloom's component when it is.
|
|
40
|
+
* Alia's profile, Bloom's `AgentProfile`: her avatar above a name capsule, joined by the agents
|
|
41
|
+
* that answered ("Alia + Lumi + Atlas"). Pressing it makes every avatar wave.
|
|
31
42
|
*/
|
|
32
|
-
function AliaProfile({ onPress }: { onPress?: () => void }) {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
+
function AliaProfile({ agents, onPress }: { agents: AliaChatAgent[]; onPress?: () => void }) {
|
|
44
|
+
const flights = useAliaFlights();
|
|
45
|
+
const team = useMemo(
|
|
46
|
+
() => [
|
|
47
|
+
{ id: 'alia', name: 'Alia', avatar: ALIA_AVATAR },
|
|
48
|
+
...agents.map((agent) => ({ ...agent, avatar: readAgentCharacter(agent.character) })),
|
|
49
|
+
],
|
|
50
|
+
[agents],
|
|
51
|
+
);
|
|
43
52
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
className="items-center"
|
|
51
|
-
>
|
|
52
|
-
<View pointerEvents="none" style={{ marginBottom: -2, zIndex: 1 }}>
|
|
53
|
-
<AliaChatMark size={56} reactionKey={reactionKey} />
|
|
54
|
-
</View>
|
|
55
|
-
<View
|
|
56
|
-
pointerEvents="none"
|
|
57
|
-
className="items-center justify-center rounded-[14px] px-2.5 py-1"
|
|
58
|
-
style={{
|
|
59
|
-
backgroundColor: hovered ? colors.backgroundTertiary : colors.backgroundSecondary,
|
|
60
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
61
|
-
borderColor: colors.border,
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
64
|
-
<Text numberOfLines={1} className="text-[16px] font-medium leading-5 text-text-primary">
|
|
65
|
-
Alia
|
|
66
|
-
</Text>
|
|
67
|
-
</View>
|
|
68
|
-
</Pressable>
|
|
53
|
+
<AgentProfile
|
|
54
|
+
agents={team}
|
|
55
|
+
onPress={onPress ?? noop}
|
|
56
|
+
avatarRef={flights?.anchor(ALIA_ANCHORS.profile)}
|
|
57
|
+
avatarHidden={flights?.isLanding(ALIA_ANCHORS.profile)}
|
|
58
|
+
/>
|
|
69
59
|
);
|
|
70
60
|
}
|
|
71
61
|
|
|
62
|
+
const noop = () => undefined;
|
|
63
|
+
|
|
72
64
|
/**
|
|
73
65
|
* The chat's top bar: Bloom's `PageHeader`, set up as the shell sets it up for the
|
|
74
|
-
* agents' screens (inline, no scrim, no safe area)
|
|
66
|
+
* agents' screens (inline, no scrim, no safe area). Alia's profile is its title only once
|
|
67
|
+
* there is a conversation: the empty chat already shows the avatar beside its greeting,
|
|
68
|
+
* so the bar carries just its buttons until then.
|
|
75
69
|
*/
|
|
76
|
-
export function AliaChatHeader({
|
|
70
|
+
export const AliaChatHeader = memo(function AliaChatHeader({
|
|
77
71
|
hasMessages,
|
|
72
|
+
agents = [],
|
|
78
73
|
onClear,
|
|
79
|
-
|
|
74
|
+
onBack,
|
|
75
|
+
onShare,
|
|
76
|
+
menu,
|
|
80
77
|
leading,
|
|
81
78
|
trailing,
|
|
82
79
|
onProfilePress,
|
|
83
80
|
}: AliaChatHeaderProps) {
|
|
84
81
|
const labels = useLabels();
|
|
82
|
+
// Share and the menu act on a conversation. With none there is nothing for them to do, and a
|
|
83
|
+
// bar of dimmed buttons over the empty chat is chrome with no job: no bar at all, unless the
|
|
84
|
+
// host brought content of its own (a back button to somewhere, a drawer's menu).
|
|
85
|
+
if (!hasMessages && !onBack && leading === undefined && trailing === undefined) return null;
|
|
85
86
|
return (
|
|
86
87
|
<PageHeader
|
|
87
|
-
leading={
|
|
88
|
-
|
|
88
|
+
leading={
|
|
89
|
+
leading ??
|
|
90
|
+
(onBack ? (
|
|
91
|
+
<ButtonGroup accessibilityLabel={labels.chatActions}>
|
|
92
|
+
<ButtonGroupItem
|
|
93
|
+
iconOnly
|
|
94
|
+
leadingIcon={RiArrowLeftLine}
|
|
95
|
+
accessibilityLabel={labels.back}
|
|
96
|
+
onPress={onBack}
|
|
97
|
+
/>
|
|
98
|
+
</ButtonGroup>
|
|
99
|
+
) : undefined)
|
|
100
|
+
}
|
|
101
|
+
title={hasMessages ? <AliaProfile agents={agents} onPress={onProfilePress} /> : undefined}
|
|
89
102
|
titleAlign="center"
|
|
90
103
|
actions={
|
|
91
104
|
<>
|
|
92
105
|
{trailing}
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
{hasMessages ? (
|
|
107
|
+
<ButtonGroup accessibilityLabel={labels.chatActions}>
|
|
108
|
+
{onShare ? (
|
|
109
|
+
<ButtonGroupItem
|
|
110
|
+
iconOnly
|
|
111
|
+
leadingIcon={RiShare2Line}
|
|
112
|
+
accessibilityLabel={labels.share}
|
|
113
|
+
onPress={onShare}
|
|
114
|
+
/>
|
|
115
|
+
) : null}
|
|
116
|
+
<DropdownMenu>
|
|
117
|
+
<DropdownMenuTrigger asChild label={labels.more}>
|
|
118
|
+
<ButtonGroupItem
|
|
119
|
+
iconOnly
|
|
120
|
+
leadingIcon={RiMoreFill}
|
|
121
|
+
accessibilityLabel={labels.more}
|
|
122
|
+
/>
|
|
123
|
+
</DropdownMenuTrigger>
|
|
124
|
+
<DropdownMenuContent align="end">
|
|
125
|
+
{menu}
|
|
126
|
+
<DropdownMenuItem
|
|
127
|
+
tone="danger"
|
|
128
|
+
leading={<RiDeleteBin6Line width={16} height={16} />}
|
|
129
|
+
onPress={onClear}
|
|
130
|
+
>
|
|
131
|
+
{labels.clear}
|
|
132
|
+
</DropdownMenuItem>
|
|
133
|
+
</DropdownMenuContent>
|
|
134
|
+
</DropdownMenu>
|
|
135
|
+
</ButtonGroup>
|
|
136
|
+
) : null}
|
|
111
137
|
</>
|
|
112
138
|
}
|
|
113
139
|
scrim="none"
|
|
@@ -115,4 +141,4 @@ export function AliaChatHeader({
|
|
|
115
141
|
safeArea={false}
|
|
116
142
|
/>
|
|
117
143
|
);
|
|
118
|
-
}
|
|
144
|
+
});
|
|
@@ -12,15 +12,19 @@ import { Textarea } from '@oxy.so/bloom/textarea';
|
|
|
12
12
|
import { TaskList } from '@oxy.so/bloom/task-list';
|
|
13
13
|
import { Text } from '@oxy.so/bloom/typography';
|
|
14
14
|
import { WebSearch } from '@oxy.so/bloom/web-search';
|
|
15
|
-
import { AliaMarkdown } from './Markdown';
|
|
16
|
-
import {
|
|
15
|
+
import { AliaMarkdown, MARKDOWN_BODY } from './Markdown';
|
|
16
|
+
import { AiChatMessageLine, AiChatThread, AiChatUserMessage } from '@oxy.so/bloom/ai-chat';
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
AgentReply,
|
|
19
|
+
AgentReplyAction,
|
|
20
|
+
AgentReplyActions,
|
|
21
|
+
AgentReplyBubble,
|
|
22
|
+
AgentReplyIdentity,
|
|
23
|
+
} from '@oxy.so/bloom/agent-reply';
|
|
24
|
+
import { readAgentCharacter } from '../lib/agent-character';
|
|
25
|
+
import { useAfterDelay } from '../lib/use-after-delay';
|
|
26
|
+
import { Collapsible } from '@oxy.so/bloom/collapsible';
|
|
27
|
+
import { ALIA_AVATAR } from './AliaAvatar';
|
|
24
28
|
import { Button } from '@oxy.so/bloom/button';
|
|
25
29
|
import { useLabels } from '../lib/labels';
|
|
26
30
|
import { isWebInvocation, taskListLog, webSearchLog } from '../lib/work-log';
|
|
@@ -33,8 +37,9 @@ import {
|
|
|
33
37
|
} from '@oxy.so/bloom/icons';
|
|
34
38
|
import { ThinkingBlock } from './ThinkingBlock';
|
|
35
39
|
import { PlanPreviewCard } from './PlanPreviewCard';
|
|
36
|
-
import {
|
|
40
|
+
import { AskedQuestions } from './QuestionCard';
|
|
37
41
|
import { askedQuestions, isQuestionInvocation } from '../lib/questions';
|
|
42
|
+
import { extractCitationSources, linkifyCitations } from '../lib/citations';
|
|
38
43
|
import { chatMessageEnter } from '../animations';
|
|
39
44
|
import { getToolLabel, getToolActiveLabel, getResearchActiveLabel } from '../lib/tool-registry';
|
|
40
45
|
import { getTextFromContent, getImagesFromContent } from '../lib/content-utils';
|
|
@@ -69,8 +74,6 @@ export interface AliaChatMessageListProps {
|
|
|
69
74
|
onThumbsDown?: (messageId: string) => void;
|
|
70
75
|
onApprovePlan?: (planId: string) => void;
|
|
71
76
|
onRejectPlan?: (planId: string) => void;
|
|
72
|
-
/** Sends the answers to Alia's questions as the person's next message. */
|
|
73
|
-
onAnswerQuestions?: (message: string) => void;
|
|
74
77
|
// Welcome
|
|
75
78
|
welcomeComponent?: React.ReactNode;
|
|
76
79
|
// Markdown renderer override (app passes CustomMarkdown, SDK uses AliaMarkdown)
|
|
@@ -154,11 +157,7 @@ const UserBubble = React.memo(function UserBubble({
|
|
|
154
157
|
<AiChatMessageLine block>
|
|
155
158
|
<View className="flex-row flex-wrap gap-2">
|
|
156
159
|
{images.map((imgUrl) => (
|
|
157
|
-
<View
|
|
158
|
-
key={imgUrl}
|
|
159
|
-
className="rounded-xl overflow-hidden"
|
|
160
|
-
style={{ width: 120, height: 120 }}
|
|
161
|
-
>
|
|
160
|
+
<View key={imgUrl} className="size-[120px] overflow-hidden rounded-xl">
|
|
162
161
|
<ExpoImage
|
|
163
162
|
source={{ uri: imgUrl }}
|
|
164
163
|
className="w-full h-full"
|
|
@@ -176,14 +175,69 @@ const UserBubble = React.memo(function UserBubble({
|
|
|
176
175
|
);
|
|
177
176
|
});
|
|
178
177
|
|
|
178
|
+
/** Alia is shown thinking only once she has for this long: a hand-over to an agent shows no flash of her. */
|
|
179
|
+
const ALIA_THINKING_DELAY_MS = 450;
|
|
180
|
+
|
|
179
181
|
// ── Assistant Message ──
|
|
180
182
|
|
|
183
|
+
/**
|
|
184
|
+
* One reply, Alia's or a delegated agent's: the reply, and under it who answered. While the one
|
|
185
|
+
* who answers is working the identity shows its avatar large and thinking, and when the reply
|
|
186
|
+
* arrives it is the same avatar that settles under it, as in Bloom's multi-agent chat. Alia's own
|
|
187
|
+
* text runs the width of the thread; an agent answers in a bubble.
|
|
188
|
+
*/
|
|
189
|
+
function ReplyBody({
|
|
190
|
+
agent,
|
|
191
|
+
thinking,
|
|
192
|
+
reasoning,
|
|
193
|
+
actions,
|
|
194
|
+
children,
|
|
195
|
+
}: {
|
|
196
|
+
agent: ChatMessage['agentInfo'];
|
|
197
|
+
thinking: boolean;
|
|
198
|
+
/** What the model has reasoned so far: the thinking avatar folds it under its row. */
|
|
199
|
+
reasoning: React.ReactNode;
|
|
200
|
+
actions: React.ReactNode;
|
|
201
|
+
children: React.ReactNode;
|
|
202
|
+
}) {
|
|
203
|
+
const identity = (
|
|
204
|
+
<AgentReplyIdentity
|
|
205
|
+
character={agent ? readAgentCharacter(agent.character) : ALIA_AVATAR}
|
|
206
|
+
name={agent?.name ?? 'Alia'}
|
|
207
|
+
thinking={thinking}
|
|
208
|
+
reasoning={reasoning}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
if (agent === undefined)
|
|
212
|
+
return (
|
|
213
|
+
<AgentReply
|
|
214
|
+
identity={identity}
|
|
215
|
+
actions={actions}
|
|
216
|
+
// The thread pads its content and spaces its messages: the text starts at its edge, full width.
|
|
217
|
+
style={{ marginHorizontal: 0, marginTop: 0, maxWidth: '100%', width: '100%' }}
|
|
218
|
+
>
|
|
219
|
+
{children}
|
|
220
|
+
</AgentReply>
|
|
221
|
+
);
|
|
222
|
+
return (
|
|
223
|
+
<AgentReply
|
|
224
|
+
identity={identity}
|
|
225
|
+
actions={actions}
|
|
226
|
+
// The reply starts where Alia's own text does and sits one thread gap from the message above.
|
|
227
|
+
style={{ marginHorizontal: 0, marginTop: 0, maxWidth: '100%' }}
|
|
228
|
+
>
|
|
229
|
+
{children ? <AgentReplyBubble text={MARKDOWN_BODY}>{children}</AgentReplyBubble> : null}
|
|
230
|
+
</AgentReply>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
181
234
|
const AssistantMessage = React.memo(function AssistantMessage({
|
|
182
235
|
message,
|
|
183
236
|
messageText,
|
|
184
237
|
isStreamingThis,
|
|
185
238
|
isLastMessage,
|
|
186
239
|
isLoading,
|
|
240
|
+
agentsWorking,
|
|
187
241
|
onReadAloud,
|
|
188
242
|
ttsActiveMessageId,
|
|
189
243
|
ttsPlaybackState,
|
|
@@ -192,7 +246,6 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
192
246
|
onThumbsDown,
|
|
193
247
|
onApprovePlan,
|
|
194
248
|
onRejectPlan,
|
|
195
|
-
onAnswerQuestions,
|
|
196
249
|
renderMarkdown,
|
|
197
250
|
}: {
|
|
198
251
|
message: ChatMessage;
|
|
@@ -200,6 +253,8 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
200
253
|
isStreamingThis: boolean;
|
|
201
254
|
isLastMessage: boolean;
|
|
202
255
|
isLoading: boolean;
|
|
256
|
+
/** An agent Alia sent for has not answered yet: it is the one thinking, not Alia. */
|
|
257
|
+
agentsWorking: boolean;
|
|
203
258
|
onReadAloud?: (messageId: string, text: string) => void;
|
|
204
259
|
ttsActiveMessageId?: string | null;
|
|
205
260
|
ttsPlaybackState?: PlaybackState;
|
|
@@ -208,11 +263,19 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
208
263
|
onThumbsDown?: (messageId: string) => void;
|
|
209
264
|
onApprovePlan?: (planId: string) => void;
|
|
210
265
|
onRejectPlan?: (planId: string) => void;
|
|
211
|
-
onAnswerQuestions?: (message: string) => void;
|
|
212
266
|
renderMarkdown?: (content: string) => React.ReactNode;
|
|
213
267
|
}) {
|
|
214
268
|
const labels = useLabels();
|
|
215
269
|
const hasContent = messageText.length > 0;
|
|
270
|
+
// `[n]` markers that name a source become links to it.
|
|
271
|
+
const citedText = useMemo(
|
|
272
|
+
() =>
|
|
273
|
+
linkifyCitations(
|
|
274
|
+
messageText,
|
|
275
|
+
extractCitationSources(message.toolInvocations, message.researchProgress?.sources),
|
|
276
|
+
),
|
|
277
|
+
[messageText, message.toolInvocations, message.researchProgress?.sources],
|
|
278
|
+
);
|
|
216
279
|
const hasThinking = !!message.thinking;
|
|
217
280
|
|
|
218
281
|
// Both logs count units and are driven by `revealed`, from the calls that have really
|
|
@@ -223,8 +286,8 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
223
286
|
() => webSearchLog(toolInvocations, researchProgress, { t: labels.workLog }),
|
|
224
287
|
[toolInvocations, researchProgress, labels.workLog],
|
|
225
288
|
);
|
|
226
|
-
// The questions Alia asked,
|
|
227
|
-
//
|
|
289
|
+
// The questions Alia asked, kept in the thread as a record. They are answered above the
|
|
290
|
+
// composer (`AliaChatContent`), where the person is about to type.
|
|
228
291
|
const questions = useMemo(
|
|
229
292
|
() => (toolInvocations ?? []).flatMap((inv) => askedQuestions(inv) ?? []),
|
|
230
293
|
[toolInvocations],
|
|
@@ -241,30 +304,37 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
241
304
|
const isThisPlaying = ttsActiveMessageId === message.id && ttsPlaybackState === 'playing';
|
|
242
305
|
const isThisPaused = ttsActiveMessageId === message.id && ttsPlaybackState === 'paused';
|
|
243
306
|
const isThisLoading = ttsActiveMessageId === message.id && ttsPlaybackState === 'loading';
|
|
307
|
+
const replyActions =
|
|
308
|
+
!isStreamingThis && hasContent ? (
|
|
309
|
+
<AgentReplyActions
|
|
310
|
+
onVoteChange={(vote) =>
|
|
311
|
+
vote === 'up' ? onThumbsUp?.(message.id) : onThumbsDown?.(message.id)
|
|
312
|
+
}
|
|
313
|
+
onCopy={() => onCopy(messageText, message.id)}
|
|
314
|
+
>
|
|
315
|
+
{onReadAloud ? (
|
|
316
|
+
<AgentReplyAction
|
|
317
|
+
icon={isThisPlaying || isThisPaused ? RiStopFill : RiVolumeUpLine}
|
|
318
|
+
label={isThisPlaying || isThisPaused ? labels.stopReading : labels.readAloud}
|
|
319
|
+
selected={isThisPlaying || isThisPaused}
|
|
320
|
+
disabled={isThisLoading}
|
|
321
|
+
onPress={() => onReadAloud(message.id, messageText)}
|
|
322
|
+
/>
|
|
323
|
+
) : null}
|
|
324
|
+
</AgentReplyActions>
|
|
325
|
+
) : undefined;
|
|
244
326
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
activeStatus = getResearchActiveLabel(rp.phase);
|
|
257
|
-
}
|
|
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
|
-
}
|
|
267
|
-
}
|
|
327
|
+
// The one who answers is thinking until its reply starts: an agent from the call that sent for it,
|
|
328
|
+
// Alia when no agent is working for her. The avatar is the indicator.
|
|
329
|
+
// Alia thinks once she has for a moment: when she hands the turn over at once, only the agent is
|
|
330
|
+
// shown thinking, not Alia for an instant first.
|
|
331
|
+
const aliaThinking = useAfterDelay(
|
|
332
|
+
isLoading && !hasContent && !message.agentInfo && isLastMessage && !agentsWorking,
|
|
333
|
+
ALIA_THINKING_DELAY_MS,
|
|
334
|
+
);
|
|
335
|
+
const thinking = message.agentInfo ? isLoading && !hasContent : aliaThinking;
|
|
336
|
+
// The reply, its avatar and its reasoning come and go with their height eased, never popped.
|
|
337
|
+
const showsReply = hasContent || message.isStreaming || thinking;
|
|
268
338
|
|
|
269
339
|
return (
|
|
270
340
|
<View className="w-full">
|
|
@@ -304,65 +374,30 @@ const AssistantMessage = React.memo(function AssistantMessage({
|
|
|
304
374
|
)}
|
|
305
375
|
|
|
306
376
|
{/* Message Content */}
|
|
307
|
-
{
|
|
308
|
-
<
|
|
309
|
-
{
|
|
310
|
-
{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
renderMarkdown(messageText)
|
|
326
|
-
) : (
|
|
327
|
-
<AliaMarkdown content={messageText} />
|
|
328
|
-
)}
|
|
329
|
-
</View>
|
|
330
|
-
{!isStreamingThis && hasContent && (
|
|
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
|
-
/>
|
|
377
|
+
<Collapsible open={showsReply}>
|
|
378
|
+
<ReplyBody
|
|
379
|
+
agent={message.agentInfo}
|
|
380
|
+
thinking={thinking}
|
|
381
|
+
reasoning={
|
|
382
|
+
thinking && hasThinking ? <AliaMarkdown content={message.thinking!} /> : undefined
|
|
383
|
+
}
|
|
384
|
+
actions={replyActions}
|
|
385
|
+
>
|
|
386
|
+
{!hasContent && !message.isStreaming ? null : message.source === 'voice' ? (
|
|
387
|
+
<Text className="text-base text-text-primary leading-7">
|
|
388
|
+
{messageText}
|
|
389
|
+
{message.isStreaming ? '\u258C' : ''}
|
|
390
|
+
</Text>
|
|
391
|
+
) : renderMarkdown ? (
|
|
392
|
+
renderMarkdown(citedText)
|
|
393
|
+
) : (
|
|
394
|
+
<AliaMarkdown content={citedText} />
|
|
351
395
|
)}
|
|
352
|
-
</
|
|
353
|
-
|
|
396
|
+
</ReplyBody>
|
|
397
|
+
</Collapsible>
|
|
354
398
|
|
|
355
|
-
{
|
|
356
|
-
|
|
357
|
-
questions={questions}
|
|
358
|
-
onSubmit={
|
|
359
|
-
isLastMessage && !isLoading && onAnswerQuestions !== undefined
|
|
360
|
-
? onAnswerQuestions
|
|
361
|
-
: undefined
|
|
362
|
-
}
|
|
363
|
-
/>
|
|
364
|
-
)}
|
|
365
|
-
{thinkingIndicator}
|
|
399
|
+
{/* What Alia asked stays in the thread as a record; answering happens above the composer. */}
|
|
400
|
+
{questions.length === 0 ? null : <AskedQuestions questions={questions} />}
|
|
366
401
|
</View>
|
|
367
402
|
);
|
|
368
403
|
});
|
|
@@ -380,7 +415,6 @@ export function AliaChatMessageList({
|
|
|
380
415
|
onThumbsDown,
|
|
381
416
|
onApprovePlan,
|
|
382
417
|
onRejectPlan,
|
|
383
|
-
onAnswerQuestions,
|
|
384
418
|
welcomeComponent,
|
|
385
419
|
renderMarkdown,
|
|
386
420
|
}: AliaChatMessageListProps) {
|
|
@@ -433,6 +467,9 @@ export function AliaChatMessageList({
|
|
|
433
467
|
}, []);
|
|
434
468
|
|
|
435
469
|
const filteredMessages = useMemo(() => messages.filter((m) => m.role !== 'system'), [messages]);
|
|
470
|
+
// An agent Alia sent for that has not answered: it is the one that is thinking.
|
|
471
|
+
const agentsWorking =
|
|
472
|
+
isStreaming && filteredMessages.some((m) => m.agentInfo && !getTextFromContent(m.content));
|
|
436
473
|
|
|
437
474
|
return (
|
|
438
475
|
// Bloom's thread: follows the newest turn, and reports its scroll to a floating-chrome
|
|
@@ -455,39 +492,42 @@ export function AliaChatMessageList({
|
|
|
455
492
|
|
|
456
493
|
return (
|
|
457
494
|
<Animated.View key={msg.id} entering={isNewMessage ? chatMessageEnter : undefined}>
|
|
458
|
-
{
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
495
|
+
{/* A new row opens up as it arrives, so what is below it moves with it. */}
|
|
496
|
+
<Collapsible open appear={isNewMessage}>
|
|
497
|
+
{msg.role === 'user' ? (
|
|
498
|
+
<UserBubble
|
|
499
|
+
message={msg}
|
|
500
|
+
text={messageText}
|
|
501
|
+
isEditing={editingMessageId === msg.id}
|
|
502
|
+
editedContent={editingMessageId === msg.id ? editedContent : ''}
|
|
503
|
+
onEditedContentChange={setEditedContent}
|
|
504
|
+
onSaveEdit={handleSaveEdit}
|
|
505
|
+
onCancelEdit={handleCancelEdit}
|
|
506
|
+
onStartEdit={handleStartEdit}
|
|
507
|
+
onCopy={handleCopy}
|
|
508
|
+
copiedMessageId={copiedMessageId}
|
|
509
|
+
showEditButton={!!onEditMessage}
|
|
510
|
+
/>
|
|
511
|
+
) : (
|
|
512
|
+
<AssistantMessage
|
|
513
|
+
message={msg}
|
|
514
|
+
messageText={messageText}
|
|
515
|
+
isStreamingThis={isStreamingThis}
|
|
516
|
+
isLastMessage={isLast}
|
|
517
|
+
isLoading={isStreaming}
|
|
518
|
+
agentsWorking={agentsWorking}
|
|
519
|
+
onReadAloud={onReadAloud}
|
|
520
|
+
ttsActiveMessageId={ttsActiveMessageId}
|
|
521
|
+
ttsPlaybackState={ttsPlaybackState}
|
|
522
|
+
onCopy={handleCopy}
|
|
523
|
+
onThumbsUp={onThumbsUp}
|
|
524
|
+
onThumbsDown={onThumbsDown}
|
|
525
|
+
onApprovePlan={onApprovePlan}
|
|
526
|
+
onRejectPlan={onRejectPlan}
|
|
527
|
+
renderMarkdown={renderMarkdown}
|
|
528
|
+
/>
|
|
529
|
+
)}
|
|
530
|
+
</Collapsible>
|
|
491
531
|
</Animated.View>
|
|
492
532
|
);
|
|
493
533
|
})}
|