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

1009 lines
29 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_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<Chapter> chapters;
/// AI模型配置列表 - 可用的生成模型
final List<UserAIModelConfigModel> 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<String>? 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<ModernConfigCard> createState() => _ModernConfigCardState();
}
/// 配置卡片状态管理
///
/// 负责:
/// 1. 本地状态管理(表单数据、验证状态等)
/// 2. 用户交互处理
/// 3. 数据验证和错误提示
/// 4. 响应式布局计算
class _ModernConfigCardState extends State<ModernConfigCard> {
late int _numOptions;
late TextEditingController _authorGuidanceController;
List<String> _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<String>(
label: '起始章节',
icon: LucideIcons.book_copy,
value: widget.startChapterId,
items: widget.chapters.map((chapter) {
return DropdownMenuItem<String>(
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<String>(
label: '结束章节',
icon: LucideIcons.book_marked,
value: widget.endChapterId,
items: widget.chapters.map((chapter) {
return DropdownMenuItem<String>(
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<int>(
label: '生成数量',
icon: LucideIcons.list_ordered,
value: _numOptions,
items: [2, 3, 4, 5].map((number) {
return DropdownMenuItem<int>(
value: number,
child: Text('$number 个选项'),
);
}).toList(),
onChanged: widget.isGenerating
? null
: (value) {
if (value != null) {
setState(() {
_numOptions = value;
});
widget.onNumOptionsChanged(value);
}
},
hint: '选择数量',
);
}
/// 通用下拉框组件
/// 提供统一的下拉框样式和交互
Widget _buildDropdownField<T>({
required String label,
required IconData icon,
required T? value,
required List<DropdownMenuItem<T>> 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<T>(
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<UserAIModelConfigModel> 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,
),
),
),
],
),
);
}
}