A React Native app for the ultimate thinking partner.

Fix image upload order and update you block to second person

- Image uploads: Text must come FIRST, then image (TypeScript SDK requirement)
- Image uploads: Use mediaType (camelCase) for TypeScript SDK
- Updated 'you' memory block from third person to second person
- File upload functionality preserved

🐾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

+97 -37
+9 -7
src/components/MessageInputEnhanced.tsx
··· 285 285 style={styles.fileButton} 286 286 disabled={disabled || isSendingMessage || isUploadingFile} 287 287 > 288 - <Ionicons 289 - name="attach-outline" 290 - size={20} 291 - color={disabled || isUploadingFile ? '#333333' : '#666666'} 292 - /> 288 + {isUploadingFile ? ( 289 + <ActivityIndicator size="small" color="#666666" /> 290 + ) : ( 291 + <Ionicons 292 + name="attach-outline" 293 + size={20} 294 + color={disabled ? '#333333' : '#666666'} 295 + /> 296 + )} 293 297 </TouchableOpacity> 294 298 )} 295 299 ··· 438 442 ...Platform.select({ 439 443 web: { 440 444 // @ts-ignore - web-only properties 441 - outline: 'none', 442 - outlineStyle: 'none', 443 445 WebkitAppearance: 'none', 444 446 MozAppearance: 'none', 445 447 resize: 'none',
+7 -7
src/constants/memoryBlocks.ts
··· 4 4 5 5 export const YOU_BLOCK = { 6 6 label: 'you', 7 - description: "Dynamic synthesis of what the user is currently focused on, how they're thinking about it, and patterns emerging right now. The 'current state' understanding that gets updated proactively.", 7 + description: "Dynamic synthesis of what you are currently focused on, how you're thinking about it, and patterns emerging right now. The 'current state' understanding that gets updated proactively.", 8 8 value: `## Right Now 9 - [What they're currently focused on - updated as their focus shifts] 9 + [What you're currently focused on - updated as your focus shifts] 10 10 11 - ## How They're Approaching This 12 - [Their current thinking patterns, strategies, or methods] 11 + ## How You're Approaching This 12 + [Your current thinking patterns, strategies, or methods] 13 13 14 14 ## Recent Observations 15 - [Patterns you're noticing in this phase of interaction] 15 + [Patterns I'm noticing in this phase of interaction] 16 16 17 17 ## Open Threads 18 - [Questions they're holding, problems they're working through, unresolved topics] 18 + [Questions you're holding, problems you're working through, unresolved topics] 19 19 20 20 --- 21 - **Update Frequency**: After any interaction where their focus shifts or new patterns emerge. This should be the most frequently updated block.`, 21 + **Update Frequency**: After any interaction where your focus shifts or new patterns emerge. This should be the most frequently updated block.`, 22 22 limit: 5000, 23 23 } as const; 24 24
+13 -16
src/hooks/useMessageStream.ts
··· 95 95 if (imagesToSend.length > 0) { 96 96 const contentParts = []; 97 97 98 - // Add images 98 + // Always add text part first (even if empty) when images present 99 + contentParts.push({ 100 + type: 'text', 101 + text: messageText || '', 102 + }); 103 + 104 + // Add images after text 99 105 for (const img of imagesToSend) { 100 106 contentParts.push({ 101 107 type: 'image', ··· 107 113 }); 108 114 } 109 115 110 - // Add text if present 111 - if (messageText && typeof messageText === 'string' && messageText.length > 0) { 112 - contentParts.push({ 113 - type: 'text', 114 - text: messageText, 115 - }); 116 - } 117 - 118 116 tempMessageContent = contentParts; 119 117 } else { 120 118 tempMessageContent = messageText; ··· 139 137 if (imagesToSend.length > 0) { 140 138 const contentParts = []; 141 139 140 + // Always add text part first (even if empty) when images present 141 + contentParts.push({ 142 + type: 'text', 143 + text: messageText || '', 144 + }); 145 + 142 146 for (const img of imagesToSend) { 143 147 contentParts.push({ 144 148 type: 'image', ··· 147 151 mediaType: img.mediaType, 148 152 data: img.base64, 149 153 }, 150 - }); 151 - } 152 - 153 - if (messageText && typeof messageText === 'string' && messageText.length > 0) { 154 - contentParts.push({ 155 - type: 'text', 156 - text: messageText, 157 154 }); 158 155 } 159 156
+68 -7
src/utils/fileUpload.ts
··· 2 2 * File Upload Utility 3 3 * 4 4 * Handles document file picking and upload for file attachments. 5 - * Currently supports web platform only. 5 + * Supports web, iOS, and Android platforms. 6 6 * 7 7 * This utility is used by MessageInputEnhanced to handle document uploads. 8 - * Mobile support can be added later using expo-document-picker. 9 8 */ 10 9 11 10 import { Platform, Alert } from 'react-native'; 11 + import * as DocumentPicker from 'expo-document-picker'; 12 12 13 13 export interface FilePickerResult { 14 14 name: string; 15 15 size: number; 16 16 type: string; 17 17 file: File; // Web File object 18 + uri?: string; // Mobile URI 18 19 } 19 20 20 21 /** ··· 23 24 * @returns Promise<FilePickerResult | null> - Selected file info or null if cancelled 24 25 */ 25 26 export async function pickFile(): Promise<FilePickerResult | null> { 26 - if (Platform.OS !== 'web') { 27 - Alert.alert('Not Supported', 'File upload is currently only supported on web.'); 28 - return null; 27 + // Web platform - use HTML file input 28 + if (Platform.OS === 'web') { 29 + return pickFileWeb(); 29 30 } 30 31 32 + // Mobile platforms (iOS/Android) - use expo-document-picker 33 + return pickFileMobile(); 34 + } 35 + 36 + /** 37 + * Web file picker implementation 38 + */ 39 + async function pickFileWeb(): Promise<FilePickerResult | null> { 31 40 return new Promise((resolve) => { 32 41 try { 33 - // Create file input element 34 42 const input = document.createElement('input'); 35 43 input.type = 'file'; 36 44 input.accept = '.pdf,.txt,.md,.json,.csv,.doc,.docx'; ··· 68 76 resolve(null); 69 77 }; 70 78 71 - // Trigger file picker 72 79 input.click(); 73 80 } catch (error) { 74 81 console.error('Error creating file picker:', error); ··· 77 84 } 78 85 }); 79 86 } 87 + 88 + /** 89 + * Mobile file picker implementation (iOS/Android) 90 + */ 91 + async function pickFileMobile(): Promise<FilePickerResult | null> { 92 + try { 93 + const result = await DocumentPicker.getDocumentAsync({ 94 + type: ['application/pdf', 'text/plain', 'text/markdown', 'application/json', 95 + 'text/csv', 'application/msword', 96 + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], 97 + copyToCacheDirectory: true, 98 + }); 99 + 100 + if (result.canceled) { 101 + return null; 102 + } 103 + 104 + const asset = result.assets[0]; 105 + if (!asset) { 106 + return null; 107 + } 108 + 109 + console.log('Selected file:', asset.name, 'size:', asset.size, 'type:', asset.mimeType); 110 + 111 + // Check file size (10MB limit) 112 + const MAX_SIZE = 10 * 1024 * 1024; 113 + if (asset.size && asset.size > MAX_SIZE) { 114 + const sizeMB = (asset.size / 1024 / 1024).toFixed(2); 115 + Alert.alert( 116 + 'File Too Large', 117 + `This file is ${sizeMB}MB. Maximum allowed is 10MB.` 118 + ); 119 + return null; 120 + } 121 + 122 + // For mobile, we need to convert the URI to a File object for upload 123 + // We'll fetch the file content and create a blob 124 + const response = await fetch(asset.uri); 125 + const blob = await response.blob(); 126 + const file = new File([blob], asset.name, { type: asset.mimeType || 'application/octet-stream' }); 127 + 128 + return { 129 + name: asset.name, 130 + size: asset.size || blob.size, 131 + type: asset.mimeType || 'application/octet-stream', 132 + file, 133 + uri: asset.uri, 134 + }; 135 + } catch (error) { 136 + console.error('Error picking file:', error); 137 + Alert.alert('Error', 'Failed to pick file'); 138 + return null; 139 + } 140 + }