From dd90f48c6f725f90d949d25b3c18e7a6cb7c565e Mon Sep 17 00:00:00 2001 From: qzl Date: Sat, 28 Feb 2026 13:43:22 +0800 Subject: [PATCH] feat(chat): integrate ChatBloc into HomeScreen --- .../features/home/ui/screens/home_screen.dart | 245 +++++++++++++----- 1 file changed, 173 insertions(+), 72 deletions(-) diff --git a/apps/lib/features/home/ui/screens/home_screen.dart b/apps/lib/features/home/ui/screens/home_screen.dart index 6a5d2cc..32fc1e5 100644 --- a/apps/lib/features/home/ui/screens/home_screen.dart +++ b/apps/lib/features/home/ui/screens/home_screen.dart @@ -1,7 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/theme/design_tokens.dart'; +import '../../../chat/data/models/chat_list_item.dart'; +import '../../../chat/presentation/bloc/chat_bloc.dart'; +import '../../../chat/ui/widgets/ui_schema_renderer.dart'; +import '../../../../shared/widgets/toast/toast.dart'; +import '../../../../shared/widgets/toast/toast_type.dart'; import 'home_sheet.dart'; class HomeScreen extends StatefulWidget { @@ -13,6 +19,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { final TextEditingController _messageController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); bool get _hasMessage => _messageController.text.trim().isNotEmpty; @@ -26,6 +33,7 @@ class _HomeScreenState extends State { void dispose() { _messageController.removeListener(_onMessageChanged); _messageController.dispose(); + _scrollController.dispose(); super.dispose(); } @@ -35,16 +43,28 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF8FAFC), - body: SafeArea( - child: Column( - children: [ - _buildHeader(context), - Expanded(child: _buildChatArea()), - _buildInputContainer(context), - ], - ), + return BlocProvider( + create: (context) => ChatBloc(), + child: BlocConsumer( + listener: (context, state) { + if (state.error != null) { + Toast.show(context, state.error!, type: ToastType.error); + } + }, + builder: (context, state) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + body: SafeArea( + child: Column( + children: [ + _buildHeader(context), + Expanded(child: _buildChatArea(context, state)), + _buildInputContainer(context), + ], + ), + ), + ); + }, ), ); } @@ -92,91 +112,159 @@ class _HomeScreenState extends State { ); } - Widget _buildChatArea() { - return Padding( + Widget _buildChatArea(BuildContext context, ChatState state) { + if (state.items.isEmpty) { + return const Center( + child: Text( + '开始对话吧', + style: TextStyle(fontSize: 16, color: AppColors.slate400), + ), + ); + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + + return ListView.builder( + controller: _scrollController, padding: const EdgeInsets.all(20), - child: Column( - children: [ - _buildUserMessageRow(), - const SizedBox(height: 16), - _buildTodoCard(), - ], - ), + itemCount: state.items.length, + itemBuilder: (context, index) { + final item = state.items[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _buildChatItem(item), + ); + }, ); } - Widget _buildUserMessageRow() { + Widget _buildChatItem(ChatListItem item) { + switch (item.type) { + case ChatItemType.message: + return _buildMessageItem(item as TextMessageItem); + case ChatItemType.toolCall: + return _buildToolCallItem(item as ToolCallItem); + case ChatItemType.toolResult: + return _buildToolResultItem(item as ToolResultItem); + } + } + + Widget _buildMessageItem(TextMessageItem item) { + final isUser = item.sender == MessageSender.user; return Row( + mainAxisAlignment: isUser + ? MainAxisAlignment.end + : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Expanded(child: SizedBox()), - Container( - padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 9), - decoration: BoxDecoration( - color: const Color(0xFFEAF1FB), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - bottomLeft: Radius.circular(12), - bottomRight: Radius.circular(0), + if (!isUser) ...[ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: AppColors.blue100, + shape: BoxShape.circle, + ), + child: const Icon( + LucideIcons.bot, + size: 18, + color: AppColors.blue600, ), ), - child: const Text( - '明天提醒我开会', - style: TextStyle(fontSize: 14, color: AppColors.slate900), + const SizedBox(width: 8), + ], + Flexible( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 9), + decoration: BoxDecoration( + color: isUser ? const Color(0xFFEAF1FB) : AppColors.white, + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(12), + topRight: const Radius.circular(12), + bottomLeft: Radius.circular(isUser ? 12 : 0), + bottomRight: Radius.circular(isUser ? 0 : 12), + ), + border: isUser ? null : Border.all(color: AppColors.slate300), + ), + child: Text( + item.content, + style: const TextStyle(fontSize: 14, color: AppColors.slate900), + ), ), ), + if (isUser) const SizedBox(width: 40), + if (!isUser) const SizedBox(width: 40), ], ); } - Widget _buildTodoCard() { + Widget _buildToolCallItem(ToolCallItem item) { + String statusText; + Color statusColor; + IconData statusIcon; + + switch (item.status) { + case ToolCallStatus.pending: + statusText = '准备中...'; + statusColor = AppColors.slate500; + statusIcon = LucideIcons.clock; + break; + case ToolCallStatus.executing: + statusText = '执行中...'; + statusColor = AppColors.blue600; + statusIcon = LucideIcons.loader; + break; + case ToolCallStatus.error: + statusText = item.errorMessage ?? '执行失败'; + statusColor = AppColors.red600; + statusIcon = LucideIcons.alertCircle; + break; + case ToolCallStatus.completed: + statusText = '已完成'; + statusColor = AppColors.emerald600; + statusIcon = LucideIcons.checkCircle; + break; + } + return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.circular(16), + color: AppColors.blue50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColors.slate300), ), child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 4, - height: 60, - decoration: const BoxDecoration( - color: AppColors.blue500, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(4), - bottomLeft: Radius.circular(4), - ), - ), - ), - const SizedBox(width: 12), - const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '明天 10:00', - style: TextStyle(fontSize: 12, color: AppColors.slate500), - ), - SizedBox(height: 4), - Text( - '开会', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: AppColors.slate900, - ), - ), - ], + Icon(statusIcon, size: 16, color: statusColor), + const SizedBox(width: 8), + Text( + item.toolName, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.slate700, ), ), + const SizedBox(width: 8), + Text(statusText, style: TextStyle(fontSize: 12, color: statusColor)), ], ), ); } + Widget _buildToolResultItem(ToolResultItem item) { + return UiSchemaRenderer.render(item.uiCard); + } + Widget _buildInputContainer(BuildContext context) { return Container( padding: const EdgeInsets.all(16), @@ -231,13 +319,19 @@ class _HomeScreenState extends State { contentPadding: EdgeInsets.zero, filled: false, ), + onSubmitted: (_) => _sendMessage(context), ), ), const SizedBox(width: 8), - Icon( - _hasMessage ? LucideIcons.send : LucideIcons.mic, - size: 24, - color: _hasMessage ? AppColors.blue600 : AppColors.slate500, + GestureDetector( + onTap: _hasMessage ? () => _sendMessage(context) : null, + child: Icon( + _hasMessage ? LucideIcons.send : LucideIcons.mic, + size: 24, + color: _hasMessage + ? AppColors.blue600 + : AppColors.slate500, + ), ), ], ), @@ -248,6 +342,13 @@ class _HomeScreenState extends State { ); } + Future _sendMessage(BuildContext context) async { + final content = _messageController.text.trim(); + if (content.isEmpty) return; + _messageController.clear(); + context.read().sendMessage(content); + } + void _showBottomSheet(BuildContext context) { showModalBottomSheet( context: context,