import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../blocs/ai_config/ai_config_bloc.dart'; import '../../blocs/public_models/public_models_bloc.dart'; import '../../config/app_config.dart'; import '../../config/provider_icons.dart'; import '../../models/ai_request_models.dart'; import '../../models/novel_setting_item.dart'; import '../../models/novel_snippet.dart'; import '../../models/novel_structure.dart'; import '../../models/setting_group.dart'; import '../../models/unified_ai_model.dart'; import '../../models/user_ai_model_config_model.dart'; import '../../models/public_model_config.dart'; import 'top_toast.dart'; import 'unified_ai_model_dropdown.dart'; /// 尺寸变体:根据不同大小展示不同的信息密度 enum ModelDisplaySize { small, medium, large } /// 通用的“模型显示与选择”组件 /// - 支持显示模型名称、标签,可选显示提供商图标 /// - 点击后弹出统一的模型下拉菜单(自动根据空间选择上下方向) class ModelDisplaySelector extends StatefulWidget { const ModelDisplaySelector({ Key? key, this.selectedModel, this.onModelSelected, this.chatConfig, this.onConfigChanged, this.novel, this.settings = const [], this.settingGroups = const [], this.snippets = const [], this.placeholder = '选择模型', this.size = ModelDisplaySize.medium, this.showIcon = true, this.showTags = true, this.showSettingsButton = true, this.width, this.height, }) : super(key: key); final UnifiedAIModel? selectedModel; final ValueChanged? onModelSelected; final UniversalAIRequest? chatConfig; final ValueChanged? onConfigChanged; final Novel? novel; final List settings; final List settingGroups; final List snippets; final String placeholder; final ModelDisplaySize size; final bool showIcon; final bool showTags; final bool showSettingsButton; final double? width; final double? height; // 可覆盖默认高度 @override State createState() => _ModelDisplaySelectorState(); } class _ModelDisplaySelectorState extends State { OverlayEntry? _overlay; bool _autoPickDone = false; @override void initState() { super.initState(); // 首帧尝试自动选择默认模型 WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAutoPickDefault()); } @override void dispose() { _removeOverlay(); super.dispose(); } void _removeOverlay() { if (_overlay != null && _overlay!.mounted) { _overlay!.remove(); } _overlay = null; } void _showDropdown() { if (_overlay != null) { _removeOverlay(); return; } // 兜底:如果没有任何可用模型,提示并返回 final aiState = context.read().state; final publicState = context.read().state; final hasPrivate = aiState.validatedConfigs.isNotEmpty; final hasPublic = publicState is PublicModelsLoaded && publicState.models.isNotEmpty; if (!hasPrivate && !hasPublic) { TopToast.error(context, '暂无可用的AI模型配置'); return; } // 计算触发器组件的全局矩形作为锚点 final RenderBox box = context.findRenderObject() as RenderBox; final Offset globalPosition = box.localToGlobal(Offset.zero); final Rect anchorRect = Rect.fromLTWH( globalPosition.dx, globalPosition.dy, box.size.width, box.size.height, ); _overlay = UnifiedAIModelDropdown.show( context: context, anchorRect: anchorRect, selectedModel: widget.selectedModel, onModelSelected: (unifiedModel) { // 直接回传统一模型 widget.onModelSelected?.call(unifiedModel); // 如果需要同步到聊天配置(保留与旧接口兼容) if (widget.onConfigChanged != null && widget.chatConfig != null && unifiedModel != null) { UserAIModelConfigModel? compatModel; if (unifiedModel.isPublic) { final publicModel = (unifiedModel as PublicAIModel).publicConfig; compatModel = UserAIModelConfigModel.fromJson({ 'id': 'public_${publicModel.id}', 'userId': AppConfig.userId ?? 'unknown', 'alias': publicModel.displayName, 'modelName': publicModel.modelId, 'provider': publicModel.provider, 'apiEndpoint': '', 'isDefault': false, 'isValidated': true, 'createdAt': DateTime.now().toIso8601String(), 'updatedAt': DateTime.now().toIso8601String(), }); } else { compatModel = (unifiedModel as PrivateAIModel).userConfig; } final Map mergedMetadata = { ...?widget.chatConfig?.metadata, 'modelName': unifiedModel.modelId, 'modelProvider': unifiedModel.provider, 'modelConfigId': unifiedModel.id, 'isPublicModel': unifiedModel.isPublic, }; if (unifiedModel.isPublic) { final publicId = (unifiedModel as PublicAIModel).publicConfig.id; mergedMetadata['publicModelConfigId'] = publicId; mergedMetadata['publicModelId'] = publicId; } else { mergedMetadata.remove('publicModelConfigId'); mergedMetadata.remove('publicModelId'); } final updated = widget.chatConfig!.copyWith( modelConfig: compatModel, metadata: mergedMetadata, ); widget.onConfigChanged!(updated); } }, showSettingsButton: widget.showSettingsButton, // 隐藏“调整并生成”入口:小说列表输入框不需要该动作 // 该组件当前仅用于首页/列表输入区,因此固定为false // 如将来复用到其他地方,可将该参数暴露为构造函数可配置 showAdjustAndGenerate: false, novel: widget.novel, settings: widget.settings, settingGroups: widget.settingGroups, snippets: widget.snippets, chatConfig: widget.chatConfig, onConfigChanged: widget.onConfigChanged, onClose: () { _overlay = null; }, ); } void _maybeAutoPickDefault() { if (_autoPickDone) return; if (widget.selectedModel != null) return; final UnifiedAIModel? defaultModel = _computeDefaultModel(); if (defaultModel != null) { _autoPickDone = true; widget.onModelSelected?.call(defaultModel); } } UnifiedAIModel? _computeDefaultModel() { // 优先:已登录用户的默认私有模型 final String? userId = AppConfig.userId; final aiState = context.read().state; if (userId != null) { final defaults = aiState.validatedConfigs.where((c) => c.isDefault).toList(); if (defaults.isNotEmpty) { return PrivateAIModel(defaults.first); } // 可选:如无默认,继续尝试公共模型 } // 未登录或无默认 → 使用公共服务 gemini-2.0(或最优的gemini可用项) final publicState = context.read().state; if (publicState is PublicModelsLoaded) { final List models = publicState.models; PublicModel? target; for (final m in models) { if (m.modelId.toLowerCase() == 'gemini-2.0') { target = m; break; } } if (target == null) { // 选择 provider/modelId 含 gemini 的优先项(按 priority 降序) final geminiCandidates = models.where((m) { final p = m.provider.toLowerCase(); final id = m.modelId.toLowerCase(); return p.contains('gemini') || p.contains('google') || id.contains('gemini'); }).toList(); if (geminiCandidates.isNotEmpty) { geminiCandidates.sort((a, b) => (b.priority ?? 0).compareTo(a.priority ?? 0)); target = geminiCandidates.first; } } if (target != null) { return PublicAIModel(target); } } return null; } String _displayName() { if (widget.selectedModel != null) return widget.selectedModel!.displayName; final configModel = widget.chatConfig?.modelConfig; if (configModel != null) { return configModel.alias.isNotEmpty ? configModel.alias : configModel.modelName; } return widget.placeholder; } double _heightForSize() { if (widget.height != null) return widget.height!; switch (widget.size) { case ModelDisplaySize.small: return 32; case ModelDisplaySize.medium: return 36; case ModelDisplaySize.large: return 44; } } double _fontSizeForSize() { switch (widget.size) { case ModelDisplaySize.small: return 12; case ModelDisplaySize.medium: return 13; case ModelDisplaySize.large: return 14; } } int _maxTagsToShow() { if (!widget.showTags) return 0; switch (widget.size) { case ModelDisplaySize.small: return 1; case ModelDisplaySize.medium: return 2; case ModelDisplaySize.large: return 4; } } @override Widget build(BuildContext context) { // 主题与展示数据 final isDark = Theme.of(context).brightness == Brightness.dark; final textColor = isDark ? const Color(0xFFD1D5DB) : const Color(0xFF374151); final borderColor = isDark ? const Color(0xFF4B5563) : const Color(0xFFD1D5DB); List tags; final sel = widget.selectedModel; if (sel != null) { final bool isPublicById = sel.id.startsWith('public_') || sel.isPublic; if (isPublicById) { tags = ['系统']; } else { tags = sel.modelTags; } } else { final cfgId = widget.chatConfig?.modelConfig?.id; if (cfgId != null && cfgId.startsWith('public_')) { tags = ['系统']; } else { tags = const []; } } final int showTagCount = _maxTagsToShow().clamp(0, tags.length); // 监听相关Bloc以在数据加载后执行一次自动选择 // 注意:仅在尚未自动选择且外部未传入selectedModel时才会触发 // 使用Listener而非Builder,避免无谓重建 final child = GestureDetector( onTap: _showDropdown, child: Container( width: widget.width, height: _heightForSize(), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isDark ? const Color(0xFF374151) : Colors.white, border: Border.all(color: borderColor, width: 1.0), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 1, offset: const Offset(0, 1), ), ], ), child: Row( children: [ if (widget.showIcon) Padding( padding: const EdgeInsets.only(right: 8), child: _buildProviderIcon(), ), Expanded( child: Row( children: [ Expanded( child: Text( _displayName(), style: TextStyle( fontSize: _fontSizeForSize(), fontWeight: FontWeight.w500, color: textColor, ), overflow: TextOverflow.ellipsis, ), ), if (showTagCount > 0) const SizedBox(width: 8), if (showTagCount > 0) Flexible( child: Wrap( spacing: 4, runSpacing: 2, children: tags .take(showTagCount) .map((t) => _TagChip(text: t)) .toList(), ), ), ], ), ), const SizedBox(width: 8), Icon( Icons.expand_more, size: 18, color: isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280), ), ], ), ), ); return MultiBlocListener( listeners: [ BlocListener( listenWhen: (p, c) => !_autoPickDone && widget.selectedModel == null, listener: (context, state) => _maybeAutoPickDefault(), ), BlocListener( listenWhen: (p, c) => !_autoPickDone && widget.selectedModel == null, listener: (context, state) => _maybeAutoPickDefault(), ), ], child: child, ); } Widget _buildProviderIcon() { final model = widget.selectedModel; final isDark = Theme.of(context).brightness == Brightness.dark; if (model == null) { return Icon( Icons.model_training_outlined, size: 16, color: isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280), ); } final color = ProviderIcons.getProviderColor(model.provider); return Container( width: 20, height: 20, 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, ), ), child: Padding( padding: const EdgeInsets.all(2), child: ProviderIcons.getProviderIcon(model.provider, size: 12, useHighQuality: true), ), ); } } class _TagChip extends StatelessWidget { const _TagChip({required this.text}); final String text; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; Color tagColor; Color backgroundColor; Color borderColor; if (text == '私有') { tagColor = Colors.blue; backgroundColor = isDark ? Colors.blue.withOpacity(0.15) : Colors.blue.withOpacity(0.1); borderColor = Colors.blue.withOpacity(isDark ? 0.3 : 0.2); } else if (text == '系统') { tagColor = Colors.green; backgroundColor = isDark ? Colors.green.withOpacity(0.15) : Colors.green.withOpacity(0.1); borderColor = Colors.green.withOpacity(isDark ? 0.3 : 0.2); } else if (text == '推荐') { tagColor = Colors.orange; backgroundColor = isDark ? Colors.orange.withOpacity(0.15) : Colors.orange.withOpacity(0.1); borderColor = Colors.orange.withOpacity(isDark ? 0.3 : 0.2); } else if (text == '免费') { tagColor = Colors.purple; backgroundColor = isDark ? Colors.purple.withOpacity(0.15) : Colors.purple.withOpacity(0.1); borderColor = Colors.purple.withOpacity(isDark ? 0.3 : 0.2); } else if (text.contains('积分')) { tagColor = Colors.red; backgroundColor = isDark ? Colors.red.withOpacity(0.15) : Colors.red.withOpacity(0.1); borderColor = Colors.red.withOpacity(isDark ? 0.3 : 0.2); } else { tagColor = cs.outline; backgroundColor = isDark ? cs.surfaceVariant.withOpacity(0.3) : cs.surfaceVariant.withOpacity(0.5); borderColor = cs.outline.withOpacity(isDark ? 0.3 : 0.2); } return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(8), border: Border.all(color: borderColor, width: 0.5), ), child: Text( text, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: tagColor.withOpacity(isDark ? 0.9 : 0.8), fontSize: 10, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ); } }