@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/package.json +5 -4
  3. package/src/components/AliaAvatar.tsx +16 -20
  4. package/src/components/AliaChatContent.tsx +234 -96
  5. package/src/components/AliaChatHeader.tsx +102 -76
  6. package/src/components/AliaChatMessageList.tsx +174 -134
  7. package/src/components/AliaChatScreen.tsx +45 -23
  8. package/src/components/AliaChatSheet.tsx +151 -50
  9. package/src/components/AliaWelcomeMessage.tsx +26 -53
  10. package/src/components/AmbientField.tsx +19 -16
  11. package/src/components/ComposerSuggestions.tsx +45 -0
  12. package/src/components/IdentityMark.tsx +3 -1
  13. package/src/components/Markdown.tsx +18 -24
  14. package/src/components/QuestionCard.tsx +35 -33
  15. package/src/components/ThinkingBlock.tsx +9 -13
  16. package/src/components/VoiceModeIcon.tsx +15 -7
  17. package/src/components/voice/VoiceControls.tsx +17 -9
  18. package/src/hooks/useAliaChat.ts +124 -5
  19. package/src/hooks/useComposerSuggestions.ts +84 -0
  20. package/src/index.ts +10 -10
  21. package/src/lib/agent-character.ts +28 -0
  22. package/src/lib/chat-stream.ts +25 -3
  23. package/src/lib/citations.ts +62 -10
  24. package/src/lib/delegation.ts +34 -0
  25. package/src/lib/domain.ts +12 -0
  26. package/src/lib/flights.ts +24 -0
  27. package/src/lib/greeting.ts +15 -0
  28. package/src/lib/image-data-url.ts +26 -5
  29. package/src/lib/labels.tsx +10 -2
  30. package/src/lib/questions.ts +15 -1
  31. package/src/lib/suggestions.ts +53 -0
  32. package/src/lib/task-labels.ts +7 -39
  33. package/src/lib/use-after-delay.ts +19 -0
  34. package/src/lib/work-log.ts +3 -9
  35. package/src/tools.ts +8 -1
  36. 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
- createContext,
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 { AliaWelcomeMessage, type WelcomeSuggestion } from './AliaWelcomeMessage';
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 { AliaAvatar, type AliaAvatarState } from './AliaAvatar';
38
+ import type { AliaAvatarState } from './AliaAvatar';
38
39
  import { VoiceModeIcon } from './VoiceModeIcon';
39
- import type { ChatMessage, VoiceMessage, VoiceSessionComponent, VoiceSessionState } from '../types';
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
- welcomeSuggestions?: WelcomeSuggestion[];
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
- * Above the breadcrumb row — a back button, a menu, or a whole custom header
63
- * (given `title={null}`). A function receives the clear handler.
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
- headerLeading?:
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
- title = 'Alia',
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 } = useAliaChat(chatOptions);
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<Array<{ id: string; name: string; uri: string }>>([]);
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(pending.map((photo) => imageUriToDataUrl(photo.uri)));
194
- send(text, { images });
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
- [send, photos, reportError],
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 = [...messages];
359
+ textSnapshotRef.current = [...messagesRef.current];
243
360
  setIsVoiceActive(true);
244
- }, [isVoiceActive, messages]);
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
- tts.readAloud(messageId, text);
381
+ readAloud(messageId, text);
264
382
  },
265
- [tts],
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
- <AliaWelcomeMessage
288
- greeting={welcomeGreeting}
289
- subtitle={welcomeSubtitle}
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, welcomeSuggestions, send, messages.length]);
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={photos.map((photo) => ({
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
- <MarkStateContext.Provider value={markState}>
524
+ <AliaFlightsProvider value={flights}>
371
525
  <AiChatContainer
372
- project={title ?? undefined}
373
- projectIcon={BreadcrumbMark}
374
- header={
375
- typeof headerLeading === 'function'
376
- ? headerLeading({ hasMessages: messages.length > 0, clear })
377
- : headerLeading
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
- background={
386
- <AmbientField
387
- waveAmplitude={wave.waveAmplitude}
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 style={styles.body}>
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
- </MarkStateContext.Provider>
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 (