@alia.onl/sdk 12.0.0 → 13.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 +120 -0
- package/README.md +10 -12
- package/package.json +3 -3
- package/src/components/AliaAvatar.tsx +5 -1
- package/src/components/AliaChatContent.tsx +233 -57
- package/src/components/AliaChatHeader.tsx +100 -67
- package/src/components/AliaChatMessageList.tsx +173 -129
- package/src/components/AliaChatScreen.tsx +44 -16
- package/src/components/AliaChatSheet.tsx +155 -61
- package/src/components/AliaWelcomeMessage.tsx +26 -53
- package/src/components/ComposerSuggestions.tsx +45 -0
- 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 -3
- package/src/lib/agent-character.ts +28 -0
- package/src/lib/character-runtime.ts +10 -0
- package/src/lib/chat-stream.ts +20 -3
- package/src/lib/citations.ts +59 -0
- package/src/lib/delegation.ts +34 -0
- package/src/lib/flights.ts +24 -0
- package/src/lib/greeting.ts +15 -0
- package/src/lib/labels.tsx +17 -2
- package/src/lib/questions.ts +15 -1
- package/src/lib/suggestions.ts +53 -0
- package/src/lib/use-after-delay.ts +19 -0
- package/src/lib/work-log.ts +2 -1
- package/src/tools.ts +7 -1
- package/src/types.ts +6 -5
|
@@ -23,7 +23,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
23
23
|
import { KeyboardAvoidingView } from '../lib/keyboard';
|
|
24
24
|
import { AliaChatContent, type AliaChatContentProps } from './AliaChatContent';
|
|
25
25
|
import { AliaChatHeader } from './AliaChatHeader';
|
|
26
|
-
import type {
|
|
26
|
+
import type { AliaSuggestion } from '../types';
|
|
27
27
|
import type { AliaChatLabels } from '../lib/labels';
|
|
28
28
|
import type { VoiceSessionComponent } from '../types';
|
|
29
29
|
|
|
@@ -34,8 +34,14 @@ export interface AliaChatScreenProps {
|
|
|
34
34
|
model?: string;
|
|
35
35
|
/** API URL override */
|
|
36
36
|
apiUrl?: string;
|
|
37
|
-
/**
|
|
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) */
|
|
38
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;
|
|
39
45
|
/** Optional header right action (e.g., settings button) */
|
|
40
46
|
headerRight?: React.ReactNode;
|
|
41
47
|
/** Welcome screen greeting */
|
|
@@ -43,42 +49,44 @@ export interface AliaChatScreenProps {
|
|
|
43
49
|
/** Welcome screen subtitle */
|
|
44
50
|
welcomeSubtitle?: string;
|
|
45
51
|
/** Welcome screen suggestions */
|
|
46
|
-
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;
|
|
47
59
|
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
48
60
|
isDarkMode?: boolean;
|
|
49
61
|
/** Voice capability — `VoiceSession` from `@alia.onl/sdk/voice`. */
|
|
50
62
|
voiceSession?: VoiceSessionComponent;
|
|
51
63
|
/** Overrides for the chat's own copy (header, composer, plan, research…); omitted, English. */
|
|
52
64
|
labels?: Partial<AliaChatLabels>;
|
|
65
|
+
/** Where the 3D avatar's runtime is served from; omitted, Alia's shared copy (`ALIA_CHARACTER_RUNTIME_URL`). */
|
|
66
|
+
characterRuntimeUrl?: string;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
export function AliaChatScreen({
|
|
56
70
|
clientContext,
|
|
57
71
|
model,
|
|
58
72
|
apiUrl,
|
|
73
|
+
onBack,
|
|
59
74
|
headerLeft,
|
|
75
|
+
onShare,
|
|
76
|
+
headerMenu,
|
|
60
77
|
headerRight,
|
|
61
78
|
welcomeGreeting,
|
|
62
79
|
welcomeSubtitle,
|
|
63
80
|
welcomeSuggestions,
|
|
81
|
+
agents,
|
|
82
|
+
suggestionMatches,
|
|
83
|
+
onSuggestionQuery,
|
|
64
84
|
isDarkMode,
|
|
65
85
|
voiceSession,
|
|
66
86
|
labels,
|
|
87
|
+
characterRuntimeUrl,
|
|
67
88
|
}: AliaChatScreenProps) {
|
|
68
89
|
const insets = useSafeAreaInsets();
|
|
69
|
-
const header = useCallback<NonNullable<AliaChatContentProps['header']>>(
|
|
70
|
-
({ hasMessages, clear, markState }) => (
|
|
71
|
-
<AliaChatHeader
|
|
72
|
-
markState={markState}
|
|
73
|
-
hasMessages={hasMessages}
|
|
74
|
-
onClear={clear}
|
|
75
|
-
leading={headerLeft}
|
|
76
|
-
trailing={headerRight}
|
|
77
|
-
/>
|
|
78
|
-
),
|
|
79
|
-
[headerLeft, headerRight],
|
|
80
|
-
);
|
|
81
|
-
|
|
82
90
|
// The screen owns the whole window, so it keeps its composer out of both
|
|
83
91
|
// system bars: the top inset clears the status bar, the bottom one the
|
|
84
92
|
// gesture bar or home indicator an edge-to-edge app draws over.
|
|
@@ -89,6 +97,22 @@ export function AliaChatScreen({
|
|
|
89
97
|
// keyboard's height minus the inset: the composer sits on the keyboard, not
|
|
90
98
|
// one inset above it. (Its parent-relative layout would read as zero overlap
|
|
91
99
|
// this deep in a host's tree, which is why the offset is measured.)
|
|
100
|
+
const header = useCallback<NonNullable<AliaChatContentProps['header']>>(
|
|
101
|
+
({ hasMessages, clear, agents: answered }) => (
|
|
102
|
+
<AliaChatHeader
|
|
103
|
+
hasMessages={hasMessages}
|
|
104
|
+
agents={answered}
|
|
105
|
+
onClear={clear}
|
|
106
|
+
onBack={onBack}
|
|
107
|
+
onShare={onShare}
|
|
108
|
+
menu={headerMenu}
|
|
109
|
+
leading={headerLeft}
|
|
110
|
+
trailing={headerRight}
|
|
111
|
+
/>
|
|
112
|
+
),
|
|
113
|
+
[onBack, onShare, headerMenu, headerLeft, headerRight],
|
|
114
|
+
);
|
|
115
|
+
|
|
92
116
|
return (
|
|
93
117
|
<View
|
|
94
118
|
className="flex-1 bg-background-full"
|
|
@@ -102,9 +126,13 @@ export function AliaChatScreen({
|
|
|
102
126
|
welcomeGreeting={welcomeGreeting}
|
|
103
127
|
welcomeSubtitle={welcomeSubtitle}
|
|
104
128
|
welcomeSuggestions={welcomeSuggestions}
|
|
129
|
+
agents={agents}
|
|
130
|
+
suggestionMatches={suggestionMatches}
|
|
131
|
+
onSuggestionQuery={onSuggestionQuery}
|
|
105
132
|
isDarkMode={isDarkMode}
|
|
106
133
|
voiceSession={voiceSession}
|
|
107
134
|
labels={labels}
|
|
135
|
+
characterRuntimeUrl={characterRuntimeUrl}
|
|
108
136
|
header={header}
|
|
109
137
|
/>
|
|
110
138
|
</KeyboardAvoidingView>
|
|
@@ -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, type AliaChatContentProps } 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,12 @@ 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
|
+
/** Where the 3D avatar's runtime is served from; omitted, Alia's shared copy (`ALIA_CHARACTER_RUNTIME_URL`). */
|
|
56
|
+
characterRuntimeUrl?: string;
|
|
57
|
+
/** Draws the header's share button once there is a conversation; omitted, no share button. */
|
|
58
|
+
onShare?: () => void;
|
|
59
|
+
/** More rows for the header's menu (`DropdownMenuItem`s from `@oxy.so/bloom/dropdown-menu`). */
|
|
60
|
+
headerMenu?: React.ReactNode;
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
export interface AliaChatSheetRef {
|
|
@@ -55,83 +74,158 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
55
74
|
welcomeGreeting,
|
|
56
75
|
welcomeSubtitle,
|
|
57
76
|
welcomeSuggestions,
|
|
77
|
+
agents,
|
|
78
|
+
suggestionMatches,
|
|
79
|
+
onSuggestionQuery,
|
|
58
80
|
isDarkMode,
|
|
59
81
|
voiceSession,
|
|
60
82
|
labels,
|
|
83
|
+
characterRuntimeUrl,
|
|
84
|
+
onShare,
|
|
85
|
+
headerMenu,
|
|
61
86
|
},
|
|
62
87
|
ref,
|
|
63
88
|
) => {
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
89
|
+
// Bloom's resolved mode, not the system's scheme.
|
|
90
|
+
const { isDark } = useTheme();
|
|
91
|
+
const mainRef = useRef<BottomSheetRef>(null);
|
|
92
|
+
const conversationRef = useRef<BottomSheetRef>(null);
|
|
93
|
+
// Each sheet's chat registers its own canceller; closing a sheet stops its own turn.
|
|
94
|
+
const mainStopRef = useRef<(() => void) | null>(null);
|
|
95
|
+
const conversationStopRef = useRef<(() => void) | null>(null);
|
|
96
|
+
|
|
97
|
+
// The conversation sheet, stacked over the main one while a chat is open. The key makes
|
|
98
|
+
// every conversation a fresh chat.
|
|
99
|
+
const [conversation, setConversation] = useState<{ key: number; turn: AliaTurn } | null>(null);
|
|
67
100
|
|
|
68
|
-
const
|
|
69
|
-
|
|
101
|
+
const registerMainStop = useCallback((stop: (() => void) | null) => {
|
|
102
|
+
mainStopRef.current = stop;
|
|
103
|
+
}, []);
|
|
104
|
+
const registerConversationStop = useCallback((stop: (() => void) | null) => {
|
|
105
|
+
conversationStopRef.current = stop;
|
|
70
106
|
}, []);
|
|
71
107
|
|
|
72
|
-
const
|
|
73
|
-
|
|
108
|
+
const startConversation = useCallback((turn: AliaTurn) => {
|
|
109
|
+
setConversation((current) => ({ key: (current?.key ?? 0) + 1, turn }));
|
|
110
|
+
}, []);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (conversation) conversationRef.current?.present();
|
|
113
|
+
}, [conversation]);
|
|
114
|
+
|
|
115
|
+
// Back, and the menu's Clear: close the conversation, back to the main screen underneath.
|
|
116
|
+
const closeConversation = useCallback(() => {
|
|
117
|
+
conversationStopRef.current?.();
|
|
118
|
+
conversationRef.current?.dismiss();
|
|
119
|
+
}, []);
|
|
120
|
+
const onConversationDismissAttempt = useCallback(() => {
|
|
121
|
+
conversationStopRef.current?.();
|
|
122
|
+
return true;
|
|
74
123
|
}, []);
|
|
75
124
|
|
|
76
125
|
const handleDismiss = useCallback(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
conversationStopRef.current?.();
|
|
127
|
+
mainStopRef.current?.();
|
|
128
|
+
conversationRef.current?.dismiss();
|
|
129
|
+
mainRef.current?.dismiss();
|
|
130
|
+
}, []);
|
|
80
131
|
|
|
81
132
|
useImperativeHandle(
|
|
82
133
|
ref,
|
|
83
|
-
() => ({ present: () =>
|
|
134
|
+
() => ({ present: () => mainRef.current?.present(), dismiss: handleDismiss }),
|
|
84
135
|
[handleDismiss],
|
|
85
136
|
);
|
|
86
137
|
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
|
|
138
|
+
// Closing the main sheet (a drag, backdrop press, Back or Escape) closes what is stacked on it.
|
|
139
|
+
const onMainDismissAttempt = useCallback(() => {
|
|
140
|
+
mainStopRef.current?.();
|
|
141
|
+
conversationStopRef.current?.();
|
|
142
|
+
conversationRef.current?.dismiss();
|
|
90
143
|
return true;
|
|
91
|
-
}, [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const { width } = useWindowDimensions();
|
|
147
|
+
const detached = width >= DETACHED_FROM;
|
|
148
|
+
// Flush with the screen edge the sheet runs behind the home indicator: the composer clears it.
|
|
149
|
+
const { bottom } = useSafeAreaInsets();
|
|
150
|
+
const sheetStyle = detached
|
|
151
|
+
? { maxWidth: 600, alignSelf: 'center' as const, borderRadius: SHEET_RADIUS }
|
|
152
|
+
: undefined;
|
|
153
|
+
|
|
154
|
+
// The avatars fly between the two sheets: one controller, one layer.
|
|
155
|
+
const flights = useAvatarFlights();
|
|
156
|
+
const shared = {
|
|
157
|
+
flights,
|
|
158
|
+
clientContext,
|
|
159
|
+
model,
|
|
160
|
+
apiUrl,
|
|
161
|
+
isDarkMode: isDarkMode ?? isDark,
|
|
162
|
+
voiceSession,
|
|
163
|
+
bottomInset: detached ? 0 : bottom,
|
|
164
|
+
labels,
|
|
165
|
+
characterRuntimeUrl,
|
|
166
|
+
};
|
|
108
167
|
|
|
109
168
|
return (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
169
|
+
<>
|
|
170
|
+
<BottomSheet
|
|
171
|
+
ref={mainRef}
|
|
172
|
+
detached={detached}
|
|
173
|
+
scrollable={false}
|
|
174
|
+
manualActivation
|
|
175
|
+
onDismissAttempt={onMainDismissAttempt}
|
|
176
|
+
onDismiss={() => setConversation(null)}
|
|
177
|
+
style={sheetStyle}
|
|
178
|
+
>
|
|
179
|
+
{/* The main screen: the greeting, the suggestions and the composer. A first turn opens
|
|
180
|
+
the conversation in its own sheet over this one, so this chat stays empty. */}
|
|
181
|
+
<AliaChatContent
|
|
182
|
+
{...shared}
|
|
183
|
+
welcomeGreeting={welcomeGreeting}
|
|
184
|
+
welcomeSubtitle={welcomeSubtitle}
|
|
185
|
+
welcomeSuggestions={welcomeSuggestions}
|
|
186
|
+
agents={agents}
|
|
187
|
+
suggestionMatches={suggestionMatches}
|
|
188
|
+
onSuggestionQuery={onSuggestionQuery}
|
|
189
|
+
onStopReady={registerMainStop}
|
|
190
|
+
onStart={startConversation}
|
|
191
|
+
/>
|
|
192
|
+
</BottomSheet>
|
|
193
|
+
|
|
194
|
+
{conversation ? (
|
|
195
|
+
<BottomSheet
|
|
196
|
+
key={conversation.key}
|
|
197
|
+
ref={conversationRef}
|
|
198
|
+
detached={detached}
|
|
199
|
+
scrollable={false}
|
|
200
|
+
manualActivation
|
|
201
|
+
onDismissAttempt={onConversationDismissAttempt}
|
|
202
|
+
onDismiss={() => setConversation(null)}
|
|
203
|
+
style={sheetStyle}
|
|
204
|
+
>
|
|
205
|
+
<AliaChatContent
|
|
206
|
+
{...shared}
|
|
207
|
+
initialTurn={conversation.turn}
|
|
208
|
+
agents={agents}
|
|
209
|
+
onStopReady={registerConversationStop}
|
|
210
|
+
// The bar is the conversation's: back to the main screen, the profile, share and
|
|
211
|
+
// the menu.
|
|
212
|
+
header={({ agents }) => (
|
|
213
|
+
<View className="pt-8">
|
|
214
|
+
<AliaChatHeader
|
|
215
|
+
hasMessages
|
|
216
|
+
agents={agents}
|
|
217
|
+
onClear={closeConversation}
|
|
218
|
+
onBack={closeConversation}
|
|
219
|
+
onShare={onShare}
|
|
220
|
+
menu={headerMenu}
|
|
221
|
+
/>
|
|
222
|
+
</View>
|
|
223
|
+
)}
|
|
224
|
+
/>
|
|
225
|
+
</BottomSheet>
|
|
226
|
+
) : null}
|
|
227
|
+
<AvatarFlightLayer flights={flights} />
|
|
228
|
+
</>
|
|
135
229
|
);
|
|
136
230
|
},
|
|
137
231
|
);
|
|
@@ -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
|
);
|
|
@@ -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,30 +1,15 @@
|
|
|
1
1
|
import React, { useMemo, type ReactNode } from 'react';
|
|
2
|
-
import { View, Text, Platform, Linking
|
|
2
|
+
import { View, Text, Platform, Linking } from 'react-native';
|
|
3
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
3
4
|
import Markdown, { MarkdownIt, type ASTNode } from 'react-native-markdown-display';
|
|
4
5
|
import { CodeBlock } from '@oxy.so/bloom/code';
|
|
5
6
|
import type { AliaColors } from '../theme';
|
|
6
7
|
|
|
7
8
|
// Hardcoded fallback colors for standalone SDK usage (when no color override is passed).
|
|
8
9
|
// The main app always passes resolved colors via the `colors` prop.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
> = {
|
|
13
|
-
text: '#11181C',
|
|
14
|
-
muted: '#F4F4F5',
|
|
15
|
-
border: '#E5E5EA',
|
|
16
|
-
primary: '#7C3AED',
|
|
17
|
-
mutedForeground: '#71717A',
|
|
18
|
-
};
|
|
19
|
-
const FALLBACK_DARK: typeof FALLBACK_LIGHT = {
|
|
20
|
-
text: '#ECEDEE',
|
|
21
|
-
muted: '#27272A',
|
|
22
|
-
border: '#2C2C2E',
|
|
23
|
-
primary: '#A78BFA',
|
|
24
|
-
mutedForeground: '#A1A1AA',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const BODY_TEXT = { fontSize: 16, lineHeight: 28 } as const;
|
|
10
|
+
/** The body text of a reply: its bubble sets its padding from these. */
|
|
11
|
+
export const MARKDOWN_BODY = { fontSize: 16, lineHeight: 28 } as const;
|
|
12
|
+
const BODY_TEXT = MARKDOWN_BODY;
|
|
28
13
|
const HEADING_TEXT = { fontSize: 16, lineHeight: 22 } as const;
|
|
29
14
|
const MONO_FONT = Platform.select({ ios: 'Courier', android: 'monospace', default: 'monospace' })!;
|
|
30
15
|
|
|
@@ -180,7 +165,8 @@ function createRules(colors: AliaColors, renderCodeBlock?: RenderCodeBlock) {
|
|
|
180
165
|
<Text style={{ marginRight: 8, minWidth: 14, ...BODY_TEXT, color: mutedForeground }}>
|
|
181
166
|
{bullet}
|
|
182
167
|
</Text>
|
|
183
|
-
|
|
168
|
+
{/* A View, not a Text: a nested list is a View, and inside a Text it runs inline. */}
|
|
169
|
+
<View style={{ flex: 1 }}>{children}</View>
|
|
184
170
|
</View>
|
|
185
171
|
);
|
|
186
172
|
},
|
|
@@ -476,9 +462,17 @@ export function AliaMarkdown({
|
|
|
476
462
|
fontFamily,
|
|
477
463
|
renderCodeBlock,
|
|
478
464
|
}: AliaMarkdownProps) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
465
|
+
// Bloom's theme, which follows the app's chosen mode, not the system's scheme: a person who
|
|
466
|
+
// picked light on a dark OS gets dark text on a light surface, not the reverse.
|
|
467
|
+
const { colors: theme } = useTheme();
|
|
468
|
+
const colors = {
|
|
469
|
+
text: theme.text,
|
|
470
|
+
muted: theme.backgroundSecondary,
|
|
471
|
+
border: theme.border,
|
|
472
|
+
primary: theme.primary,
|
|
473
|
+
mutedForeground: theme.textSecondary,
|
|
474
|
+
...colorOverrides,
|
|
475
|
+
} as AliaColors;
|
|
482
476
|
|
|
483
477
|
const customRules = useMemo(
|
|
484
478
|
() => createRules(colors, renderCodeBlock),
|