A React Native app for the ultimate thinking partner.
at bf2a37bf805fa5ccc8b5badeeb098f842186e615 228 lines 7.4 kB view raw
1import { useCallback } from 'react'; 2import { useChatStore } from '../stores/chatStore'; 3import { useAgentStore } from '../stores/agentStore'; 4import lettaApi from '../api/lettaApi'; 5import type { StreamingChunk, LettaMessage } from '../types/letta'; 6 7/** 8 * Hook to handle streaming message sending 9 */ 10export function useMessageStream() { 11 const chatStore = useChatStore(); 12 const coAgent = useAgentStore((state) => state.coAgent); 13 14 // Handle individual streaming chunks 15 const handleStreamingChunk = useCallback((chunk: StreamingChunk) => { 16 console.log('Streaming chunk:', chunk.message_type, 'content:', chunk.content); 17 18 // Handle error chunks 19 if ((chunk as any).error) { 20 console.error('Error chunk received:', (chunk as any).error); 21 chatStore.stopStreaming(); 22 chatStore.setSendingMessage(false); 23 chatStore.clearStream(); 24 return; 25 } 26 27 // Handle stop_reason chunks 28 if ((chunk as any).message_type === 'stop_reason') { 29 console.log('Stop reason received:', (chunk as any).stopReason || (chunk as any).stop_reason); 30 return; 31 } 32 33 // Process reasoning messages 34 if (chunk.message_type === 'reasoning_message' && chunk.reasoning) { 35 chatStore.updateStreamReasoning(chunk.reasoning); 36 } 37 38 // Process tool call messages 39 else if ((chunk.message_type === 'tool_call_message' || chunk.message_type === 'tool_call') && chunk.tool_call) { 40 const callObj = chunk.tool_call.function || chunk.tool_call; 41 const toolName = callObj?.name || callObj?.tool_name || 'tool'; 42 const args = callObj?.arguments || callObj?.args || {}; 43 const toolCallId = chunk.id || `tool_${toolName}_${Date.now()}`; 44 45 const formatArgsPython = (obj: any): string => { 46 if (!obj || typeof obj !== 'object') return ''; 47 return Object.entries(obj) 48 .map(([k, v]) => `${k}=${typeof v === 'string' ? `"${v}"` : JSON.stringify(v)}`) 49 .join(', '); 50 }; 51 52 const toolLine = `${toolName}(${formatArgsPython(args)})`; 53 chatStore.addStreamToolCall({ id: toolCallId, name: toolName, args: toolLine }); 54 } 55 56 // Process assistant messages 57 else if (chunk.message_type === 'assistant_message' && chunk.content) { 58 let contentText = ''; 59 const content = chunk.content as any; 60 61 if (typeof content === 'string') { 62 contentText = content; 63 } else if (typeof content === 'object' && content !== null) { 64 if (Array.isArray(content)) { 65 contentText = content 66 .filter((item: any) => item.type === 'text') 67 .map((item: any) => item.text || '') 68 .join(''); 69 } else if (content.text) { 70 contentText = content.text; 71 } 72 } 73 74 if (contentText) { 75 chatStore.updateStreamAssistant(contentText); 76 } 77 } 78 }, [chatStore]); 79 80 // Send a message with streaming 81 const sendMessage = useCallback( 82 async (messageText: string, imagesToSend: Array<{ uri: string; base64: string; mediaType: string }>) => { 83 if ((!messageText.trim() && imagesToSend.length === 0) || !coAgent || chatStore.isSendingMessage) { 84 return; 85 } 86 87 console.log('sendMessage called - messageText:', messageText, 'imagesToSend length:', imagesToSend.length); 88 89 chatStore.setSendingMessage(true); 90 91 // Immediately add user message to UI 92 let tempMessageContent: any; 93 if (imagesToSend.length > 0) { 94 const contentParts = []; 95 96 // Add images 97 for (const img of imagesToSend) { 98 contentParts.push({ 99 type: 'image', 100 source: { 101 type: 'base64', 102 mediaType: img.mediaType, 103 data: img.base64, 104 }, 105 }); 106 } 107 108 // Add text if present 109 if (messageText && typeof messageText === 'string' && messageText.length > 0) { 110 contentParts.push({ 111 type: 'text', 112 text: messageText, 113 }); 114 } 115 116 tempMessageContent = contentParts; 117 } else { 118 tempMessageContent = messageText; 119 } 120 121 const tempUserMessage: LettaMessage = { 122 id: `temp-${Date.now()}`, 123 role: 'user', 124 message_type: 'user_message', 125 content: tempMessageContent, 126 created_at: new Date().toISOString(), 127 } as LettaMessage; 128 129 chatStore.addMessage(tempUserMessage); 130 131 try { 132 chatStore.startStreaming(); 133 134 // Build message content 135 let messageContent: any; 136 if (imagesToSend.length > 0) { 137 const contentParts = []; 138 139 for (const img of imagesToSend) { 140 contentParts.push({ 141 type: 'image', 142 source: { 143 type: 'base64', 144 mediaType: img.mediaType, 145 data: img.base64, 146 }, 147 }); 148 } 149 150 if (messageText && typeof messageText === 'string' && messageText.length > 0) { 151 contentParts.push({ 152 type: 'text', 153 text: messageText, 154 }); 155 } 156 157 messageContent = contentParts; 158 } else { 159 messageContent = messageText; 160 } 161 162 const payload = { 163 messages: [{ role: 'user', content: messageContent }], 164 use_assistant_message: true, 165 stream_tokens: true, 166 }; 167 168 await lettaApi.sendMessageStream( 169 coAgent.id, 170 payload, 171 (chunk: StreamingChunk) => { 172 handleStreamingChunk(chunk); 173 }, 174 async (response) => { 175 console.log('Stream complete - refreshing messages from server'); 176 177 // Wait for server to finalize, then refresh messages 178 setTimeout(async () => { 179 try { 180 const currentCount = chatStore.messages.filter((msg) => !msg.id.startsWith('temp-')).length; 181 const fetchLimit = Math.max(currentCount + 10, 100); 182 183 const recentMessages = await lettaApi.listMessages(coAgent.id, { 184 limit: fetchLimit, 185 use_assistant_message: true, 186 }); 187 188 console.log('Received', recentMessages.length, 'messages from server after stream'); 189 190 // Replace all messages with server version 191 chatStore.setMessages(recentMessages); 192 } catch (error) { 193 console.error('Failed to refresh messages after stream:', error); 194 } finally { 195 chatStore.stopStreaming(); 196 chatStore.setSendingMessage(false); 197 chatStore.clearStream(); 198 chatStore.clearImages(); 199 } 200 }, 500); 201 }, 202 (error) => { 203 console.error('Stream error:', error); 204 chatStore.stopStreaming(); 205 chatStore.setSendingMessage(false); 206 chatStore.clearStream(); 207 } 208 ); 209 } catch (error) { 210 console.error('Failed to send message:', error); 211 chatStore.stopStreaming(); 212 chatStore.setSendingMessage(false); 213 chatStore.clearStream(); 214 throw error; 215 } 216 }, 217 [coAgent, chatStore, handleStreamingChunk] 218 ); 219 220 return { 221 isStreaming: chatStore.isStreaming, 222 isSendingMessage: chatStore.isSendingMessage, 223 currentStream: chatStore.currentStream, 224 completedStreamBlocks: chatStore.completedStreamBlocks, 225 226 sendMessage, 227 }; 228}