@alia.onl/sdk 13.1.0 → 15.2.1
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 +190 -0
- package/README.md +83 -20
- package/package.json +8 -4
- package/src/components/AliaAvatarIcon.tsx +30 -0
- package/src/components/AliaChatContent.tsx +57 -107
- package/src/components/AliaChatMessageList.tsx +2 -10
- package/src/components/AliaChatScreen.tsx +28 -25
- package/src/components/AliaChatSheet.tsx +30 -8
- package/src/components/AliaComposer.tsx +382 -0
- package/src/components/ComposerDropZone.tsx +235 -0
- package/src/components/voice/VoiceControls.tsx +28 -4
- package/src/components/voice/VoiceSession.tsx +23 -7
- package/src/hooks/useAliaActivity.ts +31 -0
- package/src/hooks/useAliaChat.ts +14 -3
- package/src/hooks/useAmbientWave.ts +3 -3
- package/src/hooks/useAttachmentIntake.ts +401 -0
- package/src/hooks/useAudioLevelMonitor.ts +2 -2
- package/src/hooks/useDictation.ts +295 -0
- package/src/hooks/useFocusWhenPresented.ts +41 -0
- package/src/hooks/usePendingQuestions.ts +19 -0
- package/src/hooks/useTTS.ts +24 -63
- package/src/hooks/useVoiceRoom.ts +148 -671
- package/src/index.ts +19 -3
- package/src/lib/activity-socket.ts +102 -0
- package/src/lib/activity-store.ts +53 -0
- package/src/lib/api-url.ts +2 -0
- package/src/lib/attachment-intake.ts +154 -0
- package/src/lib/attachment-tiles.ts +74 -0
- package/src/lib/audio-level.ts +2 -2
- package/src/lib/dictation-recorder-types.ts +37 -0
- package/src/lib/dictation-recorder.native.ts +90 -0
- package/src/lib/dictation-recorder.ts +131 -0
- package/src/lib/dictation.ts +208 -0
- package/src/lib/labels.tsx +50 -1
- package/src/lib/pcm.ts +57 -0
- package/src/lib/speech-language.ts +1 -1
- package/src/lib/speech-messages.ts +43 -65
- package/src/lib/speech-synthesis.ts +104 -9
- package/src/lib/turn-state.ts +15 -0
- package/src/lib/voice-audio-engine-types.ts +59 -0
- package/src/lib/voice-audio-engine.native.ts +232 -0
- package/src/lib/voice-audio-engine.ts +350 -0
- package/src/lib/voice-call.ts +539 -0
- package/src/lib/voice-levels.ts +7 -3
- package/src/lib/voice-protocol.ts +309 -0
- package/src/tools.ts +1 -0
- package/src/types.ts +63 -4
- package/src/voice.ts +31 -10
- package/tsconfig.json +2 -0
- package/src/hooks/useSpeechToText.ts +0 -268
- package/src/lib/speech-recognition-types.ts +0 -108
- package/src/lib/speech-recognition.native.ts +0 -237
- package/src/lib/speech-recognition.ts +0 -308
- package/src/lib/voice-text.ts +0 -198
- package/src/lib/voice-turn.ts +0 -89
|
@@ -2,39 +2,37 @@
|
|
|
2
2
|
* Shared chat content component used by both AliaChatSheet and AliaChatScreen.
|
|
3
3
|
*
|
|
4
4
|
* Owns the text chat; voice is an injected capability (`voiceSession`) so that
|
|
5
|
-
* nothing here reaches
|
|
5
|
+
* nothing here reaches the call or its audio engine at module-evaluation time. Containers
|
|
6
6
|
* only handle layout chrome (sheet modal, full-screen SafeAreaView). The
|
|
7
7
|
* breadcrumb header is Bloom's `AiChatContainer`, the one the app's conversation uses.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React, { Suspense, useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
11
|
-
import { View } from 'react-native';
|
|
11
|
+
import { View, type TextInput } from 'react-native';
|
|
12
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
13
|
+
import { turnActivityState } from '../lib/turn-state';
|
|
12
14
|
import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
|
|
13
15
|
import { useTTS } from '../hooks/useTTS';
|
|
14
|
-
import {
|
|
16
|
+
import { useFocusWhenPresented } from '../hooks/useFocusWhenPresented';
|
|
17
|
+
import { usePendingQuestions } from '../hooks/usePendingQuestions';
|
|
15
18
|
import { useImagePicker } from '../hooks/useImagePicker';
|
|
16
19
|
import { imageUriToDataUrl } from '../lib/image-data-url';
|
|
17
20
|
import { useAmbientWave } from '../hooks/useAmbientWave';
|
|
18
21
|
import { AmbientField } from './AmbientField';
|
|
19
|
-
import { QuestionCard } from './QuestionCard';
|
|
20
22
|
import { getAccountDisplayName } from '@oxy.so/core';
|
|
21
23
|
import { useOxy } from '@oxy.so/services/ui/client';
|
|
22
|
-
import { AgentAvatarProvider } from '@oxy.so/bloom/agent-avatar';
|
|
23
|
-
import { ALIA_CHARACTER_RUNTIME_URL } from '../lib/character-runtime';
|
|
24
|
-
import { pendingQuestions } from '../lib/questions';
|
|
25
24
|
import { AliaChatMessageList } from './AliaChatMessageList';
|
|
26
25
|
import { AliaFlightsProvider, ALIA_TO_PROFILE } from '../lib/flights';
|
|
27
26
|
import { AliaWelcomeMessage } from './AliaWelcomeMessage';
|
|
28
|
-
import {
|
|
27
|
+
import { AliaComposer } from './AliaComposer';
|
|
29
28
|
import { useComposerSuggestions, useSuggestionQuery } from '../hooks/useComposerSuggestions';
|
|
30
|
-
import { ComposerLoader } from '@oxy.so/bloom/composer-loader';
|
|
31
29
|
import { AiChatContainer } from '@oxy.so/bloom/ai-chat';
|
|
32
30
|
import {
|
|
33
31
|
AvatarFlightLayer,
|
|
34
32
|
useAvatarFlights,
|
|
35
33
|
type AvatarFlights,
|
|
36
34
|
} from '@oxy.so/bloom/avatar-flight';
|
|
37
|
-
import {
|
|
35
|
+
import type { ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
|
|
38
36
|
import { RiImageLine } from '@oxy.so/bloom/icons';
|
|
39
37
|
import { toast } from '@oxy.so/bloom/toast';
|
|
40
38
|
import { Button } from '@oxy.so/bloom/button';
|
|
@@ -42,6 +40,7 @@ import { type AliaChatLabels, AliaChatLabelsProvider, useLabels } from '../lib/l
|
|
|
42
40
|
import type { AliaAvatarState } from './AliaAvatar';
|
|
43
41
|
import { VoiceModeIcon } from './VoiceModeIcon';
|
|
44
42
|
import type {
|
|
43
|
+
AliaAttachment,
|
|
45
44
|
AliaSuggestion,
|
|
46
45
|
ChatMessage,
|
|
47
46
|
VoiceMessage,
|
|
@@ -137,14 +136,21 @@ export interface AliaChatContentProps {
|
|
|
137
136
|
flights?: AvatarFlights;
|
|
138
137
|
/** A turn sent once as this chat mounts: the conversation a sheet was opened for. */
|
|
139
138
|
initialTurn?: AliaTurn;
|
|
139
|
+
/**
|
|
140
|
+
* The `animatedProgress` of the Bloom sheet this chat is in. The composer takes focus (and on
|
|
141
|
+
* native opens the keyboard) once it reads `1`, so the person can type as soon as the sheet is
|
|
142
|
+
* up. Omitted, the chat is not in a sheet and focus is left alone.
|
|
143
|
+
*/
|
|
144
|
+
presentedProgress?: SharedValue<number>;
|
|
140
145
|
/** Internal sheet lifecycle bridge: receives the live request canceller. */
|
|
141
146
|
onStopReady?: (stop: (() => void) | null) => void;
|
|
142
147
|
/** Overrides for the chat's own copy; anything omitted is English. */
|
|
143
148
|
labels?: Partial<AliaChatLabels>;
|
|
144
|
-
/** Where the 3D avatar's runtime is served from; omitted, Alia's shared copy (`ALIA_CHARACTER_RUNTIME_URL`). */
|
|
145
|
-
characterRuntimeUrl?: string;
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
/** The chat API carries photos (`image_url` parts) and nothing else. */
|
|
152
|
+
const PHOTOS_ONLY = ['image'] as const;
|
|
153
|
+
|
|
148
154
|
/** The add menu: photos are the one attachment the chat API carries (`image_url` parts). */
|
|
149
155
|
function addMenu(labels: AliaChatLabels): ComposerPanelAddMenuGroup[] {
|
|
150
156
|
return [
|
|
@@ -192,6 +198,7 @@ function AliaChatContentBody({
|
|
|
192
198
|
agents: knownAgents,
|
|
193
199
|
flights: sharedFlights,
|
|
194
200
|
initialTurn,
|
|
201
|
+
presentedProgress,
|
|
195
202
|
onStopReady,
|
|
196
203
|
}: AliaChatContentProps) {
|
|
197
204
|
const ownFlights = useAvatarFlights();
|
|
@@ -212,15 +219,7 @@ function AliaChatContentBody({
|
|
|
212
219
|
const { messages, setMessages, send, isStreaming, stop, clear, approvePlan, rejectPlan } =
|
|
213
220
|
useAliaChat(chatOptions);
|
|
214
221
|
const agents = useMemo(() => participatingAgents(messages), [messages]);
|
|
215
|
-
|
|
216
|
-
// not in the thread. Any later message, typed or sent from the card, is the answer.
|
|
217
|
-
const lastMessage = messages.at(-1);
|
|
218
|
-
const questionsAsked = useMemo(
|
|
219
|
-
() => pendingQuestions(messages, isStreaming),
|
|
220
|
-
[messages, isStreaming],
|
|
221
|
-
);
|
|
222
|
-
const [dismissedQuestionsOf, setDismissedQuestionsOf] = useState<string | null>(null);
|
|
223
|
-
const asking = questionsAsked.length > 0 && dismissedQuestionsOf !== lastMessage?.id;
|
|
222
|
+
const asked = usePendingQuestions(messages, isStreaming);
|
|
224
223
|
|
|
225
224
|
// The first turn of an empty chat goes to the host when it asked for it; every other turn is sent here.
|
|
226
225
|
const startsElsewhere = onStart !== undefined && messages.length === 0;
|
|
@@ -258,9 +257,7 @@ function AliaChatContentBody({
|
|
|
258
257
|
const [inputValue, setInputValue] = useState('');
|
|
259
258
|
|
|
260
259
|
// Picked photos wait here, as tiles above the field, until the turn is sent.
|
|
261
|
-
const [photos, setPhotos] = useState<
|
|
262
|
-
Array<{ id: string; name: string; uri: string; mimeType?: string }>
|
|
263
|
-
>([]);
|
|
260
|
+
const [photos, setPhotos] = useState<AliaAttachment[]>([]);
|
|
264
261
|
const { pickImage } = useImagePicker();
|
|
265
262
|
|
|
266
263
|
const handleAddMenuSelect = useCallback(
|
|
@@ -272,8 +269,10 @@ function AliaChatContentBody({
|
|
|
272
269
|
...prev,
|
|
273
270
|
...assets.map((asset) => ({
|
|
274
271
|
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
272
|
+
type: 'image' as const,
|
|
275
273
|
name: asset.name,
|
|
276
274
|
uri: asset.uri,
|
|
275
|
+
size: asset.size,
|
|
277
276
|
mimeType: asset.mimeType,
|
|
278
277
|
})),
|
|
279
278
|
]);
|
|
@@ -323,24 +322,6 @@ function AliaChatContentBody({
|
|
|
323
322
|
[submitTurn, photos, reportError],
|
|
324
323
|
);
|
|
325
324
|
|
|
326
|
-
// ── Dictation: the pill's mic toggles it, the transcript lands in the field ──
|
|
327
|
-
const stt = useSpeechToText();
|
|
328
|
-
useEffect(() => {
|
|
329
|
-
if (stt.error) reportError(stt.error);
|
|
330
|
-
}, [stt.error, reportError]);
|
|
331
|
-
const sttRef = useRef(stt);
|
|
332
|
-
sttRef.current = stt;
|
|
333
|
-
const handleListeningChange = useCallback(async (listening: boolean) => {
|
|
334
|
-
const current = sttRef.current;
|
|
335
|
-
if (listening) {
|
|
336
|
-
if (!current.isRecording && !current.isTranscribing) current.startRecording();
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
if (!current.isRecording) return;
|
|
340
|
-
const text = await current.stopAndTranscribe();
|
|
341
|
-
if (text) setInputValue((prev) => (prev ? `${prev} ${text}` : text));
|
|
342
|
-
}, []);
|
|
343
|
-
|
|
344
325
|
// ── TTS ──
|
|
345
326
|
const tts = useTTS({ apiUrl });
|
|
346
327
|
|
|
@@ -397,12 +378,7 @@ function AliaChatContentBody({
|
|
|
397
378
|
if (voiceAgentState === 'thinking') return 'thinking';
|
|
398
379
|
return 'idle';
|
|
399
380
|
}
|
|
400
|
-
|
|
401
|
-
const lastMsg = messages[messages.length - 1];
|
|
402
|
-
if (lastMsg?.role === 'assistant' && lastMsg.toolInvocations?.some((t) => t.state === 'call'))
|
|
403
|
-
return 'working';
|
|
404
|
-
if (lastMsg?.thinking) return 'thinking';
|
|
405
|
-
return 'writing';
|
|
381
|
+
return turnActivityState(messages, isStreaming);
|
|
406
382
|
}, [messages, isStreaming, isVoiceActive, voiceAgentState]);
|
|
407
383
|
|
|
408
384
|
const { user } = useOxy();
|
|
@@ -452,16 +428,8 @@ function AliaChatContentBody({
|
|
|
452
428
|
[VoiceSession, activateVoice, labels.startVoice],
|
|
453
429
|
);
|
|
454
430
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
photos.map((photo) => ({
|
|
458
|
-
id: photo.id,
|
|
459
|
-
name: photo.name,
|
|
460
|
-
kind: 'image' as const,
|
|
461
|
-
src: photo.uri,
|
|
462
|
-
})),
|
|
463
|
-
[photos],
|
|
464
|
-
);
|
|
431
|
+
const composerRef = useRef<TextInput | null>(null);
|
|
432
|
+
useFocusWhenPresented(presentedProgress, () => composerRef.current?.focus());
|
|
465
433
|
|
|
466
434
|
const composer =
|
|
467
435
|
VoiceSession && isVoiceActive ? (
|
|
@@ -470,45 +438,33 @@ function AliaChatContentBody({
|
|
|
470
438
|
<VoiceSession apiUrl={apiUrl} onStateChange={setVoiceState} onEnd={deactivateVoice} />
|
|
471
439
|
</Suspense>
|
|
472
440
|
) : (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
{
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
placeholder={labels.placeholder}
|
|
501
|
-
addMenu={addMenuGroups}
|
|
502
|
-
onAddMenuSelect={handleAddMenuSelect}
|
|
503
|
-
attachments={pillAttachments}
|
|
504
|
-
onRemoveAttachment={(id) => setPhotos((prev) => prev.filter((p) => p.id !== id))}
|
|
505
|
-
voiceState={
|
|
506
|
-
stt.isTranscribing ? 'transcribing' : stt.isRecording ? 'recording' : 'idle'
|
|
507
|
-
}
|
|
508
|
-
onVoiceStateChange={(next) => void handleListeningChange(next === 'recording')}
|
|
509
|
-
/>
|
|
510
|
-
</ComposerLoader>
|
|
511
|
-
</>
|
|
441
|
+
<AliaComposer
|
|
442
|
+
apiUrl={apiUrl}
|
|
443
|
+
inputRef={composerRef}
|
|
444
|
+
disableKeyboardAvoidance
|
|
445
|
+
onKeyPress={suggestions.onKeyPress}
|
|
446
|
+
suggestions={{ ...suggestions, onPick: handlePickSuggestion }}
|
|
447
|
+
// Asked beside the field, not in the thread: the card docks above the composer.
|
|
448
|
+
questions={{
|
|
449
|
+
items: asked.questions,
|
|
450
|
+
onSubmit: (message) => send(message),
|
|
451
|
+
onDismiss: asked.dismiss,
|
|
452
|
+
}}
|
|
453
|
+
value={inputValue}
|
|
454
|
+
onValueChange={setInputValue}
|
|
455
|
+
onSubmit={() => void handleSubmit(inputValue)}
|
|
456
|
+
busy={isStreaming}
|
|
457
|
+
onStop={stop}
|
|
458
|
+
emptyAction={voiceActivateButton}
|
|
459
|
+
placeholder={labels.placeholder}
|
|
460
|
+
addMenu={addMenuGroups}
|
|
461
|
+
onAddMenuSelect={handleAddMenuSelect}
|
|
462
|
+
attachments={photos}
|
|
463
|
+
onAddAttachment={(photo) => setPhotos((prev) => [...prev, photo])}
|
|
464
|
+
onRemoveAttachment={(id) => setPhotos((prev) => prev.filter((p) => p.id !== id))}
|
|
465
|
+
attachmentKinds={PHOTOS_ONLY}
|
|
466
|
+
onError={onError}
|
|
467
|
+
/>
|
|
512
468
|
);
|
|
513
469
|
|
|
514
470
|
const hasMessages = messages.length > 0;
|
|
@@ -572,16 +528,10 @@ function AliaChatContentBody({
|
|
|
572
528
|
}
|
|
573
529
|
|
|
574
530
|
/** The chat: header, thread and composer, with the host's labels in reach of all of it. */
|
|
575
|
-
export function AliaChatContent({
|
|
576
|
-
labels,
|
|
577
|
-
characterRuntimeUrl = ALIA_CHARACTER_RUNTIME_URL,
|
|
578
|
-
...props
|
|
579
|
-
}: AliaChatContentProps) {
|
|
531
|
+
export function AliaChatContent({ labels, ...props }: AliaChatContentProps) {
|
|
580
532
|
return (
|
|
581
|
-
<
|
|
582
|
-
<
|
|
583
|
-
|
|
584
|
-
</AliaChatLabelsProvider>
|
|
585
|
-
</AgentAvatarProvider>
|
|
533
|
+
<AliaChatLabelsProvider labels={labels}>
|
|
534
|
+
<AliaChatContentBody {...props} />
|
|
535
|
+
</AliaChatLabelsProvider>
|
|
586
536
|
);
|
|
587
537
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
-
import { View,
|
|
3
|
-
import Animated
|
|
4
|
-
useSharedValue,
|
|
5
|
-
useAnimatedStyle,
|
|
6
|
-
withRepeat,
|
|
7
|
-
withTiming,
|
|
8
|
-
withSequence,
|
|
9
|
-
cancelAnimation,
|
|
10
|
-
} from 'react-native-reanimated';
|
|
2
|
+
import { View, Platform } from 'react-native';
|
|
3
|
+
import Animated from 'react-native-reanimated';
|
|
11
4
|
import { Textarea } from '@oxy.so/bloom/textarea';
|
|
12
5
|
import { TaskList } from '@oxy.so/bloom/task-list';
|
|
13
6
|
import { Text } from '@oxy.so/bloom/typography';
|
|
@@ -41,7 +34,6 @@ import { AskedQuestions } from './QuestionCard';
|
|
|
41
34
|
import { askedQuestions, isQuestionInvocation } from '../lib/questions';
|
|
42
35
|
import { extractCitationSources, linkifyCitations } from '../lib/citations';
|
|
43
36
|
import { chatMessageEnter } from '../animations';
|
|
44
|
-
import { getToolLabel, getToolActiveLabel, getResearchActiveLabel } from '../lib/tool-registry';
|
|
45
37
|
import { getTextFromContent, getImagesFromContent } from '../lib/content-utils';
|
|
46
38
|
import type { ChatMessage } from '../types';
|
|
47
39
|
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Voice is opt-in — pass `voiceSession={VoiceSession}` from
|
|
16
16
|
* `@alia.onl/sdk/voice` to offer calls. Without it the screen is text-only and
|
|
17
|
-
* `
|
|
17
|
+
* `react-native-audio-api` never enters the bundle.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import React, { useCallback } from 'react';
|
|
21
21
|
import { View } from 'react-native';
|
|
22
22
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
23
|
+
import { AgentAvatarProvider } from '@oxy.so/bloom/agent-avatar';
|
|
24
|
+
import { ALIA_CHARACTER_RUNTIME_URL } from '../lib/character-runtime';
|
|
23
25
|
import { KeyboardAvoidingView } from '../lib/keyboard';
|
|
24
26
|
import { AliaChatContent, type AliaChatContentProps } from './AliaChatContent';
|
|
25
27
|
import { AliaChatHeader } from './AliaChatHeader';
|
|
@@ -84,7 +86,7 @@ export function AliaChatScreen({
|
|
|
84
86
|
isDarkMode,
|
|
85
87
|
voiceSession,
|
|
86
88
|
labels,
|
|
87
|
-
characterRuntimeUrl,
|
|
89
|
+
characterRuntimeUrl = ALIA_CHARACTER_RUNTIME_URL,
|
|
88
90
|
}: AliaChatScreenProps) {
|
|
89
91
|
const insets = useSafeAreaInsets();
|
|
90
92
|
// The screen owns the whole window, so it keeps its composer out of both
|
|
@@ -114,28 +116,29 @@ export function AliaChatScreen({
|
|
|
114
116
|
);
|
|
115
117
|
|
|
116
118
|
return (
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
119
|
+
<AgentAvatarProvider runtimeUrl={characterRuntimeUrl}>
|
|
120
|
+
<View
|
|
121
|
+
className="flex-1 bg-background-full"
|
|
122
|
+
style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
|
|
123
|
+
>
|
|
124
|
+
<KeyboardAvoidingView behavior="padding" automaticOffset style={{ flex: 1 }}>
|
|
125
|
+
<AliaChatContent
|
|
126
|
+
clientContext={clientContext}
|
|
127
|
+
model={model}
|
|
128
|
+
apiUrl={apiUrl}
|
|
129
|
+
welcomeGreeting={welcomeGreeting}
|
|
130
|
+
welcomeSubtitle={welcomeSubtitle}
|
|
131
|
+
welcomeSuggestions={welcomeSuggestions}
|
|
132
|
+
agents={agents}
|
|
133
|
+
suggestionMatches={suggestionMatches}
|
|
134
|
+
onSuggestionQuery={onSuggestionQuery}
|
|
135
|
+
isDarkMode={isDarkMode}
|
|
136
|
+
voiceSession={voiceSession}
|
|
137
|
+
labels={labels}
|
|
138
|
+
header={header}
|
|
139
|
+
/>
|
|
140
|
+
</KeyboardAvoidingView>
|
|
141
|
+
</View>
|
|
142
|
+
</AgentAvatarProvider>
|
|
140
143
|
);
|
|
141
144
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { AgentAvatarProvider } from '@oxy.so/bloom/agent-avatar';
|
|
1
2
|
import { AvatarFlightLayer, useAvatarFlights } from '@oxy.so/bloom/avatar-flight';
|
|
2
3
|
import { BottomSheet, type BottomSheetRef } from '@oxy.so/bloom/bottom-sheet';
|
|
3
4
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
4
5
|
import { useTheme } from '@oxy.so/bloom/theme';
|
|
5
6
|
import { useWindowDimensions, View } from 'react-native';
|
|
7
|
+
import { useSharedValue } from 'react-native-reanimated';
|
|
6
8
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
7
9
|
import { BREAKPOINTS } from '@oxy.so/bloom/styles';
|
|
10
|
+
import { ALIA_CHARACTER_RUNTIME_URL } from '../lib/character-runtime';
|
|
8
11
|
import type { AliaChatLabels } from '../lib/labels';
|
|
9
12
|
import type { VoiceSessionComponent } from '../types';
|
|
10
13
|
import { AliaChatContent, type AliaChatContentProps, type AliaTurn } from './AliaChatContent';
|
|
@@ -46,7 +49,7 @@ export interface AliaChatSheetProps {
|
|
|
46
49
|
isDarkMode?: boolean;
|
|
47
50
|
/**
|
|
48
51
|
* Voice capability — pass `VoiceSession` from `@alia.onl/sdk/voice` to offer
|
|
49
|
-
* voice calls. Omitted, the sheet is text-only and `
|
|
52
|
+
* voice calls. Omitted, the sheet is text-only and `react-native-audio-api` never
|
|
50
53
|
* enters the bundle.
|
|
51
54
|
*/
|
|
52
55
|
voiceSession?: VoiceSessionComponent;
|
|
@@ -61,7 +64,12 @@ export interface AliaChatSheetProps {
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
export interface AliaChatSheetRef {
|
|
64
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Opens the sheet. Given a `turn`, the conversation opens over it with that message already
|
|
69
|
+
* sent (an "ask Alia about this" button: a profile, a post, a file); without one, the sheet
|
|
70
|
+
* opens on its welcome, with the composer focused.
|
|
71
|
+
*/
|
|
72
|
+
present: (turn?: AliaTurn) => void;
|
|
65
73
|
dismiss: () => void;
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -80,7 +88,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
80
88
|
isDarkMode,
|
|
81
89
|
voiceSession,
|
|
82
90
|
labels,
|
|
83
|
-
characterRuntimeUrl,
|
|
91
|
+
characterRuntimeUrl = ALIA_CHARACTER_RUNTIME_URL,
|
|
84
92
|
onShare,
|
|
85
93
|
headerMenu,
|
|
86
94
|
},
|
|
@@ -88,6 +96,9 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
88
96
|
) => {
|
|
89
97
|
// Bloom's resolved mode, not the system's scheme.
|
|
90
98
|
const { isDark } = useTheme();
|
|
99
|
+
// How far each sheet is presented (0 closed, 1 up): its chat takes the composer's focus at 1.
|
|
100
|
+
const mainProgress = useSharedValue(0);
|
|
101
|
+
const conversationProgress = useSharedValue(0);
|
|
91
102
|
const mainRef = useRef<BottomSheetRef>(null);
|
|
92
103
|
const conversationRef = useRef<BottomSheetRef>(null);
|
|
93
104
|
// Each sheet's chat registers its own canceller; closing a sheet stops its own turn.
|
|
@@ -131,8 +142,14 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
131
142
|
|
|
132
143
|
useImperativeHandle(
|
|
133
144
|
ref,
|
|
134
|
-
() => ({
|
|
135
|
-
|
|
145
|
+
() => ({
|
|
146
|
+
present: (turn) => {
|
|
147
|
+
mainRef.current?.present();
|
|
148
|
+
if (turn) startConversation(turn);
|
|
149
|
+
},
|
|
150
|
+
dismiss: handleDismiss,
|
|
151
|
+
}),
|
|
152
|
+
[handleDismiss, startConversation],
|
|
136
153
|
);
|
|
137
154
|
|
|
138
155
|
// Closing the main sheet (a drag, backdrop press, Back or Escape) closes what is stacked on it.
|
|
@@ -162,16 +179,18 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
162
179
|
voiceSession,
|
|
163
180
|
bottomInset: detached ? 0 : bottom,
|
|
164
181
|
labels,
|
|
165
|
-
characterRuntimeUrl,
|
|
166
182
|
};
|
|
167
183
|
|
|
184
|
+
// The provider wraps the sheets AND the flight layer: both draw avatars, and an avatar drawn
|
|
185
|
+
// outside it would fall back to the host's own `/bloom-character/runtime.mjs`.
|
|
168
186
|
return (
|
|
169
|
-
|
|
187
|
+
<AgentAvatarProvider runtimeUrl={characterRuntimeUrl}>
|
|
170
188
|
<BottomSheet
|
|
171
189
|
ref={mainRef}
|
|
172
190
|
detached={detached}
|
|
173
191
|
scrollable={false}
|
|
174
192
|
manualActivation
|
|
193
|
+
animatedProgress={mainProgress}
|
|
175
194
|
onDismissAttempt={onMainDismissAttempt}
|
|
176
195
|
onDismiss={() => setConversation(null)}
|
|
177
196
|
style={sheetStyle}
|
|
@@ -186,6 +205,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
186
205
|
agents={agents}
|
|
187
206
|
suggestionMatches={suggestionMatches}
|
|
188
207
|
onSuggestionQuery={onSuggestionQuery}
|
|
208
|
+
presentedProgress={mainProgress}
|
|
189
209
|
onStopReady={registerMainStop}
|
|
190
210
|
onStart={startConversation}
|
|
191
211
|
/>
|
|
@@ -198,6 +218,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
198
218
|
detached={detached}
|
|
199
219
|
scrollable={false}
|
|
200
220
|
manualActivation
|
|
221
|
+
animatedProgress={conversationProgress}
|
|
201
222
|
onDismissAttempt={onConversationDismissAttempt}
|
|
202
223
|
onDismiss={() => setConversation(null)}
|
|
203
224
|
style={sheetStyle}
|
|
@@ -205,6 +226,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
205
226
|
<AliaChatContent
|
|
206
227
|
{...shared}
|
|
207
228
|
initialTurn={conversation.turn}
|
|
229
|
+
presentedProgress={conversationProgress}
|
|
208
230
|
agents={agents}
|
|
209
231
|
onStopReady={registerConversationStop}
|
|
210
232
|
// The bar is the conversation's: back to the main screen, the profile, share and
|
|
@@ -225,7 +247,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
225
247
|
</BottomSheet>
|
|
226
248
|
) : null}
|
|
227
249
|
<AvatarFlightLayer flights={flights} />
|
|
228
|
-
|
|
250
|
+
</AgentAvatarProvider>
|
|
229
251
|
);
|
|
230
252
|
},
|
|
231
253
|
);
|