2024-06-03 14:27:28 +08:00
|
|
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
|
|
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
2024-08-09 21:55:20 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
2024-11-14 19:26:37 +03:00
|
|
|
import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart';
|
2024-08-09 21:55:20 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
|
2024-08-08 12:07:00 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/presentation/message/user_message_bubble.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
|
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
2024-08-08 09:49:08 +08:00
|
|
|
import 'package:desktop_drop/desktop_drop.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
|
|
import 'package:flowy_infra/theme_extension.dart';
|
2024-08-08 09:49:08 +08:00
|
|
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
2024-08-08 09:49:08 +08:00
|
|
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
2024-06-28 16:54:54 +02:00
|
|
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
|
2024-08-06 07:56:13 +08:00
|
|
|
import 'package:styled_widget/styled_widget.dart';
|
2024-09-12 14:40:19 +08:00
|
|
|
import 'package:universal_platform/universal_platform.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
|
2024-08-07 16:48:09 +08:00
|
|
|
import 'application/chat_member_bloc.dart';
|
2024-11-14 19:26:37 +03:00
|
|
|
import 'application/chat_side_panel_bloc.dart';
|
|
|
|
import 'presentation/chat_input/desktop_ai_prompt_input.dart';
|
|
|
|
import 'presentation/chat_input/mobile_ai_prompt_input.dart';
|
|
|
|
import 'presentation/chat_side_panel.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'presentation/chat_theme.dart';
|
|
|
|
import 'presentation/chat_user_invalid_message.dart';
|
|
|
|
import 'presentation/chat_welcome_page.dart';
|
2024-11-14 19:26:37 +03:00
|
|
|
import 'presentation/layout_define.dart';
|
2024-06-09 14:02:32 +08:00
|
|
|
import 'presentation/message/ai_text_message.dart';
|
|
|
|
import 'presentation/message/user_text_message.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
|
2024-07-15 15:23:23 +08:00
|
|
|
class AIChatPage extends StatelessWidget {
|
2024-06-03 14:27:28 +08:00
|
|
|
const AIChatPage({
|
|
|
|
super.key,
|
|
|
|
required this.view,
|
|
|
|
required this.onDeleted,
|
|
|
|
required this.userProfile,
|
|
|
|
});
|
|
|
|
|
|
|
|
final ViewPB view;
|
|
|
|
final VoidCallback onDeleted;
|
|
|
|
final UserProfilePB userProfile;
|
|
|
|
|
|
|
|
@override
|
2024-07-15 15:23:23 +08:00
|
|
|
Widget build(BuildContext context) {
|
2024-11-14 19:26:37 +03:00
|
|
|
if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) {
|
|
|
|
return Center(
|
|
|
|
child: FlowyText(
|
|
|
|
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
|
|
|
fontSize: 20,
|
2024-07-15 15:23:23 +08:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-11-14 19:26:37 +03:00
|
|
|
return MultiBlocProvider(
|
|
|
|
providers: [
|
|
|
|
/// [ChatBloc] is used to handle chat messages including send/receive message
|
|
|
|
BlocProvider(
|
|
|
|
create: (_) => ChatBloc(
|
|
|
|
view: view,
|
|
|
|
userProfile: userProfile,
|
|
|
|
)..add(const ChatEvent.initialLoad()),
|
|
|
|
),
|
|
|
|
|
|
|
|
/// [AIPromptInputBloc] is used to handle the user prompt
|
|
|
|
BlocProvider(create: (_) => AIPromptInputBloc()),
|
|
|
|
BlocProvider(create: (_) => ChatSidePanelBloc(chatId: view.id)),
|
|
|
|
BlocProvider(create: (_) => ChatMemberBloc()),
|
|
|
|
],
|
|
|
|
child: DropTarget(
|
|
|
|
onDragDone: (DropDoneDetails detail) async {
|
|
|
|
if (context.read<AIPromptInputBloc>().state.supportChatWithFile) {
|
|
|
|
for (final file in detail.files) {
|
|
|
|
context
|
|
|
|
.read<AIPromptInputBloc>()
|
|
|
|
.add(AIPromptInputEvent.newFile(file.path, file.name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
child: _ChatContentPage(
|
|
|
|
view: view,
|
|
|
|
userProfile: userProfile,
|
|
|
|
),
|
2024-07-15 15:23:23 +08:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-14 19:26:37 +03:00
|
|
|
class _ChatContentPage extends StatelessWidget {
|
2024-07-15 15:23:23 +08:00
|
|
|
const _ChatContentPage({
|
|
|
|
required this.view,
|
|
|
|
required this.userProfile,
|
|
|
|
});
|
|
|
|
|
|
|
|
final UserProfilePB userProfile;
|
|
|
|
final ViewPB view;
|
|
|
|
|
2024-06-03 14:27:28 +08:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2024-11-14 19:26:37 +03:00
|
|
|
if (UniversalPlatform.isDesktop) {
|
|
|
|
return BlocSelector<ChatSidePanelBloc, ChatSidePanelState, bool>(
|
|
|
|
selector: (state) => state.isShowPanel,
|
|
|
|
builder: (context, isShowPanel) {
|
|
|
|
return LayoutBuilder(
|
|
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
|
|
final sidePanelRatio = isShowPanel ? 0.33 : 0.0;
|
|
|
|
final chatWidth = constraints.maxWidth * (1 - sidePanelRatio);
|
|
|
|
final sidePanelWidth =
|
|
|
|
constraints.maxWidth * sidePanelRatio - 1.0;
|
|
|
|
|
|
|
|
return Row(
|
|
|
|
children: [
|
|
|
|
Center(
|
|
|
|
child: buildChatWidget()
|
|
|
|
.constrained(
|
|
|
|
maxWidth: 784,
|
2024-08-06 07:56:13 +08:00
|
|
|
)
|
2024-11-14 19:26:37 +03:00
|
|
|
.padding(horizontal: 32)
|
|
|
|
.animate(
|
|
|
|
const Duration(milliseconds: 200),
|
|
|
|
Curves.easeOut,
|
|
|
|
),
|
|
|
|
).constrained(width: chatWidth),
|
|
|
|
if (isShowPanel) ...[
|
|
|
|
const VerticalDivider(
|
|
|
|
width: 1.0,
|
|
|
|
thickness: 1.0,
|
|
|
|
),
|
|
|
|
buildChatSidePanel()
|
|
|
|
.constrained(width: sidePanelWidth)
|
2024-08-06 07:56:13 +08:00
|
|
|
.animate(
|
|
|
|
const Duration(milliseconds: 200),
|
|
|
|
Curves.easeOut,
|
|
|
|
),
|
|
|
|
],
|
2024-11-14 19:26:37 +03:00
|
|
|
],
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
2024-08-06 07:56:13 +08:00
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
2024-11-14 19:26:37 +03:00
|
|
|
return Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
Flexible(
|
|
|
|
child: ConstrainedBox(
|
|
|
|
constraints: const BoxConstraints(maxWidth: 784),
|
|
|
|
child: buildChatWidget(),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
2024-08-06 07:56:13 +08:00
|
|
|
}
|
|
|
|
}
|
2024-07-18 20:54:35 +08:00
|
|
|
|
2024-11-14 19:26:37 +03:00
|
|
|
Widget buildChatSidePanel() {
|
|
|
|
return BlocBuilder<ChatSidePanelBloc, ChatSidePanelState>(
|
|
|
|
builder: (context, state) {
|
|
|
|
if (state.metadata == null) {
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
}
|
|
|
|
return const ChatSidePanel();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-08-06 07:56:13 +08:00
|
|
|
Widget buildChatWidget() {
|
|
|
|
return BlocBuilder<ChatBloc, ChatState>(
|
2024-11-14 19:26:37 +03:00
|
|
|
builder: (context, state) {
|
|
|
|
return ScrollConfiguration(
|
|
|
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
|
|
|
child: BlocBuilder<ChatBloc, ChatState>(
|
|
|
|
builder: (_, state) => state.initialLoadingStatus.isFinish
|
|
|
|
? Chat(
|
|
|
|
messages: state.messages,
|
|
|
|
dateHeaderBuilder: (_) => const SizedBox.shrink(),
|
|
|
|
onSendPressed: (_) {
|
|
|
|
// We use custom bottom widget for chat input, so
|
|
|
|
// do not need to handle this event.
|
|
|
|
},
|
|
|
|
customBottomWidget: _buildBottom(context),
|
|
|
|
user: types.User(id: userProfile.id.toString()),
|
|
|
|
theme: _buildTheme(context),
|
|
|
|
onEndReached: () async {
|
|
|
|
if (state.hasMorePrevMessage &&
|
|
|
|
state.loadingPreviousStatus.isFinish) {
|
|
|
|
context
|
|
|
|
.read<ChatBloc>()
|
|
|
|
.add(const ChatEvent.startLoadingPrevMessage());
|
|
|
|
}
|
|
|
|
},
|
2024-11-15 12:31:51 +03:00
|
|
|
emptyState: TextFieldTapRegion(
|
|
|
|
child: ChatWelcomePage(
|
|
|
|
userProfile: userProfile,
|
|
|
|
onSelectedQuestion: (question) => context
|
|
|
|
.read<ChatBloc>()
|
|
|
|
.add(ChatEvent.sendMessage(message: question)),
|
|
|
|
),
|
2024-11-14 19:26:37 +03:00
|
|
|
),
|
|
|
|
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
|
|
|
textMessageBuilder: (
|
|
|
|
textMessage, {
|
|
|
|
required messageWidth,
|
|
|
|
required showName,
|
|
|
|
}) =>
|
|
|
|
_buildTextMessage(context, textMessage, state),
|
|
|
|
customMessageBuilder: (message, {required messageWidth}) {
|
|
|
|
final messageType = onetimeMessageTypeFromMeta(
|
|
|
|
message.metadata,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (messageType == OnetimeShotType.invalidSendMesssage) {
|
|
|
|
return ChatInvalidUserMessage(
|
|
|
|
message: message,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (messageType == OnetimeShotType.relatedQuestion) {
|
|
|
|
return RelatedQuestionList(
|
|
|
|
relatedQuestions: state.relatedQuestions,
|
|
|
|
onQuestionSelected: (question) {
|
|
|
|
final bloc = context.read<ChatBloc>();
|
|
|
|
bloc
|
|
|
|
..add(ChatEvent.sendMessage(message: question))
|
|
|
|
..add(const ChatEvent.clearRelatedQuestions());
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
},
|
|
|
|
bubbleBuilder: (
|
|
|
|
child, {
|
|
|
|
required message,
|
|
|
|
required nextMessageInGroup,
|
|
|
|
}) =>
|
|
|
|
_buildBubble(context, message, child),
|
|
|
|
)
|
|
|
|
: const Center(
|
|
|
|
child: CircularProgressIndicator.adaptive(),
|
2024-08-09 21:55:20 +08:00
|
|
|
),
|
2024-11-14 19:26:37 +03:00
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
2024-06-03 14:27:28 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-11-14 19:26:37 +03:00
|
|
|
Widget _buildTextMessage(
|
|
|
|
BuildContext context,
|
|
|
|
TextMessage message,
|
2024-08-09 07:40:24 +08:00
|
|
|
ChatState state,
|
|
|
|
) {
|
2024-11-14 19:26:37 +03:00
|
|
|
if (message.author.id == userProfile.id.toString()) {
|
|
|
|
final stream = message.metadata?["$QuestionStream"];
|
|
|
|
return ChatUserMessageWidget(
|
|
|
|
key: ValueKey(message.id),
|
|
|
|
user: message.author,
|
|
|
|
message: stream is QuestionStream ? stream : message.text,
|
2024-08-09 07:40:24 +08:00
|
|
|
);
|
|
|
|
} else if (isOtherUserMessage(message)) {
|
2024-08-10 17:23:37 +08:00
|
|
|
final stream = message.metadata?["$QuestionStream"];
|
|
|
|
return ChatUserMessageWidget(
|
|
|
|
key: ValueKey(message.id),
|
2024-06-09 14:02:32 +08:00
|
|
|
user: message.author,
|
2024-08-10 17:23:37 +08:00
|
|
|
message: stream is QuestionStream ? stream : message.text,
|
2024-06-09 14:02:32 +08:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
final stream = message.metadata?["$AnswerStream"];
|
2024-08-09 07:40:24 +08:00
|
|
|
final questionId = message.metadata?[messageQuestionIdKey];
|
2024-08-12 09:21:44 +08:00
|
|
|
final refSourceJsonString =
|
|
|
|
message.metadata?[messageRefSourceJsonStringKey] as String?;
|
2024-11-14 19:26:37 +03:00
|
|
|
|
|
|
|
return BlocSelector<ChatBloc, ChatState, bool>(
|
2024-06-09 14:02:32 +08:00
|
|
|
key: ValueKey(message.id),
|
2024-11-14 19:26:37 +03:00
|
|
|
selector: (state) {
|
|
|
|
final messages = state.messages.where((e) {
|
|
|
|
final oneTimeMessageType = onetimeMessageTypeFromMeta(e.metadata);
|
|
|
|
if (oneTimeMessageType == null) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (oneTimeMessageType
|
|
|
|
case OnetimeShotType.relatedQuestion ||
|
|
|
|
OnetimeShotType.sendingMessage ||
|
|
|
|
OnetimeShotType.invalidSendMesssage) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
return messages.isEmpty ? false : messages.first.id == message.id;
|
|
|
|
},
|
|
|
|
builder: (context, isLastMessage) {
|
|
|
|
return ChatAIMessageWidget(
|
|
|
|
user: message.author,
|
|
|
|
messageUserId: message.id,
|
|
|
|
message: message,
|
|
|
|
stream: stream is AnswerStream ? stream : null,
|
|
|
|
questionId: questionId,
|
|
|
|
chatId: view.id,
|
|
|
|
refSourceJsonString: refSourceJsonString,
|
|
|
|
isLastMessage: isLastMessage,
|
|
|
|
onSelectedMetadata: (metadata) {
|
|
|
|
context
|
|
|
|
.read<ChatSidePanelBloc>()
|
|
|
|
.add(ChatSidePanelEvent.selectedMetadata(metadata));
|
|
|
|
},
|
|
|
|
);
|
2024-08-06 07:56:13 +08:00
|
|
|
},
|
2024-06-09 14:02:32 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-14 19:26:37 +03:00
|
|
|
Widget _buildBubble(
|
|
|
|
BuildContext context,
|
2024-06-09 14:02:32 +08:00
|
|
|
Message message,
|
|
|
|
Widget child,
|
|
|
|
) {
|
2024-11-14 19:26:37 +03:00
|
|
|
if (message.author.id == userProfile.id.toString()) {
|
|
|
|
return ChatUserMessageBubble(
|
2024-06-09 14:02:32 +08:00
|
|
|
message: message,
|
2024-11-14 19:26:37 +03:00
|
|
|
child: child,
|
2024-06-09 14:02:32 +08:00
|
|
|
);
|
2024-11-14 19:26:37 +03:00
|
|
|
} else if (isOtherUserMessage(message)) {
|
|
|
|
return ChatUserMessageBubble(
|
|
|
|
message: message,
|
|
|
|
isCurrentUser: false,
|
|
|
|
child: child,
|
2024-06-09 14:02:32 +08:00
|
|
|
);
|
2024-11-14 19:26:37 +03:00
|
|
|
} else {
|
|
|
|
// The bubble is rendered in the child already
|
|
|
|
return child;
|
2024-06-09 14:02:32 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-09 07:40:24 +08:00
|
|
|
Widget _buildBottom(BuildContext context) {
|
2024-11-14 19:26:37 +03:00
|
|
|
return Padding(
|
|
|
|
padding: AIChatUILayout.safeAreaInsets(context),
|
|
|
|
child: BlocSelector<ChatBloc, ChatState, bool>(
|
|
|
|
selector: (state) => state.canSendMessage,
|
|
|
|
builder: (context, canSendMessage) {
|
|
|
|
return UniversalPlatform.isDesktop
|
|
|
|
? DesktopAIPromptInput(
|
|
|
|
chatId: view.id,
|
|
|
|
indicateFocus: true,
|
|
|
|
onSubmitted: (message) {
|
|
|
|
context.read<ChatBloc>().add(
|
|
|
|
ChatEvent.sendMessage(
|
|
|
|
message: message.text,
|
|
|
|
metadata: message.metadata,
|
|
|
|
),
|
|
|
|
);
|
2024-07-27 23:47:08 +08:00
|
|
|
},
|
2024-11-14 19:26:37 +03:00
|
|
|
isStreaming: !canSendMessage,
|
|
|
|
onStopStreaming: () {
|
|
|
|
context.read<ChatBloc>().add(const ChatEvent.stopStream());
|
|
|
|
},
|
|
|
|
)
|
|
|
|
: MobileAIPromptInput(
|
|
|
|
chatId: view.id,
|
|
|
|
onSubmitted: (message) {
|
|
|
|
context.read<ChatBloc>().add(
|
|
|
|
ChatEvent.sendMessage(
|
|
|
|
message: message.text,
|
|
|
|
metadata: message.metadata,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
isStreaming: !canSendMessage,
|
|
|
|
onStopStreaming: () {
|
|
|
|
context.read<ChatBloc>().add(const ChatEvent.stopStream());
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
2024-06-03 23:20:33 +08:00
|
|
|
),
|
2024-06-03 14:27:28 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-11-14 19:26:37 +03:00
|
|
|
AFDefaultChatTheme _buildTheme(BuildContext context) {
|
|
|
|
return AFDefaultChatTheme(
|
|
|
|
primaryColor: Theme.of(context).colorScheme.primary,
|
|
|
|
secondaryColor: AFThemeExtension.of(context).tint1,
|
|
|
|
);
|
|
|
|
}
|
2024-06-03 14:27:28 +08:00
|
|
|
}
|