@alia.onl/sdk 21.3.1 → 21.4.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 +42 -0
- package/README.md +30 -2
- package/package.json +3 -1
- package/src/ask-text.ts +10 -0
- package/src/ask.ts +6 -2
- package/src/components/AliaAskCore.tsx +183 -0
- package/src/components/AliaAskProvider.tsx +12 -169
- package/src/components/AliaAskProviderLazy.tsx +27 -0
- package/src/components/AliaAskProviderText.tsx +25 -0
- package/src/components/AliaAskSheetWithVoice.tsx +10 -0
- package/src/components/AliaChatContent.tsx +20 -8
- package/src/components/AliaChatWithVoice.tsx +13 -0
- package/src/components/voice/AliaVoiceCall.web.tsx +17 -0
- package/src/components/voice/VoiceCallContext.ts +13 -0
- package/src/components/voice/withVoiceCall.tsx +25 -0
- package/src/hooks/useAttachMenu.ts +10 -11
- package/src/hooks/useSoundEffects.ts +2 -1
- package/src/hooks/useTTS.ts +2 -1
- package/src/index.ts +3 -3
- package/src/lib/deferred/document-picker.ts +2 -0
- package/src/lib/deferred/document-picker.web.ts +12 -0
- package/src/lib/deferred/expo-audio.ts +2 -0
- package/src/lib/deferred/expo-audio.web.ts +12 -0
- package/src/lib/deferred/image-data-url.ts +5 -0
- package/src/lib/deferred/image-data-url.web.ts +12 -0
- package/src/lib/speech-synthesis.ts +2 -1
- package/src/runtime/attachment-utils.ts +4 -3
- package/src/runtime/conversations.ts +9 -2
- package/src/text.ts +18 -0
- package/tsconfig.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @alia.onl/sdk
|
|
2
2
|
|
|
3
|
+
## 21.4.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`@alia.onl/sdk/text` and `@alia.onl/sdk/ask/text`: the chat without bundling the voice
|
|
8
|
+
call.** `voice={false}` hid the call, but its code still shipped: since 21.3.2 the web build
|
|
9
|
+
inlines the call into the chat's chunk, so a text-only app paid for it. The new entries export
|
|
10
|
+
the same `AliaChatSheet`, `AliaChatScreen` and `AliaChatContent`, and `AliaAskProvider` /
|
|
11
|
+
`useAskAgent` with a lazily loaded sheet. None of them reaches the call's modules (its socket,
|
|
12
|
+
protocol and audio engine), and they never offer a call. Read-aloud and dictation stay.
|
|
13
|
+
CrowdSource's reviewer, which switched `AliaChatSheet` to `@alia.onl/sdk/text`: lazy sheet
|
|
14
|
+
chunk 1,455,831 → 1,357,287 bytes (413.8 → 374.2 KB gzip), `__common.js` 3,978,158 →
|
|
15
|
+
3,962,370 bytes, no new chunk. The README has a table of which entry to import.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `AliaChatContent` no longer imports the call. It draws whatever call `VoiceCallContext` hands it,
|
|
20
|
+
and the root entry and `@alia.onl/sdk/ask` export their chat through `withVoiceCall`, so those
|
|
21
|
+
two behave as before. `check:entries` checks that the text entries never reach the call, on any
|
|
22
|
+
platform, and draw no async boundary on web other than the ask sheet's.
|
|
23
|
+
|
|
24
|
+
## 21.3.2
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **On web the SDK draws no async chunk boundary of its own**, so an app that lazy-loads its Alia
|
|
29
|
+
sheet keeps the chat in that chunk instead of its first paint. Expo's Metro serializer hoists
|
|
30
|
+
every module two async chunks share into `__common.js`, loaded at first paint; the SDK's own
|
|
31
|
+
`import()` calls (the call's `VoiceSession`, the root entry's `AliaAskProvider` sheet, the
|
|
32
|
+
`expo-audio`, document-picker and image readers) made such chunks inside the app's, and moved the
|
|
33
|
+
whole chat and the Bloom it draws with into `__common.js`. On web those now import statically
|
|
34
|
+
(`.web` forks); native keeps its lazy loads. The root entry's `AliaAskProvider` imports its sheet
|
|
35
|
+
statically on every platform (the root already holds the chat); `@alia.onl/sdk/ask` keeps its
|
|
36
|
+
one lazy boundary. CrowdSource's reviewer: first-paint JS 1.57 MiB gzip → 1.23 MiB, largest
|
|
37
|
+
file 5.05 MiB → 3.79 MiB, its lazy Alia chunk 29 KiB → 404 KiB gzip. `check:entries` holds it.
|
|
38
|
+
- **The source compiles under `erasableSyntaxOnly`.** The SDK ships TypeScript source, and a
|
|
39
|
+
consumer that compiles it with `erasableSyntaxOnly` (OxyHQ/website, `tsc -b`) failed with TS1294
|
|
40
|
+
on two constructor parameter properties in `runtime/conversations.ts`
|
|
41
|
+
(`ConversationNotFoundError.conversationId`, the offline store). They are now declared fields
|
|
42
|
+
assigned in the constructor, and the SDK's own `tsconfig.json` sets `erasableSyntaxOnly`, so
|
|
43
|
+
`typecheck` (CI and `prepublishOnly`) refuses parameter properties, enums and namespaces.
|
|
44
|
+
|
|
3
45
|
## 21.3.1
|
|
4
46
|
|
|
5
47
|
- Ask Alia follows the active account language resolved by Oxy Services instead of defaulting to
|
package/README.md
CHANGED
|
@@ -143,6 +143,26 @@ The sheet, the screen and the Alia app are three frames around ONE conversation
|
|
|
143
143
|
stop, retry, edit and clear conversations through the same hooks, and a conversation started in
|
|
144
144
|
one is the same conversation in the others.
|
|
145
145
|
|
|
146
|
+
### Which entry to import
|
|
147
|
+
|
|
148
|
+
| You want | Import from | Bundles the voice call |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| The chat with the call (`AliaChatSheet`, `AliaChatScreen`, `AliaChatContent`) | `@alia.onl/sdk` | yes |
|
|
151
|
+
| The same chat, text only | `@alia.onl/sdk/text` | no |
|
|
152
|
+
| One "Ask" sheet at the app's root, loaded with the first `ask` | `@alia.onl/sdk/ask` | yes, in the sheet's chunk |
|
|
153
|
+
| The same, text only | `@alia.onl/sdk/ask/text` | no |
|
|
154
|
+
| Data only: conversations, the stream, the model | `@alia.onl/sdk/runtime`, `@alia.onl/sdk/model` | no |
|
|
155
|
+
| Her face on a button | `@alia.onl/sdk/avatar` | no |
|
|
156
|
+
|
|
157
|
+
`voice={false}` hides the call, but the code still ships: the root entry and `@alia.onl/sdk/ask`
|
|
158
|
+
bundle the call (its socket, protocol and audio engine) because their chat can open one. A
|
|
159
|
+
text-only app imports from `@alia.onl/sdk/text` or `@alia.onl/sdk/ask/text`. Their chat never
|
|
160
|
+
reaches those modules and never offers a call, whatever `voice` says. Read-aloud and dictation
|
|
161
|
+
are still there, because they belong to the thread and the composer. Types can still come from
|
|
162
|
+
`@alia.onl/sdk`, since type imports are erased. A value imported from the root brings the call
|
|
163
|
+
along with it. In CrowdSource's reviewer, `@alia.onl/sdk/text` made its lazy sheet chunk 98 KB
|
|
164
|
+
smaller (raw; 39 KB gzip).
|
|
165
|
+
|
|
146
166
|
## Any agent, from any app
|
|
147
167
|
|
|
148
168
|
Every agent — Alia included — is the same agent (ADR 0020), and any Oxy app opens any agent the
|
|
@@ -174,7 +194,15 @@ dismiss(); // e.g. after a host tool changed the page under the sheet
|
|
|
174
194
|
|
|
175
195
|
Mount it from `@alia.onl/sdk/ask`: that entry is the provider and its hook alone, and the sheet
|
|
176
196
|
loads with the first `ask` (fetched in the background once the person is signed in), so the chat
|
|
177
|
-
stays out of the app's entry bundle. The root entry exports the same two, with the whole chat
|
|
197
|
+
stays out of the app's entry bundle. The root entry exports the same two, with the whole chat
|
|
198
|
+
(its provider imports the sheet statically).
|
|
199
|
+
|
|
200
|
+
**On web, load the chat through ONE async boundary.** Expo's Metro serializer hoists every module
|
|
201
|
+
two async chunks share into `__common.js`, which loads at first paint. The SDK draws no async chunk
|
|
202
|
+
boundary of its own on web (the call, sounds and pickers import statically there), except the one
|
|
203
|
+
of `@alia.onl/sdk/ask`. So either mount the provider from `@alia.onl/sdk/ask`, or lazy-import your
|
|
204
|
+
own module that imports the root entry (or `@alia.onl/sdk/text`). Don't do both, or the chat they
|
|
205
|
+
share lands in `__common.js`. The text-only entries draw no extra boundary either.
|
|
178
206
|
|
|
179
207
|
The sheet offers the person's latest conversations with its lead under the greeting
|
|
180
208
|
(`recentConversations={false}` turns it off). The code that configures the sheet sits above it, so
|
|
@@ -433,7 +461,7 @@ ships no model identifier of its own.
|
|
|
433
461
|
```sh
|
|
434
462
|
bun run typecheck # tsc --noEmit
|
|
435
463
|
bun run test # vitest
|
|
436
|
-
bun run check:entries #
|
|
464
|
+
bun run check:entries # entry isolation; on web, no async chunk boundary but the ask entry's; no retired modules
|
|
437
465
|
```
|
|
438
466
|
|
|
439
467
|
## Licence
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia.onl/sdk",
|
|
3
|
-
"version": "21.
|
|
3
|
+
"version": "21.4.0",
|
|
4
4
|
"description": "Alia AI chat SDK — the conversation runtime, chat UI, voice calls and dictation for Oxy apps",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.ts",
|
|
11
11
|
"./ask": "./src/ask.ts",
|
|
12
|
+
"./ask/text": "./src/ask-text.ts",
|
|
12
13
|
"./avatar": "./src/avatar.ts",
|
|
13
14
|
"./content": "./src/lib/content-utils.ts",
|
|
14
15
|
"./csp": "./src/csp.ts",
|
|
15
16
|
"./model": "./src/runtime/model.ts",
|
|
16
17
|
"./runtime": "./src/runtime/index.ts",
|
|
18
|
+
"./text": "./src/text.ts",
|
|
17
19
|
"./tools": "./src/tools.ts",
|
|
18
20
|
"./voice": "./src/voice.ts",
|
|
19
21
|
"./package.json": "./package.json"
|
package/src/ask-text.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @alia.onl/sdk/ask/text — `@alia.onl/sdk/ask` without the voice call, and without bundling it
|
|
2
|
+
// The provider and its hook; the sheet loads with the first `ask`, and neither reaches the call's
|
|
3
|
+
// modules. `useAskAgent` is the same hook as `@alia.onl/sdk/ask`'s.
|
|
4
|
+
|
|
5
|
+
export { AliaAskProvider, useAskAgent } from './components/AliaAskProviderText';
|
|
6
|
+
export type {
|
|
7
|
+
AliaAsk,
|
|
8
|
+
AliaAskAgents,
|
|
9
|
+
AliaAskProviderProps,
|
|
10
|
+
} from './components/AliaAskProviderText';
|
package/src/ask.ts
CHANGED
|
@@ -3,5 +3,9 @@
|
|
|
3
3
|
// An app mounts the provider at its root from here rather than from the root entry, which
|
|
4
4
|
// would bring the whole chat along with it.
|
|
5
5
|
|
|
6
|
-
export { AliaAskProvider, useAskAgent } from './components/
|
|
7
|
-
export type {
|
|
6
|
+
export { AliaAskProvider, useAskAgent } from './components/AliaAskProviderLazy';
|
|
7
|
+
export type {
|
|
8
|
+
AliaAsk,
|
|
9
|
+
AliaAskAgents,
|
|
10
|
+
AliaAskProviderProps,
|
|
11
|
+
} from './components/AliaAskProviderLazy';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Ask Alia", "Ask Sindi": one sheet an app mounts once, opened on any agent from anywhere in it.
|
|
3
|
+
*
|
|
4
|
+
* The app names the agents it offers (`agents`, by a key of its own) and configures the sheet
|
|
5
|
+
* once — its host tools, renderers, words, voice, context, upsell and share. A button calls
|
|
6
|
+
* `useAskAgent().ask(key, turn)`. Signed out, the sheet is not mounted (it could not call Alia):
|
|
7
|
+
* `ask` opens Oxy's sign-in instead, and the person asks again once signed in.
|
|
8
|
+
*
|
|
9
|
+
* This module is the provider without its sheet; each entry builds one with `createAskProvider`:
|
|
10
|
+
* - `@alia.onl/sdk/ask` (`AliaAskProviderLazy`) loads the sheet with the first `ask`, through
|
|
11
|
+
* the one `import()` that keeps the chat out of the app's entry bundle;
|
|
12
|
+
* - the root entry (`AliaAskProvider`) already holds the whole chat, so its sheet is a static
|
|
13
|
+
* import. A lazy one there would put an async chunk boundary INSIDE the SDK, and on web Metro
|
|
14
|
+
* hoists every module two async chunks share into `__common.js`, loaded at first paint: an app
|
|
15
|
+
* that lazy-loads the root entry would get the chat in its first paint instead of its own chunk.
|
|
16
|
+
* Both read and write the one context below, so `useAskAgent` works under either.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useAuth, useOxy } from '@oxy.so/services/ui/client';
|
|
20
|
+
import {
|
|
21
|
+
type ComponentType,
|
|
22
|
+
createContext,
|
|
23
|
+
forwardRef,
|
|
24
|
+
type ReactNode,
|
|
25
|
+
type RefAttributes,
|
|
26
|
+
useCallback,
|
|
27
|
+
useContext,
|
|
28
|
+
useEffect,
|
|
29
|
+
useImperativeHandle,
|
|
30
|
+
useMemo,
|
|
31
|
+
useRef,
|
|
32
|
+
useState,
|
|
33
|
+
} from 'react';
|
|
34
|
+
import type { AliaTurn } from '../hooks/useChatComposer';
|
|
35
|
+
import type { AliaChatSheetProps, AliaChatSheetRef } from './AliaChatSheet';
|
|
36
|
+
import type { AliaAskOpening, AliaAskSheetProps } from './AliaAskSheet';
|
|
37
|
+
|
|
38
|
+
/** The agents an app offers, by its own key. An entry without `id` is Alia. */
|
|
39
|
+
export type AliaAskAgents = Readonly<Record<string, { id?: string }>>;
|
|
40
|
+
|
|
41
|
+
export interface AliaAskProviderProps
|
|
42
|
+
extends Pick<
|
|
43
|
+
AliaChatSheetProps,
|
|
44
|
+
| 'app'
|
|
45
|
+
| 'hostTools'
|
|
46
|
+
| 'toolRenderers'
|
|
47
|
+
| 'labels'
|
|
48
|
+
| 'voice'
|
|
49
|
+
| 'clientContext'
|
|
50
|
+
| 'onFeatureLocked'
|
|
51
|
+
| 'onShare'
|
|
52
|
+
| 'model'
|
|
53
|
+
| 'apiUrl'
|
|
54
|
+
| 'characterRuntimeUrl'
|
|
55
|
+
| 'headerMenu'
|
|
56
|
+
| 'recentConversations'
|
|
57
|
+
> {
|
|
58
|
+
/** The agents `ask` opens, by key. Omitted, the sheet only ever opens on Alia. */
|
|
59
|
+
agents?: AliaAskAgents;
|
|
60
|
+
/** The lines offered over Alia's empty composer; an agent offers its own starters. */
|
|
61
|
+
welcomeSuggestions?: AliaChatSheetProps['welcomeSuggestions'];
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AliaAsk {
|
|
66
|
+
/**
|
|
67
|
+
* Open the sheet on the agent `agentKey` (omitted, Alia), with `turn` already sent when given
|
|
68
|
+
* ("ask Sindi about this listing"). Signed out, opens Oxy's sign-in instead.
|
|
69
|
+
*/
|
|
70
|
+
ask: (agentKey?: string, turn?: AliaTurn) => void;
|
|
71
|
+
/** Open the sheet on the agent `agentKey` (omitted, Alia), on its stored conversation `conversationId`. */
|
|
72
|
+
resume: (agentKey: string | undefined, conversationId: string) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Close the sheet — a host tool that changed the page under it shows the person the result, once
|
|
75
|
+
* the turn has been written.
|
|
76
|
+
*/
|
|
77
|
+
dismiss: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const AskContext = createContext<AliaAsk | null>(null);
|
|
81
|
+
|
|
82
|
+
/** The app's way to open its one Alia sheet. Must be used under `AliaAskProvider`. */
|
|
83
|
+
export function useAskAgent(): AliaAsk {
|
|
84
|
+
const ask = useContext(AskContext);
|
|
85
|
+
if (ask === null) throw new Error('useAskAgent must be used within an AliaAskProvider');
|
|
86
|
+
return ask;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The opening that waits for the sheet of agent `key` to be the one mounted. */
|
|
90
|
+
interface Opening extends AliaAskOpening {
|
|
91
|
+
key: string | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The sheet a provider mounts: `AliaAskSheet`, imported statically or behind `lazy` + `Suspense`. */
|
|
95
|
+
export type AskSheetComponent = ComponentType<AliaAskSheetProps & RefAttributes<AliaChatSheetRef>>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Builds the provider around `Sheet`. `preload`, when given, fetches the sheet's module in the
|
|
99
|
+
* background once signed in, so the first `ask` does not wait on the network.
|
|
100
|
+
*/
|
|
101
|
+
export function createAskProvider(Sheet: AskSheetComponent, preload?: () => Promise<unknown>) {
|
|
102
|
+
/**
|
|
103
|
+
* Mounts the app's one sheet. Its ref is the same `AliaAsk` as `useAskAgent()`, for the code
|
|
104
|
+
* that configures the sheet and so sits above it — a host-tool executor that closes it.
|
|
105
|
+
*/
|
|
106
|
+
return forwardRef<AliaAsk, AliaAskProviderProps>(function AliaAskProvider(
|
|
107
|
+
{ agents, welcomeSuggestions, children, labels, ...sheet },
|
|
108
|
+
ref,
|
|
109
|
+
) {
|
|
110
|
+
const { canUsePrivateApi, signIn } = useAuth();
|
|
111
|
+
const { currentLanguage } = useOxy();
|
|
112
|
+
const localizedLabels = useMemo(
|
|
113
|
+
() => ({ ...labels, locale: labels?.locale ?? currentLanguage }),
|
|
114
|
+
[labels, currentLanguage],
|
|
115
|
+
);
|
|
116
|
+
const sheetRef = useRef<AliaChatSheetRef>(null);
|
|
117
|
+
const [opening, setOpening] = useState<Opening | null>(null);
|
|
118
|
+
const [current, setCurrent] = useState<string | undefined>(undefined);
|
|
119
|
+
/** Nothing is mounted before the first `ask`. */
|
|
120
|
+
const [asked, setAsked] = useState(false);
|
|
121
|
+
const agentsRef = useRef(agents);
|
|
122
|
+
agentsRef.current = agents;
|
|
123
|
+
|
|
124
|
+
const open = useCallback(
|
|
125
|
+
(agentKey: string | undefined, turn?: AliaTurn, conversationId?: string) => {
|
|
126
|
+
if (agentKey !== undefined && agentsRef.current?.[agentKey] === undefined) {
|
|
127
|
+
throw new Error(`AliaAskProvider has no agent "${agentKey}"`);
|
|
128
|
+
}
|
|
129
|
+
if (!canUsePrivateApi) {
|
|
130
|
+
void signIn().catch(() => undefined);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setAsked(true);
|
|
134
|
+
setCurrent(agentKey);
|
|
135
|
+
setOpening((previous) => ({
|
|
136
|
+
key: agentKey,
|
|
137
|
+
turn,
|
|
138
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
139
|
+
serial: (previous?.serial ?? 0) + 1,
|
|
140
|
+
}));
|
|
141
|
+
},
|
|
142
|
+
[canUsePrivateApi, signIn],
|
|
143
|
+
);
|
|
144
|
+
const ask = useCallback((agentKey?: string, turn?: AliaTurn) => open(agentKey, turn), [open]);
|
|
145
|
+
const resume = useCallback(
|
|
146
|
+
(agentKey: string | undefined, conversationId: string) =>
|
|
147
|
+
open(agentKey, undefined, conversationId),
|
|
148
|
+
[open],
|
|
149
|
+
);
|
|
150
|
+
const dismiss = useCallback(() => sheetRef.current?.dismiss(), []);
|
|
151
|
+
|
|
152
|
+
const opened = useCallback(() => setOpening(null), []);
|
|
153
|
+
|
|
154
|
+
// Signed in, the sheet's module comes down in the background, ahead of the first `ask`.
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (canUsePrivateApi && preload !== undefined) void preload().catch(() => undefined);
|
|
157
|
+
}, [canUsePrivateApi]);
|
|
158
|
+
|
|
159
|
+
const agentId = current === undefined ? undefined : agents?.[current]?.id;
|
|
160
|
+
const agent = useMemo(() => (agentId === undefined ? undefined : { id: agentId }), [agentId]);
|
|
161
|
+
const value = useMemo<AliaAsk>(() => ({ ask, resume, dismiss }), [ask, resume, dismiss]);
|
|
162
|
+
useImperativeHandle(ref, () => value, [value]);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<AskContext.Provider value={value}>
|
|
166
|
+
{children}
|
|
167
|
+
{canUsePrivateApi && asked ? (
|
|
168
|
+
<Sheet
|
|
169
|
+
key={agentId ?? 'alia'}
|
|
170
|
+
ref={sheetRef}
|
|
171
|
+
{...sheet}
|
|
172
|
+
labels={localizedLabels}
|
|
173
|
+
agent={agent}
|
|
174
|
+
welcomeSuggestions={agent === undefined ? welcomeSuggestions : undefined}
|
|
175
|
+
// A different agent is a new sheet: the opening waits for that one to mount.
|
|
176
|
+
opening={opening !== null && opening.key === current ? opening : null}
|
|
177
|
+
onOpened={opened}
|
|
178
|
+
/>
|
|
179
|
+
) : null}
|
|
180
|
+
</AskContext.Provider>
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -1,175 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The ask provider of the root entry (`@alia.onl/sdk`): its sheet is a static import.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* that mounts this provider at its root — imported from `@alia.onl/sdk/ask` — keeps the chat out
|
|
11
|
-
* of its entry bundle. Signed in, its module is fetched in the background so the first `ask`
|
|
12
|
-
* does not wait on the network.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { useAuth, useOxy } from '@oxy.so/services/ui/client';
|
|
16
|
-
import {
|
|
17
|
-
createContext,
|
|
18
|
-
forwardRef,
|
|
19
|
-
lazy,
|
|
20
|
-
type ReactNode,
|
|
21
|
-
Suspense,
|
|
22
|
-
useCallback,
|
|
23
|
-
useContext,
|
|
24
|
-
useEffect,
|
|
25
|
-
useImperativeHandle,
|
|
26
|
-
useMemo,
|
|
27
|
-
useRef,
|
|
28
|
-
useState,
|
|
29
|
-
} from 'react';
|
|
30
|
-
import type { AliaTurn } from '../hooks/useChatComposer';
|
|
31
|
-
import type { AliaChatSheetProps, AliaChatSheetRef } from './AliaChatSheet';
|
|
32
|
-
import type { AliaAskOpening } from './AliaAskSheet';
|
|
33
|
-
|
|
34
|
-
const loadSheet = () => import('./AliaAskSheet');
|
|
35
|
-
const LazyAskSheet = lazy(() => loadSheet().then((module) => ({ default: module.AliaAskSheet })));
|
|
36
|
-
|
|
37
|
-
/** The agents an app offers, by its own key. An entry without `id` is Alia. */
|
|
38
|
-
export type AliaAskAgents = Readonly<Record<string, { id?: string }>>;
|
|
39
|
-
|
|
40
|
-
export interface AliaAskProviderProps
|
|
41
|
-
extends Pick<
|
|
42
|
-
AliaChatSheetProps,
|
|
43
|
-
| 'app'
|
|
44
|
-
| 'hostTools'
|
|
45
|
-
| 'toolRenderers'
|
|
46
|
-
| 'labels'
|
|
47
|
-
| 'voice'
|
|
48
|
-
| 'clientContext'
|
|
49
|
-
| 'onFeatureLocked'
|
|
50
|
-
| 'onShare'
|
|
51
|
-
| 'model'
|
|
52
|
-
| 'apiUrl'
|
|
53
|
-
| 'characterRuntimeUrl'
|
|
54
|
-
| 'headerMenu'
|
|
55
|
-
| 'recentConversations'
|
|
56
|
-
> {
|
|
57
|
-
/** The agents `ask` opens, by key. Omitted, the sheet only ever opens on Alia. */
|
|
58
|
-
agents?: AliaAskAgents;
|
|
59
|
-
/** The lines offered over Alia's empty composer; an agent offers its own starters. */
|
|
60
|
-
welcomeSuggestions?: AliaChatSheetProps['welcomeSuggestions'];
|
|
61
|
-
children: ReactNode;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface AliaAsk {
|
|
65
|
-
/**
|
|
66
|
-
* Open the sheet on the agent `agentKey` (omitted, Alia), with `turn` already sent when given
|
|
67
|
-
* ("ask Sindi about this listing"). Signed out, opens Oxy's sign-in instead.
|
|
68
|
-
*/
|
|
69
|
-
ask: (agentKey?: string, turn?: AliaTurn) => void;
|
|
70
|
-
/** Open the sheet on the agent `agentKey` (omitted, Alia), on its stored conversation `conversationId`. */
|
|
71
|
-
resume: (agentKey: string | undefined, conversationId: string) => void;
|
|
72
|
-
/**
|
|
73
|
-
* Close the sheet — a host tool that changed the page under it shows the person the result, once
|
|
74
|
-
* the turn has been written.
|
|
75
|
-
*/
|
|
76
|
-
dismiss: () => void;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const AskContext = createContext<AliaAsk | null>(null);
|
|
80
|
-
|
|
81
|
-
/** The app's way to open its one Alia sheet. Must be used under `AliaAskProvider`. */
|
|
82
|
-
export function useAskAgent(): AliaAsk {
|
|
83
|
-
const ask = useContext(AskContext);
|
|
84
|
-
if (ask === null) throw new Error('useAskAgent must be used within an AliaAskProvider');
|
|
85
|
-
return ask;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The opening that waits for the sheet of agent `key` to be the one mounted. */
|
|
89
|
-
interface Opening extends AliaAskOpening {
|
|
90
|
-
key: string | undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Mounts the app's one sheet. Its ref is the same `AliaAsk` as `useAskAgent()`, for the code that
|
|
95
|
-
* configures the sheet and so sits above it — a host-tool executor that closes it.
|
|
4
|
+
* The root entry already holds the whole chat, so loading `AliaAskSheet` lazily here would save
|
|
5
|
+
* nothing and cost a lot on web: it is an async chunk boundary inside the SDK, and Metro hoists
|
|
6
|
+
* every module two async chunks share into `__common.js`, which loads at first paint. An app that
|
|
7
|
+
* lazy-loads its Alia sheet would then get the chat (and the Bloom it draws with) in its first
|
|
8
|
+
* paint. An app that wants the chat out of its entry bundle mounts the provider from
|
|
9
|
+
* `@alia.onl/sdk/ask` (`AliaAskProviderLazy`) instead. See `AliaAskCore` for the provider itself.
|
|
96
10
|
*/
|
|
97
|
-
export const AliaAskProvider = forwardRef<AliaAsk, AliaAskProviderProps>(function AliaAskProvider(
|
|
98
|
-
{ agents, welcomeSuggestions, children, labels, ...sheet },
|
|
99
|
-
ref,
|
|
100
|
-
) {
|
|
101
|
-
const { canUsePrivateApi, signIn } = useAuth();
|
|
102
|
-
const { currentLanguage } = useOxy();
|
|
103
|
-
const localizedLabels = useMemo(
|
|
104
|
-
() => ({ ...labels, locale: labels?.locale ?? currentLanguage }),
|
|
105
|
-
[labels, currentLanguage],
|
|
106
|
-
);
|
|
107
|
-
const sheetRef = useRef<AliaChatSheetRef>(null);
|
|
108
|
-
const [opening, setOpening] = useState<Opening | null>(null);
|
|
109
|
-
const [current, setCurrent] = useState<string | undefined>(undefined);
|
|
110
|
-
/** Nothing is mounted before the first `ask`. */
|
|
111
|
-
const [asked, setAsked] = useState(false);
|
|
112
|
-
const agentsRef = useRef(agents);
|
|
113
|
-
agentsRef.current = agents;
|
|
114
|
-
|
|
115
|
-
const open = useCallback(
|
|
116
|
-
(agentKey: string | undefined, turn?: AliaTurn, conversationId?: string) => {
|
|
117
|
-
if (agentKey !== undefined && agentsRef.current?.[agentKey] === undefined) {
|
|
118
|
-
throw new Error(`AliaAskProvider has no agent "${agentKey}"`);
|
|
119
|
-
}
|
|
120
|
-
if (!canUsePrivateApi) {
|
|
121
|
-
void signIn().catch(() => undefined);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
setAsked(true);
|
|
125
|
-
setCurrent(agentKey);
|
|
126
|
-
setOpening((previous) => ({
|
|
127
|
-
key: agentKey,
|
|
128
|
-
turn,
|
|
129
|
-
...(conversationId === undefined ? {} : { conversationId }),
|
|
130
|
-
serial: (previous?.serial ?? 0) + 1,
|
|
131
|
-
}));
|
|
132
|
-
},
|
|
133
|
-
[canUsePrivateApi, signIn],
|
|
134
|
-
);
|
|
135
|
-
const ask = useCallback((agentKey?: string, turn?: AliaTurn) => open(agentKey, turn), [open]);
|
|
136
|
-
const resume = useCallback(
|
|
137
|
-
(agentKey: string | undefined, conversationId: string) =>
|
|
138
|
-
open(agentKey, undefined, conversationId),
|
|
139
|
-
[open],
|
|
140
|
-
);
|
|
141
|
-
const dismiss = useCallback(() => sheetRef.current?.dismiss(), []);
|
|
142
|
-
|
|
143
|
-
const opened = useCallback(() => setOpening(null), []);
|
|
144
11
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (canUsePrivateApi) void loadSheet().catch(() => undefined);
|
|
148
|
-
}, [canUsePrivateApi]);
|
|
12
|
+
import { createAskProvider } from './AliaAskCore';
|
|
13
|
+
import { AliaAskSheet } from './AliaAskSheetWithVoice';
|
|
149
14
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const value = useMemo<AliaAsk>(() => ({ ask, resume, dismiss }), [ask, resume, dismiss]);
|
|
153
|
-
useImperativeHandle(ref, () => value, [value]);
|
|
15
|
+
export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './AliaAskCore';
|
|
16
|
+
export { useAskAgent } from './AliaAskCore';
|
|
154
17
|
|
|
155
|
-
|
|
156
|
-
<AskContext.Provider value={value}>
|
|
157
|
-
{children}
|
|
158
|
-
{canUsePrivateApi && asked ? (
|
|
159
|
-
<Suspense fallback={null}>
|
|
160
|
-
<LazyAskSheet
|
|
161
|
-
key={agentId ?? 'alia'}
|
|
162
|
-
ref={sheetRef}
|
|
163
|
-
{...sheet}
|
|
164
|
-
labels={localizedLabels}
|
|
165
|
-
agent={agent}
|
|
166
|
-
welcomeSuggestions={agent === undefined ? welcomeSuggestions : undefined}
|
|
167
|
-
// A different agent is a new sheet: the opening waits for that one to mount.
|
|
168
|
-
opening={opening !== null && opening.key === current ? opening : null}
|
|
169
|
-
onOpened={opened}
|
|
170
|
-
/>
|
|
171
|
-
</Suspense>
|
|
172
|
-
) : null}
|
|
173
|
-
</AskContext.Provider>
|
|
174
|
-
);
|
|
175
|
-
});
|
|
18
|
+
export const AliaAskProvider = createAskProvider(AliaAskSheet);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ask provider of `@alia.onl/sdk/ask`: its sheet is loaded lazily and mounted on the first
|
|
3
|
+
* `ask`, so an app that mounts this provider at its root keeps the chat out of its entry bundle.
|
|
4
|
+
* Signed in, the sheet's module is fetched in the background so the first `ask` does not wait on
|
|
5
|
+
* the network.
|
|
6
|
+
*
|
|
7
|
+
* This `import()` is the ONE async boundary the SDK draws, and only for this entry (and
|
|
8
|
+
* `@alia.onl/sdk/ask/text`): everything behind it (`AliaAskSheetWithVoice` and the chat) imports statically on web, so Metro emits one chunk for
|
|
9
|
+
* the sheet and hoists nothing of it into `__common.js` (`scripts/check-entry-isolation.mjs`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { lazy, Suspense } from 'react';
|
|
13
|
+
import { type AskSheetComponent, createAskProvider } from './AliaAskCore';
|
|
14
|
+
|
|
15
|
+
const loadSheet = () => import('./AliaAskSheetWithVoice');
|
|
16
|
+
const LazyAskSheet = lazy(() => loadSheet().then((module) => ({ default: module.AliaAskSheet })));
|
|
17
|
+
|
|
18
|
+
const SuspendedAskSheet: AskSheetComponent = (props) => (
|
|
19
|
+
<Suspense fallback={null}>
|
|
20
|
+
<LazyAskSheet {...props} />
|
|
21
|
+
</Suspense>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './AliaAskCore';
|
|
25
|
+
export { useAskAgent } from './AliaAskCore';
|
|
26
|
+
|
|
27
|
+
export const AliaAskProvider = createAskProvider(SuspendedAskSheet, loadSheet);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ask provider of `@alia.onl/sdk/ask/text`: `AliaAskProviderLazy` without the voice call. Its
|
|
3
|
+
* sheet is loaded lazily, with the first `ask`, and neither the provider nor the sheet reaches the
|
|
4
|
+
* call's modules, so a text-only app bundles none of them.
|
|
5
|
+
*
|
|
6
|
+
* Its `import()` is the one async boundary of this entry, as in `AliaAskProviderLazy`: behind it
|
|
7
|
+
* everything imports statically on web (`scripts/check-entry-isolation.mjs`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { lazy, Suspense } from 'react';
|
|
11
|
+
import { type AskSheetComponent, createAskProvider } from './AliaAskCore';
|
|
12
|
+
|
|
13
|
+
const loadSheet = () => import('./AliaAskSheet');
|
|
14
|
+
const LazyAskSheet = lazy(() => loadSheet().then((module) => ({ default: module.AliaAskSheet })));
|
|
15
|
+
|
|
16
|
+
const SuspendedAskSheet: AskSheetComponent = (props) => (
|
|
17
|
+
<Suspense fallback={null}>
|
|
18
|
+
<LazyAskSheet {...props} />
|
|
19
|
+
</Suspense>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './AliaAskCore';
|
|
23
|
+
export { useAskAgent } from './AliaAskCore';
|
|
24
|
+
|
|
25
|
+
export const AliaAskProvider = createAskProvider(SuspendedAskSheet, loadSheet);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ask sheet with the voice call: what the root entry's `AliaAskProvider` imports and
|
|
3
|
+
* `@alia.onl/sdk/ask` loads with the first `ask`. `@alia.onl/sdk/ask/text` loads `AliaAskSheet`
|
|
4
|
+
* itself, which never reaches the call.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AliaAskSheet as TextAskSheet } from './AliaAskSheet';
|
|
8
|
+
import { withVoiceCall } from './voice/withVoiceCall';
|
|
9
|
+
|
|
10
|
+
export const AliaAskSheet = withVoiceCall(TextAskSheet, 'AliaAskSheet');
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* (`useChatComposer`, `useChatVoice`, `useThreadView`), and this, which draws it — the thread
|
|
8
8
|
* (`AliaThread`) and the agent's history behind it, the composer with its levels, suggestions and
|
|
9
9
|
* questions, and the voice call: the button that opens it, the bar that replaces the composer, the
|
|
10
|
-
* notices and the transcript in the thread. The call
|
|
11
|
-
*
|
|
10
|
+
* notices and the transcript in the thread. The call itself is not imported here: the entry that
|
|
11
|
+
* bundles it hands it down (`VoiceCallContext`, `withVoiceCall`), so the text-only entries
|
|
12
|
+
* (`@alia.onl/sdk/text`, `@alia.onl/sdk/ask/text`) never reach its modules.
|
|
12
13
|
*
|
|
13
14
|
* What a host brings is data and places: the conversation to show, the chrome around it
|
|
14
15
|
* (`chrome`), its rows in the add menu, its words (`labels`), where the thought panel and a
|
|
@@ -30,7 +31,15 @@ import { RiEditLine } from '@oxy.so/bloom/icons/RiEditLine';
|
|
|
30
31
|
import { toast } from '@oxy.so/bloom/toast';
|
|
31
32
|
import { Text } from '@oxy.so/bloom/typography';
|
|
32
33
|
import { useQueryClient } from '@tanstack/react-query';
|
|
33
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
type ReactNode,
|
|
36
|
+
useCallback,
|
|
37
|
+
useContext,
|
|
38
|
+
useEffect,
|
|
39
|
+
useMemo,
|
|
40
|
+
useRef,
|
|
41
|
+
useState,
|
|
42
|
+
} from 'react';
|
|
34
43
|
import { View } from 'react-native';
|
|
35
44
|
import type { SharedValue } from 'react-native-reanimated';
|
|
36
45
|
import { useAmbientWave } from '../hooks/useAmbientWave';
|
|
@@ -66,7 +75,7 @@ import { AmbientField } from './AmbientField';
|
|
|
66
75
|
import { AliaThread } from './thread/AliaThread';
|
|
67
76
|
import { ThreadSearch } from './thread/ThreadSearch';
|
|
68
77
|
import { VoiceModeIcon } from './VoiceModeIcon';
|
|
69
|
-
import {
|
|
78
|
+
import { VoiceCallContext } from './voice/VoiceCallContext';
|
|
70
79
|
|
|
71
80
|
/** The agent a chat is opened on: its lead instead of Alia. */
|
|
72
81
|
export interface AliaChatAgentRef {
|
|
@@ -227,7 +236,8 @@ export interface AliaChatContentProps {
|
|
|
227
236
|
*/
|
|
228
237
|
renderUsageLimit?: (limit: { error: UsageLimitError; dismiss: () => void }) => ReactNode;
|
|
229
238
|
/**
|
|
230
|
-
* The chat's voice call. On by default
|
|
239
|
+
* The chat's voice call. On by default where the call is bundled (the root entry and
|
|
240
|
+
* `@alia.onl/sdk/ask`); the text-only entries have none. `false` makes the chat text-only. An object brings
|
|
231
241
|
* what only the host knows: the gate before a call (`canStartVoice`, `onVoiceBlocked`), what
|
|
232
242
|
* to refresh when one ends (`onCallEnded`), where its notices go (`onNotice`), the sounds it
|
|
233
243
|
* plays. The call always lives in the chat's conversation, made when it starts if the chat has
|
|
@@ -405,8 +415,10 @@ function AliaConversationView({
|
|
|
405
415
|
onSuggestionPicked,
|
|
406
416
|
presentedProgress,
|
|
407
417
|
});
|
|
418
|
+
// Without a call handed down (a text-only entry), the chat is text-only whatever `voice` says.
|
|
419
|
+
const VoiceCall = useContext(VoiceCallContext);
|
|
408
420
|
const voice = useChatVoice({
|
|
409
|
-
options: voiceOptions,
|
|
421
|
+
options: VoiceCall === null ? false : voiceOptions,
|
|
410
422
|
conversationId,
|
|
411
423
|
agentId,
|
|
412
424
|
isFocused,
|
|
@@ -600,8 +612,8 @@ function AliaConversationView({
|
|
|
600
612
|
|
|
601
613
|
const composerNode = (
|
|
602
614
|
<>
|
|
603
|
-
{voice.enabled && voice.isActive ? (
|
|
604
|
-
<
|
|
615
|
+
{VoiceCall !== null && voice.enabled && voice.isActive ? (
|
|
616
|
+
<VoiceCall {...voice.launcher.sessionProps} />
|
|
605
617
|
) : (
|
|
606
618
|
<AliaComposer
|
|
607
619
|
// A different account is a different composer: reads in flight for the last one are
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat components of the root entry: each draws the voice call (`withVoiceCall`). The same
|
|
3
|
+
* components without it — and without bundling it — are `@alia.onl/sdk/text`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AliaChatContent as TextChatContent } from './AliaChatContent';
|
|
7
|
+
import { AliaChatScreen as TextChatScreen } from './AliaChatScreen';
|
|
8
|
+
import { AliaChatSheet as TextChatSheet } from './AliaChatSheet';
|
|
9
|
+
import { withVoiceCall } from './voice/withVoiceCall';
|
|
10
|
+
|
|
11
|
+
export const AliaChatContent = withVoiceCall(TextChatContent, 'AliaChatContent');
|
|
12
|
+
export const AliaChatSheet = withVoiceCall(TextChatSheet, 'AliaChatSheet');
|
|
13
|
+
export const AliaChatScreen = withVoiceCall(TextChatScreen, 'AliaChatScreen');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The call of a conversation screen, on web: its bar, while a call is open.
|
|
3
|
+
*
|
|
4
|
+
* Native loads the call lazily (`AliaVoiceCall.tsx`) so a screen that never calls never evaluates
|
|
5
|
+
* `react-native-audio-api`. Web never reaches that engine (`lib/voice-audio-engine.ts` is the Web
|
|
6
|
+
* Audio one), and a lazy `import()` here would be an async chunk boundary inside the SDK: Metro
|
|
7
|
+
* hoists every module two async chunks share into `__common.js`, loaded at first paint, so an app
|
|
8
|
+
* that lazy-loads its Alia sheet would get the chat in its first paint. On web the call is a
|
|
9
|
+
* static import and travels in whatever chunk holds the chat.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { VoiceSessionProps } from '../../types';
|
|
13
|
+
import { VoiceSession } from './VoiceSession';
|
|
14
|
+
|
|
15
|
+
export function AliaVoiceCall(props: VoiceSessionProps) {
|
|
16
|
+
return <VoiceSession {...props} />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The voice call a conversation can open, handed down by the entry that bundles it.
|
|
3
|
+
*
|
|
4
|
+
* `AliaChatContent` draws the call through this rather than importing it, so a text-only entry
|
|
5
|
+
* (`@alia.onl/sdk/text`, `@alia.onl/sdk/ask/text`) never reaches the call's modules — its socket,
|
|
6
|
+
* protocol and audio engine. The root and `@alia.onl/sdk/ask` provide `AliaVoiceCall` around their
|
|
7
|
+
* chat (`withVoiceCall`); without a provider the chat is text-only, whatever its `voice` prop says.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type ComponentType, createContext } from 'react';
|
|
11
|
+
import type { VoiceSessionProps } from '../../types';
|
|
12
|
+
|
|
13
|
+
export const VoiceCallContext = createContext<ComponentType<VoiceSessionProps> | null>(null);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chat component with the voice call: `Component` drawn under `VoiceCallContext`, which hands
|
|
3
|
+
* it `AliaVoiceCall`. The root entry and `@alia.onl/sdk/ask` export their chat through this; the
|
|
4
|
+
* text-only entries export the components as they are.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ComponentType } from 'react';
|
|
8
|
+
import { AliaVoiceCall } from './AliaVoiceCall';
|
|
9
|
+
import { VoiceCallContext } from './VoiceCallContext';
|
|
10
|
+
|
|
11
|
+
export function withVoiceCall<P extends object>(
|
|
12
|
+
Component: ComponentType<P>,
|
|
13
|
+
displayName: string,
|
|
14
|
+
): ComponentType<P> {
|
|
15
|
+
// A ref is a prop of a function component (React 19), so it reaches a `forwardRef` `Component`.
|
|
16
|
+
function WithVoiceCall(props: P) {
|
|
17
|
+
return (
|
|
18
|
+
<VoiceCallContext.Provider value={AliaVoiceCall}>
|
|
19
|
+
<Component {...props} />
|
|
20
|
+
</VoiceCallContext.Provider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
WithVoiceCall.displayName = displayName;
|
|
24
|
+
return WithVoiceCall;
|
|
25
|
+
}
|
|
@@ -2,6 +2,7 @@ import { RiFileTextLine } from '@oxy.so/bloom/icons/RiFileTextLine';
|
|
|
2
2
|
import { RiImageLine } from '@oxy.so/bloom/icons/RiImageLine';
|
|
3
3
|
import { useCallback, useMemo } from 'react';
|
|
4
4
|
import type { AliaAddMenu } from '../lib/add-menu';
|
|
5
|
+
import { loadDocumentPicker } from '../lib/deferred/document-picker';
|
|
5
6
|
import type { IntakeKind } from '../lib/attachment-intake';
|
|
6
7
|
import { useLabels } from '../lib/labels';
|
|
7
8
|
import { ALIA_FEATURES, type FeatureOffer } from '../runtime/agents';
|
|
@@ -71,17 +72,15 @@ export function useAttachMenu(
|
|
|
71
72
|
size: asset.size,
|
|
72
73
|
mimeType: asset.mimeType,
|
|
73
74
|
}))
|
|
74
|
-
: //
|
|
75
|
-
((await (await
|
|
76
|
-
(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
75
|
+
: // On native the picker loads with the first document: a chat that never attaches one never does.
|
|
76
|
+
((await (await loadDocumentPicker()).pickDocuments()) ?? []).map((asset) => ({
|
|
77
|
+
id: attachmentId('doc'),
|
|
78
|
+
type: 'document' as const,
|
|
79
|
+
name: asset.name,
|
|
80
|
+
uri: asset.uri,
|
|
81
|
+
size: asset.size,
|
|
82
|
+
mimeType: asset.mimeType,
|
|
83
|
+
}));
|
|
85
84
|
for (const attachment of picked) store.addAttachment(address, attachment);
|
|
86
85
|
},
|
|
87
86
|
[offers, onFeatureLocked, pickImage, address],
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { useEffect, useRef, useCallback } from 'react';
|
|
11
11
|
import type { AudioPlayer, AudioSource } from 'expo-audio';
|
|
12
|
+
import { loadExpoAudio } from '../lib/deferred/expo-audio';
|
|
12
13
|
import type { AgentState } from '../types';
|
|
13
14
|
|
|
14
15
|
export type SoundName = 'thinking' | 'toolCall' | 'voiceConnect' | 'voiceDisconnect';
|
|
@@ -37,7 +38,7 @@ export function useSoundEffects(enabled: boolean = false, sounds: SoundSources =
|
|
|
37
38
|
|
|
38
39
|
(async () => {
|
|
39
40
|
try {
|
|
40
|
-
const { createAudioPlayer } = await
|
|
41
|
+
const { createAudioPlayer } = await loadExpoAudio();
|
|
41
42
|
if (!mounted) return;
|
|
42
43
|
|
|
43
44
|
for (const name of SOUND_NAMES) {
|
package/src/hooks/useTTS.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { useOxy } from '@oxy.so/services/ui/client';
|
|
|
5
5
|
import { errorMessage } from '../lib/utils';
|
|
6
6
|
import { create } from 'zustand';
|
|
7
7
|
import { createAudioLevelMeter } from '../lib/audio-level';
|
|
8
|
+
import { loadExpoAudio } from '../lib/deferred/expo-audio';
|
|
8
9
|
import { requestSpeechClip } from '../lib/speech-synthesis';
|
|
9
10
|
import { ALIA_API_URL } from '../lib/origins';
|
|
10
11
|
|
|
@@ -142,7 +143,7 @@ export function useTTS(options: UseTTSOptions = {}) {
|
|
|
142
143
|
|
|
143
144
|
(async () => {
|
|
144
145
|
try {
|
|
145
|
-
const { createAudioPlayer } = await
|
|
146
|
+
const { createAudioPlayer } = await loadExpoAudio();
|
|
146
147
|
if (signal.aborted) return;
|
|
147
148
|
/**
|
|
148
149
|
* `crossOrigin` is web-only and ignored elsewhere, and it is what makes
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Reusable chat UI, voice, and streaming components for Oxy apps
|
|
4
4
|
|
|
5
5
|
// ── Main components ──
|
|
6
|
-
export { AliaChatContent } from './components/
|
|
6
|
+
export { AliaChatContent } from './components/AliaChatWithVoice';
|
|
7
7
|
export type {
|
|
8
8
|
AliaChatAgentRef,
|
|
9
9
|
AliaChatChrome,
|
|
@@ -25,11 +25,11 @@ export { AliaSharedConversation } from './components/AliaSharedConversation';
|
|
|
25
25
|
export type { AliaSharedConversationProps } from './components/AliaSharedConversation';
|
|
26
26
|
export type { AliaAddMenu } from './lib/add-menu';
|
|
27
27
|
export type { AliaVoiceOptions } from './hooks/useChatVoice';
|
|
28
|
-
export { AliaChatSheet } from './components/
|
|
28
|
+
export { AliaChatSheet } from './components/AliaChatWithVoice';
|
|
29
29
|
export type { AliaChatSheetProps, AliaChatSheetRef } from './components/AliaChatSheet';
|
|
30
30
|
export { AliaRecentConversations } from './components/AliaRecentConversations';
|
|
31
31
|
export type { AliaTurn } from './hooks/useChatComposer';
|
|
32
|
-
export { AliaChatScreen } from './components/
|
|
32
|
+
export { AliaChatScreen } from './components/AliaChatWithVoice';
|
|
33
33
|
export type { AliaChatScreenProps } from './components/AliaChatScreen';
|
|
34
34
|
|
|
35
35
|
// ── Chat UI components ──
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The document picker on web, imported with the composer.
|
|
3
|
+
*
|
|
4
|
+
* On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
|
|
5
|
+
* and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
|
|
6
|
+
* an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
|
|
7
|
+
* lazy load (`./document-picker.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as DocumentPicker from '../document-picker';
|
|
11
|
+
|
|
12
|
+
export const loadDocumentPicker = async () => DocumentPicker;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `expo-audio` on web, imported with the chat.
|
|
3
|
+
*
|
|
4
|
+
* On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
|
|
5
|
+
* and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
|
|
6
|
+
* an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
|
|
7
|
+
* lazy load (`./expo-audio.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as ExpoAudio from 'expo-audio';
|
|
11
|
+
|
|
12
|
+
export const loadExpoAudio = async () => ExpoAudio;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attachment reader on web, imported with the runtime.
|
|
3
|
+
*
|
|
4
|
+
* On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
|
|
5
|
+
* and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
|
|
6
|
+
* an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
|
|
7
|
+
* lazy load (`./image-data-url.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as ImageDataUrl from '../image-data-url';
|
|
11
|
+
|
|
12
|
+
export const loadImageDataUrl = async () => ImageDataUrl;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createAudioLevelMeter } from './audio-level';
|
|
12
|
+
import { loadExpoAudio } from './deferred/expo-audio';
|
|
12
13
|
|
|
13
14
|
export type ProductVoice = 'male' | 'female';
|
|
14
15
|
|
|
@@ -171,7 +172,7 @@ export interface PlayClipOptions {
|
|
|
171
172
|
export async function playSpeechClip(uri: string, options: PlayClipOptions): Promise<void> {
|
|
172
173
|
const { signal, onLevel } = options;
|
|
173
174
|
if (signal.aborted) return;
|
|
174
|
-
const { createAudioPlayer } = await
|
|
175
|
+
const { createAudioPlayer } = await loadExpoAudio();
|
|
175
176
|
if (signal.aborted) return;
|
|
176
177
|
|
|
177
178
|
const player = createAudioPlayer({ uri }, { crossOrigin: 'anonymous' });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadImageDataUrl } from '../lib/deferred/image-data-url';
|
|
1
2
|
import type { AliaAttachment } from '../types';
|
|
2
3
|
|
|
3
4
|
export type MessageContentPart =
|
|
@@ -60,9 +61,9 @@ export async function buildMessageContent(
|
|
|
60
61
|
|
|
61
62
|
const parts: MessageContentPart[] = [{ type: 'text', text }];
|
|
62
63
|
|
|
63
|
-
// The readers need the platform's file access, loaded on the first attachment: a turn
|
|
64
|
-
// text (and everything that imports this module) never touches it.
|
|
65
|
-
const { imageUriToDataUrl } = await
|
|
64
|
+
// The readers need the platform's file access, loaded on the first attachment: on native a turn
|
|
65
|
+
// of plain text (and everything that imports this module) never touches it.
|
|
66
|
+
const { imageUriToDataUrl } = await loadImageDataUrl();
|
|
66
67
|
for (const att of usable) {
|
|
67
68
|
if (att.type !== 'image' && upload !== undefined) {
|
|
68
69
|
try {
|
|
@@ -30,8 +30,11 @@ const FIVE_MINUTES = 1000 * 60 * 5;
|
|
|
30
30
|
|
|
31
31
|
/** A stable signal for a URL whose server-side conversation no longer exists. */
|
|
32
32
|
export class ConversationNotFoundError extends Error {
|
|
33
|
-
|
|
33
|
+
readonly conversationId: string;
|
|
34
|
+
|
|
35
|
+
constructor(conversationId: string) {
|
|
34
36
|
super('Conversation not found');
|
|
37
|
+
this.conversationId = conversationId;
|
|
35
38
|
this.name = 'ConversationNotFoundError';
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -54,7 +57,11 @@ function messageText(message: Message | undefined): string {
|
|
|
54
57
|
* it holds nothing, and the 401 is the caller's to handle.
|
|
55
58
|
*/
|
|
56
59
|
class OfflineConversations {
|
|
57
|
-
|
|
60
|
+
private readonly store: OfflineConversationStore | null;
|
|
61
|
+
|
|
62
|
+
constructor(store: OfflineConversationStore | null) {
|
|
63
|
+
this.store = store;
|
|
64
|
+
}
|
|
58
65
|
|
|
59
66
|
get available(): boolean {
|
|
60
67
|
return this.store !== null;
|
package/src/text.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @alia.onl/sdk/text — the chat without the voice call, and without bundling it
|
|
2
|
+
// The same `AliaChatSheet`, `AliaChatScreen` and `AliaChatContent` as the root entry, but none of
|
|
3
|
+
// them reaches the call's modules (its socket, protocol and audio engine): a text-only app takes
|
|
4
|
+
// its chat from here. Read-aloud and dictation stay — they are the composer's and the thread's.
|
|
5
|
+
// Types can be imported from the root entry (they are erased); values from the root would bring
|
|
6
|
+
// the call along.
|
|
7
|
+
|
|
8
|
+
export { AliaChatContent } from './components/AliaChatContent';
|
|
9
|
+
export type {
|
|
10
|
+
AliaChatAgentRef,
|
|
11
|
+
AliaChatChrome,
|
|
12
|
+
AliaChatContentProps,
|
|
13
|
+
AliaChatState,
|
|
14
|
+
} from './components/AliaChatContent';
|
|
15
|
+
export { AliaChatSheet } from './components/AliaChatSheet';
|
|
16
|
+
export type { AliaChatSheetProps, AliaChatSheetRef } from './components/AliaChatSheet';
|
|
17
|
+
export { AliaChatScreen } from './components/AliaChatScreen';
|
|
18
|
+
export type { AliaChatScreenProps } from './components/AliaChatScreen';
|