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