mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-01 10:33:29 +00:00
chore: simplify chat user message bloc and widgets (#6836)
This commit is contained in:
parent
f82dabcc75
commit
e86a9d697c
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user