@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
|
@@ -7,17 +7,8 @@
|
|
|
7
7
|
* breadcrumb header is Bloom's `AiChatContainer`, the one the app's conversation uses.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import React, {
|
|
11
|
-
|
|
12
|
-
Suspense,
|
|
13
|
-
useContext,
|
|
14
|
-
useState,
|
|
15
|
-
useCallback,
|
|
16
|
-
useEffect,
|
|
17
|
-
useMemo,
|
|
18
|
-
useRef,
|
|
19
|
-
} from 'react';
|
|
20
|
-
import { View, StyleSheet } from 'react-native';
|
|
10
|
+
import React, { Suspense, useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
11
|
+
import { View } from 'react-native';
|
|
21
12
|
import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
|
|
22
13
|
import { useTTS } from '../hooks/useTTS';
|
|
23
14
|
import { useSpeechToText } from '../hooks/useSpeechToText';
|
|
@@ -25,18 +16,67 @@ import { useImagePicker } from '../hooks/useImagePicker';
|
|
|
25
16
|
import { imageUriToDataUrl } from '../lib/image-data-url';
|
|
26
17
|
import { useAmbientWave } from '../hooks/useAmbientWave';
|
|
27
18
|
import { AmbientField } from './AmbientField';
|
|
19
|
+
import { QuestionCard } from './QuestionCard';
|
|
20
|
+
import { pendingQuestions } from '../lib/questions';
|
|
28
21
|
import { AliaChatMessageList } from './AliaChatMessageList';
|
|
29
|
-
import {
|
|
22
|
+
import { AliaFlightsProvider, ALIA_TO_PROFILE } from '../lib/flights';
|
|
23
|
+
import { AliaWelcomeMessage } from './AliaWelcomeMessage';
|
|
24
|
+
import { ComposerSuggestions } from './ComposerSuggestions';
|
|
25
|
+
import { useComposerSuggestions, useSuggestionQuery } from '../hooks/useComposerSuggestions';
|
|
30
26
|
import { ComposerLoader } from '@oxy.so/bloom/composer-loader';
|
|
31
27
|
import { AiChatContainer } from '@oxy.so/bloom/ai-chat';
|
|
28
|
+
import {
|
|
29
|
+
AvatarFlightLayer,
|
|
30
|
+
useAvatarFlights,
|
|
31
|
+
type AvatarFlights,
|
|
32
|
+
} from '@oxy.so/bloom/avatar-flight';
|
|
32
33
|
import { ComposerPill, type ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
|
|
33
34
|
import { RiImageLine } from '@oxy.so/bloom/icons';
|
|
34
35
|
import { toast } from '@oxy.so/bloom/toast';
|
|
35
36
|
import { Button } from '@oxy.so/bloom/button';
|
|
36
37
|
import { type AliaChatLabels, AliaChatLabelsProvider, useLabels } from '../lib/labels';
|
|
37
|
-
import {
|
|
38
|
+
import type { AliaAvatarState } from './AliaAvatar';
|
|
38
39
|
import { VoiceModeIcon } from './VoiceModeIcon';
|
|
39
|
-
import type {
|
|
40
|
+
import type {
|
|
41
|
+
AliaSuggestion,
|
|
42
|
+
ChatMessage,
|
|
43
|
+
VoiceMessage,
|
|
44
|
+
VoiceSessionComponent,
|
|
45
|
+
VoiceSessionState,
|
|
46
|
+
} from '../types';
|
|
47
|
+
|
|
48
|
+
/** The distinct delegated agents among `messages`, in the order they first answered. */
|
|
49
|
+
function participatingAgents(messages: readonly ChatMessage[]): AliaChatAgent[] {
|
|
50
|
+
const seen = new Map<string, AliaChatAgent>();
|
|
51
|
+
for (const { agentInfo } of messages) {
|
|
52
|
+
if (agentInfo && !seen.has(agentInfo.id)) seen.set(agentInfo.id, agentInfo);
|
|
53
|
+
}
|
|
54
|
+
return [...seen.values()];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An agent that answered in the conversation, as the header shows it beside Alia. */
|
|
58
|
+
export interface AliaChatAgent {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
/** Its look, as the server stores it (JSON); absent, the default look. */
|
|
62
|
+
character?: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** What the chat tells its header and actions: whether there is a conversation to clear, and who took part. */
|
|
66
|
+
export interface AliaChatState {
|
|
67
|
+
hasMessages: boolean;
|
|
68
|
+
clear: () => void;
|
|
69
|
+
/** What Alia is doing; the header's avatar shows it. */
|
|
70
|
+
markState: AliaAvatarState;
|
|
71
|
+
/** The delegated agents that have answered, in the order they first did. */
|
|
72
|
+
agents: AliaChatAgent[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A turn of the person's: the text, and the photos (`data:` or `https:` URLs) sent with it. */
|
|
76
|
+
export interface AliaTurn {
|
|
77
|
+
text: string;
|
|
78
|
+
images?: ReadonlyArray<string>;
|
|
79
|
+
}
|
|
40
80
|
|
|
41
81
|
export interface AliaChatContentProps {
|
|
42
82
|
clientContext?: string;
|
|
@@ -47,7 +87,12 @@ export interface AliaChatContentProps {
|
|
|
47
87
|
// Welcome message
|
|
48
88
|
welcomeGreeting?: string;
|
|
49
89
|
welcomeSubtitle?: string;
|
|
50
|
-
|
|
90
|
+
/** The lines offered over the composer on an empty chat. */
|
|
91
|
+
welcomeSuggestions?: AliaSuggestion[];
|
|
92
|
+
/** What the host found for the typed text (see `onSuggestionQuery`). */
|
|
93
|
+
suggestionMatches?: AliaSuggestion[];
|
|
94
|
+
/** The typed text, once typing has settled, for the host to search suggestions with. */
|
|
95
|
+
onSuggestionQuery?: (query: string) => void;
|
|
51
96
|
// Message action callbacks
|
|
52
97
|
onEditMessage?: (messageId: string, newContent: string) => void;
|
|
53
98
|
onThumbsUp?: (messageId: string) => void;
|
|
@@ -56,17 +101,11 @@ export interface AliaChatContentProps {
|
|
|
56
101
|
onRejectPlan?: (planId: string) => void;
|
|
57
102
|
/** Override markdown renderer (app passes CustomMarkdown) */
|
|
58
103
|
renderMarkdown?: (content: string) => React.ReactNode;
|
|
59
|
-
/** Breadcrumb title beside the brand mark. Default `Alia`; `null` drops the breadcrumb. */
|
|
60
|
-
title?: string | null;
|
|
61
104
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
105
|
+
* The chat's top bar. Receives what it needs from the chat: whether there is anything to clear,
|
|
106
|
+
* how, who took part, and what Alia is doing.
|
|
64
107
|
*/
|
|
65
|
-
|
|
66
|
-
| React.ReactNode
|
|
67
|
-
| ((state: { hasMessages: boolean; clear: () => void }) => React.ReactNode);
|
|
68
|
-
/** Right end of the breadcrumb row; receives the clear handler. Replaces Bloom's share/more. */
|
|
69
|
-
actions?: (state: { hasMessages: boolean; clear: () => void }) => React.ReactNode;
|
|
108
|
+
header?: (state: AliaChatState) => React.ReactNode;
|
|
70
109
|
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
71
110
|
isDarkMode?: boolean;
|
|
72
111
|
/**
|
|
@@ -75,25 +114,31 @@ export interface AliaChatContentProps {
|
|
|
75
114
|
* from this module. May be a `React.lazy` component.
|
|
76
115
|
*/
|
|
77
116
|
voiceSession?: VoiceSessionComponent;
|
|
117
|
+
/**
|
|
118
|
+
* Space the composer keeps clear at the bottom edge, in px — the home indicator, when the
|
|
119
|
+
* chat is flush with the screen (a phone sheet). The ambient background still runs behind it.
|
|
120
|
+
*/
|
|
121
|
+
bottomInset?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Hands the first turn of an empty chat to the host instead of sending it here — a sheet opens
|
|
124
|
+
* the conversation in a new sheet over this one. Omitted, the chat sends it itself.
|
|
125
|
+
*/
|
|
126
|
+
onStart?: (turn: AliaTurn) => void;
|
|
127
|
+
/** The agents the host knows, with their look: a delegation's thinking avatar is drawn from them. */
|
|
128
|
+
agents?: UseAliaChatOptions['agents'];
|
|
129
|
+
/**
|
|
130
|
+
* The avatar flights this chat takes part in, when a sheet shares them across its chats (the
|
|
131
|
+
* greeting's avatar flies into a header in another sheet). Omitted, the chat has its own.
|
|
132
|
+
*/
|
|
133
|
+
flights?: AvatarFlights;
|
|
134
|
+
/** A turn sent once as this chat mounts: the conversation a sheet was opened for. */
|
|
135
|
+
initialTurn?: AliaTurn;
|
|
78
136
|
/** Internal sheet lifecycle bridge: receives the live request canceller. */
|
|
79
137
|
onStopReady?: (stop: (() => void) | null) => void;
|
|
80
138
|
/** Overrides for the chat's own copy; anything omitted is English. */
|
|
81
139
|
labels?: Partial<AliaChatLabels>;
|
|
82
140
|
}
|
|
83
141
|
|
|
84
|
-
/** The live state of the chat, read by the avatar wherever the header or welcome draws it. */
|
|
85
|
-
const MarkStateContext = createContext<AliaAvatarState>('idle');
|
|
86
|
-
|
|
87
|
-
/** Alia's avatar at any size, following the chat's live state. */
|
|
88
|
-
export function AliaChatMark({ size, reactionKey }: { size: number; reactionKey?: number }) {
|
|
89
|
-
return <AliaAvatar size={size} state={useContext(MarkStateContext)} reactionKey={reactionKey} />;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** `AiChatContainer`'s `projectIcon` takes a component that is handed its size as `width`. */
|
|
93
|
-
function BreadcrumbMark({ width }: { width?: number }) {
|
|
94
|
-
return <AliaChatMark size={width ?? 16} />;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
142
|
/** The add menu: photos are the one attachment the chat API carries (`image_url` parts). */
|
|
98
143
|
function addMenu(labels: AliaChatLabels): ComposerPanelAddMenuGroup[] {
|
|
99
144
|
return [
|
|
@@ -125,19 +170,26 @@ function AliaChatContentBody({
|
|
|
125
170
|
welcomeGreeting,
|
|
126
171
|
welcomeSubtitle,
|
|
127
172
|
welcomeSuggestions,
|
|
173
|
+
suggestionMatches,
|
|
174
|
+
onSuggestionQuery,
|
|
128
175
|
onEditMessage,
|
|
129
176
|
onThumbsUp,
|
|
130
177
|
onThumbsDown,
|
|
131
178
|
onApprovePlan,
|
|
132
179
|
onRejectPlan,
|
|
133
180
|
renderMarkdown,
|
|
134
|
-
|
|
135
|
-
headerLeading,
|
|
136
|
-
actions,
|
|
181
|
+
header,
|
|
137
182
|
isDarkMode,
|
|
138
183
|
voiceSession: VoiceSession,
|
|
184
|
+
bottomInset = 0,
|
|
185
|
+
onStart,
|
|
186
|
+
agents: knownAgents,
|
|
187
|
+
flights: sharedFlights,
|
|
188
|
+
initialTurn,
|
|
139
189
|
onStopReady,
|
|
140
190
|
}: AliaChatContentProps) {
|
|
191
|
+
const ownFlights = useAvatarFlights();
|
|
192
|
+
const flights = sharedFlights ?? ownFlights;
|
|
141
193
|
const labels = useLabels();
|
|
142
194
|
const addMenuGroups = useMemo(() => addMenu(labels), [labels]);
|
|
143
195
|
|
|
@@ -150,8 +202,46 @@ function AliaChatContentBody({
|
|
|
150
202
|
[onError],
|
|
151
203
|
);
|
|
152
204
|
// ── Chat ──
|
|
153
|
-
const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
|
|
154
|
-
const { messages, setMessages, send, isStreaming, stop, clear } =
|
|
205
|
+
const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext, agents: knownAgents };
|
|
206
|
+
const { messages, setMessages, send, isStreaming, stop, clear, approvePlan, rejectPlan } =
|
|
207
|
+
useAliaChat(chatOptions);
|
|
208
|
+
const agents = useMemo(() => participatingAgents(messages), [messages]);
|
|
209
|
+
// What Alia asked in her last turn and the person has not answered: asked above the composer,
|
|
210
|
+
// not in the thread. Any later message, typed or sent from the card, is the answer.
|
|
211
|
+
const lastMessage = messages.at(-1);
|
|
212
|
+
const questionsAsked = useMemo(
|
|
213
|
+
() => pendingQuestions(messages, isStreaming),
|
|
214
|
+
[messages, isStreaming],
|
|
215
|
+
);
|
|
216
|
+
const [dismissedQuestionsOf, setDismissedQuestionsOf] = useState<string | null>(null);
|
|
217
|
+
const asking = questionsAsked.length > 0 && dismissedQuestionsOf !== lastMessage?.id;
|
|
218
|
+
|
|
219
|
+
// The first turn of an empty chat goes to the host when it asked for it; every other turn is sent here.
|
|
220
|
+
const startsElsewhere = onStart !== undefined && messages.length === 0;
|
|
221
|
+
const firstTurn = messages.length === 0;
|
|
222
|
+
const submitTurn = useCallback(
|
|
223
|
+
async (text: string, images?: ReadonlyArray<string>) => {
|
|
224
|
+
if (startsElsewhere) {
|
|
225
|
+
// The host's conversation sheet opens first so that the flight, drawn when it launches,
|
|
226
|
+
// is above it; this chat stays on the greeting, so the avatar can still be measured.
|
|
227
|
+
onStart({ text, images });
|
|
228
|
+
await flights.launch([ALIA_TO_PROFILE]);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// The greeting leaves with the first turn: measure it before it does.
|
|
232
|
+
if (firstTurn) await flights.launch([ALIA_TO_PROFILE]);
|
|
233
|
+
send(text, images ? { images } : undefined);
|
|
234
|
+
},
|
|
235
|
+
[startsElsewhere, onStart, firstTurn, flights, send],
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// The conversation this chat was opened for, sent once.
|
|
239
|
+
const initialSent = useRef(false);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!initialTurn || initialSent.current) return;
|
|
242
|
+
initialSent.current = true;
|
|
243
|
+
send(initialTurn.text, initialTurn.images ? { images: initialTurn.images } : undefined);
|
|
244
|
+
}, [initialTurn, send]);
|
|
155
245
|
|
|
156
246
|
useEffect(() => {
|
|
157
247
|
onStopReady?.(stop);
|
|
@@ -162,7 +252,9 @@ function AliaChatContentBody({
|
|
|
162
252
|
const [inputValue, setInputValue] = useState('');
|
|
163
253
|
|
|
164
254
|
// Picked photos wait here, as tiles above the field, until the turn is sent.
|
|
165
|
-
const [photos, setPhotos] = useState<
|
|
255
|
+
const [photos, setPhotos] = useState<
|
|
256
|
+
Array<{ id: string; name: string; uri: string; mimeType?: string }>
|
|
257
|
+
>([]);
|
|
166
258
|
const { pickImage } = useImagePicker();
|
|
167
259
|
|
|
168
260
|
const handleAddMenuSelect = useCallback(
|
|
@@ -176,12 +268,33 @@ function AliaChatContentBody({
|
|
|
176
268
|
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
177
269
|
name: asset.name,
|
|
178
270
|
uri: asset.uri,
|
|
271
|
+
mimeType: asset.mimeType,
|
|
179
272
|
})),
|
|
180
273
|
]);
|
|
181
274
|
},
|
|
182
275
|
[pickImage],
|
|
183
276
|
);
|
|
184
277
|
|
|
278
|
+
// Suggestions over the composer on an empty chat, as the Alia app has them.
|
|
279
|
+
const suggestionQuery = useSuggestionQuery(inputValue, messages.length === 0);
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
onSuggestionQuery?.(suggestionQuery);
|
|
282
|
+
}, [suggestionQuery, onSuggestionQuery]);
|
|
283
|
+
const handlePickSuggestion = useCallback(
|
|
284
|
+
(suggestion: AliaSuggestion) => {
|
|
285
|
+
setInputValue('');
|
|
286
|
+
submitTurn(suggestion.text);
|
|
287
|
+
},
|
|
288
|
+
[submitTurn],
|
|
289
|
+
);
|
|
290
|
+
const suggestions = useComposerSuggestions({
|
|
291
|
+
draft: inputValue,
|
|
292
|
+
enabled: messages.length === 0,
|
|
293
|
+
welcome: welcomeSuggestions,
|
|
294
|
+
matches: suggestionMatches,
|
|
295
|
+
onPick: handlePickSuggestion,
|
|
296
|
+
});
|
|
297
|
+
|
|
185
298
|
const handleSubmit = useCallback(
|
|
186
299
|
async (value: string) => {
|
|
187
300
|
const text = value.trim();
|
|
@@ -190,8 +303,10 @@ function AliaChatContentBody({
|
|
|
190
303
|
setInputValue('');
|
|
191
304
|
setPhotos([]);
|
|
192
305
|
try {
|
|
193
|
-
const images = await Promise.all(
|
|
194
|
-
|
|
306
|
+
const images = await Promise.all(
|
|
307
|
+
pending.map((photo) => imageUriToDataUrl(photo.uri, photo.mimeType)),
|
|
308
|
+
);
|
|
309
|
+
submitTurn(text, images);
|
|
195
310
|
} catch (err) {
|
|
196
311
|
// Keep the draft: the photos could not be read, nothing was sent.
|
|
197
312
|
setInputValue(value);
|
|
@@ -199,7 +314,7 @@ function AliaChatContentBody({
|
|
|
199
314
|
reportError(err instanceof Error ? err.message : 'Could not attach the photos');
|
|
200
315
|
}
|
|
201
316
|
},
|
|
202
|
-
[
|
|
317
|
+
[submitTurn, photos, reportError],
|
|
203
318
|
);
|
|
204
319
|
|
|
205
320
|
// ── Dictation: the pill's mic toggles it, the transcript lands in the field ──
|
|
@@ -237,11 +352,13 @@ function AliaChatContentBody({
|
|
|
237
352
|
setMessages([...textSnapshotRef.current, ...voiceMessages.map(adaptVoiceMessage)]);
|
|
238
353
|
}, [voiceMessages, setMessages]);
|
|
239
354
|
|
|
355
|
+
const messagesRef = useRef(messages);
|
|
356
|
+
messagesRef.current = messages;
|
|
240
357
|
const activateVoice = useCallback(() => {
|
|
241
358
|
if (isVoiceActive) return;
|
|
242
|
-
textSnapshotRef.current = [...
|
|
359
|
+
textSnapshotRef.current = [...messagesRef.current];
|
|
243
360
|
setIsVoiceActive(true);
|
|
244
|
-
}, [isVoiceActive
|
|
361
|
+
}, [isVoiceActive]);
|
|
245
362
|
|
|
246
363
|
// Unmounting the session disconnects the room; the transcript merged above
|
|
247
364
|
// stays in the chat.
|
|
@@ -258,11 +375,12 @@ function AliaChatContentBody({
|
|
|
258
375
|
);
|
|
259
376
|
|
|
260
377
|
// ── TTS read aloud handler ──
|
|
378
|
+
const { readAloud } = tts;
|
|
261
379
|
const handleReadAloud = useCallback(
|
|
262
380
|
(messageId: string, text: string) => {
|
|
263
|
-
|
|
381
|
+
readAloud(messageId, text);
|
|
264
382
|
},
|
|
265
|
-
[
|
|
383
|
+
[readAloud],
|
|
266
384
|
);
|
|
267
385
|
|
|
268
386
|
// ── Mark state (drives the header brand mark) ──
|
|
@@ -284,15 +402,11 @@ function AliaChatContentBody({
|
|
|
284
402
|
const welcomeComponent = useMemo(() => {
|
|
285
403
|
if (!welcomeGreeting || messages.length > 0) return undefined;
|
|
286
404
|
return (
|
|
287
|
-
<
|
|
288
|
-
greeting={welcomeGreeting}
|
|
289
|
-
|
|
290
|
-
suggestions={welcomeSuggestions}
|
|
291
|
-
onSuggestionPress={send}
|
|
292
|
-
hideMark
|
|
293
|
-
/>
|
|
405
|
+
<View className="flex-1 items-center justify-center px-4 py-10">
|
|
406
|
+
<AliaWelcomeMessage greeting={welcomeGreeting} subtitle={welcomeSubtitle} />
|
|
407
|
+
</View>
|
|
294
408
|
);
|
|
295
|
-
}, [welcomeGreeting, welcomeSubtitle,
|
|
409
|
+
}, [welcomeGreeting, welcomeSubtitle, messages.length]);
|
|
296
410
|
|
|
297
411
|
// ── Ambient wave — one persistent overlay across idle/voice/TTS/STT ──
|
|
298
412
|
const wave = useAmbientWave({
|
|
@@ -329,6 +443,17 @@ function AliaChatContentBody({
|
|
|
329
443
|
[VoiceSession, activateVoice, labels.startVoice],
|
|
330
444
|
);
|
|
331
445
|
|
|
446
|
+
const pillAttachments = useMemo(
|
|
447
|
+
() =>
|
|
448
|
+
photos.map((photo) => ({
|
|
449
|
+
id: photo.id,
|
|
450
|
+
name: photo.name,
|
|
451
|
+
kind: 'image' as const,
|
|
452
|
+
src: photo.uri,
|
|
453
|
+
})),
|
|
454
|
+
[photos],
|
|
455
|
+
);
|
|
456
|
+
|
|
332
457
|
const composer =
|
|
333
458
|
VoiceSession && isVoiceActive ? (
|
|
334
459
|
/* Suspense so the session may be handed to us as a React.lazy import. */
|
|
@@ -337,8 +462,24 @@ function AliaChatContentBody({
|
|
|
337
462
|
</Suspense>
|
|
338
463
|
) : (
|
|
339
464
|
<>
|
|
465
|
+
<ComposerSuggestions
|
|
466
|
+
completions={suggestions.completions}
|
|
467
|
+
selected={suggestions.selected}
|
|
468
|
+
onPick={handlePickSuggestion}
|
|
469
|
+
/>
|
|
470
|
+
{/* Beside the loader, not inside the pill: its light traces the box it wraps. */}
|
|
471
|
+
{asking ? (
|
|
472
|
+
<View className="mb-2">
|
|
473
|
+
<QuestionCard
|
|
474
|
+
questions={questionsAsked}
|
|
475
|
+
onSubmit={(message) => send(message)}
|
|
476
|
+
onDismiss={() => setDismissedQuestionsOf(lastMessage?.id ?? null)}
|
|
477
|
+
/>
|
|
478
|
+
</View>
|
|
479
|
+
) : null}
|
|
340
480
|
<ComposerLoader active={isStreaming}>
|
|
341
481
|
<ComposerPill
|
|
482
|
+
onKeyPress={suggestions.onKeyPress}
|
|
342
483
|
surface={false}
|
|
343
484
|
frosted={isStreaming}
|
|
344
485
|
value={inputValue}
|
|
@@ -350,12 +491,7 @@ function AliaChatContentBody({
|
|
|
350
491
|
placeholder={labels.placeholder}
|
|
351
492
|
addMenu={addMenuGroups}
|
|
352
493
|
onAddMenuSelect={handleAddMenuSelect}
|
|
353
|
-
attachments={
|
|
354
|
-
id: photo.id,
|
|
355
|
-
name: photo.name,
|
|
356
|
-
kind: 'image' as const,
|
|
357
|
-
src: photo.uri,
|
|
358
|
-
}))}
|
|
494
|
+
attachments={pillAttachments}
|
|
359
495
|
onRemoveAttachment={(id) => setPhotos((prev) => prev.filter((p) => p.id !== id))}
|
|
360
496
|
voiceState={
|
|
361
497
|
stt.isTranscribing ? 'transcribing' : stt.isRecording ? 'recording' : 'idle'
|
|
@@ -366,32 +502,44 @@ function AliaChatContentBody({
|
|
|
366
502
|
</>
|
|
367
503
|
);
|
|
368
504
|
|
|
505
|
+
const hasMessages = messages.length > 0;
|
|
506
|
+
const headerNode = useMemo(
|
|
507
|
+
() => header?.({ hasMessages, clear, markState, agents }),
|
|
508
|
+
[header, hasMessages, clear, markState, agents],
|
|
509
|
+
);
|
|
510
|
+
const background = useMemo(
|
|
511
|
+
() => (
|
|
512
|
+
<AmbientField
|
|
513
|
+
layout="sheet"
|
|
514
|
+
waveAmplitude={wave.waveAmplitude}
|
|
515
|
+
agentState={wave.agentState}
|
|
516
|
+
intensity={ambientIntensity}
|
|
517
|
+
isDarkMode={isDarkMode}
|
|
518
|
+
/>
|
|
519
|
+
),
|
|
520
|
+
[wave.waveAmplitude, wave.agentState, ambientIntensity, isDarkMode],
|
|
521
|
+
);
|
|
522
|
+
|
|
369
523
|
return (
|
|
370
|
-
<
|
|
524
|
+
<AliaFlightsProvider value={flights}>
|
|
371
525
|
<AiChatContainer
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
526
|
+
header={headerNode}
|
|
527
|
+
composer={
|
|
528
|
+
bottomInset > 0 ? (
|
|
529
|
+
<View style={{ paddingBottom: bottomInset }}>{composer}</View>
|
|
530
|
+
) : (
|
|
531
|
+
composer
|
|
532
|
+
)
|
|
378
533
|
}
|
|
379
|
-
actions={actions?.({ hasMessages: messages.length > 0, clear })}
|
|
380
|
-
composer={composer}
|
|
381
534
|
// The thread scrolls behind the header and the composer, which fade in over it — as
|
|
382
535
|
// in the app's conversation. The wave is the container's background layer, between
|
|
383
536
|
// its surface and the turns.
|
|
384
537
|
floatingChrome
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
agentState={wave.agentState}
|
|
389
|
-
intensity={ambientIntensity}
|
|
390
|
-
isDarkMode={isDarkMode}
|
|
391
|
-
/>
|
|
392
|
-
}
|
|
538
|
+
// The sheet or screen around it owns the fill and the shape; the container adds neither.
|
|
539
|
+
surface={false}
|
|
540
|
+
background={background}
|
|
393
541
|
>
|
|
394
|
-
<View
|
|
542
|
+
<View className="flex-1">
|
|
395
543
|
{/* Welcome or Messages */}
|
|
396
544
|
<AliaChatMessageList
|
|
397
545
|
messages={messages}
|
|
@@ -402,28 +550,18 @@ function AliaChatContentBody({
|
|
|
402
550
|
onEditMessage={onEditMessage}
|
|
403
551
|
onThumbsUp={onThumbsUp}
|
|
404
552
|
onThumbsDown={onThumbsDown}
|
|
405
|
-
onApprovePlan={onApprovePlan}
|
|
406
|
-
onRejectPlan={onRejectPlan}
|
|
407
|
-
onAnswerQuestions={(message) => send(message)}
|
|
553
|
+
onApprovePlan={onApprovePlan ?? approvePlan}
|
|
554
|
+
onRejectPlan={onRejectPlan ?? rejectPlan}
|
|
408
555
|
renderMarkdown={renderMarkdown}
|
|
409
556
|
welcomeComponent={welcomeComponent}
|
|
410
557
|
/>
|
|
411
558
|
</View>
|
|
412
559
|
</AiChatContainer>
|
|
413
|
-
|
|
560
|
+
{sharedFlights ? null : <AvatarFlightLayer flights={ownFlights} />}
|
|
561
|
+
</AliaFlightsProvider>
|
|
414
562
|
);
|
|
415
563
|
}
|
|
416
564
|
|
|
417
|
-
const styles = StyleSheet.create({
|
|
418
|
-
container: {
|
|
419
|
-
flex: 1,
|
|
420
|
-
position: 'relative',
|
|
421
|
-
},
|
|
422
|
-
body: {
|
|
423
|
-
flex: 1,
|
|
424
|
-
},
|
|
425
|
-
});
|
|
426
|
-
|
|
427
565
|
/** The chat: header, thread and composer, with the host's labels in reach of all of it. */
|
|
428
566
|
export function AliaChatContent({ labels, ...props }: AliaChatContentProps) {
|
|
429
567
|
return (
|