马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View File

@@ -0,0 +1,710 @@
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<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
bool _isContextPanelExpanded = false;
@override
void initState() {
super.initState();
// 加载聊天会话列表
context.read<ChatBloc>().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<ChatBloc>().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<ChatBloc>().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<ChatBloc>().add(CreateChatSession(
title: title,
novelId: widget.novelId,
chapterId: widget.chapterId,
));
Navigator.pop(context);
}
},
child: const Text('创建'),
),
],
),
);
}
// 选择会话
void _selectSession(String sessionId) {
context.read<ChatBloc>().add(SelectChatSession(sessionId: sessionId, novelId: widget.novelId));
}
// 执行操作
void _executeAction(MessageAction action) {
context.read<ChatBloc>().add(ExecuteAction(action: action));
// 显示操作执行提示
TopToast.info(context, '执行操作: ${action.label}');
}
/// 🚀 检查消息列表中是否有正在流式传输的消息
bool _hasStreamingMessage(List<dynamic> 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<ChatBloc, ChatState>(
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<String>( // 或者继续用 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<ChatBloc, ChatState>(
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<ChatBloc>().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<ChatBloc>().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<ChatBloc>().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<ChatBloc>().add(const CancelOngoingRequest());
},
initialModel: currentChatModel,
onModelSelected: (selectedModel) {
if (selectedModel != null &&
selectedModel.id != currentChatModel?.id) {
context.read<ChatBloc>().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<ChatBloc, ChatState>(
// 监听会话列表相关状态
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<ChatBloc>().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('新建对话'),
),
],
),
);
}
}