Files
iiEsaywebUIapp/lib/features/prompts/widgets/prompt_variable_dialog.dart
2025-12-07 10:48:25 +05:30

352 lines
11 KiB
Dart

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<PromptVariable> 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<Map<String, String>?> show(
BuildContext context, {
required List<PromptVariable> variables,
required String promptTitle,
}) {
return showDialog<Map<String, String>>(
context: context,
barrierDismissible: false,
builder: (ctx) =>
PromptVariableDialog(variables: variables, promptTitle: promptTitle),
);
}
@override
State<PromptVariableDialog> createState() => _PromptVariableDialogState();
}
class _PromptVariableDialogState extends State<PromptVariableDialog> {
late final Map<String, TextEditingController> _controllers;
late final Map<String, String?> _selectValues;
final _formKey = GlobalKey<FormState>();
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 = <String, String>{};
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<String>(
initialValue: _selectValues[variable.name],
decoration: _inputDecoration(theme, variable.placeholder),
dropdownColor: theme.surfaceBackground,
style: TextStyle(color: theme.inputText),
items: options.map((option) {
return DropdownMenuItem<String>(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,
),
);
}
}