@alia.onl/sdk 15.8.0 → 19.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 +242 -13
- package/README.md +223 -151
- package/package.json +15 -5
- package/src/animations.ts +0 -1
- package/src/components/AliaAskProvider.tsx +127 -0
- package/src/components/AliaChatContent.tsx +604 -392
- package/src/components/AliaChatHeader.tsx +14 -38
- package/src/components/AliaChatScreen.tsx +56 -31
- package/src/components/AliaChatSheet.tsx +110 -36
- package/src/components/AliaComposer.tsx +1 -1
- package/src/components/AliaParticipants.tsx +202 -0
- package/src/components/AliaParticipantsHeader.tsx +60 -0
- package/src/components/AliaSharedConversation.tsx +86 -0
- package/src/components/AliaWelcomeMessage.tsx +18 -5
- package/src/components/Image.tsx +12 -0
- package/src/components/Markdown.tsx +498 -14
- package/src/components/PlanPreviewCard.tsx +7 -49
- package/src/components/ReplyMarkdown.tsx +359 -0
- package/src/components/RichBlocks.tsx +195 -0
- package/src/components/canvas/CanvasComponent.tsx +59 -0
- package/src/components/canvas/ChartRenderer.tsx +73 -0
- package/src/components/canvas/CodeRenderer.tsx +24 -0
- package/src/components/canvas/FormRenderer.tsx +101 -0
- package/src/components/canvas/MarkdownRenderer.tsx +14 -0
- package/src/components/canvas/TableRenderer.tsx +47 -0
- package/src/components/thought/FilesAndSources.tsx +120 -0
- package/src/components/thought/ThoughtPanel.tsx +417 -0
- package/src/components/thought/ToolStep.tsx +141 -0
- package/src/components/thread/AgentResultCard.tsx +125 -0
- package/src/components/thread/AgentTaskCard.tsx +127 -0
- package/src/components/thread/AliaThread.tsx +401 -0
- package/src/components/thread/MessageBlockBoundary.tsx +78 -0
- package/src/components/thread/NewConversationOffer.tsx +54 -0
- package/src/components/thread/ThreadRow.tsx +458 -0
- package/src/components/thread/ThreadSearch.tsx +129 -0
- package/src/components/thread/ToolResultCard.tsx +254 -0
- package/src/components/thread/TurnMenu.tsx +54 -0
- package/src/components/thread/TurnStatusLine.tsx +34 -0
- package/src/components/voice/AliaVoiceCall.tsx +21 -0
- package/src/components/voice/VoiceControls.tsx +2 -28
- package/src/components/voice/VoiceSession.tsx +39 -11
- package/src/hooks/useAliaActivity.ts +2 -2
- package/src/hooks/useAtBottom.ts +51 -0
- package/src/hooks/useAttachMenu.ts +94 -0
- package/src/hooks/useChatComposer.ts +299 -0
- package/src/hooks/useChatVoice.ts +129 -0
- package/src/hooks/useDocumentPicker.ts +3 -29
- package/src/hooks/useFailedTurnToast.ts +43 -0
- package/src/hooks/useMessageVote.ts +50 -0
- package/src/hooks/useOpenThought.ts +79 -0
- package/src/hooks/usePendingQuestions.ts +2 -2
- package/src/hooks/useReadAloud.ts +92 -0
- package/src/hooks/useShareHandler.ts +24 -0
- package/src/hooks/useSoundEffects.ts +1 -1
- package/src/hooks/useThoughtScope.ts +59 -0
- package/src/hooks/useThreadRows.ts +145 -0
- package/src/hooks/useThreadView.ts +68 -0
- package/src/hooks/useTimelineWindow.ts +256 -0
- package/src/hooks/useVoiceLauncher.ts +180 -0
- package/src/hooks/useVoiceRoom.ts +0 -3
- package/src/index.ts +43 -29
- package/src/lib/activity-store.ts +1 -1
- package/src/lib/add-menu.ts +7 -0
- package/src/lib/call-thread.ts +33 -0
- package/src/lib/citations.ts +79 -2
- package/src/lib/clipboard.ts +23 -0
- package/src/lib/content-utils.ts +10 -0
- package/src/lib/copy-en.ts +284 -0
- package/src/lib/copy.ts +32 -0
- package/src/lib/document-picker.ts +31 -0
- package/src/lib/flights.ts +22 -13
- package/src/lib/focus-return.ts +66 -0
- package/src/lib/keyboard.native.tsx +19 -0
- package/src/lib/keyboard.tsx +9 -0
- package/src/lib/labels.tsx +69 -17
- package/src/lib/participants.ts +86 -0
- package/src/lib/power-level-options.ts +66 -0
- package/src/lib/questions.ts +2 -2
- package/src/lib/task-labels.ts +4 -4
- package/src/lib/tool-call-status.ts +1 -1
- package/src/lib/tool-icons.ts +36 -0
- package/src/lib/tool-renderers.tsx +48 -0
- package/src/lib/turn-state.ts +2 -2
- package/src/lib/voice-call-request.ts +49 -0
- package/src/lib/voice-call.ts +76 -34
- package/src/lib/voice-controls-labels.ts +31 -0
- package/src/lib/voice-protocol.ts +32 -14
- package/src/lib/work-log.ts +6 -6
- package/src/runtime/agents.ts +151 -0
- package/src/runtime/api.ts +123 -0
- package/src/runtime/attachment-utils.ts +96 -0
- package/src/runtime/composer-draft-store.ts +270 -0
- package/src/runtime/config.tsx +186 -0
- package/src/runtime/conversation-store.ts +49 -0
- package/src/runtime/conversations.ts +557 -0
- package/src/runtime/error-utils.ts +104 -0
- package/src/runtime/host-tools.ts +140 -0
- package/src/runtime/index.ts +113 -0
- package/src/runtime/message-history.ts +124 -0
- package/src/runtime/model.ts +39 -0
- package/src/runtime/oxy-files.ts +37 -0
- package/src/runtime/power-levels.ts +66 -0
- package/src/runtime/query-keys.ts +34 -0
- package/src/runtime/random-uuid.ts +25 -0
- package/src/runtime/routes.ts +51 -0
- package/src/runtime/sharing.ts +165 -0
- package/src/runtime/sse-frame-reader.ts +88 -0
- package/src/runtime/stream-events.ts +146 -0
- package/src/runtime/stream-outcome.ts +21 -0
- package/src/runtime/thought-store.ts +103 -0
- package/src/runtime/thread/agent-activity.ts +261 -0
- package/src/runtime/thread/automation-labels.ts +79 -0
- package/src/runtime/thread/conversation-export.ts +135 -0
- package/src/runtime/thread/message-days.ts +120 -0
- package/src/runtime/thread/message-processor.ts +93 -0
- package/src/runtime/thread/task-utils.ts +11 -0
- package/src/runtime/thread/thought-utils.ts +752 -0
- package/src/runtime/thread/thread-history.ts +139 -0
- package/src/runtime/thread/timeline.ts +202 -0
- package/src/runtime/thread/title-tags.ts +8 -0
- package/src/runtime/thread/tool-cards.ts +383 -0
- package/src/runtime/turn-failure.ts +108 -0
- package/src/runtime/types.ts +148 -0
- package/src/runtime/usage-limit-error.ts +44 -0
- package/src/runtime/use-agent-activity.ts +80 -0
- package/src/runtime/use-conversation-stream.ts +1427 -0
- package/src/runtime/use-conversation-thread.ts +556 -0
- package/src/runtime/use-start-conversation.ts +107 -0
- package/src/runtime/use-thread-history.ts +64 -0
- package/src/runtime/use-thread-search.ts +95 -0
- package/src/runtime/use-turn-edit.ts +99 -0
- package/src/tools.ts +2 -2
- package/src/types.ts +18 -47
- package/src/components/AliaChatMessageList.tsx +0 -532
- package/src/components/ThinkingBlock.tsx +0 -43
- package/src/hooks/useAliaChat.ts +0 -573
- package/src/lib/catalogue.ts +0 -236
- package/src/lib/chat-stream.ts +0 -655
- package/src/lib/chat-transport.ts +0 -98
- package/src/lib/delegation.ts +0 -34
- package/src/lib/work-log-copy.ts +0 -61
|
@@ -1,532 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
-
import { View, Platform } from 'react-native';
|
|
3
|
-
import Animated from 'react-native-reanimated';
|
|
4
|
-
import { Textarea } from '@oxy.so/bloom/textarea';
|
|
5
|
-
import { TaskList } from '@oxy.so/bloom/task-list';
|
|
6
|
-
import { Text } from '@oxy.so/bloom/typography';
|
|
7
|
-
import { WebSearch } from '@oxy.so/bloom/web-search';
|
|
8
|
-
import { AliaMarkdown, MARKDOWN_BODY } from './Markdown';
|
|
9
|
-
import { AiChatMessageLine, AiChatThread, AiChatUserMessage } from '@oxy.so/bloom/ai-chat';
|
|
10
|
-
import {
|
|
11
|
-
AgentReply,
|
|
12
|
-
AgentReplyAction,
|
|
13
|
-
AgentReplyActions,
|
|
14
|
-
AgentReplyBubble,
|
|
15
|
-
AgentReplyIdentity,
|
|
16
|
-
} from '@oxy.so/bloom/agent-reply';
|
|
17
|
-
import { readAgentCharacter } from '../lib/agent-character';
|
|
18
|
-
import { useAfterDelay } from '../lib/use-after-delay';
|
|
19
|
-
import { Collapsible } from '@oxy.so/bloom/collapsible';
|
|
20
|
-
import { ALIA_AVATAR } from './AliaAvatar';
|
|
21
|
-
import { Button } from '@oxy.so/bloom/button';
|
|
22
|
-
import { useLabels } from '../lib/labels';
|
|
23
|
-
import { isWebInvocation, taskListLog, webSearchLog } from '../lib/work-log';
|
|
24
|
-
import {
|
|
25
|
-
RiCheckLine,
|
|
26
|
-
RiFileCopyLine,
|
|
27
|
-
RiPencilLine,
|
|
28
|
-
RiStopFill,
|
|
29
|
-
RiVolumeUpLine,
|
|
30
|
-
} from '@oxy.so/bloom/icons';
|
|
31
|
-
import { ThinkingBlock } from './ThinkingBlock';
|
|
32
|
-
import { PlanPreviewCard } from './PlanPreviewCard';
|
|
33
|
-
import { AskedQuestions } from './QuestionCard';
|
|
34
|
-
import { askedQuestions, isQuestionInvocation } from '../lib/questions';
|
|
35
|
-
import { extractCitationSources, linkifyCitations } from '../lib/citations';
|
|
36
|
-
import { chatMessageEnter } from '../animations';
|
|
37
|
-
import { getTextFromContent, getImagesFromContent } from '../lib/content-utils';
|
|
38
|
-
import type { ChatMessage } from '../types';
|
|
39
|
-
|
|
40
|
-
// Resolve expo-image once at module load (optional peer dep)
|
|
41
|
-
type ExpoImageProps = {
|
|
42
|
-
source: { uri: string };
|
|
43
|
-
className?: string;
|
|
44
|
-
contentFit?: string;
|
|
45
|
-
};
|
|
46
|
-
// expo-image is an optional peer dependency: SDK-only consumers that never render
|
|
47
|
-
// inline images may not install it. Degrade to skipping images when it can't be required.
|
|
48
|
-
let ExpoImage: React.ComponentType<ExpoImageProps> | null = null;
|
|
49
|
-
try {
|
|
50
|
-
ExpoImage = require('expo-image').Image;
|
|
51
|
-
} catch {
|
|
52
|
-
// Optional peer dependency absent — inline images are simply not rendered.
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type PlaybackState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
|
|
56
|
-
|
|
57
|
-
export interface AliaChatMessageListProps {
|
|
58
|
-
messages: ChatMessage[];
|
|
59
|
-
isStreaming: boolean;
|
|
60
|
-
onReadAloud?: (messageId: string, text: string) => void;
|
|
61
|
-
ttsActiveMessageId?: string | null;
|
|
62
|
-
ttsPlaybackState?: PlaybackState;
|
|
63
|
-
// Injectable callbacks
|
|
64
|
-
onEditMessage?: (messageId: string, newContent: string) => void;
|
|
65
|
-
onThumbsUp?: (messageId: string) => void;
|
|
66
|
-
onThumbsDown?: (messageId: string) => void;
|
|
67
|
-
onApprovePlan?: (planId: string) => void;
|
|
68
|
-
onRejectPlan?: (planId: string) => void;
|
|
69
|
-
// Welcome
|
|
70
|
-
welcomeComponent?: React.ReactNode;
|
|
71
|
-
// Markdown renderer override (app passes CustomMarkdown, SDK uses AliaMarkdown)
|
|
72
|
-
renderMarkdown?: (content: string) => React.ReactNode;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ── User Bubble ──
|
|
76
|
-
|
|
77
|
-
const UserBubble = React.memo(function UserBubble({
|
|
78
|
-
message,
|
|
79
|
-
text,
|
|
80
|
-
isEditing,
|
|
81
|
-
editedContent,
|
|
82
|
-
onEditedContentChange,
|
|
83
|
-
onSaveEdit,
|
|
84
|
-
onCancelEdit,
|
|
85
|
-
onStartEdit,
|
|
86
|
-
onCopy,
|
|
87
|
-
copiedMessageId,
|
|
88
|
-
showEditButton,
|
|
89
|
-
}: {
|
|
90
|
-
message: ChatMessage;
|
|
91
|
-
text: string;
|
|
92
|
-
isEditing: boolean;
|
|
93
|
-
editedContent: string;
|
|
94
|
-
onEditedContentChange: (text: string) => void;
|
|
95
|
-
onSaveEdit: (messageId: string) => void;
|
|
96
|
-
onCancelEdit: () => void;
|
|
97
|
-
onStartEdit: (messageId: string, content: string) => void;
|
|
98
|
-
onCopy: (text: string, messageId: string) => void;
|
|
99
|
-
copiedMessageId: string | null;
|
|
100
|
-
showEditButton: boolean;
|
|
101
|
-
}) {
|
|
102
|
-
const labels = useLabels();
|
|
103
|
-
const images = getImagesFromContent(message.content);
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<View className="flex-col items-end gap-0.5">
|
|
107
|
-
{isEditing ? (
|
|
108
|
-
<View className="w-full max-w-[85%] gap-2 sm:max-w-[75%]">
|
|
109
|
-
<Textarea
|
|
110
|
-
value={editedContent}
|
|
111
|
-
onValueChange={onEditedContentChange}
|
|
112
|
-
rows={1}
|
|
113
|
-
autoResize
|
|
114
|
-
autoFocus
|
|
115
|
-
accessibilityLabel={labels.edit}
|
|
116
|
-
/>
|
|
117
|
-
<View className="flex-row justify-end gap-2">
|
|
118
|
-
<Button tone="neutral" appearance="subtle" size="xs" onPress={onCancelEdit}>
|
|
119
|
-
{labels.cancel}
|
|
120
|
-
</Button>
|
|
121
|
-
<Button tone="action" size="xs" onPress={() => onSaveEdit(message.id)}>
|
|
122
|
-
{labels.save}
|
|
123
|
-
</Button>
|
|
124
|
-
</View>
|
|
125
|
-
</View>
|
|
126
|
-
) : (
|
|
127
|
-
<AiChatUserMessage
|
|
128
|
-
animate={false}
|
|
129
|
-
actions={[
|
|
130
|
-
{
|
|
131
|
-
key: 'copy',
|
|
132
|
-
label: copiedMessageId === message.id ? labels.copied : labels.copy,
|
|
133
|
-
icon: copiedMessageId === message.id ? RiCheckLine : RiFileCopyLine,
|
|
134
|
-
onPress: () => onCopy(text, message.id),
|
|
135
|
-
},
|
|
136
|
-
...(showEditButton
|
|
137
|
-
? [
|
|
138
|
-
{
|
|
139
|
-
key: 'edit',
|
|
140
|
-
label: labels.edit,
|
|
141
|
-
icon: RiPencilLine,
|
|
142
|
-
onPress: () => onStartEdit(message.id, text),
|
|
143
|
-
},
|
|
144
|
-
]
|
|
145
|
-
: []),
|
|
146
|
-
]}
|
|
147
|
-
>
|
|
148
|
-
{images.length > 0 && ExpoImage ? (
|
|
149
|
-
<AiChatMessageLine block>
|
|
150
|
-
<View className="flex-row flex-wrap gap-2">
|
|
151
|
-
{images.map((imgUrl) => (
|
|
152
|
-
<View key={imgUrl} className="size-[120px] overflow-hidden rounded-xl">
|
|
153
|
-
<ExpoImage
|
|
154
|
-
source={{ uri: imgUrl }}
|
|
155
|
-
className="w-full h-full"
|
|
156
|
-
contentFit="cover"
|
|
157
|
-
/>
|
|
158
|
-
</View>
|
|
159
|
-
))}
|
|
160
|
-
</View>
|
|
161
|
-
</AiChatMessageLine>
|
|
162
|
-
) : null}
|
|
163
|
-
<AiChatMessageLine>{text}</AiChatMessageLine>
|
|
164
|
-
</AiChatUserMessage>
|
|
165
|
-
)}
|
|
166
|
-
</View>
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
/** Alia is shown thinking only once she has for this long: a hand-over to an agent shows no flash of her. */
|
|
171
|
-
const ALIA_THINKING_DELAY_MS = 450;
|
|
172
|
-
|
|
173
|
-
// ── Assistant Message ──
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* One reply, Alia's or a delegated agent's: the reply, and under it who answered. While the one
|
|
177
|
-
* who answers is working the identity shows its avatar large and thinking, and when the reply
|
|
178
|
-
* arrives it is the same avatar that settles under it, as in Bloom's multi-agent chat. Alia's own
|
|
179
|
-
* text runs the width of the thread; an agent answers in a bubble.
|
|
180
|
-
*/
|
|
181
|
-
function ReplyBody({
|
|
182
|
-
agent,
|
|
183
|
-
thinking,
|
|
184
|
-
reasoning,
|
|
185
|
-
actions,
|
|
186
|
-
children,
|
|
187
|
-
}: {
|
|
188
|
-
agent: ChatMessage['agentInfo'];
|
|
189
|
-
thinking: boolean;
|
|
190
|
-
/** What the model has reasoned so far: the thinking avatar folds it under its row. */
|
|
191
|
-
reasoning: React.ReactNode;
|
|
192
|
-
actions: React.ReactNode;
|
|
193
|
-
children: React.ReactNode;
|
|
194
|
-
}) {
|
|
195
|
-
const identity = (
|
|
196
|
-
<AgentReplyIdentity
|
|
197
|
-
character={agent ? readAgentCharacter(agent.character) : ALIA_AVATAR}
|
|
198
|
-
name={agent?.name ?? 'Alia'}
|
|
199
|
-
thinking={thinking}
|
|
200
|
-
reasoning={reasoning}
|
|
201
|
-
/>
|
|
202
|
-
);
|
|
203
|
-
if (agent === undefined)
|
|
204
|
-
return (
|
|
205
|
-
<AgentReply
|
|
206
|
-
identity={identity}
|
|
207
|
-
actions={actions}
|
|
208
|
-
// The thread pads its content and spaces its messages: the text starts at its edge, full width.
|
|
209
|
-
style={{ marginHorizontal: 0, marginTop: 0, maxWidth: '100%', width: '100%' }}
|
|
210
|
-
>
|
|
211
|
-
{children}
|
|
212
|
-
</AgentReply>
|
|
213
|
-
);
|
|
214
|
-
return (
|
|
215
|
-
<AgentReply
|
|
216
|
-
identity={identity}
|
|
217
|
-
actions={actions}
|
|
218
|
-
// The reply starts where Alia's own text does and sits one thread gap from the message above.
|
|
219
|
-
style={{ marginHorizontal: 0, marginTop: 0, maxWidth: '100%' }}
|
|
220
|
-
>
|
|
221
|
-
{children ? <AgentReplyBubble text={MARKDOWN_BODY}>{children}</AgentReplyBubble> : null}
|
|
222
|
-
</AgentReply>
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const AssistantMessage = React.memo(function AssistantMessage({
|
|
227
|
-
message,
|
|
228
|
-
messageText,
|
|
229
|
-
isStreamingThis,
|
|
230
|
-
isLastMessage,
|
|
231
|
-
isLoading,
|
|
232
|
-
agentsWorking,
|
|
233
|
-
onReadAloud,
|
|
234
|
-
ttsActiveMessageId,
|
|
235
|
-
ttsPlaybackState,
|
|
236
|
-
onCopy,
|
|
237
|
-
onThumbsUp,
|
|
238
|
-
onThumbsDown,
|
|
239
|
-
onApprovePlan,
|
|
240
|
-
onRejectPlan,
|
|
241
|
-
renderMarkdown,
|
|
242
|
-
}: {
|
|
243
|
-
message: ChatMessage;
|
|
244
|
-
messageText: string;
|
|
245
|
-
isStreamingThis: boolean;
|
|
246
|
-
isLastMessage: boolean;
|
|
247
|
-
isLoading: boolean;
|
|
248
|
-
/** An agent Alia sent for has not answered yet: it is the one thinking, not Alia. */
|
|
249
|
-
agentsWorking: boolean;
|
|
250
|
-
onReadAloud?: (messageId: string, text: string) => void;
|
|
251
|
-
ttsActiveMessageId?: string | null;
|
|
252
|
-
ttsPlaybackState?: PlaybackState;
|
|
253
|
-
onCopy: (text: string, messageId: string) => void;
|
|
254
|
-
onThumbsUp?: (messageId: string) => void;
|
|
255
|
-
onThumbsDown?: (messageId: string) => void;
|
|
256
|
-
onApprovePlan?: (planId: string) => void;
|
|
257
|
-
onRejectPlan?: (planId: string) => void;
|
|
258
|
-
renderMarkdown?: (content: string) => React.ReactNode;
|
|
259
|
-
}) {
|
|
260
|
-
const labels = useLabels();
|
|
261
|
-
const hasContent = messageText.length > 0;
|
|
262
|
-
// `[n]` markers that name a source become links to it.
|
|
263
|
-
const citedText = useMemo(
|
|
264
|
-
() =>
|
|
265
|
-
linkifyCitations(
|
|
266
|
-
messageText,
|
|
267
|
-
extractCitationSources(message.toolInvocations, message.researchProgress?.sources),
|
|
268
|
-
),
|
|
269
|
-
[messageText, message.toolInvocations, message.researchProgress?.sources],
|
|
270
|
-
);
|
|
271
|
-
const hasThinking = !!message.thinking;
|
|
272
|
-
|
|
273
|
-
// Both logs count units and are driven by `revealed`, from the calls that have really
|
|
274
|
-
// arrived — never by their own demo ticker. The task list waits for the turn to end.
|
|
275
|
-
const toolInvocations = message.toolInvocations;
|
|
276
|
-
const researchProgress = message.researchProgress;
|
|
277
|
-
const webLog = useMemo(
|
|
278
|
-
() => webSearchLog(toolInvocations, researchProgress, { t: labels.workLog }),
|
|
279
|
-
[toolInvocations, researchProgress, labels.workLog],
|
|
280
|
-
);
|
|
281
|
-
// The questions Alia asked, kept in the thread as a record. They are answered above the
|
|
282
|
-
// composer (`AliaChatContent`), where the person is about to type.
|
|
283
|
-
const questions = useMemo(
|
|
284
|
-
() => (toolInvocations ?? []).flatMap((inv) => askedQuestions(inv) ?? []),
|
|
285
|
-
[toolInvocations],
|
|
286
|
-
);
|
|
287
|
-
const taskLog = useMemo(() => {
|
|
288
|
-
const tasks = (toolInvocations ?? []).filter(
|
|
289
|
-
(inv) => !isWebInvocation(inv) && !isQuestionInvocation(inv),
|
|
290
|
-
);
|
|
291
|
-
return tasks.length === 0 || isStreamingThis
|
|
292
|
-
? null
|
|
293
|
-
: taskListLog(tasks, false, { t: labels.workLog });
|
|
294
|
-
}, [toolInvocations, isStreamingThis, labels.workLog]);
|
|
295
|
-
|
|
296
|
-
const isThisPlaying = ttsActiveMessageId === message.id && ttsPlaybackState === 'playing';
|
|
297
|
-
const isThisPaused = ttsActiveMessageId === message.id && ttsPlaybackState === 'paused';
|
|
298
|
-
const isThisLoading = ttsActiveMessageId === message.id && ttsPlaybackState === 'loading';
|
|
299
|
-
const replyActions =
|
|
300
|
-
!isStreamingThis && hasContent ? (
|
|
301
|
-
<AgentReplyActions
|
|
302
|
-
onVoteChange={(vote) =>
|
|
303
|
-
vote === 'up' ? onThumbsUp?.(message.id) : onThumbsDown?.(message.id)
|
|
304
|
-
}
|
|
305
|
-
onCopy={() => onCopy(messageText, message.id)}
|
|
306
|
-
>
|
|
307
|
-
{onReadAloud ? (
|
|
308
|
-
<AgentReplyAction
|
|
309
|
-
icon={isThisPlaying || isThisPaused ? RiStopFill : RiVolumeUpLine}
|
|
310
|
-
label={isThisPlaying || isThisPaused ? labels.stopReading : labels.readAloud}
|
|
311
|
-
selected={isThisPlaying || isThisPaused}
|
|
312
|
-
disabled={isThisLoading}
|
|
313
|
-
onPress={() => onReadAloud(message.id, messageText)}
|
|
314
|
-
/>
|
|
315
|
-
) : null}
|
|
316
|
-
</AgentReplyActions>
|
|
317
|
-
) : undefined;
|
|
318
|
-
|
|
319
|
-
// The one who answers is thinking until its reply starts: an agent from the call that sent for it,
|
|
320
|
-
// Alia when no agent is working for her. The avatar is the indicator.
|
|
321
|
-
// Alia thinks once she has for a moment: when she hands the turn over at once, only the agent is
|
|
322
|
-
// shown thinking, not Alia for an instant first.
|
|
323
|
-
const aliaThinking = useAfterDelay(
|
|
324
|
-
isLoading && !hasContent && !message.agentInfo && isLastMessage && !agentsWorking,
|
|
325
|
-
ALIA_THINKING_DELAY_MS,
|
|
326
|
-
);
|
|
327
|
-
const thinking = message.agentInfo ? isLoading && !hasContent : aliaThinking;
|
|
328
|
-
// The reply, its avatar and its reasoning come and go with their height eased, never popped.
|
|
329
|
-
const showsReply = hasContent || message.isStreaming || thinking;
|
|
330
|
-
|
|
331
|
-
return (
|
|
332
|
-
<View className="w-full">
|
|
333
|
-
{/* Plan Preview */}
|
|
334
|
-
{message.pendingPlan && onApprovePlan && onRejectPlan && (
|
|
335
|
-
<PlanPreviewCard
|
|
336
|
-
steps={message.pendingPlan.steps}
|
|
337
|
-
approved={message.pendingPlan.approved}
|
|
338
|
-
rejected={message.pendingPlan.rejected}
|
|
339
|
-
onApprove={() => onApprovePlan(message.pendingPlan!.planId)}
|
|
340
|
-
onReject={() => onRejectPlan(message.pendingPlan!.planId)}
|
|
341
|
-
/>
|
|
342
|
-
)}
|
|
343
|
-
|
|
344
|
-
{/* What the turn did: where it went (searches, pages, research) and everything else. */}
|
|
345
|
-
{taskLog === null ? null : (
|
|
346
|
-
<TaskList
|
|
347
|
-
tasks={taskLog.tasks}
|
|
348
|
-
revealed={taskLog.revealed}
|
|
349
|
-
collapseOnComplete="all"
|
|
350
|
-
working={false}
|
|
351
|
-
/>
|
|
352
|
-
)}
|
|
353
|
-
{webLog.steps.length === 0 ? null : (
|
|
354
|
-
<WebSearch
|
|
355
|
-
steps={webLog.steps}
|
|
356
|
-
revealed={webLog.revealed}
|
|
357
|
-
working={isStreamingThis ? undefined : false}
|
|
358
|
-
/>
|
|
359
|
-
)}
|
|
360
|
-
|
|
361
|
-
{/* Extended Thinking */}
|
|
362
|
-
{hasThinking && (
|
|
363
|
-
<View className="mb-3 w-full">
|
|
364
|
-
<ThinkingBlock text={message.thinking!} streaming={isStreamingThis && !hasContent} />
|
|
365
|
-
</View>
|
|
366
|
-
)}
|
|
367
|
-
|
|
368
|
-
{/* Message Content */}
|
|
369
|
-
<Collapsible open={showsReply}>
|
|
370
|
-
<ReplyBody
|
|
371
|
-
agent={message.agentInfo}
|
|
372
|
-
thinking={thinking}
|
|
373
|
-
reasoning={
|
|
374
|
-
thinking && hasThinking ? (
|
|
375
|
-
<AliaMarkdown content={message.thinking!} streaming={message.isStreaming} />
|
|
376
|
-
) : undefined
|
|
377
|
-
}
|
|
378
|
-
actions={replyActions}
|
|
379
|
-
>
|
|
380
|
-
{!hasContent && !message.isStreaming ? null : message.source === 'voice' ? (
|
|
381
|
-
<Text className="text-base text-text-primary leading-7">
|
|
382
|
-
{messageText}
|
|
383
|
-
{message.isStreaming ? '\u258C' : ''}
|
|
384
|
-
</Text>
|
|
385
|
-
) : renderMarkdown ? (
|
|
386
|
-
renderMarkdown(citedText)
|
|
387
|
-
) : (
|
|
388
|
-
<AliaMarkdown content={citedText} streaming={message.isStreaming} />
|
|
389
|
-
)}
|
|
390
|
-
</ReplyBody>
|
|
391
|
-
</Collapsible>
|
|
392
|
-
|
|
393
|
-
{/* What Alia asked stays in the thread as a record; answering happens above the composer. */}
|
|
394
|
-
{questions.length === 0 ? null : <AskedQuestions questions={questions} />}
|
|
395
|
-
</View>
|
|
396
|
-
);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// ── List ──
|
|
400
|
-
|
|
401
|
-
export function AliaChatMessageList({
|
|
402
|
-
messages,
|
|
403
|
-
isStreaming,
|
|
404
|
-
onReadAloud,
|
|
405
|
-
ttsActiveMessageId,
|
|
406
|
-
ttsPlaybackState,
|
|
407
|
-
onEditMessage,
|
|
408
|
-
onThumbsUp,
|
|
409
|
-
onThumbsDown,
|
|
410
|
-
onApprovePlan,
|
|
411
|
-
onRejectPlan,
|
|
412
|
-
welcomeComponent,
|
|
413
|
-
renderMarkdown,
|
|
414
|
-
}: AliaChatMessageListProps) {
|
|
415
|
-
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
|
416
|
-
const [editedContent, setEditedContent] = useState('');
|
|
417
|
-
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
|
418
|
-
const prevMessageCountRef = useRef(messages.length);
|
|
419
|
-
|
|
420
|
-
useEffect(() => {
|
|
421
|
-
prevMessageCountRef.current = messages.length;
|
|
422
|
-
}, [messages.length]);
|
|
423
|
-
|
|
424
|
-
const handleCopy = useCallback(async (content: string, messageId: string) => {
|
|
425
|
-
try {
|
|
426
|
-
if (Platform.OS === 'web') {
|
|
427
|
-
await navigator.clipboard.writeText(content);
|
|
428
|
-
} else {
|
|
429
|
-
const Clipboard = await import('expo-clipboard');
|
|
430
|
-
await Clipboard.setStringAsync(content);
|
|
431
|
-
}
|
|
432
|
-
setCopiedMessageId(messageId);
|
|
433
|
-
setTimeout(() => setCopiedMessageId(null), 2000);
|
|
434
|
-
} catch (err) {
|
|
435
|
-
console.warn('[AliaChat] Failed to copy message to clipboard:', err);
|
|
436
|
-
}
|
|
437
|
-
}, []);
|
|
438
|
-
|
|
439
|
-
const handleStartEdit = useCallback((messageId: string, content: string) => {
|
|
440
|
-
setEditingMessageId(messageId);
|
|
441
|
-
setEditedContent(content);
|
|
442
|
-
}, []);
|
|
443
|
-
|
|
444
|
-
const editedContentRef = useRef(editedContent);
|
|
445
|
-
editedContentRef.current = editedContent;
|
|
446
|
-
|
|
447
|
-
const handleSaveEdit = useCallback(
|
|
448
|
-
(messageId: string) => {
|
|
449
|
-
if (onEditMessage && editedContentRef.current.trim()) {
|
|
450
|
-
onEditMessage(messageId, editedContentRef.current);
|
|
451
|
-
}
|
|
452
|
-
setEditingMessageId(null);
|
|
453
|
-
setEditedContent('');
|
|
454
|
-
},
|
|
455
|
-
[onEditMessage],
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
const handleCancelEdit = useCallback(() => {
|
|
459
|
-
setEditingMessageId(null);
|
|
460
|
-
setEditedContent('');
|
|
461
|
-
}, []);
|
|
462
|
-
|
|
463
|
-
const filteredMessages = useMemo(() => messages.filter((m) => m.role !== 'system'), [messages]);
|
|
464
|
-
// An agent Alia sent for that has not answered: it is the one that is thinking.
|
|
465
|
-
const agentsWorking =
|
|
466
|
-
isStreaming && filteredMessages.some((m) => m.agentInfo && !getTextFromContent(m.content));
|
|
467
|
-
|
|
468
|
-
return (
|
|
469
|
-
// Bloom's thread: follows the newest turn, and reports its scroll to a floating-chrome
|
|
470
|
-
// container so the header and composer fade in over it.
|
|
471
|
-
<AiChatThread>
|
|
472
|
-
<View
|
|
473
|
-
className="max-w-3xl mx-auto w-full"
|
|
474
|
-
style={!filteredMessages.length ? { flex: 1, justifyContent: 'center' } : undefined}
|
|
475
|
-
>
|
|
476
|
-
{/* Welcome or loading */}
|
|
477
|
-
{!filteredMessages.length && (welcomeComponent || null)}
|
|
478
|
-
|
|
479
|
-
{/* Messages */}
|
|
480
|
-
<View className="gap-2">
|
|
481
|
-
{filteredMessages.map((msg, i) => {
|
|
482
|
-
const isNewMessage = i >= prevMessageCountRef.current;
|
|
483
|
-
const isLast = i === filteredMessages.length - 1;
|
|
484
|
-
const isStreamingThis = isStreaming && isLast;
|
|
485
|
-
const messageText = getTextFromContent(msg.content);
|
|
486
|
-
|
|
487
|
-
return (
|
|
488
|
-
<Animated.View key={msg.id} entering={isNewMessage ? chatMessageEnter : undefined}>
|
|
489
|
-
{/* A new row opens up as it arrives, so what is below it moves with it. */}
|
|
490
|
-
<Collapsible open appear={isNewMessage}>
|
|
491
|
-
{msg.role === 'user' ? (
|
|
492
|
-
<UserBubble
|
|
493
|
-
message={msg}
|
|
494
|
-
text={messageText}
|
|
495
|
-
isEditing={editingMessageId === msg.id}
|
|
496
|
-
editedContent={editingMessageId === msg.id ? editedContent : ''}
|
|
497
|
-
onEditedContentChange={setEditedContent}
|
|
498
|
-
onSaveEdit={handleSaveEdit}
|
|
499
|
-
onCancelEdit={handleCancelEdit}
|
|
500
|
-
onStartEdit={handleStartEdit}
|
|
501
|
-
onCopy={handleCopy}
|
|
502
|
-
copiedMessageId={copiedMessageId}
|
|
503
|
-
showEditButton={!!onEditMessage}
|
|
504
|
-
/>
|
|
505
|
-
) : (
|
|
506
|
-
<AssistantMessage
|
|
507
|
-
message={msg}
|
|
508
|
-
messageText={messageText}
|
|
509
|
-
isStreamingThis={isStreamingThis}
|
|
510
|
-
isLastMessage={isLast}
|
|
511
|
-
isLoading={isStreaming}
|
|
512
|
-
agentsWorking={agentsWorking}
|
|
513
|
-
onReadAloud={onReadAloud}
|
|
514
|
-
ttsActiveMessageId={ttsActiveMessageId}
|
|
515
|
-
ttsPlaybackState={ttsPlaybackState}
|
|
516
|
-
onCopy={handleCopy}
|
|
517
|
-
onThumbsUp={onThumbsUp}
|
|
518
|
-
onThumbsDown={onThumbsDown}
|
|
519
|
-
onApprovePlan={onApprovePlan}
|
|
520
|
-
onRejectPlan={onRejectPlan}
|
|
521
|
-
renderMarkdown={renderMarkdown}
|
|
522
|
-
/>
|
|
523
|
-
)}
|
|
524
|
-
</Collapsible>
|
|
525
|
-
</Animated.View>
|
|
526
|
-
);
|
|
527
|
-
})}
|
|
528
|
-
</View>
|
|
529
|
-
</View>
|
|
530
|
-
</AiChatThread>
|
|
531
|
-
);
|
|
532
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { AgentThinking } from '@oxy.so/bloom/agent-thinking';
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { useLabels } from '../lib/labels';
|
|
4
|
-
import { AliaMarkdown } from './Markdown';
|
|
5
|
-
|
|
6
|
-
interface ThinkingBlockProps {
|
|
7
|
-
/** The model's reasoning, as Markdown. */
|
|
8
|
-
text: string;
|
|
9
|
-
/** Still reasoning: the block opens, and closes again when this turns false. */
|
|
10
|
-
streaming: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A turn's reasoning, once it is done: Bloom's `AgentThinking` as a closed disclosure, "Thought
|
|
15
|
-
* for N seconds", with the text set as Markdown. While the reasoning streams it draws nothing:
|
|
16
|
-
* the avatar under the reply is the one indicator that the turn is thinking
|
|
17
|
-
* (`AgentReplyIdentity`), and a second one beside it is noise. It still counts how long the stream
|
|
18
|
-
* ran, because the message carries no duration.
|
|
19
|
-
*/
|
|
20
|
-
export function ThinkingBlock({ text, streaming }: ThinkingBlockProps) {
|
|
21
|
-
const labels = useLabels();
|
|
22
|
-
const startedAt = useRef<number | null>(null);
|
|
23
|
-
const [seconds, setSeconds] = useState<number>();
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (streaming) {
|
|
27
|
-
startedAt.current = Date.now();
|
|
28
|
-
} else if (startedAt.current !== null) {
|
|
29
|
-
setSeconds(Math.max(1, Math.round((Date.now() - startedAt.current) / 1000)));
|
|
30
|
-
startedAt.current = null;
|
|
31
|
-
}
|
|
32
|
-
}, [streaming]);
|
|
33
|
-
|
|
34
|
-
const finishedLabel = seconds ? labels.thoughtFor(seconds) : labels.reasoning;
|
|
35
|
-
|
|
36
|
-
if (streaming) return null;
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<AgentThinking thinking={false} label={finishedLabel}>
|
|
40
|
-
<AliaMarkdown content={text} streaming={streaming} />
|
|
41
|
-
</AgentThinking>
|
|
42
|
-
);
|
|
43
|
-
}
|