马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View File

@@ -0,0 +1,768 @@
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<NovelSettingItem> settings;
/// 设定组数据
final List<SettingGroup> settingGroups;
/// 片段数据
final List<NovelSnippet> snippets;
/// 🚀 聊天配置
final UniversalAIRequest? chatConfig;
/// 🚀 配置变更回调
final ValueChanged<UniversalAIRequest>? onConfigChanged;
@override
State<ModelSelector> createState() => _ModelSelectorState();
}
class _ModelSelectorState extends State<ModelSelector> {
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
bool _isMenuOpen = false;
/// 公开方法:触发菜单显示/隐藏
void showDropdown() {
final aiConfigBloc = context.read<AiConfigBloc>();
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<UserAIModelConfigModel> configs) {
if (_isMenuOpen) {
_removeOverlay();
} else {
_createOverlay(context, configs);
_isMenuOpen = true;
}
}
void _createOverlay(BuildContext context, List<UserAIModelConfigModel> 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<UserAIModelConfigModel> 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<UserAIModelConfigModel> 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<UserAIModelConfigModel> 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<String, List<UserAIModelConfigModel>> _groupModelsByProvider(
List<UserAIModelConfigModel> configs) {
final Map<String, List<UserAIModelConfigModel>> 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<ModelTag> _getModelTags(UserAIModelConfigModel model) {
// 根据模型信息返回标签列表
List<ModelTag> 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<AiConfigBloc, AiConfigState>(
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, // 自定义
}