import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:iconsax/iconsax.dart'; import 'package:animate_do/animate_do.dart'; import 'package:timeago/timeago.dart' as timeago; import '../api/api.dart'; /// Displays conversation logs with expandable messages and copy functionality class ConversationLogViewer extends StatefulWidget { final List messages; final ScrollController? scrollController; const ConversationLogViewer({ super.key, required this.messages, this.scrollController, }); @override State createState() => _ConversationLogViewerState(); } class _ConversationLogViewerState extends State { final Set _expandedMessageIds = {}; late ScrollController _internalScrollController; @override void initState() { super.initState(); _internalScrollController = widget.scrollController ?? ScrollController(); } @override void dispose() { if (widget.scrollController == null) { _internalScrollController.dispose(); } super.dispose(); } void _toggleExpand(String messageId) { setState(() { if (_expandedMessageIds.contains(messageId)) { _expandedMessageIds.remove(messageId); } else { _expandedMessageIds.add(messageId); } }); } Future _copyToClipboard(String text, BuildContext context) async { await Clipboard.setData(ClipboardData(text: text)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Row( children: [ Icon(Iconsax.tick_circle, color: Colors.white, size: 18), SizedBox(width: 8), Text('Message copied to clipboard'), ], ), backgroundColor: Theme.of(context).colorScheme.primary, duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } @override Widget build(BuildContext context) { final ColorScheme colorScheme = Theme.of(context).colorScheme; return FadeInUp( duration: const Duration(milliseconds: 400), child: Container( decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(16), border: Border.all( color: colorScheme.outline.withValues(alpha: 0.2), width: 1, ), ), child: Column( children: [ // Header _buildHeader(colorScheme), // Divider Divider( height: 1, color: colorScheme.outline.withValues(alpha: 0.2), ), // Messages list Expanded( child: _buildMessagesList(colorScheme), ), ], ), ), ); } Widget _buildHeader(ColorScheme colorScheme) { return Container( padding: const EdgeInsets.all(16), child: Row( children: [ Icon( Iconsax.document_text, color: colorScheme.secondary, size: 20, ), const SizedBox(width: 8), Text( 'Conversation Logs', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(6), ), child: Text( '${widget.messages.length}', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: colorScheme.onSecondaryContainer, ), ), ), ], ), ); } Widget _buildMessagesList(ColorScheme colorScheme) { if (widget.messages.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Iconsax.document_text, size: 48, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3), ), const SizedBox(height: 16), Text( 'No conversation logs', style: TextStyle( fontSize: 14, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Text( 'Start chatting to see logs here', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), ), ), ], ), ); } return ListView.builder( controller: _internalScrollController, padding: const EdgeInsets.all(8), itemCount: widget.messages.length, itemBuilder: (BuildContext context, int index) { final ConversationMessageDto message = widget.messages[index]; return FadeInUp( duration: Duration(milliseconds: 200 + (index * 30)), child: _buildLogRow(message, colorScheme), ); }, ); } Widget _buildLogRow( ConversationMessageDto message, ColorScheme colorScheme, ) { final bool isUser = message.role.toLowerCase() == 'user'; final bool isExpanded = _expandedMessageIds.contains(message.id); final bool needsTruncation = message.content.length > 80; final String displayText = (isExpanded || !needsTruncation) ? message.content : '${message.content.substring(0, 80)}...'; return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: isUser ? colorScheme.primaryContainer.withValues(alpha: 0.3) : colorScheme.secondaryContainer.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(8), border: Border.all( color: isUser ? colorScheme.primary.withValues(alpha: 0.2) : colorScheme.secondary.withValues(alpha: 0.2), width: 1, ), ), child: InkWell( onTap: needsTruncation ? () => _toggleExpand(message.id) : null, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header row with role, timestamp, and copy button Row( children: [ // Role icon and name Icon( isUser ? Iconsax.user : Iconsax.cpu, size: 14, color: isUser ? colorScheme.primary : colorScheme.secondary, ), const SizedBox(width: 6), Text( isUser ? 'User' : message.role, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isUser ? colorScheme.primary : colorScheme.secondary, ), ), const SizedBox(width: 12), // Timestamp Icon( Iconsax.clock, size: 11, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( timeago.format(message.timestamp), style: TextStyle( fontSize: 11, color: colorScheme.onSurfaceVariant, ), ), const Spacer(), // Copy button Material( color: Colors.transparent, child: InkWell( onTap: () => _copyToClipboard(message.content, context), borderRadius: BorderRadius.circular(4), child: Padding( padding: const EdgeInsets.all(4), child: Icon( Iconsax.copy, size: 14, color: colorScheme.onSurfaceVariant, ), ), ), ), ], ), const SizedBox(height: 8), // Message content Text( displayText, style: TextStyle( fontSize: 13, color: colorScheme.onSurface, height: 1.4, ), ), // Expand/collapse indicator if (needsTruncation) Padding( padding: const EdgeInsets.only(top: 6), child: Row( children: [ Icon( isExpanded ? Iconsax.arrow_up_2 : Iconsax.arrow_down_1, size: 12, color: colorScheme.primary, ), const SizedBox(width: 4), Text( isExpanded ? 'Show less' : 'Show more', style: TextStyle( fontSize: 11, color: colorScheme.primary, fontWeight: FontWeight.w500, ), ), ], ), ), ], ), ), ), ); } }