import 'package:ainoval/blocs/editor/editor_bloc.dart'; import 'package:ainoval/blocs/universal_ai/universal_ai_bloc.dart'; import 'package:ainoval/blocs/universal_ai/universal_ai_event.dart'; import 'package:ainoval/blocs/universal_ai/universal_ai_state.dart'; import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart'; import 'package:ainoval/blocs/public_models/public_models_bloc.dart'; import 'package:ainoval/models/novel_structure.dart'; import 'package:ainoval/models/unified_ai_model.dart'; import 'package:ainoval/models/ai_request_models.dart'; import 'package:ainoval/widgets/common/unified_ai_model_dropdown.dart'; import 'package:ainoval/widgets/common/scene_selector.dart'; import 'package:ainoval/config/app_config.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:flutter/material.dart'; import 'package:ainoval/utils/web_theme.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; // import 'package:ainoval/screens/editor/managers/editor_layout_manager.dart'; import 'package:flutter/services.dart'; import 'package:ainoval/widgets/common/top_toast.dart'; // import 'package:ainoval/widgets/common/app_search_field.dart'; import 'package:ainoval/models/context_selection_models.dart'; import 'package:ainoval/widgets/common/form_dialog_template.dart'; import 'package:ainoval/utils/quill_helper.dart'; import 'package:ainoval/screens/editor/components/ai_dialog_common_logic.dart'; import 'package:ainoval/blocs/credit/credit_bloc.dart'; /// AI生成面板,提供根据摘要生成场景的功能 class AIGenerationPanel extends StatefulWidget { const AIGenerationPanel({ Key? key, required this.novelId, required this.onClose, this.isCardMode = false, }) : super(key: key); final String novelId; final VoidCallback onClose; final bool isCardMode; // 是否以卡片模式显示 @override State createState() => _AIGenerationPanelState(); } class _AIGenerationPanelState extends State with AIDialogCommonLogic { final TextEditingController _summaryController = TextEditingController(); final TextEditingController _styleController = TextEditingController(); final TextEditingController _generatedContentController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final LayerLink _layerLink = LayerLink(); UnifiedAIModel? _selectedModel; bool _enableSmartContext = true; bool _userScrolled = false; // bool _contentEdited = false; // 未使用,注释避免警告 bool _isGenerating = false; // String _generatedText = ''; bool _thisInstanceIsGenerating = false; // 标记是否是当前实例发起的生成请求 late ContextSelectionData _contextSelectionData; String? _selectedPromptTemplateId; // 临时自定义提示词 String? _customSystemPrompt; String? _customUserPrompt; bool _contextInitialized = false; @override void initState() { super.initState(); // 监听滚动事件,检测用户是否主动滚动 _scrollController.addListener(_handleUserScroll); // 初始化默认模型配置 WidgetsBinding.instance.addPostFrameCallback((_) { _initializeDefaultModel(); _initializeContextData(); }); // 读取待处理的摘要内容或当前场景的摘要 WidgetsBinding.instance.addPostFrameCallback((_) { final editorState = context.read().state; if (editorState is EditorLoaded) { if (editorState.pendingSummary != null && editorState.pendingSummary!.isNotEmpty) { // 优先使用待处理摘要 _summaryController.text = editorState.pendingSummary!; // 清除待处理摘要,避免下次打开时仍然显示 context.read().add(const SetPendingSummary(summary: '')); } else { // 自动导入当前场景的摘要 _loadCurrentSceneSummary(editorState); } } }); } void _initializeContextData() { if (_contextInitialized) return; final editorState = context.read().state; if (editorState is EditorLoaded) { _contextSelectionData = ContextSelectionDataBuilder.fromNovel(editorState.novel); _contextInitialized = true; } } void _initializeDefaultModel() { final aiConfigState = context.read().state; final publicModelsState = context.read().state; // 合并私有模型和公共模型 final allModels = _combineModels(aiConfigState, publicModelsState); if (allModels.isNotEmpty && _selectedModel == null) { // 优先选择默认配置 UnifiedAIModel? defaultModel; // 首先查找私有模型中的默认配置 for (final model in allModels) { if (!model.isPublic && (model as PrivateAIModel).userConfig.isDefault) { defaultModel = model; break; } } // 如果没有默认私有模型,选择第一个公共模型 defaultModel ??= allModels.firstWhere( (model) => model.isPublic, orElse: () => allModels.first, ); setState(() { _selectedModel = defaultModel; }); } } /// 合并私有模型和公共模型 List _combineModels(AiConfigState aiState, PublicModelsState publicState) { final List 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; } /// 加载当前场景的摘要到输入框 void _loadCurrentSceneSummary(EditorLoaded state) { if (state.activeActId != null && state.activeChapterId != null && state.activeSceneId != null) { final scene = state.novel.getScene( state.activeActId!, state.activeChapterId!, sceneId: state.activeSceneId, ); if (scene != null && scene.summary.content.isNotEmpty) { setState(() { _summaryController.text = scene.summary.content; }); } } } @override void dispose() { _summaryController.dispose(); _styleController.dispose(); _generatedContentController.dispose(); _scrollController.removeListener(_handleUserScroll); _scrollController.dispose(); super.dispose(); } void _handleUserScroll() { if (_scrollController.hasClients) { // 如果用户向上滚动(滚动位置不在底部),标记为用户滚动 if (_scrollController.position.pixels < _scrollController.position.maxScrollExtent - 50) { _userScrolled = true; } // 如果用户滚动到底部,重置标记 if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 10) { _userScrolled = false; } } } /// 复制内容到剪贴板 void _copyToClipboard(String content) { Clipboard.setData(ClipboardData(text: content)).then((_) { TopToast.success(context, '内容已复制到剪贴板'); }); } Widget _buildModelConfigSection(BuildContext context, EditorLoaded state) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(8), border: Border.all(color: WebTheme.getSecondaryBorderColor(context), width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '模型设置', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.black87, ), ), const SizedBox(height: 8), // 统一模型选择器 _buildUnifiedModelSelector(context, state), const SizedBox(height: 10), // 智能上下文开关 Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '智能上下文', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.black87, ), ), const SizedBox(height: 2), Text( '自动检索相关设定和背景信息', style: TextStyle( fontSize: 10, color: Colors.grey[600], ), ), ], ), ), Switch( value: _enableSmartContext, onChanged: (value) { setState(() { _enableSmartContext = value; }); }, activeColor: Colors.black, activeTrackColor: Colors.grey[300], inactiveThumbColor: Colors.grey[400], inactiveTrackColor: Colors.grey[200], ), ], ), const SizedBox(height: 10), // 上下文选择 if (_contextInitialized) FormFieldFactory.createContextSelectionField( contextData: _contextSelectionData, onSelectionChanged: (newData) { setState(() { _contextSelectionData = newData; }); }, title: '附加上下文', description: '为AI提供的任何额外信息', onReset: () { setState(() { _contextSelectionData = ContextSelectionDataBuilder.fromNovel(state.novel); }); }, dropdownWidth: 400, initialChapterId: state.activeChapterId, initialSceneId: state.activeSceneId, ), if (_contextInitialized) const SizedBox(height: 10), // 关联提示词模板 FormFieldFactory.createPromptTemplateSelectionField( selectedTemplateId: _selectedPromptTemplateId, onTemplateSelected: (templateId) { setState(() { _selectedPromptTemplateId = templateId; }); }, aiFeatureType: 'SUMMARY_TO_SCENE', title: '关联提示词模板', description: '可选,选择一个提示词模板优化生成效果', onReset: () { setState(() { _selectedPromptTemplateId = null; }); }, onTemporaryPromptsSaved: (sys, user) { setState(() { _customSystemPrompt = sys.trim().isEmpty ? null : sys.trim(); _customUserPrompt = user.trim().isEmpty ? null : user.trim(); }); }, ), ], ), ); } /// 构建统一模型选择器 Widget _buildUnifiedModelSelector(BuildContext context, EditorLoaded state) { return BlocBuilder( builder: (context, aiState) { return BlocBuilder( builder: (context, publicState) { final allModels = _combineModels(aiState, publicState); return CompositedTransformTarget( link: _layerLink, child: InkWell( onTap: () { _showModelDropdown(context, state, allModels); }, borderRadius: BorderRadius.circular(6), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: WebTheme.getCardColor(context), borderRadius: BorderRadius.circular(6), border: Border.all(color: WebTheme.getSecondaryBorderColor(context), width: 1), ), child: Row( children: [ Expanded( child: _selectedModel != null ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _selectedModel!.displayName, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: WebTheme.getTextColor(context), ), ), const SizedBox(height: 2), Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: _selectedModel!.isPublic ? Colors.green[50] : Colors.blue[50], borderRadius: BorderRadius.circular(3), border: Border.all( color: _selectedModel!.isPublic ? Colors.green[200]! : Colors.blue[200]!, width: 0.5, ), ), child: Text( _selectedModel!.isPublic ? '系统' : '私有', style: TextStyle( fontSize: 10, color: _selectedModel!.isPublic ? Colors.green[700] : Colors.blue[700], fontWeight: FontWeight.w500, ), ), ), const SizedBox(width: 6), Text( _selectedModel!.provider, style: TextStyle( fontSize: 10, color: WebTheme.getSecondaryTextColor(context), ), ), ], ), ], ) : Text( '选择AI模型', style: TextStyle( fontSize: 13, color: WebTheme.getSecondaryTextColor(context), ), ), ), Icon( Icons.arrow_drop_down, color: WebTheme.getSecondaryTextColor(context), size: 20, ), ], ), ), ), ); }, ); }, ); } /// 显示模型选择下拉菜单 void _showModelDropdown(BuildContext context, EditorLoaded state, List allModels) { UnifiedAIModelDropdown.show( context: context, layerLink: _layerLink, selectedModel: _selectedModel, onModelSelected: (model) { setState(() { _selectedModel = model; }); }, showSettingsButton: false, maxHeight: 300, novel: state.novel, ); } /// 构建章节下拉菜单选项 List> _buildChapterDropdownItems(Novel novel) { final items = >[]; for (final act in novel.acts) { // 添加Act分组标题 items.add( DropdownMenuItem( enabled: false, child: Container( margin: const EdgeInsets.only(top: 6, bottom: 3), child: Text( act.title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black54, ), ), ), ), ); // 添加Act下的Chapter for (final chapter in act.chapters) { items.add( DropdownMenuItem( value: chapter.id, child: Container( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ const SizedBox(width: 8), // 缩进 const Icon(Icons.menu_book_outlined, size: 14, color: Colors.black54), const SizedBox(width: 6), Expanded( child: Text( chapter.title, style: const TextStyle( fontSize: 12, color: Colors.black87, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ); } } return items; } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, editorState) { if (editorState is! EditorLoaded) { return const Center(child: CircularProgressIndicator()); } return BlocConsumer( listener: (context, state) { // 只处理场景生成相关的状态变化 if (state is UniversalAIStreaming) { // 检查是否是场景生成请求 if (_isGenerationRequest(state)) { setState(() { _isGenerating = true; _generatedContentController.text = state.partialResponse; // _contentEdited = false; }); // 如果用户没有主动滚动,自动滚动到底部 if (!_userScrolled && _scrollController.hasClients) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } } } else if (state is UniversalAISuccess) { // 检查是否是场景生成请求 if (_isGenerationRequest(state)) { setState(() { _isGenerating = false; _thisInstanceIsGenerating = false; // 重置实例生成标记 _generatedContentController.text = state.response.content; // _contentEdited = false; }); // 🚀 生成完成后刷新积分 try { WidgetsBinding.instance.addPostFrameCallback((_) { // ignore: use_build_context_synchronously context.read().add(const RefreshUserCredits()); }); } catch (_) {} } } else if (state is UniversalAICancelled) { // 处理取消状态 if (_thisInstanceIsGenerating) { setState(() { _isGenerating = false; _thisInstanceIsGenerating = false; }); } } else if (state is UniversalAIError) { // 检查是否是场景生成请求 if (_isGenerationRequest(state)) { setState(() { _isGenerating = false; _thisInstanceIsGenerating = false; // 重置实例生成标记 }); TopToast.error(context, '生成场景失败: ${state.message}'); } } else if (state is UniversalAILoading) { // 检查是否是场景生成请求 if (_isGenerationRequest(state)) { setState(() { _isGenerating = true; }); } } }, builder: (context, universalAIState) { return Column( children: [ // 面板标题栏 _buildHeader(context, editorState), // 面板内容 Expanded( child: _buildSceneGenerationPanel(context, editorState), ), ], ); }, ); }, ); } Widget _buildHeader(BuildContext context, EditorLoaded state) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), border: Border( bottom: BorderSide( color: WebTheme.getSecondaryBorderColor(context), width: 1, ), ), ), child: Column( children: [ // 标题行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: WebTheme.getPrimaryColor(context), borderRadius: BorderRadius.circular(4), ), child: Icon( Icons.auto_awesome, size: 14, color: WebTheme.white, ), ), const SizedBox(width: 8), Text( 'AI场景生成', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), ], ), Row( children: [ // 状态指示器 if (_isGenerating) ...[ SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(WebTheme.getTextColor(context)), ), ), const SizedBox(width: 6), Text( '正在生成...', style: TextStyle( fontSize: 11, color: WebTheme.getTextColor(context), ), ), const SizedBox(width: 8), ], // 帮助按钮 Tooltip( message: '使用说明', child: IconButton( icon: Icon( Icons.help_outline, size: 16, color: WebTheme.getSecondaryTextColor(context), ), padding: const EdgeInsets.all(4), constraints: const BoxConstraints(minWidth: 28, minHeight: 28), onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: WebTheme.getCardColor(context), surfaceTintColor: Colors.transparent, title: Text( 'AI场景生成说明', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '1. 填写场景摘要/大纲描述想要生成的内容', style: TextStyle(fontSize: 12, color: WebTheme.getTextColor(context)), ), SizedBox(height: 6), Text( '2. 选择AI模型和配置参数', style: TextStyle(fontSize: 12, color: WebTheme.getTextColor(context)), ), SizedBox(height: 6), Text( '3. 可选择启用智能上下文获取相关设定', style: TextStyle(fontSize: 12, color: WebTheme.getTextColor(context)), ), SizedBox(height: 6), Text( '4. 点击"生成场景"按钮开始生成', style: TextStyle(fontSize: 12, color: WebTheme.getTextColor(context)), ), SizedBox(height: 6), Text( '5. 生成完成后,可以编辑内容并添加为新场景', style: TextStyle(fontSize: 12, color: WebTheme.getTextColor(context)), ), ], ), ), actions: [ ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: WebTheme.getPrimaryColor(context), foregroundColor: WebTheme.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), child: const Text('了解了', style: TextStyle(fontSize: 12)), ), ], ), ); }, ), ), const SizedBox(width: 2), IconButton( icon: Icon(Icons.close, size: 16, color: WebTheme.getSecondaryTextColor(context)), constraints: const BoxConstraints(minWidth: 28, minHeight: 28), padding: const EdgeInsets.all(4), onPressed: widget.onClose, tooltip: '关闭', ), ], ), ], ), // 当前场景信息行 const SizedBox(height: 8), _buildCurrentSceneInfo(context, state), ], ), ); } Widget _buildCurrentSceneInfo(BuildContext context, EditorLoaded state) { return SceneSelector( novel: state.novel, activeSceneId: state.activeSceneId, onSceneSelected: (sceneId, actId, chapterId) { // 更新活跃场景 context.read().add(SetActiveScene( actId: actId, chapterId: chapterId, sceneId: sceneId, )); }, onSummaryLoaded: (summary) { // 加载场景摘要到输入框 setState(() { _summaryController.text = summary; }); }, ); } /// 构建场景生成面板 Widget _buildSceneGenerationPanel(BuildContext context, EditorLoaded state) { final hasGenerated = _generatedContentController.text.isNotEmpty; return Padding( padding: const EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 模型配置区域 _buildModelConfigSection(context, state), const SizedBox(height: 10), // 摘要文本输入 const Text( '场景摘要/大纲', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.black87, ), ), const SizedBox(height: 6), Container( decoration: BoxDecoration( color: WebTheme.getCardColor(context), borderRadius: BorderRadius.circular(8), border: Border.all( color: WebTheme.getSecondaryBorderColor(context), width: 1, ), ), child: TextField( controller: _summaryController, maxLines: 4, decoration: InputDecoration( hintText: '请输入场景大纲或摘要,AI将根据此内容生成完整场景', hintStyle: TextStyle(fontSize: 12, color: WebTheme.getSecondaryTextColor(context)), contentPadding: const EdgeInsets.all(12), border: InputBorder.none, suffixIcon: _summaryController.text.isNotEmpty ? IconButton( icon: Icon( Icons.clear, size: 16, color: WebTheme.getSecondaryTextColor(context), ), constraints: const BoxConstraints(minWidth: 28, minHeight: 28), padding: const EdgeInsets.all(4), onPressed: () { setState(() { _summaryController.clear(); }); }, ) : null, ), style: TextStyle( fontSize: 13, height: 1.4, color: WebTheme.getTextColor(context), ), onChanged: (_) => setState(() {}), ), ), const SizedBox(height: 10), // 风格指令输入 const Text( '风格指令(可选)', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.black87, ), ), const SizedBox(height: 6), Container( decoration: BoxDecoration( color: WebTheme.getCardColor(context), borderRadius: BorderRadius.circular(8), border: Border.all( color: WebTheme.getSecondaryBorderColor(context), width: 1, ), ), child: TextField( controller: _styleController, decoration: InputDecoration( hintText: '例如:多对话,少描写,悬疑风格', hintStyle: TextStyle(fontSize: 12, color: WebTheme.getSecondaryTextColor(context)), contentPadding: const EdgeInsets.all(12), border: InputBorder.none, suffixIcon: _styleController.text.isNotEmpty ? IconButton( icon: Icon( Icons.clear, size: 16, color: WebTheme.getSecondaryTextColor(context), ), constraints: const BoxConstraints(minWidth: 28, minHeight: 28), padding: const EdgeInsets.all(4), onPressed: () { setState(() { _styleController.clear(); }); }, ) : null, ), style: TextStyle( fontSize: 13, color: WebTheme.getTextColor(context), ), onChanged: (_) => setState(() {}), ), ), const SizedBox(height: 10), // 章节选择(可选) if (state.novel.acts.isNotEmpty) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '目标章节(可选)', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.black87, ), ), if (state.activeChapterId != null) OutlinedButton.icon( onPressed: () { // 查找当前章节信息 String chapterTitle = ""; for (final act in state.novel.acts) { for (final chapter in act.chapters) { if (chapter.id == state.activeChapterId) { chapterTitle = chapter.title; break; } } if (chapterTitle.isNotEmpty) break; } if (chapterTitle.isNotEmpty) { // 添加章节相关信息到摘要 final currentText = _summaryController.text; final chapterContext = "本场景为《$chapterTitle》章节的一部分,"; if (currentText.isNotEmpty) { _summaryController.text = '$chapterContext$currentText'; } else { _summaryController.text = chapterContext; } } }, icon: Icon(Icons.add_box_outlined, size: 14, color: WebTheme.getTextColor(context)), label: Text( '添加到摘要', style: TextStyle(fontSize: 11, color: WebTheme.getTextColor(context)), ), style: OutlinedButton.styleFrom( side: BorderSide(color: WebTheme.getSecondaryBorderColor(context), width: 1), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), minimumSize: const Size(0, 28), ), ), ], ), const SizedBox(height: 6), Container( decoration: BoxDecoration( color: WebTheme.getCardColor(context), borderRadius: BorderRadius.circular(8), border: Border.all( color: WebTheme.getSecondaryBorderColor(context), width: 1, ), ), padding: const EdgeInsets.symmetric(horizontal: 12), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: state.activeChapterId, items: _buildChapterDropdownItems(state.novel), onChanged: (chapterId) { if (chapterId != null) { // 查找选中章节所属的Act String? actId; for (final act in state.novel.acts) { for (final chapter in act.chapters) { if (chapter.id == chapterId) { actId = act.id; break; } } if (actId != null) break; } if (actId != null) { // 更新活跃章节 context.read().add(SetActiveChapter( actId: actId, chapterId: chapterId, )); } } }, style: TextStyle( fontSize: 12, color: WebTheme.getTextColor(context), ), hint: Text( '选择一个目标章节', style: TextStyle( fontSize: 12, color: WebTheme.getSecondaryTextColor(context), ), ), icon: Icon( Icons.keyboard_arrow_down_rounded, size: 16, color: WebTheme.getSecondaryTextColor(context), ), dropdownColor: WebTheme.getCardColor(context), menuMaxHeight: 240, ), ), ), ], // 生成结果或操作区域 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (hasGenerated || _isGenerating) ...[ const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '生成结果', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: WebTheme.getTextColor(context), ), ), if (hasGenerated) Row( children: [ Tooltip( message: '重新生成', child: IconButton( onPressed: () { // 重新生成内容 context.read().add( GenerateSceneFromSummaryRequested( novelId: state.novel.id, summary: _summaryController.text, chapterId: state.activeChapterId, styleInstructions: _styleController.text.isNotEmpty ? _styleController.text : null, useStreamingMode: true, ), ); // 重置用户滚动标记 _userScrolled = false; }, icon: Icon(Icons.refresh, size: 16, color: WebTheme.getSecondaryTextColor(context)), tooltip: '重新生成', constraints: const BoxConstraints(minWidth: 28, minHeight: 28), padding: const EdgeInsets.all(4), ), ), Tooltip( message: '复制全文', child: IconButton( onPressed: () => _copyToClipboard(_generatedContentController.text), icon: Icon(Icons.copy, size: 16, color: WebTheme.getSecondaryTextColor(context)), tooltip: '复制全文', constraints: const BoxConstraints(minWidth: 28, minHeight: 28), padding: const EdgeInsets.all(4), ), ), Tooltip( message: '添加为新场景', child: IconButton( onPressed: () { // 将生成内容应用到编辑器 if (state.activeActId != null && state.activeChapterId != null) { // 获取布局管理器 // 最初用于触发布局刷新,当前未使用 // final layoutManager = Provider.of(context, listen: false); // 创建新场景并使用生成内容 final sceneId = 'scene_${DateTime.now().millisecondsSinceEpoch}'; // 添加新场景 context.read().add(AddNewScene( novelId: widget.novelId, actId: state.activeActId!, chapterId: state.activeChapterId!, sceneId: sceneId, )); // 等待短暂时间,确保场景已添加 Future.delayed(const Duration(milliseconds: 500), () { // 设置场景内容 context.read().add(UpdateSceneContent( novelId: widget.novelId, actId: state.activeActId!, chapterId: state.activeChapterId!, sceneId: sceneId, content: _generatedContentController.text, )); // 设置为活动场景 context.read().add(SetActiveScene( actId: state.activeActId!, chapterId: state.activeChapterId!, sceneId: sceneId, )); // 关闭生成面板 widget.onClose(); // 显示通知 TopToast.success(context, '已创建新场景并应用生成内容'); }); } }, icon: Icon(Icons.add_circle_outline, size: 16, color: WebTheme.getSecondaryTextColor(context)), tooltip: '添加为新场景', constraints: const BoxConstraints(minWidth: 28, minHeight: 28), padding: const EdgeInsets.all(4), ), ), ], ), ], ), const SizedBox(height: 6), Expanded( child: _buildGenerationResultSection(context, state), ), ], const SizedBox(height: 12), // 生成按钮区域 _buildGenerationButtons(context, state), ], ), ), ], ), ); } Widget _buildGenerationResultSection(BuildContext context, EditorLoaded state) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all( color: Colors.grey[300]!, width: 1, ), ), clipBehavior: Clip.antiAlias, child: _isGenerating && _generatedContentController.text.isEmpty ? const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.black), ), ), SizedBox(height: 12), Text( '正在生成场景...', style: TextStyle( fontSize: 12, color: Colors.black87, ), ), ], ), ) : !_generatedContentController.text.isNotEmpty && !_isGenerating ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.auto_awesome, color: Colors.grey[400], size: 28, ), const SizedBox(height: 12), Text( '点击"生成场景"按钮开始生成', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ) : TextField( controller: _generatedContentController, scrollController: _scrollController, maxLines: null, expands: true, decoration: const InputDecoration( contentPadding: EdgeInsets.all(12), border: InputBorder.none, hintText: '生成的场景内容将显示在这里', hintStyle: TextStyle(fontSize: 12, color: Colors.black45), ), style: const TextStyle( fontSize: 13, height: 1.4, color: Colors.black87, ), onChanged: (_) { setState(() { // _contentEdited = true; }); }, ), ); } Widget _buildGenerationButtons(BuildContext context, EditorLoaded state) { final hasContent = _summaryController.text.isNotEmpty; if (!_isGenerating) { return SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: (hasContent && _selectedModel != null) ? () => _generateScene(context, state) : null, icon: const Icon(Icons.auto_awesome, size: 16, color: Colors.white), label: const Text( '生成场景', style: TextStyle(fontSize: 13, color: Colors.white), ), style: ElevatedButton.styleFrom( backgroundColor: (hasContent && _selectedModel != null) ? Colors.black : Colors.grey[400], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ), ); } else { return SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () { context.read().add(const StopStreamRequestEvent()); setState(() { _thisInstanceIsGenerating = false; _isGenerating = false; }); }, icon: const Icon(Icons.cancel, size: 16, color: Colors.black87), label: const Text( '取消生成', style: TextStyle(fontSize: 13, color: Colors.black87), ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.grey, width: 1), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ), ); } } /// 检查是否是场景生成请求 bool _isGenerationRequest(UniversalAIState state) { // 对于流式响应状态,只有当前实例发起的请求才处理 if (state is UniversalAIStreaming) { return _thisInstanceIsGenerating; } // 对于成功状态,检查请求类型 else if (state is UniversalAISuccess) { return state.response.requestType == AIRequestType.generation; } // 对于错误和加载状态,检查当前实例是否有生成任务 else if (state is UniversalAIError || state is UniversalAILoading) { return _thisInstanceIsGenerating; } return false; } /// 生成场景 void _generateScene(BuildContext context, EditorLoaded state) { if (_selectedModel == null) return; // 清空现有内容 _generatedContentController.clear(); AppLogger.i('AIGenerationPanel', '开始生成场景'); // 使用公共逻辑创建模型配置(公共模型会被包装为临时配置) final modelConfig = createModelConfig(_selectedModel!); // 构建AI请求(将摘要文本按需从Quill Delta转换为纯文本) final String plainSummaryText = QuillHelper.deltaToText(_summaryController.text); final request = UniversalAIRequest( requestType: AIRequestType.generation, userId: AppConfig.userId ?? 'unknown', novelId: widget.novelId, chapterId: state.activeChapterId, sceneId: state.activeSceneId, modelConfig: modelConfig, selectedText: plainSummaryText, // 使用纯文本作为输入 instructions: _styleController.text.isNotEmpty ? '请根据以下摘要生成完整的小说场景。风格要求:${_styleController.text}' : '请根据以下摘要生成完整的小说场景。', contextSelections: _contextSelectionData, enableSmartContext: _enableSmartContext, parameters: { 'temperature': 0.8, 'maxTokens': 2000, 'promptTemplateId': _selectedPromptTemplateId, if (_customSystemPrompt != null) 'customSystemPrompt': _customSystemPrompt, if (_customUserPrompt != null) 'customUserPrompt': _customUserPrompt, }, metadata: createModelMetadata(_selectedModel!, { 'actId': state.activeActId, 'chapterId': state.activeChapterId, 'sceneId': state.activeSceneId, 'action': 'summary_to_scene', 'source': 'ai_generation_panel', }), ); // 公共模型预估积分并确认 if (_selectedModel!.isPublic) { handlePublicModelCreditConfirmation(_selectedModel!, request).then((ok) { if (!ok) return; setState(() { _thisInstanceIsGenerating = true; }); context.read().add(SendAIStreamRequestEvent(request)); }); return; } // 私有模型直接发送 setState(() { _thisInstanceIsGenerating = true; }); context.read().add(SendAIStreamRequestEvent(request)); } }