Files
2025-09-10 00:07:52 +08:00

1391 lines
49 KiB
Dart
Raw Permalink 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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart';
import 'package:ainoval/blocs/universal_ai/universal_ai_bloc.dart';
import 'package:ainoval/blocs/universal_ai/universal_ai_state.dart';
import 'package:ainoval/blocs/universal_ai/universal_ai_event.dart';
import 'package:ainoval/models/user_ai_model_config_model.dart';
import 'package:ainoval/models/context_selection_models.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/models/ai_request_models.dart';
import 'package:ainoval/widgets/common/index.dart';
import 'package:ainoval/widgets/common/top_toast.dart';
import 'package:ainoval/widgets/common/multi_select_instructions_with_presets.dart' as multi_select;
// import 'package:ainoval/widgets/common/model_selector.dart' as ModelSelectorWidget; // unused
import 'package:ainoval/models/preset_models.dart';
// import 'package:ainoval/services/ai_preset_service.dart'; // unused
// import 'package:ainoval/screens/editor/widgets/dropdown_manager.dart'; // unused
import 'package:ainoval/utils/web_theme.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:ainoval/utils/context_selection_helper.dart';
import 'package:ainoval/config/app_config.dart';
// import 'package:ainoval/config/provider_icons.dart'; // unused
import 'package:ainoval/widgets/common/unified_ai_model_dropdown.dart';
// duplicate imports removed
// import 'package:ainoval/blocs/public_models/public_models_bloc.dart'; // unused
import 'package:ainoval/blocs/prompt_new/prompt_new_bloc.dart'; // 🚀 新增导入PromptNewBloc
import 'package:ainoval/models/unified_ai_model.dart';
import 'ai_dialog_common_logic.dart';
import 'package:ainoval/blocs/public_models/public_models_bloc.dart';
/// 扩写对话框
/// 用于扩展现有文本内容
class ExpansionDialog extends StatefulWidget {
/// 构造函数
const ExpansionDialog({
super.key,
this.aiConfigBloc,
this.selectedModel,
this.onModelChanged,
this.onGenerate,
this.onStreamingGenerate,
this.novel,
this.settings = const [],
this.settingGroups = const [],
this.snippets = const [],
this.selectedText,
this.initialInstructions,
this.initialLength,
this.initialEnableSmartContext,
this.initialContextSelections,
this.initialSelectedUnifiedModel,
});
/// AI配置Bloc
final AiConfigBloc? aiConfigBloc;
/// 当前选中的模型已废弃使用initialSelectedUnifiedModel
@Deprecated('Use initialSelectedUnifiedModel instead')
final UserAIModelConfigModel? selectedModel;
/// 模型改变回调(已废弃)
@Deprecated('No longer used')
final ValueChanged<UserAIModelConfigModel?>? onModelChanged;
/// 生成回调
final VoidCallback? onGenerate;
/// 流式生成回调
final Function(UniversalAIRequest request, UnifiedAIModel model)? onStreamingGenerate;
/// 小说数据(用于构建上下文选择)
final Novel? novel;
/// 设定数据
final List<NovelSettingItem> settings;
/// 设定组数据
final List<SettingGroup> settingGroups;
/// 片段数据
final List<NovelSnippet> snippets;
/// 选中的文本(用于扩写)
final String? selectedText;
/// 🚀 新增:初始化参数,用于返回表单时恢复设置
final String? initialInstructions;
final String? initialLength;
final bool? initialEnableSmartContext;
final ContextSelectionData? initialContextSelections;
/// 🚀 新增:初始化统一模型参数
final UnifiedAIModel? initialSelectedUnifiedModel;
@override
State<ExpansionDialog> createState() => _ExpansionDialogState();
}
class _ExpansionDialogState extends State<ExpansionDialog> with AIDialogCommonLogic {
// 控制器
final TextEditingController _instructionsController = TextEditingController();
final TextEditingController _lengthController = TextEditingController();
// 状态变量
UnifiedAIModel? _selectedUnifiedModel; // 🚀 统一AI模型
String? _selectedLength;
bool _enableSmartContext = true; // 🚀 新增:智能上下文开关,默认开启
AIPromptPreset? _currentPreset; // 🚀 新增:当前选中的预设
String? _selectedPromptTemplateId; // 🚀 新增选中的提示词模板ID
// 临时自定义提示词
String? _customSystemPrompt;
String? _customUserPrompt;
double _temperature = 0.7; // 🚀 新增:温度参数
double _topP = 0.9; // 🚀 新增Top-P参数
// 模型选择器key用于FormDialogTemplate
final GlobalKey _modelSelectorKey = GlobalKey();
// 上下文选择数据
late ContextSelectionData _contextSelectionData;
// 扩写指令预设
final List<multi_select.InstructionPreset> _expansionPresets = [
const multi_select.InstructionPreset(
id: 'descriptive',
title: '描述性扩写',
content: '请为这段文本添加更详细的描述,包括环境、感官细节和人物心理描写。',
description: '增加环境描述和感官细节',
),
const multi_select.InstructionPreset(
id: 'dialogue',
title: '对话扩写',
content: '请为这段文本添加更多的对话和人物互动,展现人物性格。',
description: '增加对话和人物互动',
),
const multi_select.InstructionPreset(
id: 'action',
title: '动作扩写',
content: '请为这段文本添加更多的动作描写和情节发展。',
description: '增加动作描写和情节',
),
];
OverlayEntry? _tempOverlay; // 🚀 临时Overlay用于ModelSelector下拉菜单
@override
void initState() {
super.initState();
// 🚀 初始化统一模型
_selectedUnifiedModel = widget.initialSelectedUnifiedModel;
// 向后兼容:如果没有提供初始化统一模型但有旧模型,则转换
if (_selectedUnifiedModel == null && widget.selectedModel != null) {
_selectedUnifiedModel = PrivateAIModel(widget.selectedModel!);
}
// 🚀 恢复之前的表单设置
if (widget.initialInstructions != null) {
_instructionsController.text = widget.initialInstructions!;
}
if (widget.initialLength != null) {
_selectedLength = widget.initialLength;
}
if (widget.initialEnableSmartContext != null) {
_enableSmartContext = widget.initialEnableSmartContext!;
}
// 🚀 初始化新的参数默认值
_selectedPromptTemplateId = null;
_temperature = 0.7;
_topP = 0.9;
// 🚀 使用公共助手类初始化上下文选择数据
_contextSelectionData = ContextSelectionHelper.initializeContextData(
novel: widget.novel,
settings: widget.settings,
settingGroups: widget.settingGroups,
snippets: widget.snippets,
initialSelections: widget.initialContextSelections,
);
debugPrint('ExpansionDialog 使用助手类初始化上下文选择数据完成: ${_contextSelectionData.selectedCount}个已选项');
// 🚀 初始化统一模型
if (widget.initialSelectedUnifiedModel != null) {
_selectedUnifiedModel = widget.initialSelectedUnifiedModel!;
}
}
/// Tab切换监听器
void _onTabChanged(String tabId) {
if (tabId == 'preview') { // 预览Tab
_triggerPreview();
}
}
@override
void dispose() {
_instructionsController.dispose();
_lengthController.dispose();
// 清理临时Overlay避免内存泄漏
_tempOverlay?.remove();
_tempOverlay = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
// 🚀 现在Bloc已经在外层showExpansionDialog中提供了直接构建FormDialogTemplate
return FormDialogTemplate(
title: '扩写文本',
tabs: const [
TabItem(
id: 'tweak',
label: '调整',
icon: Icons.edit,
),
TabItem(
id: 'preview',
label: '预览',
icon: Icons.preview,
),
],
tabContents: [
_buildTweakTab(),
_buildPreviewTab(),
],
showPresets: true,
usePresetDropdown: true,
presetFeatureType: 'TEXT_EXPANSION',
currentPreset: _currentPreset,
onPresetSelected: _handlePresetSelected,
onCreatePreset: _showCreatePresetDialog,
onManagePresets: _showManagePresetsPage,
novelId: widget.novel?.id,
showModelSelector: true, // 保留底部模型选择器按钮
modelSelectorData: _selectedUnifiedModel != null
? ModelSelectorData(
modelName: _selectedUnifiedModel!.displayName,
maxOutput: '~12000 words',
isModerated: true,
)
: const ModelSelectorData(
modelName: '选择模型',
),
onModelSelectorTap: _showModelSelectorDropdown, // 底部按钮触发下拉菜单
modelSelectorKey: _modelSelectorKey,
primaryActionLabel: '生成',
onPrimaryAction: _handleGenerate,
onClose: _handleClose,
onTabChanged: _onTabChanged,
aiConfigBloc: widget.aiConfigBloc,
);
}
/// 构建调整选项卡
Widget _buildTweakTab() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 指令字段
FormFieldFactory.createMultiSelectInstructionsWithPresetsField(
controller: _instructionsController,
presets: _expansionPresets,
title: '指令',
description: '应该如何扩写文本?',
placeholder: 'e.g. 描述设定',
dropdownPlaceholder: '选择指令预设',
onReset: _handleResetInstructions,
onExpand: _handleExpandInstructions,
onCopy: _handleCopyInstructions,
onSelectionChanged: _handlePresetSelectionChanged,
),
const SizedBox(height: 16),
// 长度字段
FormFieldFactory.createLengthField<String>(
options: const [
RadioOption(value: 'double', label: '双倍'),
RadioOption(value: 'triple', label: '三倍'),
],
value: _selectedLength,
onChanged: _handleLengthChanged,
title: '长度',
description: '扩写后的文本应该多长?',
onReset: _handleResetLength,
alternativeInput: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 40),
child: TextField(
controller: _lengthController,
decoration: InputDecoration(
hintText: 'e.g. 400 words',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? WebTheme.darkGrey300
: WebTheme.grey300,
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? WebTheme.darkGrey300
: WebTheme.grey300,
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(
color: WebTheme.getPrimaryColor(context),
width: 1,
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
fillColor: Theme.of(context).brightness == Brightness.dark
? WebTheme.darkGrey100
: WebTheme.white,
filled: true,
isDense: true,
),
onChanged: (value) {
setState(() {
_selectedLength = null;
});
},
),
),
),
const SizedBox(height: 16),
// 附加上下文字段
FormFieldFactory.createContextSelectionField(
contextData: _contextSelectionData,
onSelectionChanged: _handleContextSelectionChanged,
title: '附加上下文',
description: '为AI提供的任何额外信息',
onReset: _handleResetContexts,
dropdownWidth: 400,
initialChapterId: null,
initialSceneId: null,
),
const SizedBox(height: 16),
// 🚀 新增:智能上下文勾选组件
SmartContextToggle(
value: _enableSmartContext,
onChanged: _handleSmartContextChanged,
title: '智能上下文',
description: '使用AI自动检索相关背景信息提升生成质量',
),
const SizedBox(height: 16),
// 🚀 新增:关联提示词模板选择字段
FormFieldFactory.createPromptTemplateSelectionField(
selectedTemplateId: _selectedPromptTemplateId,
onTemplateSelected: _handlePromptTemplateSelected,
aiFeatureType: 'TEXT_EXPANSION', // 🚀 使用标准API字符串格式
title: '关联提示词模板',
description: '选择要关联的提示词模板(可选)',
onReset: _handleResetPromptTemplate,
onTemporaryPromptsSaved: (sys, user) {
setState(() {
_customSystemPrompt = sys.trim().isEmpty ? null : sys.trim();
_customUserPrompt = user.trim().isEmpty ? null : user.trim();
});
},
),
const SizedBox(height: 16),
// 🚀 新增:温度滑动组件
FormFieldFactory.createTemperatureSliderField(
context: context,
value: _temperature,
onChanged: _handleTemperatureChanged,
onReset: _handleResetTemperature,
),
const SizedBox(height: 16),
// 🚀 新增Top-P滑动组件
FormFieldFactory.createTopPSliderField(
context: context,
value: _topP,
onChanged: _handleTopPChanged,
onReset: _handleResetTopP,
),
],
);
}
/// 构建预览选项卡
Widget _buildPreviewTab() {
return BlocBuilder<UniversalAIBloc, UniversalAIState>(
builder: (context, state) {
if (state is UniversalAILoading) {
return const PromptPreviewLoadingWidget();
} else if (state is UniversalAIPreviewSuccess) {
return PromptPreviewWidget(
previewResponse: state.previewResponse,
showActions: true,
);
} else if (state is UniversalAIError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
'预览失败',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
state.message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _triggerPreview,
child: const Text('重试'),
),
],
),
);
} else {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.preview_outlined,
size: 48,
color: Theme.of(context).colorScheme.outlineVariant,
),
const SizedBox(height: 16),
const Text(
'点击预览选项卡查看提示词',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _triggerPreview,
child: const Text('生成预览'),
),
],
),
);
}
},
);
}
/// 触发预览请求
void _triggerPreview() {
if (_selectedUnifiedModel == null) {
TopToast.warning(context, '请先选择AI模型');
return;
}
if (widget.selectedText == null || widget.selectedText!.trim().isEmpty) {
TopToast.warning(context, '没有选中的文本内容');
return;
}
// 获取模型配置,根据模型类型获取适当的配置
late UserAIModelConfigModel modelConfig;
if (_selectedUnifiedModel!.isPublic) {
// 对于公共模型创建临时的模型配置用于API调用
final publicModel = (_selectedUnifiedModel as PublicAIModel).publicConfig;
modelConfig = UserAIModelConfigModel.fromJson({
'id': publicModel.id,
'userId': AppConfig.userId ?? 'unknown',
'name': publicModel.displayName,
'alias': publicModel.displayName,
'modelName': publicModel.modelId,
'provider': publicModel.provider,
'apiEndpoint': '', // 公共模型没有单独的apiEndpoint
'isDefault': false,
'isValidated': true,
'createdAt': DateTime.now().toIso8601String(),
'updatedAt': DateTime.now().toIso8601String(),
// 公共模型的额外信息
'isPublic': true,
'creditMultiplier': publicModel.creditRateMultiplier ?? 1.0,
});
} else {
// 对于私有模型,直接使用用户配置
modelConfig = (_selectedUnifiedModel as PrivateAIModel).userConfig;
}
// 构建预览请求
final request = UniversalAIRequest(
requestType: AIRequestType.expansion,
userId: AppConfig.userId ?? 'unknown',
novelId: widget.novel?.id,
modelConfig: modelConfig,
selectedText: widget.selectedText!,
instructions: _instructionsController.text.trim(),
contextSelections: _contextSelectionData,
enableSmartContext: _enableSmartContext,
parameters: {
'length': _selectedLength ?? _lengthController.text.trim(),
'temperature': _temperature, // 🚀 使用用户设置的温度值
'topP': _topP, // 🚀 新增使用用户设置的Top-P值
'maxTokens': 4000,
'modelName': _selectedUnifiedModel!.modelId,
'enableSmartContext': _enableSmartContext,
'promptTemplateId': _selectedPromptTemplateId, // 🚀 新增关联提示词模板ID
if (_customSystemPrompt != null) 'customSystemPrompt': _customSystemPrompt,
if (_customUserPrompt != null) 'customUserPrompt': _customUserPrompt,
},
metadata: {
'action': 'expand',
'source': 'preview',
'contextCount': _contextSelectionData.selectedCount,
'originalLength': widget.selectedText?.length ?? 0,
'modelName': _selectedUnifiedModel!.modelId,
'modelProvider': _selectedUnifiedModel!.provider,
'modelConfigId': _selectedUnifiedModel!.id,
'enableSmartContext': _enableSmartContext,
},
);
// 发送预览请求
context.read<UniversalAIBloc>().add(PreviewAIRequestEvent(request));
}
/// 构建当前请求对象(用于保存预设)
UniversalAIRequest? _buildCurrentRequest() {
if (_selectedUnifiedModel == null) return null;
// 🚀 使用公共逻辑创建模型配置
final modelConfig = createModelConfig(_selectedUnifiedModel!);
// 🚀 使用公共逻辑创建元数据
final metadata = createModelMetadata(_selectedUnifiedModel!, {
'action': 'expand',
'source': 'expansion_dialog',
'contextCount': _contextSelectionData.selectedCount,
'originalLength': widget.selectedText?.length ?? 0,
'enableSmartContext': _enableSmartContext,
});
return UniversalAIRequest(
requestType: AIRequestType.expansion,
userId: AppConfig.userId ?? 'unknown',
novelId: widget.novel?.id,
modelConfig: modelConfig,
selectedText: widget.selectedText,
instructions: _instructionsController.text.trim(),
contextSelections: _contextSelectionData,
enableSmartContext: _enableSmartContext,
parameters: {
'length': _selectedLength ?? _lengthController.text.trim(),
'temperature': _temperature, // 🚀 使用用户设置的温度值
'topP': _topP, // 🚀 新增使用用户设置的Top-P值
'maxTokens': 4000,
'modelName': _selectedUnifiedModel!.modelId,
'enableSmartContext': _enableSmartContext,
'promptTemplateId': _selectedPromptTemplateId, // 🚀 新增关联提示词模板ID
if (_customSystemPrompt != null) 'customSystemPrompt': _customSystemPrompt,
if (_customUserPrompt != null) 'customUserPrompt': _customUserPrompt,
},
metadata: metadata,
);
}
/// 显示创建预设对话框
void _showCreatePresetDialog() {
final currentRequest = _buildCurrentRequest();
if (currentRequest == null) {
TopToast.warning(context, '无法创建预设:缺少表单数据');
return;
}
showPresetNameDialog(currentRequest, onPresetCreated: _handlePresetCreated);
}
// 移除重复的预设创建方法,使用 AIDialogCommonLogic 中的公共方法
/// 显示预设管理页面
void _showManagePresetsPage() {
// TODO: 实现预设管理页面
TopToast.info(context, '预设管理功能开发中...');
}
/// 处理预设选择
void _handlePresetSelected(AIPromptPreset preset) {
try {
// 设置当前预设
setState(() {
_currentPreset = preset;
});
// 🚀 使用公共方法应用预设配置
applyPresetToForm(
preset,
instructionsController: _instructionsController,
onLengthChanged: (length) {
setState(() {
if (length != null && ['double', 'triple'].contains(length)) {
_selectedLength = length;
_lengthController.clear();
} else if (length != null) {
_selectedLength = null;
_lengthController.text = length;
}
});
},
onSmartContextChanged: (value) {
setState(() {
_enableSmartContext = value;
});
},
onPromptTemplateChanged: (templateId) {
setState(() {
_selectedPromptTemplateId = templateId;
});
},
onTemperatureChanged: (temperature) {
setState(() {
_temperature = temperature;
});
},
onTopPChanged: (topP) {
setState(() {
_topP = topP;
});
},
onContextSelectionChanged: (contextData) {
setState(() {
_contextSelectionData = contextData;
});
},
onModelChanged: (unifiedModel) {
setState(() {
_selectedUnifiedModel = unifiedModel;
});
},
currentContextData: _contextSelectionData,
);
} catch (e) {
AppLogger.e('ExpansionDialog', '应用预设失败', e);
TopToast.error(context, '应用预设失败: $e');
}
}
/// 处理预设创建
void _handlePresetCreated(AIPromptPreset preset) {
// 设置当前预设为新创建的预设
setState(() {
_currentPreset = preset;
});
TopToast.success(context, '预设 "${preset.presetName}" 创建成功');
AppLogger.i('ExpansionDialog', '预设创建成功: ${preset.presetName}');
}
// 模型选择器点击处理已移除现在使用内嵌的ModelSelector组件
/// 显示模型选择器覆盖层已禁用现在使用内嵌的ModelSelector组件
void _showModelSelectorOverlay() {
// 方法已禁用现在使用内嵌的ModelSelector组件
return;
/*
if (_modelSelectorOverlay != null) {
_removeModelSelectorOverlay();
return;
}
final aiConfigBloc = widget.aiConfigBloc ?? context.read<AiConfigBloc>();
final validatedConfigs = aiConfigBloc.state.validatedConfigs;
if (validatedConfigs.isEmpty) {
debugPrint('No validated configs available');
return;
}
// 获取模型选择器的位置
final RenderBox? renderBox = _modelSelectorKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) {
debugPrint('Model selector render box not found');
return;
}
final Offset position = renderBox.localToGlobal(Offset.zero);
final Size size = renderBox.size;
// 计算菜单内容高度
final groupedModels = _groupModelsByProvider(validatedConfigs);
const double groupHeaderHeight = 20.0;
const double modelItemHeight = 24.0;
const double verticalPadding = 8.0;
double totalItems = 0;
for (var group in groupedModels.values) {
totalItems += group.length;
}
final double contentHeight = (groupedModels.length * groupHeaderHeight) +
(totalItems * modelItemHeight) +
(verticalPadding * 2);
const double menuWidth = 280.0;
final double menuHeight = contentHeight.clamp(160.0, 1200.0);
// 获取屏幕尺寸用于边界检查
final mediaQuery = MediaQuery.of(context);
final screenWidth = mediaQuery.size.width;
final screenHeight = mediaQuery.size.height;
// 计算弹出位置:紧贴模型选择器上方
double leftOffset = position.dx + (size.width - menuWidth) / 2; // 相对于模型选择器居中
double topOffset = position.dy - menuHeight - 8; // 在模型选择器上方留8px间距
// 边界检查 - 确保不超出屏幕左右边界
if (leftOffset < 16) {
leftOffset = 16; // 左边距
} else if (leftOffset + menuWidth > screenWidth - 16) {
leftOffset = screenWidth - menuWidth - 16; // 右边距
}
// 边界检查 - 确保不超出屏幕上边界
if (topOffset < 16) {
topOffset = position.dy + size.height + 8; // 如果上方空间不足,显示在下方
}
_modelSelectorOverlay = OverlayEntry(
builder: (context) => Stack(
children: [
// 透明背景,点击时关闭菜单
Positioned.fill(
child: GestureDetector(
onTap: _removeModelSelectorOverlay,
child: Container(
color: Colors.transparent,
),
),
),
// 模型列表内容
Positioned(
left: leftOffset,
top: topOffset,
width: menuWidth,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surfaceContainer,
shadowColor: Theme.of(context).brightness == Brightness.dark
? Colors.black.withOpacity(0.3)
: Colors.black.withOpacity(0.1),
child: Container(
height: menuHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context)
.colorScheme
.outlineVariant
.withOpacity(0.3),
),
),
child: _buildModelListContent(validatedConfigs),
),
),
),
],
),
);
Overlay.of(context).insert(_modelSelectorOverlay!);
*/
}
void _removeModelSelectorOverlay() {
// 方法已禁用现在使用内嵌的ModelSelector组件
return;
/*
_modelSelectorOverlay?.remove();
_modelSelectorOverlay = null;
*/
}
/// 按供应商分组模型
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;
}
/// 显示模型选择器下拉菜单
void _showModelSelectorDropdown() {
// 确保公共模型加载,避免仅私人模型为空时无法点击
try {
final publicBloc = context.read<PublicModelsBloc>();
final st = publicBloc.state;
if (st is PublicModelsInitial || st is PublicModelsError) {
publicBloc.add(const LoadPublicModels());
}
} catch (_) {}
// 获取底部模型按钮的位置
final renderBox = _modelSelectorKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final position = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
final anchorRect = Rect.fromLTWH(position.dx, position.dy, size.width, size.height);
_tempOverlay?.remove();
_tempOverlay = UnifiedAIModelDropdown.show(
context: context,
anchorRect: anchorRect,
selectedModel: _selectedUnifiedModel,
onModelSelected: (unifiedModel) {
setState(() {
_selectedUnifiedModel = unifiedModel;
});
},
showSettingsButton: true,
novel: widget.novel,
settings: widget.settings,
settingGroups: widget.settingGroups,
snippets: widget.snippets,
onClose: () {
_tempOverlay = null;
},
);
}
void _handleGenerate() async {
// 检查必填字段
if (_selectedUnifiedModel == null) {
TopToast.error(context, '请选择AI模型');
return;
}
if (widget.selectedText == null || widget.selectedText!.trim().isEmpty) {
TopToast.error(context, '没有选中的文本内容');
return;
}
debugPrint('选中的上下文: ${_contextSelectionData.selectedCount}');
for (final item in _contextSelectionData.selectedItems.values) {
debugPrint('- ${item.title} (${item.type.displayName})');
}
// 🚀 新增:对于公共模型,先进行积分预估和确认
if (_selectedUnifiedModel!.isPublic) {
debugPrint('🚀 检测到公共模型,启动积分预估确认流程: ${_selectedUnifiedModel!.displayName}');
bool shouldProceed = await _showCreditEstimationAndConfirm();
if (!shouldProceed) {
debugPrint('🚀 用户取消了积分预估确认,停止生成');
return; // 用户取消或积分不足,停止执行
}
debugPrint('🚀 用户确认了积分预估,继续生成');
} else {
debugPrint('🚀 检测到私有模型,直接生成: ${_selectedUnifiedModel!.displayName}');
}
// 启动流式生成,并关闭对话框
_startStreamingGeneration();
Navigator.of(context).pop();
}
/// 启动流式生成
void _startStreamingGeneration() {
try {
// 🚀 修复:为公共模型和私有模型创建正确的模型配置
late UserAIModelConfigModel modelConfig;
if (_selectedUnifiedModel!.isPublic) {
// 对于公共模型,创建包含公共模型信息的临时配置
final publicModel = (_selectedUnifiedModel as PublicAIModel).publicConfig;
debugPrint('🚀 启动公共模型流式生成 - 显示名: ${publicModel.displayName}, 模型ID: ${publicModel.modelId}, 公共模型ID: ${publicModel.id}');
modelConfig = UserAIModelConfigModel.fromJson({
'id': 'public_${publicModel.id}', // 🚀 使用前缀区分公共模型ID
'userId': AppConfig.userId ?? 'unknown',
'alias': publicModel.displayName,
'modelName': publicModel.modelId,
'provider': publicModel.provider,
'apiEndpoint': '', // 公共模型没有单独的apiEndpoint
'isDefault': false,
'isValidated': true,
'createdAt': DateTime.now().toIso8601String(),
'updatedAt': DateTime.now().toIso8601String(),
});
} else {
// 对于私有模型,直接使用用户配置
final privateModel = (_selectedUnifiedModel as PrivateAIModel).userConfig;
debugPrint('🚀 启动私有模型流式生成 - 显示名: ${privateModel.name}, 模型名: ${privateModel.modelName}, 配置ID: ${privateModel.id}');
modelConfig = privateModel;
}
// 构建AI请求
final request = UniversalAIRequest(
requestType: AIRequestType.expansion,
userId: AppConfig.userId ?? 'unknown',
novelId: widget.novel?.id,
modelConfig: modelConfig,
selectedText: widget.selectedText!,
instructions: _instructionsController.text.trim(),
contextSelections: _contextSelectionData,
enableSmartContext: _enableSmartContext,
parameters: {
'length': _selectedLength ?? _lengthController.text.trim(),
'temperature': _temperature, // 🚀 使用用户设置的温度值
'topP': _topP, // 🚀 新增使用用户设置的Top-P值
'maxTokens': 4000,
'modelName': _selectedUnifiedModel!.modelId,
'enableSmartContext': _enableSmartContext,
'promptTemplateId': _selectedPromptTemplateId, // 🚀 新增关联提示词模板ID
if (_customSystemPrompt != null) 'customSystemPrompt': _customSystemPrompt,
if (_customUserPrompt != null) 'customUserPrompt': _customUserPrompt,
},
metadata: {
'action': 'expand',
'source': 'selection_toolbar',
'contextCount': _contextSelectionData.selectedCount,
'originalLength': widget.selectedText?.length ?? 0,
'modelName': _selectedUnifiedModel!.modelId,
'modelProvider': _selectedUnifiedModel!.provider,
'modelConfigId': _selectedUnifiedModel!.id,
'enableSmartContext': _enableSmartContext,
// 🚀 新增明确标识模型类型和公共模型的真实ID
'isPublicModel': _selectedUnifiedModel!.isPublic,
if (_selectedUnifiedModel!.isPublic) 'publicModelConfigId': (_selectedUnifiedModel as PublicAIModel).publicConfig.id,
if (_selectedUnifiedModel!.isPublic) 'publicModelId': (_selectedUnifiedModel as PublicAIModel).publicConfig.id,
},
);
// 通过回调通知父组件开始流式生成
widget.onGenerate?.call();
// 如果有流式生成回调,调用它
if (widget.onStreamingGenerate != null) {
widget.onStreamingGenerate!(request, _selectedUnifiedModel!);
}
debugPrint('流式扩写生成已启动: 模型=${_selectedUnifiedModel!.displayName}, 智能上下文=$_enableSmartContext, 原文长度=${widget.selectedText?.length ?? 0}');
} catch (e) {
TopToast.error(context, '启动生成失败: $e');
debugPrint('启动扩写生成失败: $e');
}
}
void _handleClose() {
Navigator.of(context).pop();
}
void _handleResetInstructions() {
setState(() {
_instructionsController.clear();
});
}
void _handleExpandInstructions() {
debugPrint('展开指令编辑器');
}
void _handleCopyInstructions() {
debugPrint('复制指令内容');
}
void _handleLengthChanged(String? value) {
setState(() {
_selectedLength = value;
if (value != null) {
_lengthController.clear(); // 清除文本输入
}
});
}
void _handleResetLength() {
setState(() {
_selectedLength = null;
_lengthController.clear();
});
}
void _handleContextSelectionChanged(ContextSelectionData newData) {
setState(() {
_contextSelectionData = newData;
});
debugPrint('上下文选择改变: ${newData.selectedCount} 个项目被选中');
}
void _handleResetContexts() {
setState(() {
// 🚀 使用公共助手类重置上下文选择
_contextSelectionData = ContextSelectionHelper.initializeContextData(
novel: widget.novel,
settings: widget.settings,
settingGroups: widget.settingGroups,
snippets: widget.snippets,
);
});
debugPrint('上下文选择重置');
}
void _handlePresetSelectionChanged(List<multi_select.InstructionPreset> selectedPresets) {
debugPrint('选中的预设已改变: ${selectedPresets.map((p) => p.title).join(', ')}');
}
void _handleSmartContextChanged(bool value) {
setState(() {
_enableSmartContext = value;
});
}
/// 🚀 新增:处理提示词模板选择
void _handlePromptTemplateSelected(String? templateId) {
setState(() {
_selectedPromptTemplateId = templateId;
});
debugPrint('选中的提示词模板ID: $templateId');
}
/// 🚀 新增:重置提示词模板选择
void _handleResetPromptTemplate() {
setState(() {
_selectedPromptTemplateId = null;
});
debugPrint('重置提示词模板选择');
}
/// 🚀 新增:处理温度参数变化
void _handleTemperatureChanged(double value) {
setState(() {
_temperature = value;
});
debugPrint('温度参数已更改: $value');
}
/// 🚀 新增:重置温度参数
void _handleResetTemperature() {
setState(() {
_temperature = 0.7;
});
debugPrint('温度参数已重置为默认值: 0.7');
}
/// 🚀 新增处理Top-P参数变化
void _handleTopPChanged(double value) {
setState(() {
_topP = value;
});
debugPrint('Top-P参数已更改: $value');
}
/// 🚀 新增重置Top-P参数
void _handleResetTopP() {
setState(() {
_topP = 0.9;
});
debugPrint('Top-P参数已重置为默认值: 0.9');
}
/// 🚀 新增:显示积分预估和确认对话框
Future<bool> _showCreditEstimationAndConfirm() async {
try {
// 构建预估请求
final estimationRequest = _buildCurrentRequest();
if (estimationRequest == null) {
TopToast.error(context, '无法构建预估请求');
return false;
}
// 显示积分预估确认对话框传递UniversalAIBloc
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return BlocProvider.value(
value: context.read<UniversalAIBloc>(),
child: _CreditEstimationDialog(
modelName: _selectedUnifiedModel!.displayName,
request: estimationRequest,
onConfirm: () => Navigator.of(dialogContext).pop(true),
onCancel: () => Navigator.of(dialogContext).pop(false),
),
);
},
) ?? false;
} catch (e) {
AppLogger.e('ExpansionDialog', '积分预估失败', e);
TopToast.error(context, '积分预估失败: $e');
return false;
}
}
}
/// 🚀 新增:积分预估确认对话框
class _CreditEstimationDialog extends StatefulWidget {
final String modelName;
final UniversalAIRequest request;
final VoidCallback onConfirm;
final VoidCallback onCancel;
const _CreditEstimationDialog({
super.key,
required this.modelName,
required this.request,
required this.onConfirm,
required this.onCancel,
});
@override
State<_CreditEstimationDialog> createState() => _CreditEstimationDialogState();
}
class _CreditEstimationDialogState extends State<_CreditEstimationDialog> {
CostEstimationResponse? _costEstimation;
String? _errorMessage;
@override
void initState() {
super.initState();
_estimateCost();
}
Future<void> _estimateCost() async {
try {
// 🚀 调用真实的积分预估API
final universalAIBloc = context.read<UniversalAIBloc>();
universalAIBloc.add(EstimateCostEvent(widget.request));
} catch (e) {
setState(() {
_errorMessage = '预估失败: $e';
});
}
}
@override
Widget build(BuildContext context) {
return BlocListener<UniversalAIBloc, UniversalAIState>(
listener: (context, state) {
if (state is UniversalAICostEstimationSuccess) {
setState(() {
_costEstimation = state.costEstimation;
_errorMessage = null;
});
} else if (state is UniversalAIError) {
setState(() {
_errorMessage = state.message;
_costEstimation = null;
});
}
},
child: BlocBuilder<UniversalAIBloc, UniversalAIState>(
builder: (context, state) {
final isLoading = state is UniversalAILoading;
return AlertDialog(
title: Row(
children: [
Icon(
Icons.account_balance_wallet,
color: WebTheme.getPrimaryColor(context),
),
const SizedBox(width: 8),
const Text('积分消耗预估'),
],
),
content: SizedBox(
width: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'模型: ${widget.modelName}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 16),
if (isLoading) ...[
const Row(
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 12),
Text('正在估算积分消耗...'),
],
),
] else if (_errorMessage != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
),
),
] else if (_costEstimation != null) ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: WebTheme.getPrimaryColor(context).withOpacity(0.3),
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'预估消耗积分:',
style: TextStyle(fontWeight: FontWeight.w500),
),
Text(
'${_costEstimation!.estimatedCost}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: WebTheme.getPrimaryColor(context),
fontWeight: FontWeight.bold,
),
),
],
),
if (_costEstimation!.estimatedInputTokens != null || _costEstimation!.estimatedOutputTokens != null) ...[
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Token预估:',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
Text(
'输入: ${_costEstimation!.estimatedInputTokens ?? 0}, 输出: ${_costEstimation!.estimatedOutputTokens ?? 0}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
],
const SizedBox(height: 8),
Text(
'实际消耗可能因内容长度和模型响应而有所不同',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
),
],
const SizedBox(height: 16),
Text(
'确认要继续生成吗?',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
actions: [
TextButton(
onPressed: isLoading ? null : widget.onCancel,
child: const Text('取消'),
),
ElevatedButton(
onPressed: isLoading || _errorMessage != null || _costEstimation == null ? null : widget.onConfirm,
child: const Text('确认生成'),
),
],
);
},
),
);
}
}
/// 显示扩写对话框的便捷函数
void showExpansionDialog(
BuildContext context, {
@Deprecated('Use initialSelectedUnifiedModel instead') UserAIModelConfigModel? selectedModel,
@Deprecated('No longer used') ValueChanged<UserAIModelConfigModel?>? onModelChanged,
VoidCallback? onGenerate,
Function(UniversalAIRequest request, UnifiedAIModel model)? onStreamingGenerate,
Novel? novel,
List<NovelSettingItem> settings = const [],
List<SettingGroup> settingGroups = const [],
List<NovelSnippet> snippets = const [],
String? selectedText,
// 🚀 新增:初始化参数
String? initialInstructions,
String? initialLength,
bool? initialEnableSmartContext,
ContextSelectionData? initialContextSelections,
// 🚀 新增:初始化统一模型参数
UnifiedAIModel? initialSelectedUnifiedModel,
}) {
showDialog(
context: context,
barrierDismissible: true,
builder: (dialogContext) {
// 🚀 修复为对话框提供必要的Bloc避免在内部widget中读取失败
return MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<AiConfigBloc>()),
BlocProvider.value(value: context.read<PromptNewBloc>()),
],
child: ExpansionDialog(
selectedModel: selectedModel,
onModelChanged: onModelChanged,
onGenerate: onGenerate,
onStreamingGenerate: onStreamingGenerate,
novel: novel,
settings: settings,
settingGroups: settingGroups,
snippets: snippets,
selectedText: selectedText,
initialInstructions: initialInstructions,
initialLength: initialLength,
initialEnableSmartContext: initialEnableSmartContext,
initialContextSelections: initialContextSelections,
initialSelectedUnifiedModel: initialSelectedUnifiedModel,
),
);
},
);
}