@alia.onl/sdk 13.1.0 → 13.2.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 +61 -0
- package/README.md +31 -0
- package/package.json +1 -1
- package/src/components/AliaChatContent.tsx +53 -100
- package/src/components/AliaChatScreen.tsx +27 -24
- package/src/components/AliaChatSheet.tsx +15 -4
- package/src/components/AliaComposer.tsx +336 -0
- package/src/components/ComposerDropZone.tsx +235 -0
- package/src/hooks/useAttachmentIntake.ts +401 -0
- package/src/hooks/useFocusWhenPresented.ts +41 -0
- package/src/hooks/usePendingQuestions.ts +19 -0
- package/src/hooks/useVoiceRoom.ts +81 -16
- package/src/index.ts +10 -0
- package/src/lib/attachment-intake.ts +154 -0
- package/src/lib/attachment-tiles.ts +74 -0
- package/src/lib/labels.tsx +47 -1
- package/src/lib/speech-messages.ts +34 -2
- package/src/lib/speech-synthesis.ts +99 -8
- package/src/tools.ts +1 -0
- package/src/types.ts +14 -0
- package/src/voice.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# @alia.onl/sdk
|
|
2
2
|
|
|
3
|
+
## 13.1.1
|
|
4
|
+
|
|
5
|
+
- **The avatar flights load the runtime too.** 13.1.0 mounted the character runtime provider inside
|
|
6
|
+
the chat, but the sheet's avatar-flight layer (the avatar that flies from the welcome into the
|
|
7
|
+
header) is drawn outside it, so it asked the host for its own `/bloom-character/runtime.mjs` and got
|
|
8
|
+
the app's HTML back ("Expected a JavaScript-or-Wasm module script but the server responded with
|
|
9
|
+
a MIME type of text/html"). `AliaChatSheet` and `AliaChatScreen` now wrap everything they draw in the
|
|
10
|
+
provider, once; `AliaChatContent` no longer mounts one (it is not an entry point on its own).
|
|
11
|
+
|
|
12
|
+
## 13.2.0
|
|
13
|
+
|
|
14
|
+
One composer for the sheet, the screen and the Alia app: the app's composer moved here and the
|
|
15
|
+
app consumes it.
|
|
16
|
+
|
|
17
|
+
- **`AliaComposer`** (exported) is Bloom's `ComposerPanel` plus what every Alia surface needs the
|
|
18
|
+
same way: files by paste and drop (read with real progress, retried, refused with a sentence),
|
|
19
|
+
dictation, the suggestions over the field and the questions Alia asked, docked above it. What
|
|
20
|
+
differs per surface is a prop: `addMenu` / `onAddMenuSelect`, `modes` / `mode` /
|
|
21
|
+
`onModeChange` (a power-level selector), `attachments` / `onAddAttachment` /
|
|
22
|
+
`onRemoveAttachment` (a composer given no `onAddAttachment` takes no paste or drop),
|
|
23
|
+
`attachmentKinds` (`['image']` refuses a document with a sentence), `suggestions`, `questions`,
|
|
24
|
+
`status`, `emptyAction`, `accessory`, `onKeyPress`, `speechLanguage`, `labels`, `onError`.
|
|
25
|
+
`AliaChatContent` (so the sheet and the screen) renders it with photos, voice and suggestions;
|
|
26
|
+
a host gets today's behaviour with no setup.
|
|
27
|
+
- **The sheet's composer is the panel, not the pill.** The pill has no mode selector, status tab
|
|
28
|
+
or question slot, which the Alia app needs; the sheet and the app now draw the same card. The
|
|
29
|
+
agent-working light (`ComposerLoader`) is not drawn around it, as in the app: a turn in flight
|
|
30
|
+
is its stop control and the thread's thinking line.
|
|
31
|
+
- **Photos are `AliaAttachment`s** (exported type): the chat's picked photos wait as the same
|
|
32
|
+
tiles a dropped or pasted image becomes, with the same remove and error states.
|
|
33
|
+
- **`AliaChatLabels` gains** `composerMessage`, `composerAddMenu`, `composerModes`,
|
|
34
|
+
`composerVoice`, `composerSend`, `composerStop`, `composerRemove`, `composerRetry`,
|
|
35
|
+
`composerUntitled`, `fileReadFailed`, `fileTooLarge`, `fileEmpty`, `fileUnsupported`,
|
|
36
|
+
`dropHint`, `dropUnavailable` and `voiceError(code)`, all with English defaults. A nested
|
|
37
|
+
`AliaChatLabelsProvider` now refines the labels already in reach instead of resetting them.
|
|
38
|
+
- **`usePendingQuestions(messages, isStreaming)`** (exported) is the "asked above the composer,
|
|
39
|
+
put away until she asks again" rule both the chat and the app used to keep a copy of.
|
|
40
|
+
- **`classifyIntake` / `MAX_ATTACHMENT_BYTES`** (from `@alia.onl/sdk/tools`) are the rules a file
|
|
41
|
+
passes to be attached.
|
|
42
|
+
- **The sheet focuses its composer once it has presented.** `AliaChatSheet` hands each sheet's
|
|
43
|
+
`animatedProgress` (Bloom's own "fully presented" signal, no timer) to its chat, whose composer
|
|
44
|
+
takes focus when it reads `1`: the person can type at once and the keyboard opens on native. It
|
|
45
|
+
focuses once per `present()` for the main sheet and for the conversation sheet over it; dragging
|
|
46
|
+
a sheet part-way and letting go does not take the focus again. `AliaChatContent` takes
|
|
47
|
+
`presentedProgress` for a host that builds its own sheet; the screen and the app's own screens
|
|
48
|
+
pass none and keep their focus. Not verified on a device.
|
|
49
|
+
|
|
50
|
+
A voice call that cannot speak ends and says why, instead of listening on in silence:
|
|
51
|
+
|
|
52
|
+
- **`requestSpeechClip` throws a `SpeechClipError`** (exported) with the route's HTTP `status`,
|
|
53
|
+
its typed `code` (`SPEECH_UNAVAILABLE`, `SPEECH_LIMIT_REACHED`, `SPEECH_REJECTED`,
|
|
54
|
+
`SPEECH_PROVIDER_FAILED`, `SPEECH_GENERATION_FAILED`, `INVALID_REQUEST`, `NOT_AUTHENTICATED`, or
|
|
55
|
+
`null`), `retryable` and `requestId`. An empty token is `NOT_AUTHENTICATED` without a request.
|
|
56
|
+
- **`useVoiceRoom` ends the call when a clip cannot be made or played**, after the answer has
|
|
57
|
+
finished streaming into the transcript, with `errorCode` `synthesis-unavailable`,
|
|
58
|
+
`synthesis-limited`, `synthesis-rejected`, `synthesis-failed`, `not-authenticated` (no session
|
|
59
|
+
token) or `not-played`; `synthesisError` is the `SpeechClipError` behind it. A turn no longer
|
|
60
|
+
skips a sentence it could not speak, and `turnErrorCode` is no longer `not-played`.
|
|
61
|
+
- `VoiceErrorCode` gains the four `synthesis-*` codes; an app that translates every code needs
|
|
62
|
+
their sentences.
|
|
63
|
+
|
|
3
64
|
## 13.1.0
|
|
4
65
|
|
|
5
66
|
The chat works in any app without host setup it did not document in code:
|
package/README.md
CHANGED
|
@@ -112,6 +112,37 @@ components render. Those events are the product runtime's, not part of the Oxy
|
|
|
112
112
|
generic inference contract, and this package is written against them on
|
|
113
113
|
purpose: it is a **product** client.
|
|
114
114
|
|
|
115
|
+
## The composer
|
|
116
|
+
|
|
117
|
+
The sheet, the screen and the Alia app share one composer, `AliaComposer`: Bloom's
|
|
118
|
+
`ComposerPanel` with files by paste and drop (read with real progress, retried, refused with a
|
|
119
|
+
sentence), dictation, suggestions over the field and Alia's questions docked above it.
|
|
120
|
+
`AliaChatSheet` and `AliaChatScreen` render it with photos, voice and suggestions, and
|
|
121
|
+
`AliaChatSheet` focuses it once the sheet has presented. To build your own surface, mount it and
|
|
122
|
+
pass what the surface has:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { AliaComposer } from '@alia.onl/sdk';
|
|
126
|
+
|
|
127
|
+
<AliaComposer
|
|
128
|
+
value={draft}
|
|
129
|
+
onValueChange={setDraft}
|
|
130
|
+
onSubmit={send}
|
|
131
|
+
busy={isStreaming}
|
|
132
|
+
onStop={stop}
|
|
133
|
+
modes={powerLevels} // the mode selector; omit to hide it
|
|
134
|
+
mode={level}
|
|
135
|
+
onModeChange={setLevel}
|
|
136
|
+
addMenu={groups} // the add menu's rows, and what pressing one does
|
|
137
|
+
onAddMenuSelect={onAdd}
|
|
138
|
+
attachments={files} // omit `onAddAttachment` and the composer takes no paste or drop
|
|
139
|
+
onAddAttachment={addFile}
|
|
140
|
+
onRemoveAttachment={removeFile}
|
|
141
|
+
attachmentKinds={['image']} // what your send carries; others are refused with a sentence
|
|
142
|
+
labels={{ composerSend: 'Enviar' }} // anything omitted is English
|
|
143
|
+
/>;
|
|
144
|
+
```
|
|
145
|
+
|
|
115
146
|
## Which endpoint the SDK calls, and why
|
|
116
147
|
|
|
117
148
|
The hook sends its turns to `POST {apiUrl}/v1/chat/completions`, with `apiUrl`
|
package/package.json
CHANGED
|
@@ -8,33 +8,30 @@
|
|
|
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';
|
|
12
13
|
import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
|
|
13
14
|
import { useTTS } from '../hooks/useTTS';
|
|
14
|
-
import {
|
|
15
|
+
import { useFocusWhenPresented } from '../hooks/useFocusWhenPresented';
|
|
16
|
+
import { usePendingQuestions } from '../hooks/usePendingQuestions';
|
|
15
17
|
import { useImagePicker } from '../hooks/useImagePicker';
|
|
16
18
|
import { imageUriToDataUrl } from '../lib/image-data-url';
|
|
17
19
|
import { useAmbientWave } from '../hooks/useAmbientWave';
|
|
18
20
|
import { AmbientField } from './AmbientField';
|
|
19
|
-
import { QuestionCard } from './QuestionCard';
|
|
20
21
|
import { getAccountDisplayName } from '@oxy.so/core';
|
|
21
22
|
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
23
|
import { AliaChatMessageList } from './AliaChatMessageList';
|
|
26
24
|
import { AliaFlightsProvider, ALIA_TO_PROFILE } from '../lib/flights';
|
|
27
25
|
import { AliaWelcomeMessage } from './AliaWelcomeMessage';
|
|
28
|
-
import {
|
|
26
|
+
import { AliaComposer } from './AliaComposer';
|
|
29
27
|
import { useComposerSuggestions, useSuggestionQuery } from '../hooks/useComposerSuggestions';
|
|
30
|
-
import { ComposerLoader } from '@oxy.so/bloom/composer-loader';
|
|
31
28
|
import { AiChatContainer } from '@oxy.so/bloom/ai-chat';
|
|
32
29
|
import {
|
|
33
30
|
AvatarFlightLayer,
|
|
34
31
|
useAvatarFlights,
|
|
35
32
|
type AvatarFlights,
|
|
36
33
|
} from '@oxy.so/bloom/avatar-flight';
|
|
37
|
-
import {
|
|
34
|
+
import type { ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
|
|
38
35
|
import { RiImageLine } from '@oxy.so/bloom/icons';
|
|
39
36
|
import { toast } from '@oxy.so/bloom/toast';
|
|
40
37
|
import { Button } from '@oxy.so/bloom/button';
|
|
@@ -42,6 +39,7 @@ import { type AliaChatLabels, AliaChatLabelsProvider, useLabels } from '../lib/l
|
|
|
42
39
|
import type { AliaAvatarState } from './AliaAvatar';
|
|
43
40
|
import { VoiceModeIcon } from './VoiceModeIcon';
|
|
44
41
|
import type {
|
|
42
|
+
AliaAttachment,
|
|
45
43
|
AliaSuggestion,
|
|
46
44
|
ChatMessage,
|
|
47
45
|
VoiceMessage,
|
|
@@ -137,14 +135,21 @@ export interface AliaChatContentProps {
|
|
|
137
135
|
flights?: AvatarFlights;
|
|
138
136
|
/** A turn sent once as this chat mounts: the conversation a sheet was opened for. */
|
|
139
137
|
initialTurn?: AliaTurn;
|
|
138
|
+
/**
|
|
139
|
+
* The `animatedProgress` of the Bloom sheet this chat is in. The composer takes focus (and on
|
|
140
|
+
* native opens the keyboard) once it reads `1`, so the person can type as soon as the sheet is
|
|
141
|
+
* up. Omitted, the chat is not in a sheet and focus is left alone.
|
|
142
|
+
*/
|
|
143
|
+
presentedProgress?: SharedValue<number>;
|
|
140
144
|
/** Internal sheet lifecycle bridge: receives the live request canceller. */
|
|
141
145
|
onStopReady?: (stop: (() => void) | null) => void;
|
|
142
146
|
/** Overrides for the chat's own copy; anything omitted is English. */
|
|
143
147
|
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
148
|
}
|
|
147
149
|
|
|
150
|
+
/** The chat API carries photos (`image_url` parts) and nothing else. */
|
|
151
|
+
const PHOTOS_ONLY = ['image'] as const;
|
|
152
|
+
|
|
148
153
|
/** The add menu: photos are the one attachment the chat API carries (`image_url` parts). */
|
|
149
154
|
function addMenu(labels: AliaChatLabels): ComposerPanelAddMenuGroup[] {
|
|
150
155
|
return [
|
|
@@ -192,6 +197,7 @@ function AliaChatContentBody({
|
|
|
192
197
|
agents: knownAgents,
|
|
193
198
|
flights: sharedFlights,
|
|
194
199
|
initialTurn,
|
|
200
|
+
presentedProgress,
|
|
195
201
|
onStopReady,
|
|
196
202
|
}: AliaChatContentProps) {
|
|
197
203
|
const ownFlights = useAvatarFlights();
|
|
@@ -212,15 +218,7 @@ function AliaChatContentBody({
|
|
|
212
218
|
const { messages, setMessages, send, isStreaming, stop, clear, approvePlan, rejectPlan } =
|
|
213
219
|
useAliaChat(chatOptions);
|
|
214
220
|
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;
|
|
221
|
+
const asked = usePendingQuestions(messages, isStreaming);
|
|
224
222
|
|
|
225
223
|
// The first turn of an empty chat goes to the host when it asked for it; every other turn is sent here.
|
|
226
224
|
const startsElsewhere = onStart !== undefined && messages.length === 0;
|
|
@@ -258,9 +256,7 @@ function AliaChatContentBody({
|
|
|
258
256
|
const [inputValue, setInputValue] = useState('');
|
|
259
257
|
|
|
260
258
|
// 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
|
-
>([]);
|
|
259
|
+
const [photos, setPhotos] = useState<AliaAttachment[]>([]);
|
|
264
260
|
const { pickImage } = useImagePicker();
|
|
265
261
|
|
|
266
262
|
const handleAddMenuSelect = useCallback(
|
|
@@ -272,8 +268,10 @@ function AliaChatContentBody({
|
|
|
272
268
|
...prev,
|
|
273
269
|
...assets.map((asset) => ({
|
|
274
270
|
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
271
|
+
type: 'image' as const,
|
|
275
272
|
name: asset.name,
|
|
276
273
|
uri: asset.uri,
|
|
274
|
+
size: asset.size,
|
|
277
275
|
mimeType: asset.mimeType,
|
|
278
276
|
})),
|
|
279
277
|
]);
|
|
@@ -323,24 +321,6 @@ function AliaChatContentBody({
|
|
|
323
321
|
[submitTurn, photos, reportError],
|
|
324
322
|
);
|
|
325
323
|
|
|
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
324
|
// ── TTS ──
|
|
345
325
|
const tts = useTTS({ apiUrl });
|
|
346
326
|
|
|
@@ -452,16 +432,8 @@ function AliaChatContentBody({
|
|
|
452
432
|
[VoiceSession, activateVoice, labels.startVoice],
|
|
453
433
|
);
|
|
454
434
|
|
|
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
|
-
);
|
|
435
|
+
const composerRef = useRef<TextInput | null>(null);
|
|
436
|
+
useFocusWhenPresented(presentedProgress, () => composerRef.current?.focus());
|
|
465
437
|
|
|
466
438
|
const composer =
|
|
467
439
|
VoiceSession && isVoiceActive ? (
|
|
@@ -470,45 +442,32 @@ function AliaChatContentBody({
|
|
|
470
442
|
<VoiceSession apiUrl={apiUrl} onStateChange={setVoiceState} onEnd={deactivateVoice} />
|
|
471
443
|
</Suspense>
|
|
472
444
|
) : (
|
|
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
|
-
emptyAction={voiceActivateButton}
|
|
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
|
-
</>
|
|
445
|
+
<AliaComposer
|
|
446
|
+
inputRef={composerRef}
|
|
447
|
+
disableKeyboardAvoidance
|
|
448
|
+
onKeyPress={suggestions.onKeyPress}
|
|
449
|
+
suggestions={{ ...suggestions, onPick: handlePickSuggestion }}
|
|
450
|
+
// Asked beside the field, not in the thread: the card docks above the composer.
|
|
451
|
+
questions={{
|
|
452
|
+
items: asked.questions,
|
|
453
|
+
onSubmit: (message) => send(message),
|
|
454
|
+
onDismiss: asked.dismiss,
|
|
455
|
+
}}
|
|
456
|
+
value={inputValue}
|
|
457
|
+
onValueChange={setInputValue}
|
|
458
|
+
onSubmit={() => void handleSubmit(inputValue)}
|
|
459
|
+
busy={isStreaming}
|
|
460
|
+
onStop={stop}
|
|
461
|
+
emptyAction={voiceActivateButton}
|
|
462
|
+
placeholder={labels.placeholder}
|
|
463
|
+
addMenu={addMenuGroups}
|
|
464
|
+
onAddMenuSelect={handleAddMenuSelect}
|
|
465
|
+
attachments={photos}
|
|
466
|
+
onAddAttachment={(photo) => setPhotos((prev) => [...prev, photo])}
|
|
467
|
+
onRemoveAttachment={(id) => setPhotos((prev) => prev.filter((p) => p.id !== id))}
|
|
468
|
+
attachmentKinds={PHOTOS_ONLY}
|
|
469
|
+
onError={onError}
|
|
470
|
+
/>
|
|
512
471
|
);
|
|
513
472
|
|
|
514
473
|
const hasMessages = messages.length > 0;
|
|
@@ -572,16 +531,10 @@ function AliaChatContentBody({
|
|
|
572
531
|
}
|
|
573
532
|
|
|
574
533
|
/** 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) {
|
|
534
|
+
export function AliaChatContent({ labels, ...props }: AliaChatContentProps) {
|
|
580
535
|
return (
|
|
581
|
-
<
|
|
582
|
-
<
|
|
583
|
-
|
|
584
|
-
</AliaChatLabelsProvider>
|
|
585
|
-
</AgentAvatarProvider>
|
|
536
|
+
<AliaChatLabelsProvider labels={labels}>
|
|
537
|
+
<AliaChatContentBody {...props} />
|
|
538
|
+
</AliaChatLabelsProvider>
|
|
586
539
|
);
|
|
587
540
|
}
|
|
@@ -20,6 +20,8 @@
|
|
|
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';
|
|
@@ -80,7 +83,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
80
83
|
isDarkMode,
|
|
81
84
|
voiceSession,
|
|
82
85
|
labels,
|
|
83
|
-
characterRuntimeUrl,
|
|
86
|
+
characterRuntimeUrl = ALIA_CHARACTER_RUNTIME_URL,
|
|
84
87
|
onShare,
|
|
85
88
|
headerMenu,
|
|
86
89
|
},
|
|
@@ -88,6 +91,9 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
88
91
|
) => {
|
|
89
92
|
// Bloom's resolved mode, not the system's scheme.
|
|
90
93
|
const { isDark } = useTheme();
|
|
94
|
+
// How far each sheet is presented (0 closed, 1 up): its chat takes the composer's focus at 1.
|
|
95
|
+
const mainProgress = useSharedValue(0);
|
|
96
|
+
const conversationProgress = useSharedValue(0);
|
|
91
97
|
const mainRef = useRef<BottomSheetRef>(null);
|
|
92
98
|
const conversationRef = useRef<BottomSheetRef>(null);
|
|
93
99
|
// Each sheet's chat registers its own canceller; closing a sheet stops its own turn.
|
|
@@ -162,16 +168,18 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
162
168
|
voiceSession,
|
|
163
169
|
bottomInset: detached ? 0 : bottom,
|
|
164
170
|
labels,
|
|
165
|
-
characterRuntimeUrl,
|
|
166
171
|
};
|
|
167
172
|
|
|
173
|
+
// The provider wraps the sheets AND the flight layer: both draw avatars, and an avatar drawn
|
|
174
|
+
// outside it would fall back to the host's own `/bloom-character/runtime.mjs`.
|
|
168
175
|
return (
|
|
169
|
-
|
|
176
|
+
<AgentAvatarProvider runtimeUrl={characterRuntimeUrl}>
|
|
170
177
|
<BottomSheet
|
|
171
178
|
ref={mainRef}
|
|
172
179
|
detached={detached}
|
|
173
180
|
scrollable={false}
|
|
174
181
|
manualActivation
|
|
182
|
+
animatedProgress={mainProgress}
|
|
175
183
|
onDismissAttempt={onMainDismissAttempt}
|
|
176
184
|
onDismiss={() => setConversation(null)}
|
|
177
185
|
style={sheetStyle}
|
|
@@ -186,6 +194,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
186
194
|
agents={agents}
|
|
187
195
|
suggestionMatches={suggestionMatches}
|
|
188
196
|
onSuggestionQuery={onSuggestionQuery}
|
|
197
|
+
presentedProgress={mainProgress}
|
|
189
198
|
onStopReady={registerMainStop}
|
|
190
199
|
onStart={startConversation}
|
|
191
200
|
/>
|
|
@@ -198,6 +207,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
198
207
|
detached={detached}
|
|
199
208
|
scrollable={false}
|
|
200
209
|
manualActivation
|
|
210
|
+
animatedProgress={conversationProgress}
|
|
201
211
|
onDismissAttempt={onConversationDismissAttempt}
|
|
202
212
|
onDismiss={() => setConversation(null)}
|
|
203
213
|
style={sheetStyle}
|
|
@@ -205,6 +215,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
205
215
|
<AliaChatContent
|
|
206
216
|
{...shared}
|
|
207
217
|
initialTurn={conversation.turn}
|
|
218
|
+
presentedProgress={conversationProgress}
|
|
208
219
|
agents={agents}
|
|
209
220
|
onStopReady={registerConversationStop}
|
|
210
221
|
// The bar is the conversation's: back to the main screen, the profile, share and
|
|
@@ -225,7 +236,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
225
236
|
</BottomSheet>
|
|
226
237
|
) : null}
|
|
227
238
|
<AvatarFlightLayer flights={flights} />
|
|
228
|
-
|
|
239
|
+
</AgentAvatarProvider>
|
|
229
240
|
);
|
|
230
241
|
},
|
|
231
242
|
);
|