@alia.onl/sdk 20.0.0 → 20.1.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,22 @@
1
1
  # @alia.onl/sdk
2
2
 
3
+ ## 20.1.0
4
+
5
+ The one sheet is the whole surface an app needs for an agent: it picks a conversation up again and
6
+ the app can close it.
7
+
8
+ ### Added
9
+
10
+ - **`useAskAgent().resume(agentKey, conversationId)`** opens the sheet of that agent on a stored
11
+ conversation, where the person left it (`AliaChatSheetRef.resume`).
12
+ - **`useAskAgent().dismiss()`**, and `AliaAskProvider` forwards a ref to the same `AliaAsk`, for
13
+ the code that configures the sheet and so sits above it — a host-tool executor that changed the
14
+ page under the sheet closes it once the turn is written.
15
+ - **The sheet's recent conversations.** Under the greeting, the person's three latest conversations
16
+ with the lead (`AliaRecentConversations`; Alia's sheet lists any). `recentConversations={false}`
17
+ turns it off. New label `recentConversations` (`'Recent'`).
18
+ - **`AliaChatContent` `welcomeAccessory`**, drawn under the empty chat's greeting.
19
+
3
20
  ## 20.0.0
4
21
 
5
22
  Generic chat UI is Bloom's; the SDK composes it and keeps only what is Alia's. Two copies the SDK
package/README.md CHANGED
@@ -163,10 +163,16 @@ import { AliaAskProvider, useAskAgent } from '@alia.onl/sdk';
163
163
  {children}
164
164
  </AliaAskProvider>;
165
165
 
166
- const { ask } = useAskAgent();
166
+ const { ask, resume, dismiss } = useAskAgent();
167
167
  ask('sindi', { text: 'Is this flat a good price?' }); // signed out: Oxy's sign-in instead
168
+ resume('sindi', conversationId); // a stored conversation, where the person left it
169
+ dismiss(); // e.g. after a host tool changed the page under the sheet
168
170
  ```
169
171
 
172
+ The sheet offers the person's latest conversations with its lead under the greeting
173
+ (`recentConversations={false}` turns it off). The code that configures the sheet sits above it, so
174
+ `AliaAskProvider` forwards a ref to the same `AliaAsk` for it.
175
+
170
176
  - **The lead.** `agent={{ id }}` (on `AliaChatSheet`, `AliaChatScreen` and `AliaChatContent`) makes
171
177
  that agent the conversation's lead: its greeting, name, character and starters
172
178
  (`GET /v1/agents/:id/profile`) and the header's first avatar. Omitted, Alia.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "20.0.0",
3
+ "version": "20.1.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",
@@ -10,10 +10,12 @@
10
10
  import { useAuth } from '@oxy.so/services/ui/client';
11
11
  import {
12
12
  createContext,
13
+ forwardRef,
13
14
  type ReactNode,
14
15
  useCallback,
15
16
  useContext,
16
17
  useEffect,
18
+ useImperativeHandle,
17
19
  useMemo,
18
20
  useRef,
19
21
  useState,
@@ -38,6 +40,7 @@ export interface AliaAskProviderProps
38
40
  | 'apiUrl'
39
41
  | 'characterRuntimeUrl'
40
42
  | 'headerMenu'
43
+ | 'recentConversations'
41
44
  > {
42
45
  /** The agents `ask` opens, by key. Omitted, the sheet only ever opens on Alia. */
43
46
  agents?: AliaAskAgents;
@@ -52,6 +55,13 @@ export interface AliaAsk {
52
55
  * ("ask Sindi about this listing"). Signed out, opens Oxy's sign-in instead.
53
56
  */
54
57
  ask: (agentKey?: string, turn?: AliaTurn) => void;
58
+ /** Open the sheet on the agent `agentKey` (omitted, Alia), on its stored conversation `conversationId`. */
59
+ resume: (agentKey: string | undefined, conversationId: string) => void;
60
+ /**
61
+ * Close the sheet — a host tool that changed the page under it shows the person the result, once
62
+ * the turn has been written.
63
+ */
64
+ dismiss: () => void;
55
65
  }
56
66
 
57
67
  const AskContext = createContext<AliaAsk | null>(null);
@@ -67,16 +77,20 @@ export function useAskAgent(): AliaAsk {
67
77
  interface Opening {
68
78
  key: string | undefined;
69
79
  turn: AliaTurn | undefined;
80
+ /** A stored conversation to open instead of the welcome. */
81
+ conversationId?: string;
70
82
  /** Changes on every `ask`, so asking the same agent twice opens twice. */
71
83
  serial: number;
72
84
  }
73
85
 
74
- export function AliaAskProvider({
75
- agents,
76
- welcomeSuggestions,
77
- children,
78
- ...sheet
79
- }: AliaAskProviderProps) {
86
+ /**
87
+ * Mounts the app's one sheet. Its ref is the same `AliaAsk` as `useAskAgent()`, for the code that
88
+ * configures the sheet and so sits above it — a host-tool executor that closes it.
89
+ */
90
+ export const AliaAskProvider = forwardRef<AliaAsk, AliaAskProviderProps>(function AliaAskProvider(
91
+ { agents, welcomeSuggestions, children, ...sheet },
92
+ ref,
93
+ ) {
80
94
  const { canUsePrivateApi, signIn } = useAuth();
81
95
  const sheetRef = useRef<AliaChatSheetRef>(null);
82
96
  const [opening, setOpening] = useState<Opening | null>(null);
@@ -84,8 +98,8 @@ export function AliaAskProvider({
84
98
  const agentsRef = useRef(agents);
85
99
  agentsRef.current = agents;
86
100
 
87
- const ask = useCallback(
88
- (agentKey?: string, turn?: AliaTurn) => {
101
+ const open = useCallback(
102
+ (agentKey: string | undefined, turn?: AliaTurn, conversationId?: string) => {
89
103
  if (agentKey !== undefined && agentsRef.current?.[agentKey] === undefined) {
90
104
  throw new Error(`AliaAskProvider has no agent "${agentKey}"`);
91
105
  }
@@ -94,21 +108,35 @@ export function AliaAskProvider({
94
108
  return;
95
109
  }
96
110
  setCurrent(agentKey);
97
- setOpening((previous) => ({ key: agentKey, turn, serial: (previous?.serial ?? 0) + 1 }));
111
+ setOpening((previous) => ({
112
+ key: agentKey,
113
+ turn,
114
+ ...(conversationId === undefined ? {} : { conversationId }),
115
+ serial: (previous?.serial ?? 0) + 1,
116
+ }));
98
117
  },
99
118
  [canUsePrivateApi, signIn],
100
119
  );
120
+ const ask = useCallback((agentKey?: string, turn?: AliaTurn) => open(agentKey, turn), [open]);
121
+ const resume = useCallback(
122
+ (agentKey: string | undefined, conversationId: string) =>
123
+ open(agentKey, undefined, conversationId),
124
+ [open],
125
+ );
126
+ const dismiss = useCallback(() => sheetRef.current?.dismiss(), []);
101
127
 
102
128
  // Presented once the sheet for that agent is the one mounted (a different agent is a new sheet).
103
129
  useEffect(() => {
104
130
  if (opening === null || opening.key !== current) return;
105
- sheetRef.current?.present(opening.turn);
131
+ if (opening.conversationId !== undefined) sheetRef.current?.resume(opening.conversationId);
132
+ else sheetRef.current?.present(opening.turn);
106
133
  setOpening(null);
107
134
  }, [opening, current]);
108
135
 
109
136
  const agentId = current === undefined ? undefined : agents?.[current]?.id;
110
137
  const agent = useMemo(() => (agentId === undefined ? undefined : { id: agentId }), [agentId]);
111
- const value = useMemo<AliaAsk>(() => ({ ask }), [ask]);
138
+ const value = useMemo<AliaAsk>(() => ({ ask, resume, dismiss }), [ask, resume, dismiss]);
139
+ useImperativeHandle(ref, () => value, [value]);
112
140
 
113
141
  return (
114
142
  <AskContext.Provider value={value}>
@@ -124,4 +152,4 @@ export function AliaAskProvider({
124
152
  ) : null}
125
153
  </AskContext.Provider>
126
154
  );
127
- }
155
+ });
@@ -170,6 +170,8 @@ export interface AliaChatContentProps {
170
170
  welcomeSubtitle?: string;
171
171
  /** The lines offered over the composer on an empty chat; omitted, the agent's starters. */
172
172
  welcomeSuggestions?: AliaSuggestion[];
173
+ /** Drawn under the empty chat's greeting — the sheet's recent conversations with its lead. */
174
+ welcomeAccessory?: ReactNode;
173
175
  /** What the host found for the typed text (see `onSuggestionQuery`). */
174
176
  suggestionMatches?: AliaSuggestion[];
175
177
  /** The typed text, once typing has settled, for the host to search suggestions with. */
@@ -279,6 +281,7 @@ function AliaConversationView({
279
281
  welcomeGreeting,
280
282
  welcomeSubtitle,
281
283
  welcomeSuggestions,
284
+ welcomeAccessory,
282
285
  suggestionMatches,
283
286
  onSuggestionQuery,
284
287
  onSuggestionPicked,
@@ -471,9 +474,12 @@ function AliaConversationView({
471
474
  const welcome = useMemo(
472
475
  () =>
473
476
  profileLoading ? null : (
474
- <AliaWelcomeMessage greeting={greeting} subtitle={welcomeSubtitle} lead={lead} />
477
+ <View className="items-center gap-8">
478
+ <AliaWelcomeMessage greeting={greeting} subtitle={welcomeSubtitle} lead={lead} />
479
+ {welcomeAccessory}
480
+ </View>
475
481
  ),
476
- [profileLoading, greeting, welcomeSubtitle, lead],
482
+ [profileLoading, greeting, welcomeSubtitle, lead, welcomeAccessory],
477
483
  );
478
484
 
479
485
  // ── The agent's offer to start the next stretch fresh ──
@@ -30,6 +30,7 @@ import {
30
30
  type AliaChatContentProps,
31
31
  } from './AliaChatContent';
32
32
  import { AliaChatHeader } from './AliaChatHeader';
33
+ import { AliaRecentConversations } from './AliaRecentConversations';
33
34
  import { ThoughtPanel } from './thought/ThoughtPanel';
34
35
  import type { AliaSuggestion } from '../types';
35
36
 
@@ -88,6 +89,8 @@ export interface AliaChatSheetProps {
88
89
  onShare?: (share: AliaConversationShare) => void;
89
90
  /** More rows for the header's menu (`DropdownMenuItem`s from `@oxy.so/bloom/dropdown-menu`). */
90
91
  headerMenu?: React.ReactNode;
92
+ /** The person's latest conversations with the lead, under the greeting. Default `true`. */
93
+ recentConversations?: boolean;
91
94
  }
92
95
 
93
96
  export interface AliaChatSheetRef {
@@ -97,6 +100,9 @@ export interface AliaChatSheetRef {
97
100
  * opens on its welcome, with the composer focused.
98
101
  */
99
102
  present: (turn?: AliaTurn) => void;
103
+ /** Opens the sheet on a stored conversation with its lead, where the person left it. */
104
+ resume: (conversationId: string) => void;
105
+ /** Closes the sheet and what is stacked on it, stopping a turn still being written. */
100
106
  dismiss: () => void;
101
107
  }
102
108
 
@@ -121,6 +127,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
121
127
  characterRuntimeUrl = ALIA_CHARACTER_RUNTIME_URL,
122
128
  onShare,
123
129
  headerMenu,
130
+ recentConversations = true,
124
131
  },
125
132
  ref,
126
133
  ) => {
@@ -135,9 +142,13 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
135
142
  const mainStopRef = useRef<(() => void) | null>(null);
136
143
  const conversationStopRef = useRef<(() => void) | null>(null);
137
144
 
138
- // The conversation sheet, stacked over the main one while a chat is open. The key makes
139
- // every conversation a fresh chat.
140
- const [conversation, setConversation] = useState<{ key: number; turn: AliaTurn } | null>(null);
145
+ // The conversation sheet, stacked over the main one while a chat is open: begun by a first
146
+ // turn, or a stored one picked up again. The key makes every opening a fresh chat.
147
+ const [conversation, setConversation] = useState<{
148
+ key: number;
149
+ turn?: AliaTurn;
150
+ conversationId?: string;
151
+ } | null>(null);
141
152
 
142
153
  const registerMainStop = useCallback((stop: (() => void) | null) => {
143
154
  mainStopRef.current = stop;
@@ -149,6 +160,13 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
149
160
  const startConversation = useCallback((turn: AliaTurn) => {
150
161
  setConversation((current) => ({ key: (current?.key ?? 0) + 1, turn }));
151
162
  }, []);
163
+ const resumeConversation = useCallback((conversationId: string) => {
164
+ setConversation((current) => ({ key: (current?.key ?? 0) + 1, conversationId }));
165
+ }, []);
166
+ // The chat inside follows its conversation (a first turn creates it, "start over" leaves it).
167
+ const followConversation = useCallback((conversationId: string | undefined) => {
168
+ setConversation((current) => (current ? { ...current, conversationId } : current));
169
+ }, []);
152
170
  useEffect(() => {
153
171
  if (conversation) conversationRef.current?.present();
154
172
  }, [conversation]);
@@ -197,9 +215,13 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
197
215
  mainRef.current?.present();
198
216
  if (turn) startConversation(turn);
199
217
  },
218
+ resume: (conversationId) => {
219
+ mainRef.current?.present();
220
+ resumeConversation(conversationId);
221
+ },
200
222
  dismiss: handleDismiss,
201
223
  }),
202
- [handleDismiss, startConversation],
224
+ [handleDismiss, startConversation, resumeConversation],
203
225
  );
204
226
 
205
227
  // Closing the main sheet (a drag, backdrop press, Back or Escape) closes what is stacked on it.
@@ -282,6 +304,11 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
282
304
  welcomeSuggestions={welcomeSuggestions}
283
305
  suggestionMatches={suggestionMatches}
284
306
  onSuggestionQuery={onSuggestionQuery}
307
+ welcomeAccessory={
308
+ recentConversations ? (
309
+ <AliaRecentConversations agentId={agent?.id} onOpen={resumeConversation} />
310
+ ) : undefined
311
+ }
285
312
  presentedProgress={mainProgress}
286
313
  onStopReady={registerMainStop}
287
314
  onStart={startConversation}
@@ -303,6 +330,8 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
303
330
  <AliaChatContent
304
331
  {...shared}
305
332
  initialTurn={conversation.turn}
333
+ conversationId={conversation.conversationId}
334
+ onConversationChange={followConversation}
306
335
  presentedProgress={conversationProgress}
307
336
  onStopReady={registerConversationStop}
308
337
  chrome={conversationChrome}
@@ -0,0 +1,58 @@
1
+ import { Item } from '@oxy.so/bloom/item';
2
+ import { Text } from '@oxy.so/bloom/typography';
3
+ import { useMemo } from 'react';
4
+ import { View } from 'react-native';
5
+ import { useLabels } from '../lib/labels';
6
+ import { useConversations } from '../runtime/conversations';
7
+
8
+ /** How many of the lead's conversations the sheet offers to pick up again. */
9
+ export const RECENT_CONVERSATIONS_SHOWN = 3;
10
+
11
+ /**
12
+ * The person's latest conversations with the sheet's lead, under its greeting: "Ask Sindi" picks
13
+ * up where it left off instead of every opening being a blank page. Alia's own sheet lists the
14
+ * person's latest conversations of any kind. Nothing while loading or when there are none.
15
+ */
16
+ export function AliaRecentConversations({
17
+ agentId,
18
+ onOpen,
19
+ }: {
20
+ /** The lead; omitted, Alia. */
21
+ agentId?: string;
22
+ onOpen: (conversationId: string) => void;
23
+ }) {
24
+ const labels = useLabels();
25
+ const { data } = useConversations(agentId === undefined ? {} : { agentId });
26
+ const recent = useMemo(
27
+ () => data?.pages[0]?.conversations.slice(0, RECENT_CONVERSATIONS_SHOWN) ?? [],
28
+ [data],
29
+ );
30
+ const when = useMemo(
31
+ () => new Intl.DateTimeFormat(labels.locale, { day: 'numeric', month: 'short' }),
32
+ [labels.locale],
33
+ );
34
+ if (recent.length === 0) return null;
35
+ return (
36
+ <View accessibilityRole="list" className="w-full max-w-[480px] self-center gap-1">
37
+ <Text className="px-3 text-sm text-text-secondary">{labels.recentConversations}</Text>
38
+ {recent.map((conversation) => {
39
+ const title = conversation.title || labels.newChat;
40
+ return (
41
+ <Item
42
+ key={conversation.id}
43
+ density="compact"
44
+ accessibilityRole="button"
45
+ accessibilityLabel={title}
46
+ onPress={() => onOpen(conversation.id)}
47
+ title={<Text numberOfLines={1}>{title}</Text>}
48
+ trailing={
49
+ <Text className="text-xs text-text-tertiary">
50
+ {when.format(new Date(conversation.updatedAt))}
51
+ </Text>
52
+ }
53
+ />
54
+ );
55
+ })}
56
+ </View>
57
+ );
58
+ }
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export type { AliaAddMenu } from './lib/add-menu';
27
27
  export type { AliaVoiceOptions } from './hooks/useChatVoice';
28
28
  export { AliaChatSheet } from './components/AliaChatSheet';
29
29
  export type { AliaChatSheetProps, AliaChatSheetRef } from './components/AliaChatSheet';
30
+ export { AliaRecentConversations } from './components/AliaRecentConversations';
30
31
  export type { AliaTurn } from './hooks/useChatComposer';
31
32
  export { AliaChatScreen } from './components/AliaChatScreen';
32
33
  export type { AliaChatScreenProps } from './components/AliaChatScreen';
@@ -87,6 +87,8 @@ export interface AliaChatLabels {
87
87
  // Conversations (the runtime's own words: toasts and the errors it raises)
88
88
  /** A conversation's title until it has one. */
89
89
  newChat: string;
90
+ /** Over the sheet's recent conversations with its lead. */
91
+ recentConversations: string;
90
92
  /** A turn could not be sent and the composer got its text back. */
91
93
  sendFailed: string;
92
94
  /** The session ended mid-request. */
@@ -165,6 +167,7 @@ export const DEFAULT_LABELS: AliaChatLabels = {
165
167
  t: englishCopy,
166
168
  locale: 'en',
167
169
  newChat: 'New chat',
170
+ recentConversations: 'Recent',
168
171
  sendFailed: "Your message couldn't be sent",
169
172
  signInRequired: 'Sign in to keep chatting',
170
173
  conversationSaveFailed: 'Failed to save conversation',