A React Native app for the ultimate thinking partner.
at main 9.5 kB view raw
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});