import 'package:flutter/material.dart'; import 'package:iconsax/iconsax.dart'; import 'package:animate_do/animate_do.dart'; import '../api/api.dart'; import 'message_bubble.dart'; /// A chat window component for interacting with a single AI agent class AgentChatWindow extends StatefulWidget { final String title; final List availableAgents; const AgentChatWindow({ super.key, required this.title, required this.availableAgents, }); @override State createState() => _AgentChatWindowState(); } class _AgentChatWindowState extends State { AgentDto? _selectedAgent; final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final List<_ChatMessage> _messages = []; bool _isSending = false; String? _conversationId; // Track current conversation final CqrsApiClient _apiClient = CqrsApiClient( config: ApiClientConfig.development, ); @override void initState() { super.initState(); if (widget.availableAgents.isNotEmpty) { _selectedAgent = widget.availableAgents.first; } } @override void dispose() { _messageController.dispose(); _scrollController.dispose(); _apiClient.dispose(); super.dispose(); } Future _sendMessage() async { if (_messageController.text.trim().isEmpty || _selectedAgent == null) { return; } final String messageText = _messageController.text.trim(); setState(() { _messages.add(_ChatMessage( message: messageText, isUser: true, senderName: 'You', timestamp: DateTime.now(), )); _isSending = true; }); _messageController.clear(); _scrollToBottom(); // Send message to agent via API final Result result = await _apiClient.sendMessage( SendMessageCommand( agentId: _selectedAgent!.id, conversationId: _conversationId, // null for first message message: messageText, ), ); if (!mounted) return; result.when( success: (SendMessageResult response) { setState(() { // Store conversation ID for subsequent messages _conversationId = response.conversationId; // Add agent's response to chat _messages.add(_ChatMessage( message: response.agentResponse.content, isUser: false, senderName: _selectedAgent!.name, timestamp: response.agentResponse.timestamp, inputTokens: response.agentResponse.inputTokens, outputTokens: response.agentResponse.outputTokens, estimatedCost: response.agentResponse.estimatedCost, )); _isSending = false; }); _scrollToBottom(); }, error: (ApiErrorInfo error) { setState(() { _isSending = false; }); // Show error message to user if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Iconsax.danger, color: Colors.white, size: 18), const SizedBox(width: 8), Expanded( child: Text( error.type == ApiErrorType.timeout ? 'Request timed out. Agent may be processing...' : 'Failed to send message: ${error.message}', ), ), ], ), backgroundColor: Theme.of(context).colorScheme.error, duration: const Duration(seconds: 4), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } }, ); } void _scrollToBottom() { Future.delayed(const Duration(milliseconds: 100), () { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } @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 with agent selector _buildHeader(colorScheme), // Divider Divider( height: 1, color: colorScheme.outline.withValues(alpha: 0.2), ), // Messages area Expanded( child: _buildMessagesArea(colorScheme), ), // Divider Divider( height: 1, color: colorScheme.outline.withValues(alpha: 0.2), ), // Input area _buildInputArea(colorScheme), ], ), ), ); } Widget _buildHeader(ColorScheme colorScheme) { return Container( padding: const EdgeInsets.all(16), child: Row( children: [ // Title Icon( Iconsax.messages_3, color: colorScheme.primary, size: 20, ), const SizedBox(width: 8), Text( widget.title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), const SizedBox(width: 16), // Agent Selector Dropdown Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), border: Border.all( color: colorScheme.outline.withValues(alpha: 0.3), width: 1, ), ), child: DropdownButton( value: _selectedAgent, isExpanded: true, underline: const SizedBox(), icon: Icon( Iconsax.arrow_down_1, size: 16, color: colorScheme.onSurface, ), style: TextStyle( fontSize: 13, color: colorScheme.onSurface, fontWeight: FontWeight.w500, ), dropdownColor: colorScheme.surfaceContainerHigh, items: widget.availableAgents.isEmpty ? [ DropdownMenuItem( value: null, child: Text( 'No agents available', style: TextStyle( color: colorScheme.onSurfaceVariant, fontSize: 13, ), ), ), ] : widget.availableAgents.map((AgentDto agent) { return DropdownMenuItem( value: agent, child: Row( children: [ Icon( Iconsax.cpu, size: 14, color: colorScheme.primary, ), const SizedBox(width: 8), Expanded( child: Text( agent.name, overflow: TextOverflow.ellipsis, ), ), ], ), ); }).toList(), onChanged: widget.availableAgents.isEmpty ? null : (AgentDto? newAgent) { setState(() { _selectedAgent = newAgent; }); }, ), ), ), ], ), ); } Widget _buildMessagesArea(ColorScheme colorScheme) { if (_messages.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Iconsax.message_text, size: 48, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3), ), const SizedBox(height: 16), Text( 'No messages yet', style: TextStyle( fontSize: 14, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Text( _selectedAgent != null ? 'Start chatting with ${_selectedAgent!.name}' : 'Select an agent to begin', style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), ), ), ], ), ); } return ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(16), itemCount: _messages.length, itemBuilder: (BuildContext context, int index) { final _ChatMessage msg = _messages[index]; return MessageBubble( message: msg.message, isUser: msg.isUser, senderName: msg.senderName, timestamp: msg.timestamp, inputTokens: msg.inputTokens, outputTokens: msg.outputTokens, estimatedCost: msg.estimatedCost, ); }, ); } Widget _buildInputArea(ColorScheme colorScheme) { final bool canSend = _selectedAgent != null && !_isSending && _messageController.text.isNotEmpty; return Container( padding: const EdgeInsets.all(16), child: Row( children: [ // Text input Expanded( child: TextField( controller: _messageController, enabled: _selectedAgent != null && !_isSending, maxLines: null, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( hintText: _selectedAgent != null ? 'Type your message...' : 'Select an agent first', hintStyle: TextStyle( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), fontSize: 13, ), filled: true, fillColor: colorScheme.surfaceContainerHighest, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), style: TextStyle( fontSize: 14, color: colorScheme.onSurface, ), onChanged: (_) => setState(() {}), onSubmitted: (_) => canSend ? _sendMessage() : null, ), ), const SizedBox(width: 12), // Send button Material( color: canSend ? colorScheme.primary : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), child: InkWell( onTap: canSend ? _sendMessage : null, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(12), child: _isSending ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ) : Icon( Iconsax.send_1, color: canSend ? Colors.white : colorScheme.onSurfaceVariant.withValues(alpha: 0.5), size: 20, ), ), ), ), ], ), ); } } /// Internal message model for chat display class _ChatMessage { final String message; final bool isUser; final String senderName; final DateTime timestamp; final int? inputTokens; final int? outputTokens; final double? estimatedCost; _ChatMessage({ required this.message, required this.isUser, required this.senderName, required this.timestamp, this.inputTokens, this.outputTokens, this.estimatedCost, }); }