import { ChatMessage } from '@/store/useAppStore'; import Markdown from '@ronradtke/react-native-markdown-display'; import { Bot, User } from 'lucide-react-native'; import React, { useState } from 'react'; import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'; import { MessageActions } from './MessageActions'; interface MessageBubbleProps { message: ChatMessage; onEdit?: () => void; onRegenerate?: () => void; onDelete?: () => void; isStreaming?: boolean; isLastAssistant?: boolean; } export function MessageBubble({ message, onEdit, onRegenerate, onDelete, isStreaming, isLastAssistant, }: MessageBubbleProps) { const { width } = useWindowDimensions(); const isLargeScreen = width > 768; const isUser = message.role === 'user'; const [showActions, setShowActions] = useState(false); const showingStreaming = isStreaming && isLastAssistant; const shouldShowActions = !isUser || showActions; const renderContent = () => { if (isUser) { return ( {message.content} ); } if (!message.content && showingStreaming) { return null; // TypingIndicator handled externally } return ( {message.content} ); }; return ( isUser && setShowActions(true)} > {/* Avatar */} {!isUser && ( )} {renderContent()} {isUser && ( )} {/* Timestamp + edited badge */} {!isUser && } {new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {message.isEdited ? ' ยท edited' : ''} {/* Action buttons */} {(shouldShowActions || !isUser) && message.content.length > 0 && ( )} ); } const styles = StyleSheet.create({ container: { width: '100%', paddingHorizontal: 16, marginTop: 12, flexDirection: 'row', alignItems: 'flex-end', gap: 8, }, containerUser: { justifyContent: 'flex-end', }, containerAssistant: { justifyContent: 'flex-start', }, avatarAssistant: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#F3F4F6', alignItems: 'center', justifyContent: 'center', marginBottom: 2, }, avatarUser: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#374151', alignItems: 'center', justifyContent: 'center', marginBottom: 2, }, bubble: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: 18, }, bubbleUser: { backgroundColor: '#111827', borderBottomRightRadius: 4, }, bubbleAssistant: { backgroundColor: '#F3F4F6', borderBottomLeftRadius: 4, }, text: { fontSize: 15, lineHeight: 22, }, textUser: { color: '#FFFFFF', }, metaRow: { flexDirection: 'row', paddingHorizontal: 16, marginTop: 2, gap: 8, }, metaRowUser: { justifyContent: 'flex-end', paddingRight: 52, }, metaRowAssistant: { justifyContent: 'flex-start', }, timestamp: { fontSize: 11, color: '#9CA3AF', }, }); const markdownStyles = StyleSheet.create({ body: { fontSize: 15, lineHeight: 22, color: '#111827', }, strong: { fontWeight: '700', }, em: { fontStyle: 'italic', }, code_inline: { backgroundColor: '#E5E7EB', color: '#1F2937', paddingHorizontal: 5, paddingVertical: 1, borderRadius: 4, fontSize: 13, fontFamily: 'monospace', }, fence: { backgroundColor: '#1F2937', color: '#E5E7EB', padding: 12, borderRadius: 8, fontSize: 13, fontFamily: 'monospace', marginVertical: 8, overflow: 'hidden', }, code_block: { backgroundColor: '#1F2937', color: '#E5E7EB', padding: 12, borderRadius: 8, fontSize: 13, fontFamily: 'monospace', marginVertical: 8, }, blockquote: { borderLeftWidth: 3, borderLeftColor: '#D1D5DB', paddingLeft: 12, marginVertical: 4, color: '#6B7280', }, bullet_list: { marginVertical: 4, }, ordered_list: { marginVertical: 4, }, list_item: { marginVertical: 2, }, heading1: { fontSize: 22, fontWeight: '700', marginVertical: 8, color: '#111827', }, heading2: { fontSize: 19, fontWeight: '700', marginVertical: 6, color: '#111827', }, heading3: { fontSize: 17, fontWeight: '600', marginVertical: 4, color: '#111827', }, link: { color: '#2563EB', textDecorationLine: 'underline', }, hr: { backgroundColor: '#E5E7EB', height: 1, marginVertical: 12, }, paragraph: { marginVertical: 2, }, });