A React Native app for the ultimate thinking partner.
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}