import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../blocs/ai_config/ai_config_bloc.dart'; import '../../models/user_ai_model_config_model.dart'; import '../../models/novel_structure.dart'; import '../../models/novel_setting_item.dart'; import '../../models/setting_group.dart'; import '../../models/novel_snippet.dart'; import '../../models/ai_request_models.dart'; import '../../screens/chat/widgets/chat_settings_dialog.dart'; import '../../config/provider_icons.dart'; import 'model_dropdown_menu.dart'; /// 模型选择器公共组件 /// /// 功能特性: /// - 按供应商分组显示模型 /// - 模型图标显示 /// - 默认模型标识 /// - 模型标签支持(如免费标签) /// - 分为模型列表区和底部操作区 class ModelSelector extends StatefulWidget { const ModelSelector({ Key? key, this.selectedModel, required this.onModelSelected, this.onSettingsPressed, this.compact = false, this.showSettingsButton = true, this.maxHeight = 2400, this.novel, this.settings = const [], this.settingGroups = const [], this.snippets = const [], this.chatConfig, this.onConfigChanged, }) : super(key: key); /// 当前选中的模型 final UserAIModelConfigModel? selectedModel; /// 模型选择回调 final Function(UserAIModelConfigModel?) onModelSelected; /// 设置按钮点击回调 final VoidCallback? onSettingsPressed; /// 是否紧凑模式 final bool compact; /// 是否显示设置按钮 final bool showSettingsButton; /// 最大高度 final double maxHeight; /// 小说数据,用于上下文选择 final Novel? novel; /// 设定数据 final List settings; /// 设定组数据 final List settingGroups; /// 片段数据 final List snippets; /// 🚀 聊天配置 final UniversalAIRequest? chatConfig; /// 🚀 配置变更回调 final ValueChanged? onConfigChanged; @override State createState() => _ModelSelectorState(); } class _ModelSelectorState extends State { OverlayEntry? _overlayEntry; final LayerLink _layerLink = LayerLink(); bool _isMenuOpen = false; /// 公开方法:触发菜单显示/隐藏 void showDropdown() { final aiConfigBloc = context.read(); final validatedConfigs = aiConfigBloc.state.validatedConfigs; if (validatedConfigs.isNotEmpty) { _toggleMenu(context, validatedConfigs); } } @override void dispose() { _removeOverlay(); super.dispose(); } void _removeOverlay() { _overlayEntry?.remove(); _overlayEntry = null; _isMenuOpen = false; } void _toggleMenu(BuildContext context, List configs) { if (_isMenuOpen) { _removeOverlay(); } else { _createOverlay(context, configs); _isMenuOpen = true; } } void _createOverlay(BuildContext context, List configs) { _overlayEntry = ModelDropdownMenu.show( context: context, layerLink: _layerLink, configs: configs, selectedModel: widget.selectedModel, onModelSelected: (model) { widget.onModelSelected(model); setState(() {}); }, showSettingsButton: widget.showSettingsButton, maxHeight: widget.maxHeight, novel: widget.novel, settings: widget.settings, settingGroups: widget.settingGroups, snippets: widget.snippets, chatConfig: widget.chatConfig, onConfigChanged: widget.onConfigChanged, onClose: () { _overlayEntry = null; setState(() { _isMenuOpen = false; }); }, ); } Widget _buildMenuContent(List configs) { if (configs.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.model_training_outlined, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5), ), const SizedBox(height: 12), Text( '无可用模型', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Text( '请先配置AI模型', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.7), ), ), ], ), ), ); } return Column( mainAxisSize: MainAxisSize.max, children: [ Expanded( child: _buildModelList(configs), ), if (widget.showSettingsButton) _buildBottomActions(), ], ); } Widget _buildModelList(List configs) { final groupedModels = _groupModelsByProvider(configs); final colorScheme = Theme.of(context).colorScheme; // Sort providers: default provider first, then alphabetically final sortedProviders = groupedModels.keys.toList()..sort((a, b) { final aIsDefault = groupedModels[a]!.any((c) => c.isDefault); final bIsDefault = groupedModels[b]!.any((c) => c.isDefault); if (aIsDefault && !bIsDefault) return -1; if (!aIsDefault && bIsDefault) return 1; return a.compareTo(b); }); return ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), itemCount: sortedProviders.length, separatorBuilder: (context, index) => Divider( height: 16, thickness: 0.8, color: colorScheme.outlineVariant.withOpacity(0.12), indent: 20, endIndent: 20, ), itemBuilder: (context, index) { final provider = sortedProviders[index]; final models = groupedModels[provider]!; return _buildProviderGroup(provider, models); }, ); } Widget _buildProviderGroup(String provider, List models) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 供应商分组标题 - 完全移除图标,增大字体 Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 6), child: Text( provider.toUpperCase(), style: Theme.of(context).textTheme.titleSmall?.copyWith( color: isDark ? colorScheme.primary.withOpacity(0.9) : colorScheme.primary, fontWeight: FontWeight.w700, letterSpacing: 1.0, fontSize: 14, ), ), ), // 该供应商下的模型列表 ...models.map((model) => _buildModelItem(model)).toList(), const SizedBox(height: 6), ], ); } Widget _buildModelItem(UserAIModelConfigModel model) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final isSelected = widget.selectedModel?.id == model.id; final displayName = model.alias.isNotEmpty ? model.alias : model.modelName; return InkWell( onTap: () { widget.onModelSelected(model); _removeOverlay(); }, borderRadius: BorderRadius.circular(10), splashColor: colorScheme.primary.withOpacity(0.08), highlightColor: colorScheme.primary.withOpacity(0.04), child: Container( margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: isSelected ? (isDark ? colorScheme.primaryContainer.withOpacity(0.2) : colorScheme.primaryContainer.withOpacity(0.15)) : null, borderRadius: BorderRadius.circular(8), border: isSelected ? Border.all( color: colorScheme.primary.withOpacity(0.2), width: 1, ) : null, ), child: Row( children: [ // 模型图标 - 外层包装防止突兀 Container( padding: const EdgeInsets.all(2), child: _getModelIcon(model.provider), ), const SizedBox(width: 10), // 模型信息 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 模型名称行 Row( children: [ Flexible( child: Text( displayName, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, color: isSelected ? colorScheme.primary : (isDark ? colorScheme.onSurface.withOpacity(0.9) : colorScheme.onSurface), fontSize: 13, height: 1.2, ), overflow: TextOverflow.ellipsis, ), ), // 默认模型标识 if (model.isDefault) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: isDark ? Colors.amber.withOpacity(0.15) : Colors.amber.withOpacity(0.2), borderRadius: BorderRadius.circular(4), border: Border.all( color: Colors.amber.withOpacity(isDark ? 0.4 : 0.5), width: 0.5, ), ), child: Text( '默认', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: isDark ? Colors.amber.shade300 : Colors.amber.shade700, fontSize: 9, fontWeight: FontWeight.w600, ), ), ), ], ], ), // 模型标签行(预留区域) if (_getModelTags(model).isNotEmpty) ...[ const SizedBox(height: 3), Wrap( spacing: 3, runSpacing: 2, children: _getModelTags(model).map((tag) => _buildModelTag(tag)).toList(), ), ], ], ), ), // 选中标识 if (isSelected) Icon( Icons.check_circle_rounded, size: 16, color: colorScheme.primary, ), ], ), ), ); } Widget _buildModelTag(ModelTag tag) { final isDark = Theme.of(context).brightness == Brightness.dark; MaterialColor tagColor; switch (tag.type) { case ModelTagType.free: tagColor = Colors.green; break; case ModelTagType.premium: tagColor = Colors.purple; break; case ModelTagType.beta: tagColor = Colors.orange; break; default: tagColor = Colors.grey; } return Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: isDark ? tagColor.withOpacity(0.08) : tagColor.withOpacity(0.12), borderRadius: BorderRadius.circular(3), border: Border.all( color: tagColor.withOpacity(isDark ? 0.2 : 0.3), width: 0.5, ), ), child: Text( tag.label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: isDark ? tagColor.shade300 : tagColor.shade700, fontSize: 8, fontWeight: FontWeight.w500, ), ), ); } Widget _buildBottomActions() { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? colorScheme.surface.withOpacity(0.8) : colorScheme.surface, border: Border( top: BorderSide( color: colorScheme.outlineVariant.withOpacity(isDark ? 0.15 : 0.2), width: 1.0, ), ), ), child: SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { _removeOverlay(); // 显示聊天设置对话框 showChatSettingsDialog( context, selectedModel: widget.selectedModel, onModelChanged: (model) { widget.onModelSelected(model); }, onSettingsSaved: () { widget.onSettingsPressed?.call(); }, novel: widget.novel, settings: widget.settings, settingGroups: widget.settingGroups, snippets: widget.snippets, // 🚀 传递聊天配置,确保设置对话框能够同步 initialChatConfig: widget.chatConfig, onConfigChanged: widget.onConfigChanged, initialContextSelections: null, // 🚀 让ChatSettingsDialog自己构建上下文数据 ); }, icon: const Icon(Icons.tune_rounded, size: 18), label: const Text('调整并生成'), style: ElevatedButton.styleFrom( foregroundColor: isDark ? colorScheme.primary.withOpacity(0.9) : colorScheme.primary, backgroundColor: isDark ? colorScheme.primaryContainer.withOpacity(0.08) : colorScheme.primaryContainer.withOpacity(0.1), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 0, side: BorderSide( color: colorScheme.primary.withOpacity(isDark ? 0.2 : 0.3), width: 0.8, ), ), ), ), ); } Map> _groupModelsByProvider( List configs) { final Map> grouped = {}; for (final config in configs) { final provider = config.provider; grouped.putIfAbsent(provider, () => []); grouped[provider]!.add(config); } // 对每个供应商的模型按名称排序,默认模型排在前面 for (final models in grouped.values) { models.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(String provider) { return ProviderIcons.getProviderIconForContext( provider, iconSize: IconSize.small, ); } Widget _getModelIcon(String provider) { final color = ProviderIcons.getProviderColor(provider); final isDark = Theme.of(context).brightness == Brightness.dark; return Container( width: 18, height: 18, decoration: BoxDecoration( color: isDark ? Colors.white.withOpacity(0.9) // 暗黑模式下背景为白色 : color.withOpacity(0.12), borderRadius: BorderRadius.circular(4), border: Border.all( color: isDark ? color.withOpacity(0.3) : color.withOpacity(0.25), width: 0.5, ), boxShadow: isDark ? [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 2, offset: const Offset(0, 1), ), ] : null, ), child: Padding( padding: const EdgeInsets.all(2), child: ProviderIcons.getProviderIcon( provider, size: 10, useHighQuality: true, ), ), ); } List _getModelTags(UserAIModelConfigModel model) { // 根据模型信息返回标签列表 List tags = []; // 示例:根据模型名称或其他属性添加标签 if (model.modelName.toLowerCase().contains('free') || model.modelName.toLowerCase().contains('gpt-3.5')) { tags.add(const ModelTag(label: '免费', type: ModelTagType.free)); } if (model.modelName.toLowerCase().contains('beta')) { tags.add(const ModelTag(label: 'Beta', type: ModelTagType.beta)); } if (model.modelName.toLowerCase().contains('pro') || model.modelName.toLowerCase().contains('gpt-4')) { tags.add(const ModelTag(label: '专业版', type: ModelTagType.premium)); } return tags; } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return BlocBuilder( builder: (context, state) { final validatedConfigs = state.validatedConfigs; // 确定当前选中的模型 UserAIModelConfigModel? currentSelection; if (widget.selectedModel != null && validatedConfigs.any((c) => c.id == widget.selectedModel!.id)) { currentSelection = widget.selectedModel; } else if (state.defaultConfig != null && validatedConfigs.any((c) => c.id == state.defaultConfig!.id)) { currentSelection = state.defaultConfig; } else if (validatedConfigs.isNotEmpty) { currentSelection = validatedConfigs.first; } // 加载状态 if (state.status == AiConfigStatus.loading && validatedConfigs.isEmpty) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest.withOpacity(0.3), borderRadius: BorderRadius.circular(widget.compact ? 12 : 16), border: Border.all( color: colorScheme.outline.withOpacity(0.2), width: 0.8, ), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2.5), ), SizedBox(width: 8), Text('加载中...', style: TextStyle(fontSize: 12)), ], ), ); } // 无模型状态 if (state.status != AiConfigStatus.loading && validatedConfigs.isEmpty) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: colorScheme.errorContainer.withOpacity(0.1), borderRadius: BorderRadius.circular(widget.compact ? 12 : 16), border: Border.all( color: colorScheme.error.withOpacity(0.3), width: 0.8, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.warning_outlined, size: 16, color: colorScheme.error, ), const SizedBox(width: 6), Text( '无可用模型', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: colorScheme.error, ), ), ], ), ); } // 正常状态 - 模型选择器 return CompositedTransformTarget( link: _layerLink, child: Material( color: Colors.transparent, child: InkWell( onTap: validatedConfigs.isNotEmpty ? () => _toggleMenu(context, validatedConfigs) : null, borderRadius: BorderRadius.circular(8), hoverColor: colorScheme.onSurface.withOpacity(0.08), splashColor: colorScheme.onSurface.withOpacity(0.12), child: Container( height: 44, constraints: const BoxConstraints(maxWidth: 128), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: Colors.transparent, border: Border.all( color: Colors.transparent, width: 1, ), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ // 主要内容区域 Expanded( child: Row( children: [ // 文字内容 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ // 第一行:General Chat Text( 'General Chat', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: colorScheme.onSurface, height: 1.2, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), // 第二行:模型名称 Text( _getModelDisplayName(currentSelection), style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: colorScheme.onSurface.withOpacity(0.5), height: 1.2, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), // 下拉箭头 if (validatedConfigs.length > 1) Container( margin: const EdgeInsets.only(left: 8), child: Icon( _isMenuOpen ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, size: 12, color: colorScheme.onSurface.withOpacity(0.4), ), ), ], ), ), ], ), ), ), ), ); }, ); } String _getDisplayText(UserAIModelConfigModel? model) { if (model == null) { return '选择模型'; } final namePart = model.alias.isNotEmpty ? model.alias : model.modelName; return widget.compact ? namePart : '${model.provider}/$namePart'; } String _getModelDisplayName(UserAIModelConfigModel? model) { if (model == null) { return '请选择模型'; } final namePart = model.alias.isNotEmpty ? model.alias : model.modelName; return namePart; } } /// 模型标签数据类 class ModelTag { const ModelTag({ required this.label, required this.type, }); final String label; final ModelTagType type; } /// 模型标签类型枚举 enum ModelTagType { free, // 免费 premium, // 专业版 beta, // 测试版 custom, // 自定义 }