A React Native app for the ultimate thinking partner.
1/**
2 * StreamingTestPage - Minimal test page for debugging streaming accumulation
3 *
4 * This page shows exactly what's happening with streaming chunks:
5 * - Raw chunks as they arrive
6 * - Accumulated text in real-time
7 * - Message groups as they're rendered
8 */
9
10import React, { useState } from 'react';
11import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Platform } from 'react-native';
12import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
13
14import { useAgentStore } from './src/stores/agentStore';
15import { useChatStore } from './src/stores/chatStore';
16import { useAuth } from './src/hooks/useAuth';
17import { useAgent } from './src/hooks/useAgent';
18import lettaApi from './src/api/lettaApi';
19import type { StreamingChunk } from './src/types/letta';
20import CoLoginScreen from './CoLoginScreen';
21
22export default function StreamingTestPage() {
23 const [input, setInput] = useState('');
24 const [chunks, setChunks] = useState<Array<{ type: string; content: string; timestamp: number }>>([]);
25 const [isSending, setIsSending] = useState(false);
26
27 const { isConnected, isLoadingToken } = useAuth();
28 const { coAgent, isInitializingCo } = useAgent();
29 const currentStream = useChatStore((state) => state.currentStream);
30 const isStreaming = useChatStore((state) => state.isStreaming);
31 const chatStore = useChatStore();
32
33 // Show login if not connected
34 if (!isConnected) {
35 return <CoLoginScreen />;
36 }
37
38 // Show loading if initializing agent
39 if (isLoadingToken || isInitializingCo || !coAgent) {
40 return (
41 <View style={styles.container}>
42 <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
43 <Text style={{ color: '#fff', fontSize: 16 }}>Initializing agent...</Text>
44 </View>
45 </View>
46 );
47 }
48
49 const handleSend = async () => {
50 if (!input.trim() || !coAgent || isSending) return;
51
52 const messageText = input.trim();
53 setInput('');
54 setChunks([]);
55 setIsSending(true);
56
57 try {
58 chatStore.startStreaming();
59
60 const payload = {
61 messages: [{ role: 'user', content: messageText }],
62 use_assistant_message: true,
63 stream_tokens: true,
64 };
65
66 await lettaApi.sendMessageStream(
67 coAgent.id,
68 payload,
69 (chunk: StreamingChunk) => {
70 // Log every chunk
71 const timestamp = Date.now();
72 console.log('[CHUNK]', chunk.message_type, chunk);
73
74 // Add to visual log
75 setChunks((prev) => [
76 ...prev,
77 {
78 type: chunk.message_type,
79 content: JSON.stringify(chunk, null, 2),
80 timestamp,
81 },
82 ]);
83
84 // Process through normal handlers
85 if (chunk.message_type === 'reasoning_message' && chunk.reasoning) {
86 chatStore.updateStreamReasoning(chunk.reasoning);
87 } else if (chunk.message_type === 'assistant_message' && chunk.content) {
88 let contentText = '';
89 const content = chunk.content as any;
90
91 if (typeof content === 'string') {
92 contentText = content;
93 } else if (typeof content === 'object' && content !== null) {
94 if (Array.isArray(content)) {
95 contentText = content
96 .filter((item: any) => item.type === 'text')
97 .map((item: any) => item.text || '')
98 .join('');
99 } else if (content.text) {
100 contentText = content.text;
101 }
102 }
103
104 if (contentText) {
105 chatStore.updateStreamAssistant(contentText);
106 }
107 }
108 },
109 (response) => {
110 console.log('[STREAM COMPLETE]', response);
111 chatStore.stopStreaming();
112 setIsSending(false);
113 },
114 (error) => {
115 console.error('[STREAM ERROR]', error);
116 chatStore.stopStreaming();
117 setIsSending(false);
118 }
119 );
120 } catch (error) {
121 console.error('[SEND ERROR]', error);
122 chatStore.stopStreaming();
123 setIsSending(false);
124 }
125 };
126
127 const handleClear = () => {
128 setChunks([]);
129 chatStore.clearStream();
130 chatStore.stopStreaming();
131 };
132
133 return (
134 <SafeAreaProvider>
135 <SafeAreaView style={styles.container}>
136 {/* Header */}
137 <View style={styles.header}>
138 <Text style={styles.headerTitle}>Streaming Test Page</Text>
139 <TouchableOpacity onPress={handleClear} style={styles.clearButton}>
140 <Text style={styles.clearButtonText}>Clear</Text>
141 </TouchableOpacity>
142 </View>
143
144 {/* Status */}
145 <View style={styles.status}>
146 <Text style={styles.statusText}>
147 Agent: {coAgent ? coAgent.id.substring(0, 8) : 'None'}
148 </Text>
149 <Text style={styles.statusText}>
150 Streaming: {isStreaming ? 'Yes' : 'No'}
151 </Text>
152 </View>
153
154 {/* Accumulated State */}
155 <View style={styles.section}>
156 <Text style={styles.sectionTitle}>📊 Accumulated State</Text>
157 <ScrollView style={styles.stateContainer}>
158 <Text style={styles.label}>Reasoning ({currentStream.reasoning.length} chars):</Text>
159 <Text style={styles.content}>{currentStream.reasoning || '(empty)'}</Text>
160
161 <Text style={styles.label}>Assistant ({currentStream.assistantMessage.length} chars):</Text>
162 <Text style={styles.content}>{currentStream.assistantMessage || '(empty)'}</Text>
163
164 <Text style={styles.label}>Tool Calls ({currentStream.toolCalls.length}):</Text>
165 <Text style={styles.content}>
166 {currentStream.toolCalls.length > 0
167 ? currentStream.toolCalls.map((tc) => tc.args).join('\n')
168 : '(none)'}
169 </Text>
170 </ScrollView>
171 </View>
172
173 {/* Raw Chunks */}
174 <View style={styles.section}>
175 <Text style={styles.sectionTitle}>📦 Raw Chunks ({chunks.length})</Text>
176 <ScrollView style={styles.chunksContainer}>
177 {chunks.map((chunk, idx) => (
178 <View key={idx} style={styles.chunk}>
179 <Text style={styles.chunkType}>
180 [{idx}] {chunk.type}
181 </Text>
182 <Text style={styles.chunkContent}>{chunk.content}</Text>
183 </View>
184 ))}
185 </ScrollView>
186 </View>
187
188 {/* Input */}
189 <View style={styles.inputContainer}>
190 <TextInput
191 style={styles.input}
192 value={input}
193 onChangeText={setInput}
194 placeholder="Type a message to test streaming..."
195 placeholderTextColor="#666"
196 editable={!isSending}
197 multiline
198 />
199 <TouchableOpacity
200 onPress={handleSend}
201 style={[styles.sendButton, (!input.trim() || isSending) && styles.sendButtonDisabled]}
202 disabled={!input.trim() || isSending}
203 >
204 <Text style={styles.sendButtonText}>{isSending ? 'Sending...' : 'Send'}</Text>
205 </TouchableOpacity>
206 </View>
207 </SafeAreaView>
208 </SafeAreaProvider>
209 );
210}
211
212const styles = StyleSheet.create({
213 container: {
214 flex: 1,
215 backgroundColor: '#1a1a1a',
216 },
217 header: {
218 flexDirection: 'row',
219 justifyContent: 'space-between',
220 alignItems: 'center',
221 padding: 16,
222 borderBottomWidth: 1,
223 borderBottomColor: '#333',
224 },
225 headerTitle: {
226 fontSize: 18,
227 fontWeight: 'bold',
228 color: '#fff',
229 },
230 clearButton: {
231 padding: 8,
232 backgroundColor: '#333',
233 borderRadius: 4,
234 },
235 clearButtonText: {
236 color: '#fff',
237 fontSize: 14,
238 },
239 status: {
240 padding: 16,
241 backgroundColor: '#222',
242 borderBottomWidth: 1,
243 borderBottomColor: '#333',
244 },
245 statusText: {
246 color: '#aaa',
247 fontSize: 12,
248 marginBottom: 4,
249 },
250 section: {
251 flex: 1,
252 borderBottomWidth: 1,
253 borderBottomColor: '#333',
254 },
255 sectionTitle: {
256 padding: 8,
257 backgroundColor: '#222',
258 color: '#fff',
259 fontSize: 14,
260 fontWeight: 'bold',
261 },
262 stateContainer: {
263 flex: 1,
264 padding: 12,
265 },
266 label: {
267 color: '#888',
268 fontSize: 12,
269 marginTop: 8,
270 marginBottom: 4,
271 },
272 content: {
273 color: '#fff',
274 fontSize: 13,
275 fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
276 marginBottom: 8,
277 },
278 chunksContainer: {
279 flex: 1,
280 padding: 12,
281 },
282 chunk: {
283 marginBottom: 12,
284 padding: 8,
285 backgroundColor: '#222',
286 borderRadius: 4,
287 borderLeftWidth: 3,
288 borderLeftColor: '#0a84ff',
289 },
290 chunkType: {
291 color: '#0a84ff',
292 fontSize: 12,
293 fontWeight: 'bold',
294 marginBottom: 4,
295 },
296 chunkContent: {
297 color: '#aaa',
298 fontSize: 11,
299 fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
300 },
301 inputContainer: {
302 flexDirection: 'row',
303 padding: 16,
304 borderTopWidth: 1,
305 borderTopColor: '#333',
306 alignItems: 'center',
307 },
308 input: {
309 flex: 1,
310 backgroundColor: '#222',
311 color: '#fff',
312 padding: 12,
313 borderRadius: 8,
314 marginRight: 8,
315 maxHeight: 100,
316 },
317 sendButton: {
318 backgroundColor: '#0a84ff',
319 paddingHorizontal: 20,
320 paddingVertical: 12,
321 borderRadius: 8,
322 },
323 sendButtonDisabled: {
324 backgroundColor: '#333',
325 },
326 sendButtonText: {
327 color: '#fff',
328 fontWeight: 'bold',
329 },
330});