@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.
- package/package.json +37 -3
- package/src/components/AliaChatContent.tsx +313 -0
- package/src/components/AliaChatInput.tsx +174 -27
- package/src/components/AliaChatMessageList.tsx +220 -34
- package/src/components/AliaChatScreen.tsx +132 -0
- package/src/components/AliaChatSheet.tsx +36 -49
- package/src/components/AliaChatSuggestions.tsx +1 -1
- package/src/components/AliaFace.tsx +402 -0
- package/src/components/Markdown.tsx +288 -0
- package/src/components/Reasoning.tsx +154 -0
- package/src/components/ThinkingIndicator.tsx +134 -0
- package/src/components/ui/button.tsx +104 -0
- package/src/components/ui/chat-text-input.tsx +164 -0
- package/src/components/ui/dropdown-menu.tsx +2 -0
- package/src/components/ui/dropdown-menu.web.tsx +355 -0
- package/src/components/ui/prompt-input/actions.tsx +28 -0
- package/src/components/ui/prompt-input/add-menu.tsx +83 -0
- package/src/components/ui/prompt-input/attachments.tsx +183 -0
- package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
- package/src/components/ui/prompt-input/context.tsx +71 -0
- package/src/components/ui/prompt-input/mic-button.tsx +53 -0
- package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
- package/src/components/ui/prompt-input/submit-button.tsx +49 -0
- package/src/components/ui/prompt-input/textarea.tsx +64 -0
- package/src/components/ui/text.tsx +29 -0
- package/src/components/voice/AudioWaveVisualizer.tsx +377 -0
- package/src/components/voice/VoiceControls.tsx +180 -0
- package/src/components/voice/VoiceOverlay.tsx +43 -0
- package/src/hooks/useAliaChat.ts +278 -91
- package/src/hooks/useAudioLevelMonitor.ts +142 -0
- package/src/hooks/useAudioLevels.ts +51 -0
- package/src/hooks/useDocumentPicker.ts +37 -0
- package/src/hooks/useImagePicker.ts +38 -0
- package/src/hooks/useSoundEffects.ts +128 -0
- package/src/hooks/useSpeechToText.ts +182 -0
- package/src/hooks/useTTS.ts +233 -0
- package/src/hooks/useVoiceRoom.ts +420 -0
- package/src/index.ts +71 -6
- package/src/lib/keyboard.native.tsx +6 -0
- package/src/lib/keyboard.tsx +25 -0
- package/src/lib/utils.ts +14 -0
- package/src/optional-deps.d.ts +86 -0
- package/src/theme.ts +25 -2
- 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, {
|
|
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
|
-
|
|
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:
|
|
35
|
+
scrollOffsetY: SharedValue<number>;
|
|
36
|
+
onReadAloud?: (messageId: string, text: string) => void;
|
|
37
|
+
ttsActiveMessageId?: string | null;
|
|
38
|
+
ttsPlaybackState?: PlaybackState;
|
|
17
39
|
}
|
|
18
40
|
|
|
19
|
-
|
|
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
|
|
30
|
-
|
|
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
|
-
{
|
|
37
|
-
<
|
|
38
|
-
)
|
|
39
|
-
|
|
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.
|
|
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
|
|
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
|
-
<
|
|
230
|
+
<ToolBullet key={`${tool.toolCallId || tool.toolName}-${i}`} invocation={tool} />
|
|
81
231
|
))}
|
|
82
232
|
|
|
83
|
-
{/*
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
+
cursor: {
|
|
183
355
|
fontSize: 15,
|
|
184
|
-
|
|
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 {
|
|
31
|
-
import {
|
|
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.
|
|
62
|
+
const isDark = colors.isDark;
|
|
65
63
|
const insets = useSafeAreaInsets();
|
|
66
64
|
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
const
|
|
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
|
|
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
|
-
<
|
|
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
|
-
{
|
|
252
|
-
<TouchableOpacity onPress={
|
|
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
|
-
{/*
|
|
292
|
+
{/* Chat content */}
|
|
282
293
|
<GestureDetector gesture={nativeGesture}>
|
|
283
|
-
<
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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.
|
|
13
|
+
const isDark = colors.isDark;
|
|
14
14
|
|
|
15
15
|
if (suggestions.length === 0) return null;
|
|
16
16
|
|