import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import '../../../../core/theme/app_colors.dart'; import '../../domain/entities/chat_message.dart'; /// Renders a single chat message bubble with appropriate styling /// for user and assistant messages. Assistant messages render Markdown. class MessageBubble extends StatelessWidget { final ChatMessage message; const MessageBubble({super.key, required this.message}); bool get _isUser => message.role == MessageRole.user; @override Widget build(BuildContext context) { return Align( alignment: _isUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.78, ), decoration: BoxDecoration( color: _isUser ? AppColors.primary : AppColors.surface, borderRadius: BorderRadius.only( topLeft: const Radius.circular(16), topRight: const Radius.circular(16), bottomLeft: Radius.circular(_isUser ? 16 : 4), bottomRight: Radius.circular(_isUser ? 4 : 16), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Role label for assistant messages if (!_isUser && message.type == MessageType.thinking) Padding( padding: const EdgeInsets.only(bottom: 4), child: Text( '思考中...', style: TextStyle( color: AppColors.textMuted, fontSize: 11, fontStyle: FontStyle.italic, ), ), ), // Message content β€” Markdown for assistant, plain text for user if (_isUser) SelectableText( message.content, style: const TextStyle( color: Colors.white, fontSize: 15, ), ) else MarkdownBody( data: message.content, selectable: true, styleSheet: _markdownStyleSheet(context), ), // Timestamp Padding( padding: const EdgeInsets.only(top: 4), child: Text( _formatTime(message.timestamp), style: TextStyle( color: _isUser ? Colors.white.withOpacity(0.6) : AppColors.textMuted, fontSize: 10, ), ), ), ], ), ), ); } MarkdownStyleSheet _markdownStyleSheet(BuildContext context) { return MarkdownStyleSheet( p: const TextStyle(color: AppColors.textPrimary, fontSize: 15), h1: const TextStyle(color: AppColors.textPrimary, fontSize: 22, fontWeight: FontWeight.bold), h2: const TextStyle(color: AppColors.textPrimary, fontSize: 19, fontWeight: FontWeight.bold), h3: const TextStyle(color: AppColors.textPrimary, fontSize: 17, fontWeight: FontWeight.w600), strong: const TextStyle(color: AppColors.textPrimary, fontWeight: FontWeight.bold), em: const TextStyle(color: AppColors.textSecondary, fontStyle: FontStyle.italic), code: TextStyle( color: AppColors.secondary, backgroundColor: AppColors.background.withOpacity(0.5), fontSize: 13, fontFamily: 'monospace', ), codeblockDecoration: BoxDecoration( color: AppColors.background.withOpacity(0.6), borderRadius: BorderRadius.circular(8), ), codeblockPadding: const EdgeInsets.all(10), blockquoteDecoration: BoxDecoration( border: Border(left: BorderSide(color: AppColors.primary, width: 3)), ), blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), tableBorder: TableBorder.all(color: AppColors.surfaceLight, width: 0.5), tableHead: const TextStyle(color: AppColors.textPrimary, fontWeight: FontWeight.bold, fontSize: 13), tableBody: const TextStyle(color: AppColors.textSecondary, fontSize: 13), tableCellsPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), listBullet: const TextStyle(color: AppColors.textSecondary, fontSize: 15), ); } String _formatTime(DateTime time) { final hour = time.hour.toString().padLeft(2, '0'); final minute = time.minute.toString().padLeft(2, '0'); return '$hour:$minute'; } }