@alia.onl/sdk 11.1.0 → 13.0.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 +134 -0
- package/package.json +5 -4
- package/src/components/AliaAvatar.tsx +16 -20
- package/src/components/AliaChatContent.tsx +234 -96
- package/src/components/AliaChatHeader.tsx +102 -76
- package/src/components/AliaChatMessageList.tsx +174 -134
- package/src/components/AliaChatScreen.tsx +45 -23
- package/src/components/AliaChatSheet.tsx +151 -50
- package/src/components/AliaWelcomeMessage.tsx +26 -53
- package/src/components/AmbientField.tsx +19 -16
- package/src/components/ComposerSuggestions.tsx +45 -0
- package/src/components/IdentityMark.tsx +3 -1
- package/src/components/Markdown.tsx +18 -24
- package/src/components/QuestionCard.tsx +35 -33
- package/src/components/ThinkingBlock.tsx +9 -13
- package/src/components/VoiceModeIcon.tsx +15 -7
- package/src/components/voice/VoiceControls.tsx +17 -9
- package/src/hooks/useAliaChat.ts +124 -5
- package/src/hooks/useComposerSuggestions.ts +84 -0
- package/src/index.ts +10 -10
- package/src/lib/agent-character.ts +28 -0
- package/src/lib/chat-stream.ts +25 -3
- package/src/lib/citations.ts +62 -10
- package/src/lib/delegation.ts +34 -0
- package/src/lib/domain.ts +12 -0
- package/src/lib/flights.ts +24 -0
- package/src/lib/greeting.ts +15 -0
- package/src/lib/image-data-url.ts +26 -5
- package/src/lib/labels.tsx +10 -2
- package/src/lib/questions.ts +15 -1
- package/src/lib/suggestions.ts +53 -0
- package/src/lib/task-labels.ts +7 -39
- package/src/lib/use-after-delay.ts +19 -0
- package/src/lib/work-log.ts +3 -9
- package/src/tools.ts +8 -1
- package/src/types.ts +6 -5
|
@@ -17,14 +17,13 @@
|
|
|
17
17
|
* `livekit-client` never enters the bundle.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import React from 'react';
|
|
21
|
-
import { useTheme } from '@oxy.so/bloom/theme';
|
|
20
|
+
import React, { useCallback } from 'react';
|
|
22
21
|
import { View } from 'react-native';
|
|
23
22
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
24
23
|
import { KeyboardAvoidingView } from '../lib/keyboard';
|
|
25
|
-
import { AliaChatContent } from './AliaChatContent';
|
|
24
|
+
import { AliaChatContent, type AliaChatContentProps } from './AliaChatContent';
|
|
26
25
|
import { AliaChatHeader } from './AliaChatHeader';
|
|
27
|
-
import type {
|
|
26
|
+
import type { AliaSuggestion } from '../types';
|
|
28
27
|
import type { AliaChatLabels } from '../lib/labels';
|
|
29
28
|
import type { VoiceSessionComponent } from '../types';
|
|
30
29
|
|
|
@@ -35,8 +34,14 @@ export interface AliaChatScreenProps {
|
|
|
35
34
|
model?: string;
|
|
36
35
|
/** API URL override */
|
|
37
36
|
apiUrl?: string;
|
|
38
|
-
/**
|
|
37
|
+
/** Draws the header's back button; omit where there is nowhere to go back to. */
|
|
38
|
+
onBack?: () => void;
|
|
39
|
+
/** Optional header left content instead of the back button (e.g. a drawer's menu button) */
|
|
39
40
|
headerLeft?: React.ReactNode;
|
|
41
|
+
/** Draws the header's share button once there is a conversation; omitted, no share button. */
|
|
42
|
+
onShare?: () => void;
|
|
43
|
+
/** More rows for the header's menu (`DropdownMenuItem`s from `@oxy.so/bloom/dropdown-menu`). */
|
|
44
|
+
headerMenu?: React.ReactNode;
|
|
40
45
|
/** Optional header right action (e.g., settings button) */
|
|
41
46
|
headerRight?: React.ReactNode;
|
|
42
47
|
/** Welcome screen greeting */
|
|
@@ -44,7 +49,13 @@ export interface AliaChatScreenProps {
|
|
|
44
49
|
/** Welcome screen subtitle */
|
|
45
50
|
welcomeSubtitle?: string;
|
|
46
51
|
/** Welcome screen suggestions */
|
|
47
|
-
welcomeSuggestions?:
|
|
52
|
+
welcomeSuggestions?: AliaSuggestion[];
|
|
53
|
+
/** The agents the host knows, with their look: a delegation's thinking avatar is drawn from them until the agent answers. */
|
|
54
|
+
agents?: AliaChatContentProps['agents'];
|
|
55
|
+
/** What the host found for the typed text, shown over the composer (see `onSuggestionQuery`). */
|
|
56
|
+
suggestionMatches?: AliaSuggestion[];
|
|
57
|
+
/** The typed text, once typing has settled, for the host to search suggestions with. */
|
|
58
|
+
onSuggestionQuery?: (query: string) => void;
|
|
48
59
|
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
49
60
|
isDarkMode?: boolean;
|
|
50
61
|
/** Voice capability — `VoiceSession` from `@alia.onl/sdk/voice`. */
|
|
@@ -57,18 +68,22 @@ export function AliaChatScreen({
|
|
|
57
68
|
clientContext,
|
|
58
69
|
model,
|
|
59
70
|
apiUrl,
|
|
71
|
+
onBack,
|
|
60
72
|
headerLeft,
|
|
73
|
+
onShare,
|
|
74
|
+
headerMenu,
|
|
61
75
|
headerRight,
|
|
62
76
|
welcomeGreeting,
|
|
63
77
|
welcomeSubtitle,
|
|
64
78
|
welcomeSuggestions,
|
|
79
|
+
agents,
|
|
80
|
+
suggestionMatches,
|
|
81
|
+
onSuggestionQuery,
|
|
65
82
|
isDarkMode,
|
|
66
83
|
voiceSession,
|
|
67
84
|
labels,
|
|
68
85
|
}: AliaChatScreenProps) {
|
|
69
86
|
const insets = useSafeAreaInsets();
|
|
70
|
-
const { colors } = useTheme();
|
|
71
|
-
|
|
72
87
|
// The screen owns the whole window, so it keeps its composer out of both
|
|
73
88
|
// system bars: the top inset clears the status bar, the bottom one the
|
|
74
89
|
// gesture bar or home indicator an edge-to-edge app draws over.
|
|
@@ -79,14 +94,26 @@ export function AliaChatScreen({
|
|
|
79
94
|
// keyboard's height minus the inset: the composer sits on the keyboard, not
|
|
80
95
|
// one inset above it. (Its parent-relative layout would read as zero overlap
|
|
81
96
|
// this deep in a host's tree, which is why the offset is measured.)
|
|
97
|
+
const header = useCallback<NonNullable<AliaChatContentProps['header']>>(
|
|
98
|
+
({ hasMessages, clear, agents: answered }) => (
|
|
99
|
+
<AliaChatHeader
|
|
100
|
+
hasMessages={hasMessages}
|
|
101
|
+
agents={answered}
|
|
102
|
+
onClear={clear}
|
|
103
|
+
onBack={onBack}
|
|
104
|
+
onShare={onShare}
|
|
105
|
+
menu={headerMenu}
|
|
106
|
+
leading={headerLeft}
|
|
107
|
+
trailing={headerRight}
|
|
108
|
+
/>
|
|
109
|
+
),
|
|
110
|
+
[onBack, onShare, headerMenu, headerLeft, headerRight],
|
|
111
|
+
);
|
|
112
|
+
|
|
82
113
|
return (
|
|
83
114
|
<View
|
|
84
|
-
className="flex-1"
|
|
85
|
-
style={{
|
|
86
|
-
backgroundColor: colors.background,
|
|
87
|
-
paddingTop: insets.top,
|
|
88
|
-
paddingBottom: insets.bottom,
|
|
89
|
-
}}
|
|
115
|
+
className="flex-1 bg-background-full"
|
|
116
|
+
style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
|
|
90
117
|
>
|
|
91
118
|
<KeyboardAvoidingView behavior="padding" automaticOffset style={{ flex: 1 }}>
|
|
92
119
|
<AliaChatContent
|
|
@@ -96,18 +123,13 @@ export function AliaChatScreen({
|
|
|
96
123
|
welcomeGreeting={welcomeGreeting}
|
|
97
124
|
welcomeSubtitle={welcomeSubtitle}
|
|
98
125
|
welcomeSuggestions={welcomeSuggestions}
|
|
126
|
+
agents={agents}
|
|
127
|
+
suggestionMatches={suggestionMatches}
|
|
128
|
+
onSuggestionQuery={onSuggestionQuery}
|
|
99
129
|
isDarkMode={isDarkMode}
|
|
100
130
|
voiceSession={voiceSession}
|
|
101
131
|
labels={labels}
|
|
102
|
-
|
|
103
|
-
headerLeading={({ hasMessages, clear }) => (
|
|
104
|
-
<AliaChatHeader
|
|
105
|
-
hasMessages={hasMessages}
|
|
106
|
-
onClear={clear}
|
|
107
|
-
leading={headerLeft}
|
|
108
|
-
trailing={headerRight}
|
|
109
|
-
/>
|
|
110
|
-
)}
|
|
132
|
+
header={header}
|
|
111
133
|
/>
|
|
112
134
|
</KeyboardAvoidingView>
|
|
113
135
|
</View>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { AvatarFlightLayer, useAvatarFlights } from '@oxy.so/bloom/avatar-flight';
|
|
1
2
|
import { BottomSheet, type BottomSheetRef } from '@oxy.so/bloom/bottom-sheet';
|
|
2
|
-
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
4
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
5
|
+
import { useWindowDimensions, View } from 'react-native';
|
|
6
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
7
|
+
import { BREAKPOINTS } from '@oxy.so/bloom/styles';
|
|
4
8
|
import type { AliaChatLabels } from '../lib/labels';
|
|
5
9
|
import type { VoiceSessionComponent } from '../types';
|
|
6
|
-
import { AliaChatContent } from './AliaChatContent';
|
|
10
|
+
import { AliaChatContent, type AliaChatContentProps, type AliaTurn } from './AliaChatContent';
|
|
7
11
|
import { AliaChatHeader } from './AliaChatHeader';
|
|
8
|
-
import type {
|
|
12
|
+
import type { AliaSuggestion } from '../types';
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* TODO(bloom): drop this once Bloom's sheet is rounder by default. Its detached
|
|
@@ -16,6 +20,9 @@ import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
|
16
20
|
*/
|
|
17
21
|
const SHEET_RADIUS = 40;
|
|
18
22
|
|
|
23
|
+
/** The sheet floats (detached) from tablet width up; on a phone it is flush with the screen edges. */
|
|
24
|
+
const DETACHED_FROM = BREAKPOINTS.md;
|
|
25
|
+
|
|
19
26
|
export interface AliaChatSheetProps {
|
|
20
27
|
/** App context injected into system prompt */
|
|
21
28
|
clientContext?: string;
|
|
@@ -28,7 +35,13 @@ export interface AliaChatSheetProps {
|
|
|
28
35
|
/** Welcome screen subtitle */
|
|
29
36
|
welcomeSubtitle?: string;
|
|
30
37
|
/** Welcome screen suggestions */
|
|
31
|
-
welcomeSuggestions?:
|
|
38
|
+
welcomeSuggestions?: AliaSuggestion[];
|
|
39
|
+
/** The agents the host knows, with their look: a delegation's thinking avatar is drawn from them until the agent answers. */
|
|
40
|
+
agents?: AliaChatContentProps['agents'];
|
|
41
|
+
/** What the host found for the typed text, shown over the composer (see `onSuggestionQuery`). */
|
|
42
|
+
suggestionMatches?: AliaSuggestion[];
|
|
43
|
+
/** The typed text, once typing has settled, for the host to search suggestions with. */
|
|
44
|
+
onSuggestionQuery?: (query: string) => void;
|
|
32
45
|
/** Dark-mode flag — forwarded to the ambient wave overlay (defaults to the sheet's own color scheme). */
|
|
33
46
|
isDarkMode?: boolean;
|
|
34
47
|
/**
|
|
@@ -39,6 +52,10 @@ export interface AliaChatSheetProps {
|
|
|
39
52
|
voiceSession?: VoiceSessionComponent;
|
|
40
53
|
/** Overrides for the chat's own copy (header, composer, plan, research…); omitted, English. */
|
|
41
54
|
labels?: Partial<AliaChatLabels>;
|
|
55
|
+
/** Draws the header's share button once there is a conversation; omitted, no share button. */
|
|
56
|
+
onShare?: () => void;
|
|
57
|
+
/** More rows for the header's menu (`DropdownMenuItem`s from `@oxy.so/bloom/dropdown-menu`). */
|
|
58
|
+
headerMenu?: React.ReactNode;
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
export interface AliaChatSheetRef {
|
|
@@ -55,72 +72,156 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
55
72
|
welcomeGreeting,
|
|
56
73
|
welcomeSubtitle,
|
|
57
74
|
welcomeSuggestions,
|
|
75
|
+
agents,
|
|
76
|
+
suggestionMatches,
|
|
77
|
+
onSuggestionQuery,
|
|
58
78
|
isDarkMode,
|
|
59
79
|
voiceSession,
|
|
60
80
|
labels,
|
|
81
|
+
onShare,
|
|
82
|
+
headerMenu,
|
|
61
83
|
},
|
|
62
84
|
ref,
|
|
63
85
|
) => {
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
86
|
+
// Bloom's resolved mode, not the system's scheme.
|
|
87
|
+
const { isDark } = useTheme();
|
|
88
|
+
const mainRef = useRef<BottomSheetRef>(null);
|
|
89
|
+
const conversationRef = useRef<BottomSheetRef>(null);
|
|
90
|
+
// Each sheet's chat registers its own canceller; closing a sheet stops its own turn.
|
|
91
|
+
const mainStopRef = useRef<(() => void) | null>(null);
|
|
92
|
+
const conversationStopRef = useRef<(() => void) | null>(null);
|
|
93
|
+
|
|
94
|
+
// The conversation sheet, stacked over the main one while a chat is open. The key makes
|
|
95
|
+
// every conversation a fresh chat.
|
|
96
|
+
const [conversation, setConversation] = useState<{ key: number; turn: AliaTurn } | null>(null);
|
|
67
97
|
|
|
68
|
-
const
|
|
69
|
-
|
|
98
|
+
const registerMainStop = useCallback((stop: (() => void) | null) => {
|
|
99
|
+
mainStopRef.current = stop;
|
|
100
|
+
}, []);
|
|
101
|
+
const registerConversationStop = useCallback((stop: (() => void) | null) => {
|
|
102
|
+
conversationStopRef.current = stop;
|
|
70
103
|
}, []);
|
|
71
104
|
|
|
72
|
-
const
|
|
73
|
-
|
|
105
|
+
const startConversation = useCallback((turn: AliaTurn) => {
|
|
106
|
+
setConversation((current) => ({ key: (current?.key ?? 0) + 1, turn }));
|
|
107
|
+
}, []);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (conversation) conversationRef.current?.present();
|
|
110
|
+
}, [conversation]);
|
|
111
|
+
|
|
112
|
+
// Back, and the menu's Clear: close the conversation, back to the main screen underneath.
|
|
113
|
+
const closeConversation = useCallback(() => {
|
|
114
|
+
conversationStopRef.current?.();
|
|
115
|
+
conversationRef.current?.dismiss();
|
|
116
|
+
}, []);
|
|
117
|
+
const onConversationDismissAttempt = useCallback(() => {
|
|
118
|
+
conversationStopRef.current?.();
|
|
119
|
+
return true;
|
|
74
120
|
}, []);
|
|
75
121
|
|
|
76
122
|
const handleDismiss = useCallback(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
123
|
+
conversationStopRef.current?.();
|
|
124
|
+
mainStopRef.current?.();
|
|
125
|
+
conversationRef.current?.dismiss();
|
|
126
|
+
mainRef.current?.dismiss();
|
|
127
|
+
}, []);
|
|
80
128
|
|
|
81
129
|
useImperativeHandle(
|
|
82
130
|
ref,
|
|
83
|
-
() => ({ present: () =>
|
|
131
|
+
() => ({ present: () => mainRef.current?.present(), dismiss: handleDismiss }),
|
|
84
132
|
[handleDismiss],
|
|
85
133
|
);
|
|
86
134
|
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
|
|
135
|
+
// Closing the main sheet (a drag, backdrop press, Back or Escape) closes what is stacked on it.
|
|
136
|
+
const onMainDismissAttempt = useCallback(() => {
|
|
137
|
+
mainStopRef.current?.();
|
|
138
|
+
conversationStopRef.current?.();
|
|
139
|
+
conversationRef.current?.dismiss();
|
|
90
140
|
return true;
|
|
91
|
-
}, [
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
const { width } = useWindowDimensions();
|
|
144
|
+
const detached = width >= DETACHED_FROM;
|
|
145
|
+
// Flush with the screen edge the sheet runs behind the home indicator: the composer clears it.
|
|
146
|
+
const { bottom } = useSafeAreaInsets();
|
|
147
|
+
const sheetStyle = detached
|
|
148
|
+
? { maxWidth: 600, alignSelf: 'center' as const, borderRadius: SHEET_RADIUS }
|
|
149
|
+
: undefined;
|
|
150
|
+
|
|
151
|
+
// The avatars fly between the two sheets: one controller, one layer.
|
|
152
|
+
const flights = useAvatarFlights();
|
|
153
|
+
const shared = {
|
|
154
|
+
flights,
|
|
155
|
+
clientContext,
|
|
156
|
+
model,
|
|
157
|
+
apiUrl,
|
|
158
|
+
isDarkMode: isDarkMode ?? isDark,
|
|
159
|
+
voiceSession,
|
|
160
|
+
bottomInset: detached ? 0 : bottom,
|
|
161
|
+
labels,
|
|
162
|
+
};
|
|
92
163
|
|
|
93
164
|
return (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
165
|
+
<>
|
|
166
|
+
<BottomSheet
|
|
167
|
+
ref={mainRef}
|
|
168
|
+
detached={detached}
|
|
169
|
+
scrollable={false}
|
|
170
|
+
manualActivation
|
|
171
|
+
onDismissAttempt={onMainDismissAttempt}
|
|
172
|
+
onDismiss={() => setConversation(null)}
|
|
173
|
+
style={sheetStyle}
|
|
174
|
+
>
|
|
175
|
+
{/* The main screen: the greeting, the suggestions and the composer. A first turn opens
|
|
176
|
+
the conversation in its own sheet over this one, so this chat stays empty. */}
|
|
177
|
+
<AliaChatContent
|
|
178
|
+
{...shared}
|
|
179
|
+
welcomeGreeting={welcomeGreeting}
|
|
180
|
+
welcomeSubtitle={welcomeSubtitle}
|
|
181
|
+
welcomeSuggestions={welcomeSuggestions}
|
|
182
|
+
agents={agents}
|
|
183
|
+
suggestionMatches={suggestionMatches}
|
|
184
|
+
onSuggestionQuery={onSuggestionQuery}
|
|
185
|
+
onStopReady={registerMainStop}
|
|
186
|
+
onStart={startConversation}
|
|
187
|
+
/>
|
|
188
|
+
</BottomSheet>
|
|
189
|
+
|
|
190
|
+
{conversation ? (
|
|
191
|
+
<BottomSheet
|
|
192
|
+
key={conversation.key}
|
|
193
|
+
ref={conversationRef}
|
|
194
|
+
detached={detached}
|
|
195
|
+
scrollable={false}
|
|
196
|
+
manualActivation
|
|
197
|
+
onDismissAttempt={onConversationDismissAttempt}
|
|
198
|
+
onDismiss={() => setConversation(null)}
|
|
199
|
+
style={sheetStyle}
|
|
200
|
+
>
|
|
201
|
+
<AliaChatContent
|
|
202
|
+
{...shared}
|
|
203
|
+
initialTurn={conversation.turn}
|
|
204
|
+
agents={agents}
|
|
205
|
+
onStopReady={registerConversationStop}
|
|
206
|
+
// The bar is the conversation's: back to the main screen, the profile, share and
|
|
207
|
+
// the menu.
|
|
208
|
+
header={({ agents }) => (
|
|
209
|
+
<View className="pt-8">
|
|
210
|
+
<AliaChatHeader
|
|
211
|
+
hasMessages
|
|
212
|
+
agents={agents}
|
|
213
|
+
onClear={closeConversation}
|
|
214
|
+
onBack={closeConversation}
|
|
215
|
+
onShare={onShare}
|
|
216
|
+
menu={headerMenu}
|
|
217
|
+
/>
|
|
218
|
+
</View>
|
|
219
|
+
)}
|
|
220
|
+
/>
|
|
221
|
+
</BottomSheet>
|
|
222
|
+
) : null}
|
|
223
|
+
<AvatarFlightLayer flights={flights} />
|
|
224
|
+
</>
|
|
124
225
|
);
|
|
125
226
|
},
|
|
126
227
|
);
|
|
@@ -1,71 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AvatarAnchor } from '@oxy.so/bloom/avatar-flight';
|
|
2
2
|
import { Text } from '@oxy.so/bloom/typography';
|
|
3
3
|
import { View } from 'react-native';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import { ALIA_ANCHORS, useAliaFlights } from '../lib/flights';
|
|
5
|
+
import { greetingLines } from '../lib/greeting';
|
|
6
6
|
import { AliaAvatar } from './AliaAvatar';
|
|
7
7
|
|
|
8
|
-
export type { WelcomeSuggestion };
|
|
9
|
-
|
|
10
8
|
export interface AliaWelcomeMessageProps {
|
|
11
9
|
/** The greeting, e.g. "Hi Nate, what's on your mind?". */
|
|
12
10
|
greeting: string;
|
|
13
11
|
subtitle?: string;
|
|
14
|
-
suggestions?: WelcomeSuggestion[];
|
|
15
|
-
onSuggestionPress?: (text: string) => void;
|
|
16
12
|
markSize?: number;
|
|
17
|
-
/** Leave the avatar out, where the surrounding header already shows it. */
|
|
18
|
-
hideMark?: boolean;
|
|
19
13
|
}
|
|
20
14
|
|
|
21
15
|
/**
|
|
22
|
-
* The empty chat
|
|
23
|
-
*
|
|
24
|
-
*
|
|
16
|
+
* The empty chat's greeting: the avatar above it, both centred, the address on top ("Hi Nate,")
|
|
17
|
+
* and the rest under it (`greetingLines`). The Alia app and the SDK chat
|
|
18
|
+
* draw exactly this. The greeting has a max width (`max-w-xs`, 20rem) so it sets in two lines at
|
|
19
|
+
* any width, as a headline under a mark rather than one long line across a wide column. The
|
|
20
|
+
* suggestions are not part of it; they sit over the composer (`ComposerSuggestions`).
|
|
25
21
|
*/
|
|
26
|
-
export function AliaWelcomeMessage({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
suggestions = [],
|
|
30
|
-
onSuggestionPress,
|
|
31
|
-
markSize = 38,
|
|
32
|
-
hideMark = false,
|
|
33
|
-
}: AliaWelcomeMessageProps) {
|
|
22
|
+
export function AliaWelcomeMessage({ greeting, subtitle, markSize = 88 }: AliaWelcomeMessageProps) {
|
|
23
|
+
const flights = useAliaFlights();
|
|
24
|
+
const avatar = <AliaAvatar size={markSize} interactive />;
|
|
34
25
|
return (
|
|
35
|
-
<View className="
|
|
36
|
-
<View className="items-center gap-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
) : null}
|
|
26
|
+
<View className="items-center gap-2">
|
|
27
|
+
<View className="items-center justify-center gap-4">
|
|
28
|
+
{/* It flies up into the header's profile when the conversation begins. */}
|
|
29
|
+
{flights ? (
|
|
30
|
+
<AvatarAnchor flights={flights} anchor={ALIA_ANCHORS.welcome}>
|
|
31
|
+
{avatar}
|
|
32
|
+
</AvatarAnchor>
|
|
33
|
+
) : (
|
|
34
|
+
avatar
|
|
35
|
+
)}
|
|
36
|
+
<Text selectable={false} className="max-w-xs shrink text-center text-4xl text-text-primary">
|
|
37
|
+
{greetingLines(greeting).join('\n')}
|
|
38
|
+
</Text>
|
|
49
39
|
</View>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<Animated.View
|
|
53
|
-
entering={FadeIn.duration(400)}
|
|
54
|
-
accessibilityRole="list"
|
|
55
|
-
className="w-full max-w-[768px]"
|
|
56
|
-
>
|
|
57
|
-
{suggestions.map((item) => (
|
|
58
|
-
<Item
|
|
59
|
-
key={item.id}
|
|
60
|
-
density="compact"
|
|
61
|
-
accessibilityRole="button"
|
|
62
|
-
accessibilityLabel={item.title}
|
|
63
|
-
onPress={() => onSuggestionPress?.(item.description)}
|
|
64
|
-
title={<Text numberOfLines={1}>{item.title}</Text>}
|
|
65
|
-
subtitle={item.description}
|
|
66
|
-
/>
|
|
67
|
-
))}
|
|
68
|
-
</Animated.View>
|
|
40
|
+
{subtitle ? (
|
|
41
|
+
<Text className="text-center text-lg text-text-secondary">{subtitle}</Text>
|
|
69
42
|
) : null}
|
|
70
43
|
</View>
|
|
71
44
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { memo, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { AppState, Platform, View, type LayoutChangeEvent, type ViewStyle } from 'react-native';
|
|
3
3
|
import Animated, {
|
|
4
4
|
cancelAnimation,
|
|
@@ -55,16 +55,16 @@ const EXIT_EASE = Easing.bezier(0.2, 0.7, 0.2, 1);
|
|
|
55
55
|
const PULSE_DURATION = 900;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* and
|
|
58
|
+
* How the field lays itself out in a container that is not a screen. The blobs are sized in
|
|
59
|
+
* `vmax` of the field and laid out across its width, which on a short, wide card (the chat's
|
|
60
|
+
* sheet) would make them half as big and pile them on the centre. The `sheet` layout draws
|
|
61
|
+
* them at the size of a phone screen (`reference`), spread wider across the width (`spread`)
|
|
62
|
+
* and shifted back toward the right (`shift`, a share of the width, since the spread leaves the
|
|
63
|
+
* composition leaning left), cropped by the card's edges: the blue, warm and green of the
|
|
64
|
+
* intro side by side. Asked for, not inferred from the height, so a short app screen is
|
|
65
|
+
* never reinterpreted.
|
|
63
66
|
*/
|
|
64
|
-
const
|
|
65
|
-
const SHORT_FIELD_SPREAD = 1.4;
|
|
66
|
-
/** The spread leaves the composition leaning left; this much of the width puts it back. */
|
|
67
|
-
const SHORT_FIELD_SHIFT = 0.17;
|
|
67
|
+
const SHEET_LAYOUT = { reference: 800, spread: 1.4, shift: 0.17 } as const;
|
|
68
68
|
|
|
69
69
|
/** One keyframe stop of a float loop: [translateX in vw, translateY in vh, scale]. */
|
|
70
70
|
type FloatStop = readonly [number, number, number];
|
|
@@ -368,6 +368,8 @@ function Flourish({
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
export interface AmbientFieldProps {
|
|
371
|
+
/** `screen` (default) fills a page; `sheet` is for a short card, see `SHEET_LAYOUT`. */
|
|
372
|
+
layout?: 'screen' | 'sheet';
|
|
371
373
|
/**
|
|
372
374
|
* Combined amplitude from `useAmbientWave` — swells and brightens the blobs.
|
|
373
375
|
* Omit where there is no audio (the intro) and the field simply rests.
|
|
@@ -420,7 +422,8 @@ export interface AmbientFieldProps {
|
|
|
420
422
|
* layer, nothing is seen THROUGH it, and the translucent surfaces over it are
|
|
421
423
|
* Bloom's, which follow that setting themselves.
|
|
422
424
|
*/
|
|
423
|
-
export function AmbientField({
|
|
425
|
+
export const AmbientField = memo(function AmbientField({
|
|
426
|
+
layout = 'screen',
|
|
424
427
|
waveAmplitude,
|
|
425
428
|
agentState = 'idle',
|
|
426
429
|
intensity = 1,
|
|
@@ -487,11 +490,11 @@ export function AmbientField({
|
|
|
487
490
|
setField({ width, height });
|
|
488
491
|
};
|
|
489
492
|
|
|
490
|
-
const
|
|
491
|
-
const vw = (field.width / 100) * (
|
|
493
|
+
const sheet = layout === 'sheet';
|
|
494
|
+
const vw = (field.width / 100) * (sheet ? SHEET_LAYOUT.spread : 1);
|
|
492
495
|
const vh = field.height / 100;
|
|
493
|
-
const vmax = Math.max(field.width, field.height,
|
|
494
|
-
const shiftX =
|
|
496
|
+
const vmax = Math.max(field.width, field.height, sheet ? SHEET_LAYOUT.reference : 0) / 100;
|
|
497
|
+
const shiftX = sheet ? field.width * SHEET_LAYOUT.shift : 0;
|
|
495
498
|
|
|
496
499
|
const fieldStyle = useAnimatedStyle(() => ({
|
|
497
500
|
// On the way out the field keeps 45% of its opacity, sinking as it goes.
|
|
@@ -572,6 +575,6 @@ export function AmbientField({
|
|
|
572
575
|
) : null}
|
|
573
576
|
</View>
|
|
574
577
|
);
|
|
575
|
-
}
|
|
578
|
+
});
|
|
576
579
|
|
|
577
580
|
export { PARALLAX_DURATION, PARALLAX_EASE };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Item } from '@oxy.so/bloom/item';
|
|
2
|
+
import { Text } from '@oxy.so/bloom/typography';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import type { Completion } from '../lib/suggestions';
|
|
5
|
+
import type { AliaSuggestion } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Suggestions over the composer, one line each, the typed part highlighted and the row the
|
|
9
|
+
* keyboard is on marked. The Alia app and the SDK chat draw exactly this.
|
|
10
|
+
*/
|
|
11
|
+
export function ComposerSuggestions<T extends AliaSuggestion>({
|
|
12
|
+
completions,
|
|
13
|
+
selected,
|
|
14
|
+
onPick,
|
|
15
|
+
}: {
|
|
16
|
+
completions: readonly Completion<T>[];
|
|
17
|
+
selected: number;
|
|
18
|
+
onPick: (suggestion: T) => void;
|
|
19
|
+
}) {
|
|
20
|
+
if (completions.length === 0) return null;
|
|
21
|
+
return (
|
|
22
|
+
<View accessibilityRole="list" className="w-full max-w-[768px] self-center">
|
|
23
|
+
{completions.map(({ suggestion, matchStart, matchEnd }, index) => {
|
|
24
|
+
const { text } = suggestion;
|
|
25
|
+
return (
|
|
26
|
+
<Item
|
|
27
|
+
key={suggestion.id}
|
|
28
|
+
density="compact"
|
|
29
|
+
highlighted={index === selected}
|
|
30
|
+
accessibilityRole="button"
|
|
31
|
+
accessibilityLabel={text}
|
|
32
|
+
onPress={() => onPick(suggestion)}
|
|
33
|
+
title={
|
|
34
|
+
<Text numberOfLines={1}>
|
|
35
|
+
{text.slice(0, matchStart)}
|
|
36
|
+
<Text className="font-medium text-primary">{text.slice(matchStart, matchEnd)}</Text>
|
|
37
|
+
{text.slice(matchEnd)}
|
|
38
|
+
</Text>
|
|
39
|
+
}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { Pressable, View } from 'react-native';
|
|
3
|
+
import type { AliaAvatarState } from './AliaAvatar';
|
|
3
4
|
import * as Haptics from 'expo-haptics';
|
|
4
5
|
import Svg, { Path } from 'react-native-svg';
|
|
5
6
|
import Animated, {
|
|
@@ -13,7 +14,8 @@ import Animated, {
|
|
|
13
14
|
Easing,
|
|
14
15
|
} from 'react-native-reanimated';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
/** What the mark is doing: the same states as the avatar's. */
|
|
18
|
+
export type IdentityMarkState = AliaAvatarState;
|
|
17
19
|
|
|
18
20
|
export interface IdentityMarkProps {
|
|
19
21
|
/** Width & height in px. Default 24. */
|