@alia.onl/sdk 1.0.0 → 3.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 (44) hide show
  1. package/package.json +37 -3
  2. package/src/components/AliaChatContent.tsx +313 -0
  3. package/src/components/AliaChatInput.tsx +174 -27
  4. package/src/components/AliaChatMessageList.tsx +220 -34
  5. package/src/components/AliaChatScreen.tsx +132 -0
  6. package/src/components/AliaChatSheet.tsx +36 -49
  7. package/src/components/AliaChatSuggestions.tsx +1 -1
  8. package/src/components/AliaFace.tsx +402 -0
  9. package/src/components/Markdown.tsx +288 -0
  10. package/src/components/Reasoning.tsx +154 -0
  11. package/src/components/ThinkingIndicator.tsx +134 -0
  12. package/src/components/ui/button.tsx +104 -0
  13. package/src/components/ui/chat-text-input.tsx +164 -0
  14. package/src/components/ui/dropdown-menu.tsx +2 -0
  15. package/src/components/ui/dropdown-menu.web.tsx +355 -0
  16. package/src/components/ui/prompt-input/actions.tsx +28 -0
  17. package/src/components/ui/prompt-input/add-menu.tsx +83 -0
  18. package/src/components/ui/prompt-input/attachments.tsx +183 -0
  19. package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
  20. package/src/components/ui/prompt-input/context.tsx +71 -0
  21. package/src/components/ui/prompt-input/mic-button.tsx +53 -0
  22. package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
  23. package/src/components/ui/prompt-input/submit-button.tsx +49 -0
  24. package/src/components/ui/prompt-input/textarea.tsx +64 -0
  25. package/src/components/ui/text.tsx +29 -0
  26. package/src/components/voice/AudioWaveVisualizer.tsx +377 -0
  27. package/src/components/voice/VoiceControls.tsx +180 -0
  28. package/src/components/voice/VoiceOverlay.tsx +43 -0
  29. package/src/hooks/useAliaChat.ts +278 -91
  30. package/src/hooks/useAudioLevelMonitor.ts +142 -0
  31. package/src/hooks/useAudioLevels.ts +51 -0
  32. package/src/hooks/useDocumentPicker.ts +37 -0
  33. package/src/hooks/useImagePicker.ts +38 -0
  34. package/src/hooks/useSoundEffects.ts +128 -0
  35. package/src/hooks/useSpeechToText.ts +182 -0
  36. package/src/hooks/useTTS.ts +233 -0
  37. package/src/hooks/useVoiceRoom.ts +420 -0
  38. package/src/index.ts +71 -6
  39. package/src/lib/keyboard.native.tsx +6 -0
  40. package/src/lib/keyboard.tsx +25 -0
  41. package/src/lib/utils.ts +14 -0
  42. package/src/optional-deps.d.ts +86 -0
  43. package/src/theme.ts +25 -2
  44. package/src/types.ts +63 -1
@@ -1,52 +1,95 @@
1
- import React, { useRef, useEffect } from 'react';
1
+ import React, { useRef, useEffect, useState, useCallback } from 'react';
2
2
  import {
3
3
  View,
4
4
  Text,
5
+ Pressable,
5
6
  StyleSheet,
6
7
  Platform,
7
8
  } from 'react-native';
8
9
  import { BlurView } from 'expo-blur';
9
- import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated';
10
+ import Animated, {
11
+ useAnimatedScrollHandler,
12
+ useSharedValue,
13
+ useAnimatedStyle,
14
+ withRepeat,
15
+ withTiming,
16
+ withSequence,
17
+ cancelAnimation,
18
+ Easing,
19
+ FadeInUp,
20
+ type SharedValue,
21
+ } from 'react-native-reanimated';
10
22
  import { useAliaColors } from '../theme';
