CODEX_ADK/FRONTEND/lib/pages/executions_page.dart
Svrnty 229a0698a3 Initial commit: CODEX_ADK monorepo
Multi-agent AI laboratory with ASP.NET Core 8.0 backend and Flutter frontend.
Implements CQRS architecture, OpenAPI contract-first API design.

BACKEND: Agent management, conversations, executions with PostgreSQL + Ollama
FRONTEND: Cross-platform UI with strict typing and Result-based error handling

Co-Authored-By: Jean-Philippe Brule <jp@svrnty.io>
2025-10-26 23:12:32 -04:00

665 lines
19 KiB
Dart

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<ExecutionsPage> createState() => _ExecutionsPageState();
}
class _ExecutionsPageState extends State<ExecutionsPage> {
final CqrsApiClient _apiClient = CqrsApiClient(
config: ApiClientConfig.development,
);
List<ExecutionListItemDto>? _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<void> _loadExecutions() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
final Result<List<ExecutionListItemDto>> result = _filterStatus == null
? await _apiClient.listExecutions()
: await _apiClient.listExecutionsByStatus(_filterStatus!);
result.when(
success: (List<ExecutionListItemDto> executions) {
if (mounted) {
setState(() {
_executions = executions;
_isLoading = false;
});
}
},
error: (ApiErrorInfo error) {
if (mounted) {
setState(() {
_errorMessage = error.message;
_isLoading = false;
});
}
},
);
}
Map<String, int> get _statusCounts {
if (_executions == null) return {};
final Map<String, int> 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<double>(
0.0,
(double sum, ExecutionListItemDto exec) =>
sum + (exec.estimatedCost ?? 0.0),
);
}
int get _totalTokens {
if (_executions == null) return 0;
return _executions!.fold<int>(
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<int>(
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;
}
}
}