@alia.onl/sdk 10.1.0 → 11.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 +62 -0
- package/README.md +24 -0
- package/package.json +5 -7
- package/src/components/AliaAvatar.tsx +66 -0
- package/src/components/AliaChatContent.tsx +235 -112
- package/src/components/AliaChatHeader.tsx +118 -0
- package/src/components/AliaChatMessageList.tsx +171 -260
- package/src/components/AliaChatScreen.tsx +23 -24
- package/src/components/AliaChatSheet.tsx +70 -316
- package/src/components/AliaWelcomeMessage.tsx +47 -50
- package/src/components/AmbientField.tsx +577 -0
- package/src/components/IdentityMark.tsx +10 -2
- package/src/components/Markdown.tsx +116 -40
- package/src/components/PlanPreviewCard.tsx +71 -55
- package/src/components/ThinkingBlock.tsx +47 -0
- package/src/components/VoiceModeIcon.tsx +10 -0
- package/src/components/voice/VoiceControls.tsx +22 -73
- package/src/components/voice/VoiceSession.tsx +2 -1
- package/src/hooks/useAliaChat.ts +22 -15
- package/src/hooks/useAmbientWave.ts +35 -5
- package/src/hooks/useSoundEffects.ts +14 -11
- package/src/hooks/useSpeechToText.ts +9 -2
- package/src/hooks/useTTS.ts +163 -156
- package/src/hooks/useVoiceRoom.ts +228 -170
- package/src/index.ts +18 -6
- package/src/lib/catalogue.ts +17 -4
- package/src/lib/chat-stream.ts +25 -7
- package/src/lib/chat-transport.ts +2 -1
- package/src/lib/citations.ts +63 -0
- package/src/lib/content-utils.ts +3 -2
- package/src/lib/image-data-url.ts +15 -0
- package/src/lib/keyboard.tsx +1 -1
- package/src/lib/labels.tsx +81 -0
- package/src/lib/speech-recognition.native.ts +15 -4
- package/src/lib/speech-recognition.ts +27 -7
- package/src/lib/task-labels.ts +67 -0
- package/src/lib/tool-call-status.ts +28 -0
- package/src/lib/tool-registry.ts +14 -14
- package/src/lib/voice-text.ts +12 -6
- package/src/lib/voice-turn.ts +4 -1
- package/src/lib/work-log-copy.ts +61 -0
- package/src/lib/work-log.ts +378 -0
- package/src/tools.ts +11 -0
- package/src/types.ts +5 -2
- package/src/voice.ts +12 -4
- package/src/components/Reasoning.tsx +0 -152
- package/src/components/ResearchProgressCard.tsx +0 -159
- package/src/components/ThinkingIndicator.tsx +0 -98
- package/src/components/ui/button.tsx +0 -104
- package/src/components/ui/chat-text-input.tsx +0 -185
- package/src/components/ui/dropdown-menu.tsx +0 -2
- package/src/components/ui/dropdown-menu.web.tsx +0 -383
- package/src/components/ui/prompt-input/actions.tsx +0 -28
- package/src/components/ui/prompt-input/add-menu.tsx +0 -83
- package/src/components/ui/prompt-input/attachments.tsx +0 -181
- package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
- package/src/components/ui/prompt-input/context.tsx +0 -71
- package/src/components/ui/prompt-input/mic-button.tsx +0 -53
- package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
- package/src/components/ui/prompt-input/submit-button.tsx +0 -50
- package/src/components/ui/prompt-input/textarea.tsx +0 -80
- package/src/components/ui/text.tsx +0 -29
- package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
- package/src/components/voice/VoiceOverlay.tsx +0 -75
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import React from 'react';
|
|
21
|
-
import {
|
|
21
|
+
import { useTheme } from '@oxy.so/bloom/theme';
|
|
22
|
+
import { View } from 'react-native';
|
|
22
23
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
23
24
|
import { KeyboardAvoidingView } from '../lib/keyboard';
|
|
24
25
|
import { AliaChatContent } from './AliaChatContent';
|
|
25
|
-
import {
|
|
26
|
+
import { AliaChatHeader } from './AliaChatHeader';
|
|
26
27
|
import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
28
|
+
import type { AliaChatLabels } from '../lib/labels';
|
|
27
29
|
import type { VoiceSessionComponent } from '../types';
|
|
28
30
|
|
|
29
31
|
export interface AliaChatScreenProps {
|
|
@@ -43,12 +45,12 @@ export interface AliaChatScreenProps {
|
|
|
43
45
|
welcomeSubtitle?: string;
|
|
44
46
|
/** Welcome screen suggestions */
|
|
45
47
|
welcomeSuggestions?: WelcomeSuggestion[];
|
|
46
|
-
/** Theme primary color hex — forwarded to the ambient wave overlay palette. */
|
|
47
|
-
primaryColor?: string;
|
|
48
48
|
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
49
49
|
isDarkMode?: boolean;
|
|
50
50
|
/** Voice capability — `VoiceSession` from `@alia.onl/sdk/voice`. */
|
|
51
51
|
voiceSession?: VoiceSessionComponent;
|
|
52
|
+
/** Overrides for the chat's own copy (header, composer, plan, research…); omitted, English. */
|
|
53
|
+
labels?: Partial<AliaChatLabels>;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export function AliaChatScreen({
|
|
@@ -60,11 +62,12 @@ export function AliaChatScreen({
|
|
|
60
62
|
welcomeGreeting,
|
|
61
63
|
welcomeSubtitle,
|
|
62
64
|
welcomeSuggestions,
|
|
63
|
-
primaryColor,
|
|
64
65
|
isDarkMode,
|
|
65
66
|
voiceSession,
|
|
67
|
+
labels,
|
|
66
68
|
}: AliaChatScreenProps) {
|
|
67
69
|
const insets = useSafeAreaInsets();
|
|
70
|
+
const { colors } = useTheme();
|
|
68
71
|
|
|
69
72
|
// The screen owns the whole window, so it keeps its composer out of both
|
|
70
73
|
// system bars: the top inset clears the status bar, the bottom one the
|
|
@@ -78,8 +81,12 @@ export function AliaChatScreen({
|
|
|
78
81
|
// this deep in a host's tree, which is why the offset is measured.)
|
|
79
82
|
return (
|
|
80
83
|
<View
|
|
81
|
-
className="flex-1
|
|
82
|
-
style={{
|
|
84
|
+
className="flex-1"
|
|
85
|
+
style={{
|
|
86
|
+
backgroundColor: colors.background,
|
|
87
|
+
paddingTop: insets.top,
|
|
88
|
+
paddingBottom: insets.bottom,
|
|
89
|
+
}}
|
|
83
90
|
>
|
|
84
91
|
<KeyboardAvoidingView behavior="padding" automaticOffset style={{ flex: 1 }}>
|
|
85
92
|
<AliaChatContent
|
|
@@ -89,25 +96,17 @@ export function AliaChatScreen({
|
|
|
89
96
|
welcomeGreeting={welcomeGreeting}
|
|
90
97
|
welcomeSubtitle={welcomeSubtitle}
|
|
91
98
|
welcomeSuggestions={welcomeSuggestions}
|
|
92
|
-
primaryColor={primaryColor}
|
|
93
99
|
isDarkMode={isDarkMode}
|
|
94
100
|
voiceSession={voiceSession}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<TouchableOpacity onPress={clear} className="px-2.5 py-1.5">
|
|
105
|
-
<Text className="text-sm text-muted-foreground">Clear</Text>
|
|
106
|
-
</TouchableOpacity>
|
|
107
|
-
)}
|
|
108
|
-
{headerRight}
|
|
109
|
-
</View>
|
|
110
|
-
</View>
|
|
101
|
+
labels={labels}
|
|
102
|
+
title={null}
|
|
103
|
+
headerLeading={({ hasMessages, clear }) => (
|
|
104
|
+
<AliaChatHeader
|
|
105
|
+
hasMessages={hasMessages}
|
|
106
|
+
onClear={clear}
|
|
107
|
+
leading={headerLeft}
|
|
108
|
+
trailing={headerRight}
|
|
109
|
+
/>
|
|
111
110
|
)}
|
|
112
111
|
/>
|
|
113
112
|
</KeyboardAvoidingView>
|
|
@@ -1,46 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
useState,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
} from 'react';
|
|
10
|
-
import {
|
|
11
|
-
View,
|
|
12
|
-
Text,
|
|
13
|
-
TouchableOpacity,
|
|
14
|
-
StyleSheet,
|
|
15
|
-
Modal,
|
|
16
|
-
Pressable,
|
|
17
|
-
Dimensions,
|
|
18
|
-
Platform,
|
|
19
|
-
useColorScheme,
|
|
20
|
-
type ViewStyle,
|
|
21
|
-
} from 'react-native';
|
|
22
|
-
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
23
|
-
import { useKeyboardHandler } from 'react-native-keyboard-controller';
|
|
24
|
-
import Animated, {
|
|
25
|
-
runOnJS,
|
|
26
|
-
useAnimatedStyle,
|
|
27
|
-
useSharedValue,
|
|
28
|
-
withSpring,
|
|
29
|
-
withTiming,
|
|
30
|
-
} from 'react-native-reanimated';
|
|
31
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
1
|
+
import { BottomSheet, type BottomSheetRef } from '@oxy.so/bloom/bottom-sheet';
|
|
2
|
+
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
|
|
3
|
+
import { useColorScheme, View } from 'react-native';
|
|
4
|
+
import type { AliaChatLabels } from '../lib/labels';
|
|
5
|
+
import type { VoiceSessionComponent } from '../types';
|
|
32
6
|
import { AliaChatContent } from './AliaChatContent';
|
|
33
|
-
import {
|
|
7
|
+
import { AliaChatHeader } from './AliaChatHeader';
|
|
34
8
|
import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
35
|
-
import type { VoiceSessionComponent } from '../types';
|
|
36
|
-
|
|
37
|
-
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
38
9
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
10
|
+
/**
|
|
11
|
+
* TODO(bloom): drop this once Bloom's sheet is rounder by default. Its detached
|
|
12
|
+
* sheet hardcodes `radius: 24` (`BottomSheetBase.tsx`, not a design token) and
|
|
13
|
+
* the web fork draws a plain `border-radius`, so 24 reads squarer than the
|
|
14
|
+
* native `smooth` curve it is meant to match. Remove `SHEET_RADIUS` and the
|
|
15
|
+
* `borderRadius` in `style` below when that lands.
|
|
16
|
+
*/
|
|
17
|
+
const SHEET_RADIUS = 40;
|
|
44
18
|
|
|
45
19
|
export interface AliaChatSheetProps {
|
|
46
20
|
/** App context injected into system prompt */
|
|
@@ -55,8 +29,6 @@ export interface AliaChatSheetProps {
|
|
|
55
29
|
welcomeSubtitle?: string;
|
|
56
30
|
/** Welcome screen suggestions */
|
|
57
31
|
welcomeSuggestions?: WelcomeSuggestion[];
|
|
58
|
-
/** Theme primary color hex — forwarded to the ambient wave overlay palette. */
|
|
59
|
-
primaryColor?: string;
|
|
60
32
|
/** Dark-mode flag — forwarded to the ambient wave overlay (defaults to the sheet's own color scheme). */
|
|
61
33
|
isDarkMode?: boolean;
|
|
62
34
|
/**
|
|
@@ -65,6 +37,8 @@ export interface AliaChatSheetProps {
|
|
|
65
37
|
* enters the bundle.
|
|
66
38
|
*/
|
|
67
39
|
voiceSession?: VoiceSessionComponent;
|
|
40
|
+
/** Overrides for the chat's own copy (header, composer, plan, research…); omitted, English. */
|
|
41
|
+
labels?: Partial<AliaChatLabels>;
|
|
68
42
|
}
|
|
69
43
|
|
|
70
44
|
export interface AliaChatSheetRef {
|
|
@@ -73,46 +47,24 @@ export interface AliaChatSheetRef {
|
|
|
73
47
|
}
|
|
74
48
|
|
|
75
49
|
export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
76
|
-
(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
50
|
+
(
|
|
51
|
+
{
|
|
52
|
+
clientContext,
|
|
53
|
+
model,
|
|
54
|
+
apiUrl,
|
|
55
|
+
welcomeGreeting,
|
|
56
|
+
welcomeSubtitle,
|
|
57
|
+
welcomeSuggestions,
|
|
58
|
+
isDarkMode,
|
|
59
|
+
voiceSession,
|
|
60
|
+
labels,
|
|
61
|
+
},
|
|
62
|
+
ref,
|
|
63
|
+
) => {
|
|
64
|
+
const isDark = useColorScheme() === 'dark';
|
|
65
|
+
const sheetRef = useRef<BottomSheetRef>(null);
|
|
85
66
|
const chatStopRef = useRef<(() => void) | null>(null);
|
|
86
67
|
|
|
87
|
-
// Reanimated shared values
|
|
88
|
-
const translateY = useSharedValue(SCREEN_HEIGHT);
|
|
89
|
-
const backdropOpacity = useSharedValue(0);
|
|
90
|
-
const scrollOffsetY = useSharedValue(0);
|
|
91
|
-
const allowPanClose = useSharedValue(true);
|
|
92
|
-
const keyboardHeight = useSharedValue(0);
|
|
93
|
-
const panContext = useSharedValue({ y: 0 });
|
|
94
|
-
|
|
95
|
-
useKeyboardHandler(
|
|
96
|
-
{
|
|
97
|
-
onMove: (e) => {
|
|
98
|
-
'worklet';
|
|
99
|
-
keyboardHeight.value = e.height;
|
|
100
|
-
},
|
|
101
|
-
onEnd: (e) => {
|
|
102
|
-
'worklet';
|
|
103
|
-
keyboardHeight.value = e.height;
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
[],
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Dismiss helpers
|
|
110
|
-
const finishDismiss = useCallback(() => {
|
|
111
|
-
if (hasClosedRef.current) return;
|
|
112
|
-
hasClosedRef.current = true;
|
|
113
|
-
setRendered(false);
|
|
114
|
-
}, []);
|
|
115
|
-
|
|
116
68
|
const stopChat = useCallback(() => {
|
|
117
69
|
chatStopRef.current?.();
|
|
118
70
|
}, []);
|
|
@@ -121,254 +73,56 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
121
73
|
chatStopRef.current = stop;
|
|
122
74
|
}, []);
|
|
123
75
|
|
|
124
|
-
const handlePresent = useCallback(() => {
|
|
125
|
-
hasClosedRef.current = false;
|
|
126
|
-
if (closeTimeoutRef.current) {
|
|
127
|
-
clearTimeout(closeTimeoutRef.current);
|
|
128
|
-
closeTimeoutRef.current = null;
|
|
129
|
-
}
|
|
130
|
-
setRendered(true);
|
|
131
|
-
backdropOpacity.value = withTiming(1, { duration: 250 });
|
|
132
|
-
translateY.value = withSpring(0, SPRING_CONFIG);
|
|
133
|
-
}, []);
|
|
134
|
-
|
|
135
76
|
const handleDismiss = useCallback(() => {
|
|
136
77
|
stopChat();
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
translateY.value = withSpring(SCREEN_HEIGHT, {
|
|
141
|
-
...SPRING_CONFIG,
|
|
142
|
-
stiffness: 250,
|
|
143
|
-
});
|
|
144
|
-
if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
|
|
145
|
-
closeTimeoutRef.current = setTimeout(() => {
|
|
146
|
-
finishDismiss();
|
|
147
|
-
closeTimeoutRef.current = null;
|
|
148
|
-
}, 350);
|
|
149
|
-
}, [finishDismiss, stopChat]);
|
|
150
|
-
|
|
151
|
-
useEffect(
|
|
152
|
-
() => () => {
|
|
153
|
-
if (closeTimeoutRef.current) {
|
|
154
|
-
clearTimeout(closeTimeoutRef.current);
|
|
155
|
-
closeTimeoutRef.current = null;
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
[],
|
|
159
|
-
);
|
|
78
|
+
sheetRef.current?.dismiss();
|
|
79
|
+
}, [stopChat]);
|
|
160
80
|
|
|
161
81
|
useImperativeHandle(
|
|
162
82
|
ref,
|
|
163
|
-
() => ({ present:
|
|
164
|
-
[
|
|
83
|
+
() => ({ present: () => sheetRef.current?.present(), dismiss: handleDismiss }),
|
|
84
|
+
[handleDismiss],
|
|
165
85
|
);
|
|
166
86
|
|
|
167
|
-
//
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.onStart(() => {
|
|
173
|
-
'worklet';
|
|
174
|
-
panContext.value = { y: translateY.value };
|
|
175
|
-
allowPanClose.value = scrollOffsetY.value <= 8;
|
|
176
|
-
})
|
|
177
|
-
.onUpdate((event) => {
|
|
178
|
-
'worklet';
|
|
179
|
-
if (!allowPanClose.value) return;
|
|
180
|
-
if (event.translationY > 0 && scrollOffsetY.value > 8) return;
|
|
181
|
-
const newY = panContext.value.y + event.translationY;
|
|
182
|
-
translateY.value = Math.max(0, newY);
|
|
183
|
-
})
|
|
184
|
-
.onEnd((event) => {
|
|
185
|
-
'worklet';
|
|
186
|
-
if (!allowPanClose.value) return;
|
|
187
|
-
const velocity = event.velocityY;
|
|
188
|
-
const distance = translateY.value;
|
|
189
|
-
const closeThreshold = Math.max(140, SCREEN_HEIGHT * 0.25);
|
|
190
|
-
const shouldClose =
|
|
191
|
-
velocity > 900 || (distance > closeThreshold && velocity > -300);
|
|
192
|
-
|
|
193
|
-
if (shouldClose) {
|
|
194
|
-
runOnJS(stopChat)();
|
|
195
|
-
translateY.value = withSpring(SCREEN_HEIGHT, {
|
|
196
|
-
...SPRING_CONFIG,
|
|
197
|
-
velocity,
|
|
198
|
-
});
|
|
199
|
-
backdropOpacity.value = withTiming(0, { duration: 250 }, (finished) => {
|
|
200
|
-
if (finished) runOnJS(finishDismiss)();
|
|
201
|
-
});
|
|
202
|
-
} else {
|
|
203
|
-
translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
|
|
204
|
-
}
|
|
205
|
-
}), [nativeGesture, finishDismiss, stopChat]);
|
|
206
|
-
|
|
207
|
-
// Animated styles
|
|
208
|
-
const backdropAnimStyle = useAnimatedStyle(() => ({
|
|
209
|
-
opacity: backdropOpacity.value,
|
|
210
|
-
}));
|
|
211
|
-
|
|
212
|
-
const sheetAnimStyle = useAnimatedStyle(() => ({
|
|
213
|
-
transform: [{ translateY: translateY.value - keyboardHeight.value }],
|
|
214
|
-
}));
|
|
215
|
-
|
|
216
|
-
const sheetMaxHeightStyle = useAnimatedStyle(
|
|
217
|
-
() => ({
|
|
218
|
-
maxHeight: SCREEN_HEIGHT - keyboardHeight.value - insets.top,
|
|
219
|
-
}),
|
|
220
|
-
[insets.top],
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
if (!rendered) return null;
|
|
87
|
+
// A drag, backdrop press, Back or Escape starts closing here: stop the turn first.
|
|
88
|
+
const onDismissAttempt = useCallback(() => {
|
|
89
|
+
stopChat();
|
|
90
|
+
return true;
|
|
91
|
+
}, [stopChat]);
|
|
224
92
|
|
|
225
93
|
return (
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
94
|
+
<BottomSheet
|
|
95
|
+
ref={sheetRef}
|
|
96
|
+
detached
|
|
97
|
+
scrollable={false}
|
|
98
|
+
manualActivation
|
|
99
|
+
onDismissAttempt={onDismissAttempt}
|
|
100
|
+
onDismiss={stopChat}
|
|
101
|
+
style={{ maxWidth: 600, alignSelf: 'center', borderRadius: SHEET_RADIUS }}
|
|
232
102
|
>
|
|
233
|
-
<
|
|
234
|
-
{
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
>
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
{ backgroundColor: isDark ? '#444' : '#C7C7CC' },
|
|
255
|
-
]}
|
|
256
|
-
/>
|
|
257
|
-
</View>
|
|
258
|
-
|
|
259
|
-
{/* Chat content (header rendered inside via render prop) */}
|
|
260
|
-
<GestureDetector gesture={nativeGesture}>
|
|
261
|
-
<AliaChatContent
|
|
262
|
-
clientContext={clientContext}
|
|
263
|
-
model={model}
|
|
264
|
-
apiUrl={apiUrl}
|
|
265
|
-
scrollOffsetY={scrollOffsetY}
|
|
266
|
-
welcomeGreeting={welcomeGreeting}
|
|
267
|
-
welcomeSubtitle={welcomeSubtitle}
|
|
268
|
-
welcomeSuggestions={welcomeSuggestions}
|
|
269
|
-
primaryColor={primaryColor}
|
|
270
|
-
isDarkMode={isDarkMode ?? isDark}
|
|
271
|
-
voiceSession={voiceSession}
|
|
272
|
-
onStopReady={registerChatStop}
|
|
273
|
-
header={({ markState, hasMessages, clear }) => (
|
|
274
|
-
<View style={styles.header}>
|
|
275
|
-
<View style={styles.headerLeft}>
|
|
276
|
-
<IdentityMark size={28} state={markState} spinOnPress />
|
|
277
|
-
<Text className="text-lg font-semibold text-foreground">
|
|
278
|
-
Alia
|
|
279
|
-
</Text>
|
|
280
|
-
</View>
|
|
281
|
-
<View style={styles.headerRight}>
|
|
282
|
-
{hasMessages && (
|
|
283
|
-
<TouchableOpacity onPress={clear} style={styles.clearButton}>
|
|
284
|
-
<Text className="text-sm text-muted-foreground">
|
|
285
|
-
Clear
|
|
286
|
-
</Text>
|
|
287
|
-
</TouchableOpacity>
|
|
288
|
-
)}
|
|
289
|
-
<TouchableOpacity
|
|
290
|
-
onPress={handleDismiss}
|
|
291
|
-
style={styles.closeButton}
|
|
292
|
-
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
293
|
-
>
|
|
294
|
-
<Text className="text-lg font-semibold text-muted-foreground">
|
|
295
|
-
{'\u2715'}
|
|
296
|
-
</Text>
|
|
297
|
-
</TouchableOpacity>
|
|
298
|
-
</View>
|
|
299
|
-
</View>
|
|
300
|
-
)}
|
|
301
|
-
/>
|
|
302
|
-
</GestureDetector>
|
|
303
|
-
</Animated.View>
|
|
304
|
-
</GestureDetector>
|
|
305
|
-
</GestureHandlerRootView>
|
|
306
|
-
</Modal>
|
|
103
|
+
<AliaChatContent
|
|
104
|
+
clientContext={clientContext}
|
|
105
|
+
model={model}
|
|
106
|
+
apiUrl={apiUrl}
|
|
107
|
+
welcomeGreeting={welcomeGreeting}
|
|
108
|
+
welcomeSubtitle={welcomeSubtitle}
|
|
109
|
+
welcomeSuggestions={welcomeSuggestions}
|
|
110
|
+
isDarkMode={isDarkMode ?? isDark}
|
|
111
|
+
voiceSession={voiceSession}
|
|
112
|
+
labels={labels}
|
|
113
|
+
onStopReady={registerChatStop}
|
|
114
|
+
// The top padding clears the 28px drag handle, which Bloom floats over the content
|
|
115
|
+
// instead of reserving room for.
|
|
116
|
+
title={null}
|
|
117
|
+
headerLeading={({ hasMessages, clear }) => (
|
|
118
|
+
<View className="pt-8">
|
|
119
|
+
<AliaChatHeader hasMessages={hasMessages} onClear={clear} onClose={handleDismiss} />
|
|
120
|
+
</View>
|
|
121
|
+
)}
|
|
122
|
+
/>
|
|
123
|
+
</BottomSheet>
|
|
307
124
|
);
|
|
308
125
|
},
|
|
309
126
|
);
|
|
310
127
|
|
|
311
128
|
AliaChatSheet.displayName = 'AliaChatSheet';
|
|
312
|
-
|
|
313
|
-
const styles = StyleSheet.create({
|
|
314
|
-
backdrop: {
|
|
315
|
-
...StyleSheet.absoluteFill,
|
|
316
|
-
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
317
|
-
},
|
|
318
|
-
sheet: {
|
|
319
|
-
position: 'absolute',
|
|
320
|
-
bottom: 0,
|
|
321
|
-
left: 0,
|
|
322
|
-
right: 0,
|
|
323
|
-
borderTopLeftRadius: 24,
|
|
324
|
-
borderTopRightRadius: 24,
|
|
325
|
-
overflow: 'hidden',
|
|
326
|
-
maxWidth: 600,
|
|
327
|
-
alignSelf: 'center',
|
|
328
|
-
...Platform.select({
|
|
329
|
-
web: {
|
|
330
|
-
marginHorizontal: 'auto',
|
|
331
|
-
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
|
332
|
-
} as ViewStyle & { boxShadow?: string },
|
|
333
|
-
default: { elevation: 16 },
|
|
334
|
-
}),
|
|
335
|
-
},
|
|
336
|
-
dragHandle: {
|
|
337
|
-
alignItems: 'center',
|
|
338
|
-
paddingTop: 10,
|
|
339
|
-
paddingBottom: 4,
|
|
340
|
-
},
|
|
341
|
-
dragHandlePill: {
|
|
342
|
-
width: 36,
|
|
343
|
-
height: 5,
|
|
344
|
-
borderRadius: 3,
|
|
345
|
-
},
|
|
346
|
-
header: {
|
|
347
|
-
flexDirection: 'row',
|
|
348
|
-
alignItems: 'center',
|
|
349
|
-
justifyContent: 'space-between',
|
|
350
|
-
paddingHorizontal: 16,
|
|
351
|
-
paddingVertical: 8,
|
|
352
|
-
},
|
|
353
|
-
headerLeft: {
|
|
354
|
-
flexDirection: 'row',
|
|
355
|
-
alignItems: 'center',
|
|
356
|
-
gap: 10,
|
|
357
|
-
},
|
|
358
|
-
headerRight: {
|
|
359
|
-
flexDirection: 'row',
|
|
360
|
-
alignItems: 'center',
|
|
361
|
-
gap: 8,
|
|
362
|
-
},
|
|
363
|
-
clearButton: {
|
|
364
|
-
paddingHorizontal: 10,
|
|
365
|
-
paddingVertical: 6,
|
|
366
|
-
},
|
|
367
|
-
closeButton: {
|
|
368
|
-
width: 32,
|
|
369
|
-
height: 32,
|
|
370
|
-
alignItems: 'center',
|
|
371
|
-
justifyContent: 'center',
|
|
372
|
-
borderRadius: 16,
|
|
373
|
-
},
|
|
374
|
-
});
|
|
@@ -1,75 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Item } from '@oxy.so/bloom/item';
|
|
2
|
+
import { Text } from '@oxy.so/bloom/typography';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
4
|
import Animated, { FadeIn } from 'react-native-reanimated';
|
|
5
|
-
import { IdentityMark } from './IdentityMark';
|
|
6
5
|
import type { WelcomeSuggestion } from '../types';
|
|
6
|
+
import { AliaAvatar } from './AliaAvatar';
|
|
7
7
|
|
|
8
8
|
export type { WelcomeSuggestion };
|
|
9
9
|
|
|
10
10
|
export interface AliaWelcomeMessageProps {
|
|
11
|
+
/** The greeting, e.g. "Hi Nate, what's on your mind?". */
|
|
11
12
|
greeting: string;
|
|
12
13
|
subtitle?: string;
|
|
13
14
|
suggestions?: WelcomeSuggestion[];
|
|
14
15
|
onSuggestionPress?: (text: string) => void;
|
|
15
16
|
markSize?: number;
|
|
17
|
+
/** Leave the avatar out, where the surrounding header already shows it. */
|
|
18
|
+
hideMark?: boolean;
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The empty chat, as the Alia app has it: the avatar beside the greeting at display
|
|
23
|
+
* size, centred (stacked on a phone, side by side from `md`), and the suggestions as
|
|
24
|
+
* compact rows under it.
|
|
25
|
+
*/
|
|
18
26
|
export function AliaWelcomeMessage({
|
|
19
27
|
greeting,
|
|
20
28
|
subtitle,
|
|
21
29
|
suggestions = [],
|
|
22
30
|
onSuggestionPress,
|
|
23
|
-
markSize =
|
|
31
|
+
markSize = 38,
|
|
32
|
+
hideMark = false,
|
|
24
33
|
}: AliaWelcomeMessageProps) {
|
|
25
34
|
return (
|
|
26
|
-
<View className="flex-1 items-center justify-center px-4">
|
|
27
|
-
<View className="
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</Text>
|
|
37
|
-
{subtitle ? (
|
|
38
|
-
<Text className="text-xl font-medium text-muted-foreground">
|
|
39
|
-
{subtitle}
|
|
40
|
-
</Text>
|
|
41
|
-
) : null}
|
|
42
|
-
</View>
|
|
35
|
+
<View className="flex-1 items-center justify-center gap-6 px-4">
|
|
36
|
+
<View className="items-center gap-2">
|
|
37
|
+
<View className="flex-col items-center justify-center gap-3 md:flex-row">
|
|
38
|
+
{hideMark ? null : <AliaAvatar size={markSize} interactive />}
|
|
39
|
+
<Text
|
|
40
|
+
selectable={false}
|
|
41
|
+
className="text-center text-4xl tracking-tight text-text-primary md:text-left"
|
|
42
|
+
>
|
|
43
|
+
{greeting}
|
|
44
|
+
</Text>
|
|
43
45
|
</View>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<Animated.View entering={FadeIn.duration(400)}>
|
|
48
|
-
<View className="flex-row flex-wrap gap-2">
|
|
49
|
-
{suggestions.map((item) => (
|
|
50
|
-
<Pressable
|
|
51
|
-
key={item.id}
|
|
52
|
-
className="flex-1 min-w-[35%] flex-col items-start rounded-3xl border border-border overflow-hidden active:bg-muted/50"
|
|
53
|
-
onPress={() => onSuggestionPress?.(item.description)}
|
|
54
|
-
>
|
|
55
|
-
<BlurView intensity={60} tint="default" style={StyleSheet.absoluteFill} />
|
|
56
|
-
<View className="p-4 w-full">
|
|
57
|
-
<Text className="text-sm font-medium text-surface-foreground mb-1" numberOfLines={1}>
|
|
58
|
-
{item.title}
|
|
59
|
-
</Text>
|
|
60
|
-
<Text
|
|
61
|
-
className="text-xs text-muted-foreground line-clamp-1"
|
|
62
|
-
numberOfLines={1}
|
|
63
|
-
>
|
|
64
|
-
{item.description}
|
|
65
|
-
</Text>
|
|
66
|
-
</View>
|
|
67
|
-
</Pressable>
|
|
68
|
-
))}
|
|
69
|
-
</View>
|
|
70
|
-
</Animated.View>
|
|
71
|
-
)}
|
|
46
|
+
{subtitle ? (
|
|
47
|
+
<Text className="text-center text-lg text-text-secondary">{subtitle}</Text>
|
|
48
|
+
) : null}
|
|
72
49
|
</View>
|
|
50
|
+
|
|
51
|
+
{suggestions.length > 0 ? (
|
|
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>
|
|
69
|
+
) : null}
|
|
73
70
|
</View>
|
|
74
71
|
);
|
|
75
72
|
}
|