马良AI写作初始化仓库
This commit is contained in:
464
AINoval/lib/widgets/common/dynamic_form_field_widget.dart
Normal file
464
AINoval/lib/widgets/common/dynamic_form_field_widget.dart
Normal file
@@ -0,0 +1,464 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/models/ai_feature_form_config.dart';
|
||||
import 'package:ainoval/models/context_selection_models.dart';
|
||||
import 'package:ainoval/widgets/common/index.dart';
|
||||
import 'package:ainoval/widgets/common/multi_select_instructions_with_presets.dart' as multi_select;
|
||||
// import 'package:ainoval/utils/web_theme.dart';
|
||||
|
||||
/// 动态表单字段组件
|
||||
/// 根据FormFieldConfig配置动态渲染对应的表单字段
|
||||
class DynamicFormFieldWidget extends StatelessWidget {
|
||||
/// 字段配置
|
||||
final FormFieldConfig config;
|
||||
|
||||
/// 当前值映射表
|
||||
final Map<AIFormFieldType, dynamic> values;
|
||||
|
||||
/// 值变更回调
|
||||
final Function(AIFormFieldType type, dynamic value) onValueChanged;
|
||||
|
||||
/// 重置回调
|
||||
final Function(AIFormFieldType type) onReset;
|
||||
|
||||
/// 上下文选择数据(仅用于上下文选择字段)
|
||||
final ContextSelectionData? contextSelectionData;
|
||||
|
||||
/// 控制器映射表(用于文本输入字段)
|
||||
final Map<AIFormFieldType, TextEditingController>? controllers;
|
||||
|
||||
/// AI功能类型(用于提示词模板选择)
|
||||
final String? aiFeatureType;
|
||||
|
||||
/// 当前编辑的预设是否为系统预设(用于模板过滤)
|
||||
final bool? isSystemPreset;
|
||||
|
||||
/// 当前编辑的预设是否为公共预设(用于模板过滤)
|
||||
final bool? isPublicPreset;
|
||||
|
||||
const DynamicFormFieldWidget({
|
||||
super.key,
|
||||
required this.config,
|
||||
required this.values,
|
||||
required this.onValueChanged,
|
||||
required this.onReset,
|
||||
this.contextSelectionData,
|
||||
this.controllers,
|
||||
this.aiFeatureType,
|
||||
this.isSystemPreset,
|
||||
this.isPublicPreset,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (config.type) {
|
||||
case AIFormFieldType.instructions:
|
||||
return _buildInstructionsField(context);
|
||||
case AIFormFieldType.length:
|
||||
return _buildLengthField(context);
|
||||
case AIFormFieldType.style:
|
||||
return _buildStyleField(context);
|
||||
case AIFormFieldType.contextSelection:
|
||||
return _buildContextSelectionField(context);
|
||||
case AIFormFieldType.smartContext:
|
||||
return _buildSmartContextField(context);
|
||||
case AIFormFieldType.promptTemplate:
|
||||
return _buildPromptTemplateField(context);
|
||||
case AIFormFieldType.temperature:
|
||||
return _buildTemperatureField(context);
|
||||
case AIFormFieldType.topP:
|
||||
return _buildTopPField(context);
|
||||
case AIFormFieldType.memoryCutoff:
|
||||
return _buildMemoryCutoffField(context);
|
||||
case AIFormFieldType.quickAccess:
|
||||
return _buildQuickAccessField(context);
|
||||
// 不需要 default:枚举已覆盖所有分支
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建指令字段
|
||||
Widget _buildInstructionsField(BuildContext context) {
|
||||
final controller = controllers?[config.type] ?? TextEditingController();
|
||||
final presets = _parseInstructionPresets(config.options?['presets']);
|
||||
|
||||
if (presets.isNotEmpty) {
|
||||
// 如果有预设,使用多选指令组件
|
||||
return FormFieldFactory.createMultiSelectInstructionsWithPresetsField(
|
||||
controller: controller,
|
||||
presets: presets,
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
placeholder: config.options?['placeholder'] ?? 'e.g. 输入指令...',
|
||||
dropdownPlaceholder: '选择指令预设',
|
||||
onReset: () => onReset(config.type),
|
||||
onExpand: () => _handleExpandInstructions(),
|
||||
onCopy: () => _handleCopyInstructions(),
|
||||
onSelectionChanged: (selectedPresets) => _handlePresetSelectionChanged(selectedPresets),
|
||||
);
|
||||
} else {
|
||||
// 如果没有预设,使用简单的指令字段
|
||||
return FormFieldFactory.createInstructionsField(
|
||||
controller: controller,
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
placeholder: config.options?['placeholder'] ?? 'e.g. 输入指令...',
|
||||
onReset: () => onReset(config.type),
|
||||
onExpand: () => _handleExpandInstructions(),
|
||||
onCopy: () => _handleCopyInstructions(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建长度字段
|
||||
Widget _buildLengthField(BuildContext context) {
|
||||
final radioOptions = _parseRadioOptions(config.options?['radioOptions']);
|
||||
final placeholder = config.options?['placeholder'] ?? 'e.g. 输入长度...';
|
||||
final controller = controllers?[config.type] ?? TextEditingController();
|
||||
|
||||
return FormFieldFactory.createLengthField<String>(
|
||||
options: radioOptions,
|
||||
value: values[config.type] as String?,
|
||||
onChanged: (value) => onValueChanged(config.type, value),
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
isRequired: config.isRequired,
|
||||
onReset: () => onReset(config.type),
|
||||
alternativeInput: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 40),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
filled: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
onValueChanged(config.type, null); // 清除单选按钮选择
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建重构方式字段
|
||||
Widget _buildStyleField(BuildContext context) {
|
||||
final radioOptions = _parseRadioOptions(config.options?['radioOptions']);
|
||||
final placeholder = config.options?['placeholder'] ?? 'e.g. 输入样式...';
|
||||
final controller = controllers?[config.type] ?? TextEditingController();
|
||||
|
||||
return FormFieldFactory.createLengthField<String>(
|
||||
options: radioOptions,
|
||||
value: values[config.type] as String?,
|
||||
onChanged: (value) => onValueChanged(config.type, value),
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
onReset: () => onReset(config.type),
|
||||
alternativeInput: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 40),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
filled: true,
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
onValueChanged(config.type, null); // 清除单选按钮选择
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建上下文选择字段
|
||||
Widget _buildContextSelectionField(BuildContext context) {
|
||||
if (contextSelectionData == null) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_outlined,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'上下文选择数据未提供',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return FormFieldFactory.createContextSelectionField(
|
||||
contextData: contextSelectionData!,
|
||||
onSelectionChanged: (newData) => onValueChanged(config.type, newData),
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
onReset: () => onReset(config.type),
|
||||
dropdownWidth: 400,
|
||||
initialChapterId: null,
|
||||
initialSceneId: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建智能上下文字段
|
||||
Widget _buildSmartContextField(BuildContext context) {
|
||||
return SmartContextToggle(
|
||||
value: values[config.type] as bool? ?? true,
|
||||
onChanged: (value) => onValueChanged(config.type, value),
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建提示词模板字段
|
||||
Widget _buildPromptTemplateField(BuildContext context) {
|
||||
// 获取AI功能类型,如果没有提供则默认为TEXT_EXPANSION
|
||||
final featureType = aiFeatureType ?? 'TEXT_EXPANSION';
|
||||
|
||||
// 根据预设类型确定允许的模板类型
|
||||
// 系统预设:允许 系统默认 + 私有;禁止 公共
|
||||
// 公共预设:允许 系统默认 + 公共(仅已验证);禁止 私有
|
||||
// 用户预设:允许全部(系统默认 + 私有 + 公共)
|
||||
Set<PromptTemplateType>? allowedTypes;
|
||||
bool onlyVerifiedPublic = false;
|
||||
if (isSystemPreset == true) {
|
||||
allowedTypes = {PromptTemplateType.system, PromptTemplateType.private};
|
||||
onlyVerifiedPublic = false;
|
||||
} else if (isPublicPreset == true) {
|
||||
allowedTypes = {PromptTemplateType.system, PromptTemplateType.public};
|
||||
onlyVerifiedPublic = true;
|
||||
} else {
|
||||
allowedTypes = {PromptTemplateType.system, PromptTemplateType.private, PromptTemplateType.public};
|
||||
onlyVerifiedPublic = false;
|
||||
}
|
||||
|
||||
return FormFieldFactory.createPromptTemplateSelectionField(
|
||||
selectedTemplateId: values[config.type] as String?,
|
||||
onTemplateSelected: (templateId) => onValueChanged(config.type, templateId),
|
||||
aiFeatureType: featureType,
|
||||
allowedTypes: allowedTypes,
|
||||
onlyVerifiedPublic: onlyVerifiedPublic,
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
onReset: () => onReset(config.type),
|
||||
onTemporaryPromptsSaved: (sys, user) {
|
||||
// 将临时提示词放入 values 的扩展槽位(若业务侧读取,需要自定义键)
|
||||
onValueChanged(config.type, values[config.type]);
|
||||
// 通过额外键把自定义提示词也放入values,供表单容器在提交时拼接到请求parameters
|
||||
onValueChanged(AIFormFieldType.promptTemplate, values[config.type]);
|
||||
values[AIFormFieldType.promptTemplate] = values[config.type];
|
||||
values[AIFormFieldType.instructions] = values[AIFormFieldType.instructions];
|
||||
// 不在此层直接发送请求,仅存储由上层容器读取
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建温度字段
|
||||
Widget _buildTemperatureField(BuildContext context) {
|
||||
return FormFieldFactory.createTemperatureSliderField(
|
||||
context: context,
|
||||
value: values[config.type] as double? ?? 0.7,
|
||||
onChanged: (value) => onValueChanged(config.type, value),
|
||||
onReset: () => onReset(config.type),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建Top-P字段
|
||||
Widget _buildTopPField(BuildContext context) {
|
||||
return FormFieldFactory.createTopPSliderField(
|
||||
context: context,
|
||||
value: values[config.type] as double? ?? 0.9,
|
||||
onChanged: (value) => onValueChanged(config.type, value),
|
||||
onReset: () => onReset(config.type),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建记忆截断字段
|
||||
Widget _buildMemoryCutoffField(BuildContext context) {
|
||||
final radioOptions = _parseRadioIntOptions(config.options?['radioOptions']);
|
||||
final placeholder = config.options?['placeholder'] ?? 'e.g. 24';
|
||||
final controller = controllers?[config.type] ?? TextEditingController();
|
||||
|
||||
return FormFieldFactory.createMemoryCutoffField(
|
||||
options: radioOptions,
|
||||
value: values[config.type] as int?,
|
||||
onChanged: (value) => onValueChanged(config.type, value),
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
customInput: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
filled: true,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final intValue = int.tryParse(value);
|
||||
if (intValue != null) {
|
||||
onValueChanged(config.type, null); // 清除单选按钮选择
|
||||
}
|
||||
},
|
||||
),
|
||||
onReset: () => onReset(config.type),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建快捷访问字段
|
||||
Widget _buildQuickAccessField(BuildContext context) {
|
||||
return FormFieldFactory.createQuickAccessToggleField(
|
||||
value: values[config.type] as bool? ?? false,
|
||||
onChanged: (value) => onValueChanged(config.type, value),
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
onReset: () => onReset(config.type),
|
||||
);
|
||||
}
|
||||
|
||||
// 已移除未使用的不支持字段提示构建函数
|
||||
|
||||
// 工具方法
|
||||
|
||||
/// 解析指令预设
|
||||
List<multi_select.InstructionPreset> _parseInstructionPresets(dynamic presets) {
|
||||
if (presets is! List) return [];
|
||||
|
||||
return presets.map<multi_select.InstructionPreset>((preset) {
|
||||
if (preset is Map<String, dynamic>) {
|
||||
return multi_select.InstructionPreset(
|
||||
id: preset['id'] as String? ?? '',
|
||||
title: preset['title'] as String? ?? '',
|
||||
content: preset['content'] as String? ?? '',
|
||||
description: preset['description'] as String?,
|
||||
);
|
||||
}
|
||||
return const multi_select.InstructionPreset(
|
||||
id: '',
|
||||
title: '',
|
||||
content: '',
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 解析单选按钮选项(字符串值)
|
||||
List<RadioOption<String>> _parseRadioOptions(dynamic options) {
|
||||
if (options is! List) return [];
|
||||
|
||||
return options.map<RadioOption<String>>((option) {
|
||||
if (option is Map<String, dynamic>) {
|
||||
return RadioOption<String>(
|
||||
value: option['value'] as String? ?? '',
|
||||
label: option['label'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
return const RadioOption<String>(value: '', label: '');
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 解析单选按钮选项(整数值)
|
||||
List<RadioOption<int>> _parseRadioIntOptions(dynamic options) {
|
||||
if (options is! List) return [];
|
||||
|
||||
return options.map<RadioOption<int>>((option) {
|
||||
if (option is Map<String, dynamic>) {
|
||||
return RadioOption<int>(
|
||||
value: option['value'] as int? ?? 0,
|
||||
label: option['label'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
return const RadioOption<int>(value: 0, label: '');
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// 事件处理器
|
||||
|
||||
void _handleExpandInstructions() {
|
||||
debugPrint('展开指令编辑器');
|
||||
}
|
||||
|
||||
void _handleCopyInstructions() {
|
||||
debugPrint('复制指令内容');
|
||||
}
|
||||
|
||||
void _handlePresetSelectionChanged(List<multi_select.InstructionPreset> selectedPresets) {
|
||||
debugPrint('选中的预设已改变: ${selectedPresets.map((p) => p.title).join(', ')}');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user