import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:conduit/core/utils/prompt_variable_parser.dart'; import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/shared/theme/theme_extensions.dart'; // Use AppTypography constants for font sizes /// A dialog that collects user input for prompt variables. /// /// Displays input fields for each variable based on their type (text, textarea, /// select, number) and validates required fields before submission. class PromptVariableDialog extends StatefulWidget { const PromptVariableDialog({ super.key, required this.variables, required this.promptTitle, }); /// The variables that require user input. final List variables; /// The title of the prompt being used. final String promptTitle; /// Shows the dialog and returns a map of variable name to user-provided value. /// Returns null if the user cancels the dialog. static Future?> show( BuildContext context, { required List variables, required String promptTitle, }) { return showDialog>( context: context, barrierDismissible: false, builder: (ctx) => PromptVariableDialog(variables: variables, promptTitle: promptTitle), ); } @override State createState() => _PromptVariableDialogState(); } class _PromptVariableDialogState extends State { late final Map _controllers; late final Map _selectValues; final _formKey = GlobalKey(); bool _isSubmitting = false; @override void initState() { super.initState(); _controllers = {}; _selectValues = {}; for (final variable in widget.variables) { if (variable.type == PromptVariableType.select) { _selectValues[variable.name] = variable.defaultValue; } else { _controllers[variable.name] = TextEditingController( text: variable.defaultValue ?? '', ); } } } @override void dispose() { for (final controller in _controllers.values) { controller.dispose(); } super.dispose(); } void _submit() { if (_isSubmitting) return; if (!_formKey.currentState!.validate()) { return; } setState(() => _isSubmitting = true); final values = {}; for (final variable in widget.variables) { if (variable.type == PromptVariableType.select) { values[variable.name] = _selectValues[variable.name] ?? ''; } else { values[variable.name] = _controllers[variable.name]?.text ?? ''; } } Navigator.of(context).pop(values); } @override Widget build(BuildContext context) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; return AlertDialog( backgroundColor: theme.surfaceBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.dialog), ), title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.promptTitle.isNotEmpty ? widget.promptTitle : l10n.promptVariablesTitle, style: TextStyle( color: theme.textPrimary, fontSize: AppTypography.bodyLarge, fontWeight: FontWeight.w600, ), ), const SizedBox(height: Spacing.xs), Text( l10n.promptVariablesDescription, style: TextStyle( color: theme.textSecondary, fontSize: AppTypography.bodySmall, fontWeight: FontWeight.normal, ), ), ], ), content: SizedBox( width: double.maxFinite, child: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < widget.variables.length; i++) ...[ if (i > 0) const SizedBox(height: Spacing.md), _buildField(widget.variables[i]), ], ], ), ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(null), child: Text( l10n.cancel, style: TextStyle(color: theme.textSecondary), ), ), TextButton( onPressed: _isSubmitting ? null : _submit, child: Text( l10n.continueAction, style: TextStyle( color: _isSubmitting ? theme.textSecondary : theme.buttonPrimary, ), ), ), ], ); } Widget _buildField(PromptVariable variable) { final theme = context.conduitTheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( variable.displayLabel, style: TextStyle( color: theme.textPrimary, fontSize: AppTypography.bodySmall, fontWeight: FontWeight.w500, ), ), ), if (variable.isRequired) Text( ' *', style: TextStyle( color: theme.error, fontSize: AppTypography.bodySmall, fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: Spacing.xs), _buildInputWidget(variable), ], ); } Widget _buildInputWidget(PromptVariable variable) { switch (variable.type) { case PromptVariableType.textarea: return _buildTextareaField(variable); case PromptVariableType.select: return _buildSelectField(variable); case PromptVariableType.number: return _buildNumberField(variable); case PromptVariableType.text: return _buildTextField(variable); } } Widget _buildTextField(PromptVariable variable) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; return TextFormField( controller: _controllers[variable.name], style: TextStyle(color: theme.inputText), decoration: _inputDecoration(theme, variable.placeholder), validator: (value) { if (variable.isRequired && (value == null || value.trim().isEmpty)) { return l10n.requiredFieldHelper; } return null; }, onFieldSubmitted: (_) => _submit(), ); } Widget _buildTextareaField(PromptVariable variable) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; return TextFormField( controller: _controllers[variable.name], style: TextStyle(color: theme.inputText), decoration: _inputDecoration(theme, variable.placeholder), minLines: 3, maxLines: 6, validator: (value) { if (variable.isRequired && (value == null || value.trim().isEmpty)) { return l10n.requiredFieldHelper; } return null; }, ); } Widget _buildSelectField(PromptVariable variable) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; final options = variable.options; return DropdownButtonFormField( initialValue: _selectValues[variable.name], decoration: _inputDecoration(theme, variable.placeholder), dropdownColor: theme.surfaceBackground, style: TextStyle(color: theme.inputText), items: options.map((option) { return DropdownMenuItem(value: option, child: Text(option)); }).toList(), onChanged: (value) { setState(() { _selectValues[variable.name] = value; }); }, validator: (value) { if (variable.isRequired && (value == null || value.isEmpty)) { return l10n.requiredFieldHelper; } return null; }, ); } Widget _buildNumberField(PromptVariable variable) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; return TextFormField( controller: _controllers[variable.name], style: TextStyle(color: theme.inputText), decoration: _inputDecoration(theme, variable.placeholder), keyboardType: TextInputType.numberWithOptions( decimal: variable.step != null && variable.step! < 1, signed: variable.min != null && variable.min! < 0, ), inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.-]'))], validator: (value) { if (variable.isRequired && (value == null || value.trim().isEmpty)) { return l10n.requiredFieldHelper; } if (value != null && value.isNotEmpty) { final num = double.tryParse(value); if (num == null) { return l10n.validationFormatError; } if (variable.min != null && num < variable.min!) { return l10n.promptVariableNumberMin(variable.min!); } if (variable.max != null && num > variable.max!) { return l10n.promptVariableNumberMax(variable.max!); } } return null; }, onFieldSubmitted: (_) => _submit(), ); } InputDecoration _inputDecoration(ConduitThemeExtension theme, String? hint) { return InputDecoration( hintText: hint, hintStyle: TextStyle(color: theme.inputPlaceholder), filled: true, fillColor: theme.inputBackground, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), borderSide: BorderSide(color: theme.inputBorder, width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), borderSide: BorderSide(color: theme.buttonPrimary, width: 1), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), borderSide: BorderSide(color: theme.error, width: 1), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), borderSide: BorderSide(color: theme.error, width: 1.5), ), contentPadding: const EdgeInsets.symmetric( horizontal: Spacing.md, vertical: Spacing.md, ), ); } }