@alia.onl/sdk 3.0.0 → 3.1.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 +1 -1
- package/src/components/AliaChatContent.tsx +62 -19
- package/src/components/AliaChatMessageList.tsx +437 -279
- package/src/components/AliaChatScreen.tsx +13 -5
- package/src/components/AliaChatSheet.tsx +11 -5
- package/src/components/AliaWelcomeMessage.tsx +75 -0
- package/src/components/Markdown.tsx +58 -114
- package/src/components/PlanPreviewCard.tsx +76 -0
- package/src/components/ResearchProgressCard.tsx +159 -0
- package/src/hooks/useAliaChat.ts +2 -1
- package/src/hooks/useSpeechToText.ts +21 -7
- package/src/index.ts +8 -1
- package/src/lib/content-utils.ts +23 -0
- package/src/lib/tool-registry.ts +55 -0
- package/src/types.ts +20 -7
- package/src/components/AliaChatSuggestions.tsx +0 -70
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
View,
|
|
4
|
-
Text,
|
|
5
|
-
Pressable,
|
|
6
|
-
StyleSheet,
|
|
7
|
-
Platform,
|
|
8
|
-
} from 'react-native';
|
|
1
|
+
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { View, Pressable, TextInput, StyleSheet, Platform } from 'react-native';
|
|
9
3
|
import { BlurView } from 'expo-blur';
|
|
10
4
|
import Animated, {
|
|
11
5
|
useAnimatedScrollHandler,
|
|
@@ -15,17 +9,32 @@ import Animated, {
|
|
|
15
9
|
withTiming,
|
|
16
10
|
withSequence,
|
|
17
11
|
cancelAnimation,
|
|
18
|
-
Easing,
|
|
19
12
|
FadeInUp,
|
|
20
13
|
type SharedValue,
|
|
21
14
|
} from 'react-native-reanimated';
|
|
15
|
+
import {
|
|
16
|
+
Copy,
|
|
17
|
+
Check,
|
|
18
|
+
Volume2,
|
|
19
|
+
Square,
|
|
20
|
+
ThumbsUp,
|
|
21
|
+
ThumbsDown,
|
|
22
|
+
Pencil,
|
|
23
|
+
} from 'lucide-react-native';
|
|
22
24
|
import { useAliaColors } from '../theme';
|
|
25
|
+
import { Text } from './ui/text';
|
|
23
26
|
import { AliaMarkdown } from './Markdown';
|
|
24
27
|
import { ThinkingIndicator } from './ThinkingIndicator';
|
|
25
28
|
import { Reasoning, ReasoningTrigger, ReasoningContent } from './Reasoning';
|
|
29
|
+
import { ResearchProgressCard } from './ResearchProgressCard';
|
|
30
|
+
import { PlanPreviewCard } from './PlanPreviewCard';
|
|
31
|
+
import { getToolLabel, getToolActiveLabel, getResearchActiveLabel } from '../lib/tool-registry';
|
|
32
|
+
import { getTextFromContent, getImagesFromContent } from '../lib/content-utils';
|
|
26
33
|
import type { ChatMessage, ToolInvocation } from '../types';
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
// Resolve expo-image once at module load (optional peer dep)
|
|
36
|
+
let ExpoImage: any = null;
|
|
37
|
+
try { ExpoImage = require('expo-image').Image; } catch {}
|
|
29
38
|
|
|
30
39
|
type PlaybackState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
|
|
31
40
|
|
|
@@ -36,223 +45,359 @@ export interface AliaChatMessageListProps {
|
|
|
36
45
|
onReadAloud?: (messageId: string, text: string) => void;
|
|
37
46
|
ttsActiveMessageId?: string | null;
|
|
38
47
|
ttsPlaybackState?: PlaybackState;
|
|
48
|
+
// Injectable callbacks
|
|
49
|
+
onEditMessage?: (messageId: string, newContent: string) => void;
|
|
50
|
+
onThumbsUp?: (messageId: string) => void;
|
|
51
|
+
onThumbsDown?: (messageId: string) => void;
|
|
52
|
+
onApprovePlan?: (planId: string) => void;
|
|
53
|
+
onRejectPlan?: (planId: string) => void;
|
|
54
|
+
onToolResultPress?: (messageId: string) => void;
|
|
55
|
+
// Welcome
|
|
56
|
+
welcomeComponent?: React.ReactNode;
|
|
57
|
+
conversationLoading?: boolean;
|
|
58
|
+
// Markdown renderer override (app passes CustomMarkdown, SDK uses AliaMarkdown)
|
|
59
|
+
renderMarkdown?: (content: string) => React.ReactNode;
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
// ── Tool Bullet ──
|
|
42
63
|
|
|
43
|
-
function ToolBullet({
|
|
44
|
-
const colors = useAliaColors();
|
|
45
|
-
|
|
46
|
-
const displayName = invocation.toolName
|
|
47
|
-
.replace(/^oxy_\w+__/, '')
|
|
48
|
-
.replace(/([A-Z])/g, ' $1')
|
|
49
|
-
.replace(/^./, (s) => s.toUpperCase())
|
|
50
|
-
.trim();
|
|
51
|
-
|
|
52
|
-
const isActive = invocation.state === 'call' || invocation.state === 'partial-call';
|
|
53
|
-
const label = isActive ? `${displayName}...` : displayName;
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<View style={styles.toolStatus}>
|
|
57
|
-
{isActive ? (
|
|
58
|
-
<PulsingDot color={colors.tint} />
|
|
59
|
-
) : (
|
|
60
|
-
<Text style={[styles.toolCheckmark, { color: colors.tint }]}>{'\u2713'}</Text>
|
|
61
|
-
)}
|
|
62
|
-
<Text style={[styles.toolLabel, { color: colors.secondaryText }]}>{label}</Text>
|
|
63
|
-
</View>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function PulsingDot({ color }: { color: string }) {
|
|
64
|
+
function ToolBullet({ isRunning }: { isRunning: boolean }) {
|
|
68
65
|
const opacity = useSharedValue(1);
|
|
69
|
-
|
|
70
66
|
useEffect(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
if (isRunning) {
|
|
68
|
+
opacity.value = withRepeat(
|
|
69
|
+
withSequence(
|
|
70
|
+
withTiming(0.3, { duration: 500 }),
|
|
71
|
+
withTiming(1, { duration: 500 }),
|
|
72
|
+
),
|
|
73
|
+
-1,
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
opacity.value = 1;
|
|
77
|
+
}
|
|
78
78
|
return () => cancelAnimation(opacity);
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
const animStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
82
|
-
|
|
79
|
+
}, [isRunning]);
|
|
80
|
+
const style = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
83
81
|
return (
|
|
84
|
-
<Animated.View style={
|
|
82
|
+
<Animated.View style={style}>
|
|
83
|
+
<Text style={{ color: isRunning ? '#eab308' : '#22c55e', fontSize: 10 }}>
|
|
84
|
+
{'\u25CF'}
|
|
85
|
+
</Text>
|
|
86
|
+
</Animated.View>
|
|
85
87
|
);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
// ── User Bubble ──
|
|
89
91
|
|
|
90
|
-
function UserBubble({
|
|
91
|
-
const colors = useAliaColors();
|
|
92
|
-
const isDark = colors.isDark;
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<View style={styles.userMessage}>
|
|
96
|
-
<View style={[styles.userBubble, { borderColor: colors.border }]}>
|
|
97
|
-
<BlurView
|
|
98
|
-
intensity={60}
|
|
99
|
-
tint={isDark ? 'dark' : 'light'}
|
|
100
|
-
style={StyleSheet.absoluteFill}
|
|
101
|
-
/>
|
|
102
|
-
<Text style={[styles.userText, { color: colors.text }]}>{content}</Text>
|
|
103
|
-
</View>
|
|
104
|
-
</View>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ── Message Actions ──
|
|
109
|
-
|
|
110
|
-
function MessageActions({
|
|
92
|
+
function UserBubble({
|
|
111
93
|
message,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
94
|
+
text,
|
|
95
|
+
isEditing,
|
|
96
|
+
editedContent,
|
|
97
|
+
onEditedContentChange,
|
|
98
|
+
onSaveEdit,
|
|
99
|
+
onCancelEdit,
|
|
100
|
+
onStartEdit,
|
|
101
|
+
onCopy,
|
|
102
|
+
copiedMessageId,
|
|
103
|
+
showEditButton,
|
|
115
104
|
}: {
|
|
116
105
|
message: ChatMessage;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
106
|
+
text: string;
|
|
107
|
+
isEditing: boolean;
|
|
108
|
+
editedContent: string;
|
|
109
|
+
onEditedContentChange: (text: string) => void;
|
|
110
|
+
onSaveEdit: () => void;
|
|
111
|
+
onCancelEdit: () => void;
|
|
112
|
+
onStartEdit: () => void;
|
|
113
|
+
onCopy: () => void;
|
|
114
|
+
copiedMessageId: string | null;
|
|
115
|
+
showEditButton: boolean;
|
|
120
116
|
}) {
|
|
121
|
-
const
|
|
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;
|
|
117
|
+
const images = getImagesFromContent(message.content);
|
|
141
118
|
|
|
142
119
|
return (
|
|
143
|
-
<View
|
|
144
|
-
{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
120
|
+
<View className="flex-col items-end gap-0.5">
|
|
121
|
+
{isEditing ? (
|
|
122
|
+
<View className="max-w-[85%] sm:max-w-[75%] rounded-[24px] overflow-hidden border border-border">
|
|
123
|
+
<BlurView intensity={60} tint="default" style={StyleSheet.absoluteFill} />
|
|
124
|
+
<View className="px-4 py-2">
|
|
125
|
+
<TextInput
|
|
126
|
+
value={editedContent}
|
|
127
|
+
onChangeText={onEditedContentChange}
|
|
128
|
+
multiline
|
|
129
|
+
className="text-base text-foreground leading-7"
|
|
130
|
+
autoFocus
|
|
131
|
+
/>
|
|
132
|
+
<View className="flex-row gap-2 mt-2">
|
|
133
|
+
<Pressable
|
|
134
|
+
className="px-3 py-1.5 rounded-lg bg-primary"
|
|
135
|
+
onPress={onSaveEdit}
|
|
136
|
+
>
|
|
137
|
+
<Text className="text-xs text-primary-foreground">Save</Text>
|
|
138
|
+
</Pressable>
|
|
139
|
+
<Pressable
|
|
140
|
+
className="px-3 py-1.5 rounded-lg bg-muted-foreground"
|
|
141
|
+
onPress={onCancelEdit}
|
|
142
|
+
>
|
|
143
|
+
<Text className="text-xs text-background">Cancel</Text>
|
|
144
|
+
</Pressable>
|
|
145
|
+
</View>
|
|
146
|
+
</View>
|
|
147
|
+
</View>
|
|
148
|
+
) : (
|
|
149
|
+
<View className="max-w-[85%] sm:max-w-[75%] rounded-[24px] overflow-hidden border border-border">
|
|
150
|
+
<BlurView intensity={60} tint="default" style={StyleSheet.absoluteFill} />
|
|
151
|
+
<View className="px-4 py-2">
|
|
152
|
+
{/* Inline images from multi-part content */}
|
|
153
|
+
{images.length > 0 && (
|
|
154
|
+
<View className="flex-row flex-wrap gap-2 mb-2">
|
|
155
|
+
{images.map((imgUrl, imgIdx) => {
|
|
156
|
+
if (!ExpoImage) return null;
|
|
157
|
+
return (
|
|
158
|
+
<View key={`img-${imgIdx}`} className="rounded-xl overflow-hidden" style={{ width: 120, height: 120 }}>
|
|
159
|
+
<ExpoImage
|
|
160
|
+
source={{ uri: imgUrl }}
|
|
161
|
+
className="w-full h-full"
|
|
162
|
+
contentFit="cover"
|
|
163
|
+
/>
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</View>
|
|
168
|
+
)}
|
|
169
|
+
<Text className="text-base text-foreground leading-7">{text}</Text>
|
|
170
|
+
</View>
|
|
171
|
+
</View>
|
|
172
|
+
)}
|
|
173
|
+
{/* User message actions */}
|
|
174
|
+
{!isEditing && (
|
|
175
|
+
<View className="flex-row gap-1">
|
|
176
|
+
<Pressable
|
|
177
|
+
className="p-1.5 rounded-lg active:bg-muted"
|
|
178
|
+
onPress={onCopy}
|
|
179
|
+
>
|
|
180
|
+
{copiedMessageId === message.id ? (
|
|
181
|
+
<Check size={14} className="text-green-500" />
|
|
182
|
+
) : (
|
|
183
|
+
<Copy size={14} className="text-muted-foreground" />
|
|
184
|
+
)}
|
|
185
|
+
</Pressable>
|
|
186
|
+
{showEditButton && (
|
|
187
|
+
<Pressable
|
|
188
|
+
className="p-1.5 rounded-lg active:bg-muted"
|
|
189
|
+
onPress={onStartEdit}
|
|
190
|
+
>
|
|
191
|
+
<Pencil size={14} className="text-muted-foreground" />
|
|
192
|
+
</Pressable>
|
|
164
193
|
)}
|
|
165
|
-
</
|
|
194
|
+
</View>
|
|
166
195
|
)}
|
|
167
196
|
</View>
|
|
168
197
|
);
|
|
169
198
|
}
|
|
170
199
|
|
|
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
200
|
// ── Assistant Message ──
|
|
196
201
|
|
|
197
202
|
function AssistantMessage({
|
|
198
203
|
message,
|
|
204
|
+
messageText,
|
|
199
205
|
isStreamingThis,
|
|
206
|
+
isLastMessage,
|
|
207
|
+
isLoading,
|
|
200
208
|
onReadAloud,
|
|
201
209
|
ttsActiveMessageId,
|
|
202
210
|
ttsPlaybackState,
|
|
211
|
+
onCopy,
|
|
212
|
+
copiedMessageId,
|
|
213
|
+
onThumbsUp,
|
|
214
|
+
onThumbsDown,
|
|
215
|
+
onApprovePlan,
|
|
216
|
+
onRejectPlan,
|
|
217
|
+
onToolResultPress,
|
|
218
|
+
renderMarkdown,
|
|
203
219
|
}: {
|
|
204
220
|
message: ChatMessage;
|
|
221
|
+
messageText: string;
|
|
205
222
|
isStreamingThis: boolean;
|
|
223
|
+
isLastMessage: boolean;
|
|
224
|
+
isLoading: boolean;
|
|
206
225
|
onReadAloud?: (messageId: string, text: string) => void;
|
|
207
226
|
ttsActiveMessageId?: string | null;
|
|
208
227
|
ttsPlaybackState?: PlaybackState;
|
|
228
|
+
onCopy: () => void;
|
|
229
|
+
copiedMessageId: string | null;
|
|
230
|
+
onThumbsUp?: (messageId: string) => void;
|
|
231
|
+
onThumbsDown?: (messageId: string) => void;
|
|
232
|
+
onApprovePlan?: (planId: string) => void;
|
|
233
|
+
onRejectPlan?: (planId: string) => void;
|
|
234
|
+
onToolResultPress?: (messageId: string) => void;
|
|
235
|
+
renderMarkdown?: (content: string) => React.ReactNode;
|
|
209
236
|
}) {
|
|
210
|
-
const
|
|
211
|
-
const hasContent = !!message.content;
|
|
237
|
+
const hasContent = messageText.length > 0;
|
|
212
238
|
const hasThinking = !!message.thinking;
|
|
213
|
-
const showThinkingIndicator = isStreamingThis && !hasContent && !message.toolInvocations?.length && !hasThinking;
|
|
214
239
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
)}
|
|
240
|
+
const isThisPlaying = ttsActiveMessageId === message.id && ttsPlaybackState === 'playing';
|
|
241
|
+
const isThisPaused = ttsActiveMessageId === message.id && ttsPlaybackState === 'paused';
|
|
242
|
+
const isThisLoading = ttsActiveMessageId === message.id && ttsPlaybackState === 'loading';
|
|
227
243
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
244
|
+
// ThinkingIndicator — context-aware status
|
|
245
|
+
let thinkingIndicator: React.ReactNode = null;
|
|
246
|
+
if (isLoading && isLastMessage && !hasContent) {
|
|
247
|
+
const activeTool = message.toolInvocations?.find(
|
|
248
|
+
t => t.state === 'call' || t.state === 'partial-call',
|
|
249
|
+
);
|
|
250
|
+
const rp = message.researchProgress;
|
|
251
|
+
let activeStatus: string | undefined;
|
|
252
|
+
if (activeTool) {
|
|
253
|
+
activeStatus = getToolActiveLabel(activeTool.toolName);
|
|
254
|
+
} else if (rp?.phase && rp.phase !== 'complete') {
|
|
255
|
+
activeStatus = getResearchActiveLabel(rp.phase);
|
|
256
|
+
} else if (message.thinking) {
|
|
257
|
+
activeStatus = 'Reasoning...';
|
|
258
|
+
}
|
|
259
|
+
thinkingIndicator = (
|
|
260
|
+
<ThinkingIndicator
|
|
261
|
+
isWorking={(message.toolInvocations?.length ?? 0) > 0}
|
|
262
|
+
statusText={activeStatus}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
232
266
|
|
|
233
|
-
|
|
234
|
-
|
|
267
|
+
return (
|
|
268
|
+
<View className="w-full">
|
|
269
|
+
{/* Plan Preview */}
|
|
270
|
+
{message.pendingPlan && onApprovePlan && onRejectPlan && (
|
|
271
|
+
<PlanPreviewCard
|
|
272
|
+
steps={message.pendingPlan.steps}
|
|
273
|
+
approved={message.pendingPlan.approved}
|
|
274
|
+
rejected={message.pendingPlan.rejected}
|
|
275
|
+
onApprove={() => onApprovePlan(message.pendingPlan!.planId)}
|
|
276
|
+
onReject={() => onRejectPlan(message.pendingPlan!.planId)}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
235
279
|
|
|
236
|
-
{/*
|
|
237
|
-
{
|
|
238
|
-
|
|
280
|
+
{/* Tool Invocations */}
|
|
281
|
+
{message.toolInvocations?.map((t, ti) => {
|
|
282
|
+
const key = t.toolCallId || `tool-${message.id}-${ti}`;
|
|
283
|
+
const toolLabel = getToolLabel(t.toolName);
|
|
284
|
+
const isRunning = t.state === 'call' || t.state === 'partial-call';
|
|
285
|
+
const isDone = t.state === 'result';
|
|
286
|
+
|
|
287
|
+
let description = '';
|
|
288
|
+
if (t.args?.url) {
|
|
289
|
+
const url = String(t.args.url);
|
|
290
|
+
description = url.length > 40 ? url.substring(0, 40) + '...' : url;
|
|
291
|
+
} else if (t.args?.query) {
|
|
292
|
+
const q = String(t.args.query);
|
|
293
|
+
description = `"${q.length > 30 ? q.substring(0, 30) + '...' : q}"`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<Pressable
|
|
298
|
+
key={key}
|
|
299
|
+
className="flex-row items-center gap-2 py-1 active:opacity-70"
|
|
300
|
+
onPress={isDone && onToolResultPress ? () => onToolResultPress(message.id) : undefined}
|
|
301
|
+
disabled={!isDone || !onToolResultPress}
|
|
302
|
+
>
|
|
303
|
+
<ToolBullet isRunning={isRunning} />
|
|
304
|
+
<Text className="text-sm text-foreground flex-1 flex-shrink">
|
|
305
|
+
<Text className="font-bold">{toolLabel}</Text>
|
|
306
|
+
{description ? (
|
|
307
|
+
<Text className="text-muted-foreground"> {description}</Text>
|
|
308
|
+
) : null}
|
|
309
|
+
</Text>
|
|
310
|
+
</Pressable>
|
|
311
|
+
);
|
|
312
|
+
})}
|
|
313
|
+
|
|
314
|
+
{/* Research Progress */}
|
|
315
|
+
{message.researchProgress && (
|
|
316
|
+
<ResearchProgressCard progress={message.researchProgress} />
|
|
239
317
|
)}
|
|
240
318
|
|
|
241
|
-
{/*
|
|
242
|
-
{
|
|
243
|
-
<
|
|
319
|
+
{/* Extended Thinking */}
|
|
320
|
+
{hasThinking && (
|
|
321
|
+
<View className="mb-3 w-full">
|
|
322
|
+
<Reasoning isStreaming={isStreamingThis && !hasContent}>
|
|
323
|
+
<ReasoningTrigger />
|
|
324
|
+
<ReasoningContent>{message.thinking!}</ReasoningContent>
|
|
325
|
+
</Reasoning>
|
|
326
|
+
</View>
|
|
244
327
|
)}
|
|
245
328
|
|
|
246
|
-
{/* Message
|
|
247
|
-
{
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
329
|
+
{/* Message Content */}
|
|
330
|
+
{(hasContent || message.isStreaming) && (
|
|
331
|
+
<View className="flex-col items-start gap-0.5">
|
|
332
|
+
{/* Voice cohost label */}
|
|
333
|
+
{message.source === 'voice' && message.speaker === 'cohost' && (
|
|
334
|
+
<Text className="text-xs text-indigo-400 mb-0.5">Cohost</Text>
|
|
335
|
+
)}
|
|
336
|
+
{/* Agent identity */}
|
|
337
|
+
{message.agentInfo && (
|
|
338
|
+
<View className="flex-row items-center gap-2 mb-0.5">
|
|
339
|
+
<View className="w-5 h-5 rounded-full bg-orange-500/20 items-center justify-center">
|
|
340
|
+
<Text style={{ fontSize: 8, color: '#f97316' }}>{'\u25CF'}</Text>
|
|
341
|
+
</View>
|
|
342
|
+
<Text className="text-xs font-semibold" style={{ color: '#f97316' }}>
|
|
343
|
+
{message.agentInfo.name}
|
|
344
|
+
</Text>
|
|
345
|
+
</View>
|
|
346
|
+
)}
|
|
347
|
+
<View className="w-full">
|
|
348
|
+
{message.source === 'voice' ? (
|
|
349
|
+
<Text className="text-base text-foreground leading-7">
|
|
350
|
+
{messageText}
|
|
351
|
+
{message.isStreaming ? '\u258C' : ''}
|
|
352
|
+
</Text>
|
|
353
|
+
) : renderMarkdown ? (
|
|
354
|
+
renderMarkdown(messageText)
|
|
355
|
+
) : (
|
|
356
|
+
<AliaMarkdown content={messageText} />
|
|
357
|
+
)}
|
|
358
|
+
</View>
|
|
359
|
+
{/* Action buttons */}
|
|
360
|
+
{!isStreamingThis && hasContent && (
|
|
361
|
+
<View className="flex-row gap-1">
|
|
362
|
+
{onReadAloud && (
|
|
363
|
+
<Pressable
|
|
364
|
+
className="p-1.5 rounded-lg active:bg-muted"
|
|
365
|
+
onPress={() => onReadAloud(message.id, messageText)}
|
|
366
|
+
>
|
|
367
|
+
{isThisPlaying || isThisPaused ? (
|
|
368
|
+
<Square size={14} className={isThisPlaying ? 'text-primary' : 'text-muted-foreground'} />
|
|
369
|
+
) : (
|
|
370
|
+
<Volume2 size={14} className={isThisLoading ? 'text-primary opacity-50' : 'text-muted-foreground'} />
|
|
371
|
+
)}
|
|
372
|
+
</Pressable>
|
|
373
|
+
)}
|
|
374
|
+
<Pressable
|
|
375
|
+
className="p-1.5 rounded-lg active:bg-muted"
|
|
376
|
+
onPress={onCopy}
|
|
377
|
+
>
|
|
378
|
+
{copiedMessageId === message.id ? (
|
|
379
|
+
<Check size={14} className="text-green-500" />
|
|
380
|
+
) : (
|
|
381
|
+
<Copy size={14} className="text-muted-foreground" />
|
|
382
|
+
)}
|
|
383
|
+
</Pressable>
|
|
384
|
+
{onThumbsUp && (
|
|
385
|
+
<Pressable className="p-1.5 rounded-lg active:bg-muted" onPress={() => onThumbsUp(message.id)}>
|
|
386
|
+
<ThumbsUp size={14} className="text-muted-foreground" />
|
|
387
|
+
</Pressable>
|
|
388
|
+
)}
|
|
389
|
+
{onThumbsDown && (
|
|
390
|
+
<Pressable className="p-1.5 rounded-lg active:bg-muted" onPress={() => onThumbsDown(message.id)}>
|
|
391
|
+
<ThumbsDown size={14} className="text-muted-foreground" />
|
|
392
|
+
</Pressable>
|
|
393
|
+
)}
|
|
394
|
+
</View>
|
|
395
|
+
)}
|
|
396
|
+
</View>
|
|
254
397
|
)}
|
|
255
|
-
|
|
398
|
+
|
|
399
|
+
{thinkingIndicator}
|
|
400
|
+
</View>
|
|
256
401
|
);
|
|
257
402
|
}
|
|
258
403
|
|
|
@@ -265,10 +410,27 @@ export function AliaChatMessageList({
|
|
|
265
410
|
onReadAloud,
|
|
266
411
|
ttsActiveMessageId,
|
|
267
412
|
ttsPlaybackState,
|
|
413
|
+
onEditMessage,
|
|
414
|
+
onThumbsUp,
|
|
415
|
+
onThumbsDown,
|
|
416
|
+
onApprovePlan,
|
|
417
|
+
onRejectPlan,
|
|
418
|
+
onToolResultPress,
|
|
419
|
+
welcomeComponent,
|
|
420
|
+
conversationLoading,
|
|
421
|
+
renderMarkdown,
|
|
268
422
|
}: AliaChatMessageListProps) {
|
|
269
423
|
const scrollRef = useRef<Animated.ScrollView>(null);
|
|
424
|
+
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
|
425
|
+
const [editedContent, setEditedContent] = useState('');
|
|
426
|
+
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
|
427
|
+
const prevMessageCountRef = useRef(messages.length);
|
|
428
|
+
|
|
429
|
+
useEffect(() => {
|
|
430
|
+
prevMessageCountRef.current = messages.length;
|
|
431
|
+
}, [messages.length]);
|
|
270
432
|
|
|
271
|
-
// Auto-scroll
|
|
433
|
+
// Auto-scroll
|
|
272
434
|
useEffect(() => {
|
|
273
435
|
if (messages.length > 0) {
|
|
274
436
|
const timer = setTimeout(() => {
|
|
@@ -276,7 +438,7 @@ export function AliaChatMessageList({
|
|
|
276
438
|
}, 100);
|
|
277
439
|
return () => clearTimeout(timer);
|
|
278
440
|
}
|
|
279
|
-
}, [messages]);
|
|
441
|
+
}, [messages.length, isStreaming]);
|
|
280
442
|
|
|
281
443
|
const scrollHandler = useAnimatedScrollHandler({
|
|
282
444
|
onScroll: (event) => {
|
|
@@ -284,114 +446,110 @@ export function AliaChatMessageList({
|
|
|
284
446
|
},
|
|
285
447
|
});
|
|
286
448
|
|
|
449
|
+
const handleCopy = useCallback(async (content: string, messageId: string) => {
|
|
450
|
+
try {
|
|
451
|
+
if (Platform.OS === 'web') {
|
|
452
|
+
await navigator.clipboard.writeText(content);
|
|
453
|
+
} else {
|
|
454
|
+
const Clipboard = await import('expo-clipboard');
|
|
455
|
+
await Clipboard.setStringAsync(content);
|
|
456
|
+
}
|
|
457
|
+
setCopiedMessageId(messageId);
|
|
458
|
+
setTimeout(() => setCopiedMessageId(null), 2000);
|
|
459
|
+
} catch {}
|
|
460
|
+
}, []);
|
|
461
|
+
|
|
462
|
+
const handleStartEdit = useCallback((messageId: string, content: string) => {
|
|
463
|
+
setEditingMessageId(messageId);
|
|
464
|
+
setEditedContent(content);
|
|
465
|
+
}, []);
|
|
466
|
+
|
|
467
|
+
const editedContentRef = useRef(editedContent);
|
|
468
|
+
editedContentRef.current = editedContent;
|
|
469
|
+
|
|
470
|
+
const handleSaveEdit = useCallback((messageId: string) => {
|
|
471
|
+
if (onEditMessage && editedContentRef.current.trim()) {
|
|
472
|
+
onEditMessage(messageId, editedContentRef.current);
|
|
473
|
+
}
|
|
474
|
+
setEditingMessageId(null);
|
|
475
|
+
setEditedContent('');
|
|
476
|
+
}, [onEditMessage]);
|
|
477
|
+
|
|
478
|
+
const handleCancelEdit = useCallback(() => {
|
|
479
|
+
setEditingMessageId(null);
|
|
480
|
+
setEditedContent('');
|
|
481
|
+
}, []);
|
|
482
|
+
|
|
483
|
+
const filteredMessages = useMemo(() => messages.filter(m => m.role !== 'system'), [messages]);
|
|
484
|
+
|
|
287
485
|
return (
|
|
288
486
|
<Animated.ScrollView
|
|
289
487
|
ref={scrollRef}
|
|
290
|
-
|
|
291
|
-
contentContainerStyle={
|
|
488
|
+
className="flex-1 bg-background px-4 py-4"
|
|
489
|
+
contentContainerStyle={{ flexGrow: 1, paddingTop: 4, paddingBottom: 16 }}
|
|
292
490
|
showsVerticalScrollIndicator={false}
|
|
293
491
|
keyboardShouldPersistTaps="handled"
|
|
294
492
|
onScroll={scrollHandler}
|
|
295
493
|
scrollEventThrottle={16}
|
|
296
494
|
>
|
|
297
|
-
{
|
|
298
|
-
|
|
299
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
495
|
+
<View className="max-w-3xl mx-auto w-full" style={!filteredMessages.length ? { flex: 1, justifyContent: 'center' } : undefined}>
|
|
496
|
+
{/* Welcome or loading */}
|
|
497
|
+
{!filteredMessages.length && (
|
|
498
|
+
welcomeComponent || null
|
|
499
|
+
)}
|
|
500
|
+
|
|
501
|
+
{/* Messages */}
|
|
502
|
+
<View className="gap-2">
|
|
503
|
+
{filteredMessages.map((msg, i) => {
|
|
504
|
+
const isNewMessage = i >= prevMessageCountRef.current;
|
|
505
|
+
const isLast = i === filteredMessages.length - 1;
|
|
506
|
+
const isStreamingThis = isStreaming && isLast;
|
|
507
|
+
const messageText = getTextFromContent(msg.content);
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<Animated.View
|
|
511
|
+
key={msg.id}
|
|
512
|
+
entering={isNewMessage ? FadeInUp.springify() : undefined}
|
|
513
|
+
>
|
|
514
|
+
{msg.role === 'user' ? (
|
|
515
|
+
<UserBubble
|
|
516
|
+
message={msg}
|
|
517
|
+
text={messageText}
|
|
518
|
+
isEditing={editingMessageId === msg.id}
|
|
519
|
+
editedContent={editedContent}
|
|
520
|
+
onEditedContentChange={setEditedContent}
|
|
521
|
+
onSaveEdit={() => handleSaveEdit(msg.id)}
|
|
522
|
+
onCancelEdit={handleCancelEdit}
|
|
523
|
+
onStartEdit={() => handleStartEdit(msg.id, messageText)}
|
|
524
|
+
onCopy={() => handleCopy(messageText, msg.id)}
|
|
525
|
+
copiedMessageId={copiedMessageId}
|
|
526
|
+
showEditButton={!!onEditMessage}
|
|
527
|
+
/>
|
|
528
|
+
) : (
|
|
529
|
+
<AssistantMessage
|
|
530
|
+
message={msg}
|
|
531
|
+
messageText={messageText}
|
|
532
|
+
isStreamingThis={isStreamingThis}
|
|
533
|
+
isLastMessage={isLast}
|
|
534
|
+
isLoading={isStreaming}
|
|
535
|
+
onReadAloud={onReadAloud}
|
|
536
|
+
ttsActiveMessageId={ttsActiveMessageId}
|
|
537
|
+
ttsPlaybackState={ttsPlaybackState}
|
|
538
|
+
onCopy={() => handleCopy(messageText, msg.id)}
|
|
539
|
+
copiedMessageId={copiedMessageId}
|
|
540
|
+
onThumbsUp={onThumbsUp}
|
|
541
|
+
onThumbsDown={onThumbsDown}
|
|
542
|
+
onApprovePlan={onApprovePlan}
|
|
543
|
+
onRejectPlan={onRejectPlan}
|
|
544
|
+
onToolResultPress={onToolResultPress}
|
|
545
|
+
renderMarkdown={renderMarkdown}
|
|
546
|
+
/>
|
|
547
|
+
)}
|
|
548
|
+
</Animated.View>
|
|
549
|
+
);
|
|
550
|
+
})}
|
|
551
|
+
</View>
|
|
552
|
+
</View>
|
|
317
553
|
</Animated.ScrollView>
|
|
318
554
|
);
|
|
319
555
|
}
|
|
320
|
-
|
|
321
|
-
const styles = StyleSheet.create({
|
|
322
|
-
scrollView: {
|
|
323
|
-
flex: 1,
|
|
324
|
-
},
|
|
325
|
-
content: {
|
|
326
|
-
padding: 16,
|
|
327
|
-
paddingTop: 4,
|
|
328
|
-
flexGrow: 1,
|
|
329
|
-
},
|
|
330
|
-
|
|
331
|
-
// User bubble — frosted glass
|
|
332
|
-
userMessage: {
|
|
333
|
-
alignItems: 'flex-end',
|
|
334
|
-
marginBottom: 12,
|
|
335
|
-
},
|
|
336
|
-
userBubble: {
|
|
337
|
-
maxWidth: '85%',
|
|
338
|
-
paddingHorizontal: 16,
|
|
339
|
-
paddingVertical: 10,
|
|
340
|
-
borderRadius: 24,
|
|
341
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
342
|
-
overflow: 'hidden',
|
|
343
|
-
},
|
|
344
|
-
userText: {
|
|
345
|
-
fontSize: 15,
|
|
346
|
-
lineHeight: 22,
|
|
347
|
-
},
|
|
348
|
-
|
|
349
|
-
// Assistant message — no bubble
|
|
350
|
-
assistantMessage: {
|
|
351
|
-
alignItems: 'flex-start',
|
|
352
|
-
marginBottom: 12,
|
|
353
|
-
},
|
|
354
|
-
cursor: {
|
|
355
|
-
fontSize: 15,
|
|
356
|
-
marginTop: -4,
|
|
357
|
-
},
|
|
358
|
-
|
|
359
|
-
// Tool status
|
|
360
|
-
toolStatus: {
|
|
361
|
-
flexDirection: 'row',
|
|
362
|
-
alignItems: 'center',
|
|
363
|
-
gap: 6,
|
|
364
|
-
marginBottom: 6,
|
|
365
|
-
paddingVertical: 2,
|
|
366
|
-
},
|
|
367
|
-
toolDot: {
|
|
368
|
-
width: 8,
|
|
369
|
-
height: 8,
|
|
370
|
-
borderRadius: 4,
|
|
371
|
-
},
|
|
372
|
-
toolCheckmark: {
|
|
373
|
-
fontSize: 14,
|
|
374
|
-
fontWeight: '600',
|
|
375
|
-
},
|
|
376
|
-
toolLabel: {
|
|
377
|
-
fontSize: 13,
|
|
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
|
-
},
|
|
397
|
-
});
|