import 'package:flutter/material.dart'; import 'package:animate_do/animate_do.dart'; import 'package:iconsax/iconsax.dart'; /// Represents a single chat message bubble class MessageBubble extends StatelessWidget { final String message; final bool isUser; final String senderName; final DateTime timestamp; final int? inputTokens; final int? outputTokens; final double? estimatedCost; const MessageBubble({ super.key, required this.message, required this.isUser, required this.senderName, required this.timestamp, this.inputTokens, this.outputTokens, this.estimatedCost, }); @override Widget build(BuildContext context) { final ColorScheme colorScheme = Theme.of(context).colorScheme; return FadeInUp( duration: const Duration(milliseconds: 300), child: Align( alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.7, ), child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ // Sender name and timestamp Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( senderName, style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 8), Text( _formatTimestamp(timestamp), style: TextStyle( fontSize: 10, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), ), ), ], ), ), // Message bubble Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), decoration: BoxDecoration( color: isUser ? colorScheme.primary.withValues(alpha: 0.15) : colorScheme.secondary.withValues(alpha: 0.15), 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), ), border: Border.all( color: isUser ? colorScheme.primary.withValues(alpha: 0.3) : colorScheme.secondary.withValues(alpha: 0.3), width: 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( message, style: TextStyle( fontSize: 14, color: colorScheme.onSurface, height: 1.4, ), ), // Token metrics (only for agent responses) if (!isUser && (inputTokens != null || outputTokens != null)) Padding( padding: const EdgeInsets.only(top: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Iconsax.cpu, size: 10, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( '${inputTokens ?? 0} in', style: TextStyle( fontSize: 10, color: colorScheme.onSurfaceVariant, fontFamily: 'IBM Plex Mono', ), ), const SizedBox(width: 4), Text( '•', style: TextStyle( fontSize: 10, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 4), Text( '${outputTokens ?? 0} out', style: TextStyle( fontSize: 10, color: colorScheme.onSurfaceVariant, fontFamily: 'IBM Plex Mono', ), ), if (estimatedCost != null) ...[ const SizedBox(width: 4), Text( '•', style: TextStyle( fontSize: 10, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 4), Text( '\$${estimatedCost!.toStringAsFixed(4)}', style: TextStyle( fontSize: 10, color: colorScheme.secondary, fontFamily: 'IBM Plex Mono', fontWeight: FontWeight.w600, ), ), ], ], ), ), ], ), ), ], ), ), ), ); } String _formatTimestamp(DateTime dt) { final DateTime now = DateTime.now(); final Duration diff = now.difference(dt); if (diff.inMinutes < 1) { return 'Just now'; } else if (diff.inHours < 1) { return '${diff.inMinutes}m ago'; } else if (diff.inDays < 1) { return '${diff.inHours}h ago'; } else { return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } } }