Backend team successfully fixed Swagger conflicts and implemented simple GET endpoints. Frontend now integrates with GET /api/agents to list all agents. Changes: - agent_endpoint.dart: - Added fromInt() methods to all enums (AgentType, AgentStatus, ModelProviderType) - Updated AgentDto.fromJson() to handle integer enum values from backend - Added listAgents() method using HTTP GET /api/agents - Added imports: dart:async, dart:convert, dart:io, package:http - agents_page.dart: - Updated _loadAgents() to call listAgents() API method - Removed placeholder delay, now uses real data from backend - Removed unused getwidget import Backend Integration: ✅ Backend returns 5 test agents (seeded successfully) ✅ Enums transmitted as integers (CodeGenerator=0, Active=0, etc.) ✅ Frontend properly parses integer enums to Dart enum types ✅ GET /api/agents endpoint working and tested ✅ Full CRUD cycle now functional Testing: - Flutter analyze: 0 errors, 0 warnings - Backend health check: ✅ passing - List endpoint: ✅ returns 5 agents - App running: http://localhost:8080 Phase 2 Complete: Frontend can now display agents from backend! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
499 lines
14 KiB
Dart
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}'),
|
|
),
|
|
);
|
|
}
|
|
}
|