chore: simplify chat user message bloc and widgets (#6836)

This commit is contained in:
Richard Shiue 2024-11-20 13:13:38 +03:00 committed by GitHub
parent f82dabcc75
commit e86a9d697c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 154 additions and 204 deletions

View File

@ -8,72 +8,19 @@ part 'chat_user_message_bloc.freezed.dart';
class ChatUserMessageBloc class ChatUserMessageBloc
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> { extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
ChatUserMessageBloc({ ChatUserMessageBloc({
required dynamic message, required this.questionStream,
}) : super(ChatUserMessageState.initial(message)) { required String text,
}) : super(ChatUserMessageState.initial(text)) {
_dispatch();
_startListening();
}
final QuestionStream? questionStream;
void _dispatch() {
on<ChatUserMessageEvent>( on<ChatUserMessageEvent>(
(event, emit) { (event, emit) {
event.when( event.when(
initial: () {
if (state.stream != null) {
if (!isClosed) {
add(ChatUserMessageEvent.updateText(state.stream!.text));
}
}
state.stream?.listen(
onData: (text) {
if (!isClosed) {
add(ChatUserMessageEvent.updateText(text));
}
},
onMessageId: (messageId) {
if (!isClosed) {
add(ChatUserMessageEvent.updateMessageId(messageId));
}
},
onError: (error) {
if (!isClosed) {
add(ChatUserMessageEvent.receiveError(error.toString()));
}
},
onFileIndexStart: (indexName) {
Log.debug("index start: $indexName");
},
onFileIndexEnd: (indexName) {
Log.info("index end: $indexName");
},
onFileIndexFail: (indexName) {
Log.debug("index fail: $indexName");
},
onIndexStart: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.indexStart(),
),
);
}
},
onIndexEnd: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.indexEnd(),
),
);
}
},
onDone: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.finish(),
),
);
}
},
);
},
updateText: (String text) { updateText: (String text) {
emit(state.copyWith(text: text)); emit(state.copyWith(text: text));
}, },
@ -88,11 +35,66 @@ class ChatUserMessageBloc
}, },
); );
} }
void _startListening() {
questionStream?.listen(
onData: (text) {
if (!isClosed) {
add(ChatUserMessageEvent.updateText(text));
}
},
onMessageId: (messageId) {
if (!isClosed) {
add(ChatUserMessageEvent.updateMessageId(messageId));
}
},
onError: (error) {
if (!isClosed) {
add(ChatUserMessageEvent.receiveError(error.toString()));
}
},
onFileIndexStart: (indexName) {
Log.debug("index start: $indexName");
},
onFileIndexEnd: (indexName) {
Log.info("index end: $indexName");
},
onFileIndexFail: (indexName) {
Log.debug("index fail: $indexName");
},
onIndexStart: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.indexStart(),
),
);
}
},
onIndexEnd: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.indexEnd(),
),
);
}
},
onDone: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.finish(),
),
);
}
},
);
}
} }
@freezed @freezed
class ChatUserMessageEvent with _$ChatUserMessageEvent { class ChatUserMessageEvent with _$ChatUserMessageEvent {
const factory ChatUserMessageEvent.initial() = Initial;
const factory ChatUserMessageEvent.updateText(String text) = _UpdateText; const factory ChatUserMessageEvent.updateText(String text) = _UpdateText;
const factory ChatUserMessageEvent.updateQuestionState( const factory ChatUserMessageEvent.updateQuestionState(
QuestionMessageState newState, QuestionMessageState newState,
@ -106,17 +108,14 @@ class ChatUserMessageEvent with _$ChatUserMessageEvent {
class ChatUserMessageState with _$ChatUserMessageState { class ChatUserMessageState with _$ChatUserMessageState {
const factory ChatUserMessageState({ const factory ChatUserMessageState({
required String text, required String text,
QuestionStream? stream, required String? messageId,
String? messageId, required QuestionMessageState messageState,
@Default(QuestionMessageState.finish()) QuestionMessageState messageState,
}) = _ChatUserMessageState; }) = _ChatUserMessageState;
factory ChatUserMessageState.initial( factory ChatUserMessageState.initial(String message) => ChatUserMessageState(
dynamic message, text: message,
) => messageId: null,
ChatUserMessageState( messageState: const QuestionMessageState.finish(),
text: message is String ? message : "",
stream: message is QuestionStream ? message : null,
); );
} }

View File

@ -1,60 +0,0 @@
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'chat_message_service.dart';
part 'chat_user_message_bubble_bloc.freezed.dart';
class ChatUserMessageBubbleBloc
extends Bloc<ChatUserMessageBubbleEvent, ChatUserMessageBubbleState> {
ChatUserMessageBubbleBloc({
required Message message,
}) : super(
ChatUserMessageBubbleState.initial(
message,
_getFiles(message.metadata),
),
) {
on<ChatUserMessageBubbleEvent>(
(event, emit) async {
event.when(
initial: () {},
);
},
);
}
}
List<ChatFile> _getFiles(Map<String, dynamic>? metadata) {
if (metadata == null) {
return [];
}
final refSourceMetadata = metadata[messageRefSourceJsonStringKey] as String?;
final files = metadata[messageChatFileListKey] as List<ChatFile>?;
if (refSourceMetadata != null) {
return chatFilesFromMetadataString(refSourceMetadata);
}
return files ?? [];
}
@freezed
class ChatUserMessageBubbleEvent with _$ChatUserMessageBubbleEvent {
const factory ChatUserMessageBubbleEvent.initial() = Initial;
}
@freezed
class ChatUserMessageBubbleState with _$ChatUserMessageBubbleState {
const factory ChatUserMessageBubbleState({
required Message message,
required List<ChatFile> files,
}) = _ChatUserMessageBubbleState;
factory ChatUserMessageBubbleState.initial(
Message message,
List<ChatFile> files,
) =>
ChatUserMessageBubbleState(message: message, files: files);
}

View File

@ -4,7 +4,6 @@ import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/user_message_bubble.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
@ -233,27 +232,18 @@ class _ChatContentPage extends StatelessWidget {
} }
if (message.author.id == userProfile.id.toString()) { if (message.author.id == userProfile.id.toString()) {
final stream = message.metadata?["$QuestionStream"]; return ChatUserMessageWidget(
return ChatUserMessageBubble( user: message.author,
key: ValueKey(message.id),
message: message, message: message,
child: ChatUserMessageWidget( isCurrentUser: true,
user: message.author,
message: stream is QuestionStream ? stream : message.text,
),
); );
} }
if (isOtherUserMessage(message)) { if (isOtherUserMessage(message)) {
final stream = message.metadata?["$QuestionStream"]; return ChatUserMessageWidget(
return ChatUserMessageBubble( user: message.author,
key: ValueKey(message.id),
message: message, message: message,
isCurrentUser: false, isCurrentUser: false,
child: ChatUserMessageWidget(
user: message.author,
message: stream is QuestionStream ? stream : message.text,
),
); );
} }
@ -263,7 +253,6 @@ class _ChatContentPage extends StatelessWidget {
message.metadata?[messageRefSourceJsonStringKey] as String?; message.metadata?[messageRefSourceJsonStringKey] as String?;
return BlocSelector<ChatBloc, ChatState, bool>( return BlocSelector<ChatBloc, ChatState, bool>(
key: ValueKey(message.id),
selector: (state) { selector: (state) {
final chatController = context.read<ChatBloc>().chatController; final chatController = context.read<ChatBloc>().chatController;
final messages = chatController.messages.where((e) { final messages = chatController.messages.where((e) {

View File

@ -4,7 +4,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
@ -17,13 +16,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:universal_platform/universal_platform.dart'; import 'package:universal_platform/universal_platform.dart';
import '../chat_avatar.dart';
import '../layout_define.dart'; import '../layout_define.dart';
/// Wraps an AI response message with the avatar and actions. On desktop, /// Wraps an AI response message with the avatar and actions. On desktop,
/// the actions will be displayed below the response if the response is the /// the actions will be displayed below the response if the response is the
/// last message in the chat. For other AI responses, the actions will be shown /// last message in the chat. For the others, the actions will be shown on hover
/// on hover. On mobile, the actions will be displayed in a bottom sheet on /// On mobile, the actions will be displayed in a bottom sheet on long press.
/// long press.
class ChatAIMessageBubble extends StatelessWidget { class ChatAIMessageBubble extends StatelessWidget {
const ChatAIMessageBubble({ const ChatAIMessageBubble({
super.key, super.key,

View File

@ -1,9 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bubble_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -11,57 +8,50 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:universal_platform/universal_platform.dart'; import 'package:universal_platform/universal_platform.dart';
import '../chat_avatar.dart';
import '../layout_define.dart';
class ChatUserMessageBubble extends StatelessWidget { class ChatUserMessageBubble extends StatelessWidget {
const ChatUserMessageBubble({ const ChatUserMessageBubble({
super.key, super.key,
required this.message, required this.message,
required this.child, required this.child,
this.isCurrentUser = true, required this.isCurrentUser,
this.files = const [],
}); });
final Message message; final Message message;
final Widget child; final Widget child;
final bool isCurrentUser; final bool isCurrentUser;
final List<ChatFile> files;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (context.read<ChatMemberBloc>().state.members[message.author.id] == context
null) { .read<ChatMemberBloc>()
context .add(ChatMemberEvent.getMemberInfo(message.author.id));
.read<ChatMemberBloc>()
.add(ChatMemberEvent.getMemberInfo(message.author.id));
}
return BlocProvider( return Padding(
create: (context) => ChatUserMessageBubbleBloc( padding: UniversalPlatform.isMobile
message: message, ? const EdgeInsets.symmetric(horizontal: 16)
), : EdgeInsets.zero,
child: BlocBuilder<ChatUserMessageBubbleBloc, ChatUserMessageBubbleState>( child: Column(
builder: (context, state) { mainAxisSize: MainAxisSize.min,
return Padding( crossAxisAlignment: CrossAxisAlignment.end,
padding: UniversalPlatform.isMobile children: [
? const EdgeInsets.symmetric(horizontal: 16) if (files.isNotEmpty) ...[
: EdgeInsets.zero, Padding(
child: Column( padding: const EdgeInsets.only(right: 32),
mainAxisSize: MainAxisSize.min, child: _MessageFileList(files: files),
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (state.files.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(right: 32),
child: _MessageFileList(files: state.files),
),
const VSpace(6),
],
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: getChildren(context),
),
],
), ),
); const VSpace(6),
}, ],
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: getChildren(context),
),
],
), ),
); );
} }

View File

@ -1,3 +1,6 @@
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -5,33 +8,63 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'user_message_bubble.dart';
class ChatUserMessageWidget extends StatelessWidget { class ChatUserMessageWidget extends StatelessWidget {
const ChatUserMessageWidget({ const ChatUserMessageWidget({
super.key, super.key,
required this.user, required this.user,
required this.message, required this.message,
required this.isCurrentUser,
}); });
final User user; final User user;
final dynamic message; final TextMessage message;
final bool isCurrentUser;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final stream = message.metadata?["$QuestionStream"];
final messageText = stream is QuestionStream ? stream.text : message.text;
return BlocProvider( return BlocProvider(
create: (context) => ChatUserMessageBloc(message: message) create: (context) => ChatUserMessageBloc(
..add(const ChatUserMessageEvent.initial()), text: messageText,
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>( questionStream: stream,
builder: (context, state) { ),
return Opacity( child: ChatUserMessageBubble(
opacity: state.messageState.isFinish ? 1.0 : 0.8, message: message,
child: TextMessageText( isCurrentUser: isCurrentUser,
text: state.text, files: _getFiles(),
), child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
); builder: (context, state) {
}, return Opacity(
opacity: state.messageState.isFinish ? 1.0 : 0.8,
child: TextMessageText(
text: state.text,
),
);
},
),
), ),
); );
} }
List<ChatFile> _getFiles() {
if (message.metadata == null) {
return const [];
}
final refSourceMetadata =
message.metadata?[messageRefSourceJsonStringKey] as String?;
if (refSourceMetadata != null) {
return chatFilesFromMetadataString(refSourceMetadata);
}
final chatFileList =
message.metadata![messageChatFileListKey] as List<ChatFile>?;
return chatFileList ?? [];
}
} }
/// Widget to reuse the markdown capabilities, e.g., for previews. /// Widget to reuse the markdown capabilities, e.g., for previews.