Files
MaliangAINovalWriter/AINoval/lib/screens/setting_generation/widgets/settings_tree_widget.dart
2025-09-10 00:07:52 +08:00

650 lines
23 KiB
Dart
Raw 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:ainoval/utils/web_theme.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../blocs/setting_generation/setting_generation_bloc.dart';
import '../../../blocs/setting_generation/setting_generation_event.dart';
import '../../../blocs/setting_generation/setting_generation_state.dart';
import '../../../models/setting_node.dart';
import 'setting_node_widget.dart';
import 'ai_shimmer_placeholder.dart';
import '../../../utils/logger.dart';
import '../../../widgets/common/top_toast.dart';
/// 节点与层级信息的包装类
class _NodeWithLevel {
final SettingNode node;
final int level;
const _NodeWithLevel({
required this.node,
required this.level,
});
}
/// 设定树组件
class SettingsTreeWidget extends StatelessWidget {
final String? lastInitialPrompt;
final String? lastStrategy;
final String? lastModelConfigId;
final String? novelId;
final String? userId;
const SettingsTreeWidget({
Key? key,
this.lastInitialPrompt,
this.lastStrategy,
this.lastModelConfigId,
this.novelId,
this.userId,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<SettingGenerationBloc, SettingGenerationState>(
buildWhen: (previous, current) {
// 类型变化:一定重建
if (previous.runtimeType != current.runtimeType) return true;
// 进行中:当节点树/渲染相关/选中/视图模式或操作文案改变时才重建
if (previous is SettingGenerationInProgress && current is SettingGenerationInProgress) {
return previous.activeSession.rootNodes != current.activeSession.rootNodes ||
previous.renderedNodeIds != current.renderedNodeIds ||
previous.selectedNodeId != current.selectedNodeId ||
previous.viewMode != current.viewMode ||
previous.currentOperation != current.currentOperation;
}
// 完成:当节点树/渲染集合/选中/视图模式/活跃会话切换时才重建
if (previous is SettingGenerationCompleted && current is SettingGenerationCompleted) {
return previous.activeSession.rootNodes != current.activeSession.rootNodes ||
previous.renderedNodeIds != current.renderedNodeIds ||
previous.selectedNodeId != current.selectedNodeId ||
previous.viewMode != current.viewMode ||
previous.activeSessionId != current.activeSessionId;
}
// 修改中:当节点树/渲染集合/选中/修改目标/是否更新中变化时才重建
if (previous is SettingGenerationNodeUpdating && current is SettingGenerationNodeUpdating) {
return previous.activeSession.rootNodes != current.activeSession.rootNodes ||
previous.renderedNodeIds != current.renderedNodeIds ||
previous.selectedNodeId != current.selectedNodeId ||
previous.updatingNodeId != current.updatingNodeId ||
previous.isUpdating != current.isUpdating;
}
// 就绪:会话/活跃会话/视图模式变化
if (previous is SettingGenerationReady && current is SettingGenerationReady) {
return previous.sessions != current.sessions ||
previous.activeSessionId != current.activeSessionId ||
previous.viewMode != current.viewMode;
}
// 其他状态:保守起见重建
return true;
},
builder: (context, state) {
// 🔧 新增:详细的状态日志
AppLogger.i('SettingsTreeWidget', '🔄 状态变更: ${state.runtimeType}');
// 加载状态
if (state is SettingGenerationLoading) {
AppLogger.i('SettingsTreeWidget', '⏳ 显示加载状态');
return const AIShimmerPlaceholder();
}
// 生成进行中状态
if (state is SettingGenerationInProgress) {
AppLogger.i('SettingsTreeWidget', '🚀 显示生成进行中状态 - 已渲染节点: ${state.renderedNodeIds.length}');
return _buildInProgressView(context, state);
}
// 🔧 新增:节点修改中状态
if (state is SettingGenerationNodeUpdating) {
AppLogger.i('SettingsTreeWidget', '🔧 显示节点修改中状态 - 修改节点: ${state.updatingNodeId}');
return _buildNodeUpdatingView(context, state);
}
// 生成完成状态
if (state is SettingGenerationCompleted) {
AppLogger.i('SettingsTreeWidget', '✅ 显示完成状态 - 会话: ${state.activeSessionId}');
return _buildCompletedView(context, state);
}
// 保存成功状态 - 仍然显示完成视图,避免界面闪烁
if (state is SettingGenerationSaved) {
AppLogger.i('SettingsTreeWidget', '💾 显示保存成功状态,会话数: ${state.sessions.length}');
return _buildSavedView(context, state);
}
// 无会话状态
if (state is SettingGenerationReady) {
AppLogger.i('SettingsTreeWidget', '🎯 显示就绪状态,会话数: ${state.sessions.length}');
return _buildNoSessionView(context, state);
}
// 错误状态
if (state is SettingGenerationError) {
AppLogger.w('SettingsTreeWidget', '❌ 显示错误状态: ${state.message}');
return _buildErrorView(context, state);
}
// 默认状态(初始状态等)
AppLogger.w('SettingsTreeWidget', '🤔 未知状态: ${state.runtimeType}');
return _buildNoSessionView(context, state);
},
);
}
Widget _buildInProgressView(BuildContext context, SettingGenerationInProgress state) {
// 如果没有任何已渲染的节点(不管渲染状态如何),显示等待状态
if (state.renderedNodeIds.isEmpty) {
return const AIShimmerPlaceholder();
}
// 显示流式渲染界面(进度/提示统一由父级状态条显示,避免重复)
return Column(
children: [
Expanded(
child: _buildStreamingTreeView(context, state),
),
],
);
}
Widget _buildCompletedView(BuildContext context, SettingGenerationCompleted state) {
// 🔧 新增:详细的渲染日志
AppLogger.i('SettingsTreeWidget', '🎨 渲染完成状态视图 - 节点数: ${state.activeSession.rootNodes.length}, 会话ID: ${state.activeSessionId}');
// 🔧 修复:当没有节点数据时,显示空状态提示
if (state.activeSession.rootNodes.isEmpty) {
AppLogger.w('SettingsTreeWidget', '⚠️ 会话中没有设定节点数据,显示空状态提示');
return _buildEmptyStateView(context, '此历史记录暂无设定数据');
}
return _buildTreeView(
context,
state.activeSession.rootNodes,
state.selectedNodeId,
state.viewMode,
state.renderedNodeIds,
);
}
Widget _buildStreamingTreeView(BuildContext context, SettingGenerationInProgress state) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
color: isDark
? const Color(0xFF1F2937).withOpacity(0.3)
: const Color(0xFFF9FAFB).withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDark
? const Color(0xFF1F2937)
: const Color(0xFFE5E7EB),
width: 1,
),
),
child: state.renderedNodeIds.isEmpty
? _buildWaitingForFirstNode(context)
: _buildRenderableNodesListView(
context,
state.activeSession.rootNodes,
state.selectedNodeId,
state.viewMode,
state.renderedNodeIds,
state.nodeRenderStates,
),
);
}
Widget _buildWaitingForFirstNode(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
WebTheme.getPrimaryColor(context),
),
),
),
const SizedBox(height: 16),
Text(
'AI 正在构思第一个设定节点...',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// 构建可渲染的节点列表视图
Widget _buildRenderableNodesListView(
BuildContext context,
List<SettingNode> nodes,
String? selectedNodeId,
String viewMode,
Set<String> renderedNodeIds,
Map<String, NodeRenderInfo> nodeRenderStates,
) {
// 获取所有需要渲染的节点(扁平化列表)
final renderableNodes = _getRenderableNodesList(
nodes,
renderedNodeIds,
nodeRenderStates,
);
return ListView.builder(
padding: const EdgeInsets.all(4),
itemCount: renderableNodes.length,
itemBuilder: (context, index) {
final nodeInfo = renderableNodes[index];
final node = nodeInfo.node;
final level = nodeInfo.level;
return Padding(
padding: EdgeInsets.only(bottom: index < renderableNodes.length - 1 ? 4 : 0),
child: SettingNodeWidget(
node: node,
selectedNodeId: selectedNodeId,
viewMode: viewMode,
level: level,
renderedNodeIds: renderedNodeIds,
nodeRenderStates: nodeRenderStates,
renderChildren: false,
onTap: (nodeId) {
context.read<SettingGenerationBloc>().add(
SelectNodeEvent(nodeId),
);
},
),
);
},
);
}
/// 获取所有需要渲染的节点列表(扁平化,包含层级信息)
List<_NodeWithLevel> _getRenderableNodesList(
List<SettingNode> nodes,
Set<String> renderedNodeIds,
Map<String, NodeRenderInfo> nodeRenderStates,
{
int level = 0,
}) {
final List<_NodeWithLevel> result = [];
for (final node in nodes) {
// 只添加已经渲染的节点或正在渲染的节点
if (renderedNodeIds.contains(node.id) ||
nodeRenderStates[node.id]?.state == NodeRenderState.rendering) {
result.add(_NodeWithLevel(node: node, level: level));
// 递归添加子节点
if (node.children != null && node.children!.isNotEmpty) {
result.addAll(_getRenderableNodesList(
node.children!,
renderedNodeIds,
nodeRenderStates,
level: level + 1,
));
}
}
}
return result;
}
Widget _buildTreeView(
BuildContext context,
List<SettingNode> nodes,
String? selectedNodeId,
String viewMode,
Set<String> renderedNodeIds,
) {
// 🔧 新增:日志和空状态处理
AppLogger.i('SettingsTreeWidget', '🌳 构建设定树视图 - 节点数: ${nodes.length}, 选中节点: $selectedNodeId');
// 🔧 修复:当节点列表为空时,显示空状态提示
if (nodes.isEmpty) {
AppLogger.w('SettingsTreeWidget', '⚠️ 节点列表为空,显示空状态提示');
return _buildEmptyStateView(context, '暂无设定数据');
}
// 🔧 如果 renderedNodeIds 为空(通常发生在生成已完成的状态),
// 将所有可见节点都视为已渲染,避免由于 Opacity=0 导致的内容不可见。
Set<String> effectiveRenderedIds = renderedNodeIds;
if (effectiveRenderedIds.isEmpty) {
effectiveRenderedIds = _collectAllNodeIds(nodes).toSet();
AppLogger.i('SettingsTreeWidget', '🔧 renderedNodeIds 为空自动填充所有节点ID (${effectiveRenderedIds.length})');
}
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
color: isDark
? const Color(0xFF1F2937).withOpacity(0.3)
: const Color(0xFFF9FAFB).withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDark
? const Color(0xFF1F2937)
: const Color(0xFFE5E7EB),
width: 1,
),
),
child: ListView.builder(
padding: const EdgeInsets.all(4),
itemCount: nodes.length,
itemBuilder: (context, index) {
final node = nodes[index];
return Padding(
padding: EdgeInsets.only(bottom: index < nodes.length - 1 ? 4 : 0),
child: SettingNodeWidget(
node: node,
selectedNodeId: selectedNodeId,
viewMode: viewMode,
level: 0,
renderedNodeIds: effectiveRenderedIds,
nodeRenderStates: const {}, // 完成状态下不需要渲染状态
onTap: (nodeId) {
context.read<SettingGenerationBloc>().add(
SelectNodeEvent(nodeId),
);
},
),
);
},
),
);
}
/// 递归收集所有节点 ID
List<String> _collectAllNodeIds(List<SettingNode> nodes) {
final List<String> ids = [];
for (final node in nodes) {
ids.add(node.id);
if (node.children != null && node.children!.isNotEmpty) {
ids.addAll(_collectAllNodeIds(node.children!));
}
}
return ids;
}
Widget _buildErrorView(BuildContext context, SettingGenerationError state) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
color: isDark
? const Color(0xFF1F2937).withOpacity(0.3)
: const Color(0xFFF9FAFB).withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDark
? const Color(0xFF1F2937)
: const Color(0xFFE5E7EB),
width: 1,
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: Colors.red.withOpacity(0.7),
),
const SizedBox(height: 16),
Text(
'生成失败',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
_getFriendlyErrorMessage(state.message),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// 重试按钮
if (state.isRecoverable && _canRetry())
Row(
mainAxisSize: MainAxisSize.min,
children: [
OutlinedButton.icon(
onPressed: () => _retryGeneration(context),
icon: const Icon(Icons.refresh, size: 18),
label: const Text('重试生成'),
style: OutlinedButton.styleFrom(
foregroundColor: isDark
? const Color(0xFFF9FAFB)
: const Color(0xFF111827),
side: BorderSide(
color: isDark
? const Color(0xFF374151)
: const Color(0xFFD1D5DB),
),
),
),
const SizedBox(width: 12),
TextButton.icon(
onPressed: () => _resetAndReload(context),
icon: const Icon(Icons.settings_backup_restore, size: 18),
label: const Text('重新开始'),
style: TextButton.styleFrom(
foregroundColor: isDark
? const Color(0xFF9CA3AF)
: const Color(0xFF6B7280),
),
),
],
),
],
),
),
),
);
}
/// 将后端错误信息转换为用户友好的提示
String _getFriendlyErrorMessage(String originalMessage) {
// 检查常见的错误模式并返回友好提示
final message = originalMessage.toLowerCase();
if (message.contains('timeout') || message.contains('超时')) {
return 'AI生成响应时间过长请稍后重试';
}
if (message.contains('network') || message.contains('connection') ||
message.contains('网络') || message.contains('连接')) {
return '网络连接不稳定,请检查网络后重试';
}
if (message.contains('rate limit') || message.contains('too many') ||
message.contains('频率') || message.contains('限制')) {
return '请求过于频繁,请稍等片刻后重试';
}
if (message.contains('invalid') || message.contains('无效') ||
message.contains('bad request')) {
return '请求参数有误,请重新配置后重试';
}
if (message.contains('unauthorized') || message.contains('permission') ||
message.contains('未授权') || message.contains('权限')) {
return '授权已过期,请重新登录后重试';
}
if (message.contains('server error') || message.contains('internal') ||
message.contains('服务器') || message.contains('内部错误')) {
return '服务器暂时无法处理请求,请稍后重试';
}
if (message.contains('model') || message.contains('模型')) {
return 'AI模型暂时不可用请尝试切换其他模型';
}
if (message.contains('quota') || message.contains('balance') ||
message.contains('额度') || message.contains('余额')) {
return '账户余额不足或已达到使用限额';
}
// 如果无法识别具体错误类型,返回通用友好提示
return '生成过程中遇到问题,请重试或联系客服';
}
/// 检查是否可以重试
bool _canRetry() {
return lastInitialPrompt != null &&
lastStrategy != null &&
lastModelConfigId != null;
}
/// 重试生成
void _retryGeneration(BuildContext context) {
if (!_canRetry()) return;
// 重试时无法保证仍保留公共模型对象这里仅传基础参数若有需要可在Bloc中从上次session metadata取回
context.read<SettingGenerationBloc>().add(
StartGenerationEvent(
initialPrompt: lastInitialPrompt!,
promptTemplateId: lastStrategy!,
novelId: novelId,
modelConfigId: lastModelConfigId!,
userId: userId ?? 'current_user',
),
);
}
/// 重置并重新加载
void _resetAndReload(BuildContext context) {
context.read<SettingGenerationBloc>().add(const LoadStrategiesEvent());
}
/// 构建空状态提示视图
Widget _buildEmptyStateView(BuildContext context, String message) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.info_outline,
size: 48,
color: isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280),
),
const SizedBox(height: 16),
Text(
message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280),
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// 🔧 新增:构建节点修改中视图
Widget _buildNodeUpdatingView(BuildContext context, SettingGenerationNodeUpdating state) {
AppLogger.i('SettingsTreeWidget', '🔧 渲染节点修改中状态 - 修改节点: ${state.updatingNodeId}');
// 使用TopToast显示修改提示
if (state.isUpdating && state.message.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
TopToast.info(
context,
state.message,
);
});
}
// 显示设定树,突出显示正在修改的节点
return _buildTreeView(
context,
state.activeSession.rootNodes,
state.selectedNodeId,
state.viewMode,
state.renderedNodeIds,
);
}
/// 🔧 新增:构建保存成功视图
Widget _buildSavedView(BuildContext context, SettingGenerationSaved state) {
AppLogger.i('SettingsTreeWidget', '💾 渲染保存成功状态');
// 尝试从sessions中找到当前活跃会话以渲染
if (state.sessions.isNotEmpty && state.activeSessionId != null) {
final session = state.sessions.firstWhere(
(s) => s.sessionId == state.activeSessionId,
orElse: () => state.sessions.first,
);
return _buildTreeView(
context,
session.rootNodes,
null, // 保存操作后保持原选中节点逻辑,可根据需要扩展
'compact',
const {},
);
}
// 如果找不到会话,显示空状态
AppLogger.w('SettingsTreeWidget', '⚠️ 保存状态下找不到活跃会话,显示空状态');
return _buildEmptyStateView(context, '设定已保存,但无法显示内容');
}
/// 🔧 新增:构建无会话视图
Widget _buildNoSessionView(BuildContext context, dynamic state) {
// 检查是否有活跃会话
if (state is SettingGenerationReady) {
AppLogger.i('SettingsTreeWidget', '📋 渲染就绪状态 - 活跃会话: ${state.activeSessionId}');
// 如果有活跃会话,显示对应的设定树
if (state.activeSessionId != null && state.sessions.isNotEmpty) {
final session = state.sessions.firstWhere(
(s) => s.sessionId == state.activeSessionId,
orElse: () => state.sessions.first,
);
// 如果会话有内容,显示设定树
if (session.rootNodes.isNotEmpty) {
AppLogger.i('SettingsTreeWidget', '🌳 就绪状态下显示设定树 - 节点数: ${session.rootNodes.length}');
return _buildTreeView(
context,
session.rootNodes,
null, // SettingGenerationReady 没有 selectedNodeId
state.viewMode,
const {},
);
}
}
}
// 默认显示无会话提示
AppLogger.i('SettingsTreeWidget', '📝 显示无会话提示');
return _buildEmptyStateView(context, '请开始生成设定或选择已有历史记录');
}
}