CODEX_ADK/FRONTEND/lib/dialogs/create_agent_dialog.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

576 lines
20 KiB
Dart

import 'package:flutter/material.dart';
import 'package:iconsax/iconsax.dart';
import '../api/api.dart';
/// Dialog for creating a new AI agent
///
/// Provides a comprehensive form with all required fields for agent creation.
/// Includes validation and integrates with the CQRS API.
class CreateAgentDialog extends StatefulWidget {
final Function(CreateAgentCommand) onCreateAgent;
const CreateAgentDialog({
super.key,
required this.onCreateAgent,
});
@override
State<CreateAgentDialog> createState() => _CreateAgentDialogState();
}
class _CreateAgentDialogState extends State<CreateAgentDialog> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Form fields
final TextEditingController _nameController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final TextEditingController _modelProviderController = TextEditingController();
final TextEditingController _modelNameController = TextEditingController();
final TextEditingController _endpointController = TextEditingController();
final TextEditingController _apiKeyController = TextEditingController();
final TextEditingController _systemPromptController = TextEditingController();
final TextEditingController _maxTokensController = TextEditingController(text: '4000');
AgentType _selectedType = AgentType.codeGenerator;
ModelProviderType _selectedProviderType = ModelProviderType.localEndpoint;
double _temperature = 0.7;
bool _enableMemory = true;
int _conversationWindowSize = 10;
bool _isCreating = false;
@override
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_modelProviderController.dispose();
_modelNameController.dispose();
_endpointController.dispose();
_apiKeyController.dispose();
_systemPromptController.dispose();
_maxTokensController.dispose();
super.dispose();
}
void _handleCreate() {
if (_formKey.currentState!.validate()) {
setState(() => _isCreating = true);
final CreateAgentCommand command = CreateAgentCommand(
name: _nameController.text.trim(),
description: _descriptionController.text.trim(),
type: _selectedType,
modelProvider: _modelProviderController.text.trim(),
modelName: _modelNameController.text.trim(),
providerType: _selectedProviderType,
modelEndpoint: _endpointController.text.trim().isEmpty
? null
: _endpointController.text.trim(),
apiKey: _apiKeyController.text.trim().isEmpty
? null
: _apiKeyController.text.trim(),
temperature: _temperature,
maxTokens: int.parse(_maxTokensController.text),
systemPrompt: _systemPromptController.text.trim(),
enableMemory: _enableMemory,
conversationWindowSize: _conversationWindowSize,
);
widget.onCreateAgent(command);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Dialog(
backgroundColor: colorScheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
width: 700,
constraints: const BoxConstraints(maxHeight: 800),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Iconsax.cpu,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create New Agent',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 4),
Text(
'Configure your AI agent settings',
style: TextStyle(
fontSize: 13,
color: colorScheme.onPrimaryContainer.withValues(alpha: 0.7),
),
),
],
),
),
IconButton(
icon: Icon(
Iconsax.close_circle,
color: colorScheme.onPrimaryContainer,
),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Form Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Basic Information Section
_buildSectionHeader('Basic Information', Iconsax.information),
const SizedBox(height: 16),
_buildTextField(
controller: _nameController,
label: 'Agent Name',
hint: 'e.g., Code Generator',
icon: Iconsax.edit,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return 'Name is required';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _descriptionController,
label: 'Description',
hint: 'Describe what this agent does',
icon: Iconsax.document_text,
maxLines: 3,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return 'Description is required';
}
return null;
},
),
const SizedBox(height: 16),
_buildDropdown<AgentType>(
label: 'Agent Type',
value: _selectedType,
items: AgentType.values,
itemLabel: (AgentType type) => type.value,
onChanged: (AgentType? value) {
if (value != null) {
setState(() => _selectedType = value);
}
},
),
const SizedBox(height: 32),
// Model Configuration Section
_buildSectionHeader('Model Configuration', Iconsax.cpu),
const SizedBox(height: 16),
_buildDropdown<ModelProviderType>(
label: 'Provider Type',
value: _selectedProviderType,
items: ModelProviderType.values,
itemLabel: (ModelProviderType type) => type.value,
onChanged: (ModelProviderType? value) {
if (value != null) {
setState(() => _selectedProviderType = value);
}
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _modelProviderController,
label: 'Model Provider',
hint: 'e.g., ollama, openai, anthropic',
icon: Iconsax.cloud,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return 'Model provider is required';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _modelNameController,
label: 'Model Name',
hint: 'e.g., phi, gpt-4o, claude-3.5-sonnet',
icon: Iconsax.cpu,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return 'Model name is required';
}
return null;
},
),
const SizedBox(height: 16),
if (_selectedProviderType == ModelProviderType.localEndpoint)
_buildTextField(
controller: _endpointController,
label: 'Model Endpoint',
hint: 'http://localhost:11434',
icon: Iconsax.link,
),
if (_selectedProviderType == ModelProviderType.cloudApi)
_buildTextField(
controller: _apiKeyController,
label: 'API Key',
hint: 'sk-...',
icon: Iconsax.key,
obscureText: true,
),
const SizedBox(height: 32),
// Generation Parameters Section
_buildSectionHeader('Generation Parameters', Iconsax.setting_2),
const SizedBox(height: 16),
_buildSlider(
label: 'Temperature',
value: _temperature,
min: 0.0,
max: 2.0,
divisions: 20,
onChanged: (double value) {
setState(() => _temperature = value);
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _maxTokensController,
label: 'Max Tokens',
hint: '4000',
icon: Iconsax.maximize,
keyboardType: TextInputType.number,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return 'Max tokens is required';
}
final int? tokens = int.tryParse(value);
if (tokens == null || tokens <= 0) {
return 'Must be a positive number';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _systemPromptController,
label: 'System Prompt',
hint: 'You are a helpful AI assistant...',
icon: Iconsax.message_text,
maxLines: 4,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return 'System prompt is required';
}
return null;
},
),
const SizedBox(height: 32),
// Memory Settings Section
_buildSectionHeader('Memory Settings', Iconsax.archive),
const SizedBox(height: 16),
_buildSwitch(
label: 'Enable Memory',
value: _enableMemory,
onChanged: (bool value) {
setState(() => _enableMemory = value);
},
),
if (_enableMemory) ...[
const SizedBox(height: 16),
_buildSlider(
label: 'Conversation Window Size',
value: _conversationWindowSize.toDouble(),
min: 1,
max: 100,
divisions: 99,
onChanged: (double value) {
setState(() => _conversationWindowSize = value.toInt());
},
displayValue: _conversationWindowSize.toString(),
),
],
],
),
),
),
),
// Footer Actions
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isCreating ? null : () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: _isCreating ? null : _handleCreate,
icon: _isCreating
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Icon(Iconsax.add),
label: Text(_isCreating ? 'Creating...' : 'Create Agent'),
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
),
),
],
),
),
],
),
),
);
}
Widget _buildSectionHeader(String title, IconData icon) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Row(
children: [
Icon(icon, size: 20, color: colorScheme.primary),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
],
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required String hint,
required IconData icon,
String? Function(String?)? validator,
int maxLines = 1,
bool obscureText = false,
TextInputType? keyboardType,
}) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return TextFormField(
controller: controller,
validator: validator,
maxLines: maxLines,
obscureText: obscureText,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: label,
hintText: hint,
prefixIcon: Icon(icon, color: colorScheme.primary),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: colorScheme.outline.withValues(alpha: 0.3),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
filled: true,
fillColor: colorScheme.surfaceContainerLow,
),
);
}
Widget _buildDropdown<T>({
required String label,
required T value,
required List<T> items,
required String Function(T) itemLabel,
required void Function(T?) onChanged,
}) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return DropdownButtonFormField<T>(
initialValue: value,
decoration: InputDecoration(
labelText: label,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: colorScheme.outline.withValues(alpha: 0.3),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
filled: true,
fillColor: colorScheme.surfaceContainerLow,
),
items: items.map((T item) {
return DropdownMenuItem<T>(
value: item,
child: Text(itemLabel(item)),
);
}).toList(),
onChanged: onChanged,
);
}
Widget _buildSlider({
required String label,
required double value,
required double min,
required double max,
required int divisions,
required void Function(double) onChanged,
String? displayValue,
}) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
Text(
displayValue ?? value.toStringAsFixed(2),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
],
),
Slider(
value: value,
min: min,
max: max,
divisions: divisions,
label: displayValue ?? value.toStringAsFixed(2),
onChanged: onChanged,
),
],
);
}
Widget _buildSwitch({
required String label,
required bool value,
required void Function(bool) onChanged,
}) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
Switch(
value: value,
onChanged: onChanged,
activeTrackColor: colorScheme.primary,
),
],
);
}
}