import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { Ionicons } from '@expo/vector-icons'; import { Image } from 'expo-image'; import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Markdown from 'react-native-markdown-display'; import { ChatMessage, MessageAttachment } from './types'; interface MessageAttachmentComponentProps { attachment: MessageAttachment; onPreview?: (uri: string) => void; } const MessageAttachmentComponent: React.FC = ({ attachment, onPreview }) => { const colorScheme = useColorScheme(); const theme = Colors[colorScheme ?? 'light']; if (attachment.type === 'image') { return ( onPreview?.(attachment.url)} activeOpacity={0.8} > {attachment.uploadProgress !== undefined && attachment.uploadProgress < 1 && ( {Math.round(attachment.uploadProgress * 100)}% )} {attachment.uploadError && ( 上传失败 )} ); } if (attachment.type === 'video') { return ( {attachment.filename && ( {attachment.filename} )} ); } if (attachment.type === 'file') { return ( {attachment.filename || '未知文件'} ); } return null; }; interface ChatMessageComponentProps { message: ChatMessage; onPreviewImage?: (uri: string) => void; onChoiceSelect?: (messageId: string, choiceId: string) => void; selectedChoices?: Record; pendingChoiceConfirmation?: Record; isStreaming?: boolean; onCancelStream?: () => void; } const ChatMessageComponent: React.FC = ({ message, onPreviewImage, onChoiceSelect, selectedChoices, pendingChoiceConfirmation, isStreaming, onCancelStream }) => { const colorScheme = useColorScheme(); const theme = Colors[colorScheme ?? 'light']; const isUser = message.role === 'user'; const isAssistant = message.role === 'assistant'; return ( {isAssistant && ( AI )} {/* 文本内容 */} {message.content ? ( isUser ? ( {message.content} ) : ( {message.content} ) ) : null} {/* 附件 */} {message.attachments && message.attachments.length > 0 && ( {message.attachments.map((attachment) => ( ))} )} {/* 选择选项 */} {message.choices && message.choices.length > 0 && ( {message.choices.map((choice) => { const isSelected = selectedChoices?.[message.id] === choice.id; const isLoading = pendingChoiceConfirmation?.[message.id]; const isDisabled = isLoading || (selectedChoices?.[message.id] && !isSelected) || false; return ( onChoiceSelect?.(message.id, choice.id)} disabled={isDisabled} > {choice.emoji ? `${choice.emoji} ` : ''}{choice.label} {!!choice.recommended && !isSelected && ( 推荐 )} {isSelected && ( 已选择 )} ); })} )} {/* 流式回复控制 */} {isAssistant && isStreaming && !message.content && ( 正在思考... 停止 )} ); }; const styles = StyleSheet.create({ row: { flexDirection: 'row', alignItems: 'flex-end', gap: 8, marginVertical: 6, }, userRow: { justifyContent: 'flex-end', }, avatar: { width: 28, height: 28, borderRadius: 14, alignItems: 'center', justifyContent: 'center', }, avatarText: { color: '#192126', fontSize: 12, fontWeight: '800', }, bubble: { paddingHorizontal: 12, paddingVertical: 10, borderRadius: 16, }, bubbleText: { fontSize: 15, lineHeight: 22, }, attachmentsContainer: { marginTop: 8, gap: 6, }, imageAttachment: { borderRadius: 12, overflow: 'hidden', position: 'relative', }, attachmentImage: { width: '100%', minHeight: 120, maxHeight: 200, borderRadius: 12, }, attachmentProgressOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.4)', alignItems: 'center', justifyContent: 'center', borderRadius: 12, }, attachmentProgressText: { color: '#fff', fontSize: 14, fontWeight: '600', }, attachmentErrorOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(255,0,0,0.4)', alignItems: 'center', justifyContent: 'center', borderRadius: 12, }, attachmentErrorText: { color: '#fff', fontSize: 12, fontWeight: '600', }, videoAttachment: { borderRadius: 12, overflow: 'hidden', backgroundColor: 'rgba(0,0,0,0.1)', }, videoPlaceholder: { height: 120, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.6)', }, videoFilename: { color: '#fff', fontSize: 12, marginTop: 4, textAlign: 'center', }, fileAttachment: { flexDirection: 'row', alignItems: 'center', padding: 12, backgroundColor: 'rgba(0,0,0,0.06)', borderRadius: 8, gap: 8, }, fileFilename: { flex: 1, fontSize: 14, }, choicesContainer: { gap: 8, width: '100%', marginTop: 8, }, choiceButton: { backgroundColor: 'rgba(255,255,255,0.9)', borderWidth: 1, borderColor: '#7a5af84d', borderRadius: 12, padding: 12, width: '100%', minWidth: 0, }, choiceButtonRecommended: { borderColor: '#7a5af899', backgroundColor: '#7a5af81a', }, choiceButtonSelected: { borderColor: '#19b36e', backgroundColor: '#19b36e33', borderWidth: 2, }, choiceButtonDisabled: { backgroundColor: 'rgba(0,0,0,0.05)', borderColor: 'rgba(0,0,0,0.1)', opacity: 0.5, }, choiceContent: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', width: '100%', }, choiceLabel: { fontSize: 15, fontWeight: '600', color: '#192126', flex: 1, flexWrap: 'wrap', }, choiceLabelRecommended: { color: '#19b36e', }, choiceLabelSelected: { color: '#19b36e', fontWeight: '700', }, choiceLabelDisabled: { color: '#687076', }, choiceStatusContainer: { flexDirection: 'row', alignItems: 'center', gap: 8, }, recommendedBadge: { backgroundColor: '#7a5af8cc', borderRadius: 6, paddingHorizontal: 8, paddingVertical: 2, }, recommendedText: { fontSize: 12, fontWeight: '700', color: '#19b36e', }, selectedBadge: { backgroundColor: '#19b36e', borderRadius: 6, paddingHorizontal: 8, paddingVertical: 2, }, selectedText: { fontSize: 12, fontWeight: '700', color: '#FFFFFF', }, streamingContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 12, }, cancelStreamBtn: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, backgroundColor: 'rgba(255,68,68,0.1)', borderWidth: 1, borderColor: 'rgba(255,68,68,0.3)', }, cancelStreamText: { fontSize: 12, fontWeight: '600', color: '#FF4444', }, }); export default ChatMessageComponent;