import 'package:flutter/material.dart'; import 'package:iconsax/iconsax.dart'; import 'package:animate_do/animate_do.dart'; import 'package:timeago/timeago.dart' as timeago; import '../api/api.dart'; /// Executions dashboard page /// /// Displays all agent executions with filtering by status, metrics, and details. /// Integrates with backend CQRS API for execution monitoring and analysis. class ExecutionsPage extends StatefulWidget { const ExecutionsPage({super.key}); @override State createState() => _ExecutionsPageState(); } class _ExecutionsPageState extends State { final CqrsApiClient _apiClient = CqrsApiClient( config: ApiClientConfig.development, ); List? _executions; bool _isLoading = true; String? _errorMessage; ExecutionStatus? _filterStatus; // null = all, or specific status @override void initState() { super.initState(); _loadExecutions(); } @override void dispose() { _apiClient.dispose(); super.dispose(); } Future _loadExecutions() async { setState(() { _isLoading = true; _errorMessage = null; }); final Result> result = _filterStatus == null ? await _apiClient.listExecutions() : await _apiClient.listExecutionsByStatus(_filterStatus!); result.when( success: (List executions) { if (mounted) { setState(() { _executions = executions; _isLoading = false; }); } }, error: (ApiErrorInfo error) { if (mounted) { setState(() { _errorMessage = error.message; _isLoading = false; }); } }, ); } Map get _statusCounts { if (_executions == null) return {}; final Map counts = {}; for (final ExecutionListItemDto exec in _executions!) { counts[exec.status.value] = (counts[exec.status.value] ?? 0) + 1; } return counts; } double get _totalCost { if (_executions == null) return 0.0; return _executions!.fold( 0.0, (double sum, ExecutionListItemDto exec) => sum + (exec.estimatedCost ?? 0.0), ); } int get _totalTokens { if (_executions == null) return 0; return _executions!.fold( 0, (int sum, ExecutionListItemDto exec) => sum + (exec.inputTokens ?? 0) + (exec.outputTokens ?? 0), ); } @override Widget build(BuildContext context) { final ColorScheme colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header with actions _buildHeader(colorScheme), const SizedBox(height: 24), // Metrics cards _buildMetricsCards(colorScheme), const SizedBox(height: 24), // Status filter chips _buildStatusFilters(colorScheme), const SizedBox(height: 24), // Executions list Expanded( child: _buildExecutionsList(colorScheme), ), ], ), ); } Widget _buildHeader(ColorScheme colorScheme) { return Row( children: [ Icon( Iconsax.flash_1, color: colorScheme.primary, size: 28, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Agent Executions', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), Text( _executions != null ? '${_executions!.length} total executions' : 'Loading...', style: TextStyle( fontSize: 14, color: colorScheme.onSurfaceVariant, ), ), ], ), ), IconButton( icon: Icon(Iconsax.refresh, color: colorScheme.primary), onPressed: _loadExecutions, tooltip: 'Refresh executions', ), ], ); } Widget _buildMetricsCards(ColorScheme colorScheme) { return Row( children: [ Expanded( child: _buildMetricCard( 'Total Cost', '\$${_totalCost.toStringAsFixed(4)}', Iconsax.dollar_circle, colorScheme.primaryContainer, colorScheme.onPrimaryContainer, ), ), const SizedBox(width: 16), Expanded( child: _buildMetricCard( 'Total Tokens', _totalTokens.toString(), Iconsax.cpu, colorScheme.secondaryContainer, colorScheme.onSecondaryContainer, ), ), const SizedBox(width: 16), Expanded( child: _buildMetricCard( 'Avg Messages', _executions != null && _executions!.isNotEmpty ? (_executions!.fold( 0, (int sum, ExecutionListItemDto exec) => sum + exec.messageCount, ) / _executions!.length) .toStringAsFixed(1) : '0', Iconsax.message_text, colorScheme.tertiaryContainer, colorScheme.onTertiaryContainer, ), ), ], ); } Widget _buildMetricCard( String label, String value, IconData icon, Color backgroundColor, Color textColor, ) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(icon, color: textColor, size: 32), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 12, color: textColor.withValues(alpha: 0.8), ), ), Text( value, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: textColor, ), ), ], ), ), ], ), ); } Widget _buildStatusFilters(ColorScheme colorScheme) { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildStatusChip('All', null, colorScheme), const SizedBox(width: 8), _buildStatusChip( 'Pending', ExecutionStatus.pending, colorScheme, ), const SizedBox(width: 8), _buildStatusChip( 'Running', ExecutionStatus.running, colorScheme, ), const SizedBox(width: 8), _buildStatusChip( 'Completed', ExecutionStatus.completed, colorScheme, ), const SizedBox(width: 8), _buildStatusChip( 'Failed', ExecutionStatus.failed, colorScheme, ), const SizedBox(width: 8), _buildStatusChip( 'Cancelled', ExecutionStatus.cancelled, colorScheme, ), ], ), ); } Widget _buildStatusChip( String label, ExecutionStatus? status, ColorScheme colorScheme, ) { final bool isSelected = _filterStatus == status; final int count = status == null ? (_executions?.length ?? 0) : (_statusCounts[status.value] ?? 0); return FilterChip( label: Row( mainAxisSize: MainAxisSize.min, children: [ Text(label), if (count > 0) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: isSelected ? colorScheme.onPrimaryContainer.withValues(alpha: 0.2) : colorScheme.onSurface.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Text( count.toString(), style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurface, ), ), ), ], ], ), selected: isSelected, onSelected: (bool selected) { setState(() { _filterStatus = status; }); _loadExecutions(); }, backgroundColor: colorScheme.surfaceContainerHigh, selectedColor: colorScheme.primaryContainer, labelStyle: TextStyle( color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurface, ), ); } Widget _buildExecutionsList(ColorScheme colorScheme) { if (_isLoading) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: colorScheme.primary), const SizedBox(height: 16), Text( 'Loading executions...', style: TextStyle(color: colorScheme.onSurfaceVariant), ), ], ), ); } if (_errorMessage != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Iconsax.warning_2, size: 48, color: colorScheme.error, ), const SizedBox(height: 16), Text( 'Failed to load executions', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), const SizedBox(height: 8), Text( _errorMessage!, style: TextStyle(color: colorScheme.error), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: _loadExecutions, icon: const Icon(Iconsax.refresh), label: const Text('Retry'), ), ], ), ); } if (_executions == null || _executions!.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Iconsax.flash_1, size: 64, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3), ), const SizedBox(height: 16), Text( 'No executions found', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), const SizedBox(height: 8), Text( _filterStatus == null ? 'Start an agent execution to see it here' : 'No ${_filterStatus!.value.toLowerCase()} executions', style: TextStyle(color: colorScheme.onSurfaceVariant), ), ], ), ); } return ListView.builder( itemCount: _executions!.length, itemBuilder: (BuildContext context, int index) { return FadeInUp( duration: Duration(milliseconds: 300 + (index * 50)), child: _buildExecutionCard(_executions![index], colorScheme), ); }, ); } Widget _buildExecutionCard( ExecutionListItemDto execution, ColorScheme colorScheme, ) { final Color statusColor = _getStatusColor(execution.status, colorScheme); return Card( margin: const EdgeInsets.only(bottom: 16), elevation: 0, color: colorScheme.surfaceContainerLow, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: colorScheme.outline.withValues(alpha: 0.2), width: 1, ), ), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () { // TODO: Navigate to execution detail ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('View execution details (not implemented)'), ), ); }, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header row Row( children: [ // Status indicator Container( width: 12, height: 12, decoration: BoxDecoration( color: statusColor, shape: BoxShape.circle, ), ), const SizedBox(width: 12), // Agent name Expanded( child: Text( execution.agentName, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ), // Status badge Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( execution.status.value, style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: statusColor, ), ), ), ], ), const SizedBox(height: 12), // User prompt Text( execution.userPrompt, style: TextStyle( fontSize: 14, color: colorScheme.onSurfaceVariant, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), // Error message if failed if (execution.errorMessage != null) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.errorContainer, borderRadius: BorderRadius.circular(6), ), child: Row( children: [ Icon( Iconsax.warning_2, size: 16, color: colorScheme.onErrorContainer, ), const SizedBox(width: 8), Expanded( child: Text( execution.errorMessage!, style: TextStyle( fontSize: 12, color: colorScheme.onErrorContainer, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ], const SizedBox(height: 12), // Stats row Row( children: [ _buildExecutionStat( Iconsax.message_text, '${execution.messageCount}', colorScheme, ), if (execution.inputTokens != null && execution.outputTokens != null) ...[ const SizedBox(width: 16), _buildExecutionStat( Iconsax.cpu, '${execution.inputTokens! + execution.outputTokens!}', colorScheme, ), ], if (execution.estimatedCost != null) ...[ const SizedBox(width: 16), _buildExecutionStat( Iconsax.dollar_circle, '\$${execution.estimatedCost!.toStringAsFixed(4)}', colorScheme, ), ], const Spacer(), Icon( Iconsax.clock, size: 14, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: 4), Text( timeago.format(execution.startedAt), style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), ], ), ], ), ), ), ); } Widget _buildExecutionStat( IconData icon, String value, ColorScheme colorScheme, ) { return Row( children: [ Icon( icon, size: 16, color: colorScheme.primary, ), const SizedBox(width: 4), Text( value, style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ], ); } Color _getStatusColor(ExecutionStatus status, ColorScheme colorScheme) { switch (status) { case ExecutionStatus.pending: return colorScheme.tertiary; case ExecutionStatus.running: return colorScheme.primary; case ExecutionStatus.completed: return Colors.green; case ExecutionStatus.failed: return colorScheme.error; case ExecutionStatus.cancelled: return colorScheme.onSurfaceVariant; } } }