import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ainoval/models/preset_models.dart'; import 'package:ainoval/models/ai_request_models.dart'; import 'package:ainoval/models/user_ai_model_config_model.dart'; import 'package:ainoval/models/novel_structure.dart'; import 'package:ainoval/models/novel_setting_item.dart'; import 'package:ainoval/models/setting_group.dart'; import 'package:ainoval/models/novel_snippet.dart'; import 'package:ainoval/blocs/preset/preset_bloc.dart'; import 'package:ainoval/blocs/preset/preset_state.dart'; import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart'; import 'package:ainoval/blocs/public_models/public_models_bloc.dart'; import 'package:ainoval/blocs/universal_ai/universal_ai_bloc.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:ainoval/config/provider_icons.dart'; import 'package:ainoval/config/app_config.dart'; import 'package:ainoval/models/unified_ai_model.dart'; import 'package:ainoval/models/context_selection_models.dart'; import 'package:ainoval/screens/editor/components/ai_dialog_common_logic.dart'; import 'package:ainoval/widgets/common/top_toast.dart'; /// 基于MenuAnchor的预设快捷菜单组件(重构版本) /// 使用Flutter官方推荐的MenuAnchor组件实现级联菜单功能 class PresetQuickMenuRefactored extends StatefulWidget { const PresetQuickMenuRefactored({ super.key, required this.requestType, required this.selectedText, this.defaultModel, required this.onPresetSelected, required this.onAdjustAndGenerate, this.onPresetWithModelSelected, this.onStreamingGenerate, this.onMenuClosed, this.novel, this.settings = const [], this.settingGroups = const [], this.snippets = const [], }); final AIRequestType requestType; final String selectedText; final UserAIModelConfigModel? defaultModel; final Function(AIPromptPreset preset) onPresetSelected; final Function() onAdjustAndGenerate; final Function(AIPromptPreset preset, UnifiedAIModel model)? onPresetWithModelSelected; final Function(UniversalAIRequest, UnifiedAIModel)? onStreamingGenerate; final VoidCallback? onMenuClosed; final Novel? novel; final List settings; final List settingGroups; final List snippets; @override State createState() => _PresetQuickMenuRefactoredState(); } class _PresetQuickMenuRefactoredState extends State with AIDialogCommonLogic { static const String _tag = 'PresetQuickMenuRefactored'; final MenuController _menuController = MenuController(); // 级联菜单管理 OverlayEntry? _cascadeMenuOverlay; AIPromptPreset? _currentHoveredPreset; bool _isHoveringCascadeMenu = false; Timer? _cascadeHideTimer; Timer? _cascadeShowTimer; double _cascadeMenuMaxHeight = 300.0; // 🚀 移除缓存机制 - 缓存会导致数据更新后仍显示旧数据 // 预设分类计算成本不高,但数据一致性更重要 @override void dispose() { _removeCascadeMenu(); super.dispose(); } /// 移除级联菜单 void _removeCascadeMenu() { _cascadeHideTimer?.cancel(); _cascadeHideTimer = null; _cascadeShowTimer?.cancel(); _cascadeShowTimer = null; _cascadeMenuOverlay?.remove(); _cascadeMenuOverlay = null; _currentHoveredPreset = null; _isHoveringCascadeMenu = false; } /// 延迟移除级联菜单(允许鼠标移到级联菜单上) void _scheduleCascadeMenuRemoval() { _cascadeHideTimer?.cancel(); _cascadeHideTimer = Timer(const Duration(milliseconds: 420), () { if (mounted && !_isHoveringCascadeMenu) { _removeCascadeMenu(); } }); } /// 请求显示级联菜单(防抖,避免闪烁) void _requestShowCascadeMenu(BuildContext context, AIPromptPreset preset, GlobalKey presetKey) { // 若已显示相同预设的子菜单,只需取消隐藏定时器 if (_currentHoveredPreset == preset && _cascadeMenuOverlay != null) { _cascadeHideTimer?.cancel(); return; } _cascadeShowTimer?.cancel(); _cascadeHideTimer?.cancel(); _cascadeShowTimer = Timer(const Duration(milliseconds: 120), () { if (!mounted) return; _showCascadeMenu(context, preset, presetKey); }); } /// 显示级联菜单 void _showCascadeMenu(BuildContext context, AIPromptPreset preset, GlobalKey presetKey) { // 如果是同一个预设,不重复显示 if (_currentHoveredPreset == preset) return; // 移除现有的级联菜单 _removeCascadeMenu(); _currentHoveredPreset = preset; // 获取预设项的位置 final RenderBox? presetBox = presetKey.currentContext?.findRenderObject() as RenderBox?; if (presetBox == null) return; final presetPosition = presetBox.localToGlobal(Offset.zero); final presetSize = presetBox.size; // 计算屏幕可用高度,尽可能显示更多内容 final double screenHeight = MediaQuery.of(context).size.height; final double overlayTop = (presetPosition.dy - 4).clamp(8.0, screenHeight - 100.0); final double availableBelow = (screenHeight - overlayTop - 8).clamp(100.0, screenHeight); _cascadeMenuMaxHeight = availableBelow; // 创建级联菜单 _cascadeMenuOverlay = OverlayEntry( builder: (context) => Positioned( left: presetPosition.dx + presetSize.width + 8, // 在预设项右侧 top: overlayTop, // 稍微向上对齐,并根据屏幕高度约束 child: MouseRegion( onEnter: (_) { // 鼠标进入级联菜单,保持显示 _isHoveringCascadeMenu = true; _cascadeHideTimer?.cancel(); }, onExit: (_) { // 鼠标离开级联菜单,延迟移除 _isHoveringCascadeMenu = false; _scheduleCascadeMenuRemoval(); }, child: _buildCascadeModelMenu(context, preset), ), ), ); Overlay.of(context).insert(_cascadeMenuOverlay!); } /// 构建级联模型菜单 Widget _buildCascadeModelMenu(BuildContext context, AIPromptPreset preset) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return NotificationListener( onNotification: (notification) { // 阻止所有滚动通知传播 return true; }, child: Listener( // 阻止滚动事件传播到父组件 onPointerSignal: (event) { if (event is PointerScrollEvent) { // 完全阻止滚动事件传播到父组件 return; } }, child: Material( elevation: isDark ? 16.0 : 12.0, shadowColor: Colors.black.withOpacity(isDark ? 0.4 : 0.2), borderRadius: BorderRadius.circular(8), color: isDark ? cs.surface.withOpacity(0.98) : cs.surface, child: Container( width: 220, constraints: BoxConstraints( maxHeight: _cascadeMenuMaxHeight, minHeight: 100, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all( color: cs.outlineVariant.withOpacity(isDark ? 0.3 : 0.4), width: 1.0, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 标题 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: cs.primaryContainer.withOpacity(0.1), borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), ), ), child: Row( children: [ Icon(Icons.memory, size: 16, color: cs.primary), const SizedBox(width: 6), Expanded( child: Text( '选择模型', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, color: cs.onSurface, ), ), ), ], ), ), // 模型列表 Flexible( child: BlocBuilder( builder: (context, state) { return _buildCascadeModelList(context, preset, state); }, ), ), ], ), ), ), ), ); } /// 构建级联模型列表 Widget _buildCascadeModelList(BuildContext context, AIPromptPreset preset, AiConfigState state) { return BlocBuilder( builder: (context, publicState) { final allModels = _combineModels(state, publicState); if (allModels.isEmpty) { return const SizedBox( height: 80, child: Center(child: Text('无可用模型')), ); } // 按提供商分组模型 final grouped = _groupUnifiedModelsByProvider(allModels); final providers = grouped.keys.toList(); // 供应商排序:有系统模型的供应商优先 providers.sort((a, b) { final aHasPublic = grouped[a]!.any((m) => m.isPublic); final bHasPublic = grouped[b]!.any((m) => m.isPublic); if (aHasPublic && !bHasPublic) return -1; if (!aHasPublic && bHasPublic) return 1; return a.compareTo(b); }); return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), shrinkWrap: true, itemCount: providers.length, itemBuilder: (context, providerIndex) { final provider = providers[providerIndex]; final models = grouped[provider]!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 提供商标题 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( children: [ _getProviderIcon(context, provider), const SizedBox(width: 6), Text( provider.toUpperCase(), style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w700, letterSpacing: 0.5, ), ), ], ), ), // 该提供商下的模型 ...models.map((model) { return Material( color: Colors.transparent, child: InkWell( onTap: () async { _removeCascadeMenu(); // 也关闭主菜单 widget.onMenuClosed?.call(); _handleModelSelected(preset, model); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( children: [ _getModelIcon(context, model.provider), const SizedBox(width: 8), Expanded( child: Text( model.displayName, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ), ..._buildModelTags(context, model), ], ), ), ), ); }).toList(), if (providerIndex < providers.length - 1) const Divider(height: 4, thickness: 0.3), ], ); }, ); }, ); } @override Widget build(BuildContext context) { // 🚀 确保有UniversalAIBloc可用于积分预估 return BlocProvider.value( value: context.read(), child: _buildDirectMenu(context), ); } /// 直接构建菜单,不使用MenuAnchor避免ParentDataWidget冲突 Widget _buildDirectMenu(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return NotificationListener( onNotification: (notification) { // 阻止所有滚动通知传播 return true; }, child: Listener( // 阻止滚动事件传播到父组件 onPointerSignal: (event) { if (event is PointerScrollEvent) { // 完全阻止滚动事件传播到父组件 return; } }, child: Material( elevation: isDark ? 16.0 : 12.0, shadowColor: Colors.black.withOpacity(isDark ? 0.4 : 0.2), borderRadius: BorderRadius.circular(12), color: isDark ? cs.surface.withOpacity(0.98) : cs.surface, child: Container( width: 260, constraints: const BoxConstraints( maxHeight: 600, minHeight: 180, ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( color: cs.outlineVariant.withOpacity(isDark ? 0.3 : 0.4), width: 1.0, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 预设列表 Flexible( child: BlocBuilder( builder: (context, state) { return _buildPresetList(context, state); }, ), ), // 底部按钮(始终可见) _buildBottomSection(context), ], ), ), ), ), ); } /// 构建菜单标题 - 已移除 Widget _buildMenuHeader(BuildContext context) { // 移除预设头,直接返回空容器 return const SizedBox.shrink(); } /// 构建预设列表 Widget _buildPresetList(BuildContext context, PresetState state) { if (state.isLoading) { return const SizedBox( height: 120, child: Center(child: CircularProgressIndicator()), ); } if (state.errorMessage != null) { return SizedBox( height: 120, child: Center( child: Text( '加载失败: ${state.errorMessage}', style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ); } // 按优先级分类预设 final categorizedPresets = _categorizePresets(state, widget.requestType.value); if (categorizedPresets.isEmpty) { return const SizedBox( height: 120, child: Center(child: Text('暂无可用预设')), ); } return Container( constraints: const BoxConstraints(maxHeight: 480), child: ListView( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), shrinkWrap: true, children: _buildPresetItems(context, categorizedPresets), ), ); } /// 构建预设项列表 List _buildPresetItems(BuildContext context, Map> categorizedPresets) { final List items = []; final categoryOrder = ['quick', 'system', 'public', 'user']; final categoryLabels = { 'quick': '快捷预设', 'system': '系统预设', 'public': '公共预设', 'user': '用户预设', }; bool needsDivider = false; for (final category in categoryOrder) { if (categorizedPresets.containsKey(category)) { final presets = categorizedPresets[category]!; // 添加分隔线(除了第一个分类) if (needsDivider) { items.add(const Divider(height: 1, thickness: 0.3, indent: 12, endIndent: 12)); } // 添加分类标题(如果有多个分类) if (categorizedPresets.length > 1) { items.add(_buildCategoryHeader(context, categoryLabels[category]!)); } // 添加该分类下的预设项 for (final preset in presets) { final isQuickAccess = category == 'quick'; items.add(_buildPresetItem(context, preset, isQuickAccess)); } needsDivider = true; } } return items; } /// 构建分类标题 Widget _buildCategoryHeader(BuildContext context, String label) { final cs = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Text( label, style: Theme.of(context).textTheme.labelMedium?.copyWith( color: cs.primary, fontWeight: FontWeight.w600, letterSpacing: 0.3, ), ), ); } /// 构建预设项 - 优化布局,移除图标,减少高度 Widget _buildPresetItem(BuildContext context, AIPromptPreset preset, bool isQuickAccess) { final cs = Theme.of(context).colorScheme; final GlobalKey presetKey = GlobalKey(); return Container( key: presetKey, margin: const EdgeInsets.only(bottom: 1), child: Material( color: Colors.transparent, child: MouseRegion( onEnter: (_) { if (widget.onPresetWithModelSelected != null) { _requestShowCascadeMenu(context, preset, presetKey); } }, onExit: (_) { if (widget.onPresetWithModelSelected != null) { _scheduleCascadeMenuRemoval(); } }, child: InkWell( onTap: () { if (widget.onPresetWithModelSelected != null) { _showModelSelectionDialog(context, preset); } else { widget.onPresetSelected(preset); } }, borderRadius: BorderRadius.circular(6), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( children: [ // 预设信息 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( preset.displayName, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, color: cs.onSurface, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (preset.presetDescription != null && preset.presetDescription!.isNotEmpty) ...[ const SizedBox(height: 2), Text( preset.presetDescription!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: cs.onSurfaceVariant, height: 1.3, fontSize: 12, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ), // 指示器 if (widget.onPresetWithModelSelected != null) ...[ const SizedBox(width: 8), Icon( Icons.keyboard_arrow_right, size: 16, color: cs.onSurfaceVariant.withOpacity(0.7), ), ], ], ), ), ), ), ), ); } /// 显示模型选择对话框 void _showModelSelectionDialog(BuildContext context, AIPromptPreset preset) { final cs = Theme.of(context).colorScheme; showDialog( context: context, builder: (BuildContext context) { return BlocBuilder( builder: (context, aiState) { return BlocBuilder( builder: (context, publicState) { final allModels = _combineModels(aiState, publicState); if (allModels.isEmpty) { return AlertDialog( title: const Text('无可用模型'), content: const Text('请先配置AI模型后再使用预设功能'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('确定'), ), ], ); } // 按提供商分组模型 final grouped = _groupUnifiedModelsByProvider(allModels); final providers = grouped.keys.toList(); // 供应商排序:有系统模型的供应商优先 providers.sort((a, b) { final aHasPublic = grouped[a]!.any((m) => m.isPublic); final bHasPublic = grouped[b]!.any((m) => m.isPublic); if (aHasPublic && !bHasPublic) return -1; if (!aHasPublic && bHasPublic) return 1; return a.compareTo(b); }); return AlertDialog( title: Text('选择模型 - ${preset.displayName}'), contentPadding: const EdgeInsets.symmetric(vertical: 16), content: SizedBox( width: 320, height: 400, child: ListView.builder( itemCount: providers.length, itemBuilder: (context, providerIndex) { final provider = providers[providerIndex]; final models = grouped[provider]!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 提供商标题 Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), child: Row( children: [ _getProviderIcon(context, provider), const SizedBox(width: 8), Text( provider.toUpperCase(), style: Theme.of(context).textTheme.labelMedium?.copyWith( color: cs.primary, fontWeight: FontWeight.w700, letterSpacing: 1, ), ), ], ), ), // 该提供商下的模型 ...models.map((model) { return ListTile( dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), leading: _getModelIcon(context, model.provider), title: Text( model.displayName, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, ), ), subtitle: _buildModelSubtitle(context, model), onTap: () async { Navigator.of(context).pop(); _handleModelSelected(preset, model); }, ); }).toList(), if (providerIndex < providers.length - 1) const Divider(height: 8, thickness: 0.5), ], ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), ], ); }, ); }, ); }, ); } /// 按提供商分组模型 static Map> _groupModelsByProvider( List configs) { final Map> grouped = {}; for (var c in configs) { grouped.putIfAbsent(c.provider, () => []); grouped[c.provider]!.add(c); } for (var list in grouped.values) { list.sort((a, b) { if (a.isDefault && !b.isDefault) return -1; if (!a.isDefault && b.isDefault) return 1; return a.name.compareTo(b.name); }); } return grouped; } /// 获取提供商图标 Widget _getProviderIcon(BuildContext context, String provider) { try { final color = ProviderIcons.getProviderColor(provider); final isDark = Theme.of(context).brightness == Brightness.dark; return Container( width: 16, height: 16, decoration: BoxDecoration( color: isDark ? Colors.white.withOpacity(0.9) : color.withOpacity(0.12), borderRadius: BorderRadius.circular(3), border: Border.all( color: isDark ? color.withOpacity(0.3) : color.withOpacity(0.25), width: 0.5, ), ), child: Padding( padding: const EdgeInsets.all(1), child: ProviderIcons.getProviderIcon(provider, size: 10, useHighQuality: true), ), ); } catch (e) { return Container( width: 16, height: 16, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(3), ), child: Icon( Icons.memory, size: 10, color: Theme.of(context).colorScheme.primary, ), ); } } /// 获取模型图标 Widget _getModelIcon(BuildContext context, String provider) { try { final color = ProviderIcons.getProviderColor(provider); return Container( width: 14, height: 14, decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(2), ), child: Icon( Icons.memory, size: 8, color: color, ), ); } catch (e) { return Container( width: 14, height: 14, decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondary.withOpacity(0.1), borderRadius: BorderRadius.circular(2), ), child: Icon( Icons.memory, size: 8, color: Theme.of(context).colorScheme.secondary, ), ); } } /// 构建底部操作区域 Widget _buildBottomSection(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), decoration: BoxDecoration( border: Border( top: BorderSide( color: cs.outlineVariant.withOpacity(0.3), width: 0.5, ), ), ), child: SizedBox( width: double.infinity, height: 32, child: ElevatedButton.icon( onPressed: widget.onAdjustAndGenerate, icon: Icon(Icons.tune_rounded, size: 14, color: cs.primary), label: Text( '调整并生成', style: TextStyle( fontWeight: FontWeight.w500, color: cs.primary, fontSize: 13, ), ), style: ElevatedButton.styleFrom( foregroundColor: cs.primary, backgroundColor: cs.primaryContainer.withOpacity(0.12), padding: const EdgeInsets.symmetric(horizontal: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), side: BorderSide( color: cs.primary.withOpacity(0.3), width: 1.0, ), ), elevation: 0, ), ), ), ); } /// 按优先级分类预设(移除缓存机制确保数据一致性) Map> _categorizePresets(PresetState state, String featureType) { // 🚀 移除缓存逻辑,确保每次都获取最新数据 final Map> categorized = { 'quick': [], // 用户快捷预设 'system': [], // 系统预设 'public': [], // 公共预设 'user': [], // 其他用户预设 }; final Set seenIds = {}; // 1. 优先处理快捷访问预设 for (final preset in state.quickAccessPresets) { if (preset.aiFeatureType == featureType && !seenIds.contains(preset.presetId)) { categorized['quick']!.add(preset); seenIds.add(preset.presetId); } } // 2. 处理分组预设中的预设(优先 groupedPresets,保证最新状态) final currentGroupedPresets = state.groupedPresets[featureType] ?? []; for (final preset in currentGroupedPresets) { if (!seenIds.contains(preset.presetId)) { if (preset.isSystem) { categorized['system']!.add(preset); } else { categorized['user']!.add(preset); } seenIds.add(preset.presetId); } } // 3. 处理聚合数据中的预设 if (state.allPresetData != null) { final allData = state.allPresetData!; // 系统预设 for (final preset in allData.systemPresets) { if (preset.aiFeatureType == featureType && !seenIds.contains(preset.presetId)) { categorized['system']!.add(preset); seenIds.add(preset.presetId); } } // 公共预设(这里假设有公共预设字段,如果没有可以忽略) // 由于代码中没有明确的公共预设字段,暂时跳过 // 用户预设 final userPresets = allData.userPresetsByFeatureType[featureType] ?? []; for (final preset in userPresets) { if (!seenIds.contains(preset.presetId)) { categorized['user']!.add(preset); seenIds.add(preset.presetId); } } } // 3. 处理分组预设中的剩余预设 for (final preset in currentGroupedPresets) { if (!seenIds.contains(preset.presetId)) { // 根据isSystem字段判断分类 if (preset.isSystem) { categorized['system']!.add(preset); } else { categorized['user']!.add(preset); } seenIds.add(preset.presetId); } } // 移除空分类 categorized.removeWhere((key, value) => value.isEmpty); // 🚀 移除缓存存储,确保数据一致性 // AppLogger.d(_tag, '预设分类结果: 功能类型=$featureType, 分类=${categorized.keys.join(", ")}'); return categorized; } /// 合并私有模型和公共模型 List _combineModels(AiConfigState aiState, PublicModelsState publicState) { final List allModels = []; // 添加已验证的私有模型 final validatedConfigs = aiState.validatedConfigs; for (final config in validatedConfigs) { allModels.add(PrivateAIModel(config)); } // 添加公共模型 if (publicState is PublicModelsLoaded) { for (final publicModel in publicState.models) { allModels.add(PublicAIModel(publicModel)); } } return allModels; } /// 按提供商分组统一模型 Map> _groupUnifiedModelsByProvider(List models) { final Map> grouped = {}; for (var model in models) { final provider = model.provider; grouped.putIfAbsent(provider, () => []); grouped[provider]!.add(model); } // 对每个供应商内的模型进行排序 for (var list in grouped.values) { list.sort((a, b) { // 系统模型(公共模型)优先 if (a.isPublic && !b.isPublic) return -1; if (!a.isPublic && b.isPublic) return 1; // 如果都是公共模型,按优先级排序 if (a.isPublic && b.isPublic) { final aPriority = (a as PublicAIModel).publicConfig.priority ?? 0; final bPriority = (b as PublicAIModel).publicConfig.priority ?? 0; if (aPriority != bPriority) { return bPriority.compareTo(aPriority); // 优先级高的在前 } } // 如果都是私有模型,默认配置在前 if (!a.isPublic && !b.isPublic) { final aIsDefault = (a as PrivateAIModel).userConfig.isDefault; final bIsDefault = (b as PrivateAIModel).userConfig.isDefault; if (aIsDefault && !bIsDefault) return -1; if (!aIsDefault && bIsDefault) return 1; } // 最后按名称排序 return a.displayName.compareTo(b.displayName); }); } return grouped; } /// 处理模型选择 - 支持公共模型和私有模型 void _handleModelSelected(AIPromptPreset preset, UnifiedAIModel model) async { try { AppLogger.i(_tag, '选择模型: ${model.displayName} (公共: ${model.isPublic})'); // 🚀 对于公共模型,先进行积分预估和确认 if (model.isPublic) { AppLogger.i(_tag, '检测到公共模型,启动积分预估确认流程: ${model.displayName}'); // 构建用于积分预估的请求对象 final estimationRequest = _buildEstimationRequest(preset, model); bool shouldProceed = await handlePublicModelCreditConfirmation(model, estimationRequest); if (!shouldProceed) { AppLogger.i(_tag, '用户取消了积分预估确认,停止操作'); return; // 用户取消或积分不足,停止执行 } AppLogger.i(_tag, '用户确认了积分预估,继续操作'); } else { AppLogger.i(_tag, '检测到私有模型,直接操作: ${model.displayName}'); } // 🚀 先缓存回调,避免异步期间组件被卸载导致无法调用 final streamingGenerate = widget.onStreamingGenerate; final presetWithModel = widget.onPresetWithModelSelected; // 🚀 优先启动流式生成(如果回调可用) if (streamingGenerate != null) { _startStreamingGeneration(preset, model, callback: streamingGenerate); } else { // 回退到传统回调 presetWithModel?.call(preset, model); } AppLogger.i(_tag, '模型选择完成: 预设=${preset.presetName}, 模型=${model.displayName}'); } catch (e) { AppLogger.e(_tag, '处理模型选择失败', e); if (mounted) { TopToast.error(context, '模型选择失败: $e'); } } } /// 构建用于积分预估的请求对象 UniversalAIRequest _buildEstimationRequest(AIPromptPreset preset, UnifiedAIModel model) { // 🚀 使用公共逻辑创建模型配置 final modelConfig = createModelConfig(model); // 🚀 从预设中解析参数和上下文选择(用于积分预估) final parsedRequest = preset.parsedRequest; double temperature = 0.7; double topP = 0.9; int maxTokens = 4000; bool enableSmartContext = false; String? promptTemplateId; ContextSelectionData contextSelectionData; if (parsedRequest != null) { // 从预设中读取参数 final presetTemperature = parsedRequest.parameters['temperature']; if (presetTemperature is double) { temperature = presetTemperature; } else if (presetTemperature is num) { temperature = presetTemperature.toDouble(); } final presetTopP = parsedRequest.parameters['topP']; if (presetTopP is double) { topP = presetTopP; } else if (presetTopP is num) { topP = presetTopP.toDouble(); } final presetMaxTokens = parsedRequest.parameters['maxTokens']; if (presetMaxTokens is int) { maxTokens = presetMaxTokens; } else if (presetMaxTokens is num) { maxTokens = presetMaxTokens.toInt(); } enableSmartContext = parsedRequest.enableSmartContext; // 🚀 从预设中读取提示词模板ID(用于积分预估) final presetTemplateId = parsedRequest.parameters['promptTemplateId'] ?? parsedRequest.parameters['associatedTemplateId']; if (presetTemplateId is String && presetTemplateId.isNotEmpty) { promptTemplateId = presetTemplateId; AppLogger.i(_tag, '🔧 积分预估 - 从预设中读取提示词模板ID: $promptTemplateId'); } // 🚀 从预设中读取上下文选择数据(用于积分预估) if (parsedRequest.contextSelections != null) { contextSelectionData = parsedRequest.contextSelections!; AppLogger.i(_tag, '🔧 积分预估 - 从预设中读取上下文选择: ${contextSelectionData.selectedCount}个项目'); } else { // 创建空的上下文选择数据 contextSelectionData = ContextSelectionData( novelId: widget.novel?.id ?? 'unknown', availableItems: [], flatItems: {}, ); AppLogger.i(_tag, '🔧 积分预估 - 预设中没有上下文选择,使用空数据'); } AppLogger.i(_tag, '🔧 积分预估 - 从预设中读取参数: temperature=$temperature, topP=$topP, maxTokens=$maxTokens, enableSmartContext=$enableSmartContext'); } else { AppLogger.w(_tag, '⚠️ 积分预估 - 无法解析预设参数,使用默认值'); // 创建空的上下文选择数据 contextSelectionData = ContextSelectionData( novelId: widget.novel?.id ?? 'unknown', availableItems: [], flatItems: {}, ); } // 🚀 使用公共逻辑创建元数据 final metadata = createModelMetadata(model, { 'action': widget.requestType.name, 'source': 'preset_quick_menu', 'presetId': preset.presetId, 'presetName': preset.presetName, 'originalLength': widget.selectedText.length, 'contextCount': contextSelectionData.selectedCount, // 🚀 使用实际的上下文数量 'enableSmartContext': enableSmartContext, }); return UniversalAIRequest( requestType: widget.requestType, userId: AppConfig.userId ?? 'unknown', novelId: widget.novel?.id, modelConfig: modelConfig, selectedText: widget.selectedText, instructions: preset.effectiveUserPrompt, // 使用预设的提示词 contextSelections: contextSelectionData, enableSmartContext: enableSmartContext, // 🚀 从预设中读取 parameters: { 'temperature': temperature, // 🚀 从预设中读取 'topP': topP, // 🚀 从预设中读取 'maxTokens': maxTokens, // 🚀 从预设中读取 'modelName': model.modelId, 'presetId': preset.presetId, 'presetName': preset.presetName, 'enableSmartContext': enableSmartContext, // 🚀 从预设中读取 if (promptTemplateId != null) 'promptTemplateId': promptTemplateId, // 🚀 从预设中读取模板ID }, metadata: metadata, ); } /// 🚀 启动流式生成(参考 refactor_dialog.dart 的实现) void _startStreamingGeneration(AIPromptPreset preset, UnifiedAIModel model, {required Function(UniversalAIRequest, UnifiedAIModel) callback}) { try { // 🚀 使用公共逻辑创建模型配置 final modelConfig = createModelConfig(model); // 🚀 从预设中解析参数和上下文选择 final parsedRequest = preset.parsedRequest; double temperature = 0.7; double topP = 0.9; int maxTokens = 4000; bool enableSmartContext = false; String? promptTemplateId; ContextSelectionData contextSelectionData; if (parsedRequest != null) { // 从预设中读取参数 final presetTemperature = parsedRequest.parameters['temperature']; if (presetTemperature is double) { temperature = presetTemperature; } else if (presetTemperature is num) { temperature = presetTemperature.toDouble(); } final presetTopP = parsedRequest.parameters['topP']; if (presetTopP is double) { topP = presetTopP; } else if (presetTopP is num) { topP = presetTopP.toDouble(); } final presetMaxTokens = parsedRequest.parameters['maxTokens']; if (presetMaxTokens is int) { maxTokens = presetMaxTokens; } else if (presetMaxTokens is num) { maxTokens = presetMaxTokens.toInt(); } enableSmartContext = parsedRequest.enableSmartContext; // 🚀 从预设中读取提示词模板ID final presetTemplateId = parsedRequest.parameters['promptTemplateId'] ?? parsedRequest.parameters['associatedTemplateId']; if (presetTemplateId is String && presetTemplateId.isNotEmpty) { promptTemplateId = presetTemplateId; AppLogger.i(_tag, '🔧 从预设中读取提示词模板ID: $promptTemplateId'); } // 🚀 从预设中读取上下文选择数据 if (parsedRequest.contextSelections != null) { contextSelectionData = parsedRequest.contextSelections!; AppLogger.i(_tag, '🔧 从预设中读取上下文选择: ${contextSelectionData.selectedCount}个项目'); } else { // 创建空的上下文选择数据 contextSelectionData = ContextSelectionData( novelId: widget.novel?.id ?? 'unknown', availableItems: [], flatItems: {}, ); AppLogger.i(_tag, '🔧 预设中没有上下文选择,使用空数据'); } AppLogger.i(_tag, '🔧 从预设中读取参数: temperature=$temperature, topP=$topP, maxTokens=$maxTokens, enableSmartContext=$enableSmartContext'); // 🔍 调试:输出原始预设数据以排查更新问题 AppLogger.i(_tag, '🔍 预设原始数据:'); AppLogger.i(_tag, ' - presetId: ${preset.presetId}'); AppLogger.i(_tag, ' - presetName: ${preset.presetName}'); AppLogger.i(_tag, ' - requestData前50字符: ${preset.requestData.length > 50 ? preset.requestData.substring(0, 50) + "..." : preset.requestData}'); AppLogger.i(_tag, ' - parsedRequest.parameters: ${parsedRequest.parameters}'); } else { AppLogger.w(_tag, '⚠️ 无法解析预设参数,使用默认值'); // 创建空的上下文选择数据 contextSelectionData = ContextSelectionData( novelId: widget.novel?.id ?? 'unknown', availableItems: [], flatItems: {}, ); } // 🚀 使用公共逻辑创建元数据 final metadata = createModelMetadata(model, { 'action': widget.requestType.name, 'source': 'preset_quick_menu', 'presetId': preset.presetId, 'presetName': preset.presetName, 'originalLength': widget.selectedText.length, 'contextCount': contextSelectionData.selectedCount, // 🚀 使用实际的上下文数量 'enableSmartContext': enableSmartContext, }); // 构建AI请求 final request = UniversalAIRequest( requestType: widget.requestType, userId: AppConfig.userId ?? 'unknown', novelId: widget.novel?.id, modelConfig: modelConfig, selectedText: widget.selectedText, instructions: preset.effectiveUserPrompt, // 使用预设的提示词 contextSelections: contextSelectionData, enableSmartContext: enableSmartContext, // 🚀 从预设中读取 parameters: { 'temperature': temperature, // 🚀 从预设中读取 'topP': topP, // 🚀 从预设中读取 'maxTokens': maxTokens, // 🚀 从预设中读取 'modelName': model.modelId, 'presetId': preset.presetId, 'presetName': preset.presetName, 'enableSmartContext': enableSmartContext, // 🚀 从预设中读取 if (promptTemplateId != null) 'promptTemplateId': promptTemplateId, // 🚀 从预设中读取模板ID }, metadata: metadata, ); // 🚀 调用流式生成回调启动AI生成工具栏 callback(request, model); AppLogger.i(_tag, '流式生成已启动: 预设=${preset.presetName}, 模型=${model.displayName}, 智能上下文=false, 原文长度=${widget.selectedText.length}'); } catch (e) { AppLogger.e(_tag, '启动流式生成失败', e); if (mounted) { TopToast.error(context, '启动生成失败: $e'); } } } /// 构建模型标签 List _buildModelTags(BuildContext context, UnifiedAIModel model) { final List tags = []; // 公共模型标签 if (model.isPublic) { tags.addAll([ const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondary.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( '公共', style: TextStyle( fontSize: 10, color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.w600, ), ), ), ]); } // 默认模型标签 if (!model.isPublic && (model as PrivateAIModel).userConfig.isDefault) { tags.addAll([ const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( '默认', style: TextStyle( fontSize: 10, color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w600, ), ), ), ]); } return tags; } /// 构建模型子标题 Widget? _buildModelSubtitle(BuildContext context, UnifiedAIModel model) { final List subtitles = []; if (model.isPublic) { subtitles.add('公共模型'); } if (!model.isPublic && (model as PrivateAIModel).userConfig.isDefault) { subtitles.add('默认模型'); } if (subtitles.isEmpty) return null; return Text( subtitles.join(' · '), style: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 12, ), ); } }