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';
|
|
|
|
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';
|
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';
|
2024-11-20 10:47:35 +03:00
|
|
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
|
|
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart'
|
|
|
|
hide ChatAnimatedListReversed;
|
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';
|
2024-11-20 10:47:35 +03:00
|
|
|
import 'presentation/animated_chat_list.dart';
|
2024-11-14 19:26:37 +03:00
|
|
|
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_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-11-20 10:47:35 +03:00
|
|
|
import 'presentation/scroll_to_bottom.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(
|
2024-11-20 10:47:35 +03:00
|
|
|
chatId: view.id,
|
|
|
|
userId: userProfile.id.toString(),
|
|
|
|
),
|
2024-11-14 19:26:37 +03:00
|
|
|
),
|
|
|
|
|
|
|
|
/// [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(
|
2024-11-20 10:47:35 +03:00
|
|
|
child: buildChatWidget(context)
|
2024-11-14 19:26:37 +03:00
|
|
|
.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),
|
2024-11-20 10:47:35 +03:00
|
|
|
child: buildChatWidget(context),
|
2024-11-14 19:26:37 +03:00
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
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-11-20 10:47:35 +03:00
|
|
|
Widget buildChatWidget(BuildContext context) {
|
|
|
|
return ScrollConfiguration(
|
|
|
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
|
|
|
child: BlocBuilder<ChatBloc, ChatState>(
|
|
|
|
builder: (context, state) {
|
|
|
|
return state.loadingState.when(
|
|
|
|
loading: () {
|
|
|
|
return const Center(
|
|
|
|
child: CircularProgressIndicator.adaptive(),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
finish: (_) {
|
|
|
|
final chatController = context.read<ChatBloc>().chatController;
|
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: Chat(
|
|
|
|
chatController: chatController,
|
|
|
|
user: User(id: userProfile.id.toString()),
|
|
|
|
darkTheme: ChatTheme.fromThemeData(Theme.of(context)),
|
|
|
|
theme: ChatTheme.fromThemeData(Theme.of(context)),
|
|
|
|
builders: Builders(
|
|
|
|
inputBuilder: (_) => const SizedBox.shrink(),
|
|
|
|
textMessageBuilder: _buildTextMessage,
|
|
|
|
chatMessageBuilder: _buildChatMessage,
|
|
|
|
scrollToBottomBuilder: _buildScrollToBottom,
|
|
|
|
chatAnimatedListBuilder: _buildChatAnimatedList,
|
2024-11-15 12:31:51 +03:00
|
|
|
),
|
2024-11-14 19:26:37 +03:00
|
|
|
),
|
2024-08-09 21:55:20 +08:00
|
|
|
),
|
2024-11-20 10:47:35 +03:00
|
|
|
_buildInput(context),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
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
|
|
|
) {
|
2024-11-20 10:47:35 +03:00
|
|
|
final messageType = onetimeMessageTypeFromMeta(
|
|
|
|
message.metadata,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (messageType == OnetimeShotType.invalidSendMesssage) {
|
|
|
|
return ChatInvalidUserMessage(
|
|
|
|
message: message,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (messageType == OnetimeShotType.relatedQuestion) {
|
|
|
|
return RelatedQuestionList(
|
|
|
|
relatedQuestions: message.metadata!['questions'],
|
|
|
|
onQuestionSelected: (question) {
|
|
|
|
context
|
|
|
|
.read<ChatBloc>()
|
|
|
|
.add(ChatEvent.sendMessage(message: question));
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-11-14 19:26:37 +03:00
|
|
|
if (message.author.id == userProfile.id.toString()) {
|
2024-11-20 13:13:38 +03:00
|
|
|
return ChatUserMessageWidget(
|
|
|
|
user: message.author,
|
2024-11-20 10:47:35 +03:00
|
|
|
message: message,
|
2024-11-20 13:13:38 +03:00
|
|
|
isCurrentUser: true,
|
2024-08-09 07:40:24 +08:00
|
|
|
);
|
2024-11-20 10:47:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isOtherUserMessage(message)) {
|
2024-11-20 13:13:38 +03:00
|
|
|
return ChatUserMessageWidget(
|
|
|
|
user: message.author,
|
2024-11-20 10:47:35 +03:00
|
|
|
message: message,
|
|
|
|
isCurrentUser: false,
|
2024-06-09 14:02:32 +08:00
|
|
|
);
|
2024-11-20 10:47:35 +03:00
|
|
|
}
|
2024-11-14 19:26:37 +03:00
|
|
|
|
2024-11-20 10:47:35 +03:00
|
|
|
final stream = message.metadata?["$AnswerStream"];
|
|
|
|
final questionId = message.metadata?[messageQuestionIdKey];
|
|
|
|
final refSourceJsonString =
|
|
|
|
message.metadata?[messageRefSourceJsonStringKey] as String?;
|
|
|
|
|
|
|
|
return BlocSelector<ChatBloc, ChatState, bool>(
|
|
|
|
selector: (state) {
|
|
|
|
final chatController = context.read<ChatBloc>().chatController;
|
|
|
|
final messages = chatController.messages.where((e) {
|
|
|
|
final oneTimeMessageType = onetimeMessageTypeFromMeta(e.metadata);
|
|
|
|
if (oneTimeMessageType == null) {
|
2024-11-14 19:26:37 +03:00
|
|
|
return true;
|
2024-11-20 10:47:35 +03:00
|
|
|
}
|
|
|
|
if (oneTimeMessageType
|
|
|
|
case OnetimeShotType.relatedQuestion ||
|
|
|
|
OnetimeShotType.sendingMessage ||
|
|
|
|
OnetimeShotType.invalidSendMesssage) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
return messages.isEmpty ? false : messages.last.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-06-09 14:02:32 +08:00
|
|
|
}
|
|
|
|
|
2024-11-20 10:47:35 +03:00
|
|
|
Widget _buildChatMessage(
|
2024-11-14 19:26:37 +03:00
|
|
|
BuildContext context,
|
2024-06-09 14:02:32 +08:00
|
|
|
Message message,
|
2024-11-20 10:47:35 +03:00
|
|
|
Animation<double> animation,
|
2024-06-09 14:02:32 +08:00
|
|
|
Widget child,
|
|
|
|
) {
|
2024-11-20 10:47:35 +03:00
|
|
|
return ChatMessage(
|
|
|
|
message: message,
|
|
|
|
animation: animation,
|
2024-11-24 22:43:45 +08:00
|
|
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
2024-11-20 10:47:35 +03:00
|
|
|
child: child,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildScrollToBottom(
|
|
|
|
BuildContext context,
|
|
|
|
Animation<double> animation,
|
|
|
|
VoidCallback onPressed,
|
|
|
|
) {
|
|
|
|
return CustomScrollToBottom(
|
|
|
|
animation: animation,
|
|
|
|
onPressed: onPressed,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildChatAnimatedList(
|
|
|
|
BuildContext context,
|
|
|
|
ScrollController scrollController,
|
|
|
|
ChatItem itemBuilder,
|
|
|
|
) {
|
|
|
|
final bloc = context.read<ChatBloc>();
|
|
|
|
|
|
|
|
if (bloc.chatController.messages.isEmpty) {
|
|
|
|
return ChatWelcomePage(
|
|
|
|
userProfile: userProfile,
|
|
|
|
onSelectedQuestion: (question) {
|
|
|
|
bloc.add(ChatEvent.sendMessage(message: question));
|
|
|
|
},
|
2024-06-09 14:02:32 +08:00
|
|
|
);
|
|
|
|
}
|
2024-11-20 10:47:35 +03:00
|
|
|
|
|
|
|
return ChatAnimatedListReversed(
|
|
|
|
scrollController: scrollController,
|
|
|
|
itemBuilder: itemBuilder,
|
|
|
|
onLoadPreviousMessages: () {
|
|
|
|
bloc.add(const ChatEvent.loadPreviousMessages());
|
|
|
|
},
|
|
|
|
);
|
2024-06-09 14:02:32 +08:00
|
|
|
}
|
|
|
|
|
2024-11-20 10:47:35 +03:00
|
|
|
Widget _buildInput(BuildContext context) {
|
2024-11-14 19:26:37 +03:00
|
|
|
return Padding(
|
|
|
|
padding: AIChatUILayout.safeAreaInsets(context),
|
|
|
|
child: BlocSelector<ChatBloc, ChatState, bool>(
|
2024-11-20 10:47:35 +03:00
|
|
|
selector: (state) {
|
|
|
|
return state.promptResponseState == PromptResponseState.ready;
|
|
|
|
},
|
2024-11-14 19:26:37 +03:00
|
|
|
builder: (context, canSendMessage) {
|
|
|
|
return UniversalPlatform.isDesktop
|
|
|
|
? DesktopAIPromptInput(
|
|
|
|
chatId: view.id,
|
|
|
|
indicateFocus: true,
|
2024-11-20 10:47:35 +03:00
|
|
|
onSubmitted: (text, metadata) {
|
2024-11-14 19:26:37 +03:00
|
|
|
context.read<ChatBloc>().add(
|
|
|
|
ChatEvent.sendMessage(
|
2024-11-20 10:47:35 +03:00
|
|
|
message: text,
|
|
|
|
metadata: metadata,
|
2024-11-14 19:26:37 +03:00
|
|
|
),
|
|
|
|
);
|
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,
|
2024-11-20 10:47:35 +03:00
|
|
|
onSubmitted: (text, metadata) {
|
2024-11-14 19:26:37 +03:00
|
|
|
context.read<ChatBloc>().add(
|
|
|
|
ChatEvent.sendMessage(
|
2024-11-20 10:47:35 +03:00
|
|
|
message: text,
|
|
|
|
metadata: metadata,
|
2024-11-14 19:26:37 +03:00
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
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
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|