From 2926fc0fbac3b3483b558300d0075bd0a114e415 Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 2 Jun 2024 12:54:07 +0800 Subject: [PATCH] chore: return related question --- .../application/chat_ai_message_bloc.dart | 42 ++++ .../ai_chat/application/chat_bloc.dart | 70 +++++-- .../application/chat_message_listener.dart | 96 ++++----- .../chat_related_question_bloc.dart | 103 +++++++++ .../application/chat_user_message_bloc.dart | 44 ++++ .../lib/plugins/ai_chat/chat_page.dart | 179 +++++++++++----- .../ai_chat/presentation/chat_ai_message.dart | 197 ++++++++++++++++++ .../ai_chat/presentation/chat_avatar.dart | 184 ++++++++++++++++ .../presentation/chat_error_message.dart | 43 ++++ .../ai_chat/presentation/chat_input.dart | 117 ++++++----- .../presentation/chat_message_hover.dart | 106 ---------- .../presentation/chat_related_question.dart | 81 +++++++ .../ai_chat/presentation/chat_theme.dart | 4 +- .../presentation/chat_user_message.dart | 174 ++++++++++++++++ .../presentation/chat_welcome_page.dart | 10 + .../lib/style_widget/button.dart | 2 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 34 +-- frontend/appflowy_tauri/src-tauri/Cargo.toml | 2 +- frontend/appflowy_web/wasm-libs/Cargo.lock | 29 +-- frontend/appflowy_web/wasm-libs/Cargo.toml | 2 +- .../appflowy_web_app/src-tauri/Cargo.lock | 31 +-- .../appflowy_web_app/src-tauri/Cargo.toml | 2 +- frontend/resources/translations/en.json | 7 +- frontend/rust-lib/Cargo.lock | 28 +-- frontend/rust-lib/Cargo.toml | 2 +- frontend/rust-lib/flowy-chat-pub/src/cloud.rs | 8 + frontend/rust-lib/flowy-chat/src/chat.rs | 40 +++- frontend/rust-lib/flowy-chat/src/entities.rs | 49 ++++- .../rust-lib/flowy-chat/src/event_handler.rs | 13 ++ frontend/rust-lib/flowy-chat/src/event_map.rs | 4 + frontend/rust-lib/flowy-chat/src/manager.rs | 12 +- .../rust-lib/flowy-chat/src/notification.rs | 2 + .../flowy-core/src/integrate/trait_impls.rs | 18 ++ .../flowy-server/src/af_cloud/impls/chat.rs | 21 ++ .../src/af_cloud/impls/user/dto.rs | 1 + .../rust-lib/flowy-server/src/default_impl.rs | 12 ++ .../rust-lib/flowy-user-pub/src/entities.rs | 1 + .../flowy-user/src/entities/workspace.rs | 4 + 38 files changed, 1417 insertions(+), 357 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_related_question_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_ai_message.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_error_message.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_hover.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_user_message.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart new file mode 100644 index 0000000000000..2c5c8f5b0e36b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart @@ -0,0 +1,42 @@ +import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_ai_message_bloc.freezed.dart'; + +class ChatAIMessageBloc extends Bloc { + ChatAIMessageBloc({ + required Message message, + }) : super(ChatAIMessageState.initial(message)) { + on( + (event, emit) async { + await event.when( + initial: () async {}, + update: (userProfile, deviceId, states) {}, + ); + }, + ); + } +} + +@freezed +class ChatAIMessageEvent with _$ChatAIMessageEvent { + const factory ChatAIMessageEvent.initial() = Initial; + const factory ChatAIMessageEvent.update( + UserProfilePB userProfile, + String deviceId, + DocumentAwarenessStatesPB states, + ) = Update; +} + +@freezed +class ChatAIMessageState with _$ChatAIMessageState { + const factory ChatAIMessageState({ + required Message message, + }) = _ChatAIMessageState; + + factory ChatAIMessageState.initial(Message message) => + ChatAIMessageState(message: message); +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 959218dd69d62..21bb63510170b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -27,12 +27,9 @@ class ChatBloc extends Bloc { listener.start( chatMessageCallback: _handleChatMessage, chatErrorMessageCallback: (err) { - final error = TextMessage( - metadata: { - CustomMessageType.streamError.toString(): - CustomMessageType.streamError, - }, - text: err.errorMessage, + Log.error("chat error: ${err.errorMessage}"); + final error = CustomMessage( + metadata: OnetimeMessageType.serverStreamError.toMap(), author: const User(id: "system"), id: 'system', ); @@ -109,13 +106,10 @@ class ChatBloc extends Bloc { }, tapMessage: (Message message) {}, streamingChatMessage: (List messages) { + // filter out loading or error messages final allMessages = state.messages.where((element) { - return !(element.metadata - ?.containsValue(CustomMessageType.loading) == - true || - element.metadata - ?.containsValue(CustomMessageType.streamError) == - true); + return !(element.metadata?.containsKey(onetimeMessageType) == + true); }).toList(); allMessages.insertAll(0, messages); emit(state.copyWith(messages: allMessages)); @@ -140,6 +134,7 @@ class ChatBloc extends Bloc { ), ); }, + retryGenerate: () {}, ); }, ); @@ -179,21 +174,20 @@ class ChatBloc extends Bloc { Message _loadingMessage(String id) { return CustomMessage( author: User(id: id), - metadata: { - CustomMessageType.loading.toString(): CustomMessageType.loading, - }, + metadata: OnetimeMessageType.loading.toMap(), + // fake id id: 'chat_message_loading_id', ); } Message _createChatMessage(ChatMessagePB message) { - final id = message.messageId.toString(); + final messageId = message.messageId.toString(); return TextMessage( author: User(id: message.authorId), - id: id, + id: messageId, text: message.content, createdAt: message.createdAt.toInt(), - repliedMessage: _getReplyMessage(state.messages, id), + repliedMessage: _getReplyMessage(state.messages, messageId), ); } @@ -218,6 +212,7 @@ class ChatEvent with _$ChatEvent { List messages, bool hasMore, ) = _DidLoadPreviousMessages; + const factory ChatEvent.retryGenerate() = _RetryGenerate; } @freezed @@ -229,6 +224,7 @@ class ChatState with _$ChatState { required LoadingState loadingStatus, required LoadingState loadingPreviousStatus, required LoadingState answerQuestionStatus, + required List relatedQuestions, required bool hasMore, }) = _ChatState; @@ -241,6 +237,7 @@ class ChatState with _$ChatState { loadingPreviousStatus: const LoadingState.finish(), answerQuestionStatus: const LoadingState.finish(), hasMore: true, + relatedQuestions: [], ); } @@ -250,4 +247,39 @@ class LoadingState with _$LoadingState { const factory LoadingState.finish() = _Finish; } -enum CustomMessageType { loading, streamError } +enum OnetimeMessageType { unknown, loading, serverStreamError } + +const onetimeMessageType = "OnetimeMessageType"; + +extension OnetimeMessageTypeExtension on OnetimeMessageType { + static OnetimeMessageType fromString(String value) { + switch (value) { + case 'OnetimeMessageType.loading': + return OnetimeMessageType.loading; + case 'OnetimeMessageType.serverStreamError': + return OnetimeMessageType.serverStreamError; + default: + Log.error('Unknown OnetimeMessageType: $value'); + return OnetimeMessageType.unknown; + } + } + + Map toMap() { + return { + onetimeMessageType: toString(), + }; + } +} + +OnetimeMessageType? restoreOnetimeMessageType(Map? metadata) { + if (metadata == null) { + return null; + } + + for (final entry in metadata.entries) { + if (entry.key == onetimeMessageType) { + return OnetimeMessageTypeExtension.fromString(entry.value as String); + } + } + return null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart index 8d0a5b9b39c59..c37d25608efd7 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart @@ -10,29 +10,14 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'chat_notification.dart'; -typedef ChatMessageCallback = void Function( - ChatMessagePB message, -); - -typedef ChatErrorMessageCallback = void Function( - ChatMessageErrorPB message, -); - -typedef LatestMessageCallback = void Function( - ChatMessageListPB list, -); -typedef PrevMessageCallback = void Function( - ChatMessageListPB list, -); +typedef ChatMessageCallback = void Function(ChatMessagePB message); +typedef ChatErrorMessageCallback = void Function(ChatMessageErrorPB message); +typedef LatestMessageCallback = void Function(ChatMessageListPB list); +typedef PrevMessageCallback = void Function(ChatMessageListPB list); class ChatMessageListener { - ChatMessageListener({ - required this.chatId, - }) { - _parser = ChatNotificationParser( - id: chatId, - callback: _callback, - ); + ChatMessageListener({required this.chatId}) { + _parser = ChatNotificationParser(id: chatId, callback: _callback); _subscription = RustStreamReceiver.listen( (observable) => _parser?.parse(observable), ); @@ -41,7 +26,9 @@ class ChatMessageListener { final String chatId; StreamSubscription? _subscription; ChatNotificationParser? _parser; + ChatMessageCallback? chatMessageCallback; + ChatMessageCallback? lastSentMessageCallback; ChatErrorMessageCallback? chatErrorMessageCallback; LatestMessageCallback? latestMessageCallback; PrevMessageCallback? prevMessageCallback; @@ -52,58 +39,45 @@ class ChatMessageListener { ChatErrorMessageCallback? chatErrorMessageCallback, LatestMessageCallback? latestMessageCallback, PrevMessageCallback? prevMessageCallback, + ChatMessageCallback? lastSentMessageCallback, void Function()? finishAnswerQuestionCallback, }) { this.chatMessageCallback = chatMessageCallback; this.chatErrorMessageCallback = chatErrorMessageCallback; - this.finishAnswerQuestionCallback = finishAnswerQuestionCallback; this.latestMessageCallback = latestMessageCallback; this.prevMessageCallback = prevMessageCallback; + this.lastSentMessageCallback = lastSentMessageCallback; + this.finishAnswerQuestionCallback = finishAnswerQuestionCallback; } void _callback( ChatNotification ty, FlowyResult result, ) { - switch (ty) { - case ChatNotification.DidReceiveChatMessage: - result.map( - (r) { - final value = ChatMessagePB.fromBuffer(r); - chatMessageCallback?.call(value); - }, - ); - break; - case ChatNotification.ChatMessageError: - result.map( - (r) { - final value = ChatMessageErrorPB.fromBuffer(r); - chatErrorMessageCallback?.call(value); - }, - ); - break; - case ChatNotification.DidLoadLatestChatMessage: - result.map( - (r) { - final value = ChatMessageListPB.fromBuffer(r); - latestMessageCallback?.call(value); - }, - ); - break; - case ChatNotification.DidLoadPrevChatMessage: - result.map( - (r) { - final value = ChatMessageListPB.fromBuffer(r); - prevMessageCallback?.call(value); - }, - ); - break; - case ChatNotification.FinishAnswerQuestion: - finishAnswerQuestionCallback?.call(); - break; - default: - break; - } + result.map((r) { + switch (ty) { + case ChatNotification.DidReceiveChatMessage: + chatMessageCallback?.call(ChatMessagePB.fromBuffer(r)); + break; + case ChatNotification.LastSentMessage: + lastSentMessageCallback?.call(ChatMessagePB.fromBuffer(r)); + break; + case ChatNotification.ChatMessageError: + chatErrorMessageCallback?.call(ChatMessageErrorPB.fromBuffer(r)); + break; + case ChatNotification.DidLoadLatestChatMessage: + latestMessageCallback?.call(ChatMessageListPB.fromBuffer(r)); + break; + case ChatNotification.DidLoadPrevChatMessage: + prevMessageCallback?.call(ChatMessageListPB.fromBuffer(r)); + break; + case ChatNotification.FinishAnswerQuestion: + finishAnswerQuestionCallback?.call(); + break; + default: + break; + } + }); } Future stop() async { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_related_question_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_related_question_bloc.dart new file mode 100644 index 0000000000000..24bf5e56cb014 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_related_question_bloc.dart @@ -0,0 +1,103 @@ +import 'package:appflowy/plugins/ai_chat/application/chat_message_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_related_question_bloc.freezed.dart'; + +class ChatRelatedMessageBloc + extends Bloc { + ChatRelatedMessageBloc({ + required String chatId, + }) : listener = ChatMessageListener(chatId: chatId), + super(ChatRelatedMessageState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + listener.start( + lastSentMessageCallback: (message) { + if (!isClosed) { + add(ChatRelatedMessageEvent.updateLastSentMessage(message)); + } + }, + ); + }, + didReceiveRelatedQuestion: (List questions) { + Log.debug("Related questions: $questions"); + emit( + state.copyWith( + relatedQuestions: questions, + ), + ); + }, + updateLastSentMessage: (ChatMessagePB message) { + final payload = + ChatMessageIdPB(chatId: chatId, messageId: message.messageId); + ChatEventGetRelatedQuestion(payload).send().then((result) { + if (!isClosed) { + result.fold( + (list) { + add( + ChatRelatedMessageEvent.didReceiveRelatedQuestion( + list.items, + ), + ); + }, + (err) { + Log.error("Failed to get related question: $err"); + }, + ); + } + }); + + emit( + state.copyWith( + lastSentMessage: message, + relatedQuestions: [], + ), + ); + }, + clear: () { + emit( + state.copyWith( + relatedQuestions: [], + ), + ); + }, + ); + }, + ); + } + + final ChatMessageListener listener; + @override + Future close() { + listener.stop(); + return super.close(); + } +} + +@freezed +class ChatRelatedMessageEvent with _$ChatRelatedMessageEvent { + const factory ChatRelatedMessageEvent.initial() = Initial; + const factory ChatRelatedMessageEvent.updateLastSentMessage( + ChatMessagePB message, + ) = _LastSentMessage; + const factory ChatRelatedMessageEvent.didReceiveRelatedQuestion( + List questions, + ) = _RelatedQuestion; + const factory ChatRelatedMessageEvent.clear() = _Clear; +} + +@freezed +class ChatRelatedMessageState with _$ChatRelatedMessageState { + const factory ChatRelatedMessageState({ + ChatMessagePB? lastSentMessage, + @Default([]) List relatedQuestions, + }) = _ChatRelatedMessageState; + + factory ChatRelatedMessageState.initial() => const ChatRelatedMessageState(); +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart new file mode 100644 index 0000000000000..d75b0533e2385 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart @@ -0,0 +1,44 @@ +import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_user_message_bloc.freezed.dart'; + +class ChatUserMessageBloc + extends Bloc { + ChatUserMessageBloc({ + required Message message, + }) : super(ChatUserMessageState.initial(message)) { + on( + (event, emit) async { + await event.when( + initial: () async {}, + update: (userProfile, deviceId, states) {}, + ); + }, + ); + } +} + +@freezed +class ChatUserMessageEvent with _$ChatUserMessageEvent { + const factory ChatUserMessageEvent.initial() = Initial; + const factory ChatUserMessageEvent.update( + UserProfilePB userProfile, + String deviceId, + DocumentAwarenessStatesPB states, + ) = Update; +} + +@freezed +class ChatUserMessageState with _$ChatUserMessageState { + const factory ChatUserMessageState({ + required Message message, + WorkspaceMemberPB? member, + }) = _ChatUserMessageState; + + factory ChatUserMessageState.initial(Message message) => + ChatUserMessageState(message: message); +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 0bb055b74a741..273d3e1445199 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,5 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_ai_message.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_error_message.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_user_message.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -14,9 +17,9 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'presentation/chat_input.dart'; -import 'presentation/chat_message_hover.dart'; import 'presentation/chat_popmenu.dart'; import 'presentation/chat_theme.dart'; +import 'presentation/chat_welcome_page.dart'; class AIChatPage extends StatefulWidget { const AIChatPage({ @@ -55,7 +58,7 @@ class _AIChatPageState extends State { Widget buildChatWidget() { return SizedBox.expand( child: Padding( - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.symmetric(horizontal: 60), child: BlocProvider( create: (context) => ChatBloc( view: widget.view, @@ -78,13 +81,7 @@ class _AIChatPageState extends State { customBottomWidget: buildChatInput(blocContext), user: _user, theme: buildTheme(context), - customMessageBuilder: (message, {required messageWidth}) { - return const SizedBox( - width: 100, - height: 50, - child: CircularProgressIndicator.adaptive(), - ); - }, + customMessageBuilder: _customMessageBuilder, onEndReached: () async { if (state.hasMore && state.loadingPreviousStatus != @@ -94,16 +91,47 @@ class _AIChatPageState extends State { .add(const ChatEvent.loadPrevMessage()); } }, - emptyState: const Center( - child: CircularProgressIndicator.adaptive(), + emptyState: BlocBuilder( + builder: (context, state) { + return state.loadingStatus == const LoadingState.finish() + ? const ChatWelcomePage() + : const Center( + child: CircularProgressIndicator.adaptive(), + ); + }, ), - messageWidthRatio: 0.7, + messageWidthRatio: isMobile ? 0.8 : 0.9, bubbleBuilder: ( child, { required message, required nextMessageInGroup, - }) => - buildBubble(message, child), + }) { + if (message.author.id == _user.id) { + return ChatUserMessageBubble( + message: message, + child: child, + ); + } else { + final messageType = + restoreOnetimeMessageType(message.metadata); + + if (messageType == OnetimeMessageType.serverStreamError) { + return ChatErrorMessage( + onRetryPressed: () { + blocContext.read().add( + const ChatEvent.retryGenerate(), + ); + }, + ); + } + + return ChatAIMessageBubble( + message: message, + customMessageType: messageType, + child: child, + ); + } + }, ); }, ), @@ -123,40 +151,85 @@ class _AIChatPageState extends State { Widget buildBubble(Message message, Widget child) { final isAuthor = message.author.id == _user.id; - const borderRadius = BorderRadius.all(Radius.circular(20)); - final decoratedChild = DecoratedBox( - decoration: BoxDecoration( - borderRadius: borderRadius, - color: !isAuthor || message.type == types.MessageType.image - ? AFThemeExtension.of(context).tint1 - : Theme.of(context).colorScheme.secondary, - ), - child: child, - ); + const borderRadius = BorderRadius.all(Radius.circular(6)); - if (isMobile) { - return ChatPopupMenu( - onAction: (action) { - switch (action) { - case ChatMessageAction.copy: - if (message is TextMessage) { - Clipboard.setData(ClipboardData(text: message.text)); - showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); - } - break; - } - }, - builder: (context) => - ClipRRect(borderRadius: borderRadius, child: decoratedChild), - ); - } else { + final childWithPadding = isAuthor + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: child, + ) + : Padding( + padding: const EdgeInsets.all(8), + child: child, + ); + + // If the message is from the author, we will decorate it with a different color + final decoratedChild = isAuthor + ? DecoratedBox( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: !isAuthor || message.type == types.MessageType.image + ? AFThemeExtension.of(context).tint1 + : Theme.of(context).colorScheme.secondary, + ), + child: childWithPadding, + ) + : childWithPadding; + + // If the message is from the author, no further actions are needed + if (isAuthor) { return ClipRRect( borderRadius: borderRadius, - child: ChatMessageHover( - message: message, - child: decoratedChild, - ), + child: decoratedChild, ); + } else { + if (isMobile) { + return ChatPopupMenu( + onAction: (action) { + switch (action) { + case ChatMessageAction.copy: + if (message is TextMessage) { + Clipboard.setData(ClipboardData(text: message.text)); + showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); + } + break; + } + }, + builder: (context) => + ClipRRect(borderRadius: borderRadius, child: decoratedChild), + ); + } else { + // Show hover effect only on desktop + return ClipRRect( + borderRadius: borderRadius, + child: ChatAIMessageHover( + message: message, + child: decoratedChild, + ), + ); + } + } + } + + Widget _customMessageBuilder( + types.CustomMessage message, { + required int messageWidth, + }) { + // iteration custom message type + final messageType = restoreOnetimeMessageType(message.metadata); + if (messageType == null) { + return const SizedBox.shrink(); + } + + switch (messageType) { + case OnetimeMessageType.loading: + return const SizedBox( + width: 50, + height: 30, + child: CircularProgressIndicator.adaptive(), + ); + default: + return const SizedBox.shrink(); } } @@ -170,10 +243,16 @@ class _AIChatPageState extends State { query.viewInsets.bottom + query.padding.bottom, ) : EdgeInsets.zero; - return Padding( - padding: safeAreaInsets, - child: ChatInput( - onSendPressed: (message) => onSendPressed(context, message), + return ClipRect( + child: Padding( + padding: safeAreaInsets, + child: ChatInput( + chatId: widget.view.id, + onSendPressed: (message) => onSendPressed(context, message.text), + onQuestionSelected: (question) { + onSendPressed(context, question); + }, + ), ), ); } @@ -224,7 +303,7 @@ class _AIChatPageState extends State { ); } - void onSendPressed(BuildContext context, types.PartialText message) { - context.read().add(ChatEvent.sendMessage(message.text)); + void onSendPressed(BuildContext context, String message) { + context.read().add(ChatEvent.sendMessage(message)); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_ai_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_ai_message.dart new file mode 100644 index 0000000000000..39e971f4dd18d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_ai_message.dart @@ -0,0 +1,197 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:styled_widget/styled_widget.dart'; + +const _leftPadding = 16.0; + +class ChatAIMessageBubble extends StatelessWidget { + const ChatAIMessageBubble({ + super.key, + required this.message, + required this.child, + this.customMessageType, + }); + + final Message message; + final Widget child; + final OnetimeMessageType? customMessageType; + + @override + Widget build(BuildContext context) { + const padding = EdgeInsets.symmetric(horizontal: _leftPadding); + final childWithPadding = Padding(padding: padding, child: child); + + return BlocProvider( + create: (context) => ChatAIMessageBloc(message: message), + child: BlocBuilder( + builder: (context, state) { + final widget = isMobile + ? _wrapPopMenu(childWithPadding) + : _wrapHover(childWithPadding); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ChatBorderedCircleAvatar( + child: FlowySvg( + FlowySvgs.flowy_logo_xl, + blendMode: null, + size: Size.square(24), + ), + ), + Expanded(child: widget), + ], + ); + }, + ), + ); + } + + ChatAIMessageHover _wrapHover(Padding child) { + return ChatAIMessageHover( + message: message, + customMessageType: customMessageType, + child: child, + ); + } + + ChatPopupMenu _wrapPopMenu(Padding childWithPadding) { + return ChatPopupMenu( + onAction: (action) { + if (action == ChatMessageAction.copy && message is TextMessage) { + Clipboard.setData(ClipboardData(text: (message as TextMessage).text)); + showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); + } + }, + builder: (context) => childWithPadding, + ); + } +} + +class ChatAIMessageHover extends StatefulWidget { + const ChatAIMessageHover({ + super.key, + required this.child, + required this.message, + this.customMessageType, + }); + + final Widget child; + final Message message; + final bool autoShowHover = true; + final OnetimeMessageType? customMessageType; + + @override + State createState() => _ChatAIMessageHoverState(); +} + +class _ChatAIMessageHoverState extends State { + bool _isHover = false; + + @override + void initState() { + super.initState(); + _isHover = widget.autoShowHover ? false : true; + } + + @override + Widget build(BuildContext context) { + final List children = [ + DecoratedBox( + decoration: const BoxDecoration( + color: Colors.transparent, + borderRadius: Corners.s6Border, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 40), + child: widget.child, + ), + ), + ]; + + if (_isHover) { + children.addAll(_buildOnHoverItems()); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => setState(() { + if (widget.autoShowHover) { + _isHover = true; + } + }), + onExit: (p) => setState(() { + if (widget.autoShowHover) { + _isHover = false; + } + }), + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: children, + ), + ); + } + + List _buildOnHoverItems() { + final List children = []; + if (widget.customMessageType != null) { + // + } else { + if (widget.message is TextMessage) { + children.add( + CopyButton( + textMessage: widget.message as TextMessage, + ).positioned(left: _leftPadding, bottom: 0), + ); + } + } + + return children; + } +} + +class CopyButton extends StatelessWidget { + const CopyButton({ + super.key, + required this.textMessage, + }); + final TextMessage textMessage; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.settings_menu_clickToCopy.tr(), + child: FlowyIconButton( + width: 24, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + fillColor: Theme.of(context).cardColor, + icon: FlowySvg( + FlowySvgs.ai_copy_s, + size: const Size.square(14), + color: Theme.of(context).colorScheme.primary, + ), + onPressed: () { + Clipboard.setData(ClipboardData(text: textMessage.text)); + showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart new file mode 100644 index 0000000000000..141432520bb76 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/built_in_svgs.dart'; +import 'package:appflowy/util/color_generator/color_generator.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:string_validator/string_validator.dart'; + +class ChatChatUserAvatar extends StatelessWidget { + const ChatChatUserAvatar({required this.userId, super.key}); + + final String userId; + + @override + Widget build(BuildContext context) { + return const ChatBorderedCircleAvatar(); + } +} + +class ChatBorderedCircleAvatar extends StatelessWidget { + const ChatBorderedCircleAvatar({ + super.key, + this.border = const BorderSide(), + this.backgroundImage, + this.backgroundColor, + this.child, + }); + + final BorderSide border; + final ImageProvider? backgroundImage; + final Color? backgroundColor; + final Widget? child; + + @override + Widget build(BuildContext context) { + return CircleAvatar( + backgroundColor: border.color, + child: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: CircleAvatar( + backgroundImage: backgroundImage, + backgroundColor: backgroundColor, + child: child, + ), + ), + ); + } +} + +class ChatUserAvatar extends StatelessWidget { + const ChatUserAvatar({ + super.key, + required this.iconUrl, + required this.name, + required this.size, + this.isHovering = false, + }); + + final String iconUrl; + final String name; + final double size; + + // If true, a border will be applied on top of the avatar + final bool isHovering; + + @override + Widget build(BuildContext context) { + if (iconUrl.isEmpty) { + return _buildEmptyAvatar(context); + } else if (isURL(iconUrl)) { + return _buildUrlAvatar(context); + } else { + return _buildEmojiAvatar(context); + } + } + + Widget _buildEmptyAvatar(BuildContext context) { + final String nameOrDefault = _userName(name); + final Color color = ColorGenerator(name).toColor(); + const initialsCount = 2; + + // Taking the first letters of the name components and limiting to 2 elements + final nameInitials = nameOrDefault + .split(' ') + .where((element) => element.isNotEmpty) + .take(initialsCount) + .map((element) => element[0].toUpperCase()) + .join(); + + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: _darken(color), + width: 4, + ) + : null, + ), + child: FlowyText.regular( + nameInitials, + color: Colors.black, + ), + ); + } + + Widget _buildUrlAvatar(BuildContext context) { + return SizedBox.square( + dimension: size, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 4, + ) + : null, + ), + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: Image.network( + iconUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildEmptyAvatar(context), + ), + ), + ), + ), + ); + } + + Widget _buildEmojiAvatar(BuildContext context) { + return SizedBox.square( + dimension: size, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 4, + ) + : null, + ), + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: builtInSVGIcons.contains(iconUrl) + ? FlowySvg( + FlowySvgData('emoji/$iconUrl'), + blendMode: null, + ) + : FlowyText.emoji(iconUrl), + ), + ), + ), + ); + } + + /// Return the user name, if the user name is empty, + /// return the default user name. + /// + String _userName(String name) => + name.isEmpty ? LocaleKeys.defaultUsername.tr() : name; + + /// Used to darken the generated color for the hover border effect. + /// The color is darkened by 15% - Hence the 0.15 value. + /// + Color _darken(Color color) { + final hsl = HSLColor.fromColor(color); + return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_error_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_error_message.dart new file mode 100644 index 0000000000000..4846df1487476 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_error_message.dart @@ -0,0 +1,43 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; + +class ChatErrorMessage extends StatelessWidget { + const ChatErrorMessage({required this.onRetryPressed, super.key}); + + final void Function() onRetryPressed; + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Divider(height: 4, thickness: 1), + const VSpace(16), + Center( + child: FlowyTooltip( + message: LocaleKeys.chat_clickToRetry.tr(), + child: FlowyButton( + useIntrinsicWidth: true, + text: Padding( + padding: const EdgeInsets.all(8.0), + child: FlowyText( + LocaleKeys.chat_serverUnavailable.tr(), + fontSize: 14, + ), + ), + onTap: () { + onRetryPressed(); + }, + iconPadding: 0, + rightIcon: const Icon( + Icons.refresh, + size: 20, + ), + ), + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart index 0d091d61586fd..88ff3821021e7 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart @@ -1,3 +1,5 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -5,6 +7,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'chat_related_question.dart'; + class ChatInput extends StatefulWidget { /// Creates [ChatInput] widget. const ChatInput({ @@ -12,6 +16,8 @@ class ChatInput extends StatefulWidget { this.isAttachmentUploading, this.onAttachmentPressed, required this.onSendPressed, + required this.chatId, + required this.onQuestionSelected, this.options = const InputOptions(), }); @@ -19,6 +25,8 @@ class ChatInput extends StatefulWidget { final VoidCallback? onAttachmentPressed; final void Function(types.PartialText) onSendPressed; final InputOptions options; + final String chatId; + final Function(String) onQuestionSelected; @override State createState() => _ChatInputState(); @@ -110,57 +118,23 @@ class _ChatInputState extends State { ? Theme.of(context).colorScheme.surfaceContainer : Theme.of(context).colorScheme.surfaceContainerHighest, elevation: 0.6, - child: Row( - textDirection: TextDirection.ltr, + child: Column( children: [ - if (widget.onAttachmentPressed != null) - AttachmentButton( - isLoading: widget.isAttachmentUploading ?? false, - onPressed: widget.onAttachmentPressed, - padding: buttonPadding, - ), - Expanded( - child: Padding( - padding: textPadding, - child: TextField( - enabled: widget.options.enabled, - autocorrect: widget.options.autocorrect, - autofocus: widget.options.autofocus, - enableSuggestions: widget.options.enableSuggestions, - controller: _textController, - decoration: InputDecoration( - border: InputBorder.none, - hintText: '', - hintStyle: TextStyle( - color: AFThemeExtension.of(context) - .textColor - .withOpacity(0.5), - ), - ), - focusNode: _inputFocusNode, - keyboardType: widget.options.keyboardType, - maxLines: 5, - minLines: 1, - onChanged: widget.options.onTextChanged, - onTap: widget.options.onTextFieldTap, - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - ), - textCapitalization: TextCapitalization.sentences, - ), - ), + RelatedQuestionList( + chatId: widget.chatId, + onQuestionSelected: widget.onQuestionSelected, ), - ConstrainedBox( - constraints: BoxConstraints( - minHeight: buttonPadding.bottom + buttonPadding.top + 24, - ), - child: Visibility( - visible: _sendButtonVisible, - child: SendButton( - onPressed: _handleSendPressed, - padding: buttonPadding, - ), - ), + Row( + children: [ + if (widget.onAttachmentPressed != null) + AttachmentButton( + isLoading: widget.isAttachmentUploading ?? false, + onPressed: widget.onAttachmentPressed, + padding: buttonPadding, + ), + Expanded(child: _inputTextField(textPadding)), + _sendButton(buttonPadding), + ], ), ], ), @@ -169,6 +143,51 @@ class _ChatInputState extends State { ); } + Padding _inputTextField(EdgeInsets textPadding) { + return Padding( + padding: textPadding, + child: TextField( + enabled: widget.options.enabled, + autocorrect: widget.options.autocorrect, + autofocus: widget.options.autofocus, + enableSuggestions: widget.options.enableSuggestions, + controller: _textController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: LocaleKeys.chat_inputMessageHint.tr(), + hintStyle: TextStyle( + color: AFThemeExtension.of(context).textColor.withOpacity(0.5), + ), + ), + focusNode: _inputFocusNode, + keyboardType: widget.options.keyboardType, + maxLines: 5, + minLines: 1, + onChanged: widget.options.onTextChanged, + onTap: widget.options.onTextFieldTap, + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + ), + textCapitalization: TextCapitalization.sentences, + ), + ); + } + + ConstrainedBox _sendButton(EdgeInsets buttonPadding) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: buttonPadding.bottom + buttonPadding.top + 24, + ), + child: Visibility( + visible: _sendButtonVisible, + child: SendButton( + onPressed: _handleSendPressed, + padding: buttonPadding, + ), + ), + ); + } + @override void didUpdateWidget(covariant ChatInput oldWidget) { super.didUpdateWidget(oldWidget); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_hover.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_hover.dart deleted file mode 100644 index 4a1f59cbdd8c7..0000000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_hover.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_chat_types/flutter_chat_types.dart'; -import 'package:styled_widget/styled_widget.dart'; - -class ChatMessageHover extends StatefulWidget { - const ChatMessageHover({ - super.key, - required this.child, - required this.message, - }); - - final Widget child; - final Message message; - - @override - State createState() => _ChatMessageHoverState(); -} - -class _ChatMessageHoverState extends State { - bool _isHover = false; - - @override - Widget build(BuildContext context) { - final List children = [ - DecoratedBox( - decoration: BoxDecoration( - color: _isHover - ? AFThemeExtension.of(context).lightGreyHover - : Colors.transparent, - borderRadius: Corners.s6Border, - ), - child: widget.child, - ), - ]; - - if (_isHover) { - if (widget.message is TextMessage) { - children.add( - Padding( - padding: const EdgeInsets.only(right: 6), - child: CopyButton( - textMessage: widget.message as TextMessage, - ), - ).positioned(top: 12, left: 12), - ); - } - } - - return MouseRegion( - cursor: SystemMouseCursors.click, - opaque: false, - onEnter: (p) => setState(() => _isHover = true), - onExit: (p) => setState(() => _isHover = false), - child: Stack( - alignment: AlignmentDirectional.center, - children: children, - ), - ); - } -} - -class CopyButton extends StatelessWidget { - const CopyButton({ - super.key, - required this.textMessage, - }); - final TextMessage textMessage; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.settings_menu_clickToCopy.tr(), - child: Container( - width: 26, - height: 26, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).dividerColor), - ), - borderRadius: Corners.s6Border, - ), - child: FlowyIconButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - fillColor: Theme.of(context).cardColor, - icon: FlowySvg( - FlowySvgs.ai_copy_s, - color: Theme.of(context).colorScheme.primary, - ), - onPressed: () { - Clipboard.setData(ClipboardData(text: textMessage.text)); - showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart new file mode 100644 index 0000000000000..826bf77e80e5e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart @@ -0,0 +1,81 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_related_question_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class RelatedQuestionList extends StatefulWidget { + const RelatedQuestionList({ + required this.chatId, + required this.onQuestionSelected, + super.key, + }); + + final String chatId; + final Function(String) onQuestionSelected; + + @override + State createState() => _RelatedQuestionListState(); +} + +class _RelatedQuestionListState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ChatRelatedMessageBloc(chatId: widget.chatId) + ..add( + const ChatRelatedMessageEvent.initial(), + ), + child: BlocBuilder( + builder: (blocContext, state) { + if (state.relatedQuestions.isEmpty) { + return const SizedBox.shrink(); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const FlowySvg( + FlowySvgs.ai_summary_generate_s, + size: Size.square(24), + ), + const HSpace(6), + FlowyText( + LocaleKeys.chat_relatedQuestion.tr(), + fontSize: 18, + ), + ], + ), + ), + const Divider(height: 6), + ListView.builder( + shrinkWrap: true, + itemCount: state.relatedQuestions.length, + itemBuilder: (context, index) { + final question = state.relatedQuestions[index]; + return ListTile( + title: Text(question.content), + onTap: () { + widget.onQuestionSelected(question.content); + blocContext.read().add( + const ChatRelatedMessageEvent.clear(), + ); + }, + trailing: const FlowySvg(FlowySvgs.add_m), + ); + }, + ), + ], + ); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_theme.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_theme.dart index f3bad391bd5f9..456ac0c184391 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_theme.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_theme.dart @@ -100,8 +100,8 @@ class AFDefaultChatTheme extends ChatTheme { height: 1.5, ), super.messageBorderRadius = 20, - super.messageInsetsHorizontal = 20, - super.messageInsetsVertical = 16, + super.messageInsetsHorizontal = 0, + super.messageInsetsVertical = 0, super.messageMaxWidth = 1000, super.primaryColor = primary, super.receivedEmojiMessageTextStyle = const TextStyle(fontSize: 40), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_user_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_user_message.dart new file mode 100644 index 0000000000000..8e972315071dd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_user_message.dart @@ -0,0 +1,174 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class ChatUserMessageBubble extends StatelessWidget { + const ChatUserMessageBubble({ + super.key, + required this.message, + required this.child, + }); + + final Message message; + final Widget child; + + @override + Widget build(BuildContext context) { + const borderRadius = BorderRadius.all(Radius.circular(6)); + final backgroundColor = Theme.of(context).colorScheme.secondary; + + return BlocProvider( + create: (context) => ChatUserMessageBloc(message: message), + child: BlocBuilder( + builder: (context, state) { + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _wrapHover( + DecoratedBox( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: backgroundColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: child, + ), + ), + ), + BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ChatUserAvatar( + iconUrl: state.member?.avatarUrl ?? "", + name: state.member?.name ?? "", + size: 36, + ), + ); + }, + ), + ], + ); + }, + ), + ); + } + + ChatUserMessageHover _wrapHover(Widget child) { + return ChatUserMessageHover( + message: message, + child: child, + ); + } +} + +class ChatUserMessageHover extends StatefulWidget { + const ChatUserMessageHover({ + super.key, + required this.child, + required this.message, + }); + + final Widget child; + final Message message; + final bool autoShowHover = true; + + @override + State createState() => _ChatUserMessageHoverState(); +} + +class _ChatUserMessageHoverState extends State { + bool _isHover = false; + + @override + void initState() { + super.initState(); + _isHover = widget.autoShowHover ? false : true; + } + + @override + Widget build(BuildContext context) { + final List children = [ + DecoratedBox( + decoration: const BoxDecoration( + color: Colors.transparent, + borderRadius: Corners.s6Border, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: widget.child, + ), + ), + ]; + + if (_isHover) { + if (widget.message is TextMessage) { + children.add( + EditButton( + textMessage: widget.message as TextMessage, + ).positioned(right: 0, bottom: 0), + ); + } + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => setState(() { + if (widget.autoShowHover) { + _isHover = true; + } + }), + onExit: (p) => setState(() { + if (widget.autoShowHover) { + _isHover = false; + } + }), + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: children, + ), + ); + } +} + +class EditButton extends StatelessWidget { + const EditButton({ + super.key, + required this.textMessage, + }); + final TextMessage textMessage; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.settings_menu_clickToCopy.tr(), + child: FlowyIconButton( + width: 24, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + fillColor: Theme.of(context).cardColor, + icon: FlowySvg( + FlowySvgs.ai_copy_s, + size: const Size.square(14), + color: Theme.of(context).colorScheme.primary, + ), + onPressed: () {}, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart new file mode 100644 index 0000000000000..e6fc55a15a130 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class ChatWelcomePage extends StatelessWidget { + const ChatWelcomePage({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index acd628a22204f..e99ee90f56a13 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -106,7 +106,7 @@ class FlowyButton extends StatelessWidget { } if (rightIcon != null) { - children.add(const HSpace(6)); + children.add(HSpace(iconPadding)); // No need to define the size of rightIcon. Just use its intrinsic width children.add(rightIcon!); } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 9c69e8db590fb..38252652c6d09 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -162,7 +162,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "bincode", @@ -182,9 +182,11 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", + "bytes", + "futures", "serde", "serde_json", "serde_repr", @@ -543,9 +545,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" dependencies = [ "serde", ] @@ -755,7 +757,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "again", "anyhow", @@ -802,7 +804,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "futures-channel", "futures-util", @@ -1041,7 +1043,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "bincode", @@ -1066,7 +1068,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "async-trait", @@ -1312,7 +1314,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1423,7 +1425,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -2831,7 +2833,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "futures-util", @@ -2848,7 +2850,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -3280,7 +3282,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "reqwest", @@ -4783,7 +4785,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -4804,7 +4806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.47", @@ -5768,7 +5770,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 382f3332d27d4..82fa349dd107b 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "559d924cd182d8db16df69c030c96e63153988f7" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "98534de86557e3f1166cd8fce19314f3138426f0" } [dependencies] serde_json.workspace = true diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index c696b6929bd2c..d0c36ed64dcd5 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -216,7 +216,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "bincode", @@ -236,9 +236,11 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", + "bytes", + "futures", "serde", "serde_json", "serde_repr", @@ -440,9 +442,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" dependencies = [ "serde", ] @@ -560,7 +562,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "again", "anyhow", @@ -607,7 +609,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "futures-channel", "futures-util", @@ -785,7 +787,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "bincode", @@ -810,7 +812,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "async-trait", @@ -1024,7 +1026,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -1813,7 +1815,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "futures-util", @@ -1830,7 +1832,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -2131,7 +2133,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "reqwest", @@ -3771,7 +3773,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -4262,6 +4264,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 77c639f044fd8..c33b10f75da18 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -55,7 +55,7 @@ yrs = "0.18.8" # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "559d924cd182d8db16df69c030c96e63153988f7" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "98534de86557e3f1166cd8fce19314f3138426f0" } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 312ca437efafe..ae8c8e40b9450 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -153,7 +153,7 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "bincode", @@ -173,9 +173,11 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", + "bytes", + "futures", "serde", "serde_json", "serde_repr", @@ -197,6 +199,7 @@ dependencies = [ "flowy-notification", "flowy-user", "lib-dispatch", + "semver", "serde", "serde_json", "tauri", @@ -727,7 +730,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "again", "anyhow", @@ -774,7 +777,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "futures-channel", "futures-util", @@ -1022,7 +1025,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "bincode", @@ -1047,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "async-trait", @@ -1408,7 +1411,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -1507,6 +1510,7 @@ dependencies = [ "diesel_derives", "libsqlite3-sys", "r2d2", + "serde_json", "time", ] @@ -2282,6 +2286,7 @@ dependencies = [ "postgrest", "rand 0.8.5", "reqwest", + "semver", "serde", "serde_json", "thiserror", @@ -2901,7 +2906,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "futures-util", @@ -2918,7 +2923,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -3355,7 +3360,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "reqwest", @@ -5666,9 +5671,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] @@ -5859,7 +5864,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index aea296b1168a9..d92053ea4421e 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "559d924cd182d8db16df69c030c96e63153988f7" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "98534de86557e3f1166cd8fce19314f3138426f0" } [dependencies] serde_json.workspace = true diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a07586a3bc76d..430a17f566aee 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -149,7 +149,12 @@ "newBoardText": "New board", "chat": { "newChat": "New chat", - "unsupportedCloudPrompt": "This feature is only available when using AppFlowy Cloud" + "inputMessageHint": "Message", + "unsupportedCloudPrompt": "This feature is only available when using AppFlowy Cloud", + "relatedQuestion": "Related", + "serverUnavailable": "Service Temporarily Unavailable", + "clickToRetry": "Click to retry", + "aiMistakePrompt": "AI can make mistakes. Check important info." }, "trash": { "text": "Trash", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 36b675ac9f5bd..395f2c00625ad 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "bincode", @@ -183,9 +183,11 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", + "bytes", + "futures", "serde", "serde_json", "serde_repr", @@ -542,9 +544,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" dependencies = [ "serde", ] @@ -662,7 +664,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "again", "anyhow", @@ -709,7 +711,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "futures-channel", "futures-util", @@ -917,7 +919,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "bincode", @@ -942,7 +944,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "async-trait", @@ -1262,7 +1264,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -2516,7 +2518,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "futures-util", @@ -2533,7 +2535,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", @@ -2898,7 +2900,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "reqwest", @@ -4981,7 +4983,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=559d924cd182d8db16df69c030c96e63153988f7#559d924cd182d8db16df69c030c96e63153988f7" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=98534de86557e3f1166cd8fce19314f3138426f0#98534de86557e3f1166cd8fce19314f3138426f0" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index bb81863be9027..612785200a507 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -93,7 +93,7 @@ yrs = "0.18.8" # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "559d924cd182d8db16df69c030c96e63153988f7" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "98534de86557e3f1166cd8fce19314f3138426f0" } [profile.dev] opt-level = 1 diff --git a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs index 36ca710d321ee..42ab020dd1ab9 100644 --- a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs @@ -1,3 +1,4 @@ +pub use client_api::entity::ai_dto::{RelatedQuestion, RepeatedRelatedQuestion}; pub use client_api::entity::{ ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage, }; @@ -32,4 +33,11 @@ pub trait ChatCloudService: Send + Sync + 'static { offset: MessageCursor, limit: u64, ) -> FutureResult; + + fn get_related_message( + &self, + workspace_id: &str, + chat_id: &str, + message_id: i64, + ) -> FutureResult; } diff --git a/frontend/rust-lib/flowy-chat/src/chat.rs b/frontend/rust-lib/flowy-chat/src/chat.rs index c850f71100e6a..a87901bcba584 100644 --- a/frontend/rust-lib/flowy-chat/src/chat.rs +++ b/frontend/rust-lib/flowy-chat/src/chat.rs @@ -1,4 +1,6 @@ -use crate::entities::{ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB}; +use crate::entities::{ + ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB, +}; use crate::manager::ChatUserService; use crate::notification::{send_notification, ChatNotification}; use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable}; @@ -124,7 +126,7 @@ impl Chat { .load_remote_chat_messages(limit, before_message_id, None) .await { - error!("Failed to load chat messages: {}", err); + error!("Failed to load previous chat messages: {}", err); } } @@ -244,6 +246,25 @@ impl Chat { Ok(()) } + pub async fn get_related_question( + &self, + message_id: i64, + ) -> Result { + let workspace_id = self.user_service.workspace_id()?; + let resp = self + .cloud_service + .get_related_message(&workspace_id, &self.chat_id, message_id) + .await?; + + trace!( + "Related messages: chat_id={}, message_id={}, messages:{:?}", + self.chat_id, + message_id, + resp.items + ); + Ok(RepeatedRelatedQuestionPB::from(resp)) + } + async fn load_local_chat_messages( &self, limit: i64, @@ -297,7 +318,7 @@ fn stream_send_chat_messages( .send_chat_message(&workspace_id, &chat_id, &message_content, message_type) .await; - let mut reply_message_id = None; + let mut user_asked_question = None; // By default, stream only returns two messages: // 1. user message @@ -308,11 +329,16 @@ fn stream_send_chat_messages( match result { Ok(message) => { let mut pb = ChatMessagePB::from(message.clone()); - if reply_message_id.is_none() { + if user_asked_question.is_none() { pb.has_following = true; - reply_message_id = Some(pb.message_id); + user_asked_question = Some(pb.clone()); } else { - pb.reply_message_id = reply_message_id; + pb.reply_message_id = user_asked_question.as_ref().map(|m| m.message_id); + if let Some(user_asked_question) = &user_asked_question { + send_notification(&chat_id, ChatNotification::LastSentMessage) + .payload(user_asked_question.clone()) + .send(); + } } send_notification(&chat_id, ChatNotification::DidReceiveChatMessage) .payload(pb) @@ -369,7 +395,7 @@ fn stream_send_chat_messages( created_at: message.created_at.timestamp(), author_type: message.author.author_type as i64, author_id: message.author.author_id.to_string(), - reply_message_id, + reply_message_id: user_asked_question.as_ref().map(|m| m.message_id), }) .collect::>(); insert_chat_messages(conn, &records)?; diff --git a/frontend/rust-lib/flowy-chat/src/entities.rs b/frontend/rust-lib/flowy-chat/src/entities.rs index cab3b90847758..e01c2aaad1bc4 100644 --- a/frontend/rust-lib/flowy-chat/src/entities.rs +++ b/frontend/rust-lib/flowy-chat/src/entities.rs @@ -1,4 +1,6 @@ -use flowy_chat_pub::cloud::{ChatMessage, RepeatedChatMessage}; +use flowy_chat_pub::cloud::{ + ChatMessage, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, +}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; use validator::Validate; @@ -141,3 +143,48 @@ impl From> for RepeatedChatMessagePB { } } } + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct ChatMessageIdPB { + #[pb(index = 1)] + pub chat_id: String, + + #[pb(index = 2)] + pub message_id: i64, +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct RelatedQuestionPB { + #[pb(index = 1)] + pub content: String, +} + +impl From for RelatedQuestionPB { + fn from(value: RelatedQuestion) -> Self { + RelatedQuestionPB { + content: value.content, + } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct RepeatedRelatedQuestionPB { + #[pb(index = 1)] + pub message_id: i64, + + #[pb(index = 2)] + pub items: Vec, +} + +impl From for RepeatedRelatedQuestionPB { + fn from(value: RepeatedRelatedQuestion) -> Self { + RepeatedRelatedQuestionPB { + message_id: value.message_id, + items: value + .items + .into_iter() + .map(RelatedQuestionPB::from) + .collect(), + } + } +} diff --git a/frontend/rust-lib/flowy-chat/src/event_handler.rs b/frontend/rust-lib/flowy-chat/src/event_handler.rs index 6f2db442770f3..607ee8e5dfd0b 100644 --- a/frontend/rust-lib/flowy-chat/src/event_handler.rs +++ b/frontend/rust-lib/flowy-chat/src/event_handler.rs @@ -65,3 +65,16 @@ pub(crate) async fn load_next_message_handler( .await?; data_result_ok(messages) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_related_question_handler( + data: AFPluginData, + chat_manager: AFPluginState>, +) -> DataResult { + let chat_manager = upgrade_chat_manager(chat_manager)?; + let data = data.into_inner(); + let messages = chat_manager + .get_related_questions(&data.chat_id, data.message_id) + .await?; + data_result_ok(messages) +} diff --git a/frontend/rust-lib/flowy-chat/src/event_map.rs b/frontend/rust-lib/flowy-chat/src/event_map.rs index 41dd0db445176..b2ea726394ce0 100644 --- a/frontend/rust-lib/flowy-chat/src/event_map.rs +++ b/frontend/rust-lib/flowy-chat/src/event_map.rs @@ -15,6 +15,7 @@ pub fn init(chat_manager: Weak) -> AFPlugin { .event(ChatEvent::SendMessage, send_chat_message_handler) .event(ChatEvent::LoadPrevMessage, load_prev_message_handler) .event(ChatEvent::LoadNextMessage, load_next_message_handler) + .event(ChatEvent::GetRelatedQuestion, get_related_question_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -29,4 +30,7 @@ pub enum ChatEvent { #[event(input = "SendChatPayloadPB")] SendMessage = 2, + + #[event(input = "ChatMessageIdPB", output = "RepeatedRelatedQuestionPB")] + GetRelatedQuestion = 3, } diff --git a/frontend/rust-lib/flowy-chat/src/manager.rs b/frontend/rust-lib/flowy-chat/src/manager.rs index 1bc2f63b4f2ca..67cec38761555 100644 --- a/frontend/rust-lib/flowy-chat/src/manager.rs +++ b/frontend/rust-lib/flowy-chat/src/manager.rs @@ -1,5 +1,5 @@ use crate::chat::Chat; -use crate::entities::ChatMessageListPB; +use crate::entities::{ChatMessageListPB, RepeatedRelatedQuestionPB}; use crate::persistence::{insert_chat, ChatTable}; use dashmap::DashMap; use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageType}; @@ -148,6 +148,16 @@ impl ChatManager { .await?; Ok(list) } + + pub async fn get_related_questions( + &self, + chat_id: &str, + message_id: i64, + ) -> Result { + let chat = self.get_or_create_chat_instance(chat_id).await?; + let resp = chat.get_related_question(message_id).await?; + Ok(resp) + } } fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-chat/src/notification.rs b/frontend/rust-lib/flowy-chat/src/notification.rs index 15918d57be378..c13936c667075 100644 --- a/frontend/rust-lib/flowy-chat/src/notification.rs +++ b/frontend/rust-lib/flowy-chat/src/notification.rs @@ -12,6 +12,7 @@ pub enum ChatNotification { DidReceiveChatMessage = 3, ChatMessageError = 4, FinishAnswerQuestion = 5, + LastSentMessage = 6, } impl std::convert::From for i32 { @@ -27,6 +28,7 @@ impl std::convert::From for ChatNotification { 3 => ChatNotification::DidReceiveChatMessage, 4 => ChatNotification::ChatMessageError, 5 => ChatNotification::FinishAnswerQuestion, + 6 => ChatNotification::LastSentMessage, _ => ChatNotification::Unknown, } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index b5a66e9fd5d2f..4e02d26068689 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Error; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; +use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::ChatMessageType; use collab::core::origin::{CollabClient, CollabOrigin}; @@ -492,4 +493,21 @@ impl ChatCloudService for ServerProvider { .await }) } + + fn get_related_message( + &self, + workspace_id: &str, + chat_id: &str, + message_id: i64, + ) -> FutureResult { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .chat_service() + .get_related_message(&workspace_id, &chat_id, message_id) + .await + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 88a3aa64ec2c5..673586979b2e2 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -1,4 +1,5 @@ use crate::af_cloud::AFServer; +use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::{ CreateChatMessageParams, CreateChatParams, MessageCursor, RepeatedChatMessage, }; @@ -85,4 +86,24 @@ where Ok(resp) }) } + + fn get_related_message( + &self, + workspace_id: &str, + chat_id: &str, + message_id: i64, + ) -> FutureResult { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let try_get_client = self.inner.try_get_client(); + + FutureResult::new(async move { + let resp = try_get_client? + .get_chat_related_question(&workspace_id, &chat_id, message_id) + .await + .map_err(FlowyError::from)?; + + Ok(resp) + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index c24ddbb51eead..a33852dd53c1e 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -89,6 +89,7 @@ pub fn from_af_workspace_member(member: AFWorkspaceMember) -> WorkspaceMember { email: member.email, role: from_af_role(member.role), name: member.name, + avatar_url: member.avatar_url, } } diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index d34f4a0d24bca..c1cc631e8017c 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -1,3 +1,4 @@ +use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage}; use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageStream}; use flowy_error::FlowyError; @@ -40,4 +41,15 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) }) } + + fn get_related_message( + &self, + workspace_id: &str, + chat_id: &str, + message_id: i64, + ) -> FutureResult { + FutureResult::new(async move { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + }) + } } diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index db9a81e8d81d8..6d579c70dc0f6 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -395,6 +395,7 @@ pub struct WorkspaceMember { pub email: String, pub role: Role, pub name: String, + pub avatar_url: Option, } /// represent the user awareness object id for the workspace. diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index cdbd928fe09c6..bef8151d527ab 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -14,6 +14,9 @@ pub struct WorkspaceMemberPB { #[pb(index = 3)] pub role: AFRolePB, + + #[pb(index = 4, one_of)] + pub avatar_url: Option, } impl From for WorkspaceMemberPB { @@ -22,6 +25,7 @@ impl From for WorkspaceMemberPB { email: value.email, name: value.name, role: value.role.into(), + avatar_url: value.avatar_url, } } }