@alia.onl/sdk 3.1.2 → 3.1.4

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.
@@ -13,11 +13,11 @@
13
13
  * ```
14
14
  */
15
15
 
16
- import React, { useRef, useState, useEffect, useCallback } from '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, type AliaChatContentRef } from './AliaChatContent';
20
- import { AliaFace, type AliaExpression } from './AliaFace';
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, type AliaChatContentRef } from './AliaChatContent';
32
- import { AliaFace, type AliaExpression } from './AliaFace';
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
- {/* Header */}
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 any,
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 { AliaFace } from './AliaFace';
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
- faceSize?: number;
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
- faceSize = 64,
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
- {/* Face + Title */}
28
+ {/* Mark + Title */}
29
29
  <View className="items-start mb-8">
30
30
  <View className="mb-4">
31
- <AliaFace size={faceSize} expression="Greeting" />
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: any, children: any) => (
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: any, children: any) => (
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: any, children: any) => (
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: any, children: any) => (
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: any, children: any) => (
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: any, children: any) => (
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: any, children: any, parent: any) => {
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: any, children: any, parent: any) => {
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: any, children: any) => (
125
+ ordered_list: (node: ASTNode, children: ReactNode[]) => (
126
126
  <View key={node.key} style={{ marginVertical: 8 }}>{children}</View>
127
127
  ),
128
- unordered_list: (node: any, children: any) => (
128
+ unordered_list: (node: ASTNode, children: ReactNode[]) => (
129
129
  <View key={node.key} style={{ marginVertical: 8 }}>{children}</View>
130
130
  ),
131
- strong: (node: any, children: any) => (
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: any, children: any) => (
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: any, children: any) => (
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: any, children: any) => (
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: any) => (
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: any, children: any) => {
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
- // Spinning asterisk animation
48
- const rotation = useSharedValue(0);
49
- useEffect(() => {
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
- <Animated.View style={spinStyle}>
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' as any,
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}