@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +24 -0
  3. package/package.json +5 -7
  4. package/src/components/AliaAvatar.tsx +66 -0
  5. package/src/components/AliaChatContent.tsx +235 -112
  6. package/src/components/AliaChatHeader.tsx +118 -0
  7. package/src/components/AliaChatMessageList.tsx +171 -260
  8. package/src/components/AliaChatScreen.tsx +23 -24
  9. package/src/components/AliaChatSheet.tsx +70 -316
  10. package/src/components/AliaWelcomeMessage.tsx +47 -50
  11. package/src/components/AmbientField.tsx +577 -0
  12. package/src/components/IdentityMark.tsx +10 -2
  13. package/src/components/Markdown.tsx +116 -40
  14. package/src/components/PlanPreviewCard.tsx +71 -55
  15. package/src/components/ThinkingBlock.tsx +47 -0
  16. package/src/components/VoiceModeIcon.tsx +10 -0
  17. package/src/components/voice/VoiceControls.tsx +22 -73
  18. package/src/components/voice/VoiceSession.tsx +2 -1
  19. package/src/hooks/useAliaChat.ts +22 -15
  20. package/src/hooks/useAmbientWave.ts +35 -5
  21. package/src/hooks/useSoundEffects.ts +14 -11
  22. package/src/hooks/useSpeechToText.ts +9 -2
  23. package/src/hooks/useTTS.ts +163 -156
  24. package/src/hooks/useVoiceRoom.ts +228 -170
  25. package/src/index.ts +18 -6
  26. package/src/lib/catalogue.ts +17 -4
  27. package/src/lib/chat-stream.ts +25 -7
  28. package/src/lib/chat-transport.ts +2 -1
  29. package/src/lib/citations.ts +63 -0
  30. package/src/lib/content-utils.ts +3 -2
  31. package/src/lib/image-data-url.ts +15 -0
  32. package/src/lib/keyboard.tsx +1 -1
  33. package/src/lib/labels.tsx +81 -0
  34. package/src/lib/speech-recognition.native.ts +15 -4
  35. package/src/lib/speech-recognition.ts +27 -7
  36. package/src/lib/task-labels.ts +67 -0
  37. package/src/lib/tool-call-status.ts +28 -0
  38. package/src/lib/tool-registry.ts +14 -14
  39. package/src/lib/voice-text.ts +12 -6
  40. package/src/lib/voice-turn.ts +4 -1
  41. package/src/lib/work-log-copy.ts +61 -0
  42. package/src/lib/work-log.ts +378 -0
  43. package/src/tools.ts +11 -0
  44. package/src/types.ts +5 -2
  45. package/src/voice.ts +12 -4
  46. package/src/components/Reasoning.tsx +0 -152
  47. package/src/components/ResearchProgressCard.tsx +0 -159
  48. package/src/components/ThinkingIndicator.tsx +0 -98
  49. package/src/components/ui/button.tsx +0 -104
  50. package/src/components/ui/chat-text-input.tsx +0 -185
  51. package/src/components/ui/dropdown-menu.tsx +0 -2
  52. package/src/components/ui/dropdown-menu.web.tsx +0 -383
  53. package/src/components/ui/prompt-input/actions.tsx +0 -28
  54. package/src/components/ui/prompt-input/add-menu.tsx +0 -83
  55. package/src/components/ui/prompt-input/attachments.tsx +0 -181
  56. package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
  57. package/src/components/ui/prompt-input/context.tsx +0 -71
  58. package/src/components/ui/prompt-input/mic-button.tsx +0 -53
  59. package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
  60. package/src/components/ui/prompt-input/submit-button.tsx +0 -50
  61. package/src/components/ui/prompt-input/textarea.tsx +0 -80
  62. package/src/components/ui/text.tsx +0 -29
  63. package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
  64. package/src/components/voice/VoiceOverlay.tsx +0 -75
@@ -18,12 +18,14 @@
18
18
  */
19
19
 
20
20
  import React from 'react';
21
- import { View, Text, TouchableOpacity } from 'react-native';
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 { IdentityMark } from './IdentityMark';
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 bg-background"
82
- style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
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
- header={({ markState, hasMessages, clear }) => (
96
- <View className="flex-row items-center justify-between px-4 py-3">
97
- <View className="flex-row items-center gap-2.5">
98
- {headerLeft}
99
- <IdentityMark size={28} state={markState} spinOnPress />
100
- <Text className="text-lg font-semibold text-foreground">Alia</Text>
101
- </View>
102
- <View className="flex-row items-center gap-2">
103
- {hasMessages && (
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 React, {
2
- forwardRef,
3
- useImperativeHandle,
4
- useRef,
5
- useCallback,
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 { IdentityMark } from './IdentityMark';
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
- const SPRING_CONFIG = {
40
- damping: 25,
41
- stiffness: 300,
42
- mass: 0.8,
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
- ({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions, primaryColor, isDarkMode, voiceSession }, ref) => {
77
- const scheme = useColorScheme();
78
- const isDark = scheme === 'dark';
79
- const insets = useSafeAreaInsets();
80
-
81
- // Sheet visibility
82
- const [rendered, setRendered] = useState(false);
83
- const hasClosedRef = useRef(false);
84
- const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
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
- backdropOpacity.value = withTiming(0, { duration: 250 }, (finished) => {
138
- if (finished) runOnJS(finishDismiss)();
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: handlePresent, dismiss: handleDismiss }),
164
- [handlePresent, handleDismiss],
83
+ () => ({ present: () => sheetRef.current?.present(), dismiss: handleDismiss }),
84
+ [handleDismiss],
165
85
  );
166
86
 
167
- // Pan gesture for swipe-to-dismiss
168
- const nativeGesture = useMemo(() => Gesture.Native(), []);
169
-
170
- const panGesture = useMemo(() => Gesture.Pan()
171
- .simultaneousWithExternalGesture(nativeGesture)
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
- <Modal
227
- visible={rendered}
228
- transparent
229
- animationType="none"
230
- statusBarTranslucent
231
- onRequestClose={handleDismiss}
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
- <GestureHandlerRootView style={StyleSheet.absoluteFill}>
234
- {/* Backdrop */}
235
- <Animated.View style={[styles.backdrop, backdropAnimStyle]}>
236
- <Pressable style={StyleSheet.absoluteFill} onPress={handleDismiss} />
237
- </Animated.View>
238
-
239
- {/* Sheet */}
240
- <GestureDetector gesture={panGesture}>
241
- <Animated.View
242
- className="bg-background"
243
- style={[
244
- styles.sheet,
245
- sheetAnimStyle,
246
- sheetMaxHeightStyle,
247
- ]}
248
- >
249
- {/* Drag handle */}
250
- <View style={styles.dragHandle}>
251
- <View
252
- style={[
253
- styles.dragHandlePill,
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 { Pressable, StyleSheet, View } from 'react-native';
2
- import { BlurView } from 'expo-blur';
3
- import { Text } from './ui/text';
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 = 64,
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="w-full max-w-2xl">
28
- {/* Mark + Title */}
29
- <View className="items-start mb-8">
30
- <View className="mb-4">
31
- <IdentityMark size={markSize} spinOnPress />
32
- </View>
33
- <View className="space-y-2">
34
- <Text className="text-3xl font-bold tracking-tight text-foreground">
35
- {greeting}
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
- {/* Suggestion Grid */}
46
- {suggestions.length > 0 && (
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
  }