@alia.onl/sdk 3.0.0 → 3.1.1
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/package.json +47 -19
- package/src/components/AliaChatContent.tsx +62 -19
- package/src/components/AliaChatMessageList.tsx +437 -280
- package/src/components/AliaChatScreen.tsx +21 -48
- package/src/components/AliaChatSheet.tsx +19 -31
- package/src/components/AliaFace.tsx +108 -58
- package/src/components/AliaWelcomeMessage.tsx +75 -0
- package/src/components/Markdown.tsx +94 -130
- package/src/components/PlanPreviewCard.tsx +76 -0
- package/src/components/Reasoning.tsx +1 -3
- package/src/components/ResearchProgressCard.tsx +158 -0
- package/src/components/ThinkingIndicator.tsx +6 -21
- package/src/components/voice/AudioWaveVisualizer.tsx +72 -22
- package/src/components/voice/VoiceControls.tsx +25 -42
- package/src/components/voice/VoiceOverlay.tsx +5 -1
- package/src/hooks/useAliaChat.ts +2 -1
- package/src/hooks/useSpeechToText.ts +21 -7
- package/src/hooks/useVoiceRoom.ts +24 -8
- package/src/index.ts +10 -3
- package/src/lib/content-utils.ts +23 -0
- package/src/lib/tool-registry.ts +55 -0
- package/src/theme.ts +2 -41
- package/src/types.ts +20 -7
- package/src/components/AliaChatInput.tsx +0 -294
- package/src/components/AliaChatSuggestions.tsx +0 -70
|
@@ -14,18 +14,15 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
17
|
-
import { View, Text, TouchableOpacity
|
|
17
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
18
18
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
19
19
|
import { AliaChatContent, type AliaChatContentRef } from './AliaChatContent';
|
|
20
20
|
import { AliaFace, type AliaExpression } from './AliaFace';
|
|
21
|
-
import {
|
|
22
|
-
import type { AliaChatSuggestion } from '../types';
|
|
21
|
+
import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
23
22
|
|
|
24
23
|
export interface AliaChatScreenProps {
|
|
25
24
|
/** App context injected into system prompt */
|
|
26
25
|
clientContext?: string;
|
|
27
|
-
/** Quick action suggestions shown when chat is empty */
|
|
28
|
-
suggestions?: AliaChatSuggestion[];
|
|
29
26
|
/** Alia model (default: 'alia-v1') */
|
|
30
27
|
model?: string;
|
|
31
28
|
/** API URL override */
|
|
@@ -34,17 +31,24 @@ export interface AliaChatScreenProps {
|
|
|
34
31
|
headerLeft?: React.ReactNode;
|
|
35
32
|
/** Optional header right action (e.g., settings button) */
|
|
36
33
|
headerRight?: React.ReactNode;
|
|
34
|
+
/** Welcome screen greeting */
|
|
35
|
+
welcomeGreeting?: string;
|
|
36
|
+
/** Welcome screen subtitle */
|
|
37
|
+
welcomeSubtitle?: string;
|
|
38
|
+
/** Welcome screen suggestions */
|
|
39
|
+
welcomeSuggestions?: WelcomeSuggestion[];
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export function AliaChatScreen({
|
|
40
43
|
clientContext,
|
|
41
|
-
suggestions = [],
|
|
42
44
|
model,
|
|
43
45
|
apiUrl,
|
|
44
46
|
headerLeft,
|
|
45
47
|
headerRight,
|
|
48
|
+
welcomeGreeting,
|
|
49
|
+
welcomeSubtitle,
|
|
50
|
+
welcomeSuggestions,
|
|
46
51
|
}: AliaChatScreenProps) {
|
|
47
|
-
const colors = useAliaColors();
|
|
48
52
|
const insets = useSafeAreaInsets();
|
|
49
53
|
const contentRef = useRef<AliaChatContentRef>(null);
|
|
50
54
|
|
|
@@ -67,18 +71,18 @@ export function AliaChatScreen({
|
|
|
67
71
|
}, []);
|
|
68
72
|
|
|
69
73
|
return (
|
|
70
|
-
<View style={
|
|
74
|
+
<View className="flex-1 bg-background" style={{ paddingTop: insets.top }}>
|
|
71
75
|
{/* Header */}
|
|
72
|
-
<View
|
|
73
|
-
<View
|
|
76
|
+
<View className="flex-row items-center justify-between px-4 py-3">
|
|
77
|
+
<View className="flex-row items-center gap-2.5">
|
|
74
78
|
{headerLeft}
|
|
75
79
|
<AliaFace size={28} expression={faceExpression} />
|
|
76
|
-
<Text
|
|
80
|
+
<Text className="text-lg font-semibold text-foreground">Alia</Text>
|
|
77
81
|
</View>
|
|
78
|
-
<View
|
|
82
|
+
<View className="flex-row items-center gap-2">
|
|
79
83
|
{hasMessages && (
|
|
80
|
-
<TouchableOpacity onPress={handleClear}
|
|
81
|
-
<Text
|
|
84
|
+
<TouchableOpacity onPress={handleClear} className="px-2.5 py-1.5">
|
|
85
|
+
<Text className="text-sm text-muted-foreground">Clear</Text>
|
|
82
86
|
</TouchableOpacity>
|
|
83
87
|
)}
|
|
84
88
|
{headerRight}
|
|
@@ -89,44 +93,13 @@ export function AliaChatScreen({
|
|
|
89
93
|
<AliaChatContent
|
|
90
94
|
ref={contentRef}
|
|
91
95
|
clientContext={clientContext}
|
|
92
|
-
suggestions={suggestions}
|
|
93
96
|
model={model}
|
|
94
97
|
apiUrl={apiUrl}
|
|
98
|
+
welcomeGreeting={welcomeGreeting}
|
|
99
|
+
welcomeSubtitle={welcomeSubtitle}
|
|
100
|
+
welcomeSuggestions={welcomeSuggestions}
|
|
95
101
|
/>
|
|
96
102
|
</View>
|
|
97
103
|
);
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
const styles = StyleSheet.create({
|
|
101
|
-
container: {
|
|
102
|
-
flex: 1,
|
|
103
|
-
},
|
|
104
|
-
header: {
|
|
105
|
-
flexDirection: 'row',
|
|
106
|
-
alignItems: 'center',
|
|
107
|
-
justifyContent: 'space-between',
|
|
108
|
-
paddingHorizontal: 16,
|
|
109
|
-
paddingVertical: 12,
|
|
110
|
-
},
|
|
111
|
-
headerLeft: {
|
|
112
|
-
flexDirection: 'row',
|
|
113
|
-
alignItems: 'center',
|
|
114
|
-
gap: 10,
|
|
115
|
-
},
|
|
116
|
-
headerTitle: {
|
|
117
|
-
fontSize: 18,
|
|
118
|
-
fontWeight: '600',
|
|
119
|
-
},
|
|
120
|
-
headerRight: {
|
|
121
|
-
flexDirection: 'row',
|
|
122
|
-
alignItems: 'center',
|
|
123
|
-
gap: 8,
|
|
124
|
-
},
|
|
125
|
-
clearButton: {
|
|
126
|
-
paddingHorizontal: 10,
|
|
127
|
-
paddingVertical: 6,
|
|
128
|
-
},
|
|
129
|
-
clearText: {
|
|
130
|
-
fontSize: 14,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
Pressable,
|
|
17
17
|
Dimensions,
|
|
18
18
|
Platform,
|
|
19
|
+
useColorScheme,
|
|
19
20
|
} from 'react-native';
|
|
20
21
|
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
21
22
|
import { useKeyboardHandler } from 'react-native-keyboard-controller';
|
|
@@ -29,8 +30,7 @@ import Animated, {
|
|
|
29
30
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
30
31
|
import { AliaChatContent, type AliaChatContentRef } from './AliaChatContent';
|
|
31
32
|
import { AliaFace, type AliaExpression } from './AliaFace';
|
|
32
|
-
import {
|
|
33
|
-
import type { AliaChatSuggestion } from '../types';
|
|
33
|
+
import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
34
34
|
|
|
35
35
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
36
36
|
|
|
@@ -43,12 +43,16 @@ const SPRING_CONFIG = {
|
|
|
43
43
|
export interface AliaChatSheetProps {
|
|
44
44
|
/** App context injected into system prompt */
|
|
45
45
|
clientContext?: string;
|
|
46
|
-
/** Quick action suggestions shown when chat is empty */
|
|
47
|
-
suggestions?: AliaChatSuggestion[];
|
|
48
46
|
/** Alia model (default: 'alia-v1') */
|
|
49
47
|
model?: string;
|
|
50
48
|
/** API URL override */
|
|
51
49
|
apiUrl?: string;
|
|
50
|
+
/** Welcome screen greeting */
|
|
51
|
+
welcomeGreeting?: string;
|
|
52
|
+
/** Welcome screen subtitle */
|
|
53
|
+
welcomeSubtitle?: string;
|
|
54
|
+
/** Welcome screen suggestions */
|
|
55
|
+
welcomeSuggestions?: WelcomeSuggestion[];
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
export interface AliaChatSheetRef {
|
|
@@ -57,9 +61,9 @@ export interface AliaChatSheetRef {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
60
|
-
({ clientContext,
|
|
61
|
-
const
|
|
62
|
-
const isDark =
|
|
64
|
+
({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions }, ref) => {
|
|
65
|
+
const scheme = useColorScheme();
|
|
66
|
+
const isDark = scheme === 'dark';
|
|
63
67
|
const insets = useSafeAreaInsets();
|
|
64
68
|
|
|
65
69
|
// Content ref for reading state
|
|
@@ -233,9 +237,9 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
233
237
|
{/* Sheet */}
|
|
234
238
|
<GestureDetector gesture={panGesture}>
|
|
235
239
|
<Animated.View
|
|
240
|
+
className="bg-background"
|
|
236
241
|
style={[
|
|
237
242
|
styles.sheet,
|
|
238
|
-
{ backgroundColor: colors.background },
|
|
239
243
|
sheetAnimStyle,
|
|
240
244
|
sheetMaxHeightStyle,
|
|
241
245
|
]}
|
|
@@ -254,19 +258,14 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
254
258
|
<View style={styles.header}>
|
|
255
259
|
<View style={styles.headerLeft}>
|
|
256
260
|
<AliaFace size={28} expression={faceExpression} />
|
|
257
|
-
<Text
|
|
261
|
+
<Text className="text-lg font-semibold text-foreground">
|
|
258
262
|
Alia
|
|
259
263
|
</Text>
|
|
260
264
|
</View>
|
|
261
265
|
<View style={styles.headerRight}>
|
|
262
266
|
{hasMessages && (
|
|
263
267
|
<TouchableOpacity onPress={handleClear} style={styles.clearButton}>
|
|
264
|
-
<Text
|
|
265
|
-
style={[
|
|
266
|
-
styles.clearText,
|
|
267
|
-
{ color: colors.secondaryText },
|
|
268
|
-
]}
|
|
269
|
-
>
|
|
268
|
+
<Text className="text-sm text-muted-foreground">
|
|
270
269
|
Clear
|
|
271
270
|
</Text>
|
|
272
271
|
</TouchableOpacity>
|
|
@@ -276,13 +275,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
276
275
|
style={styles.closeButton}
|
|
277
276
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
278
277
|
>
|
|
279
|
-
<Text
|
|
280
|
-
style={{
|
|
281
|
-
fontSize: 18,
|
|
282
|
-
fontWeight: '600',
|
|
283
|
-
color: colors.icon,
|
|
284
|
-
}}
|
|
285
|
-
>
|
|
278
|
+
<Text className="text-lg font-semibold text-muted-foreground">
|
|
286
279
|
{'\u2715'}
|
|
287
280
|
</Text>
|
|
288
281
|
</TouchableOpacity>
|
|
@@ -294,10 +287,12 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
294
287
|
<AliaChatContent
|
|
295
288
|
ref={contentRef}
|
|
296
289
|
clientContext={clientContext}
|
|
297
|
-
suggestions={suggestions}
|
|
298
290
|
model={model}
|
|
299
291
|
apiUrl={apiUrl}
|
|
300
292
|
scrollOffsetY={scrollOffsetY}
|
|
293
|
+
welcomeGreeting={welcomeGreeting}
|
|
294
|
+
welcomeSubtitle={welcomeSubtitle}
|
|
295
|
+
welcomeSuggestions={welcomeSuggestions}
|
|
301
296
|
/>
|
|
302
297
|
</GestureDetector>
|
|
303
298
|
</Animated.View>
|
|
@@ -312,7 +307,7 @@ AliaChatSheet.displayName = 'AliaChatSheet';
|
|
|
312
307
|
|
|
313
308
|
const styles = StyleSheet.create({
|
|
314
309
|
backdrop: {
|
|
315
|
-
...StyleSheet.
|
|
310
|
+
...StyleSheet.absoluteFill,
|
|
316
311
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
317
312
|
},
|
|
318
313
|
sheet: {
|
|
@@ -355,10 +350,6 @@ const styles = StyleSheet.create({
|
|
|
355
350
|
alignItems: 'center',
|
|
356
351
|
gap: 10,
|
|
357
352
|
},
|
|
358
|
-
headerTitle: {
|
|
359
|
-
fontSize: 18,
|
|
360
|
-
fontWeight: '600',
|
|
361
|
-
},
|
|
362
353
|
headerRight: {
|
|
363
354
|
flexDirection: 'row',
|
|
364
355
|
alignItems: 'center',
|
|
@@ -368,9 +359,6 @@ const styles = StyleSheet.create({
|
|
|
368
359
|
paddingHorizontal: 10,
|
|
369
360
|
paddingVertical: 6,
|
|
370
361
|
},
|
|
371
|
-
clearText: {
|
|
372
|
-
fontSize: 14,
|
|
373
|
-
},
|
|
374
362
|
closeButton: {
|
|
375
363
|
width: 32,
|
|
376
364
|
height: 32,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useEffect } from "react";
|
|
2
|
-
import { View, Text } from "react-native";
|
|
1
|
+
import React, { useEffect, useMemo } from "react";
|
|
2
|
+
import { View, Text, Image, ImageSourcePropType } from "react-native";
|
|
3
3
|
import Svg, { Path, Ellipse } from "react-native-svg";
|
|
4
4
|
import Animated, {
|
|
5
5
|
useSharedValue,
|
|
@@ -12,7 +12,6 @@ import Animated, {
|
|
|
12
12
|
cancelAnimation,
|
|
13
13
|
Easing,
|
|
14
14
|
} from "react-native-reanimated";
|
|
15
|
-
import { useAliaColors } from "../theme";
|
|
16
15
|
|
|
17
16
|
// ─── Animated SVG wrappers ──────────────────────────────────────────────────
|
|
18
17
|
|
|
@@ -33,10 +32,19 @@ export type AliaExpression =
|
|
|
33
32
|
| "Searching F"
|
|
34
33
|
| "Looking Down";
|
|
35
34
|
|
|
35
|
+
export interface AliaAccessory {
|
|
36
|
+
layer: 'front' | 'behind';
|
|
37
|
+
image: ImageSourcePropType;
|
|
38
|
+
/** Optional position overrides (ratios 0-1, scale multiplier, rotation in degrees) */
|
|
39
|
+
position?: { x: number; y: number; scale: number; rotation: number };
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
export interface AliaFaceProps {
|
|
37
43
|
expression?: AliaExpression;
|
|
38
44
|
size?: number;
|
|
39
45
|
showLabel?: boolean;
|
|
46
|
+
/** Accessory overlays rendered on/around the face circle */
|
|
47
|
+
accessories?: AliaAccessory[];
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
// ─── Expression data (numeric decomposition of SVG paths) ───────────────────
|
|
@@ -129,11 +137,9 @@ export function AliaFace({
|
|
|
129
137
|
expression = DEFAULT_EXPRESSION,
|
|
130
138
|
size = 120,
|
|
131
139
|
showLabel = false,
|
|
140
|
+
accessories,
|
|
132
141
|
}: AliaFaceProps) {
|
|
133
|
-
const
|
|
134
|
-
const strokeColor = colors.isDark ? "#ffffff" : "#000000";
|
|
135
|
-
const circleBg = colors.background;
|
|
136
|
-
const circleBorder = colors.border;
|
|
142
|
+
const strokeColor = 'currentColor';
|
|
137
143
|
|
|
138
144
|
const initial = EXPRESSIONS[DEFAULT_EXPRESSION];
|
|
139
145
|
|
|
@@ -336,64 +342,108 @@ export function AliaFace({
|
|
|
336
342
|
transform: [{ translateY: breatheY.value }],
|
|
337
343
|
}));
|
|
338
344
|
|
|
345
|
+
// ── Accessories ──
|
|
346
|
+
|
|
347
|
+
const { behind, front } = useMemo(() => {
|
|
348
|
+
if (!accessories?.length) return { behind: [] as AliaAccessory[], front: [] as AliaAccessory[] };
|
|
349
|
+
const b: AliaAccessory[] = [];
|
|
350
|
+
const f: AliaAccessory[] = [];
|
|
351
|
+
for (const acc of accessories) {
|
|
352
|
+
if (acc.layer === 'behind') b.push(acc);
|
|
353
|
+
else f.push(acc);
|
|
354
|
+
}
|
|
355
|
+
return { behind: b, front: f };
|
|
356
|
+
}, [accessories]);
|
|
357
|
+
|
|
358
|
+
const accessoryStyle = useMemo(
|
|
359
|
+
() => ({ position: 'absolute' as const, width: size, height: size, top: 0, left: 0 }),
|
|
360
|
+
[size],
|
|
361
|
+
);
|
|
362
|
+
|
|
339
363
|
// ── Render ──
|
|
340
364
|
|
|
341
365
|
return (
|
|
342
366
|
<View style={{ alignItems: "center" }}>
|
|
343
|
-
<View
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
367
|
+
<View style={{ width: size, height: size, overflow: 'visible' as const }}>
|
|
368
|
+
{/* Behind-layer accessories (e.g. headphones) */}
|
|
369
|
+
{behind.map((acc, i) => (
|
|
370
|
+
<Image key={`behind-${i}`} source={acc.image} style={acc.position ? {
|
|
371
|
+
position: 'absolute',
|
|
372
|
+
width: size * acc.position.scale,
|
|
373
|
+
height: size * acc.position.scale,
|
|
374
|
+
top: (acc.position.y - 0.5) * size + (size - size * acc.position.scale) / 2,
|
|
375
|
+
left: (acc.position.x - 0.5) * size + (size - size * acc.position.scale) / 2,
|
|
376
|
+
transform: [{ rotate: `${acc.position.rotation}deg` }],
|
|
377
|
+
} : accessoryStyle} />
|
|
378
|
+
))}
|
|
379
|
+
|
|
380
|
+
{/* Face circle */}
|
|
381
|
+
<View
|
|
382
|
+
className="bg-background border-primary/20 text-foreground"
|
|
383
|
+
style={{
|
|
384
|
+
width: size,
|
|
385
|
+
height: size,
|
|
386
|
+
borderRadius: size / 2,
|
|
387
|
+
alignItems: "center",
|
|
388
|
+
justifyContent: "center",
|
|
389
|
+
borderWidth: 1,
|
|
390
|
+
}}
|
|
391
|
+
>
|
|
392
|
+
<Animated.View style={breatheStyle}>
|
|
393
|
+
<Svg
|
|
394
|
+
width={size * 0.95}
|
|
395
|
+
height={size * 0.95}
|
|
396
|
+
viewBox="35 35 250 250"
|
|
397
|
+
>
|
|
398
|
+
{/* Left brow */}
|
|
399
|
+
<AnimatedPath
|
|
400
|
+
animatedProps={leftBrowProps}
|
|
401
|
+
stroke={strokeColor}
|
|
402
|
+
strokeWidth={15}
|
|
403
|
+
strokeLinecap="round"
|
|
404
|
+
fill="none"
|
|
405
|
+
/>
|
|
406
|
+
|
|
407
|
+
{/* Left eye */}
|
|
408
|
+
<AnimatedEllipse
|
|
409
|
+
animatedProps={leftEyeProps}
|
|
410
|
+
fill={strokeColor}
|
|
411
|
+
/>
|
|
412
|
+
|
|
413
|
+
{/* Right eye */}
|
|
414
|
+
<AnimatedEllipse
|
|
415
|
+
animatedProps={rightEyeProps}
|
|
416
|
+
fill={strokeColor}
|
|
417
|
+
/>
|
|
418
|
+
|
|
419
|
+
{/* Nose + right brow */}
|
|
420
|
+
<AnimatedPath
|
|
421
|
+
animatedProps={noseBrowProps}
|
|
422
|
+
stroke={strokeColor}
|
|
423
|
+
strokeWidth={15}
|
|
424
|
+
strokeLinecap="round"
|
|
425
|
+
strokeLinejoin="round"
|
|
426
|
+
fill="none"
|
|
427
|
+
/>
|
|
428
|
+
</Svg>
|
|
429
|
+
</Animated.View>
|
|
430
|
+
</View>
|
|
431
|
+
|
|
432
|
+
{/* Front-layer accessories (e.g. hat, glasses, tie) */}
|
|
433
|
+
{front.map((acc, i) => (
|
|
434
|
+
<Image key={`front-${i}`} source={acc.image} style={acc.position ? {
|
|
435
|
+
position: 'absolute',
|
|
436
|
+
width: size * acc.position.scale,
|
|
437
|
+
height: size * acc.position.scale,
|
|
438
|
+
top: (acc.position.y - 0.5) * size + (size - size * acc.position.scale) / 2,
|
|
439
|
+
left: (acc.position.x - 0.5) * size + (size - size * acc.position.scale) / 2,
|
|
440
|
+
transform: [{ rotate: `${acc.position.rotation}deg` }],
|
|
441
|
+
} : accessoryStyle} />
|
|
442
|
+
))}
|
|
393
443
|
</View>
|
|
394
444
|
|
|
395
445
|
{showLabel && (
|
|
396
|
-
<Text style={{ fontSize: 10,
|
|
446
|
+
<Text className="text-muted-foreground" style={{ fontSize: 10, marginTop: 8, textTransform: 'uppercase', letterSpacing: 3, fontWeight: '700' }}>
|
|
397
447
|
{expression}
|
|
398
448
|
</Text>
|
|
399
449
|
)}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
2
|
+
import { BlurView } from 'expo-blur';
|
|
3
|
+
import { Text } from './ui/text';
|
|
4
|
+
import Animated, { FadeIn } from 'react-native-reanimated';
|
|
5
|
+
import { AliaFace } from './AliaFace';
|
|
6
|
+
import type { WelcomeSuggestion } from '../types';
|
|
7
|
+
|
|
8
|
+
export type { WelcomeSuggestion };
|
|
9
|
+
|
|
10
|
+
export interface AliaWelcomeMessageProps {
|
|
11
|
+
greeting: string;
|
|
12
|
+
subtitle?: string;
|
|
13
|
+
suggestions?: WelcomeSuggestion[];
|
|
14
|
+
onSuggestionPress?: (text: string) => void;
|
|
15
|
+
faceSize?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function AliaWelcomeMessage({
|
|
19
|
+
greeting,
|
|
20
|
+
subtitle,
|
|
21
|
+
suggestions = [],
|
|
22
|
+
onSuggestionPress,
|
|
23
|
+
faceSize = 64,
|
|
24
|
+
}: AliaWelcomeMessageProps) {
|
|
25
|
+
return (
|
|
26
|
+
<View className="flex-1 items-center justify-center px-4">
|
|
27
|
+
<View className="w-full max-w-2xl">
|
|
28
|
+
{/* Face + Title */}
|
|
29
|
+
<View className="items-start mb-8">
|
|
30
|
+
<View className="mb-4">
|
|
31
|
+
<AliaFace size={faceSize} expression="Greeting" />
|
|
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>
|
|
43
|
+
</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
|
+
)}
|
|
72
|
+
</View>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|