| | import 'package:flutter/material.dart'; |
| | import 'package:flutter/services.dart'; |
| | import '../models/conversation.dart'; |
| |
|
| | class MessageBubble extends StatelessWidget { |
| | final Message message; |
| | final Function(String)? onCopy; |
| | |
| | const MessageBubble({ |
| | super.key, |
| | required this.message, |
| | this.onCopy, |
| | }); |
| |
|
| | @override |
| | Widget build(BuildContext context) { |
| | final theme = Theme.of(context); |
| | final isUser = message.isUser; |
| | |
| | return Padding( |
| | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), |
| | child: Row( |
| | mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, |
| | crossAxisAlignment: CrossAxisAlignment.start, |
| | children: [ |
| | if (!isUser) ...[ |
| | CircleAvatar( |
| | radius: 16, |
| | backgroundColor: theme.colorScheme.primary, |
| | child: Icon( |
| | _getMessageIcon(), |
| | color: Colors.white, |
| | size: 16, |
| | ), |
| | ), |
| | const SizedBox(width: 8), |
| | ], |
| | |
| | Flexible( |
| | child: Container( |
| | constraints: BoxConstraints( |
| | maxWidth: MediaQuery.of(context).size.width * 0.75, |
| | ), |
| | child: Column( |
| | crossAxisAlignment: isUser |
| | ? CrossAxisAlignment.end |
| | : CrossAxisAlignment.start, |
| | children: [ |
| | |
| | GestureDetector( |
| | onLongPress: () => _showMessageOptions(context), |
| | child: Container( |
| | padding: const EdgeInsets.symmetric( |
| | horizontal: 16, |
| | vertical: 12, |
| | ), |
| | decoration: BoxDecoration( |
| | color: isUser |
| | ? theme.colorScheme.primary |
| | : theme.colorScheme.surfaceVariant, |
| | borderRadius: BorderRadius.circular(20).copyWith( |
| | bottomLeft: isUser ? const Radius.circular(20) : const Radius.circular(4), |
| | bottomRight: isUser ? const Radius.circular(4) : const Radius.circular(20), |
| | ), |
| | ), |
| | child: Column( |
| | crossAxisAlignment: CrossAxisAlignment.start, |
| | children: [ |
| | |
| | SelectableText( |
| | message.displayContent, |
| | style: theme.textTheme.bodyMedium?.copyWith( |
| | color: isUser |
| | ? Colors.white |
| | : theme.colorScheme.onSurfaceVariant, |
| | ), |
| | ), |
| | |
| | |
| | if (!isUser && _hasMetadata()) ...[ |
| | const SizedBox(height: 8), |
| | _buildMetadata(context), |
| | ], |
| | ], |
| | ), |
| | ), |
| | ), |
| | |
| | |
| | Padding( |
| | padding: const EdgeInsets.only(top: 4), |
| | child: Row( |
| | mainAxisSize: MainAxisSize.min, |
| | children: [ |
| | Text( |
| | _formatTimestamp(), |
| | style: theme.textTheme.bodySmall?.copyWith( |
| | color: theme.colorScheme.onSurface.withOpacity(0.6), |
| | ), |
| | ), |
| | if (!isUser && message.confidence != null) ...[ |
| | const SizedBox(width: 4), |
| | Icon( |
| | _getConfidenceIcon(), |
| | size: 12, |
| | color: _getConfidenceColor(theme), |
| | ), |
| | ], |
| | ], |
| | ), |
| | ), |
| | ], |
| | ), |
| | ), |
| | ), |
| | |
| | if (isUser) ...[ |
| | const SizedBox(width: 8), |
| | CircleAvatar( |
| | radius: 16, |
| | backgroundColor: theme.colorScheme.secondary, |
| | child: const Icon( |
| | Icons.person, |
| | color: Colors.white, |
| | size: 16, |
| | ), |
| | ), |
| | ], |
| | ], |
| | ), |
| | ); |
| | } |
| |
|
| | IconData _getMessageIcon() { |
| | switch (message.type) { |
| | case MessageType.voice: |
| | return Icons.mic; |
| | case MessageType.image: |
| | return Icons.camera_alt; |
| | case MessageType.error: |
| | return Icons.error; |
| | case MessageType.system: |
| | return Icons.settings; |
| | default: |
| | return Icons.psychology; |
| | } |
| | } |
| |
|
| | bool _hasMetadata() { |
| | return message.model != null || |
| | message.confidence != null || |
| | message.processingTime != null; |
| | } |
| |
|
| | Widget _buildMetadata(BuildContext context) { |
| | final theme = Theme.of(context); |
| | |
| | return Container( |
| | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), |
| | decoration: BoxDecoration( |
| | color: theme.colorScheme.surface.withOpacity(0.3), |
| | borderRadius: BorderRadius.circular(8), |
| | ), |
| | child: Row( |
| | mainAxisSize: MainAxisSize.min, |
| | children: [ |
| | if (message.model != null) ...[ |
| | Icon( |
| | Icons.memory, |
| | size: 12, |
| | color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), |
| | ), |
| | const SizedBox(width: 4), |
| | Text( |
| | message.model!, |
| | style: theme.textTheme.bodySmall?.copyWith( |
| | color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), |
| | fontSize: 10, |
| | ), |
| | ), |
| | ], |
| | if (message.processingTime != null) ...[ |
| | if (message.model != null) const SizedBox(width: 8), |
| | Icon( |
| | Icons.speed, |
| | size: 12, |
| | color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), |
| | ), |
| | const SizedBox(width: 4), |
| | Text( |
| | '${message.processingTime}ms', |
| | style: theme.textTheme.bodySmall?.copyWith( |
| | color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), |
| | fontSize: 10, |
| | ), |
| | ), |
| | ], |
| | ], |
| | ), |
| | ); |
| | } |
| |
|
| | IconData _getConfidenceIcon() { |
| | final confidence = message.confidence ?? 0.0; |
| | if (confidence > 0.8) return Icons.check_circle; |
| | if (confidence > 0.5) return Icons.help; |
| | return Icons.warning; |
| | } |
| |
|
| | Color _getConfidenceColor(ThemeData theme) { |
| | final confidence = message.confidence ?? 0.0; |
| | if (confidence > 0.8) return Colors.green; |
| | if (confidence > 0.5) return Colors.orange; |
| | return Colors.red; |
| | } |
| |
|
| | String _formatTimestamp() { |
| | final now = DateTime.now(); |
| | final messageTime = message.timestamp; |
| | |
| | if (now.day == messageTime.day && |
| | now.month == messageTime.month && |
| | now.year == messageTime.year) { |
| | |
| | return '${messageTime.hour.toString().padLeft(2, '0')}:${messageTime.minute.toString().padLeft(2, '0')}'; |
| | } else { |
| | |
| | return '${messageTime.day}/${messageTime.month} ${messageTime.hour.toString().padLeft(2, '0')}:${messageTime.minute.toString().padLeft(2, '0')}'; |
| | } |
| | } |
| |
|
| | void _showMessageOptions(BuildContext context) { |
| | showModalBottomSheet( |
| | context: context, |
| | builder: (context) => SafeArea( |
| | child: Column( |
| | mainAxisSize: MainAxisSize.min, |
| | children: [ |
| | ListTile( |
| | leading: const Icon(Icons.copy), |
| | title: const Text('Copy message'), |
| | onTap: () { |
| | Navigator.of(context).pop(); |
| | onCopy?.call(message.content); |
| | }, |
| | ), |
| | if (!message.isUser && message.model != null) ...[ |
| | ListTile( |
| | leading: const Icon(Icons.info), |
| | title: const Text('Message details'), |
| | onTap: () { |
| | Navigator.of(context).pop(); |
| | _showMessageDetails(context); |
| | }, |
| | ), |
| | ], |
| | ], |
| | ), |
| | ), |
| | ); |
| | } |
| |
|
| | void _showMessageDetails(BuildContext context) { |
| | showDialog( |
| | context: context, |
| | builder: (context) => AlertDialog( |
| | title: const Text('Message Details'), |
| | content: Column( |
| | mainAxisSize: MainAxisSize.min, |
| | crossAxisAlignment: CrossAxisAlignment.start, |
| | children: [ |
| | if (message.model != null) ...[ |
| | Text('Model: ${message.model}'), |
| | const SizedBox(height: 8), |
| | ], |
| | if (message.confidence != null) ...[ |
| | Text('Confidence: ${(message.confidence! * 100).toStringAsFixed(1)}%'), |
| | const SizedBox(height: 8), |
| | ], |
| | if (message.processingTime != null) ...[ |
| | Text('Processing Time: ${message.processingTime}ms'), |
| | const SizedBox(height: 8), |
| | ], |
| | Text('Type: ${message.type.name}'), |
| | const SizedBox(height: 8), |
| | Text('Timestamp: ${message.timestamp}'), |
| | if (message.metadata.isNotEmpty) ...[ |
| | const SizedBox(height: 8), |
| | const Text('Metadata:'), |
| | Text( |
| | message.metadata.toString(), |
| | style: Theme.of(context).textTheme.bodySmall, |
| | ), |
| | ], |
| | ], |
| | ), |
| | actions: [ |
| | TextButton( |
| | onPressed: () => Navigator.of(context).pop(), |
| | child: const Text('Close'), |
| | ), |
| | TextButton( |
| | onPressed: () { |
| | Clipboard.setData(ClipboardData( |
| | text: 'Message: ${message.content}\n' |
| | 'Model: ${message.model}\n' |
| | 'Confidence: ${message.confidence}\n' |
| | 'Processing Time: ${message.processingTime}ms\n' |
| | 'Timestamp: ${message.timestamp}', |
| | )); |
| | Navigator.of(context).pop(); |
| | ScaffoldMessenger.of(context).showSnackBar( |
| | const SnackBar( |
| | content: Text('Details copied to clipboard'), |
| | duration: Duration(seconds: 2), |
| | ), |
| | ); |
| | }, |
| | child: const Text('Copy Details'), |
| | ), |
| | ], |
| | ), |
| | ); |
| | } |
| | } |
| |
|