@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 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.1.0",
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 { AliaChatSheet, type AliaChatSheetProps, type AliaChatSheetRef } from './AliaChatSheet';
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
- /** Which agent the sheet is on, and the opening that waits for that sheet to mount. */
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
- // Presented once the sheet for that agent is the one mounted (a different agent is a new sheet).
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 (opening === null || opening.key !== current) return;
131
- if (opening.conversationId !== undefined) sheetRef.current?.resume(opening.conversationId);
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
- <AliaChatSheet
146
- key={agentId ?? 'alia'}
147
- ref={sheetRef}
148
- {...sheet}
149
- agent={agent}
150
- welcomeSuggestions={agent === undefined ? welcomeSuggestions : undefined}
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: NativeSyntheticEvent<TextInputKeyPressEventData>) => void;
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<TextInput | null>;
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 { NativeSyntheticEvent, TextInput, TextInputKeyPressEventData } from 'react-native';
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<TextInput | null>(null);
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: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
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 { NativeSyntheticEvent, TextInputKeyPressEventData } from 'react-native';
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: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
65
+ (event: TextInputKeyPressEvent) => {
66
66
  if (completions.length === 0) return;
67
67
  const key = event.nativeEvent.key;
68
68
  if (key === 'ArrowDown' || key === 'ArrowUp') {
@@ -16,11 +16,14 @@ type KeyboardAwareScrollViewProps = ScrollViewProps & {
16
16
  extraKeyboardSpace?: number;
17
17
  };
18
18
 
19
- const KeyboardAwareScrollView = React.forwardRef<ScrollView, KeyboardAwareScrollViewProps>(
20
- ({ bottomOffset, disableScrollOnKeyboardHide, enabled, extraKeyboardSpace, ...props }, ref) => (
21
- <ScrollView ref={ref} {...props} />
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>;