A React Native app for the ultimate thinking partner.

fix: Handle multimodal message content in MessageBubble

- Add support for array content (images + text)
- Properly render image sources from base64 data
- Delegate tool messages to ToolCallItem component
- Use MessageContent for text rendering
- Add null checks to prevent undefined errors

Fixes: TypeError: Cannot read properties of undefined (reading 'uri')

+77 -35
+77 -35
src/components/MessageBubble.tsx
··· 1 1 import React from 'react'; 2 - import { View, Text, StyleSheet } from 'react-native'; 2 + import { View, Text, StyleSheet, Image } from 'react-native'; 3 3 import { darkTheme } from '../theme'; 4 4 import { LettaMessage } from '../types/letta'; 5 + import MessageContent from './MessageContent'; 6 + import ToolCallItem from './ToolCallItem'; 5 7 6 8 interface MessageBubbleProps { 7 9 message: LettaMessage; 10 + theme?: any; 8 11 } 9 12 10 - const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => { 13 + const MessageBubble: React.FC<MessageBubbleProps> = ({ message, theme = darkTheme }) => { 11 14 const isUser = message.role === 'user'; 12 15 const isSystem = message.role === 'system'; 13 - 16 + const isTool = message.role === 'tool' || message.message_type?.includes('tool'); 17 + 14 18 const formatTime = (dateString: string) => { 15 19 const date = new Date(dateString); 16 - return date.toLocaleTimeString('en-US', { 17 - hour: '2-digit', 20 + return date.toLocaleTimeString('en-US', { 21 + hour: '2-digit', 18 22 minute: '2-digit', 19 - hour12: true 23 + hour12: true 20 24 }); 21 25 }; 22 26 ··· 40 44 ]; 41 45 }; 42 46 47 + // Render tool call messages separately 48 + if (isTool) { 49 + return ( 50 + <View style={styles.toolContainer}> 51 + <ToolCallItem message={message} theme={theme} /> 52 + </View> 53 + ); 54 + } 55 + 56 + // Render multimodal content (images + text) 57 + const renderContent = () => { 58 + if (Array.isArray(message.content)) { 59 + return ( 60 + <View> 61 + {message.content.map((item: any, index: number) => { 62 + if (item.type === 'image' && item.source) { 63 + const imageUri = item.source.type === 'base64' 64 + ? `data:${item.source.mediaType || 'image/jpeg'};base64,${item.source.data}` 65 + : item.source.url; 66 + 67 + return ( 68 + <Image 69 + key={index} 70 + source={{ uri: imageUri }} 71 + style={styles.messageImage} 72 + resizeMode="cover" 73 + /> 74 + ); 75 + } else if (item.type === 'text') { 76 + return ( 77 + <Text key={index} style={getTextStyle()}> 78 + {item.text} 79 + </Text> 80 + ); 81 + } 82 + return null; 83 + })} 84 + </View> 85 + ); 86 + } 87 + 88 + // Render regular text content 89 + if (typeof message.content === 'string') { 90 + return ( 91 + <MessageContent 92 + content={message.content} 93 + messageType={message.message_type} 94 + theme={theme} 95 + /> 96 + ); 97 + } 98 + 99 + return null; 100 + }; 101 + 43 102 return ( 44 103 <View style={[styles.container, isUser ? styles.userContainer : styles.assistantContainer]}> 45 104 <View style={getBubbleStyle()}> 46 - <Text style={getTextStyle()}> 47 - {message.content} 48 - </Text> 105 + {renderContent()} 49 106 <Text style={styles.timestamp}> 50 107 {formatTime(message.created_at)} 51 108 </Text> 52 - {message.tool_calls && message.tool_calls.length > 0 && ( 53 - <View style={styles.toolCallsContainer}> 54 - <Text style={styles.toolCallsTitle}>Tools used:</Text> 55 - {message.tool_calls.map((toolCall, index) => ( 56 - <Text key={index} style={styles.toolCallText}> 57 - • {toolCall.function.name} 58 - </Text> 59 - ))} 60 - </View> 61 - )} 62 109 </View> 63 110 </View> 64 111 ); ··· 74 121 }, 75 122 assistantContainer: { 76 123 alignItems: 'flex-start', 124 + }, 125 + toolContainer: { 126 + marginVertical: 2, 127 + marginHorizontal: 16, 77 128 }, 78 129 bubble: { 79 130 maxWidth: '80%', ··· 117 168 fontSize: 14, 118 169 fontStyle: 'italic', 119 170 }, 171 + messageImage: { 172 + width: 200, 173 + height: 200, 174 + borderRadius: 8, 175 + marginVertical: 8, 176 + }, 120 177 timestamp: { 121 178 fontSize: 12, 122 179 marginTop: 4, 123 180 opacity: 0.7, 124 - }, 125 - toolCallsContainer: { 126 - marginTop: 8, 127 - padding: 8, 128 - backgroundColor: darkTheme.colors.background.tertiary, 129 - borderRadius: 8, 130 - }, 131 - toolCallsTitle: { 132 - fontSize: 12, 133 - fontWeight: '600', 134 - marginBottom: 4, 135 - opacity: 0.8, 136 - }, 137 - toolCallText: { 138 - fontSize: 12, 139 - opacity: 0.8, 181 + color: darkTheme.colors.text.tertiary, 140 182 }, 141 183 }); 142 184