import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../blocs/chat/chat_bloc.dart'; import '../../blocs/chat/chat_event.dart'; import '../../blocs/chat/chat_state.dart'; import '../../models/chat_models.dart'; import '../../models/user_ai_model_config_model.dart'; import '../../utils/logger.dart'; import '../../widgets/common/top_toast.dart'; import 'widgets/chat_input.dart'; import 'widgets/chat_message_bubble.dart'; import 'widgets/context_panel.dart'; import 'widgets/typing_indicator.dart'; class ChatScreen extends StatefulWidget { const ChatScreen({ Key? key, required this.novelId, this.chapterId, }) : super(key: key); final String novelId; final String? chapterId; @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); bool _isContextPanelExpanded = false; @override void initState() { super.initState(); // 加载聊天会话列表 context.read().add(LoadChatSessions(novelId: widget.novelId)); } @override void dispose() { _messageController.dispose(); _scrollController.dispose(); super.dispose(); } // 滚动到底部 void _scrollToBottom() { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } } // 发送消息 void _sendMessage() { final message = _messageController.text.trim(); if (message.isNotEmpty) { context.read().add(SendMessage(content: message)); _messageController.clear(); // 延迟滚动到底部,等待消息添加到列表 Future.delayed(const Duration(milliseconds: 100), _scrollToBottom); } } // 切换上下文面板 void _toggleContextPanel() { setState(() { _isContextPanelExpanded = !_isContextPanelExpanded; }); } // 创建新会话 void _createNewSession() { final TextEditingController titleController = TextEditingController(); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('创建新会话'), content: TextField( controller: titleController, autofocus: true, decoration: const InputDecoration( hintText: '输入会话标题', ), onSubmitted: (value) { if (value.isNotEmpty) { context.read().add(CreateChatSession( title: value, novelId: widget.novelId, chapterId: widget.chapterId, )); Navigator.pop(context); } }, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), TextButton( onPressed: () { final title = titleController.text.trim(); if (title.isNotEmpty) { context.read().add(CreateChatSession( title: title, novelId: widget.novelId, chapterId: widget.chapterId, )); Navigator.pop(context); } }, child: const Text('创建'), ), ], ), ); } // 选择会话 void _selectSession(String sessionId) { context.read().add(SelectChatSession(sessionId: sessionId, novelId: widget.novelId)); } // 执行操作 void _executeAction(MessageAction action) { context.read().add(ExecuteAction(action: action)); // 显示操作执行提示 TopToast.info(context, '执行操作: ${action.label}'); } /// 🚀 检查消息列表中是否有正在流式传输的消息 bool _hasStreamingMessage(List messages) { return messages.any((message) => message.status == 'streaming' || message.status?.toString() == 'MessageStatus.streaming'); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( // 使用 surfaceContainerLow 作为基础背景色 backgroundColor: colorScheme.surfaceContainerLow, appBar: AppBar( // AppBar 背景色 backgroundColor: colorScheme.surfaceContainer, // 移除默认阴影,让边框控制分割 elevation: 0, // 底部边框 shape: Border( bottom: BorderSide( color: colorScheme.outlineVariant.withOpacity(0.5), width: 1.0)), title: BlocBuilder( builder: (context, state) { String titleText = 'AI 聊天助手'; // 默认标题 if (state is ChatSessionActive) { titleText = state.session.title; // 活动会话标题 } else if (state is ChatSessionsLoaded) { // 可以考虑在列表视图显示不同的标题 titleText = '聊天会话'; } return Text( titleText, style: TextStyle( // 统一标题样式 color: colorScheme.onSurface, fontWeight: FontWeight.w600, fontSize: 18, ), ); }, ), centerTitle: false, // 标题居左 // AppBar 操作按钮颜色 iconTheme: IconThemeData(color: colorScheme.onSurfaceVariant), actionsIconTheme: IconThemeData(color: colorScheme.onSurfaceVariant), actions: [ // 新建会话按钮 IconButton( icon: const Icon(Icons.add_comment_outlined), // 换图标 tooltip: '新建会话', onPressed: _createNewSession, ), // 上下文面板切换按钮 IconButton( // 根据状态改变图标,增加视觉反馈 icon: Icon(_isContextPanelExpanded ? Icons.info_rounded : Icons.info_outline_rounded), tooltip: _isContextPanelExpanded ? '关闭上下文' : '打开上下文', // 可以根据状态改变颜色 color: _isContextPanelExpanded ? colorScheme.primary : colorScheme.onSurfaceVariant, onPressed: _toggleContextPanel, ), // 会话列表按钮 (如果希望保留在 AppBar 中) IconButton( icon: const Icon(Icons.menu_open_rounded), // 换图标 tooltip: '会话列表', onPressed: _showSessionsDialog, ), /* PopupMenuButton( // 或者继续用 PopupMenu icon: const Icon(Icons.more_vert_rounded), onSelected: (value) { if (value == 'sessions') { _showSessionsDialog(); } // TODO: 添加其他菜单项,如删除会话、重命名等 }, itemBuilder: (context) => [ const PopupMenuItem( value: 'sessions', child: ListTile(leading: Icon(Icons.list_alt_rounded), title: Text('会话列表')), ), // Add other options here... ], ), */ const SizedBox(width: 8), // 右边距 ], ), // 使用 SafeArea 避免内容与系统 UI 重叠 body: SafeArea( child: BlocConsumer( listener: (context, state) { // --- SnackBar 错误提示 (样式不变) --- if (state is ChatSessionsLoaded && state.error != null) { TopToast.error(context, state.error!); } if (state is ChatSessionActive && state.error != null) { TopToast.error(context, state.error!); } // --- 滚动逻辑 --- if (state is ChatSessionActive && !state.isLoadingHistory) { // 当新消息添加或流式更新时,滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom(); }); } }, // --- buildWhen 优化检查 --- buildWhen: (previous, current) { // 允许从加载状态转换 if ((previous is ChatSessionsLoading || previous is ChatSessionLoading) && (current is ChatSessionsLoaded || current is ChatSessionActive)) { return true; } // 允许错误和初始状态 if (current is ChatError || current is ChatInitial) return true; // 在 ChatSessionActive 内更新的条件 if (previous is ChatSessionActive && current is ChatSessionActive) { return previous.session.id != current.session.id || // 会话切换 previous.messages != current.messages || // 消息变化 (浅比较) previous.isGenerating != current.isGenerating || previous.isLoadingHistory != current.isLoadingHistory || previous.error != current.error || previous.selectedModel?.id != current.selectedModel?.id; // 模型变化 } // 在 ChatSessionsLoaded 内更新的条件 if (previous is ChatSessionsLoaded && current is ChatSessionsLoaded) { return previous.sessions != current.sessions || // 列表变化 previous.error != current.error; } // 从活动会话返回列表 if (previous is ChatSessionActive && current is ChatSessionsLoaded) { return true; } // 从列表进入活动会话 if (previous is ChatSessionsLoaded && current is ChatSessionActive) { return true; } // 其他情况,如果类型不同则重建 return previous.runtimeType != current.runtimeType; }, builder: (context, state) { AppLogger.d('ChatScreen builder', 'Building UI for state: ${state.runtimeType}'); // --- 加载状态 --- if (state is ChatSessionsLoading || state is ChatSessionLoading) { return const Center(child: CircularProgressIndicator()); } // --- 列表或活动会话 --- // 修改:不再直接显示列表,主界面始终是聊天视图 // 会话列表通过 AppBar 按钮或侧边栏显示 else if (state is ChatSessionActive || state is ChatSessionsLoaded || state is ChatInitial) { // 如果当前是列表状态且有会话,可以自动选择第一个或上次的会话 // 这里简化处理:如果 state 不是 ChatSessionActive,则显示提示或空状态 if (state is ChatSessionActive) { return _buildChatView(state); } else { // 显示初始/空状态视图,提示用户选择或创建会话 return _buildInitialEmptyState(); } } // (旧的 _buildSessionsList 调用被移除或移到对话框/侧边栏) // else if (state is ChatSessionsLoaded) { ... } // --- 错误状态 --- else if (state is ChatError) { return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( // 改进错误显示 mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline_rounded, color: colorScheme.error, size: 48), const SizedBox(height: 16), Text('出现错误', style: Theme.of(context) .textTheme .titleLarge ?.copyWith(color: colorScheme.error)), const SizedBox(height: 8), Text(state.message, textAlign: TextAlign.center, style: TextStyle(color: colorScheme.onErrorContainer)), const SizedBox(height: 16), // 可以添加重试按钮 /* ElevatedButton.icon( onPressed: () { // 根据错误类型决定重试哪个操作 if (state.message.contains("加载会话列表失败")) { context.read().add(LoadChatSessions(novelId: widget.novelId)); } else if (state.message.contains("加载消息失败")){ // 需要知道当前会话 ID 来重试加载消息 } }, icon: Icon(Icons.refresh_rounded), label: Text("重试"), style: ElevatedButton.styleFrom(foregroundColor: colorScheme.onError, backgroundColor: colorScheme.error), )*/ ], ), ), ); } // --- 其他未处理状态 --- else { // 可以返回一个更通用的空状态或加载指示器 return _buildInitialEmptyState(); } }, ), ), ); } // 构建初始空状态视图 Widget _buildInitialEmptyState() { final colorScheme = Theme.of(context).colorScheme; return Center( child: Padding( padding: const EdgeInsets.all(32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.forum_outlined, size: 64, color: colorScheme.secondary), // 使用不同图标 const SizedBox(height: 24), Text( '选择或创建会话', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), textAlign: TextAlign.center, ), const SizedBox(height: 12), Text( '开始与 AI 助手聊天吧!', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 24), Row( // 并排显示按钮 mainAxisAlignment: MainAxisAlignment.center, children: [ OutlinedButton.icon( // 打开列表按钮 onPressed: _showSessionsDialog, icon: const Icon(Icons.list_alt_rounded), label: const Text('选择已有对话'), style: OutlinedButton.styleFrom( foregroundColor: colorScheme.primary, side: BorderSide( color: colorScheme.outline.withOpacity(0.8)), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10), ), ), const SizedBox(width: 12), ElevatedButton.icon( // 创建新会话按钮 onPressed: _createNewSession, icon: const Icon(Icons.add_comment_outlined), label: const Text('创建新对话'), style: ElevatedButton.styleFrom( foregroundColor: colorScheme.onPrimary, backgroundColor: colorScheme.primary, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10), ), ), ]) ], ), ), ); } // 构建会话列表 - 从主 builder 移出,现在只用于对话框或侧边栏 // (这里保留,适配对话框使用) Widget _buildSessionsListForDialog(ChatSessionsLoaded state) { final sessions = state.sessions; final colorScheme = Theme.of(context).colorScheme; return SizedBox( width: double.maxFinite, // 根据内容调整高度,限制最大高度 // height: sessions.isEmpty ? 150 : (sessions.length * 60.0 + (state.error != null ? 40 : 0)).clamp(150.0, 400.0), child: Column( mainAxisSize: MainAxisSize.min, // 高度自适应内容 children: [ // 显示错误 if (state.error != null) Padding( padding: const EdgeInsets.only(bottom: 8.0, left: 16, right: 16), child: Text(state.error!, style: TextStyle(color: colorScheme.error)), ), // 列表或空状态 Flexible( // 使用 Flexible 允许列表在 Column 内滚动 child: sessions.isEmpty ? const Center( child: Padding( // 改进空列表提示 padding: EdgeInsets.symmetric(vertical: 32.0), child: Text('没有找到任何对话记录'), )) : ListView.builder( shrinkWrap: true, // 在 Column 中需要 itemCount: sessions.length, itemBuilder: (context, index) { final session = sessions[index]; // 获取当前活动会话 ID String? activeSessionId; final currentState = context.read().state; if (currentState is ChatSessionActive) { activeSessionId = currentState.session.id; } final bool isSelected = session.id == activeSessionId; return ListTile( leading: Icon( // 图标指示 isSelected ? Icons.chat_bubble_rounded : Icons.chat_bubble_outline_rounded, color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, ), title: Text( session.title, style: TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal), ), subtitle: Text( '更新于: ${DateFormat('yyyy-MM-dd HH:mm').format(session.lastUpdatedAt)}', style: Theme.of(context) .textTheme .bodySmall ?.copyWith( color: colorScheme.onSurfaceVariant .withOpacity(0.8)), ), selected: isSelected, selectedTileColor: colorScheme.primaryContainer.withOpacity(0.1), onTap: () { _selectSession(session.id); Navigator.pop(context); // Close dialog }, // 可以添加删除按钮 /* trailing: IconButton( icon: Icon(Icons.delete_outline, size: 20, color: Theme.of(context).colorScheme.onSurfaceVariant), onPressed: () { // TODO: 确认删除逻辑 // context.read().add(DeleteChatSession(sessionId: session.id)); }, tooltip: '删除会话', ), */ ); }, ), ), ], ), ); } // 构建聊天视图 (样式调整) Widget _buildChatView(ChatSessionActive state) { final UserAIModelConfigModel? currentChatModel = state.selectedModel; final colorScheme = Theme.of(context).colorScheme; return Row( children: [ // 聊天主界面 Expanded( // 根据上下文面板状态调整 flex 比例 flex: _isContextPanelExpanded ? 3 : 5, // 主聊天区域占比更大 // 使用 Container 设置背景色 child: Container( color: colorScheme.surface, // 主聊天区域背景色 child: Column( children: [ // 历史加载指示器(保持不变) if (state.isLoadingHistory) const Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))), ), // 可以考虑在此处显示持久的错误信息(如果不用 SnackBar) /* if (state.error != null && !state.isLoadingHistory) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: colorScheme.errorContainer, child: Row(children: [ Icon(Icons.error_outline, color: colorScheme.onErrorContainer, size: 16), SizedBox(width: 8), Expanded(child: Text(state.error!, style: TextStyle(color: colorScheme.onErrorContainer))), ]), ), */ // 消息列表 Expanded( child: ListView.builder( controller: _scrollController, // 增加上下内边距,左右在 Bubble 中处理 padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16.0), itemCount: state.messages.length + (state.isGenerating && !state.isLoadingHistory && !_hasStreamingMessage(state.messages) ? 1 : 0), itemBuilder: (context, index) { // 🚀 只有在没有流式消息且正在生成时才显示TypingIndicator if (state.isGenerating && !state.isLoadingHistory && !_hasStreamingMessage(state.messages) && index == state.messages.length) { return const TypingIndicator(); } final message = state.messages[index]; // 🚀 所有消息都使用ChatMessageBubble,包括streaming状态的消息 return ChatMessageBubble( message: message, onActionSelected: _executeAction, // 动作回调 ); }, ), ), // 输入区域 (ChatInput 已在上面修改) ChatInput( controller: _messageController, onSend: _sendMessage, isGenerating: state.isGenerating, onCancel: () { context.read().add(const CancelOngoingRequest()); }, initialModel: currentChatModel, onModelSelected: (selectedModel) { if (selectedModel != null && selectedModel.id != currentChatModel?.id) { context.read().add(UpdateChatModel( sessionId: state.session.id, modelConfigId: selectedModel.id, )); AppLogger.i('ChatScreen', 'Model selected event dispatched: ${selectedModel.id} for session ${state.session.id}'); } }, ), ], ), ), ), // 上下文面板 (ContextPanel 已在上面修改) if (_isContextPanelExpanded) Expanded( flex: 2, // 上下文面板 flex 比例 child: ContextPanel( context: state.context, onClose: _toggleContextPanel, ), ), ], ); } // 显示会话列表对话框 (样式调整) void _showSessionsDialog() { final colorScheme = Theme.of(context).colorScheme; showDialog( context: context, builder: (context) => AlertDialog( // 对话框样式 shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), backgroundColor: colorScheme.surfaceContainerHigh, // 背景色 titlePadding: const EdgeInsets.only(top: 20, left: 24, right: 24, bottom: 10), contentPadding: const EdgeInsets.only(bottom: 8), // 调整内容边距 actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), title: Text('选择对话', style: TextStyle( fontWeight: FontWeight.bold, color: colorScheme.onSurface)), content: BlocBuilder( // 监听会话列表相关状态 buildWhen: (prev, curr) => curr is ChatSessionsLoaded || curr is ChatSessionsLoading || curr is ChatSessionActive, builder: (context, state) { // 尝试从 Bloc 获取当前的会话列表状态 ChatSessionsLoaded? listState; if (state is ChatSessionsLoaded) { listState = state; } else if (state is ChatSessionActive) { // 如果当前是活动会话,也需要显示列表,需要能从ChatBloc获取到完整列表 // 这要求 ChatBloc 在 ChatSessionActive 状态下仍然持有 sessions 列表 // 或者在这里触发一次 LoadChatSessions (但不推荐,可能导致状态混乱) // 更好的方式是修改 Bloc,使其在 Active 状态下也能提供列表 // 暂时假设可以获取到 (如果不行,对话框内容需要调整) // listState = context.read().getAllSessionsState(); // 假设有这个方法 } if (listState != null) { // 使用更新后的列表构建方法 return _buildSessionsListForDialog(listState); } else if (state is ChatSessionsLoading) { // 处理加载状态 return const SizedBox( height: 150, // 固定高度 child: Center(child: CircularProgressIndicator()), ); } else { // 处理其他未能获取列表的状态 return const SizedBox( height: 100, child: Center(child: Text('无法加载会话列表'))); } }, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), style: TextButton.styleFrom( foregroundColor: colorScheme.onSurfaceVariant), child: const Text('关闭'), ), TextButton( onPressed: () { Navigator.pop(context); // 先关闭对话框 _createNewSession(); // 再打开创建对话框 }, style: TextButton.styleFrom( foregroundColor: colorScheme.primary, textStyle: const TextStyle(fontWeight: FontWeight.bold)), child: const Text('新建对话'), ), ], ), ); } }