import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:ainoval/utils/web_theme.dart'; import '../../../models/novel_structure.dart'; import '../../../models/user_ai_model_config_model.dart'; /// 现代化剧情大纲生成配置卡片 - 全局通用组件 /// /// 此组件负责剧情推演的参数配置功能: /// 1. 章节范围选择 - 确定推演的上下文范围 /// 2. AI模型配置 - 选择和管理生成模型 /// 3. 生成参数设置 - 选项数量、作者引导等 /// 4. 配置验证和错误提示 /// /// 设计特点: /// - 采用纯黑白配色方案,符合现代简洁审美 /// - 响应式布局,适配宽屏和窄屏设备 /// - 统一的视觉层次和组件间距 /// - 清晰的信息架构和用户引导 class ModernConfigCard extends StatefulWidget { /// 章节列表 - 用于范围选择 final List chapters; /// AI模型配置列表 - 可用的生成模型 final List aiModelConfigs; /// 当前选中的上下文开始章节ID final String? startChapterId; /// 当前选中的上下文结束章节ID final String? endChapterId; /// 生成选项数量 - 控制生成的剧情选项个数 final int numOptions; /// 作者引导 - 用户对剧情发展的指导意见 final String? authorGuidance; /// 是否正在生成 - 控制界面状态 final bool isGenerating; /// 开始章节变更回调 final Function(String?) onStartChapterChanged; /// 结束章节变更回调 final Function(String?) onEndChapterChanged; /// 选项数量变更回调 final Function(int) onNumOptionsChanged; /// 作者引导变更回调 final Function(String?) onAuthorGuidanceChanged; /// 生成回调 - 触发剧情生成 final Function(int numOptions, String? authorGuidance, List? selectedConfigIds) onGenerate; /// 跳转到添加模型页面的回调 final VoidCallback? onNavigateToAddModel; /// 跳转到配置特定模型页面的回调 final Function(String configId)? onConfigureModel; const ModernConfigCard({ Key? key, required this.chapters, required this.aiModelConfigs, this.startChapterId, this.endChapterId, this.numOptions = 3, this.authorGuidance, this.isGenerating = false, required this.onStartChapterChanged, required this.onEndChapterChanged, required this.onNumOptionsChanged, required this.onAuthorGuidanceChanged, required this.onGenerate, this.onNavigateToAddModel, this.onConfigureModel, }) : super(key: key); @override State createState() => _ModernConfigCardState(); } /// 配置卡片状态管理 /// /// 负责: /// 1. 本地状态管理(表单数据、验证状态等) /// 2. 用户交互处理 /// 3. 数据验证和错误提示 /// 4. 响应式布局计算 class _ModernConfigCardState extends State { late int _numOptions; late TextEditingController _authorGuidanceController; List _selectedConfigIds = []; String? _chapterRangeError; @override void initState() { super.initState(); _numOptions = widget.numOptions; _authorGuidanceController = TextEditingController(text: widget.authorGuidance); // 默认选择第一个已验证的模型配置 final validatedConfigs = widget.aiModelConfigs.where((config) => config.isValidated).toList(); if (validatedConfigs.isNotEmpty) { _selectedConfigIds = [validatedConfigs.first.id]; } // 初始化时验证章节范围 _validateChapterRange(widget.startChapterId, widget.endChapterId); } @override void didUpdateWidget(ModernConfigCard oldWidget) { super.didUpdateWidget(oldWidget); // 同步外部状态变化 if (oldWidget.authorGuidance != widget.authorGuidance) { _authorGuidanceController.text = widget.authorGuidance ?? ''; } if (oldWidget.numOptions != widget.numOptions) { _numOptions = widget.numOptions; } // 当起止章节ID变化时验证范围 if (oldWidget.startChapterId != widget.startChapterId || oldWidget.endChapterId != widget.endChapterId) { _validateChapterRange(widget.startChapterId, widget.endChapterId); } } @override void dispose() { _authorGuidanceController.dispose(); super.dispose(); } /// 验证章节范围的合理性 /// 确保选择的章节范围符合逻辑要求 void _validateChapterRange(String? startChapterId, String? endChapterId) { setState(() { _chapterRangeError = null; }); if (startChapterId == null || endChapterId == null) { setState(() { _chapterRangeError = '请选择完整的章节范围'; }); return; } // 查找章节在列表中的位置 final startIndex = widget.chapters.indexWhere((c) => c.id == startChapterId); final endIndex = widget.chapters.indexWhere((c) => c.id == endChapterId); if (startIndex == -1 || endIndex == -1) { setState(() { _chapterRangeError = '选择的章节不存在'; }); return; } if (startIndex > endIndex) { setState(() { _chapterRangeError = '开始章节不能晚于结束章节'; }); return; } // 检查章节范围是否过大(可选的业务逻辑) final rangeSize = endIndex - startIndex + 1; if (rangeSize > 10) { setState(() { _chapterRangeError = '章节范围过大,建议选择不超过10个章节'; }); return; } } /// 根据模型名称获取对应的图标 /// 提供视觉区分不同类型的AI模型 IconData _getIconForModel(String modelName) { final lowerName = modelName.toLowerCase(); if (lowerName.contains('gpt') || lowerName.contains('openai')) { return LucideIcons.gem; } else if (lowerName.contains('claude')) { return LucideIcons.search_code; } else if (lowerName.contains('gemini') || lowerName.contains('bard')) { return LucideIcons.brain_circuit; } else if (lowerName.contains('llama') || lowerName.contains('meta')) { return LucideIcons.flask_conical; } else if (lowerName.contains('mistral') || lowerName.contains('mixtral')) { return LucideIcons.zap; } return LucideIcons.cpu; // 默认图标 } @override Widget build(BuildContext context) { // 检查生成按钮是否应该禁用 final bool isGenerateButtonDisabled = widget.isGenerating || _chapterRangeError != null; return Container( padding: const EdgeInsets.all(32), // 统一的内边距 child: LayoutBuilder( builder: (context, constraints) { // 响应式布局判断 final isWideScreen = constraints.maxWidth >= 960; return isWideScreen ? _buildWideLayout(context, isGenerateButtonDisabled) : _buildNarrowLayout(context, isGenerateButtonDisabled); }, ), ); } /// 宽屏布局(AI模型配置显示在右侧) /// 充分利用宽屏空间,提供更好的信息组织 Widget _buildWideLayout(BuildContext context, bool isGenerateButtonDisabled) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 左侧:主要配置区域 Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题区域 _buildSectionHeader( context, '生成配置', '设置剧情推演的基本参数', LucideIcons.settings, ), const SizedBox(height: 24), // 章节配置字段 _buildChapterConfigFields(), // 章节范围验证错误提示 if (_chapterRangeError != null) _buildErrorMessage(_chapterRangeError!), const SizedBox(height: 24), // 作者引导输入区域 _buildAuthorGuidanceField(), const SizedBox(height: 32), // 生成按钮 Align( alignment: Alignment.centerRight, child: _buildGenerateButton(isGenerateButtonDisabled), ), ], ), ), const SizedBox(width: 40), // 左右区域间距 // 右侧区域已移到左侧栏独立显示 ], ); } /// 窄屏布局(AI模型配置显示在下方) /// 适配移动设备和小屏幕的垂直布局 Widget _buildNarrowLayout(BuildContext context, bool isGenerateButtonDisabled) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题区域 _buildSectionHeader( context, '生成配置', '设置剧情推演的基本参数', LucideIcons.settings, ), const SizedBox(height: 24), // 章节配置字段 _buildChapterConfigFields(), // 章节范围验证错误提示 if (_chapterRangeError != null) _buildErrorMessage(_chapterRangeError!), const SizedBox(height: 24), // 作者引导输入区域 _buildAuthorGuidanceField(), const SizedBox(height: 24), // AI模型选择区域已移到左侧栏独立显示 const SizedBox(height: 32), // 生成按钮 Align( alignment: Alignment.centerRight, child: _buildGenerateButton(isGenerateButtonDisabled), ), ], ); } /// 构建区域标题组件 /// 提供统一的标题样式和视觉层次 Widget _buildSectionHeader(BuildContext context, String title, String subtitle, IconData icon) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( icon, size: 24, color: WebTheme.getTextColor(context), ), const SizedBox(width: 12), Text( title, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: WebTheme.getTextColor(context), fontWeight: FontWeight.w600, height: 1.2, ), ), ], ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.only(left: 36), child: Text( subtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: WebTheme.getSecondaryTextColor(context), height: 1.4, ), ), ), ], ); } /// 构建错误提示组件 /// 统一的错误信息展示样式 Widget _buildErrorMessage(String message) { return Container( margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: WebTheme.error.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: WebTheme.error.withOpacity(0.3), width: 1, ), ), child: Row( children: [ Icon( LucideIcons.circle_alert, size: 18, color: WebTheme.error, ), const SizedBox(width: 12), Expanded( child: Text( message, style: TextStyle( color: WebTheme.error, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ], ), ); } /// 构建章节配置区域 /// 包含起始章节、结束章节和生成数量的选择 Widget _buildChapterConfigFields() { return LayoutBuilder( builder: (context, constraints) { final totalWidth = constraints.maxWidth; return Wrap( spacing: 20, runSpacing: 20, children: [ // 起始章节选择 SizedBox( width: totalWidth < 600 ? totalWidth : (totalWidth - 40) / 3, child: _buildStartChapterDropdown(), ), // 结束章节选择 SizedBox( width: totalWidth < 600 ? totalWidth : (totalWidth - 40) / 3, child: _buildEndChapterDropdown(), ), // 生成数量选择 SizedBox( width: totalWidth < 600 ? totalWidth : (totalWidth - 40) / 3, child: _buildNumOptionsDropdown(), ), ], ); }, ); } /// 构建起始章节下拉框 Widget _buildStartChapterDropdown() { return _buildDropdownField( label: '起始章节', icon: LucideIcons.book_copy, value: widget.startChapterId, items: widget.chapters.map((chapter) { return DropdownMenuItem( value: chapter.id, child: Text( chapter.title, style: const TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis, ), ); }).toList(), onChanged: widget.isGenerating ? null : widget.onStartChapterChanged, hint: '选择起始章节', ); } /// 构建结束章节下拉框 Widget _buildEndChapterDropdown() { return _buildDropdownField( label: '结束章节', icon: LucideIcons.book_marked, value: widget.endChapterId, items: widget.chapters.map((chapter) { return DropdownMenuItem( value: chapter.id, child: Text( chapter.title, style: const TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis, ), ); }).toList(), onChanged: widget.isGenerating ? null : widget.onEndChapterChanged, hint: '选择结束章节', ); } /// 构建生成数量下拉框 Widget _buildNumOptionsDropdown() { return _buildDropdownField( label: '生成数量', icon: LucideIcons.list_ordered, value: _numOptions, items: [2, 3, 4, 5].map((number) { return DropdownMenuItem( value: number, child: Text('$number 个选项'), ); }).toList(), onChanged: widget.isGenerating ? null : (value) { if (value != null) { setState(() { _numOptions = value; }); widget.onNumOptionsChanged(value); } }, hint: '选择数量', ); } /// 通用下拉框组件 /// 提供统一的下拉框样式和交互 Widget _buildDropdownField({ required String label, required IconData icon, required T? value, required List> items, required Function(T?)? onChanged, required String hint, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标签 Row( children: [ Icon( icon, size: 18, color: WebTheme.getTextColor(context), ), const SizedBox(width: 8), Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: WebTheme.getTextColor(context), ), ), ], ), const SizedBox(height: 8), // 下拉框 Container( decoration: BoxDecoration( border: Border.all( color: WebTheme.getBorderColor(context), width: 1, ), borderRadius: BorderRadius.circular(8), ), child: DropdownButtonFormField( value: value, decoration: WebTheme.getBorderlessInputDecoration( context: context, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), items: items, onChanged: onChanged, isExpanded: true, icon: Icon( LucideIcons.chevron_down, size: 18, color: WebTheme.getSecondaryTextColor(context), ), hint: Text( hint, style: TextStyle( color: WebTheme.getSecondaryTextColor(context), fontSize: 14, ), ), dropdownColor: WebTheme.getCardColor(context), style: TextStyle( color: WebTheme.getTextColor(context), fontSize: 14, ), ), ), ], ); } /// 构建作者引导文本框 /// 用户输入对剧情发展的指导意见 Widget _buildAuthorGuidanceField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标签 Row( children: [ Icon( LucideIcons.lightbulb, size: 18, color: WebTheme.getTextColor(context), ), const SizedBox(width: 8), Text( '作者引导(可选)', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: WebTheme.getTextColor(context), ), ), ], ), const SizedBox(height: 8), // 输入框 Container( decoration: BoxDecoration( border: Border.all( color: WebTheme.getBorderColor(context), width: 1, ), borderRadius: BorderRadius.circular(8), ), child: TextField( controller: _authorGuidanceController, enabled: !widget.isGenerating, decoration: WebTheme.getBorderlessInputDecoration( hintText: '例如:希望侧重角色成长;引入新的冲突;避免某些情节元素...', context: context, contentPadding: const EdgeInsets.all(16), ), style: TextStyle( color: WebTheme.getTextColor(context), fontSize: 14, height: 1.5, ), maxLines: 3, onChanged: widget.onAuthorGuidanceChanged, ), ), const SizedBox(height: 8), // 提示信息 Row( children: [ Icon( LucideIcons.info, size: 14, color: WebTheme.getSecondaryTextColor(context), ), const SizedBox(width: 6), Expanded( child: Text( '告诉AI您对下一段剧情的期望、偏好或需要避免的元素', style: TextStyle( fontSize: 12, color: WebTheme.getSecondaryTextColor(context), height: 1.3, ), ), ), ], ), ], ); } /// 构建生成按钮 /// 统一的主要操作按钮样式 Widget _buildGenerateButton(bool isDisabled) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: isDisabled ? null : [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.3), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: ElevatedButton.icon( onPressed: isDisabled ? null : () { widget.onGenerate( _numOptions, _authorGuidanceController.text.isEmpty ? null : _authorGuidanceController.text, _selectedConfigIds.isEmpty ? null : _selectedConfigIds, ); }, icon: widget.isGenerating ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( color: WebTheme.getCardColor(context), strokeWidth: 2, ), ) : Icon( LucideIcons.brain_circuit, size: 20, color: WebTheme.getCardColor(context), ), label: Text( widget.isGenerating ? '生成中...' : '生成剧情大纲', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: WebTheme.getCardColor(context), ), ), style: WebTheme.getPrimaryButtonStyle(context).copyWith( padding: MaterialStateProperty.all( const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), minimumSize: MaterialStateProperty.all(const Size(160, 48)), ), ), ); } /// 构建AI模型选择器 /// 支持多选和模型状态显示 Widget _buildAIModelSelection() { final allConfigs = widget.aiModelConfigs; // 模型列表为空的情况 if (allConfigs.isEmpty) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader( context, 'AI 模型', '选择用于生成的AI模型', LucideIcons.list_checks, ), const SizedBox(height: 16), _buildEmptyModelState(), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题和添加按钮 Row( children: [ Expanded( child: _buildSectionHeader( context, 'AI 模型', '选择用于生成的AI模型', LucideIcons.list_checks, ), ), if (widget.onNavigateToAddModel != null) TextButton.icon( icon: Icon( LucideIcons.plus, size: 16, color: WebTheme.getTextColor(context), ), label: Text( '添加', style: TextStyle( fontSize: 12, color: WebTheme.getTextColor(context), ), ), onPressed: widget.onNavigateToAddModel, style: WebTheme.getSecondaryButtonStyle(context).copyWith( padding: MaterialStateProperty.all( const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ), minimumSize: MaterialStateProperty.all(Size.zero), ), ), ], ), const SizedBox(height: 16), // 模型列表 _buildModelList(allConfigs), // 选择提示信息 const SizedBox(height: 16), _buildModelSelectionHints(), ], ); } /// 构建空模型状态 Widget _buildEmptyModelState() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: WebTheme.getEmptyStateColor(context), borderRadius: BorderRadius.circular(8), border: Border.all( color: WebTheme.getBorderColor(context), width: 1, ), ), child: Column( children: [ Icon( LucideIcons.info, size: 32, color: WebTheme.getSecondaryTextColor(context), ), const SizedBox(height: 12), Text( '暂无可用模型', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: WebTheme.getTextColor(context), ), ), const SizedBox(height: 8), Text( '请前往设置页面添加和配置AI模型服务', style: TextStyle( fontSize: 14, color: WebTheme.getSecondaryTextColor(context), ), textAlign: TextAlign.center, ), ], ), ); } /// 构建模型列表 Widget _buildModelList(List configs) { return Container( decoration: BoxDecoration( border: Border.all( color: WebTheme.getBorderColor(context), width: 1, ), borderRadius: BorderRadius.circular(8), ), child: Column( children: configs.asMap().entries.map((entry) { final index = entry.key; final config = entry.value; final isSelected = _selectedConfigIds.contains(config.id); final isValidated = config.isValidated; final isLast = index == configs.length - 1; return Container( decoration: BoxDecoration( border: isLast ? null : Border( bottom: BorderSide( color: WebTheme.getBorderColor(context), width: 1, ), ), ), child: isValidated ? _buildValidatedModelItem(config, isSelected) : _buildUnvalidatedModelItem(config), ); }).toList(), ), ); } /// 构建已验证的模型项 Widget _buildValidatedModelItem(UserAIModelConfigModel config, bool isSelected) { return CheckboxListTile( title: Text( config.name, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: WebTheme.getTextColor(context), ), ), subtitle: Text( '已验证可用', style: TextStyle( fontSize: 12, color: WebTheme.success, ), ), value: isSelected, onChanged: widget.isGenerating ? null : (selected) { setState(() { if (selected == true) { _selectedConfigIds.add(config.id); } else { _selectedConfigIds.remove(config.id); } }); }, secondary: Icon( _getIconForModel(config.name), color: isSelected ? WebTheme.getTextColor(context) : WebTheme.getSecondaryTextColor(context), size: 20, ), controlAffinity: ListTileControlAffinity.leading, activeColor: WebTheme.getTextColor(context), checkColor: WebTheme.getCardColor(context), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), ); } /// 构建未验证的模型项 Widget _buildUnvalidatedModelItem(UserAIModelConfigModel config) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: Icon( _getIconForModel(config.name), color: WebTheme.getSecondaryTextColor(context), size: 20, ), title: Text( config.name, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: WebTheme.getSecondaryTextColor(context), fontStyle: FontStyle.italic, ), ), subtitle: Text( '需要配置验证', style: TextStyle( fontSize: 12, color: WebTheme.warning, ), ), trailing: OutlinedButton( onPressed: () { if (widget.onConfigureModel != null) { widget.onConfigureModel!(config.id); } }, style: WebTheme.getSecondaryButtonStyle(context).copyWith( padding: MaterialStateProperty.all( const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ), minimumSize: MaterialStateProperty.all(Size.zero), ), child: Text( '配置', style: TextStyle( fontSize: 12, color: WebTheme.getTextColor(context), ), ), ), enabled: false, ); } /// 构建模型选择提示信息 Widget _buildModelSelectionHints() { if (_selectedConfigIds.isEmpty) { return _buildHintBox( '请至少选择一个AI模型', LucideIcons.circle_alert, WebTheme.error, ); } else if (_selectedConfigIds.length < _numOptions) { return _buildHintBox( '注意:选择的模型数量少于生成数量,部分模型将被重复使用', LucideIcons.info, WebTheme.warning, ); } return const SizedBox.shrink(); } /// 构建提示框组件 Widget _buildHintBox(String message, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(6), border: Border.all( color: color.withOpacity(0.3), width: 1, ), ), child: Row( children: [ Icon( icon, size: 16, color: color, ), const SizedBox(width: 8), Expanded( child: Text( message, style: TextStyle( fontSize: 12, color: color, fontWeight: FontWeight.w500, ), ), ), ], ), ); } }