@alia.onl/sdk 13.0.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 +72 -0
- package/README.md +41 -12
- package/package.json +2 -2
- package/src/components/AliaChatContent.tsx +58 -90
- package/src/components/AliaChatScreen.tsx +29 -22
- package/src/components/AliaChatSheet.tsx +17 -2
- 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 +11 -0
- package/src/lib/attachment-intake.ts +154 -0
- package/src/lib/attachment-tiles.ts +74 -0
- package/src/lib/character-runtime.ts +10 -0
- package/src/lib/labels.tsx +54 -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,77 @@
|
|
|
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
|
+
|
|
64
|
+
## 13.1.0
|
|
65
|
+
|
|
66
|
+
The chat works in any app without host setup it did not document in code:
|
|
67
|
+
|
|
68
|
+
- **The avatar loads.** The chat mounts Bloom's `AgentAvatarProvider` itself, pointing at Alia's
|
|
69
|
+
shared runtime (`ALIA_CHARACTER_RUNTIME_URL`, served with CORS open); a host that serves its own
|
|
70
|
+
passes `characterRuntimeUrl`. Before, an app that had not hosted the 214 MB runtime showed
|
|
71
|
+
Bloom's "unavailable" state.
|
|
72
|
+
- **The empty chat greets.** With no `welcomeGreeting` it greets the signed-in person by name
|
|
73
|
+
(`labels.greeting` / `labels.greetingNamed`). Before, it drew nothing.
|
|
74
|
+
|
|
3
75
|
## 13.0.0
|
|
4
76
|
|
|
5
77
|
Fixes found by driving the sheet end to end (the sheet shows plans, links citations, nests lists):
|
package/README.md
CHANGED
|
@@ -25,20 +25,18 @@ the default in Expo SDK 53+), because every icon import is a subpath.
|
|
|
25
25
|
The chat is built from Bloom (`@oxy.so/bloom`, a peer dependency): mount Bloom's
|
|
26
26
|
`BloomProvider` above it, with the one `KeyboardProvider` your app already has.
|
|
27
27
|
|
|
28
|
-
Alia's avatar is Bloom's 3D character (`AgentAvatar`)
|
|
29
|
-
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<AgentAvatarProvider runtimeUrl="/bloom-character/runtime.mjs">…</AgentAvatarProvider>
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Without it the avatar shows Bloom's "unavailable" state. Native avatars run in a
|
|
28
|
+
Alia's avatar is Bloom's 3D character (`AgentAvatar`). It draws through a runtime of
|
|
29
|
+
static files (about 214 MB), which the chat loads from Alia's own origin
|
|
30
|
+
(`ALIA_CHARACTER_RUNTIME_URL`, served with CORS open), so the app hosts nothing and mounts
|
|
31
|
+
no avatar provider. An app with a strict CSP allows `https://alia.onl` in `script-src` and
|
|
32
|
+
`connect-src` (and `'wasm-unsafe-eval'`). To serve your own copy instead, serve the Bloom
|
|
33
|
+
package's `assets/character-runtime/` directory unchanged and pass the URL of `runtime.mjs`
|
|
34
|
+
as `characterRuntimeUrl` to `AliaChatSheet` or `AliaChatScreen`; native avatars run in a
|
|
40
35
|
WebView, so there the URL must be absolute and reachable from the device.
|
|
41
36
|
|
|
37
|
+
The empty chat greets the signed-in person by name ("Hi Nate, what's on your mind?"); pass
|
|
38
|
+
`welcomeGreeting` to say something else, or `labels.greeting` / `labels.greetingNamed` to translate it.
|
|
39
|
+
|
|
42
40
|
Failures (a voice call that cannot start, a dictation error) are shown with Bloom's `toast`,
|
|
43
41
|
so the app needs a `ToastOutlet` (`OxyProvider` mounts one), or pass `onError` to handle them.
|
|
44
42
|
|
|
@@ -114,6 +112,37 @@ components render. Those events are the product runtime's, not part of the Oxy
|
|
|
114
112
|
generic inference contract, and this package is written against them on
|
|
115
113
|
purpose: it is a **product** client.
|
|
116
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
|
+
|
|
117
146
|
## Which endpoint the SDK calls, and why
|
|
118
147
|
|
|
119
148
|
The hook sends its turns to `POST {apiUrl}/v1/chat/completions`, with `apiUrl`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia.onl/sdk",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.2.0",
|
|
4
4
|
"description": "Alia AI chat SDK — reusable chat UI, voice, and streaming components for Oxy apps",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"clsx": "^2.1.1",
|
|
34
34
|
"react-native-markdown-display": "^7.0.2",
|
|
35
|
-
"tailwind-merge": "^3.
|
|
35
|
+
"tailwind-merge": "^3.7.0",
|
|
36
36
|
"zustand": "^5.0.15"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
@@ -8,29 +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 {
|
|
20
|
-
import {
|
|
21
|
+
import { getAccountDisplayName } from '@oxy.so/core';
|
|
22
|
+
import { useOxy } from '@oxy.so/services/ui/client';
|
|
21
23
|
import { AliaChatMessageList } from './AliaChatMessageList';
|
|
22
24
|
import { AliaFlightsProvider, ALIA_TO_PROFILE } from '../lib/flights';
|
|
23
25
|
import { AliaWelcomeMessage } from './AliaWelcomeMessage';
|
|
24
|
-
import {
|
|
26
|
+
import { AliaComposer } from './AliaComposer';
|
|
25
27
|
import { useComposerSuggestions, useSuggestionQuery } from '../hooks/useComposerSuggestions';
|
|
26
|
-
import { ComposerLoader } from '@oxy.so/bloom/composer-loader';
|
|
27
28
|
import { AiChatContainer } from '@oxy.so/bloom/ai-chat';
|
|
28
29
|
import {
|
|
29
30
|
AvatarFlightLayer,
|
|
30
31
|
useAvatarFlights,
|
|
31
32
|
type AvatarFlights,
|
|
32
33
|
} from '@oxy.so/bloom/avatar-flight';
|
|
33
|
-
import {
|
|
34
|
+
import type { ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
|
|
34
35
|
import { RiImageLine } from '@oxy.so/bloom/icons';
|
|
35
36
|
import { toast } from '@oxy.so/bloom/toast';
|
|
36
37
|
import { Button } from '@oxy.so/bloom/button';
|
|
@@ -38,6 +39,7 @@ import { type AliaChatLabels, AliaChatLabelsProvider, useLabels } from '../lib/l
|
|
|
38
39
|
import type { AliaAvatarState } from './AliaAvatar';
|
|
39
40
|
import { VoiceModeIcon } from './VoiceModeIcon';
|
|
40
41
|
import type {
|
|
42
|
+
AliaAttachment,
|
|
41
43
|
AliaSuggestion,
|
|
42
44
|
ChatMessage,
|
|
43
45
|
VoiceMessage,
|
|
@@ -84,7 +86,7 @@ export interface AliaChatContentProps {
|
|
|
84
86
|
apiUrl?: string;
|
|
85
87
|
/** Error handler (e.g. toast) */
|
|
86
88
|
onError?: (message: string) => void;
|
|
87
|
-
|
|
89
|
+
/** The empty chat's greeting; omitted, Alia greets the signed-in person by name (`labels`). */
|
|
88
90
|
welcomeGreeting?: string;
|
|
89
91
|
welcomeSubtitle?: string;
|
|
90
92
|
/** The lines offered over the composer on an empty chat. */
|
|
@@ -133,12 +135,21 @@ export interface AliaChatContentProps {
|
|
|
133
135
|
flights?: AvatarFlights;
|
|
134
136
|
/** A turn sent once as this chat mounts: the conversation a sheet was opened for. */
|
|
135
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>;
|
|
136
144
|
/** Internal sheet lifecycle bridge: receives the live request canceller. */
|
|
137
145
|
onStopReady?: (stop: (() => void) | null) => void;
|
|
138
146
|
/** Overrides for the chat's own copy; anything omitted is English. */
|
|
139
147
|
labels?: Partial<AliaChatLabels>;
|
|
140
148
|
}
|
|
141
149
|
|
|
150
|
+
/** The chat API carries photos (`image_url` parts) and nothing else. */
|
|
151
|
+
const PHOTOS_ONLY = ['image'] as const;
|
|
152
|
+
|
|
142
153
|
/** The add menu: photos are the one attachment the chat API carries (`image_url` parts). */
|
|
143
154
|
function addMenu(labels: AliaChatLabels): ComposerPanelAddMenuGroup[] {
|
|
144
155
|
return [
|
|
@@ -186,6 +197,7 @@ function AliaChatContentBody({
|
|
|
186
197
|
agents: knownAgents,
|
|
187
198
|
flights: sharedFlights,
|
|
188
199
|
initialTurn,
|
|
200
|
+
presentedProgress,
|
|
189
201
|
onStopReady,
|
|
190
202
|
}: AliaChatContentProps) {
|
|
191
203
|
const ownFlights = useAvatarFlights();
|
|
@@ -206,15 +218,7 @@ function AliaChatContentBody({
|
|
|
206
218
|
const { messages, setMessages, send, isStreaming, stop, clear, approvePlan, rejectPlan } =
|
|
207
219
|
useAliaChat(chatOptions);
|
|
208
220
|
const agents = useMemo(() => participatingAgents(messages), [messages]);
|
|
209
|
-
|
|
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;
|
|
221
|
+
const asked = usePendingQuestions(messages, isStreaming);
|
|
218
222
|
|
|
219
223
|
// The first turn of an empty chat goes to the host when it asked for it; every other turn is sent here.
|
|
220
224
|
const startsElsewhere = onStart !== undefined && messages.length === 0;
|
|
@@ -252,9 +256,7 @@ function AliaChatContentBody({
|
|
|
252
256
|
const [inputValue, setInputValue] = useState('');
|
|
253
257
|
|
|
254
258
|
// Picked photos wait here, as tiles above the field, until the turn is sent.
|
|
255
|
-
const [photos, setPhotos] = useState<
|
|
256
|
-
Array<{ id: string; name: string; uri: string; mimeType?: string }>
|
|
257
|
-
>([]);
|
|
259
|
+
const [photos, setPhotos] = useState<AliaAttachment[]>([]);
|
|
258
260
|
const { pickImage } = useImagePicker();
|
|
259
261
|
|
|
260
262
|
const handleAddMenuSelect = useCallback(
|
|
@@ -266,8 +268,10 @@ function AliaChatContentBody({
|
|
|
266
268
|
...prev,
|
|
267
269
|
...assets.map((asset) => ({
|
|
268
270
|
id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
271
|
+
type: 'image' as const,
|
|
269
272
|
name: asset.name,
|
|
270
273
|
uri: asset.uri,
|
|
274
|
+
size: asset.size,
|
|
271
275
|
mimeType: asset.mimeType,
|
|
272
276
|
})),
|
|
273
277
|
]);
|
|
@@ -317,24 +321,6 @@ function AliaChatContentBody({
|
|
|
317
321
|
[submitTurn, photos, reportError],
|
|
318
322
|
);
|
|
319
323
|
|
|
320
|
-
// ── Dictation: the pill's mic toggles it, the transcript lands in the field ──
|
|
321
|
-
const stt = useSpeechToText();
|
|
322
|
-
useEffect(() => {
|
|
323
|
-
if (stt.error) reportError(stt.error);
|
|
324
|
-
}, [stt.error, reportError]);
|
|
325
|
-
const sttRef = useRef(stt);
|
|
326
|
-
sttRef.current = stt;
|
|
327
|
-
const handleListeningChange = useCallback(async (listening: boolean) => {
|
|
328
|
-
const current = sttRef.current;
|
|
329
|
-
if (listening) {
|
|
330
|
-
if (!current.isRecording && !current.isTranscribing) current.startRecording();
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (!current.isRecording) return;
|
|
334
|
-
const text = await current.stopAndTranscribe();
|
|
335
|
-
if (text) setInputValue((prev) => (prev ? `${prev} ${text}` : text));
|
|
336
|
-
}, []);
|
|
337
|
-
|
|
338
324
|
// ── TTS ──
|
|
339
325
|
const tts = useTTS({ apiUrl });
|
|
340
326
|
|
|
@@ -399,14 +385,17 @@ function AliaChatContentBody({
|
|
|
399
385
|
return 'writing';
|
|
400
386
|
}, [messages, isStreaming, isVoiceActive, voiceAgentState]);
|
|
401
387
|
|
|
388
|
+
const { user } = useOxy();
|
|
389
|
+
const greeting =
|
|
390
|
+
welcomeGreeting ?? (user ? labels.greetingNamed(getAccountDisplayName(user)) : labels.greeting);
|
|
402
391
|
const welcomeComponent = useMemo(() => {
|
|
403
|
-
if (
|
|
392
|
+
if (messages.length > 0) return undefined;
|
|
404
393
|
return (
|
|
405
394
|
<View className="flex-1 items-center justify-center px-4 py-10">
|
|
406
|
-
<AliaWelcomeMessage greeting={
|
|
395
|
+
<AliaWelcomeMessage greeting={greeting} subtitle={welcomeSubtitle} />
|
|
407
396
|
</View>
|
|
408
397
|
);
|
|
409
|
-
}, [
|
|
398
|
+
}, [greeting, welcomeSubtitle, messages.length]);
|
|
410
399
|
|
|
411
400
|
// ── Ambient wave — one persistent overlay across idle/voice/TTS/STT ──
|
|
412
401
|
const wave = useAmbientWave({
|
|
@@ -443,16 +432,8 @@ function AliaChatContentBody({
|
|
|
443
432
|
[VoiceSession, activateVoice, labels.startVoice],
|
|
444
433
|
);
|
|
445
434
|
|
|
446
|
-
const
|
|
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
|
-
);
|
|
435
|
+
const composerRef = useRef<TextInput | null>(null);
|
|
436
|
+
useFocusWhenPresented(presentedProgress, () => composerRef.current?.focus());
|
|
456
437
|
|
|
457
438
|
const composer =
|
|
458
439
|
VoiceSession && isVoiceActive ? (
|
|
@@ -461,45 +442,32 @@ function AliaChatContentBody({
|
|
|
461
442
|
<VoiceSession apiUrl={apiUrl} onStateChange={setVoiceState} onEnd={deactivateVoice} />
|
|
462
443
|
</Suspense>
|
|
463
444
|
) : (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
{
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
emptyAction={voiceActivateButton}
|
|
491
|
-
placeholder={labels.placeholder}
|
|
492
|
-
addMenu={addMenuGroups}
|
|
493
|
-
onAddMenuSelect={handleAddMenuSelect}
|
|
494
|
-
attachments={pillAttachments}
|
|
495
|
-
onRemoveAttachment={(id) => setPhotos((prev) => prev.filter((p) => p.id !== id))}
|
|
496
|
-
voiceState={
|
|
497
|
-
stt.isTranscribing ? 'transcribing' : stt.isRecording ? 'recording' : 'idle'
|
|
498
|
-
}
|
|
499
|
-
onVoiceStateChange={(next) => void handleListeningChange(next === 'recording')}
|
|
500
|
-
/>
|
|
501
|
-
</ComposerLoader>
|
|
502
|
-
</>
|
|
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
|
+
/>
|
|
503
471
|
);
|
|
504
472
|
|
|
505
473
|
const hasMessages = messages.length > 0;
|
|
@@ -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';
|
|
@@ -62,6 +64,8 @@ export interface AliaChatScreenProps {
|
|
|
62
64
|
voiceSession?: VoiceSessionComponent;
|
|
63
65
|
/** Overrides for the chat's own copy (header, composer, plan, research…); omitted, English. */
|
|
64
66
|
labels?: Partial<AliaChatLabels>;
|
|
67
|
+
/** Where the 3D avatar's runtime is served from; omitted, Alia's shared copy (`ALIA_CHARACTER_RUNTIME_URL`). */
|
|
68
|
+
characterRuntimeUrl?: string;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
export function AliaChatScreen({
|
|
@@ -82,6 +86,7 @@ export function AliaChatScreen({
|
|
|
82
86
|
isDarkMode,
|
|
83
87
|
voiceSession,
|
|
84
88
|
labels,
|
|
89
|
+
characterRuntimeUrl = ALIA_CHARACTER_RUNTIME_URL,
|
|
85
90
|
}: AliaChatScreenProps) {
|
|
86
91
|
const insets = useSafeAreaInsets();
|
|
87
92
|
// The screen owns the whole window, so it keeps its composer out of both
|
|
@@ -111,27 +116,29 @@ export function AliaChatScreen({
|
|
|
111
116
|
);
|
|
112
117
|
|
|
113
118
|
return (
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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>
|
|
136
143
|
);
|
|
137
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';
|
|
@@ -52,6 +55,8 @@ export interface AliaChatSheetProps {
|
|
|
52
55
|
voiceSession?: VoiceSessionComponent;
|
|
53
56
|
/** Overrides for the chat's own copy (header, composer, plan, research…); omitted, English. */
|
|
54
57
|
labels?: Partial<AliaChatLabels>;
|
|
58
|
+
/** Where the 3D avatar's runtime is served from; omitted, Alia's shared copy (`ALIA_CHARACTER_RUNTIME_URL`). */
|
|
59
|
+
characterRuntimeUrl?: string;
|
|
55
60
|
/** Draws the header's share button once there is a conversation; omitted, no share button. */
|
|
56
61
|
onShare?: () => void;
|
|
57
62
|
/** More rows for the header's menu (`DropdownMenuItem`s from `@oxy.so/bloom/dropdown-menu`). */
|
|
@@ -78,6 +83,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
78
83
|
isDarkMode,
|
|
79
84
|
voiceSession,
|
|
80
85
|
labels,
|
|
86
|
+
characterRuntimeUrl = ALIA_CHARACTER_RUNTIME_URL,
|
|
81
87
|
onShare,
|
|
82
88
|
headerMenu,
|
|
83
89
|
},
|
|
@@ -85,6 +91,9 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
85
91
|
) => {
|
|
86
92
|
// Bloom's resolved mode, not the system's scheme.
|
|
87
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);
|
|
88
97
|
const mainRef = useRef<BottomSheetRef>(null);
|
|
89
98
|
const conversationRef = useRef<BottomSheetRef>(null);
|
|
90
99
|
// Each sheet's chat registers its own canceller; closing a sheet stops its own turn.
|
|
@@ -161,13 +170,16 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
161
170
|
labels,
|
|
162
171
|
};
|
|
163
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`.
|
|
164
175
|
return (
|
|
165
|
-
|
|
176
|
+
<AgentAvatarProvider runtimeUrl={characterRuntimeUrl}>
|
|
166
177
|
<BottomSheet
|
|
167
178
|
ref={mainRef}
|
|
168
179
|
detached={detached}
|
|
169
180
|
scrollable={false}
|
|
170
181
|
manualActivation
|
|
182
|
+
animatedProgress={mainProgress}
|
|
171
183
|
onDismissAttempt={onMainDismissAttempt}
|
|
172
184
|
onDismiss={() => setConversation(null)}
|
|
173
185
|
style={sheetStyle}
|
|
@@ -182,6 +194,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
182
194
|
agents={agents}
|
|
183
195
|
suggestionMatches={suggestionMatches}
|
|
184
196
|
onSuggestionQuery={onSuggestionQuery}
|
|
197
|
+
presentedProgress={mainProgress}
|
|
185
198
|
onStopReady={registerMainStop}
|
|
186
199
|
onStart={startConversation}
|
|
187
200
|
/>
|
|
@@ -194,6 +207,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
194
207
|
detached={detached}
|
|
195
208
|
scrollable={false}
|
|
196
209
|
manualActivation
|
|
210
|
+
animatedProgress={conversationProgress}
|
|
197
211
|
onDismissAttempt={onConversationDismissAttempt}
|
|
198
212
|
onDismiss={() => setConversation(null)}
|
|
199
213
|
style={sheetStyle}
|
|
@@ -201,6 +215,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
201
215
|
<AliaChatContent
|
|
202
216
|
{...shared}
|
|
203
217
|
initialTurn={conversation.turn}
|
|
218
|
+
presentedProgress={conversationProgress}
|
|
204
219
|
agents={agents}
|
|
205
220
|
onStopReady={registerConversationStop}
|
|
206
221
|
// The bar is the conversation's: back to the main screen, the profile, share and
|
|
@@ -221,7 +236,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
221
236
|
</BottomSheet>
|
|
222
237
|
) : null}
|
|
223
238
|
<AvatarFlightLayer flights={flights} />
|
|
224
|
-
|
|
239
|
+
</AgentAvatarProvider>
|
|
225
240
|
);
|
|
226
241
|
},
|
|
227
242
|
);
|