CODEX_ADK/FRONTEND/lib/pages/agents_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

499 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:iconsax/iconsax.dart';
import 'package:animate_do/animate_do.dart';
import '../api/api.dart';
import '../dialogs/create_agent_dialog.dart';
/// Agents management page
///
/// Displays all AI agents with ability to create, view, edit, and delete agents.
/// Integrates with backend CQRS API for agent management.
class AgentsPage extends StatefulWidget {
const AgentsPage({super.key});
@override
State<AgentsPage> createState() => _AgentsPageState();
}
class _AgentsPageState extends State<AgentsPage> {
final CqrsApiClient _apiClient = CqrsApiClient(
config: ApiClientConfig.development,
);
List<AgentDto>? _agents;
bool _isLoading = true;
String? _errorMessage;
@override
void initState() {
super.initState();
_loadAgents();
}
@override
void dispose() {
_apiClient.dispose();
super.dispose();
}
Future<void> _loadAgents() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
final Result<List<AgentDto>> result = await _apiClient.listAgents();
result.when(
success: (List<AgentDto> agents) {
if (mounted) {
setState(() {
_agents = agents;
_isLoading = false;
});
}
},
error: (ApiErrorInfo error) {
if (mounted) {
setState(() {
_errorMessage = error.message;
_isLoading = false;
});
}
},
);
}
Future<void> _createAgent(CreateAgentCommand command) async {
final Result<void> result = await _apiClient.createAgent(command);
result.when(
success: (_) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Agent "${command.name}" created successfully'),
backgroundColor: Theme.of(context).colorScheme.primary,
),
);
_loadAgents();
}
},
error: (ApiErrorInfo error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to create agent: ${error.message}'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
},
);
}
@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 Section
_buildHeader(colorScheme),
const SizedBox(height: 24),
// Content Section
Expanded(
child: _buildContent(colorScheme),
),
],
),
);
}
Widget _buildHeader(ColorScheme colorScheme) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Title & Description
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AI Agents',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'Manage your AI agents and their configurations',
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
),
],
),
// Create Agent Button
FadeInRight(
duration: const Duration(milliseconds: 400),
child: ElevatedButton.icon(
onPressed: () => _showCreateAgentDialog(),
icon: const Icon(Iconsax.add, size: 20),
label: const Text('Create Agent'),
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
);
}
Widget _buildContent(ColorScheme colorScheme) {
if (_isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(colorScheme.primary),
),
const SizedBox(height: 16),
Text(
'Loading agents...',
style: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 14,
),
),
],
),
);
}
if (_errorMessage != null) {
return _buildErrorState(colorScheme);
}
if (_agents == null || _agents!.isEmpty) {
return _buildEmptyState(colorScheme);
}
return _buildAgentsList(colorScheme);
}
Widget _buildEmptyState(ColorScheme colorScheme) {
return Center(
child: FadeIn(
duration: const Duration(milliseconds: 600),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Iconsax.cpu,
size: 80,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
const SizedBox(height: 24),
Text(
'No Agents Yet',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 12),
Text(
'Create your first AI agent to get started',
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () => _showCreateAgentDialog(),
icon: const Icon(Iconsax.add),
label: const Text('Create Your First Agent'),
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
),
);
}
Widget _buildErrorState(ColorScheme colorScheme) {
return Center(
child: FadeIn(
duration: const Duration(milliseconds: 400),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Iconsax.danger,
size: 64,
color: colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Error Loading Agents',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
_errorMessage ?? 'Unknown error',
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _loadAgents,
icon: const Icon(Iconsax.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
Widget _buildAgentsList(ColorScheme colorScheme) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400,
childAspectRatio: 1.5,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: _agents!.length,
itemBuilder: (BuildContext context, int index) {
return FadeInUp(
duration: Duration(milliseconds: 300 + (index * 100)),
child: _buildAgentCard(_agents![index], colorScheme),
);
},
);
}
Widget _buildAgentCard(AgentDto agent, ColorScheme colorScheme) {
return Card(
elevation: 0,
color: colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
child: InkWell(
onTap: () => _showAgentDetails(agent),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Row
Row(
children: [
// Agent Type Icon
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
_getAgentTypeIcon(agent.type),
color: colorScheme.primary,
size: 24,
),
),
const SizedBox(width: 12),
// Agent Name & Status
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
agent.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
_buildStatusBadge(agent.status, colorScheme),
],
),
),
// More Options
IconButton(
icon: Icon(
Iconsax.more,
color: colorScheme.onSurfaceVariant,
),
onPressed: () => _showAgentMenu(agent),
),
],
),
const SizedBox(height: 16),
// Description
Text(
agent.description,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
// Footer
Row(
children: [
Icon(
Iconsax.cpu,
size: 14,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 6),
Expanded(
child: Text(
'${agent.modelProvider}/${agent.modelName}',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
),
);
}
Widget _buildStatusBadge(AgentStatus status, ColorScheme colorScheme) {
Color badgeColor;
IconData icon;
switch (status) {
case AgentStatus.active:
badgeColor = Colors.green;
icon = Iconsax.tick_circle5;
case AgentStatus.inactive:
badgeColor = Colors.orange;
icon = Iconsax.pause_circle5;
case AgentStatus.error:
badgeColor = colorScheme.error;
icon = Iconsax.danger5;
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: badgeColor),
const SizedBox(width: 4),
Text(
status.value,
style: TextStyle(
fontSize: 11,
color: badgeColor,
fontWeight: FontWeight.w500,
),
),
],
);
}
IconData _getAgentTypeIcon(AgentType type) {
switch (type) {
case AgentType.codeGenerator:
return Iconsax.code;
case AgentType.codeReviewer:
return Iconsax.search_zoom_in;
case AgentType.debugger:
return Iconsax.shield_search;
case AgentType.documenter:
return Iconsax.document_text;
case AgentType.custom:
return Iconsax.setting_2;
}
}
void _showCreateAgentDialog() {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return CreateAgentDialog(
onCreateAgent: _createAgent,
);
},
);
}
void _showAgentDetails(AgentDto agent) {
// TODO: Implement agent details view
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Viewing agent: ${agent.name}'),
),
);
}
void _showAgentMenu(AgentDto agent) {
// TODO: Implement agent menu (edit, delete, etc.)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Agent menu for: ${agent.name}'),
),
);
}
}