@alia.onl/sdk 20.1.0 → 20.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 +21 -0
- package/README.md +5 -1
- package/package.json +2 -1
- package/src/ask.ts +7 -0
- package/src/components/AliaAskProvider.tsx +35 -22
- package/src/components/AliaAskSheet.tsx +52 -0
- package/src/components/AliaComposer.tsx +4 -8
- package/src/hooks/useChatComposer.ts +4 -3
- package/src/hooks/useComposerSuggestions.ts +2 -2
- package/src/lib/keyboard.tsx +8 -5
- package/src/lib/text-input.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @alia.onl/sdk
|
|
2
2
|
|
|
3
|
+
## 20.2.0
|
|
4
|
+
|
|
5
|
+
An app mounts its one sheet at the root without the chat in its entry bundle.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`@alia.onl/sdk/ask`**, a leaf entry with `AliaAskProvider` and `useAskAgent` alone. The root
|
|
10
|
+
entry still exports both, but brings the whole chat with them.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`AliaAskProvider` loads its sheet lazily** (`components/AliaAskSheet.tsx`, one `import()`) and
|
|
15
|
+
mounts it on the first `ask`. Signed in, the module is fetched in the background, so the first
|
|
16
|
+
`ask` does not wait on the network. `check:entries` holds that `./ask` loads none of the chat
|
|
17
|
+
with the entry.
|
|
18
|
+
- **Ref and event types are React Native 0.88-ready.** A ref to a `ScrollView` or `TextInput` is
|
|
19
|
+
typed `ComponentRef<typeof …>` (0.88 types both as function components), and key presses are
|
|
20
|
+
`TextInputKeyPressEvent` (`TextInputKeyPressEventData` is deprecated since 0.86). Nothing
|
|
21
|
+
changes on 0.86. The peer ranges stay on Expo 57 / React Native 0.86: Bloom's own `TextInput`
|
|
22
|
+
ref props are not 0.88-ready yet.
|
|
23
|
+
|
|
3
24
|
## 20.1.0
|
|
4
25
|
|
|
5
26
|
The one sheet is the whole surface an app needs for an agent: it picks a conversation up again and
|
package/README.md
CHANGED
|
@@ -146,7 +146,7 @@ Every agent — Alia included — is the same agent (ADR 0020), and any Oxy app
|
|
|
146
146
|
person can reach. The wire contract is [`docs/agent-embedding.md`](../../docs/agent-embedding.md).
|
|
147
147
|
|
|
148
148
|
```tsx
|
|
149
|
-
import { AliaAskProvider, useAskAgent } from '@alia.onl/sdk';
|
|
149
|
+
import { AliaAskProvider, useAskAgent } from '@alia.onl/sdk/ask';
|
|
150
150
|
|
|
151
151
|
<AliaAskProvider
|
|
152
152
|
agents={{ alia: {}, sindi: { id: SINDI_AGENT_ID } }} // no id = Alia
|
|
@@ -169,6 +169,10 @@ resume('sindi', conversationId); // a stored conversation, where the person left
|
|
|
169
169
|
dismiss(); // e.g. after a host tool changed the page under the sheet
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
Mount it from `@alia.onl/sdk/ask`: that entry is the provider and its hook alone, and the sheet
|
|
173
|
+
loads with the first `ask` (fetched in the background once the person is signed in), so the chat
|
|
174
|
+
stays out of the app's entry bundle. The root entry exports the same two, with the whole chat.
|
|
175
|
+
|
|
172
176
|
The sheet offers the person's latest conversations with its lead under the greeting
|
|
173
177
|
(`recentConversations={false}` turns it off). The code that configures the sheet sits above it, so
|
|
174
178
|
`AliaAskProvider` forwards a ref to the same `AliaAsk` for it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia.onl/sdk",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.2.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",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"types": "src/index.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.ts",
|
|
11
|
+
"./ask": "./src/ask.ts",
|
|
11
12
|
"./avatar": "./src/avatar.ts",
|
|
12
13
|
"./content": "./src/lib/content-utils.ts",
|
|
13
14
|
"./csp": "./src/csp.ts",
|
package/src/ask.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @alia.onl/sdk/ask — the app's one Alia sheet, without the chat in the entry bundle
|
|
2
|
+
// `AliaAskProvider` and `useAskAgent` alone: the sheet they open is loaded with the first `ask`.
|
|
3
|
+
// An app mounts the provider at its root from here rather than from the root entry, which
|
|
4
|
+
// would bring the whole chat along with it.
|
|
5
|
+
|
|
6
|
+
export { AliaAskProvider, useAskAgent } from './components/AliaAskProvider';
|
|
7
|
+
export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './components/AliaAskProvider';
|
|
@@ -5,13 +5,20 @@
|
|
|
5
5
|
* once — its host tools, renderers, words, voice, context, upsell and share. A button calls
|
|
6
6
|
* `useAskAgent().ask(key, turn)`. Signed out, the sheet is not mounted (it could not call Alia):
|
|
7
7
|
* `ask` opens Oxy's sign-in instead, and the person asks again once signed in.
|
|
8
|
+
*
|
|
9
|
+
* The sheet itself (`AliaAskSheet`) is loaded lazily and mounted on the first `ask`, so an app
|
|
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.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { useAuth } from '@oxy.so/services/ui/client';
|
|
11
16
|
import {
|
|
12
17
|
createContext,
|
|
13
18
|
forwardRef,
|
|
19
|
+
lazy,
|
|
14
20
|
type ReactNode,
|
|
21
|
+
Suspense,
|
|
15
22
|
useCallback,
|
|
16
23
|
useContext,
|
|
17
24
|
useEffect,
|
|
@@ -21,7 +28,11 @@ import {
|
|
|
21
28
|
useState,
|
|
22
29
|
} from 'react';
|
|
23
30
|
import type { AliaTurn } from '../hooks/useChatComposer';
|
|
24
|
-
import {
|
|
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 })));
|
|
25
36
|
|
|
26
37
|
/** The agents an app offers, by its own key. An entry without `id` is Alia. */
|
|
27
38
|
export type AliaAskAgents = Readonly<Record<string, { id?: string }>>;
|
|
@@ -73,14 +84,9 @@ export function useAskAgent(): AliaAsk {
|
|
|
73
84
|
return ask;
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
/**
|
|
77
|
-
interface Opening {
|
|
87
|
+
/** The opening that waits for the sheet of agent `key` to be the one mounted. */
|
|
88
|
+
interface Opening extends AliaAskOpening {
|
|
78
89
|
key: string | undefined;
|
|
79
|
-
turn: AliaTurn | undefined;
|
|
80
|
-
/** A stored conversation to open instead of the welcome. */
|
|
81
|
-
conversationId?: string;
|
|
82
|
-
/** Changes on every `ask`, so asking the same agent twice opens twice. */
|
|
83
|
-
serial: number;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
@@ -95,6 +101,8 @@ export const AliaAskProvider = forwardRef<AliaAsk, AliaAskProviderProps>(functio
|
|
|
95
101
|
const sheetRef = useRef<AliaChatSheetRef>(null);
|
|
96
102
|
const [opening, setOpening] = useState<Opening | null>(null);
|
|
97
103
|
const [current, setCurrent] = useState<string | undefined>(undefined);
|
|
104
|
+
/** Nothing is mounted before the first `ask`. */
|
|
105
|
+
const [asked, setAsked] = useState(false);
|
|
98
106
|
const agentsRef = useRef(agents);
|
|
99
107
|
agentsRef.current = agents;
|
|
100
108
|
|
|
@@ -107,6 +115,7 @@ export const AliaAskProvider = forwardRef<AliaAsk, AliaAskProviderProps>(functio
|
|
|
107
115
|
void signIn().catch(() => undefined);
|
|
108
116
|
return;
|
|
109
117
|
}
|
|
118
|
+
setAsked(true);
|
|
110
119
|
setCurrent(agentKey);
|
|
111
120
|
setOpening((previous) => ({
|
|
112
121
|
key: agentKey,
|
|
@@ -125,13 +134,12 @@ export const AliaAskProvider = forwardRef<AliaAsk, AliaAskProviderProps>(functio
|
|
|
125
134
|
);
|
|
126
135
|
const dismiss = useCallback(() => sheetRef.current?.dismiss(), []);
|
|
127
136
|
|
|
128
|
-
|
|
137
|
+
const opened = useCallback(() => setOpening(null), []);
|
|
138
|
+
|
|
139
|
+
// Signed in, the sheet's module comes down in the background, ahead of the first `ask`.
|
|
129
140
|
useEffect(() => {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
else sheetRef.current?.present(opening.turn);
|
|
133
|
-
setOpening(null);
|
|
134
|
-
}, [opening, current]);
|
|
141
|
+
if (canUsePrivateApi) void loadSheet().catch(() => undefined);
|
|
142
|
+
}, [canUsePrivateApi]);
|
|
135
143
|
|
|
136
144
|
const agentId = current === undefined ? undefined : agents?.[current]?.id;
|
|
137
145
|
const agent = useMemo(() => (agentId === undefined ? undefined : { id: agentId }), [agentId]);
|
|
@@ -141,14 +149,19 @@ export const AliaAskProvider = forwardRef<AliaAsk, AliaAskProviderProps>(functio
|
|
|
141
149
|
return (
|
|
142
150
|
<AskContext.Provider value={value}>
|
|
143
151
|
{children}
|
|
144
|
-
{canUsePrivateApi ? (
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
{canUsePrivateApi && asked ? (
|
|
153
|
+
<Suspense fallback={null}>
|
|
154
|
+
<LazyAskSheet
|
|
155
|
+
key={agentId ?? 'alia'}
|
|
156
|
+
ref={sheetRef}
|
|
157
|
+
{...sheet}
|
|
158
|
+
agent={agent}
|
|
159
|
+
welcomeSuggestions={agent === undefined ? welcomeSuggestions : undefined}
|
|
160
|
+
// A different agent is a new sheet: the opening waits for that one to mount.
|
|
161
|
+
opening={opening !== null && opening.key === current ? opening : null}
|
|
162
|
+
onOpened={opened}
|
|
163
|
+
/>
|
|
164
|
+
</Suspense>
|
|
152
165
|
) : null}
|
|
153
166
|
</AskContext.Provider>
|
|
154
167
|
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sheet `AliaAskProvider` mounts, in its own module so the provider can load it lazily: an app
|
|
3
|
+
* that mounts the provider at its root carries only the provider in its entry bundle, and the
|
|
4
|
+
* chat — thread, composer, voice, renderers — arrives with the first `ask`.
|
|
5
|
+
*
|
|
6
|
+
* It presents the opening it is handed once it is mounted, which is the only moment a lazily
|
|
7
|
+
* loaded sheet can be presented, and tells the provider the opening is spent.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
11
|
+
import type { AliaTurn } from '../hooks/useChatComposer';
|
|
12
|
+
import { AliaChatSheet, type AliaChatSheetProps, type AliaChatSheetRef } from './AliaChatSheet';
|
|
13
|
+
|
|
14
|
+
/** What the person asked for: a turn to send, or a stored conversation to pick up again. */
|
|
15
|
+
export interface AliaAskOpening {
|
|
16
|
+
turn: AliaTurn | undefined;
|
|
17
|
+
conversationId?: string;
|
|
18
|
+
/** Changes on every `ask`, so asking the same agent twice opens twice. */
|
|
19
|
+
serial: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AliaAskSheetProps extends AliaChatSheetProps {
|
|
23
|
+
/** The opening to present once mounted; `null` when there is none waiting. */
|
|
24
|
+
opening: AliaAskOpening | null;
|
|
25
|
+
/** The opening was presented. */
|
|
26
|
+
onOpened: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const AliaAskSheet = forwardRef<AliaChatSheetRef, AliaAskSheetProps>(function AliaAskSheet(
|
|
30
|
+
{ opening, onOpened, ...sheet },
|
|
31
|
+
ref,
|
|
32
|
+
) {
|
|
33
|
+
const sheetRef = useRef<AliaChatSheetRef>(null);
|
|
34
|
+
useImperativeHandle(
|
|
35
|
+
ref,
|
|
36
|
+
() => ({
|
|
37
|
+
present: (turn?: AliaTurn) => sheetRef.current?.present(turn),
|
|
38
|
+
resume: (conversationId: string) => sheetRef.current?.resume(conversationId),
|
|
39
|
+
dismiss: () => sheetRef.current?.dismiss(),
|
|
40
|
+
}),
|
|
41
|
+
[],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (opening === null) return;
|
|
46
|
+
if (opening.conversationId !== undefined) sheetRef.current?.resume(opening.conversationId);
|
|
47
|
+
else sheetRef.current?.present(opening.turn);
|
|
48
|
+
onOpened();
|
|
49
|
+
}, [opening, onOpened]);
|
|
50
|
+
|
|
51
|
+
return <AliaChatSheet ref={sheetRef} {...sheet} />;
|
|
52
|
+
});
|
|
@@ -7,12 +7,7 @@ import {
|
|
|
7
7
|
type ReactNode,
|
|
8
8
|
type RefObject,
|
|
9
9
|
} from 'react';
|
|
10
|
-
import {
|
|
11
|
-
View,
|
|
12
|
-
type NativeSyntheticEvent,
|
|
13
|
-
type TextInput,
|
|
14
|
-
type TextInputKeyPressEventData,
|
|
15
|
-
} from 'react-native';
|
|
10
|
+
import { View, type TextInputKeyPressEvent } from 'react-native';
|
|
16
11
|
import {
|
|
17
12
|
ComposerPill,
|
|
18
13
|
type ComposerPanelAddMenuGroup,
|
|
@@ -28,6 +23,7 @@ import type { Completion } from '../lib/suggestions';
|
|
|
28
23
|
import { composerTiles, intakeError } from '../lib/attachment-tiles';
|
|
29
24
|
import type { IntakeKind } from '../lib/attachment-intake';
|
|
30
25
|
import { KeyboardAvoidingView } from '../lib/keyboard';
|
|
26
|
+
import type { TextInputHandle } from '../lib/text-input';
|
|
31
27
|
import { type AliaChatLabels, AliaChatLabelsProvider, useLabels } from '../lib/labels';
|
|
32
28
|
import type { AskedQuestion } from '../lib/questions';
|
|
33
29
|
import type { VoiceErrorCode } from '../lib/speech-messages';
|
|
@@ -116,11 +112,11 @@ export interface AliaComposerProps<T extends AliaSuggestion = AliaSuggestion> {
|
|
|
116
112
|
*/
|
|
117
113
|
accessory?: ReactNode;
|
|
118
114
|
/** The field's keys, before the panel's own Enter rule (suggestions over it). */
|
|
119
|
-
onKeyPress?: (event:
|
|
115
|
+
onKeyPress?: (event: TextInputKeyPressEvent) => void;
|
|
120
116
|
/** The host already avoids the keyboard. */
|
|
121
117
|
disableKeyboardAvoidance?: boolean;
|
|
122
118
|
/** The field itself, for a host that moves focus into it. */
|
|
123
|
-
inputRef?: RefObject<
|
|
119
|
+
inputRef?: RefObject<TextInputHandle | null>;
|
|
124
120
|
/** Dictation's language, a BCP-47 tag; omitted, the platform's own. */
|
|
125
121
|
speechLanguage?: string;
|
|
126
122
|
/** Alia's API, which transcribes dictation; omitted, `EXPO_PUBLIC_ALIA_API_URL` or production. */
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { AvatarFlights } from '@oxy.so/bloom/avatar-flight';
|
|
2
2
|
import { useOxy } from '@oxy.so/services/ui/client';
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
|
-
import type {
|
|
4
|
+
import type { TextInputKeyPressEvent } from 'react-native';
|
|
5
5
|
import type { SharedValue } from 'react-native-reanimated';
|
|
6
6
|
import { leadToProfile } from '../lib/flights';
|
|
7
7
|
import type { AliaLead } from '../lib/participants';
|
|
8
|
+
import type { TextInputHandle } from '../lib/text-input';
|
|
8
9
|
import { imageUriToDataUrl } from '../lib/image-data-url';
|
|
9
10
|
import {
|
|
10
11
|
useComposerDraft,
|
|
@@ -254,7 +255,7 @@ export function useChatComposer({
|
|
|
254
255
|
});
|
|
255
256
|
|
|
256
257
|
// ── The field: focus, and its keys ──
|
|
257
|
-
const inputRef = useRef<
|
|
258
|
+
const inputRef = useRef<TextInputHandle | null>(null);
|
|
258
259
|
useFocusWhenPresented(presentedProgress, () => inputRef.current?.focus());
|
|
259
260
|
// Starting an edit puts the caret in the composer that now holds the question: the Edit press
|
|
260
261
|
// left focus on the row's button, so typing went nowhere and Escape could not reach the edit.
|
|
@@ -265,7 +266,7 @@ export function useChatComposer({
|
|
|
265
266
|
const { onKeyPress: onSuggestionKeyPress } = suggestions;
|
|
266
267
|
const cancelEdit = edit.cancel;
|
|
267
268
|
const onKeyPress = useCallback(
|
|
268
|
-
(event:
|
|
269
|
+
(event: TextInputKeyPressEvent) => {
|
|
269
270
|
// Escape leaves an edit, as its Cancel does: it is the top-most thing open while the
|
|
270
271
|
// question is being rewritten.
|
|
271
272
|
if (editing !== undefined && event.nativeEvent.key === 'Escape') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { TextInputKeyPressEvent } from 'react-native';
|
|
3
3
|
import {
|
|
4
4
|
type Completion,
|
|
5
5
|
SEARCH_DEBOUNCE_MS,
|
|
@@ -62,7 +62,7 @@ export function useComposerSuggestions<T extends AliaSuggestion>({
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
const onKeyPress = useCallback(
|
|
65
|
-
(event:
|
|
65
|
+
(event: TextInputKeyPressEvent) => {
|
|
66
66
|
if (completions.length === 0) return;
|
|
67
67
|
const key = event.nativeEvent.key;
|
|
68
68
|
if (key === 'ArrowDown' || key === 'ArrowUp') {
|
package/src/lib/keyboard.tsx
CHANGED
|
@@ -16,11 +16,14 @@ type KeyboardAwareScrollViewProps = ScrollViewProps & {
|
|
|
16
16
|
extraKeyboardSpace?: number;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
// `ComponentRef`, not `ScrollView` itself: React Native 0.88 types `ScrollView` as a function
|
|
20
|
+
// component, so the class is no longer its instance type.
|
|
21
|
+
const KeyboardAwareScrollView = React.forwardRef<
|
|
22
|
+
React.ComponentRef<typeof ScrollView>,
|
|
23
|
+
KeyboardAwareScrollViewProps
|
|
24
|
+
>(({ bottomOffset, disableScrollOnKeyboardHide, enabled, extraKeyboardSpace, ...props }, ref) => (
|
|
25
|
+
<ScrollView ref={ref} {...props} />
|
|
26
|
+
));
|
|
24
27
|
KeyboardAwareScrollView.displayName = 'KeyboardAwareScrollView';
|
|
25
28
|
|
|
26
29
|
// `automaticOffset` is keyboard-controller's: measure the view's position in
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComponentRef } from 'react';
|
|
2
|
+
import type { TextInput } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a ref to a `TextInput` holds. `ComponentRef`, not `TextInput` itself: React Native 0.88
|
|
6
|
+
* types `TextInput` as a function component, so the class is no longer its instance type, and
|
|
7
|
+
* `focus()` is only on the instance.
|
|
8
|
+
*/
|
|
9
|
+
export type TextInputHandle = ComponentRef<typeof TextInput>;
|