23
+ import { AliaMarkdown } from './Markdown';
24
+ import { ThinkingIndicator } from './ThinkingIndicator';
25
+ import { Reasoning, ReasoningTrigger, ReasoningContent } from './Reasoning';
11
26
  import type { ChatMessage, ToolInvocation } from '../types';
12
27
 
13
- interface AliaChatMessageListProps {
28
+ const ENTER_ANIMATION = FadeInUp.duration(200);
29
+
30
+ type PlaybackState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
31
+
32
+ export interface AliaChatMessageListProps {
14
33
  messages: ChatMessage[];
15
34
  isStreaming: boolean;
16
- scrollOffsetY: Animated.SharedValue<number>;
35
+ scrollOffsetY: SharedValue<number>;
36
+ onReadAloud?: (messageId: string, text: string) => void;
37
+ ttsActiveMessageId?: string | null;
38
+ ttsPlaybackState?: PlaybackState;
17
39
  }
18
40
 
19
- function ToolStatus({ invocation }: { invocation: ToolInvocation }) {
41
+ // ── Tool Bullet ──
42
+
43
+ function ToolBullet({ invocation }: { invocation: ToolInvocation }) {
20
44
  const colors = useAliaColors();
21
45
 
22
- // Clean up tool name for display: "oxy_inbox__searchEmails" → "Searching emails"
23
46
  const displayName = invocation.toolName
24
47
  .replace(/^oxy_\w+__/, '')
25
48
  .replace(/([A-Z])/g, ' $1')
26
49
  .replace(/^./, (s) => s.toUpperCase())
27
50
  .trim();
28
51
 
29
- const label =
30
- invocation.state === 'call'
31
- ? `${displayName}...`
32
- : displayName;
52
+ const isActive = invocation.state === 'call' || invocation.state === 'partial-call';
53
+ const label = isActive ? `${displayName}...` : displayName;
33
54
 
34
55
  return (
35
56
  <View style={styles.toolStatus}>
36
- {invocation.state === 'call' && (
37
- <View style={[styles.toolDot, { backgroundColor: colors.tint }]} />
38
- )}
39
- {invocation.state === 'result' && (
40
- <Text style={[styles.toolCheckmark, { color: colors.tint }]}>{'✓'}</Text>
57
+ {isActive ? (
58
+ <PulsingDot color={colors.tint} />
59
+ ) : (
60
+ <Text style={[styles.toolCheckmark, { color: colors.tint }]}>{'\u2713'}</Text>
41
61
  )}
42
62
  <Text style={[styles.toolLabel, { color: colors.secondaryText }]}>{label}</Text>
43
63
  </View>
44
64
  );
45
65
  }
46
66
 
67
+ function PulsingDot({ color }: { color: string }) {
68
+ const opacity = useSharedValue(1);
69
+
70
+ useEffect(() => {
71
+ opacity.value = withRepeat(
72
+ withSequence(
73
+ withTiming(0.3, { duration: 600, easing: Easing.inOut(Easing.sin) }),
74
+ withTiming(1, { duration: 600, easing: Easing.inOut(Easing.sin) }),
75
+ ),
76
+ -1,
77
+ );
78
+ return () => cancelAnimation(opacity);
79
+ }, []);
80
+
81
+ const animStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
82
+
83
+ return (
84
+ <Animated.View style={[styles.toolDot, { backgroundColor: color }, animStyle]} />
85
+ );
86
+ }
87
+
88
+ // ── User Bubble ──
89
+
47
90
  function UserBubble({ content }: { content: string }) {
48
91
  const colors = useAliaColors();
49
- const isDark = colors.background === '#000000';
92
+ const isDark = colors.isDark;
50
93
 
51
94
  return (
52
95
  <View style={styles.userMessage}>
@@ -62,38 +105,166 @@ function UserBubble({ content }: { content: string }) {
62
105
  );
63
106
  }
64
107
 
108
+ // ── Message Actions ──
109
+
110
+ function MessageActions({
111
+ message,
112
+ onReadAloud,
113
+ ttsActiveMessageId,
114
+ ttsPlaybackState,
115
+ }: {
116
+ message: ChatMessage;
117
+ onReadAloud?: (messageId: string, text: string) => void;
118
+ ttsActiveMessageId?: string | null;
119
+ ttsPlaybackState?: PlaybackState;
120
+ }) {
121
+ const colors = useAliaColors();
122
+ const [copied, setCopied] = useState(false);
123
+
124
+ const handleCopy = useCallback(async () => {
125
+ try {
126
+ if (Platform.OS === 'web') {
127
+ await navigator.clipboard.writeText(message.content);
128
+ } else {
129
+ const Clipboard = await import('expo-clipboard');
130
+ await Clipboard.setStringAsync(message.content);
131
+ }
132
+ setCopied(true);
133
+ setTimeout(() => setCopied(false), 2000);
134
+ } catch {}
135
+ }, [message.content]);
136
+
137
+ const isThisPlaying = ttsActiveMessageId === message.id && ttsPlaybackState === 'playing';
138
+ const isThisLoading = ttsActiveMessageId === message.id && ttsPlaybackState === 'loading';
139
+
140
+ if (!message.content) return null;
141
+
142
+ return (
143
+ <View style={styles.actionsRow}>
144
+ {/* Copy */}
145
+ <Pressable onPress={handleCopy} style={styles.actionBtn} hitSlop={8}>
146
+ {copied ? (
147
+ <Text style={[styles.actionIcon, { color: colors.tint }]}>{'\u2713'}</Text>
148
+ ) : (
149
+ <CopyIcon color={colors.secondaryText} />
150
+ )}
151
+ </Pressable>
152
+
153
+ {/* Read aloud */}
154
+ {onReadAloud && (
155
+ <Pressable
156
+ onPress={() => onReadAloud(message.id, message.content)}
157
+ style={styles.actionBtn}
158
+ hitSlop={8}
159
+ >
160
+ {isThisLoading ? (
161
+ <Text style={[styles.actionIcon, { color: colors.secondaryText }]}>{'\u2026'}</Text>
162
+ ) : (
163
+ <VolumeIcon color={isThisPlaying ? colors.tint : colors.secondaryText} />
164
+ )}
165
+ </Pressable>
166
+ )}
167
+ </View>
168
+ );
169
+ }
170
+
171
+ function CopyIcon({ color }: { color: string }) {
172
+ if (Platform.OS === 'web') {
173
+ return (
174
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
175
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
176
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
177
+ </svg>
178
+ );
179
+ }
180
+ return <Text style={[styles.actionIcon, { color }]}>{'\u2398'}</Text>;
181
+ }
182
+
183
+ function VolumeIcon({ color }: { color: string }) {
184
+ if (Platform.OS === 'web') {
185
+ return (
186
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
187
+ <path d="M11 5L6 9H2v6h4l5 4V5z" />
188
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
189
+ </svg>
190
+ );
191
+ }
192
+ return <Text style={[styles.actionIcon, { color }]}>{'\u{1F50A}'}</Text>;
193
+ }
194
+
195
+ // ── Assistant Message ──
196
+
65
197
  function AssistantMessage({
66
198
  message,
67
- isFirst,
68
199
  isStreamingThis,
200
+ onReadAloud,
201
+ ttsActiveMessageId,
202
+ ttsPlaybackState,
69
203
  }: {
70
204
  message: ChatMessage;
71
- isFirst: boolean;
72
205
  isStreamingThis: boolean;
206
+ onReadAloud?: (messageId: string, text: string) => void;
207
+ ttsActiveMessageId?: string | null;
208
+ ttsPlaybackState?: PlaybackState;
73
209
  }) {
74
210
  const colors = useAliaColors();
211
+ const hasContent = !!message.content;
212
+ const hasThinking = !!message.thinking;
213
+ const showThinkingIndicator = isStreamingThis && !hasContent && !message.toolInvocations?.length && !hasThinking;
75
214
 
76
215
  return (
77
- <View style={styles.assistantMessage}>
216
+ <Animated.View
217
+ entering={ENTER_ANIMATION}
218
+ style={styles.assistantMessage}
219
+ >
220
+ {/* Extended thinking / reasoning */}
221
+ {hasThinking && (
222
+ <Reasoning isStreaming={isStreamingThis && !hasContent}>
223
+ <ReasoningTrigger />
224
+ <ReasoningContent>{message.thinking!}</ReasoningContent>
225
+ </Reasoning>
226
+ )}
227
+
78
228
  {/* Tool invocations */}
79
229
  {message.toolInvocations?.map((tool, i) => (
80
- <ToolStatus key={`${tool.toolName}-${i}`} invocation={tool} />
230
+ <ToolBullet key={`${tool.toolCallId || tool.toolName}-${i}`} invocation={tool} />
81
231
  ))}
82
232
 
83
- {/* Text content */}
84
- {(message.content || isStreamingThis) && (
85
- <Text style={[styles.assistantText, { color: colors.text }]}>
86
- {message.content || (isStreamingThis ? '\u2758' : '')}
87
- </Text>
233
+ {/* Thinking indicator (no content yet) */}
234
+ {showThinkingIndicator && <ThinkingIndicator />}
235
+
236
+ {/* Text content — rendered as markdown */}
237
+ {hasContent && (
238
+ <AliaMarkdown content={message.content} />
88
239
  )}
89
- </View>
240
+
241
+ {/* Streaming cursor */}
242
+ {isStreamingThis && hasContent && (
243
+ <Text style={[styles.cursor, { color: colors.secondaryText }]}>{'\u2758'}</Text>
244
+ )}
245
+
246
+ {/* Message actions (copy, read aloud) — shown when not streaming */}
247
+ {!isStreamingThis && hasContent && (
248
+ <MessageActions
249
+ message={message}
250
+ onReadAloud={onReadAloud}
251
+ ttsActiveMessageId={ttsActiveMessageId}
252
+ ttsPlaybackState={ttsPlaybackState}
253
+ />
254
+ )}
255
+ </Animated.View>
90
256
  );
91
257
  }
92
258
 
259
+ // ── List ──
260
+
93
261
  export function AliaChatMessageList({
94
262
  messages,
95
263
  isStreaming,
96
264
  scrollOffsetY,
265
+ onReadAloud,
266
+ ttsActiveMessageId,
267
+ ttsPlaybackState,
97
268
  }: AliaChatMessageListProps) {
98
269
  const scrollRef = useRef<Animated.ScrollView>(null);
99
270
 
@@ -130,15 +301,16 @@ export function AliaChatMessageList({
130
301
  return <UserBubble key={msg.id} content={msg.content} />;
131
302
  }
132
303
 
133
- const isFirstInGroup = i === 0 || arr[i - 1]?.role === 'user';
134
304
  const isStreamingThis = isStreaming && i === arr.length - 1;
135
305
 
136
306
  return (
137
307
  <AssistantMessage
138
308
  key={msg.id}
139
309
  message={msg}
140
- isFirst={isFirstInGroup}
141
310
  isStreamingThis={isStreamingThis}
311
+ onReadAloud={onReadAloud}
312
+ ttsActiveMessageId={ttsActiveMessageId}
313
+ ttsPlaybackState={ttsPlaybackState}
142
314
  />
143
315
  );
144
316
  })}
@@ -179,9 +351,9 @@ const styles = StyleSheet.create({
179
351
  alignItems: 'flex-start',
180
352
  marginBottom: 12,
181
353
  },
182
- assistantText: {
354
+ cursor: {
183
355
  fontSize: 15,
184
- lineHeight: 24,
356
+ marginTop: -4,
185
357
  },
186
358
 
187
359
  // Tool status
@@ -196,10 +368,6 @@ const styles = StyleSheet.create({
196
368
  width: 8,
197
369
  height: 8,
198
370
  borderRadius: 4,
199
- ...Platform.select({
200
- web: {} as any,
201
- default: {},
202
- }),
203
371
  },
204
372
  toolCheckmark: {
205
373
  fontSize: 14,
@@ -208,4 +376,22 @@ const styles = StyleSheet.create({
208
376
  toolLabel: {
209
377
  fontSize: 13,
210
378
  },
379
+
380
+ // Message actions
381
+ actionsRow: {
382
+ flexDirection: 'row',
383
+ alignItems: 'center',
384
+ gap: 12,
385
+ marginTop: 6,
386
+ opacity: 0.7,
387
+ },
388
+ actionBtn: {
389
+ width: 28,
390
+ height: 28,
391
+ alignItems: 'center',
392
+ justifyContent: 'center',
393
+ },
394
+ actionIcon: {
395
+ fontSize: 14,
396
+ },
211
397
  });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Full-screen Alia chat component for use as a route/screen in Oxy ecosystem apps.
3
+ *
4
+ * Usage:
5
+ * ```tsx
6
+ * import { AliaChatScreen } from '@alia.onl/sdk';
7
+ *
8
+ * <AliaChatScreen
9
+ * clientContext="Notes app — user is viewing note #42"
10
+ * suggestions={[{ label: 'Summarize', prompt: 'Summarize this note' }]}
11
+ * headerLeft={<BackButton />}
12
+ * />
13
+ * ```
14
+ */
15
+
16
+ import React, { useRef, useState, useEffect, useCallback } from 'react';
17
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
18
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
19
+ import { AliaChatContent, type AliaChatContentRef } from './AliaChatContent';
20
+ import { AliaFace, type AliaExpression } from './AliaFace';
21
+ import { useAliaColors } from '../theme';
22
+ import type { AliaChatSuggestion } from '../types';
23
+
24
+ export interface AliaChatScreenProps {
25
+ /** App context injected into system prompt */
26
+ clientContext?: string;
27
+ /** Quick action suggestions shown when chat is empty */
28
+ suggestions?: AliaChatSuggestion[];
29
+ /** Alia model (default: 'alia-v1') */
30
+ model?: string;
31
+ /** API URL override */
32
+ apiUrl?: string;
33
+ /** Optional header left action (e.g., back button) */
34
+ headerLeft?: React.ReactNode;
35
+ /** Optional header right action (e.g., settings button) */
36
+ headerRight?: React.ReactNode;
37
+ }
38
+
39
+ export function AliaChatScreen({
40
+ clientContext,
41
+ suggestions = [],
42
+ model,
43
+ apiUrl,
44
+ headerLeft,
45
+ headerRight,
46
+ }: AliaChatScreenProps) {
47
+ const colors = useAliaColors();
48
+ const insets = useSafeAreaInsets();
49
+ const contentRef = useRef<AliaChatContentRef>(null);
50
+
51
+ const [faceExpression, setFaceExpression] = useState<AliaExpression>('Idle A');
52
+ const [hasMessages, setHasMessages] = useState(false);
53
+
54
+ // Poll content ref for header state
55
+ useEffect(() => {
56
+ const interval = setInterval(() => {
57
+ if (contentRef.current) {
58
+ setFaceExpression(contentRef.current.faceExpression);
59
+ setHasMessages(contentRef.current.messages.length > 0);
60
+ }
61
+ }, 200);
62
+ return () => clearInterval(interval);
63
+ }, []);
64
+
65
+ const handleClear = useCallback(() => {
66
+ contentRef.current?.clear();
67
+ }, []);
68
+
69
+ return (
70
+ <View style={[styles.container, { backgroundColor: colors.background, paddingTop: insets.top }]}>
71
+ {/* Header */}
72
+ <View style={styles.header}>
73
+ <View style={styles.headerLeft}>
74
+ {headerLeft}
75
+ <AliaFace size={28} expression={faceExpression} />
76
+ <Text style={[styles.headerTitle, { color: colors.text }]}>Alia</Text>
77
+ </View>
78
+ <View style={styles.headerRight}>
79
+ {hasMessages && (
80
+ <TouchableOpacity onPress={handleClear} style={styles.clearButton}>
81
+ <Text style={[styles.clearText, { color: colors.secondaryText }]}>Clear</Text>
82
+ </TouchableOpacity>
83
+ )}
84
+ {headerRight}
85
+ </View>
86
+ </View>
87
+
88
+ {/* Chat content */}
89
+ <AliaChatContent
90
+ ref={contentRef}
91
+ clientContext={clientContext}
92
+ suggestions={suggestions}
93
+ model={model}
94
+ apiUrl={apiUrl}
95
+ />
96
+ </View>
97
+ );
98
+ }
99
+
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
+ });
@@ -27,10 +27,8 @@ import Animated, {
27
27
  withTiming,
28
28
  } from 'react-native-reanimated';
29
29
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
30
- import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
31
- import { AliaChatMessageList } from './AliaChatMessageList';
32
- import { AliaChatInput } from './AliaChatInput';
33
- import { AliaChatSuggestions } from './AliaChatSuggestions';
30
+ import { AliaChatContent, type AliaChatContentRef } from './AliaChatContent';
31
+ import { AliaFace, type AliaExpression } from './AliaFace';
34
32
  import { useAliaColors } from '../theme';
35
33
  import type { AliaChatSuggestion } from '../types';
36
34
 
@@ -61,18 +59,31 @@ export interface AliaChatSheetRef {
61
59
  export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
62
60
  ({ clientContext, suggestions = [], model, apiUrl }, ref) => {
63
61
  const colors = useAliaColors();
64
- const isDark = colors.background === '#000000';
62
+ const isDark = colors.isDark;
65
63
  const insets = useSafeAreaInsets();
66
64
 
67
- // Chat
68
- const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
69
- const { messages, send, isStreaming, clear } = useAliaChat(chatOptions);
65
+ // Content ref for reading state
66
+ const contentRef = useRef<AliaChatContentRef>(null);
67
+ const [faceExpression, setFaceExpression] = useState<AliaExpression>('Idle A');
68
+ const [hasMessages, setHasMessages] = useState(false);
70
69
 
71
70
  // Sheet visibility
72
71
  const [rendered, setRendered] = useState(false);
73
72
  const hasClosedRef = useRef(false);
74
73
  const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
75
74
 
75
+ // Poll content ref for header state (lightweight, only when rendered)
76
+ useEffect(() => {
77
+ if (!rendered) return;
78
+ const interval = setInterval(() => {
79
+ if (contentRef.current) {
80
+ setFaceExpression(contentRef.current.faceExpression);
81
+ setHasMessages(contentRef.current.messages.length > 0);
82
+ }
83
+ }, 200);
84
+ return () => clearInterval(interval);
85
+ }, [rendered]);
86
+
76
87
  // Reanimated shared values
77
88
  const translateY = useSharedValue(SCREEN_HEIGHT);
78
89
  const backdropOpacity = useSharedValue(0);
@@ -147,7 +158,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
147
158
  // Pan gesture for swipe-to-dismiss
148
159
  const nativeGesture = useMemo(() => Gesture.Native(), []);
149
160
 
150
- const panGesture = Gesture.Pan()
161
+ const panGesture = useMemo(() => Gesture.Pan()
151
162
  .simultaneousWithExternalGesture(nativeGesture)
152
163
  .onStart(() => {
153
164
  'worklet';
@@ -181,7 +192,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
181
192
  } else {
182
193
  translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
183
194
  }
184
- });
195
+ }), [nativeGesture, finishDismiss]);
185
196
 
186
197
  // Animated styles
187
198
  const backdropAnimStyle = useAnimatedStyle(() => ({
@@ -199,7 +210,9 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
199
210
  [insets.top],
200
211
  );
201
212
 
202
- const showSuggestions = messages.length === 0 && suggestions.length > 0;
213
+ const handleClear = useCallback(() => {
214
+ contentRef.current?.clear();
215
+ }, []);
203
216
 
204
217
  if (!rendered) return null;
205
218
 
@@ -240,16 +253,14 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
240
253
  {/* Header */}
241
254
  <View style={styles.header}>
242
255
  <View style={styles.headerLeft}>
243
- <View style={[styles.aliaAvatar, { backgroundColor: colors.tint }]}>
244
- <Text style={styles.aliaAvatarText}>A</Text>
245
- </View>
256
+ <AliaFace size={28} expression={faceExpression} />
246
257
  <Text style={[styles.headerTitle, { color: colors.text }]}>
247
258
  Alia
248
259
  </Text>
249
260
  </View>
250
261
  <View style={styles.headerRight}>
251
- {messages.length > 0 && (
252
- <TouchableOpacity onPress={clear} style={styles.clearButton}>
262
+ {hasMessages && (
263
+ <TouchableOpacity onPress={handleClear} style={styles.clearButton}>
253
264
  <Text
254
265
  style={[
255
266
  styles.clearText,
@@ -278,26 +289,17 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
278
289
  </View>
279
290
  </View>
280
291
 
281
- {/* Suggestions or Messages */}
292
+ {/* Chat content */}
282
293
  <GestureDetector gesture={nativeGesture}>
283
- <View style={styles.chatArea}>
284
- {showSuggestions ? (
285
- <AliaChatSuggestions
286
- suggestions={suggestions}
287
- onSelect={send}
288
- />
289
- ) : (
290
- <AliaChatMessageList
291
- messages={messages}
292
- isStreaming={isStreaming}
293
- scrollOffsetY={scrollOffsetY}
294
- />
295
- )}
296
- </View>
294
+ <AliaChatContent
295
+ ref={contentRef}
296
+ clientContext={clientContext}
297
+ suggestions={suggestions}
298
+ model={model}
299
+ apiUrl={apiUrl}
300
+ scrollOffsetY={scrollOffsetY}
301
+ />
297
302
  </GestureDetector>
298
-
299
- {/* Input */}
300
- <AliaChatInput onSend={send} isStreaming={isStreaming} />
301
303
  </Animated.View>
302
304
  </GestureDetector>
303
305
  </GestureHandlerRootView>
@@ -353,18 +355,6 @@ const styles = StyleSheet.create({
353
355
  alignItems: 'center',
354
356
  gap: 10,
355
357
  },
356
- aliaAvatar: {
357
- width: 28,
358
- height: 28,
359
- borderRadius: 14,
360
- alignItems: 'center',
361
- justifyContent: 'center',
362
- },
363
- aliaAvatarText: {
364
- color: '#FFFFFF',
365
- fontSize: 14,
366
- fontWeight: '700',
367
- },
368
358
  headerTitle: {
369
359
  fontSize: 18,
370
360
  fontWeight: '600',
@@ -388,7 +378,4 @@ const styles = StyleSheet.create({
388
378
  justifyContent: 'center',
389
379
  borderRadius: 16,
390
380
  },
391
- chatArea: {
392
- flex: 1,
393
- },
394
381
  });
@@ -10,7 +10,7 @@ interface AliaChatSuggestionsProps {
10
10
 
11
11
  export function AliaChatSuggestions({ suggestions, onSelect }: AliaChatSuggestionsProps) {
12
12
  const colors = useAliaColors();
13
- const isDark = colors.background === '#000000';
13
+ const isDark = colors.isDark;
14
14
 
15
15
  if (suggestions.length === 0) return null;
16
16