@alia.onl/sdk 3.1.2 → 3.1.3
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/alia.onl-sdk-3.1.3.tgz +0 -0
- package/package.json +36 -28
- package/src/components/AliaChatContent.tsx +215 -253
- package/src/components/AliaChatMessageList.tsx +7 -2
- package/src/components/AliaChatScreen.tsx +28 -42
- package/src/components/AliaChatSheet.tsx +40 -55
- package/src/components/AliaMark.tsx +144 -0
- package/src/components/AliaWelcomeMessage.tsx +5 -5
- package/src/components/Markdown.tsx +18 -18
- package/src/components/ThinkingIndicator.tsx +6 -27
- package/src/components/ui/chat-text-input.tsx +4 -3
- package/src/components/ui/dropdown-menu.web.tsx +7 -6
- package/src/components/ui/prompt-input/autocomplete.tsx +1 -1
- package/src/components/ui/prompt-input/context.tsx +1 -1
- package/src/components/ui/prompt-input/prompt-input.tsx +3 -1
- package/src/components/voice/AudioWaveVisualizer.tsx +27 -10
- package/src/components/voice/VoiceOverlay.tsx +37 -9
- package/src/hooks/useAliaChat.ts +6 -6
- package/src/hooks/useAmbientWave.ts +142 -0
- package/src/hooks/useAudioLevelMonitor.ts +6 -6
- package/src/hooks/useDocumentPicker.ts +1 -1
- package/src/hooks/useImagePicker.ts +1 -1
- package/src/hooks/useSoundEffects.ts +6 -5
- package/src/hooks/useSpeechToText.ts +21 -7
- package/src/hooks/useTTS.ts +6 -4
- package/src/hooks/useVoiceRoom.ts +52 -7
- package/src/index.ts +4 -2
- package/src/lib/content-utils.ts +18 -6
- package/src/lib/utils.ts +11 -0
- package/src/optional-deps.d.ts +76 -15
- package/src/types.ts +4 -4
- package/src/components/AliaFace.tsx +0 -452
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import React
|
|
16
|
+
import React from 'react';
|
|
17
17
|
import { View, Text, TouchableOpacity } from 'react-native';
|
|
18
18
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
19
|
-
import { AliaChatContent
|
|
20
|
-
import {
|
|
19
|
+
import { AliaChatContent } from './AliaChatContent';
|
|
20
|
+
import { AliaMark } from './AliaMark';
|
|
21
21
|
import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
22
22
|
|
|
23
23
|
export interface AliaChatScreenProps {
|
|
@@ -37,6 +37,10 @@ export interface AliaChatScreenProps {
|
|
|
37
37
|
welcomeSubtitle?: string;
|
|
38
38
|
/** Welcome screen suggestions */
|
|
39
39
|
welcomeSuggestions?: WelcomeSuggestion[];
|
|
40
|
+
/** Theme primary color hex — forwarded to the ambient wave overlay palette. */
|
|
41
|
+
primaryColor?: string;
|
|
42
|
+
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
43
|
+
isDarkMode?: boolean;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
export function AliaChatScreen({
|
|
@@ -48,58 +52,40 @@ export function AliaChatScreen({
|
|
|
48
52
|
welcomeGreeting,
|
|
49
53
|
welcomeSubtitle,
|
|
50
54
|
welcomeSuggestions,
|
|
55
|
+
primaryColor,
|
|
56
|
+
isDarkMode,
|
|
51
57
|
}: AliaChatScreenProps) {
|
|
52
58
|
const insets = useSafeAreaInsets();
|
|
53
|
-
const contentRef = useRef<AliaChatContentRef>(null);
|
|
54
|
-
|
|
55
|
-
const [faceExpression, setFaceExpression] = useState<AliaExpression>('Idle A');
|
|
56
|
-
const [hasMessages, setHasMessages] = useState(false);
|
|
57
|
-
|
|
58
|
-
// Poll content ref for header state
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
const interval = setInterval(() => {
|
|
61
|
-
if (contentRef.current) {
|
|
62
|
-
setFaceExpression(contentRef.current.faceExpression);
|
|
63
|
-
setHasMessages(contentRef.current.messages.length > 0);
|
|
64
|
-
}
|
|
65
|
-
}, 200);
|
|
66
|
-
return () => clearInterval(interval);
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
|
-
const handleClear = useCallback(() => {
|
|
70
|
-
contentRef.current?.clear();
|
|
71
|
-
}, []);
|
|
72
59
|
|
|
73
60
|
return (
|
|
74
61
|
<View className="flex-1 bg-background" style={{ paddingTop: insets.top }}>
|
|
75
|
-
{/* Header */}
|
|
76
|
-
<View className="flex-row items-center justify-between px-4 py-3">
|
|
77
|
-
<View className="flex-row items-center gap-2.5">
|
|
78
|
-
{headerLeft}
|
|
79
|
-
<AliaFace size={28} expression={faceExpression} />
|
|
80
|
-
<Text className="text-lg font-semibold text-foreground">Alia</Text>
|
|
81
|
-
</View>
|
|
82
|
-
<View className="flex-row items-center gap-2">
|
|
83
|
-
{hasMessages && (
|
|
84
|
-
<TouchableOpacity onPress={handleClear} className="px-2.5 py-1.5">
|
|
85
|
-
<Text className="text-sm text-muted-foreground">Clear</Text>
|
|
86
|
-
</TouchableOpacity>
|
|
87
|
-
)}
|
|
88
|
-
{headerRight}
|
|
89
|
-
</View>
|
|
90
|
-
</View>
|
|
91
|
-
|
|
92
|
-
{/* Chat content */}
|
|
93
62
|
<AliaChatContent
|
|
94
|
-
ref={contentRef}
|
|
95
63
|
clientContext={clientContext}
|
|
96
64
|
model={model}
|
|
97
65
|
apiUrl={apiUrl}
|
|
98
66
|
welcomeGreeting={welcomeGreeting}
|
|
99
67
|
welcomeSubtitle={welcomeSubtitle}
|
|
100
68
|
welcomeSuggestions={welcomeSuggestions}
|
|
69
|
+
primaryColor={primaryColor}
|
|
70
|
+
isDarkMode={isDarkMode}
|
|
71
|
+
header={({ markState, hasMessages, clear }) => (
|
|
72
|
+
<View className="flex-row items-center justify-between px-4 py-3">
|
|
73
|
+
<View className="flex-row items-center gap-2.5">
|
|
74
|
+
{headerLeft}
|
|
75
|
+
<AliaMark size={28} state={markState} />
|
|
76
|
+
<Text className="text-lg font-semibold text-foreground">Alia</Text>
|
|
77
|
+
</View>
|
|
78
|
+
<View className="flex-row items-center gap-2">
|
|
79
|
+
{hasMessages && (
|
|
80
|
+
<TouchableOpacity onPress={clear} className="px-2.5 py-1.5">
|
|
81
|
+
<Text className="text-sm text-muted-foreground">Clear</Text>
|
|
82
|
+
</TouchableOpacity>
|
|
83
|
+
)}
|
|
84
|
+
{headerRight}
|
|
85
|
+
</View>
|
|
86
|
+
</View>
|
|
87
|
+
)}
|
|
101
88
|
/>
|
|
102
89
|
</View>
|
|
103
90
|
);
|
|
104
91
|
}
|
|
105
|
-
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Dimensions,
|
|
18
18
|
Platform,
|
|
19
19
|
useColorScheme,
|
|
20
|
+
type ViewStyle,
|
|
20
21
|
} from 'react-native';
|
|
21
22
|
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
22
23
|
import { useKeyboardHandler } from 'react-native-keyboard-controller';
|
|
@@ -28,8 +29,8 @@ import Animated, {
|
|
|
28
29
|
withTiming,
|
|
29
30
|
} from 'react-native-reanimated';
|
|
30
31
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
31
|
-
import { AliaChatContent
|
|
32
|
-
import {
|
|
32
|
+
import { AliaChatContent } from './AliaChatContent';
|
|
33
|
+
import { AliaMark } from './AliaMark';
|
|
33
34
|
import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
34
35
|
|
|
35
36
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
@@ -53,6 +54,10 @@ export interface AliaChatSheetProps {
|
|
|
53
54
|
welcomeSubtitle?: string;
|
|
54
55
|
/** Welcome screen suggestions */
|
|
55
56
|
welcomeSuggestions?: WelcomeSuggestion[];
|
|
57
|
+
/** Theme primary color hex — forwarded to the ambient wave overlay palette. */
|
|
58
|
+
primaryColor?: string;
|
|
59
|
+
/** Dark-mode flag — forwarded to the ambient wave overlay (defaults to the sheet's own color scheme). */
|
|
60
|
+
isDarkMode?: boolean;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
export interface AliaChatSheetRef {
|
|
@@ -61,33 +66,16 @@ export interface AliaChatSheetRef {
|
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
64
|
-
({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions }, ref) => {
|
|
69
|
+
({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions, primaryColor, isDarkMode }, ref) => {
|
|
65
70
|
const scheme = useColorScheme();
|
|
66
71
|
const isDark = scheme === 'dark';
|
|
67
72
|
const insets = useSafeAreaInsets();
|
|
68
73
|
|
|
69
|
-
// Content ref for reading state
|
|
70
|
-
const contentRef = useRef<AliaChatContentRef>(null);
|
|
71
|
-
const [faceExpression, setFaceExpression] = useState<AliaExpression>('Idle A');
|
|
72
|
-
const [hasMessages, setHasMessages] = useState(false);
|
|
73
|
-
|
|
74
74
|
// Sheet visibility
|
|
75
75
|
const [rendered, setRendered] = useState(false);
|
|
76
76
|
const hasClosedRef = useRef(false);
|
|
77
77
|
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
78
78
|
|
|
79
|
-
// Poll content ref for header state (lightweight, only when rendered)
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (!rendered) return;
|
|
82
|
-
const interval = setInterval(() => {
|
|
83
|
-
if (contentRef.current) {
|
|
84
|
-
setFaceExpression(contentRef.current.faceExpression);
|
|
85
|
-
setHasMessages(contentRef.current.messages.length > 0);
|
|
86
|
-
}
|
|
87
|
-
}, 200);
|
|
88
|
-
return () => clearInterval(interval);
|
|
89
|
-
}, [rendered]);
|
|
90
|
-
|
|
91
79
|
// Reanimated shared values
|
|
92
80
|
const translateY = useSharedValue(SCREEN_HEIGHT);
|
|
93
81
|
const backdropOpacity = useSharedValue(0);
|
|
@@ -214,10 +202,6 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
214
202
|
[insets.top],
|
|
215
203
|
);
|
|
216
204
|
|
|
217
|
-
const handleClear = useCallback(() => {
|
|
218
|
-
contentRef.current?.clear();
|
|
219
|
-
}, []);
|
|
220
|
-
|
|
221
205
|
if (!rendered) return null;
|
|
222
206
|
|
|
223
207
|
return (
|
|
@@ -254,38 +238,9 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
254
238
|
/>
|
|
255
239
|
</View>
|
|
256
240
|
|
|
257
|
-
{/*
|
|
258
|
-
<View style={styles.header}>
|
|
259
|
-
<View style={styles.headerLeft}>
|
|
260
|
-
<AliaFace size={28} expression={faceExpression} />
|
|
261
|
-
<Text className="text-lg font-semibold text-foreground">
|
|
262
|
-
Alia
|
|
263
|
-
</Text>
|
|
264
|
-
</View>
|
|
265
|
-
<View style={styles.headerRight}>
|
|
266
|
-
{hasMessages && (
|
|
267
|
-
<TouchableOpacity onPress={handleClear} style={styles.clearButton}>
|
|
268
|
-
<Text className="text-sm text-muted-foreground">
|
|
269
|
-
Clear
|
|
270
|
-
</Text>
|
|
271
|
-
</TouchableOpacity>
|
|
272
|
-
)}
|
|
273
|
-
<TouchableOpacity
|
|
274
|
-
onPress={handleDismiss}
|
|
275
|
-
style={styles.closeButton}
|
|
276
|
-
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
277
|
-
>
|
|
278
|
-
<Text className="text-lg font-semibold text-muted-foreground">
|
|
279
|
-
{'\u2715'}
|
|
280
|
-
</Text>
|
|
281
|
-
</TouchableOpacity>
|
|
282
|
-
</View>
|
|
283
|
-
</View>
|
|
284
|
-
|
|
285
|
-
{/* Chat content */}
|
|
241
|
+
{/* Chat content (header rendered inside via render prop) */}
|
|
286
242
|
<GestureDetector gesture={nativeGesture}>
|
|
287
243
|
<AliaChatContent
|
|
288
|
-
ref={contentRef}
|
|
289
244
|
clientContext={clientContext}
|
|
290
245
|
model={model}
|
|
291
246
|
apiUrl={apiUrl}
|
|
@@ -293,6 +248,36 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
293
248
|
welcomeGreeting={welcomeGreeting}
|
|
294
249
|
welcomeSubtitle={welcomeSubtitle}
|
|
295
250
|
welcomeSuggestions={welcomeSuggestions}
|
|
251
|
+
primaryColor={primaryColor}
|
|
252
|
+
isDarkMode={isDarkMode ?? isDark}
|
|
253
|
+
header={({ markState, hasMessages, clear }) => (
|
|
254
|
+
<View style={styles.header}>
|
|
255
|
+
<View style={styles.headerLeft}>
|
|
256
|
+
<AliaMark size={28} state={markState} />
|
|
257
|
+
<Text className="text-lg font-semibold text-foreground">
|
|
258
|
+
Alia
|
|
259
|
+
</Text>
|
|
260
|
+
</View>
|
|
261
|
+
<View style={styles.headerRight}>
|
|
262
|
+
{hasMessages && (
|
|
263
|
+
<TouchableOpacity onPress={clear} style={styles.clearButton}>
|
|
264
|
+
<Text className="text-sm text-muted-foreground">
|
|
265
|
+
Clear
|
|
266
|
+
</Text>
|
|
267
|
+
</TouchableOpacity>
|
|
268
|
+
)}
|
|
269
|
+
<TouchableOpacity
|
|
270
|
+
onPress={handleDismiss}
|
|
271
|
+
style={styles.closeButton}
|
|
272
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
273
|
+
>
|
|
274
|
+
<Text className="text-lg font-semibold text-muted-foreground">
|
|
275
|
+
{'\u2715'}
|
|
276
|
+
</Text>
|
|
277
|
+
</TouchableOpacity>
|
|
278
|
+
</View>
|
|
279
|
+
</View>
|
|
280
|
+
)}
|
|
296
281
|
/>
|
|
297
282
|
</GestureDetector>
|
|
298
283
|
</Animated.View>
|
|
@@ -324,7 +309,7 @@ const styles = StyleSheet.create({
|
|
|
324
309
|
web: {
|
|
325
310
|
marginHorizontal: 'auto',
|
|
326
311
|
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
|
327
|
-
} as
|
|
312
|
+
} as ViewStyle & { boxShadow?: string },
|
|
328
313
|
default: { elevation: 16 },
|
|
329
314
|
}),
|
|
330
315
|
},
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Pressable } from 'react-native';
|
|
3
|
+
import * as Haptics from 'expo-haptics';
|
|
4
|
+
import Svg, { Path } from 'react-native-svg';
|
|
5
|
+
import Animated, {
|
|
6
|
+
useSharedValue,
|
|
7
|
+
useAnimatedReaction,
|
|
8
|
+
useAnimatedStyle,
|
|
9
|
+
withTiming,
|
|
10
|
+
withRepeat,
|
|
11
|
+
withSequence,
|
|
12
|
+
cancelAnimation,
|
|
13
|
+
Easing,
|
|
14
|
+
} from 'react-native-reanimated';
|
|
15
|
+
|
|
16
|
+
export type AliaMarkState = 'idle' | 'thinking' | 'working' | 'writing';
|
|
17
|
+
|
|
18
|
+
export interface AliaMarkProps {
|
|
19
|
+
/** Width & height in px. Default 24. */
|
|
20
|
+
size?: number;
|
|
21
|
+
/** Animation state — spins while thinking/working, opacity-pulses while writing, still when idle. Default 'idle'. */
|
|
22
|
+
state?: AliaMarkState;
|
|
23
|
+
/** Layout classes for the outer box (color comes from `color`, not classes). */
|
|
24
|
+
className?: string;
|
|
25
|
+
/** Glyph fill. Defaults to the Alia brand purple — never a theme token. */
|
|
26
|
+
color?: string;
|
|
27
|
+
/** Called after the press flourish. The mark spins on press wherever it's used. */
|
|
28
|
+
onPress?: () => void;
|
|
29
|
+
/** Accessible name when the mark acts as a button. */
|
|
30
|
+
accessibilityLabel?: string;
|
|
31
|
+
/** Play the one-shot spin when the mark mounts (e.g. the sidebar rail collapsing in). */
|
|
32
|
+
spinOnMount?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Alia brand purple — the mark keeps its identity color across themes. */
|
|
36
|
+
const BRAND_FILL = '#d269e6';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Square viewBox centered on the glyph (exact curve bounds sampled from the
|
|
40
|
+
* path: x 140.6–1359.6, y 74.4–1425.0), so the mark renders margin-free at any
|
|
41
|
+
* size without distortion.
|
|
42
|
+
*/
|
|
43
|
+
const VIEWBOX = '74.8 74.4 1350.6 1350.6';
|
|
44
|
+
|
|
45
|
+
const GLYPH_PATH =
|
|
46
|
+
'M 728 76.199219 C 659.800781 84 596.601562 133.199219 558.800781 207.199219 C 539.800781 244.601562 527.199219 292.398438 524.601562 336.398438 L 523.398438 357.800781 L 506.199219 349 C 457.398438 324.199219 413.601562 313.398438 361 313.199219 C 314.601562 313 281.800781 320 247.199219 337.199219 C 194.398438 363.601562 161 404.601562 145.800781 461.199219 C 140.199219 482.199219 139.601562 527.398438 144.601562 550.199219 C 156.199219 601.800781 182 649.199219 220.601562 689.398438 C 239.800781 709.199219 271 734.601562 287.800781 744 C 292.199219 746.601562 296 749.199219 296 749.800781 C 296 750.398438 292.398438 753 288 755.601562 C 270.800781 765.398438 240 790.601562 221 810.398438 C 137.199219 897.398438 116.601562 1013 170.199219 1095.199219 C 200.199219 1141.199219 247.601562 1171.199219 309 1183.199219 C 333.601562 1188 383.800781 1188 410.800781 1183.199219 C 444 1177.199219 474.398438 1167 505.800781 1151.199219 L 524 1142 L 524 1153.601562 C 524 1180.398438 530.800781 1218.601562 541.199219 1250 C 582.800781 1376 690 1446.800781 796.800781 1419 C 846.398438 1406 893.800781 1369.800781 926 1320 C 953.601562 1277.199219 972.199219 1218.398438 975.398438 1163.601562 L 976.601562 1142.398438 L 994.398438 1151.398438 C 1044.800781 1176.601562 1087.800781 1187 1142 1186.800781 C 1192.601562 1186.601562 1229.601562 1177.199219 1269 1154.199219 C 1289.601562 1142.199219 1319.800781 1112.398438 1331.800781 1092.398438 C 1351.398438 1059.800781 1358.398438 1033.800781 1358.601562 994 C 1358.800781 953.199219 1353.199219 929.398438 1334 888.601562 C 1310.800781 839.601562 1265.398438 789 1217.601562 759.398438 C 1210.199219 754.800781 1204 750.601562 1204 750 C 1204 749.398438 1210.199219 745.199219 1217.601562 740.601562 C 1251.398438 719.601562 1291 681.199219 1313 648 C 1348.601562 594.601562 1364.601562 537 1358.199219 484.800781 C 1347.398438 397.800781 1285.398438 335.398438 1191 316.800781 C 1164.398438 311.398438 1114.199219 311.800781 1085 317.601562 C 1054.601562 323.601562 1029.398438 332 1000.601562 346 L 976 357.800781 L 976 346.398438 C 976 290 952.800781 217.800781 919.800781 171 C 881 116.398438 829 83.601562 769.199219 76 C 751.398438 73.800781 749.398438 73.800781 728 76.199219 Z';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Alia brand mark — the six-lobed flower glyph as an SVG (unselectable by
|
|
50
|
+
* nature). Effect-free reanimated: spin while thinking/using tools,
|
|
51
|
+
* opacity-pulse while streaming text, still when idle.
|
|
52
|
+
*/
|
|
53
|
+
export function AliaMark({
|
|
54
|
+
size = 24,
|
|
55
|
+
state = 'idle',
|
|
56
|
+
className,
|
|
57
|
+
color,
|
|
58
|
+
onPress,
|
|
59
|
+
accessibilityLabel,
|
|
60
|
+
spinOnMount = false,
|
|
61
|
+
}: AliaMarkProps) {
|
|
62
|
+
// Imperative animations via reanimated's reactive primitive (NOT a React effect):
|
|
63
|
+
// on this reanimated-web setup, animations returned from mappers/styles never
|
|
64
|
+
// tick, but `sharedValue.value = withRepeat(...)` does.
|
|
65
|
+
const rotation = useSharedValue(0);
|
|
66
|
+
useAnimatedReaction(
|
|
67
|
+
() => state === 'thinking' || state === 'working',
|
|
68
|
+
(spinning, previous) => {
|
|
69
|
+
if (spinning === previous) return;
|
|
70
|
+
if (spinning) {
|
|
71
|
+
rotation.value = 0;
|
|
72
|
+
rotation.value = withRepeat(withTiming(360, { duration: 1000, easing: Easing.linear }), -1);
|
|
73
|
+
} else {
|
|
74
|
+
cancelAnimation(rotation);
|
|
75
|
+
rotation.value = withTiming(0, { duration: 200 });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[state],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const opacity = useSharedValue(1);
|
|
82
|
+
useAnimatedReaction(
|
|
83
|
+
() => state === 'writing',
|
|
84
|
+
(pulsing, previous) => {
|
|
85
|
+
if (pulsing === previous) return;
|
|
86
|
+
if (pulsing) {
|
|
87
|
+
opacity.value = withRepeat(
|
|
88
|
+
withSequence(
|
|
89
|
+
withTiming(0.5, { duration: 1000, easing: Easing.bezier(0.4, 0, 0.6, 1) }),
|
|
90
|
+
withTiming(1, { duration: 1000, easing: Easing.bezier(0.4, 0, 0.6, 1) }),
|
|
91
|
+
),
|
|
92
|
+
-1,
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
cancelAnimation(opacity);
|
|
96
|
+
opacity.value = withTiming(1, { duration: 200 });
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
[state],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// One-shot press/mount flourish. Skipped while the state-driven spin runs so
|
|
103
|
+
// the two never fight over the shared rotation value.
|
|
104
|
+
const [spinCount, setSpinCount] = useState(spinOnMount ? 1 : 0);
|
|
105
|
+
useAnimatedReaction(
|
|
106
|
+
() => spinCount,
|
|
107
|
+
(current, previous) => {
|
|
108
|
+
if (current === previous || current === 0) return;
|
|
109
|
+
rotation.value = 0;
|
|
110
|
+
rotation.value = withTiming(360, { duration: 500, easing: Easing.bezier(0.34, 1.3, 0.64, 1) });
|
|
111
|
+
},
|
|
112
|
+
[spinCount],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
116
|
+
opacity: opacity.value,
|
|
117
|
+
transform: [{ rotate: `${rotation.value}deg` }],
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const handlePress = () => {
|
|
121
|
+
if (state !== 'thinking' && state !== 'working') {
|
|
122
|
+
setSpinCount((count) => count + 1);
|
|
123
|
+
}
|
|
124
|
+
// Best-effort flourish: rejects on platforms without haptics — not an error.
|
|
125
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
|
126
|
+
onPress?.();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Pressable
|
|
131
|
+
onPress={handlePress}
|
|
132
|
+
accessibilityRole={onPress ? 'button' : 'image'}
|
|
133
|
+
accessibilityLabel={accessibilityLabel}
|
|
134
|
+
className={className}
|
|
135
|
+
style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}
|
|
136
|
+
>
|
|
137
|
+
<Animated.View style={animatedStyle}>
|
|
138
|
+
<Svg width={size} height={size} viewBox={VIEWBOX}>
|
|
139
|
+
<Path d={GLYPH_PATH} fill={color ?? BRAND_FILL} />
|
|
140
|
+
</Svg>
|
|
141
|
+
</Animated.View>
|
|
142
|
+
</Pressable>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -2,7 +2,7 @@ import { Pressable, StyleSheet, View } from 'react-native';
|
|
|
2
2
|
import { BlurView } from 'expo-blur';
|
|
3
3
|
import { Text } from './ui/text';
|
|
4
4
|
import Animated, { FadeIn } from 'react-native-reanimated';
|
|
5
|
-
import {
|
|
5
|
+
import { AliaMark } from './AliaMark';
|
|
6
6
|
import type { WelcomeSuggestion } from '../types';
|
|
7
7
|
|
|
8
8
|
export type { WelcomeSuggestion };
|
|
@@ -12,7 +12,7 @@ export interface AliaWelcomeMessageProps {
|
|
|
12
12
|
subtitle?: string;
|
|
13
13
|
suggestions?: WelcomeSuggestion[];
|
|
14
14
|
onSuggestionPress?: (text: string) => void;
|
|
15
|
-
|
|
15
|
+
markSize?: number;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function AliaWelcomeMessage({
|
|
@@ -20,15 +20,15 @@ export function AliaWelcomeMessage({
|
|
|
20
20
|
subtitle,
|
|
21
21
|
suggestions = [],
|
|
22
22
|
onSuggestionPress,
|
|
23
|
-
|
|
23
|
+
markSize = 64,
|
|
24
24
|
}: AliaWelcomeMessageProps) {
|
|
25
25
|
return (
|
|
26
26
|
<View className="flex-1 items-center justify-center px-4">
|
|
27
27
|
<View className="w-full max-w-2xl">
|
|
28
|
-
{/*
|
|
28
|
+
{/* Mark + Title */}
|
|
29
29
|
<View className="items-start mb-8">
|
|
30
30
|
<View className="mb-4">
|
|
31
|
-
<
|
|
31
|
+
<AliaMark size={markSize} />
|
|
32
32
|
</View>
|
|
33
33
|
<View className="space-y-2">
|
|
34
34
|
<Text className="text-3xl font-bold tracking-tight text-foreground">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
1
|
+
import React, { useMemo, type ReactNode } from 'react';
|
|
2
2
|
import { View, Text, Platform, Linking, useColorScheme } from 'react-native';
|
|
3
|
-
import Markdown from 'react-native-markdown-display';
|
|
3
|
+
import Markdown, { type ASTNode } from 'react-native-markdown-display';
|
|
4
4
|
import type { AliaColors } from '../theme';
|
|
5
5
|
|
|
6
6
|
// Hardcoded fallback colors for standalone SDK usage (when no color override is passed).
|
|
@@ -29,7 +29,7 @@ function createRules(colors: AliaColors) {
|
|
|
29
29
|
const { text: textColor, muted, border, primary, mutedForeground } = colors;
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
|
-
heading1: (node:
|
|
32
|
+
heading1: (node: ASTNode, children: ReactNode[]) => (
|
|
33
33
|
<Text
|
|
34
34
|
key={node.key}
|
|
35
35
|
style={{ marginBottom: 8, marginTop: 12, fontSize: 18, fontWeight: '600', lineHeight: 24, color: textColor }}
|
|
@@ -37,7 +37,7 @@ function createRules(colors: AliaColors) {
|
|
|
37
37
|
{children}
|
|
38
38
|
</Text>
|
|
39
39
|
),
|
|
40
|
-
heading2: (node:
|
|
40
|
+
heading2: (node: ASTNode, children: ReactNode[]) => (
|
|
41
41
|
<Text
|
|
42
42
|
key={node.key}
|
|
43
43
|
style={{ marginBottom: 8, marginTop: 12, ...HEADING_TEXT, fontWeight: '600', color: textColor }}
|
|
@@ -45,7 +45,7 @@ function createRules(colors: AliaColors) {
|
|
|
45
45
|
{children}
|
|
46
46
|
</Text>
|
|
47
47
|
),
|
|
48
|
-
heading3: (node:
|
|
48
|
+
heading3: (node: ASTNode, children: ReactNode[]) => (
|
|
49
49
|
<Text
|
|
50
50
|
key={node.key}
|
|
51
51
|
style={{ marginBottom: 6, marginTop: 8, ...HEADING_TEXT, fontWeight: '600', color: textColor }}
|
|
@@ -53,7 +53,7 @@ function createRules(colors: AliaColors) {
|
|
|
53
53
|
{children}
|
|
54
54
|
</Text>
|
|
55
55
|
),
|
|
56
|
-
heading4: (node:
|
|
56
|
+
heading4: (node: ASTNode, children: ReactNode[]) => (
|
|
57
57
|
<Text
|
|
58
58
|
key={node.key}
|
|
59
59
|
style={{ marginBottom: 6, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
|
|
@@ -61,7 +61,7 @@ function createRules(colors: AliaColors) {
|
|
|
61
61
|
{children}
|
|
62
62
|
</Text>
|
|
63
63
|
),
|
|
64
|
-
heading5: (node:
|
|
64
|
+
heading5: (node: ASTNode, children: ReactNode[]) => (
|
|
65
65
|
<Text
|
|
66
66
|
key={node.key}
|
|
67
67
|
style={{ marginBottom: 4, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
|
|
@@ -69,7 +69,7 @@ function createRules(colors: AliaColors) {
|
|
|
69
69
|
{children}
|
|
70
70
|
</Text>
|
|
71
71
|
),
|
|
72
|
-
heading6: (node:
|
|
72
|
+
heading6: (node: ASTNode, children: ReactNode[]) => (
|
|
73
73
|
<Text
|
|
74
74
|
key={node.key}
|
|
75
75
|
style={{ marginBottom: 4, marginTop: 8, ...HEADING_TEXT, fontWeight: '500', color: textColor }}
|
|
@@ -77,7 +77,7 @@ function createRules(colors: AliaColors) {
|
|
|
77
77
|
{children}
|
|
78
78
|
</Text>
|
|
79
79
|
),
|
|
80
|
-
code: (node:
|
|
80
|
+
code: (node: ASTNode, children: ReactNode[], parent: ASTNode[]) => {
|
|
81
81
|
return parent.length > 1 ? (
|
|
82
82
|
<View
|
|
83
83
|
key={node.key}
|
|
@@ -109,7 +109,7 @@ function createRules(colors: AliaColors) {
|
|
|
109
109
|
</Text>
|
|
110
110
|
);
|
|
111
111
|
},
|
|
112
|
-
list_item: (node:
|
|
112
|
+
list_item: (node: ASTNode, children: ReactNode[], parent: ASTNode[]) => {
|
|
113
113
|
const isOrdered = parent[parent.length - 1]?.type === 'ordered_list';
|
|
114
114
|
const bullet = isOrdered
|
|
115
115
|
? `${parent[parent.length - 1].children.indexOf(node) + 1}.`
|
|
@@ -122,16 +122,16 @@ function createRules(colors: AliaColors) {
|
|
|
122
122
|
</View>
|
|
123
123
|
);
|
|
124
124
|
},
|
|
125
|
-
ordered_list: (node:
|
|
125
|
+
ordered_list: (node: ASTNode, children: ReactNode[]) => (
|
|
126
126
|
<View key={node.key} style={{ marginVertical: 8 }}>{children}</View>
|
|
127
127
|
),
|
|
128
|
-
unordered_list: (node:
|
|
128
|
+
unordered_list: (node: ASTNode, children: ReactNode[]) => (
|
|
129
129
|
<View key={node.key} style={{ marginVertical: 8 }}>{children}</View>
|
|
130
130
|
),
|
|
131
|
-
strong: (node:
|
|
131
|
+
strong: (node: ASTNode, children: ReactNode[]) => (
|
|
132
132
|
<Text key={node.key} style={{ fontWeight: '600', ...BODY_TEXT, color: textColor }}>{children}</Text>
|
|
133
133
|
),
|
|
134
|
-
link: (node:
|
|
134
|
+
link: (node: ASTNode, children: ReactNode[]) => (
|
|
135
135
|
<Text
|
|
136
136
|
key={node.key}
|
|
137
137
|
style={{ color: primary, textDecorationLine: 'underline', ...BODY_TEXT }}
|
|
@@ -142,10 +142,10 @@ function createRules(colors: AliaColors) {
|
|
|
142
142
|
{children}
|
|
143
143
|
</Text>
|
|
144
144
|
),
|
|
145
|
-
paragraph: (node:
|
|
145
|
+
paragraph: (node: ASTNode, children: ReactNode[]) => (
|
|
146
146
|
<Text key={node.key} style={{ marginBottom: 8, ...BODY_TEXT, color: textColor }}>{children}</Text>
|
|
147
147
|
),
|
|
148
|
-
blockquote: (node:
|
|
148
|
+
blockquote: (node: ASTNode, children: ReactNode[]) => (
|
|
149
149
|
<View
|
|
150
150
|
key={node.key}
|
|
151
151
|
style={{
|
|
@@ -162,7 +162,7 @@ function createRules(colors: AliaColors) {
|
|
|
162
162
|
{children}
|
|
163
163
|
</View>
|
|
164
164
|
),
|
|
165
|
-
hr: (node:
|
|
165
|
+
hr: (node: ASTNode) => (
|
|
166
166
|
<View
|
|
167
167
|
key={node.key}
|
|
168
168
|
style={{
|
|
@@ -172,7 +172,7 @@ function createRules(colors: AliaColors) {
|
|
|
172
172
|
}}
|
|
173
173
|
/>
|
|
174
174
|
),
|
|
175
|
-
body: (node:
|
|
175
|
+
body: (node: ASTNode, children: ReactNode[]) => {
|
|
176
176
|
return <View key={node.key} style={{ marginBottom: -8 }}>{children}</View>;
|
|
177
177
|
},
|
|
178
178
|
};
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { View, Text } from "react-native";
|
|
3
3
|
import Animated, {
|
|
4
|
-
useSharedValue,
|
|
5
4
|
useAnimatedStyle,
|
|
6
5
|
withRepeat,
|
|
7
6
|
withTiming,
|
|
8
7
|
withSequence,
|
|
9
|
-
Easing,
|
|
10
8
|
} from "react-native-reanimated";
|
|
9
|
+
import { AliaMark } from "./AliaMark";
|
|
11
10
|
|
|
12
11
|
const thinkingPhrases = [
|
|
13
12
|
"Thinking...",
|
|
@@ -44,33 +43,15 @@ export function ThinkingIndicator({ isWorking = false, statusText }: { isWorking
|
|
|
44
43
|
setPhraseIndex(Math.floor(Math.random() * phrases.length));
|
|
45
44
|
}, [isWorking]);
|
|
46
45
|
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
rotation.value = withRepeat(
|
|
51
|
-
withTiming(360, { duration: 2000, easing: Easing.linear }),
|
|
52
|
-
-1
|
|
53
|
-
);
|
|
54
|
-
}, [rotation]);
|
|
55
|
-
|
|
56
|
-
const spinStyle = useAnimatedStyle(() => ({
|
|
57
|
-
transform: [{ rotate: `${rotation.value}deg` }],
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
// Pulsing cursor animation
|
|
61
|
-
const cursorOpacity = useSharedValue(1);
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
cursorOpacity.value = withRepeat(
|
|
46
|
+
// Pulsing cursor animation (effect-free, declarative)
|
|
47
|
+
const cursorStyle = useAnimatedStyle(() => ({
|
|
48
|
+
opacity: withRepeat(
|
|
64
49
|
withSequence(
|
|
65
50
|
withTiming(0.2, { duration: 400 }),
|
|
66
51
|
withTiming(1, { duration: 400 })
|
|
67
52
|
),
|
|
68
53
|
-1
|
|
69
|
-
)
|
|
70
|
-
}, [cursorOpacity]);
|
|
71
|
-
|
|
72
|
-
const cursorStyle = useAnimatedStyle(() => ({
|
|
73
|
-
opacity: cursorOpacity.value,
|
|
54
|
+
),
|
|
74
55
|
}));
|
|
75
56
|
|
|
76
57
|
// Typewriter effect — skipped when statusText is provided (real-time status shown directly)
|
|
@@ -103,9 +84,7 @@ export function ThinkingIndicator({ isWorking = false, statusText }: { isWorking
|
|
|
103
84
|
|
|
104
85
|
return (
|
|
105
86
|
<View className="flex-row items-center gap-2 py-2">
|
|
106
|
-
<
|
|
107
|
-
<Text className="text-base text-muted-foreground">✱</Text>
|
|
108
|
-
</Animated.View>
|
|
87
|
+
<AliaMark size={16} state={isWorking ? 'working' : 'thinking'} className="text-muted-foreground" />
|
|
109
88
|
<View className="flex-row items-center">
|
|
110
89
|
<Text className="text-base text-muted-foreground">{shownText}</Text>
|
|
111
90
|
{(statusText || isTyping) && (
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type TextInputKeyPressEventData,
|
|
8
8
|
type NativeSyntheticEvent as RNSyntheticEvent,
|
|
9
9
|
type TextInputContentSizeChangeEventData,
|
|
10
|
+
type TextStyle,
|
|
10
11
|
} from "react-native";
|
|
11
12
|
import { cn } from "../../lib/utils";
|
|
12
13
|
|
|
@@ -144,12 +145,12 @@ const ChatTextInput = React.forwardRef<TextInput, ChatTextInputProps>(
|
|
|
144
145
|
scrollEnabled={fillContainer || props.multiline}
|
|
145
146
|
style={[
|
|
146
147
|
style,
|
|
147
|
-
!fillContainer && props.multiline && !disableAutoHeight && {
|
|
148
|
+
!fillContainer && props.multiline && !disableAutoHeight && ({
|
|
148
149
|
minHeight,
|
|
149
150
|
maxHeight,
|
|
150
|
-
overflow: 'auto'
|
|
151
|
+
overflow: 'auto',
|
|
151
152
|
...(Platform.OS === 'web' ? { fieldSizing: 'content' } : {}),
|
|
152
|
-
},
|
|
153
|
+
} as TextStyle & { overflow: 'auto'; fieldSizing?: string }),
|
|
153
154
|
fillContainer && { flex: 1, height: '100%' },
|
|
154
155
|
]}
|
|
155
156
|
{...props}
|