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
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
ChatUserMessageBloc({
required dynamic message,
}) : super(ChatUserMessageState.initial(message)) {
required this.questionStream,
required String text,
}) : super(ChatUserMessageState.initial(text)) {
_dispatch();
_startListening();
}
final QuestionStream? questionStream;
void _dispatch() {
on<ChatUserMessageEvent>(
(event, emit) {
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) {
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
class ChatUserMessageEvent with _$ChatUserMessageEvent {
const factory ChatUserMessageEvent.initial() = Initial;
const factory ChatUserMessageEvent.updateText(String text) = _UpdateText;
const factory ChatUserMessageEvent.updateQuestionState(
QuestionMessageState newState,
@ -106,17 +108,14 @@ class ChatUserMessageEvent with _$ChatUserMessageEvent {
class ChatUserMessageState with _$ChatUserMessageState {
const factory ChatUserMessageState({
required String text,
QuestionStream? stream,
String? messageId,
@Default(QuestionMessageState.finish()) QuestionMessageState messageState,
required String? messageId,
required QuestionMessageState messageState,
}) = _ChatUserMessageState;
factory ChatUserMessageState.initial(
dynamic message,
) =>
ChatUserMessageState(
text: message is String ? message : "",
stream: message is QuestionStream ? message : null,
factory ChatUserMessageState.initial(String message) => ChatUserMessageState(
text: message,
messageId: null,
messageState: const QuestionMessageState.finish(),
);
}

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/chat_message_stream.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-user/protobuf.dart';
import 'package:desktop_drop/desktop_drop.dart';
@ -233,27 +232,18 @@ class _ChatContentPage extends StatelessWidget {
}
if (message.author.id == userProfile.id.toString()) {
final stream = message.metadata?["$QuestionStream"];
return ChatUserMessageBubble(
key: ValueKey(message.id),
return ChatUserMessageWidget(
user: message.author,
message: message,
child: ChatUserMessageWidget(
user: message.author,
message: stream is QuestionStream ? stream : message.text,
),
isCurrentUser: true,
);
}
if (isOtherUserMessage(message)) {
final stream = message.metadata?["$QuestionStream"];
return ChatUserMessageBubble(
key: ValueKey(message.id),
return ChatUserMessageWidget(
user: message.author,
message: message,
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?;
return BlocSelector<ChatBloc, ChatState, bool>(
key: ValueKey(message.id),
selector: (state) {
final chatController = context.read<ChatBloc>().chatController;
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/mobile/presentation/bottom_sheet/bottom_sheet.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/shared/markdown_to_document.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:universal_platform/universal_platform.dart';
import '../chat_avatar.dart';
import '../layout_define.dart';
/// 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
/// last message in the chat. For other AI responses, the actions will be shown
/// on hover. On mobile, the actions will be displayed in a bottom sheet on
/// long press.
/// last message in the chat. For the others, the actions will be shown on hover
/// On mobile, the actions will be displayed in a bottom sheet on long press.
class ChatAIMessageBubble extends StatelessWidget {
const ChatAIMessageBubble({
super.key,

View File

@ -1,9 +1,6 @@
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_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/widget/spacing.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:universal_platform/universal_platform.dart';
import '../chat_avatar.dart';
import '../layout_define.dart';
class ChatUserMessageBubble extends StatelessWidget {
const ChatUserMessageBubble({
super.key,
required this.message,
required this.child,
this.isCurrentUser = true,
required this.isCurrentUser,
this.files = const [],
});
final Message message;
final Widget child;
final bool isCurrentUser;
final List<ChatFile> files;
@override
Widget build(BuildContext context) {
if (context.read<ChatMemberBloc>().state.members[message.author.id] ==
null) {
context
.read<ChatMemberBloc>()
.add(ChatMemberEvent.getMemberInfo(message.author.id));
}
context
.read<ChatMemberBloc>()
.add(ChatMemberEvent.getMemberInfo(message.author.id));
return BlocProvider(
create: (context) => ChatUserMessageBubbleBloc(
message: message,
),
child: BlocBuilder<ChatUserMessageBubbleBloc, ChatUserMessageBubbleState>(
builder: (context, state) {
return Padding(
padding: UniversalPlatform.isMobile
? const EdgeInsets.symmetric(horizontal: 16)
: EdgeInsets.zero,
child: Column(
mainAxisSize: MainAxisSize.min,
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),
),
],
return Padding(
padding: UniversalPlatform.isMobile
? const EdgeInsets.symmetric(horizontal: 16)
: EdgeInsets.zero,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (files.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(right: 32),
child: _MessageFileList(files: files),
),
);
},
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:flowy_infra/theme_extension.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_chat_core/flutter_chat_core.dart';
import 'user_message_bubble.dart';
class ChatUserMessageWidget extends StatelessWidget {
const ChatUserMessageWidget({
super.key,
required this.user,
required this.message,
required this.isCurrentUser,
});
final User user;
final dynamic message;
final TextMessage message;
final bool isCurrentUser;
@override
Widget build(BuildContext context) {
final stream = message.metadata?["$QuestionStream"];
final messageText = stream is QuestionStream ? stream.text : message.text;
return BlocProvider(
create: (context) => ChatUserMessageBloc(message: message)
..add(const ChatUserMessageEvent.initial()),
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
builder: (context, state) {
return Opacity(
opacity: state.messageState.isFinish ? 1.0 : 0.8,
child: TextMessageText(
text: state.text,
),
);
},
create: (context) => ChatUserMessageBloc(
text: messageText,
questionStream: stream,
),
child: ChatUserMessageBubble(
message: message,
isCurrentUser: isCurrentUser,
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.