@alia.onl/sdk 15.8.0 → 19.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.
Files changed (142) hide show
  1. package/CHANGELOG.md +262 -13
  2. package/README.md +223 -151
  3. package/package.json +16 -6
  4. package/src/animations.ts +0 -1
  5. package/src/components/AliaAskProvider.tsx +127 -0
  6. package/src/components/AliaChatContent.tsx +604 -392
  7. package/src/components/AliaChatHeader.tsx +14 -38
  8. package/src/components/AliaChatScreen.tsx +56 -31
  9. package/src/components/AliaChatSheet.tsx +110 -36
  10. package/src/components/AliaComposer.tsx +1 -1
  11. package/src/components/AliaParticipants.tsx +202 -0
  12. package/src/components/AliaParticipantsHeader.tsx +60 -0
  13. package/src/components/AliaSharedConversation.tsx +86 -0
  14. package/src/components/AliaWelcomeMessage.tsx +18 -5
  15. package/src/components/Image.tsx +12 -0
  16. package/src/components/Markdown.tsx +498 -14
  17. package/src/components/PlanPreviewCard.tsx +7 -49
  18. package/src/components/ReplyMarkdown.tsx +359 -0
  19. package/src/components/RichBlocks.tsx +195 -0
  20. package/src/components/canvas/CanvasComponent.tsx +59 -0
  21. package/src/components/canvas/ChartRenderer.tsx +73 -0
  22. package/src/components/canvas/CodeRenderer.tsx +24 -0
  23. package/src/components/canvas/FormRenderer.tsx +101 -0
  24. package/src/components/canvas/MarkdownRenderer.tsx +14 -0
  25. package/src/components/canvas/TableRenderer.tsx +47 -0
  26. package/src/components/thought/FilesAndSources.tsx +120 -0
  27. package/src/components/thought/ThoughtPanel.tsx +417 -0
  28. package/src/components/thought/ToolStep.tsx +141 -0
  29. package/src/components/thread/AgentResultCard.tsx +125 -0
  30. package/src/components/thread/AgentTaskCard.tsx +127 -0
  31. package/src/components/thread/AliaThread.tsx +401 -0
  32. package/src/components/thread/MessageBlockBoundary.tsx +78 -0
  33. package/src/components/thread/NewConversationOffer.tsx +54 -0
  34. package/src/components/thread/ThreadRow.tsx +458 -0
  35. package/src/components/thread/ThreadSearch.tsx +129 -0
  36. package/src/components/thread/ToolResultCard.tsx +254 -0
  37. package/src/components/thread/TurnMenu.tsx +54 -0
  38. package/src/components/thread/TurnStatusLine.tsx +34 -0
  39. package/src/components/voice/AliaVoiceCall.tsx +21 -0
  40. package/src/components/voice/VoiceControls.tsx +2 -28
  41. package/src/components/voice/VoiceSession.tsx +39 -11
  42. package/src/hooks/useAliaActivity.ts +2 -2
  43. package/src/hooks/useAtBottom.ts +51 -0
  44. package/src/hooks/useAttachMenu.ts +94 -0
  45. package/src/hooks/useChatComposer.ts +299 -0
  46. package/src/hooks/useChatVoice.ts +129 -0
  47. package/src/hooks/useDocumentPicker.ts +3 -29
  48. package/src/hooks/useFailedTurnToast.ts +43 -0
  49. package/src/hooks/useMessageVote.ts +50 -0
  50. package/src/hooks/useOpenThought.ts +79 -0
  51. package/src/hooks/usePendingQuestions.ts +2 -2
  52. package/src/hooks/useReadAloud.ts +92 -0
  53. package/src/hooks/useShareHandler.ts +24 -0
  54. package/src/hooks/useSoundEffects.ts +1 -1
  55. package/src/hooks/useThoughtScope.ts +59 -0
  56. package/src/hooks/useThreadRows.ts +145 -0
  57. package/src/hooks/useThreadView.ts +68 -0
  58. package/src/hooks/useTimelineWindow.ts +256 -0
  59. package/src/hooks/useVoiceLauncher.ts +180 -0
  60. package/src/hooks/useVoiceRoom.ts +4 -4
  61. package/src/index.ts +43 -29
  62. package/src/lib/activity-store.ts +1 -1
  63. package/src/lib/add-menu.ts +7 -0
  64. package/src/lib/call-thread.ts +33 -0
  65. package/src/lib/citations.ts +79 -2
  66. package/src/lib/clipboard.ts +23 -0
  67. package/src/lib/content-utils.ts +10 -0
  68. package/src/lib/copy-en.ts +284 -0
  69. package/src/lib/copy.ts +32 -0
  70. package/src/lib/document-picker.ts +31 -0
  71. package/src/lib/flights.ts +22 -13
  72. package/src/lib/focus-return.ts +66 -0
  73. package/src/lib/keyboard.native.tsx +19 -0
  74. package/src/lib/keyboard.tsx +9 -0
  75. package/src/lib/labels.tsx +69 -17
  76. package/src/lib/participants.ts +86 -0
  77. package/src/lib/power-level-options.ts +66 -0
  78. package/src/lib/questions.ts +2 -2
  79. package/src/lib/task-labels.ts +4 -4
  80. package/src/lib/tool-call-status.ts +1 -1
  81. package/src/lib/tool-icons.ts +36 -0
  82. package/src/lib/tool-renderers.tsx +48 -0
  83. package/src/lib/turn-state.ts +2 -2
  84. package/src/lib/voice-call-request.ts +49 -0
  85. package/src/lib/voice-call.ts +98 -35
  86. package/src/lib/voice-controls-labels.ts +31 -0
  87. package/src/lib/voice-protocol.ts +34 -15
  88. package/src/lib/work-log.ts +6 -6
  89. package/src/runtime/agents.ts +151 -0
  90. package/src/runtime/api.ts +141 -0
  91. package/src/runtime/app-assertion.ts +53 -0
  92. package/src/runtime/attachment-utils.ts +96 -0
  93. package/src/runtime/composer-draft-store.ts +270 -0
  94. package/src/runtime/config.tsx +186 -0
  95. package/src/runtime/conversation-store.ts +49 -0
  96. package/src/runtime/conversations.ts +557 -0
  97. package/src/runtime/error-utils.ts +104 -0
  98. package/src/runtime/host-tools.ts +140 -0
  99. package/src/runtime/index.ts +113 -0
  100. package/src/runtime/message-history.ts +124 -0
  101. package/src/runtime/model.ts +39 -0
  102. package/src/runtime/oxy-files.ts +37 -0
  103. package/src/runtime/power-levels.ts +66 -0
  104. package/src/runtime/query-keys.ts +34 -0
  105. package/src/runtime/random-uuid.ts +25 -0
  106. package/src/runtime/routes.ts +51 -0
  107. package/src/runtime/sharing.ts +165 -0
  108. package/src/runtime/sse-frame-reader.ts +88 -0
  109. package/src/runtime/stream-events.ts +146 -0
  110. package/src/runtime/stream-outcome.ts +21 -0
  111. package/src/runtime/thought-store.ts +103 -0
  112. package/src/runtime/thread/agent-activity.ts +261 -0
  113. package/src/runtime/thread/automation-labels.ts +79 -0
  114. package/src/runtime/thread/conversation-export.ts +135 -0
  115. package/src/runtime/thread/message-days.ts +120 -0
  116. package/src/runtime/thread/message-processor.ts +93 -0
  117. package/src/runtime/thread/task-utils.ts +11 -0
  118. package/src/runtime/thread/thought-utils.ts +752 -0
  119. package/src/runtime/thread/thread-history.ts +139 -0
  120. package/src/runtime/thread/timeline.ts +202 -0
  121. package/src/runtime/thread/title-tags.ts +8 -0
  122. package/src/runtime/thread/tool-cards.ts +383 -0
  123. package/src/runtime/turn-failure.ts +108 -0
  124. package/src/runtime/types.ts +148 -0
  125. package/src/runtime/usage-limit-error.ts +44 -0
  126. package/src/runtime/use-agent-activity.ts +80 -0
  127. package/src/runtime/use-conversation-stream.ts +1433 -0
  128. package/src/runtime/use-conversation-thread.ts +556 -0
  129. package/src/runtime/use-start-conversation.ts +107 -0
  130. package/src/runtime/use-thread-history.ts +64 -0
  131. package/src/runtime/use-thread-search.ts +95 -0
  132. package/src/runtime/use-turn-edit.ts +99 -0
  133. package/src/tools.ts +2 -2
  134. package/src/types.ts +18 -47
  135. package/src/components/AliaChatMessageList.tsx +0 -532
  136. package/src/components/ThinkingBlock.tsx +0 -43
  137. package/src/hooks/useAliaChat.ts +0 -573
  138. package/src/lib/catalogue.ts +0 -236
  139. package/src/lib/chat-stream.ts +0 -655
  140. package/src/lib/chat-transport.ts +0 -98
  141. package/src/lib/delegation.ts +0 -34
  142. 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
- }