Files
MaliangAINovalWriter/AINoval/lib/widgets/common/unified_ai_model_dropdown.dart
2025-09-10 00:07:52 +08:00

976 lines
34 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// import 'package:ainoval/utils/logger.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../models/unified_ai_model.dart';
import '../../models/user_ai_model_config_model.dart';
// import '../../models/public_model_config.dart';
import '../../models/novel_structure.dart';
import '../../models/novel_setting_item.dart';
import '../../models/setting_group.dart';
import '../../models/novel_snippet.dart';
import '../../blocs/ai_config/ai_config_bloc.dart';
import '../../blocs/public_models/public_models_bloc.dart';
import '../../screens/chat/widgets/chat_settings_dialog.dart';
import '../../config/provider_icons.dart';
import '../../models/ai_request_models.dart';
import '../../screens/editor/managers/editor_layout_manager.dart';
import 'package:provider/provider.dart';
import 'package:ainoval/config/app_config.dart';
import 'package:ainoval/screens/settings/settings_panel.dart';
import 'package:ainoval/screens/editor/managers/editor_state_manager.dart';
import 'package:ainoval/widgets/common/top_toast.dart';
// ==================== 统一 AI 模型下拉菜单 - 尺寸常量定义 ====================
/// 菜单整体尺寸配置
class _MenuDimensions {
/// 菜单固定宽度
static const double menuWidth = 320.0;
/// 菜单默认最大高度
static const double defaultMaxHeight = 900.0;
/// 屏幕边缘的安全边距,防止菜单被状态栏或导航栏遮挡
static const double screenSafeMargin = 80.0;
/// 菜单最小高度(有设置按钮时)
static const double minHeightWithSettings = 180.0;
/// 菜单最小高度(无设置按钮时)
static const double minHeightWithoutSettings = 120.0;
/// 菜单与锚点的垂直间距
static const double anchorVerticalOffset = 6.0;
/// 菜单水平边距
static const double horizontalMargin = 16.0;
}
/// 菜单内容区域尺寸配置
class _ContentDimensions {
/// 供应商分组标题高度
static const double groupHeaderHeight = 36.0;
/// 单个模型项的高度(包含标签显示空间)
static const double modelItemHeight = 40.0;
/// 底部操作按钮区域高度
static const double bottomButtonHeight = 56.0;
/// 菜单内容的上下内边距
static const double verticalPadding = 6.0;
/// 菜单内容的左右内边距
static const double horizontalPadding = 4.0;
}
/// 模型项内部尺寸配置
class _ModelItemDimensions {
/// 模型图标容器大小
static const double iconContainerSize = 20.0;
/// 模型图标实际大小
static const double iconSize = 12.0;
/// 模型图标与文字的间距
static const double iconTextSpacing = 10.0;
/// 选中指示器图标大小
static const double selectedIconSize = 16.0;
/// 模型项的水平内边距
static const double itemHorizontalPadding = 12.0;
/// 模型项的垂直内边距
static const double itemVerticalPadding = 10.0;
/// 模型项的外边距
static const double itemMargin = 6.0;
/// 模型项的圆角半径
static const double itemBorderRadius = 8.0;
}
/// 标签样式尺寸配置
class _TagDimensions {
/// 标签水平内边距
static const double tagHorizontalPadding = 6.0;
/// 标签垂直内边距
static const double tagVerticalPadding = 2.0;
/// 标签圆角半径
static const double tagBorderRadius = 8.0;
/// 标签边框宽度
static const double tagBorderWidth = 0.5;
/// 标签之间的间距
static const double tagSpacing = 4.0;
/// 标签行之间的间距
static const double tagRunSpacing = 2.0;
/// 标签与模型名称的间距
static const double tagTopSpacing = 2.0;
}
/// 菜单外观样式配置
class _MenuStyling {
/// 菜单圆角半径
static const double menuBorderRadius = 16.0;
/// 菜单边框宽度
static const double menuBorderWidth = 0.8;
/// 分割线高度
static const double dividerHeight = 8.0;
/// 分割线厚度
static const double dividerThickness = 0.6;
/// 分割线缩进
static const double dividerIndent = 16.0;
/// 分割线结束缩进
static const double dividerEndIndent = 16.0;
/// 菜单阴影高度(暗色主题)
static const double elevationDark = 12.0;
/// 菜单阴影高度(亮色主题)
static const double elevationLight = 8.0;
}
/// 底部操作区域尺寸配置
class _BottomActionDimensions {
/// 底部操作区域内边距
static const double bottomPadding = 12.0;
/// 按钮垂直内边距
static const double buttonVerticalPadding = 12.0;
/// 按钮圆角半径
static const double buttonBorderRadius = 10.0;
/// 按钮边框宽度
static const double buttonBorderWidth = 0.8;
/// 按钮图标大小
static const double buttonIconSize = 18.0;
/// “添加我的私人模型”按钮的高度估算(用于高度计算)
static const double secondaryButtonHeight = 44.0;
}
/// 空状态显示尺寸配置
class _EmptyStateDimensions {
/// 空状态容器内边距
static const double emptyPadding = 24.0;
/// 空状态图标大小
static const double emptyIconSize = 48.0;
/// 空状态图标与文字的间距
static const double emptyIconTextSpacing = 12.0;
/// 空状态标题与副标题的间距
static const double emptyTitleSubtitleSpacing = 8.0;
}
// ==================== 统一 AI 模型下拉菜单组件实现 ====================
/// 统一的AI模型下拉菜单组件支持显示私有模型和公共模型
/// 通过 [show] 静态方法弹出 Overlay 菜单
class UnifiedAIModelDropdown {
static OverlayEntry show({
required BuildContext context,
LayerLink? layerLink,
Rect? anchorRect,
UnifiedAIModel? selectedModel,
required Function(UnifiedAIModel?) onModelSelected,
bool showSettingsButton = true,
bool showAdjustAndGenerate = true,
double maxHeight = _MenuDimensions.defaultMaxHeight,
Novel? novel,
List<NovelSettingItem> settings = const [],
List<SettingGroup> settingGroups = const [],
List<NovelSnippet> snippets = const [],
UniversalAIRequest? chatConfig,
ValueChanged<UniversalAIRequest>? onConfigChanged,
VoidCallback? onClose,
}) {
assert(layerLink != null || anchorRect != null, '必须提供 layerLink 或 anchorRect');
late OverlayEntry entry;
bool _closed = false;
void safeClose() {
if (_closed) return;
_closed = true;
if (entry.mounted) {
entry.remove();
}
onClose?.call();
}
entry = OverlayEntry(
builder: (ctx) {
return Stack(
children: [
// 点击空白处关闭
Positioned.fill(
child: GestureDetector(
onTap: safeClose,
child: Container(color: Colors.transparent),
),
),
if (layerLink != null) ...[
Positioned(
width: _MenuDimensions.menuWidth,
child: CompositedTransformFollower(
link: layerLink,
showWhenUnlinked: false,
targetAnchor: Alignment.bottomCenter,
followerAnchor: Alignment.topCenter,
offset: const Offset(0, _MenuDimensions.anchorVerticalOffset), // 向下偏移
child: BlocBuilder<AiConfigBloc, AiConfigState>(
builder: (context, aiState) {
return BlocBuilder<PublicModelsBloc, PublicModelsState>(
builder: (context, publicState) {
final allModels = _combineModels(aiState, publicState);
// 结合当前屏幕高度动态限制菜单高度,避免超出屏幕导致无法滚动
final screenH = MediaQuery.of(context).size.height;
final double maxAllowableHeight = screenH - _MenuDimensions.screenSafeMargin;
final menuHeight = _calculateMenuHeight(allModels, showSettingsButton, showAdjustAndGenerate, maxHeight)
.clamp(0.0, maxAllowableHeight)
.toDouble();
return _buildMenuContainer(
context,
menuHeight,
allModels,
selectedModel,
onModelSelected,
showSettingsButton,
showAdjustAndGenerate,
novel,
settings,
settingGroups,
snippets,
chatConfig,
onConfigChanged,
safeClose
);
},
);
},
),
),
),
] else if (anchorRect != null) ...[
BlocBuilder<AiConfigBloc, AiConfigState>(
builder: (context, aiState) {
return BlocBuilder<PublicModelsBloc, PublicModelsState>(
builder: (context, publicState) {
final allModels = _combineModels(aiState, publicState);
// 结合当前屏幕高度动态限制菜单高度,避免超出屏幕导致无法滚动
final screenH = MediaQuery.of(context).size.height;
final double maxAllowableHeight = screenH - _MenuDimensions.screenSafeMargin;
final menuHeight = _calculateMenuHeight(allModels, showSettingsButton, showAdjustAndGenerate, maxHeight)
.clamp(0.0, maxAllowableHeight)
.toDouble();
return _buildPositionedMenu(
context,
anchorRect,
menuHeight,
allModels,
selectedModel,
onModelSelected,
showSettingsButton,
showAdjustAndGenerate,
novel,
settings,
settingGroups,
snippets,
chatConfig,
onConfigChanged,
safeClose
);
},
);
},
),
],
],
);
},
);
Overlay.of(context).insert(entry);
return entry;
}
/// 合并私有模型和公共模型
static List<UnifiedAIModel> _combineModels(AiConfigState aiState, PublicModelsState publicState) {
final List<UnifiedAIModel> 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;
}
/// 按供应商分组模型,系统模型优先
static Map<String, List<UnifiedAIModel>> _groupModelsByProvider(List<UnifiedAIModel> models) {
final Map<String, List<UnifiedAIModel>> 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;
}
/// 计算菜单高度
static double _calculateMenuHeight(
List<UnifiedAIModel> models,
bool showSettingsButton,
bool showAdjustAndGenerate,
double maxHeight,
) {
final grouped = _groupModelsByProvider(models);
int totalItems = models.length;
final bool hasPrivateModels = models.any((m) => !m.isPublic);
final double addButtonHeight = showSettingsButton && !hasPrivateModels
? (_BottomActionDimensions.secondaryButtonHeight + 8.0)
: 0.0;
final double adjustButtonHeight = showSettingsButton && showAdjustAndGenerate
? _ContentDimensions.bottomButtonHeight
: 0.0;
final double contentHeight =
(grouped.length * _ContentDimensions.groupHeaderHeight) +
(totalItems * _ContentDimensions.modelItemHeight) +
addButtonHeight +
adjustButtonHeight +
(_ContentDimensions.verticalPadding * 2);
final double minHeight = showSettingsButton
? _MenuDimensions.minHeightWithSettings
: _MenuDimensions.minHeightWithoutSettings;
return contentHeight.clamp(minHeight, maxHeight);
}
static Widget _buildMenuContainer(
BuildContext context,
double menuHeight,
List<UnifiedAIModel> models,
UnifiedAIModel? selectedModel,
Function(UnifiedAIModel?) onModelSelected,
bool showSettingsButton,
bool showAdjustAndGenerate,
Novel? novel,
List<NovelSettingItem> settings,
List<SettingGroup> settingGroups,
List<NovelSnippet> snippets,
UniversalAIRequest? chatConfig,
ValueChanged<UniversalAIRequest>? onConfigChanged,
VoidCallback onClose,
) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Material(
elevation: isDark ? _MenuStyling.elevationDark : _MenuStyling.elevationLight,
borderRadius: BorderRadius.circular(_MenuStyling.menuBorderRadius),
color: isDark
? Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.95)
: Theme.of(context).colorScheme.surfaceContainer,
shadowColor: Colors.black.withOpacity(isDark ? 0.3 : 0.15),
child: Container(
height: menuHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_MenuStyling.menuBorderRadius),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant.withOpacity(isDark ? 0.2 : 0.3),
width: _MenuStyling.menuBorderWidth,
),
),
child: _UnifiedMenuContent(
models: models,
selectedModel: selectedModel,
onModelSelected: onModelSelected,
onClose: onClose,
showSettingsButton: showSettingsButton,
showAdjustAndGenerate: showAdjustAndGenerate,
novel: novel,
settings: settings,
settingGroups: settingGroups,
snippets: snippets,
chatConfig: chatConfig,
onConfigChanged: onConfigChanged,
),
),
);
}
static Widget _buildPositionedMenu(
BuildContext context,
Rect anchorRect,
double menuHeight,
List<UnifiedAIModel> models,
UnifiedAIModel? selectedModel,
Function(UnifiedAIModel?) onModelSelected,
bool showSettingsButton,
bool showAdjustAndGenerate,
Novel? novel,
List<NovelSettingItem> settings,
List<SettingGroup> settingGroups,
List<NovelSnippet> snippets,
UniversalAIRequest? chatConfig,
ValueChanged<UniversalAIRequest>? onConfigChanged,
VoidCallback onClose,
) {
final screenSize = MediaQuery.of(context).size;
double left = anchorRect.left;
if (left + _MenuDimensions.menuWidth > screenSize.width - _MenuDimensions.horizontalMargin) {
left = screenSize.width - _MenuDimensions.menuWidth - _MenuDimensions.horizontalMargin;
}
// 计算垂直放置位置,确保菜单完整显示在屏幕内
double top = anchorRect.top - menuHeight - _MenuDimensions.anchorVerticalOffset; // 先尝试放在目标组件上方
final double safeTop = MediaQuery.of(context).padding.top + 10;
final double safeBottom = screenSize.height - 10;
// 如果上方空间不足则放到下方
if (top < safeTop) {
top = anchorRect.bottom + _MenuDimensions.anchorVerticalOffset;
}
// 如果下方还是溢出,则将菜单整体上移
if (top + menuHeight > safeBottom) {
top = safeBottom - menuHeight;
// 仍保证不碰到状态栏
if (top < safeTop) {
top = safeTop;
}
}
return Positioned(
left: left,
top: top,
width: _MenuDimensions.menuWidth,
child: _buildMenuContainer(
context,
menuHeight,
models,
selectedModel,
onModelSelected,
showSettingsButton,
showAdjustAndGenerate,
novel,
settings,
settingGroups,
snippets,
chatConfig,
onConfigChanged,
onClose,
),
);
}
}
// ------------------ 内部菜单内容 ------------------
class _UnifiedMenuContent extends StatelessWidget {
const _UnifiedMenuContent({
Key? key,
required this.models,
required this.selectedModel,
required this.onModelSelected,
required this.onClose,
required this.showSettingsButton,
required this.showAdjustAndGenerate,
this.novel,
this.settings = const [],
this.settingGroups = const [],
this.snippets = const [],
this.chatConfig,
this.onConfigChanged,
}) : super(key: key);
final List<UnifiedAIModel> models;
final UnifiedAIModel? selectedModel;
final Function(UnifiedAIModel?) onModelSelected;
final VoidCallback onClose;
final bool showSettingsButton;
final bool showAdjustAndGenerate;
final Novel? novel;
final List<NovelSettingItem> settings;
final List<SettingGroup> settingGroups;
final List<NovelSnippet> snippets;
final UniversalAIRequest? chatConfig;
final ValueChanged<UniversalAIRequest>? onConfigChanged;
@override
Widget build(BuildContext context) {
if (models.isEmpty) {
return _buildEmpty(context);
}
final grouped = UnifiedAIModelDropdown._groupModelsByProvider(models);
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 Column(
children: [
Expanded(
child: ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: _ContentDimensions.horizontalPadding,
vertical: _ContentDimensions.verticalPadding
),
itemCount: providers.length,
separatorBuilder: (c, i) => Divider(
height: _MenuStyling.dividerHeight,
thickness: _MenuStyling.dividerThickness,
color: Theme.of(context)
.colorScheme
.outlineVariant
.withOpacity(0.12),
indent: _MenuStyling.dividerIndent,
endIndent: _MenuStyling.dividerEndIndent,
),
itemBuilder: (c, index) {
final provider = providers[index];
final providerModels = grouped[provider]!;
return _ProviderGroup(
provider: provider,
models: providerModels,
selectedModel: selectedModel,
onModelSelected: (m) {
onModelSelected(m);
onClose();
},
);
},
),
),
if (showSettingsButton) _buildBottomActions(context),
],
);
}
Widget _buildEmpty(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(_EmptyStateDimensions.emptyPadding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.model_training_outlined,
size: _EmptyStateDimensions.emptyIconSize, color: cs.onSurfaceVariant.withOpacity(0.5)),
const SizedBox(height: _EmptyStateDimensions.emptyIconTextSpacing),
Text('无可用模型',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: cs.onSurfaceVariant)),
const SizedBox(height: _EmptyStateDimensions.emptyTitleSubtitleSpacing),
Text('请先配置AI模型或等待公共模型加载',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: cs.onSurfaceVariant.withOpacity(0.7))),
],
),
),
);
}
Widget _buildBottomActions(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
padding: const EdgeInsets.all(_BottomActionDimensions.bottomPadding),
decoration: BoxDecoration(
color: isDark ? cs.surface.withOpacity(0.8) : cs.surface,
border: Border(
top: BorderSide(
color: cs.outlineVariant.withOpacity(isDark ? 0.15 : 0.2),
width: 1.0,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
if (!models.any((m) => !m.isPublic)) ...[
OutlinedButton.icon(
onPressed: () {
onClose();
// 优先尝试编辑器内打开
try {
final layoutManager = Provider.of<EditorLayoutManager>(context, listen: false);
layoutManager.toggleSettingsPanel();
return;
} catch (_) {}
// 回退:列表页等环境直接弹出设置对话框
final userId = AppConfig.userId;
if (userId == null || userId.isEmpty) {
TopToast.info(context, '请先登录后再添加私人模型');
return;
}
showDialog(
context: context,
barrierDismissible: true,
builder: (dialogContext) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: dialogContext.read<AiConfigBloc>()),
],
child: Dialog(
insetPadding: const EdgeInsets.all(16),
backgroundColor: Colors.transparent,
child: SettingsPanel(
stateManager: EditorStateManager(),
userId: userId,
onClose: () => Navigator.of(dialogContext).pop(),
),
),
);
},
);
},
icon: const Icon(Icons.add),
label: const Text('添加我的私人模型'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 10),
foregroundColor: isDark ? cs.primary.withOpacity(0.9) : cs.primary,
side: BorderSide(color: cs.primary.withOpacity(isDark ? 0.2 : 0.3), width: _BottomActionDimensions.buttonBorderWidth),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(_BottomActionDimensions.buttonBorderRadius)),
),
),
const SizedBox(height: 8),
],
if (showAdjustAndGenerate)
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
onClose(); // 先关闭 Overlay
// 只有选中私有模型时才能进入设置对话框
UserAIModelConfigModel? userModel;
if (selectedModel != null && !selectedModel!.isPublic) {
userModel = (selectedModel as PrivateAIModel).userConfig;
}
showChatSettingsDialog(
context,
selectedModel: userModel,
onModelChanged: (m) {
if (m != null) {
onModelSelected(PrivateAIModel(m));
}
},
novel: novel,
settings: settings,
settingGroups: settingGroups,
snippets: snippets,
initialChatConfig: chatConfig,
onConfigChanged: onConfigChanged,
initialContextSelections: null, // 🚀 让ChatSettingsDialog自己构建上下文数据
);
},
icon: const Icon(Icons.tune_rounded, size: _BottomActionDimensions.buttonIconSize),
label: const Text('调整并生成'),
style: ElevatedButton.styleFrom(
foregroundColor: isDark ? cs.primary.withOpacity(0.9) : cs.primary,
backgroundColor: isDark ? cs.primaryContainer.withOpacity(0.08) : cs.primaryContainer.withOpacity(0.1),
padding: const EdgeInsets.symmetric(vertical: _BottomActionDimensions.buttonVerticalPadding),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(_BottomActionDimensions.buttonBorderRadius)),
elevation: 0,
side: BorderSide(color: cs.primary.withOpacity(isDark ? 0.2 : 0.3), width: _BottomActionDimensions.buttonBorderWidth),
),
),
),
],
),
);
}
}
// 供应商分组组件
class _ProviderGroup extends StatelessWidget {
const _ProviderGroup({
Key? key,
required this.provider,
required this.models,
required this.selectedModel,
required this.onModelSelected,
}) : super(key: key);
final String provider;
final List<UnifiedAIModel> models;
final UnifiedAIModel? selectedModel;
final Function(UnifiedAIModel?) onModelSelected;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
// 检查是否有系统模型
final hasPublicModels = models.any((m) => m.isPublic);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 6),
child: Row(
children: [
Icon(
hasPublicModels ? Icons.public : Icons.person_outline,
size: 16,
color: isDark ? cs.primary.withOpacity(0.8) : cs.primary,
),
const SizedBox(width: 6),
Text(
provider.toUpperCase(),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: isDark ? cs.primary.withOpacity(0.9) : cs.primary,
fontWeight: FontWeight.w700,
letterSpacing: 1,
fontSize: 14,
),
),
const Spacer(),
Text(
'${models.length}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: cs.onSurfaceVariant.withOpacity(0.7),
fontSize: 12,
),
),
],
),
),
...models.map((m) => _UnifiedModelItem(
model: m,
isSelected: selectedModel?.id == m.id,
onTap: () => onModelSelected(m),
)),
const SizedBox(height: 4),
],
);
}
}
class _UnifiedModelItem extends StatelessWidget {
const _UnifiedModelItem({
Key? key,
required this.model,
required this.isSelected,
required this.onTap,
}) : super(key: key);
final UnifiedAIModel model;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(_ModelItemDimensions.itemBorderRadius),
splashColor: cs.primary.withOpacity(0.08),
highlightColor: cs.primary.withOpacity(0.04),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: _ModelItemDimensions.itemMargin, vertical: 1.0),
padding: const EdgeInsets.symmetric(
horizontal: _ModelItemDimensions.itemHorizontalPadding,
vertical: _ModelItemDimensions.itemVerticalPadding
),
decoration: BoxDecoration(
color: isSelected
? (isDark
? cs.primaryContainer.withOpacity(0.2)
: cs.primaryContainer.withOpacity(0.15))
: null,
borderRadius: BorderRadius.circular(_ModelItemDimensions.itemBorderRadius),
border: isSelected
? Border.all(color: cs.primary.withOpacity(0.2), width: 1.0)
: null,
),
child: Row(
children: [
// Icon
Container(
padding: const EdgeInsets.all(2),
child: _getModelIcon(model.provider, context),
),
const SizedBox(width: _ModelItemDimensions.iconTextSpacing),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
model.displayName,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected
? cs.primary
: (isDark
? cs.onSurface.withOpacity(0.9)
: cs.onSurface),
fontSize: 13,
height: 1.2,
),
overflow: TextOverflow.ellipsis,
),
// 显示所有标签
if (model.modelTags.isNotEmpty) ...[
const SizedBox(height: _TagDimensions.tagTopSpacing),
Wrap(
spacing: _TagDimensions.tagSpacing,
runSpacing: _TagDimensions.tagRunSpacing,
children: model.modelTags.map((tag) => _buildTag(tag, context)).toList(),
),
],
],
),
),
if (isSelected)
Icon(Icons.check_circle_rounded, size: _ModelItemDimensions.selectedIconSize, color: cs.primary),
],
),
),
);
}
Widget _buildTag(String tag, BuildContext context) {
final cs = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
Color tagColor;
Color backgroundColor;
Color borderColor;
if (tag == '私有') {
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 (tag == '系统') {
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 (tag == '推荐') {
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 (tag == '免费') {
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 (tag.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: _TagDimensions.tagHorizontalPadding,
vertical: _TagDimensions.tagVerticalPadding
),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(_TagDimensions.tagBorderRadius),
border: Border.all(
color: borderColor,
width: _TagDimensions.tagBorderWidth,
),
),
child: Text(
tag,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: tagColor.withOpacity(isDark ? 0.9 : 0.8),
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
);
}
Widget _getModelIcon(String provider, BuildContext context) {
final color = ProviderIcons.getProviderColor(provider);
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: _ModelItemDimensions.iconContainerSize,
height: _ModelItemDimensions.iconContainerSize,
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(provider, size: _ModelItemDimensions.iconSize, useHighQuality: true),
),
);
}
}