CODEX_ADK/FRONTEND/lib/pages/agents_page.dart
jean-philippe b6106f326f feat: Add comprehensive Create Agent dialog with validation
Implement full-featured agent creation dialog with extensive form fields,
validation, and Material 3 design. Fully integrated with CQRS API backend.

## What's New
- **Create Agent Dialog**: 575 lines of production-ready UI
- **Complete Form**: All 13 agent configuration fields
- **Validation**: Field-level validation with user-friendly messages
- **Dynamic UI**: Form adapts based on provider type selection
- **Testing Guide**: Comprehensive manual testing documentation

## Dialog Features
### Form Sections
1. **Basic Information**
   - Agent name (required)
   - Description (required)
   - Agent type dropdown (5 types)

2. **Model Configuration**
   - Provider type (CloudApi/LocalEndpoint/Custom)
   - Model provider (e.g., ollama, openai)
   - Model name (e.g., phi, gpt-4o)
   - Conditional fields:
     - Endpoint input (for local models)
     - API key input (for cloud providers, obscured)

3. **Generation Parameters**
   - Temperature slider (0.0-2.0)
   - Max tokens input (validated)
   - System prompt textarea (4 lines)

4. **Memory Settings**
   - Enable memory toggle
   - Conversation window size slider (1-100)

### UI/UX Enhancements
 Material 3 design language
 Svrnty brand colors throughout
 Icon-prefixed input fields
 Smooth animations and transitions
 Responsive layout (700px width, scrollable)
 Loading state on submit
 Form validation with error messages
 Cancel and Create buttons
 Professional header with agent icon

### Integration
 Connected to AgentsPage
 Calls _createAgent() with CreateAgentCommand
 API client integration ready
 SnackBar notifications for feedback
 Dialog closes on success

## Technical Details
- **Lines**: 575 (dialog) + updates
- **Widgets**: Custom form components
  - _buildTextField() with validation
  - _buildDropdown() with generics
  - _buildSlider() with live values
  - _buildSwitch() with styling
  - _buildSectionHeader() with icons
- **State Management**: StatefulWidget with form state
- **Validation**: GlobalKey<FormState> pattern
- **Type Safety**: 100% explicit types
- **Dispose**: Proper controller cleanup

## Files Added
- lib/dialogs/create_agent_dialog.dart (575 lines)
- docs/TESTING_GUIDE.md (450 lines)

## Files Modified
- lib/pages/agents_page.dart (+3 lines - dialog integration)

## Testing
- Flutter analyze: 0 errors, 0 warnings 
- Hot reload compatible 
- Form validation tested 
- All field types working 

## User Flow
1. Click "Create Agent" button (anywhere)
2. Dialog opens with smooth animation
3. Fill required fields (validated)
4. Choose provider type (form adapts)
5. Adjust sliders for parameters
6. Review all settings
7. Click "Create Agent"
8. Loading indicator shows
9. API call executes
10. Success message displays
11. Dialog closes
12. Agent appears in list (when backend ready)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 19:03:15 -04:00

488 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:iconsax/iconsax.dart';
import 'package:animate_do/animate_do.dart';
import 'package:getwidget/getwidget.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;
});
// TODO: Implement list agents endpoint when available
// For now, show empty state
await Future<void>.delayed(const Duration(milliseconds: 500));
setState(() {
_agents = [];
_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}'),
),
);
}
}