import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import '../../../../core/services/rag_service.dart'; import '../../../../core/theme/app_theme_extension.dart'; /// Widget for displaying chat messages with source citations class MessageBubble extends StatelessWidget { final String content; final bool isUser; final DateTime timestamp; final List? sources; final double? confidence; final bool showSources; final VoidCallback? onSourceTap; const MessageBubble({ super.key, required this.content, required this.isUser, required this.timestamp, this.sources, this.confidence, this.showSources = true, this.onSourceTap, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isUser) ...[ _buildAvatar(context), const SizedBox(width: 12), ], Flexible( child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ _buildMessageBubble(context), if (!isUser && showSources && sources != null && sources!.isNotEmpty) ...[ const SizedBox(height: 8), _buildSourceCitations(context), ], if (!isUser && confidence != null) ...[ const SizedBox(height: 4), _buildConfidenceIndicator(context), ], ], ), ), if (isUser) ...[ const SizedBox(width: 12), _buildAvatar(context), ], ], ), const SizedBox(height: 4), _buildTimestamp(context), ], ), ).animate().fadeIn(duration: const Duration(milliseconds: 300)).slideY( begin: isUser ? 0.1 : -0.1, end: 0, duration: const Duration(milliseconds: 400), ); } Widget _buildAvatar(BuildContext context) { final cs = Theme.of(context).colorScheme; final extras = AppThemeExtras.of(context); final accent = isUser ? cs.primary : cs.secondary; return Container( width: 36, height: 36, decoration: BoxDecoration( gradient: isUser ? LinearGradient( colors: [ extras.actionCardGradientStart, extras.actionCardGradientEnd, ], ) : LinearGradient( colors: [cs.secondary, cs.secondary.withOpacity(0.85)], ), borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: accent.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Icon( isUser ? Icons.person : Icons.school, color: Colors.white, size: 20, ), ); } Widget _buildMessageBubble(BuildContext context) { final cs = Theme.of(context).colorScheme; final extras = AppThemeExtras.of(context); return Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75, ), padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( gradient: isUser ? LinearGradient( colors: [ extras.actionCardGradientStart, extras.actionCardGradientEnd, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ) : LinearGradient( colors: [ cs.surfaceContainerHighest, cs.surface, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.only( topLeft: const Radius.circular(20), topRight: const Radius.circular(20), bottomLeft: isUser ? const Radius.circular(20) : const Radius.circular(4), bottomRight: isUser ? const Radius.circular(4) : const Radius.circular(20), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ isUser ? Text( content, style: TextStyle( color: Colors.white, fontSize: 16, height: 1.4, fontWeight: FontWeight.w500, ), ) : MarkdownBody( data: content, styleSheet: MarkdownStyleSheet( p: TextStyle( color: cs.onSurface, fontSize: 16, height: 1.4, ), strong: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.bold, height: 1.4, ), em: TextStyle( color: cs.onSurface, fontSize: 16, fontStyle: FontStyle.italic, height: 1.4, ), listBullet: TextStyle( color: cs.onSurface, fontSize: 16, height: 1.4, ), ), ), ], ), ); } Widget _buildSourceCitations(BuildContext context) { return Container( padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.grey[200]!, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.source, size: 16, color: Colors.grey[600], ), const SizedBox(width: 6), Text( 'Fontes', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), const Spacer(), Text( '${sources!.length} ${sources!.length == 1 ? 'fonte' : 'fontes'}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), ], ), const SizedBox(height: 8), ...sources!.take(3).map((source) => _buildSourceItem(context, source)), if (sources!.length > 3) ...[ const SizedBox(height: 4), Text( '+${sources!.length - 3} mais fontes...', style: TextStyle( fontSize: 11, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], ], ), ); } Widget _buildSourceItem(BuildContext context, SourceCitation source) { return Padding( padding: const EdgeInsets.only(bottom: 6.0), child: InkWell( onTap: () => onSourceTap?.call(), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all( color: Colors.grey[200]!, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.menu_book, size: 14, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 6), Expanded( child: Text( source.title, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.grey[800], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Text( '${(source.relevance * 100).toInt()}%', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), ), ], ), const SizedBox(height: 4), Row( children: [ Icon( Icons.category, size: 12, color: Colors.grey[600], ), const SizedBox(width: 4), Text( '${source.subject} • ${source.concept}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), if (source.pageNumber != null) ...[ const Spacer(), Icon( Icons.book, size: 12, color: Colors.grey[600], ), const SizedBox(width: 2), Text( 'p. ${source.pageNumber}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), ], ], ), ], ), ), ), ); } Widget _buildConfidenceIndicator(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.verified, size: 14, color: _getConfidenceColor(confidence!), ), const SizedBox(width: 4), Text( 'Confiança: ${(confidence! * 100).toInt()}%', style: TextStyle( fontSize: 11, color: _getConfidenceColor(confidence!), fontWeight: FontWeight.w600, ), ), ], ); } Color _getConfidenceColor(double confidence) { if (confidence >= 0.8) return Colors.green; if (confidence >= 0.6) return Colors.orange; return Colors.red; } Widget _buildTimestamp(BuildContext context) { return Padding( padding: EdgeInsets.only( left: isUser ? 0 : 48, right: isUser ? 48 : 0, ), child: Text( _formatTimestamp(timestamp), style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), ); } String _formatTimestamp(DateTime timestamp) { final now = DateTime.now(); final difference = now.difference(timestamp); if (difference.inMinutes < 1) { return 'Agora'; } else if (difference.inMinutes < 60) { return '${difference.inMinutes} min atrás'; } else if (difference.inHours < 24) { return '${difference.inHours} h atrás'; } else { return '${difference.inDays} dias atrás'; } } }