import 'package:ainoval/widgets/common/multi_select_instructions_with_presets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ainoval/utils/web_theme.dart'; import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart'; import 'package:ainoval/blocs/prompt_new/prompt_new_bloc.dart'; import 'package:ainoval/blocs/prompt_new/prompt_new_state.dart'; import 'package:ainoval/blocs/prompt_new/prompt_new_event.dart'; import 'package:ainoval/widgets/common/prompt_quick_edit_dialog.dart'; import 'package:ainoval/models/context_selection_models.dart'; import 'package:ainoval/models/preset_models.dart'; import 'package:ainoval/models/prompt_models.dart'; import 'dialog_container.dart'; import 'dialog_header.dart'; import 'custom_tab_bar.dart'; import 'form_fieldset.dart'; import 'custom_text_editor.dart'; import 'context_badge.dart'; import 'radio_button_group.dart'; import 'bottom_action_bar.dart'; import 'context_selection_dropdown_menu_anchor.dart'; import 'instructions_with_presets.dart'; import 'multi_select_instructions_with_presets.dart' as multi_select; /// 表单对话框模板组件 /// 提供完整的对话框表单布局,支持多个Bloc的依赖注入 class FormDialogTemplate extends StatefulWidget { /// 构造函数 const FormDialogTemplate({ super.key, required this.title, required this.tabs, required this.tabContents, this.primaryActionLabel = '保存', this.onPrimaryAction, this.showModelSelector = true, this.modelSelectorData, this.onModelSelectorTap, this.modelSelectorKey, this.showPresets = false, this.onPresetsPressed, this.usePresetDropdown = false, this.presetFeatureType, this.currentPreset, this.onPresetSelected, this.onCreatePreset, this.onManagePresets, this.novelId, this.aiConfigBloc, this.onClose, this.onTabChanged, }); /// 对话框标题 final String title; /// 选项卡列表 final List tabs; /// 选项卡内容列表 final List tabContents; /// 主要操作按钮文字 final String primaryActionLabel; /// 主要操作回调 final VoidCallback? onPrimaryAction; /// 是否显示模型选择器 final bool showModelSelector; /// 模型选择器数据 final ModelSelectorData? modelSelectorData; /// 模型选择器点击回调 final VoidCallback? onModelSelectorTap; /// 模型选择器的 GlobalKey final GlobalKey? modelSelectorKey; /// 是否显示预设按钮 final bool showPresets; /// 预设按钮回调 final VoidCallback? onPresetsPressed; /// 是否使用新的预设下拉框 final bool usePresetDropdown; /// 预设功能类型(用于过滤预设) final String? presetFeatureType; /// 当前选中的预设 final AIPromptPreset? currentPreset; /// 预设选择回调 final ValueChanged? onPresetSelected; /// 创建预设回调 final VoidCallback? onCreatePreset; /// 管理预设回调 final VoidCallback? onManagePresets; /// 小说ID(用于过滤预设) final String? novelId; /// AI配置Bloc(可选) final AiConfigBloc? aiConfigBloc; /// 关闭回调 final VoidCallback? onClose; /// Tab切换回调 final ValueChanged? onTabChanged; @override State createState() => _FormDialogTemplateState(); } class _FormDialogTemplateState extends State { late String _selectedTabId; @override void initState() { super.initState(); _selectedTabId = widget.tabs.isNotEmpty ? widget.tabs.first.id : ''; } @override Widget build(BuildContext context) { // 构建 providers 列表,确保至少有一个空的 provider final providers = [ // 如果传入了aiConfigBloc,则提供给子组件使用 if (widget.aiConfigBloc != null) BlocProvider.value(value: widget.aiConfigBloc!), ]; // 如果没有任何 providers,添加一个空的 provider 避免 MultiBlocProvider 报错 if (providers.isEmpty) { return DialogContainer( child: _buildDialogContent(), ); } return MultiBlocProvider( providers: providers, child: DialogContainer( child: _buildDialogContent(), ), ); } /// 构建对话框内容 Widget _buildDialogContent() { return Column( children: [ // 标题栏 DialogHeader( title: widget.title, onClose: widget.onClose, ), // 内容区域 Expanded( child: Column( children: [ // 选项卡栏 if (widget.tabs.isNotEmpty) CustomTabBar( tabs: widget.tabs, selectedTabId: _selectedTabId, onTabChanged: (tabId) { setState(() { _selectedTabId = tabId; }); // 调用外部回调 widget.onTabChanged?.call(tabId); }, showPresets: widget.showPresets, onPresetsPressed: widget.onPresetsPressed, usePresetDropdown: widget.usePresetDropdown, presetFeatureType: widget.presetFeatureType, currentPreset: widget.currentPreset, onPresetSelected: widget.onPresetSelected, onCreatePreset: widget.onCreatePreset, onManagePresets: widget.onManagePresets, novelId: widget.novelId, ), // 选项卡内容 Expanded( child: _buildTabContent(), ), ], ), ), // 底部操作栏 BottomActionBar( modelSelector: widget.showModelSelector ? _buildModelSelector() : null, primaryAction: _buildPrimaryAction(), ), ], ); } /// 构建选项卡内容 Widget _buildTabContent() { final tabIndex = widget.tabs.indexWhere((tab) => tab.id == _selectedTabId); if (tabIndex == -1 || tabIndex >= widget.tabContents.length) { return const Center(child: Text('内容未找到')); } return SingleChildScrollView( padding: const EdgeInsets.all(24), child: widget.tabContents[tabIndex], ); } /// 构建模型选择器 Widget? _buildModelSelector() { if (!widget.showModelSelector || widget.modelSelectorData == null) { return null; } final data = widget.modelSelectorData!; return Container( key: widget.modelSelectorKey, child: ModelSelector( modelName: data.modelName, onTap: widget.onModelSelectorTap, providerIcon: data.providerIcon, maxOutput: data.maxOutput, isModerated: data.isModerated, ), ); } /// 构建主要操作按钮 Widget _buildPrimaryAction() { final isDark = WebTheme.isDarkMode(context); return ElevatedButton( onPressed: widget.onPrimaryAction, style: ElevatedButton.styleFrom( backgroundColor: isDark ? WebTheme.darkGrey700 : WebTheme.grey700, foregroundColor: isDark ? WebTheme.darkGrey50 : WebTheme.grey50, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), ), child: Text( widget.primaryActionLabel, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ); } } /// 模型选择器数据 class ModelSelectorData { /// 构造函数 const ModelSelectorData({ required this.modelName, this.providerIcon, this.maxOutput, this.isModerated = false, }); /// 模型名称 final String modelName; /// 提供商图标 final Widget? providerIcon; /// 最大输出 final String? maxOutput; /// 是否受监管 final bool isModerated; } /// 常用表单字段工厂类 /// 提供快速创建常用表单字段的方法 class FormFieldFactory { /// 私有构造函数 FormFieldFactory._(); /// 创建指令输入字段 static Widget createInstructionsField({ TextEditingController? controller, String title = '指令', String description = '为AI提供的任务指令和角色说明', String placeholder = '请输入指令内容...', bool showReset = true, VoidCallback? onReset, VoidCallback? onExpand, VoidCallback? onCopy, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, child: CustomTextEditor( controller: controller, placeholder: placeholder, onExpand: onExpand, onCopy: onCopy, ), ); } /// 创建带预设选项的指令输入字段 static Widget createInstructionsWithPresetsField({ TextEditingController? controller, List presets = const [], String title = '指令', String description = '为AI提供的任务指令和角色说明', String placeholder = 'e.g. You are a...', String dropdownPlaceholder = 'Select \'Instructions\'...', bool isRequired = false, bool showReset = true, VoidCallback? onReset, VoidCallback? onExpand, VoidCallback? onCopy, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, showRequired: isRequired, child: InstructionsWithPresets( controller: controller, presets: presets, placeholder: placeholder, dropdownPlaceholder: dropdownPlaceholder, onExpand: onExpand, onCopy: onCopy, ), ); } /// 创建多选指令预设字段 static Widget createMultiSelectInstructionsWithPresetsField({ TextEditingController? controller, List presets = const [], String title = '指令', String description = '为AI提供的任务指令和角色说明', String placeholder = 'e.g. You are a...', String dropdownPlaceholder = 'Select Instructions...', bool isRequired = false, bool showReset = true, VoidCallback? onReset, VoidCallback? onExpand, VoidCallback? onCopy, ValueChanged>? onSelectionChanged, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, showRequired: isRequired, child: multi_select.MultiSelectInstructionsWithPresets( controller: controller, presets: presets, placeholder: placeholder, dropdownPlaceholder: dropdownPlaceholder, onExpand: onExpand, onCopy: onCopy, onSelectionChanged: onSelectionChanged, ), ); } /// 创建上下文字段 static Widget createContextField({ required List contexts, required ValueChanged onRemoveContext, required VoidCallback onAddContext, String title = '附加上下文', String description = '为AI提供的额外信息和参考资料', bool showReset = true, VoidCallback? onReset, Map? contextKeys, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, child: Builder( builder: (context) => Wrap( spacing: 8, runSpacing: 8, children: [ // 添加上下文按钮 SizedBox( height: 36, // 与 ContextBadge 保持一致的高度 child: ElevatedButton.icon( onPressed: onAddContext, icon: const Icon(Icons.add, size: 16), label: const Text( 'Context', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), ), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).brightness == Brightness.dark ? const Color(0xFF374151) // gray-700 : Colors.white, foregroundColor: Theme.of(context).brightness == Brightness.dark ? const Color(0xFFD1D5DB) // gray-300 : const Color(0xFF4B5563), // gray-600 side: BorderSide( color: Theme.of(context).brightness == Brightness.dark ? const Color(0xFF374151) // gray-700 : const Color(0xFFD1D5DB), // gray-300 width: 1, ), elevation: 1, shadowColor: Colors.black.withOpacity(0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), ), ), ), // 上下文标签列表 ...contexts.map((contextData) => ContextBadge( data: contextData, onDelete: () => onRemoveContext(contextData), globalKey: contextKeys?[contextData], )).toList(), ], ), ), ); } /// 创建长度选择字段 static Widget createLengthField({ required List> options, T? value, required ValueChanged onChanged, String title = '长度', String description = '生成内容的长度设置', bool isRequired = false, bool showReset = true, VoidCallback? onReset, Widget? alternativeInput, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, showRequired: isRequired, child: alternativeInput != null ? RadioButtonGroupWithSeparator( radioGroup: RadioButtonGroup( options: options, value: value, onChanged: onChanged, showClear: true, ), alternativeWidget: alternativeInput, ) : RadioButtonGroup( options: options, value: value, onChanged: onChanged, showClear: true, ), ); } /// 创建记忆截断字段 static Widget createMemoryCutoffField({ required List> options, int? value, required ValueChanged onChanged, String title = '记忆截断', String description = '指定发送给AI的最大消息对数,超出此限制的消息将被忽略', bool showReset = true, VoidCallback? onReset, Widget? customInput, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, child: customInput != null ? RadioButtonGroupWithSeparator( radioGroup: RadioButtonGroup( options: options, value: value, onChanged: onChanged, ), alternativeWidget: customInput, ) : RadioButtonGroup( options: options, value: value, onChanged: onChanged, ), ); } /// 创建新版上下文选择字段 static Widget createContextSelectionField({ required ContextSelectionData contextData, required ValueChanged onSelectionChanged, String title = '附加上下文', String description = '选择要包含在对话中的上下文信息', bool showReset = true, VoidCallback? onReset, double? dropdownWidth, double maxDropdownHeight = 400, String? initialChapterId, String? initialSceneId, Map? typeColorMap, Color Function(ContextSelectionType type, BuildContext context)? typeColorResolver, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 上下文选择下拉框 ContextSelectionDropdownBuilder.buildMenuAnchor( data: contextData, onSelectionChanged: onSelectionChanged, placeholder: '点击添加上下文', width: dropdownWidth, maxHeight: maxDropdownHeight, initialChapterId: initialChapterId, initialSceneId: initialSceneId, typeColorMap: typeColorMap, typeColorResolver: typeColorResolver, ), // 显示已选择的上下文标签 if (contextData.selectedCount > 0) ...[ const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: contextData.selectedItems.values.map((item) { return ContextBadge( data: ContextData( id: item.id, title: item.title, subtitle: item.displaySubtitle, icon: item.type.icon, ), onDelete: () { final newData = contextData.deselectItem(item.id); onSelectionChanged(newData); }, maxWidth: 200, ); }).toList(), ), ], ], ), ); } /// 🚀 新增:创建提示词模板选择字段 static Widget createPromptTemplateSelectionField({ String? selectedTemplateId, required ValueChanged onTemplateSelected, required String aiFeatureType, String title = '关联提示词模板', String description = '选择要关联的提示词模板', bool showReset = true, VoidCallback? onReset, void Function(String systemPrompt, String userPrompt)? onTemporaryPromptsSaved, Set? allowedTypes, bool onlyVerifiedPublic = false, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, child: _PromptTemplateDropdown( selectedTemplateId: selectedTemplateId, onTemplateSelected: onTemplateSelected, aiFeatureType: aiFeatureType, allowedTypes: allowedTypes, onlyVerifiedPublic: onlyVerifiedPublic, onEdit: (contextForEdit, currentTemplateId) { if (currentTemplateId == null || currentTemplateId.isEmpty) { ScaffoldMessenger.of(contextForEdit).showSnackBar( const SnackBar(content: Text('请先选择提示词模板')), ); return; } showDialog( context: contextForEdit, barrierDismissible: true, builder: (dialogContext) { return PromptQuickEditDialog( templateId: currentTemplateId, aiFeatureType: aiFeatureType, onTemporaryPromptsSaved: (sys, user) { if (onTemporaryPromptsSaved != null) { onTemporaryPromptsSaved(sys, user); } }, ); }, ); }, ), ); } /// 🚀 新增:创建快捷访问勾选字段 static Widget createQuickAccessToggleField({ required bool value, required ValueChanged onChanged, String title = '快捷访问', String description = '是否在快捷访问列表中显示此预设', bool showReset = true, VoidCallback? onReset, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, child: CheckboxListTile( value: value, onChanged: (bool? newValue) { if (newValue != null) { onChanged(newValue); } }, title: const Text('显示在快捷访问列表'), subtitle: const Text('勾选后此预设将显示在功能对话框的快捷列表中'), controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.zero, dense: true, ), ); } /// 🚀 新增:创建温度滑动组件 static Widget createTemperatureSliderField({ required BuildContext context, required double value, required ValueChanged onChanged, String title = '温度 (Temperature)', String description = '控制生成文本的随机性和创造性', bool showReset = true, VoidCallback? onReset, double min = 0.0, double max = 2.0, int divisions = 40, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Slider( value: value.clamp(min, max), min: min, max: max, divisions: divisions, label: value.toStringAsFixed(2), onChanged: onChanged, ), ), const SizedBox(width: 12), Container( width: 60, child: Text( value.toStringAsFixed(2), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ), ], ), const SizedBox(height: 8), Text( '温度越高,文本越随机和创造性;温度越低,文本越确定和重复。推荐范围:0.7-1.0', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), ), ), ], ), ); } /// 🚀 新增:创建Top-P滑动组件 static Widget createTopPSliderField({ required BuildContext context, required double value, required ValueChanged onChanged, String title = 'Top-P (Nucleus Sampling)', String description = '控制词汇选择的多样性', bool showReset = true, VoidCallback? onReset, double min = 0.0, double max = 1.0, int divisions = 100, }) { return FormFieldset( title: title, description: description, showReset: showReset, onReset: onReset, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Slider( value: value.clamp(min, max), min: min, max: max, divisions: divisions, label: value.toStringAsFixed(2), onChanged: onChanged, ), ), const SizedBox(width: 12), Container( width: 60, child: Text( value.toStringAsFixed(2), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ), ], ), const SizedBox(height: 8), Text( '从概率累计达到该值的词组中选择。较低值使文本更可预测,较高值增加多样性。推荐范围:0.8-0.95', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), ), ), ], ), ); } /// 🚀 新增:为预设模板创建硬编码上下文数据 static ContextSelectionData createPresetTemplateContextData({ String novelId = 'preset_template', }) { final hardcodedItems = [ // 核心上下文项 ContextSelectionItem( id: 'preset_full_novel_text', title: 'Full Novel Text', type: ContextSelectionType.fullNovelText, subtitle: '包含完整的小说文本内容', metadata: {'isHardcoded': true}, order: 0, ), ContextSelectionItem( id: 'preset_full_outline', title: 'Full Outline', type: ContextSelectionType.fullOutline, subtitle: '包含完整的小说大纲结构', metadata: {'isHardcoded': true}, order: 1, ), ContextSelectionItem( id: 'preset_novel_basic_info', title: 'Novel Basic Info', type: ContextSelectionType.novelBasicInfo, subtitle: '小说的基本信息(标题、作者、简介等)', metadata: {'isHardcoded': true}, order: 2, ), ContextSelectionItem( id: 'preset_recent_chapters_content', title: 'Recent 5 Chapters Content', type: ContextSelectionType.recentChaptersContent, subtitle: '最近5章的内容', metadata: {'isHardcoded': true}, order: 3, ), ContextSelectionItem( id: 'preset_recent_chapters_summary', title: 'Recent 5 Chapters Summary', type: ContextSelectionType.recentChaptersSummary, subtitle: '最近5章的摘要', metadata: {'isHardcoded': true}, order: 4, ), // 结构化上下文 ContextSelectionItem( id: 'preset_settings', title: 'Character & World Settings', type: ContextSelectionType.settings, subtitle: '角色和世界观设定', metadata: {'isHardcoded': true}, order: 5, ), ContextSelectionItem( id: 'preset_snippets', title: 'Reference Snippets', type: ContextSelectionType.snippets, subtitle: '参考片段和素材', metadata: {'isHardcoded': true}, order: 6, ), // 当前场景上下文 ContextSelectionItem( id: 'preset_current_chapter', title: 'Current Chapter', type: ContextSelectionType.chapters, subtitle: '当前章节内容', metadata: {'isHardcoded': true}, order: 7, ), ContextSelectionItem( id: 'preset_current_scene', title: 'Current Scene', type: ContextSelectionType.scenes, subtitle: '当前场景内容', metadata: {'isHardcoded': true}, order: 8, ), ]; // 构建扁平化映射 final flatItems = {}; for (final item in hardcodedItems) { flatItems[item.id] = item; } return ContextSelectionData( novelId: novelId, availableItems: hardcodedItems, flatItems: flatItems, ); } } /// 🚀 新增:提示词模板下拉组件 class _PromptTemplateDropdown extends StatelessWidget { const _PromptTemplateDropdown({ required this.selectedTemplateId, required this.onTemplateSelected, required this.aiFeatureType, this.onEdit, this.allowedTypes, this.onlyVerifiedPublic = false, }); final String? selectedTemplateId; final ValueChanged onTemplateSelected; final String aiFeatureType; final void Function(BuildContext context, String? currentTemplateId)? onEdit; final Set? allowedTypes; final bool onlyVerifiedPublic; @override Widget build(BuildContext context) { debugPrint('🎨 [_PromptTemplateDropdown] 构建下拉框,功能类型: $aiFeatureType'); return BlocBuilder( builder: (context, state) { debugPrint('🔍 [_PromptTemplateDropdown] BlocBuilder状态更新:'); debugPrint(' - 状态类型: ${state.runtimeType}'); debugPrint(' - 是否正在加载: ${state.isLoading}'); debugPrint(' - 提示词包数量: ${state.promptPackages.length}'); debugPrint(' - 状态状态: ${state.status}'); // 如果还没有加载数据,先触发加载 if (state.promptPackages.isEmpty && !state.isLoading && state.status == PromptNewStatus.initial) { debugPrint('📢 [_PromptTemplateDropdown] 触发提示词包加载请求'); // 在下一帧触发加载,避免在build过程中修改状态 WidgetsBinding.instance.addPostFrameCallback((_) { context.read().add(const LoadAllPromptPackages()); }); } // 显示加载指示器 if (state.isLoading) { debugPrint('⏳ [_PromptTemplateDropdown] 显示加载指示器'); return Container( height: 48, decoration: BoxDecoration( border: Border.all(color: Theme.of(context).dividerColor), borderRadius: BorderRadius.circular(8), ), child: const Center( child: SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), ), ); } // 显示错误状态 if (state.status == PromptNewStatus.failure) { debugPrint('❌ [_PromptTemplateDropdown] 显示错误状态: ${state.errorMessage}'); return Container( height: 48, decoration: BoxDecoration( border: Border.all(color: Theme.of(context).colorScheme.error), borderRadius: BorderRadius.circular(8), ), child: Center( child: Text( '加载失败', style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 12, ), ), ), ); } // 提取模板数据 final templates = _filterTemplates( _extractTemplatesFromState(state), allowedTypes, onlyVerifiedPublic, ); debugPrint('📋 [_PromptTemplateDropdown] 可用模板选项: ${templates.length}个'); for (final template in templates) { debugPrint(' - ${template.id}: ${template.name} (${template.type})'); } // 验证选中的值是否在可用选项中 final validSelectedValue = templates.any((t) => t.id == selectedTemplateId) ? selectedTemplateId : null; if (selectedTemplateId != null && validSelectedValue == null) { debugPrint('⚠️ [_PromptTemplateDropdown] 选中的模板ID不在可用选项中: $selectedTemplateId'); } else if (validSelectedValue != null) { debugPrint('✅ [_PromptTemplateDropdown] 有效的选中值: $validSelectedValue'); } else { debugPrint('ℹ️ [_PromptTemplateDropdown] 无选中值'); } // 自定义美观下拉:带类型/次数标签 return _PromptTemplatePrettyDropdown( options: templates, selectedId: validSelectedValue, onChanged: onTemplateSelected, onEdit: validSelectedValue == null ? null : () => onEdit?.call(context, validSelectedValue), ); }, ); } /// 从状态中提取模板数据 List _extractTemplatesFromState(PromptNewState state) { // 获取当前功能类型的枚举值 final AIFeatureType? featureType = _parseFeatureType(aiFeatureType); debugPrint('🎯 [_PromptTemplateDropdown] 解析功能类型: $aiFeatureType -> $featureType'); if (featureType == null) { debugPrint('⚠️ [_PromptTemplateDropdown] 无法解析功能类型,返回空列表'); return []; } // 获取指定功能类型的提示词包 final package = state.promptPackages[featureType]; if (package == null) { debugPrint('⚠️ [_PromptTemplateDropdown] 找不到功能类型对应的提示词包: $featureType'); debugPrint(' - 可用的功能类型: ${state.promptPackages.keys.toList()}'); return []; } final templates = []; debugPrint('🔍 [_PromptTemplateDropdown] 处理功能类型: $featureType'); debugPrint(' - 系统默认提示词: ${package.systemPrompt.defaultSystemPrompt.isNotEmpty ? '存在' : '不存在'}'); debugPrint(' - 用户提示词数量: ${package.userPrompts.length}'); debugPrint(' - 公开提示词数量: ${package.publicPrompts.length}'); // 1. 🚀 添加系统默认模板(如果存在) if (package.systemPrompt.defaultSystemPrompt.isNotEmpty) { templates.add(PromptTemplateOption( id: 'system_default_${featureType.toString()}', name: '系统默认模板', type: PromptTemplateType.system, )); debugPrint(' + 系统默认模板: system_default_${featureType.toString()} - 系统默认模板'); } // 2. 添加用户自定义提示词模板 for (final userPrompt in package.userPrompts) { templates.add(PromptTemplateOption( id: userPrompt.id, name: userPrompt.name, type: PromptTemplateType.private, usageCount: userPrompt.usageCount, )); debugPrint(' + 用户模板: ${userPrompt.id} - ${userPrompt.name}'); } // 3. 添加公开提示词模板(视为系统模板) for (final publicPrompt in package.publicPrompts) { templates.add(PromptTemplateOption( id: 'public_${publicPrompt.id}', // 添加前缀避免ID冲突 name: publicPrompt.name, type: PromptTemplateType.public, isVerified: publicPrompt.isVerified, )); debugPrint(' + 公开模板: public_${publicPrompt.id} - ${publicPrompt.name}'); } debugPrint('✅ [_PromptTemplateDropdown] 提取完成,总模板数: ${templates.length}'); return templates; } /// 过滤模板选项,根据允许的类型与是否仅允许已验证公共模板 List _filterTemplates( List options, Set? allowed, bool onlyVerifiedPublic, ) { if (allowed == null || allowed.isEmpty) return options; return options.where((o) { if (!allowed.contains(o.type)) return false; if (onlyVerifiedPublic && o.type == PromptTemplateType.public && !o.isVerified) return false; return true; }).toList(); } /// 解析功能类型字符串 AIFeatureType? _parseFeatureType(String featureTypeString) { try { return AIFeatureTypeHelper.fromApiString(featureTypeString.toUpperCase()); } catch (e) { debugPrint('无法解析功能类型: $featureTypeString'); return null; } } } /// 🚀 新增:模板类型 enum PromptTemplateType { system, public, private } /// 🚀 新增:提示词模板选项数据模型 class PromptTemplateOption { final String id; final String name; final PromptTemplateType type; final int? usageCount; // 仅 private 关心 final bool isVerified; // 仅 public 关心 const PromptTemplateOption({ required this.id, required this.name, required this.type, this.usageCount, this.isVerified = false, }); } /// 🚀 新增:更美观的下拉组件(带标签/次数) class _PromptTemplatePrettyDropdown extends StatelessWidget { const _PromptTemplatePrettyDropdown({ required this.options, required this.selectedId, required this.onChanged, this.onEdit, }); final List options; final String? selectedId; final ValueChanged onChanged; final VoidCallback? onEdit; @override Widget build(BuildContext context) { final isDark = WebTheme.isDarkMode(context); final selected = options.firstWhere( (o) => o.id == selectedId, orElse: () => const PromptTemplateOption(id: '', name: '', type: PromptTemplateType.private), ); final hasSelection = selectedId != null && selected.id.isNotEmpty; return Builder( builder: (buttonContext) => Material( type: MaterialType.transparency, child: InkWell( onTap: () => _showMenu(buttonContext), borderRadius: BorderRadius.circular(8), child: Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer, border: Border.all(color: Theme.of(context).colorScheme.outlineVariant, width: 1), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( _iconForType(hasSelection ? selected.type : null), size: 16, color: hasSelection ? _iconColorForType(context, selected.type) : (isDark ? WebTheme.darkGrey400 : WebTheme.grey400), ), const SizedBox(width: 8), Expanded( child: Text( hasSelection ? selected.name : '选择提示词模板', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14, fontWeight: hasSelection ? FontWeight.w500 : FontWeight.normal, color: hasSelection ? (isDark ? WebTheme.darkGrey900 : WebTheme.grey900) : (isDark ? WebTheme.darkGrey500 : WebTheme.grey500), ), ), ), const SizedBox(width: 8), if (hasSelection) _buildTrailingTag(context, selected), Icon( Icons.keyboard_arrow_down, size: 16, color: isDark ? WebTheme.darkGrey600 : WebTheme.grey400, ), const SizedBox(width: 4), // 右侧编辑按钮(当已选择模板时显示) if (hasSelection) Tooltip( message: '编辑提示词', child: InkWell( borderRadius: BorderRadius.circular(6), onTap: onEdit, child: const Padding( padding: EdgeInsets.all(2), child: Icon(Icons.edit_outlined, size: 16), ), ), ), ], ), ), ), ), ); } void _showMenu(BuildContext context) { final renderBox = context.findRenderObject() as RenderBox; final offset = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; showMenu ( context: context, position: RelativeRect.fromLTRB( offset.dx, offset.dy + size.height + 4, offset.dx + size.width, offset.dy + size.height + 4, ), items: [ PopupMenuItem ( value: null, child: Row( children: [ Icon(Icons.block, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(width: 8), const Text('不关联模板'), ], ), ), const PopupMenuDivider(height: 8), ...options.map((o) => PopupMenuItem ( value: o.id, child: Row( children: [ Icon(_iconForType(o.type), size: 16, color: _iconColorForType(context, o.type)), const SizedBox(width: 8), Expanded( child: Row( children: [ Expanded( child: Text( o.name, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14), ), ), if (o.isVerified && o.type == PromptTemplateType.public) ...[ const SizedBox(width: 6), Icon(Icons.verified, size: 16, color: Theme.of(context).colorScheme.primary), ], ], ), ), const SizedBox(width: 8), _buildTrailingTag(context, o), ], ), )), ], elevation: 8, color: Theme.of(context).colorScheme.surfaceContainer, shadowColor: WebTheme.getShadowColor(context, opacity: 0.12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 1), ), ).then((String? value) { onChanged(value); }); } static IconData _iconForType(PromptTemplateType? type) { switch (type) { case PromptTemplateType.system: return Icons.settings; case PromptTemplateType.public: return Icons.public; case PromptTemplateType.private: return Icons.person; default: return Icons.description; } } static Color _iconColorForType(BuildContext context, PromptTemplateType type) { final colorScheme = Theme.of(context).colorScheme; switch (type) { case PromptTemplateType.system: return colorScheme.primary; case PromptTemplateType.public: return colorScheme.secondary; case PromptTemplateType.private: return colorScheme.tertiary; } } Widget _buildTrailingTag(BuildContext context, PromptTemplateOption option) { switch (option.type) { case PromptTemplateType.system: return _buildTag(context, label: '系统', color: Theme.of(context).colorScheme.primary); case PromptTemplateType.public: return _buildTag(context, label: '公共', color: Theme.of(context).colorScheme.secondary); case PromptTemplateType.private: final count = option.usageCount ?? 0; return _buildTag( context, label: count > 0 ? '${count}次' : '私有', color: count > 0 ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.onSurfaceVariant, ); } } Widget _buildTag(BuildContext context, {required String label, required Color color}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.3), width: 1), ), child: Text( label, style: TextStyle( fontSize: 11, color: color, fontWeight: FontWeight.w600, ), ), ); } }