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 5cc7525238..5c85256de3 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 @@ -26,6 +26,8 @@ part 'chat_bloc.g.dart'; part 'chat_bloc.freezed.dart'; const sendMessageErrorKey = "sendMessageError"; +const systemUserId = "system"; +const aiResponseUserId = "0"; class ChatBloc extends Bloc { ChatBloc({ @@ -87,6 +89,7 @@ class ChatBloc extends Bloc { }, ); }, + // Loading messages startLoadingPrevMessage: () async { Int64? beforeMessageId; final oldestMessage = _getOlderstMessage(); @@ -130,21 +133,58 @@ class ChatBloc extends Bloc { ), ); }, + // streaming message streaming: (Message message) { final allMessages = _perminentMessages(); allMessages.insert(0, message); emit( state.copyWith( messages: allMessages, - streamingStatus: const LoadingState.loading(), + streamingState: const StreamingState.streaming(), + canSendMessage: false, ), ); }, - didFinishStreaming: () { + finishStreaming: () { emit( - state.copyWith(streamingStatus: const LoadingState.finish()), + state.copyWith( + streamingState: const StreamingState.done(), + canSendMessage: + state.sendingState == const SendMessageState.done(), + ), ); }, + didUpdateAnswerStream: (AnswerStream stream) { + emit(state.copyWith(answerStream: stream)); + }, + stopStream: () async { + if (state.answerStream == null) { + return; + } + + final payload = StopStreamPB(chatId: chatId); + await AIEventStopStream(payload).send(); + final allMessages = _perminentMessages(); + if (state.streamingState != const StreamingState.done()) { + // If the streaming is not started, remove the message from the list + if (!state.answerStream!.hasStarted) { + allMessages.removeWhere( + (element) => element.id == lastStreamMessageId, + ); + lastStreamMessageId = ""; + } + + // when stop stream, we will set the answer stream to null. Which means the streaming + // is finished or canceled. + emit( + state.copyWith( + messages: allMessages, + answerStream: null, + streamingState: const StreamingState.done(), + ), + ); + } + }, receveMessage: (Message message) { final allMessages = _perminentMessages(); // remove message with the same id @@ -164,15 +204,32 @@ class ChatBloc extends Bloc { lastSentMessage: null, messages: allMessages, relatedQuestions: [], + sendingState: const SendMessageState.sending(), + canSendMessage: false, ), ); }, + finishSending: (ChatMessagePB message) { + emit( + state.copyWith( + lastSentMessage: message, + sendingState: const SendMessageState.done(), + canSendMessage: + state.streamingState == const StreamingState.done(), + ), + ); + }, + // related question didReceiveRelatedQuestion: (List questions) { + if (questions.isEmpty) { + return; + } + final allMessages = _perminentMessages(); final message = CustomMessage( metadata: OnetimeShotType.relatedQuestion.toMap(), - author: const User(id: "system"), - id: 'system', + author: const User(id: systemUserId), + id: systemUserId, ); allMessages.insert(0, message); emit( @@ -189,44 +246,6 @@ class ChatBloc extends Bloc { ), ); }, - didSentUserMessage: (ChatMessagePB message) { - emit( - state.copyWith( - lastSentMessage: message, - ), - ); - }, - didUpdateAnswerStream: (AnswerStream stream) { - emit(state.copyWith(answerStream: stream)); - }, - stopStream: () async { - if (state.answerStream == null) { - return; - } - - final payload = StopStreamPB(chatId: chatId); - await AIEventStopStream(payload).send(); - final allMessages = _perminentMessages(); - if (state.streamingStatus != const LoadingState.finish()) { - // If the streaming is not started, remove the message from the list - if (!state.answerStream!.hasStarted) { - allMessages.removeWhere( - (element) => element.id == lastStreamMessageId, - ); - lastStreamMessageId = ""; - } - - // when stop stream, we will set the answer stream to null. Which means the streaming - // is finished or canceled. - emit( - state.copyWith( - messages: allMessages, - answerStream: null, - streamingStatus: const LoadingState.finish(), - ), - ); - } - }, ); }, ); @@ -250,7 +269,7 @@ class ChatBloc extends Bloc { chatErrorMessageCallback: (err) { if (!isClosed) { Log.error("chat error: ${err.errorMessage}"); - add(const ChatEvent.didFinishStreaming()); + add(const ChatEvent.finishStreaming()); } }, latestMessageCallback: (list) { @@ -267,7 +286,7 @@ class ChatBloc extends Bloc { }, finishStreamingCallback: () { if (!isClosed) { - add(const ChatEvent.didFinishStreaming()); + add(const ChatEvent.finishStreaming()); // The answer strema will bet set to null after the streaming is finished or canceled. // so if the answer stream is null, we will not get related question. if (state.lastSentMessage != null && state.answerStream != null) { @@ -353,7 +372,7 @@ class ChatBloc extends Bloc { result.fold( (ChatMessagePB question) { if (!isClosed) { - add(ChatEvent.didSentUserMessage(question)); + add(ChatEvent.finishSending(question)); final questionMessageId = question.messageId; final message = _createTextMessage(question); @@ -374,8 +393,8 @@ class ChatBloc extends Bloc { final error = CustomMessage( metadata: metadata, - author: const User(id: "system"), - id: 'system', + author: const User(id: systemUserId), + id: systemUserId, ); add(ChatEvent.receveMessage(error)); @@ -390,7 +409,7 @@ class ChatBloc extends Bloc { lastStreamMessageId = streamMessageId; return TextMessage( - author: User(id: nanoid()), + author: User(id: "streamId:${nanoid()}"), metadata: { "$AnswerStream": stream, "question": questionMessageId, @@ -425,10 +444,21 @@ class ChatBloc extends Bloc { @freezed class ChatEvent with _$ChatEvent { const factory ChatEvent.initialLoad() = _InitialLoadMessage; + + // send message const factory ChatEvent.sendMessage({ required String message, Map? metadata, }) = _SendMessage; + const factory ChatEvent.finishSending(ChatMessagePB message) = + _FinishSendMessage; + +// receive message + const factory ChatEvent.streaming(Message message) = _StreamingMessage; + const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage; + const factory ChatEvent.finishStreaming() = _FinishStreamingMessage; + +// loading messages const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage; const factory ChatEvent.didLoadPreviousMessages( List messages, @@ -436,16 +466,13 @@ class ChatEvent with _$ChatEvent { ) = _DidLoadPreviousMessages; const factory ChatEvent.didLoadLatestMessages(List messages) = _DidLoadMessages; - const factory ChatEvent.streaming(Message message) = _StreamingMessage; - const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage; - const factory ChatEvent.didFinishStreaming() = _FinishStreamingMessage; +// related questions const factory ChatEvent.didReceiveRelatedQuestion( List questions, ) = _DidReceiveRelatedQueston; const factory ChatEvent.clearReleatedQuestion() = _ClearRelatedQuestion; - const factory ChatEvent.didSentUserMessage(ChatMessagePB message) = - _DidSendUserMessage; + const factory ChatEvent.didUpdateAnswerStream( AnswerStream stream, ) = _DidUpdateAnswerStream; @@ -466,7 +493,8 @@ class ChatState with _$ChatState { required LoadingState loadingPreviousStatus, // When sending a user message, the status will be set as loading. // After the message is sent, the status will be set as finished. - required LoadingState streamingStatus, + required StreamingState streamingState, + required SendMessageState sendingState, // Indicate whether there are more previous messages to load. required bool hasMorePrevMessage, // The related questions that are received after the user message is sent. @@ -474,6 +502,7 @@ class ChatState with _$ChatState { // The last user message that is sent to the server. ChatMessagePB? lastSentMessage, AnswerStream? answerStream, + @Default(true) bool canSendMessage, }) = _ChatState; factory ChatState.initial(ViewPB view, UserProfilePB userProfile) => @@ -483,12 +512,19 @@ class ChatState with _$ChatState { userProfile: userProfile, initialLoadingStatus: const LoadingState.finish(), loadingPreviousStatus: const LoadingState.finish(), - streamingStatus: const LoadingState.finish(), + streamingState: const StreamingState.done(), + sendingState: const SendMessageState.done(), hasMorePrevMessage: true, relatedQuestions: [], ); } +bool isOtherUserMessage(Message message) { + return message.author.id != aiResponseUserId && + message.author.id != systemUserId && + !message.author.id.startsWith("streamId:"); +} + @freezed class LoadingState with _$LoadingState { const factory LoadingState.loading() = _Loading; @@ -497,6 +533,7 @@ class LoadingState with _$LoadingState { enum OnetimeShotType { unknown, + sendingMessage, relatedQuestion, invalidSendMesssage, } @@ -638,7 +675,9 @@ List chatMessageMetadataFromString(String? s) { } if (metadataJson is Map) { - metadata.add(ChatMessageMetadata.fromJson(metadataJson)); + if (metadataJson.isNotEmpty) { + metadata.add(ChatMessageMetadata.fromJson(metadataJson)); + } } else if (metadataJson is List) { metadata.addAll( metadataJson.map( @@ -672,3 +711,15 @@ class ChatMessageMetadata { Map toJson() => _$ChatMessageMetadataToJson(this); } + +@freezed +class StreamingState with _$StreamingState { + const factory StreamingState.streaming() = _Streaming; + const factory StreamingState.done({FlowyError? error}) = _StreamDone; +} + +@freezed +class SendMessageState with _$SendMessageState { + const factory SendMessageState.sending() = _Sending; + const factory SendMessageState.done({FlowyError? error}) = _SendDone; +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart index 1f32a045e4..3a23106e8f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart @@ -108,6 +108,14 @@ class ChatInputActionBloc ), ); }, + clear: () { + emit( + state.copyWith( + selectedPages: [], + filter: "", + ), + ); + }, ); } } @@ -171,6 +179,7 @@ class ChatInputActionEvent with _$ChatInputActionEvent { const factory ChatInputActionEvent.addPage(ChatInputActionPage page) = _AddPage; const factory ChatInputActionEvent.removePage(String text) = _RemovePage; + const factory ChatInputActionEvent.clear() = _Clear; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_control.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_control.dart index 6569b3a1df..51362c052f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_control.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_control.dart @@ -35,10 +35,18 @@ class ChatInputActionControl extends ChatActionHandler { List get tags => _commandBloc.state.selectedPages.map((e) => e.title).toList(); - ChatInputMetadata get metaData => _commandBloc.state.selectedPages.fold( - {}, - (map, page) => map..putIfAbsent(page.pageId, () => page), - ); + ChatInputMetadata consumeMetaData() { + final metadata = _commandBloc.state.selectedPages.fold( + {}, + (map, page) => map..putIfAbsent(page.pageId, () => page), + ); + + if (metadata.isNotEmpty) { + _commandBloc.add(const ChatInputActionEvent.clear()); + } + + return metadata; + } void handleKeyEvent(KeyEvent event) { // ignore: deprecated_member_use diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart new file mode 100644 index 0000000000..c0f68bb9b3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart @@ -0,0 +1,80 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_member_bloc.freezed.dart'; + +class ChatMemberBloc extends Bloc { + ChatMemberBloc() : super(const ChatMemberState()) { + on( + (event, emit) async { + event.when( + initial: () {}, + receiveMemberInfo: (String id, WorkspaceMemberPB memberInfo) { + final members = Map.from(state.members); + members[id] = ChatMember(info: memberInfo); + emit(state.copyWith(members: members)); + }, + getMemberInfo: (String userId) { + if (state.members.containsKey(userId)) { + // Member info already exists. Debouncing refresh member info from backend would be better. + return; + } + + final payload = WorkspaceMemberIdPB( + uid: Int64.parseInt(userId), + ); + UserEventGetMemberInfo(payload).send().then((result) { + if (!isClosed) { + result.fold((member) { + add( + ChatMemberEvent.receiveMemberInfo( + userId, + member, + ), + ); + }, (err) { + Log.error("Error getting member info: $err"); + }); + } + }); + }, + ); + }, + ); + } +} + +@freezed +class ChatMemberEvent with _$ChatMemberEvent { + const factory ChatMemberEvent.initial() = Initial; + const factory ChatMemberEvent.getMemberInfo( + String userId, + ) = _GetMemberInfo; + const factory ChatMemberEvent.receiveMemberInfo( + String id, + WorkspaceMemberPB memberInfo, + ) = _ReceiveMemberInfo; +} + +@freezed +class ChatMemberState with _$ChatMemberState { + const factory ChatMemberState({ + @Default({}) Map members, + }) = _ChatMemberState; +} + +class ChatMember extends Equatable { + ChatMember({ + required this.info, + }); + final DateTime _date = DateTime.now(); + final WorkspaceMemberPB info; + + @override + List get props => [_date, info]; +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart index 2dcc710e74..dc2bc5dbbc 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart @@ -7,7 +7,8 @@ import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; Future> metadataPBFromMetadata( - Map? map,) async { + Map? map, +) async { final List metadata = []; if (map != null) { for (final entry in map.entries) { @@ -18,11 +19,14 @@ Future> metadataPBFromMetadata( final payload = OpenDocumentPayloadPB(documentId: view.id); final result = await DocumentEventGetDocumentText(payload).send(); result.fold((pb) { - metadata.add(ChatMessageMetaPB( - id: view.id, - name: view.name, - text: pb.text, - ),); + metadata.add( + ChatMessageMetaPB( + id: view.id, + name: view.name, + text: pb.text, + source: "appflowy document", + ), + ); }, (err) { Log.error('Failed to get document text: $err'); }); 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 index cae2b28c30..aa40d65078 100644 --- 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 @@ -1,7 +1,4 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:fixnum/fixnum.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -12,25 +9,14 @@ class ChatUserMessageBloc extends Bloc { ChatUserMessageBloc({ required Message message, - }) : super(ChatUserMessageState.initial(message)) { + required ChatMember? member, + }) : super(ChatUserMessageState.initial(message, member)) { on( (event, emit) async { event.when( - initial: () { - final payload = - WorkspaceMemberIdPB(uid: Int64.parseInt(message.author.id)); - UserEventGetMemberInfo(payload).send().then((result) { - if (!isClosed) { - result.fold((member) { - add(ChatUserMessageEvent.didReceiveMemberInfo(member)); - }, (err) { - Log.error("Error getting member info: $err"); - }); - } - }); - }, - didReceiveMemberInfo: (WorkspaceMemberPB memberInfo) { - emit(state.copyWith(member: memberInfo)); + initial: () {}, + refreshMember: (ChatMember member) { + emit(state.copyWith(member: member)); }, ); }, @@ -41,18 +27,20 @@ class ChatUserMessageBloc @freezed class ChatUserMessageEvent with _$ChatUserMessageEvent { const factory ChatUserMessageEvent.initial() = Initial; - const factory ChatUserMessageEvent.didReceiveMemberInfo( - WorkspaceMemberPB memberInfo, - ) = _MemberInfo; + const factory ChatUserMessageEvent.refreshMember(ChatMember member) = + _MemberInfo; } @freezed class ChatUserMessageState with _$ChatUserMessageState { const factory ChatUserMessageState({ required Message message, - WorkspaceMemberPB? member, + ChatMember? member, }) = _ChatUserMessageState; - factory ChatUserMessageState.initial(Message message) => - ChatUserMessageState(message: message); + factory ChatUserMessageState.initial( + Message message, + ChatMember? member, + ) => + ChatUserMessageState(message: message, member: member); } 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 cbc3327b72..bd9c46ceae 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/other_user_message_bubble.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:desktop_drop/desktop_drop.dart'; @@ -27,6 +28,7 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat; import 'package:styled_widget/styled_widget.dart'; +import 'application/chat_member_bloc.dart'; import 'application/chat_side_pannel_bloc.dart'; import 'presentation/chat_input/chat_input.dart'; import 'presentation/chat_popmenu.dart'; @@ -101,6 +103,7 @@ class AIChatPage extends StatelessWidget { ChatInputStateBloc()..add(const ChatInputStateEvent.started()), ), BlocProvider(create: (_) => ChatSidePannelBloc(chatId: view.id)), + BlocProvider(create: (_) => ChatMemberBloc()), ], child: BlocListener( listenWhen: (previous, current) => @@ -298,6 +301,7 @@ class _ChatContentPageState extends State<_ChatContentPage> { Widget buildChatWidget() { return BlocBuilder( builder: (blocContext, state) => Chat( + key: ValueKey(widget.view.id), messages: state.messages, onSendPressed: (_) { // We use custom bottom widget for chat input, so @@ -335,7 +339,7 @@ class _ChatContentPageState extends State<_ChatContentPage> { required messageWidth, required showName, }) => - _buildAITextMessage(blocContext, textMessage), + _buildTextMessage(blocContext, textMessage), bubbleBuilder: ( child, { required message, @@ -346,17 +350,21 @@ class _ChatContentPageState extends State<_ChatContentPage> { message: message, child: child, ); + } else if (isOtherUserMessage(message)) { + return OtherUserMessageBubble( + message: message, + child: child, + ); + } else { + return _buildAIBubble(message, blocContext, state, child); } - - return _buildAIBubble(message, blocContext, state, child); }, ), ); } - Widget _buildAITextMessage(BuildContext context, TextMessage message) { - final isAuthor = message.author.id == _user.id; - if (isAuthor) { + Widget _buildTextMessage(BuildContext context, TextMessage message) { + if (message.author.id == _user.id) { return ChatTextMessageWidget( user: message.author, messageUserId: message.id, @@ -497,9 +505,9 @@ class _ChatContentPageState extends State<_ChatContentPage> { return Column( children: [ - BlocSelector( - selector: (state) => state.streamingStatus, - builder: (context, state) { + BlocSelector( + selector: (state) => state.canSendMessage, + builder: (context, canSendMessage) { return ChatInput( aiType: aiType, chatId: widget.view.id, @@ -511,7 +519,7 @@ class _ChatContentPageState extends State<_ChatContentPage> { ), ); }, - isStreaming: state != const LoadingState.finish(), + isStreaming: !canSendMessage, onStopStreaming: () { context .read() 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 index 7f2e01f989..b91d4e1099 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart @@ -55,11 +55,13 @@ class ChatUserAvatar extends StatelessWidget { required this.name, required this.size, this.isHovering = false, + this.defaultName, }); final String iconUrl; final String name; final double size; + final String? defaultName; // If true, a border will be applied on top of the avatar final bool isHovering; @@ -76,7 +78,8 @@ class ChatUserAvatar extends StatelessWidget { } Widget _buildEmptyAvatar(BuildContext context) { - final String nameOrDefault = _userName(name); + final String nameOrDefault = _userName(name, defaultName); + final Color color = ColorGenerator(name).toColor(); const initialsCount = 2; @@ -170,8 +173,8 @@ class ChatUserAvatar extends StatelessWidget { /// 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; + String _userName(String name, String? defaultName) => + name.isEmpty ? (defaultName ?? 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. diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart index faef4f8a80..f342a94299 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart @@ -144,7 +144,7 @@ class _ChatInputState extends State { if (trimmedText != '') { final partialText = types.PartialText( text: trimmedText, - metadata: _inputActionControl.metaData, + metadata: _inputActionControl.consumeMetaData(), ); widget.onSendPressed(partialText); _textController.clear(); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart index 723d35301c..3b3f06114b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart @@ -44,9 +44,12 @@ class AIMessageMetadata extends StatelessWidget { ), useIntrinsicWidth: true, radius: BorderRadius.circular(6), - text: FlowyText( - m.source, - fontSize: 14, + text: Opacity( + opacity: 0.5, + child: FlowyText( + m.name, + fontSize: 14, + ), ), onTap: () { onSelectedMetadata(m); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/other_user_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/other_user_message_bubble.dart new file mode 100644 index 0000000000..676df936c1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/other_user_message_bubble.dart @@ -0,0 +1,212 @@ +import 'dart:convert'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/shared/markdown_to_document.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.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 OtherUserMessageBubble extends StatelessWidget { + const OtherUserMessageBubble({ + super.key, + required this.message, + required this.child, + }); + + final Message message; + final Widget child; + + @override + Widget build(BuildContext context) { + const padding = EdgeInsets.symmetric(horizontal: _leftPadding); + final childWithPadding = Padding(padding: padding, child: child); + final widget = isMobile + ? _wrapPopMenu(childWithPadding) + : _wrapHover(childWithPadding); + + if (context.read().state.members[message.author.id] == + null) { + context + .read() + .add(ChatMemberEvent.getMemberInfo(message.author.id)); + } + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlocConsumer( + listenWhen: (previous, current) { + return previous.members[message.author.id] != + current.members[message.author.id]; + }, + listener: (context, state) {}, + builder: (context, state) { + final member = state.members[message.author.id]; + return ChatUserAvatar( + iconUrl: member?.info.avatarUrl ?? "", + name: member?.info.name ?? "", + size: 36, + defaultName: "", + ); + }, + ), + Expanded(child: widget), + ], + ); + } + + OtherUserMessageHover _wrapHover(Padding child) { + return OtherUserMessageHover( + message: message, + 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 OtherUserMessageHover extends StatefulWidget { + const OtherUserMessageHover({ + super.key, + required this.child, + required this.message, + }); + + final Widget child; + final Message message; + final bool autoShowHover = true; + + @override + State createState() => _OtherUserMessageHoverState(); +} + +class _OtherUserMessageHoverState 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) { + 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.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: () async { + final document = customMarkdownToDocument(textMessage.text); + await getIt().setData( + ClipboardServiceData( + plainText: textMessage.text, + inAppJson: jsonEncode(document.toJson()), + ), + ); + if (context.mounted) { + showToastNotification( + context, + message: LocaleKeys.grid_url_copiedNotification.tr(), + ); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/user_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/user_message_bubble.dart index 9adf3d593b..9374af8a7b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/user_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/user_message_bubble.dart @@ -1,6 +1,6 @@ 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/application/chat_member_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'; @@ -27,49 +27,53 @@ class ChatUserMessageBubble extends StatelessWidget { const borderRadius = BorderRadius.all(Radius.circular(6)); final backgroundColor = Theme.of(context).colorScheme.surfaceContainerHighest; + if (context.read().state.members[message.author.id] == + null) { + context + .read() + .add(ChatMemberEvent.getMemberInfo(message.author.id)); + } - return BlocProvider( - create: (context) => ChatUserMessageBloc(message: message) - ..add(const ChatUserMessageEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // _wrapHover( - Flexible( - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: borderRadius, - color: backgroundColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: child, + return BlocConsumer( + listenWhen: (previous, current) { + return previous.members[message.author.id] != + current.members[message.author.id]; + }, + listener: (context, state) {}, + builder: (context, state) { + final member = state.members[message.author.id]; + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // _wrapHover( + Flexible( + child: 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, - ), - ); - }, + ), + // ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ChatUserAvatar( + iconUrl: member?.info.avatarUrl ?? "", + name: member?.info.name ?? "", + size: 36, ), - ], - ); - }, - ), + ), + ], + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart index 7a7d0fecd7..66cfa11bf0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart @@ -1,6 +1,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; @@ -10,12 +11,12 @@ part 'local_ai_on_boarding_bloc.freezed.dart'; class LocalAIOnBoardingBloc extends Bloc { - LocalAIOnBoardingBloc(this.workspaceId) + LocalAIOnBoardingBloc(this.userProfile) : super(const LocalAIOnBoardingState()) { _dispatch(); } - final String workspaceId; + final UserProfilePB userProfile; void _dispatch() { on((event, emit) { @@ -44,7 +45,7 @@ class LocalAIOnBoardingBloc } void _loadSubscriptionPlans() { - final payload = UserWorkspaceIdPB()..workspaceId = workspaceId; + final payload = UserWorkspaceIdPB()..workspaceId = userProfile.workspaceId; UserEventGetWorkspaceSubscriptionInfo(payload).send().then((result) { if (!isClosed) { add(LocalAIOnBoardingEvent.didGetSubscriptionPlans(result)); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index d2050637f1..fd07db4d48 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -1,4 +1,5 @@ import 'package:appflowy/user/application/user_listener.dart'; +import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -10,14 +11,31 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_ai_bloc.freezed.dart'; class SettingsAIBloc extends Bloc { - SettingsAIBloc(this.userProfile) + SettingsAIBloc(this.userProfile, WorkspaceMemberPB? member) : _userListener = UserListener(userProfile: userProfile), - super(SettingsAIState(userProfile: userProfile)) { + _userService = UserBackendService(userId: userProfile.id), + super(SettingsAIState(userProfile: userProfile, member: member)) { _dispatch(); + + if (member == null) { + _userService.getWorkspaceMember().then((result) { + result.fold( + (member) { + if (!isClosed) { + add(SettingsAIEvent.refreshMember(member)); + } + }, + (err) { + Log.error(err); + }, + ); + }); + } } final UserListener _userListener; final UserProfilePB userProfile; + final UserBackendService _userService; @override Future close() async { @@ -62,6 +80,9 @@ class SettingsAIBloc extends Bloc { ), ); }, + refreshMember: (member) { + emit(state.copyWith(member: member)); + }, ); }); } @@ -112,6 +133,7 @@ class SettingsAIEvent with _$SettingsAIEvent { ) = _DidLoadWorkspaceSetting; const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch; + const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) = _RefreshMember; const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel; @@ -125,6 +147,7 @@ class SettingsAIState with _$SettingsAIState { const factory SettingsAIState({ required UserProfilePB userProfile, UseAISettingPB? aiSettings, + WorkspaceMemberPB? member, @Default(true) bool enableSearchIndexing, }) = _SettingsAIState; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 2183c37c08..79f1de6da0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; @@ -37,15 +38,20 @@ class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget { } class SettingsAIView extends StatelessWidget { - const SettingsAIView({super.key, required this.userProfile}); + const SettingsAIView({ + super.key, + required this.userProfile, + required this.member, + }); final UserProfilePB userProfile; + final WorkspaceMemberPB? member; @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => - SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()), + create: (_) => SettingsAIBloc(userProfile, member) + ..add(const SettingsAIEvent.started()), child: BlocBuilder( builder: (context, state) { final children = [ @@ -53,11 +59,15 @@ class SettingsAIView extends StatelessWidget { ]; children.add(const _AISearchToggle(value: false)); - children.add( - _LocalAIOnBoarding( - workspaceId: userProfile.workspaceId, - ), - ); + + if (state.member != null) { + children.add( + _LocalAIOnBoarding( + userProfile: userProfile, + member: state.member!, + ), + ); + } return SettingsBody( title: LocaleKeys.settings_aiPage_title.tr(), @@ -116,8 +126,12 @@ class _AISearchToggle extends StatelessWidget { // ignore: unused_element class _LocalAIOnBoarding extends StatelessWidget { - const _LocalAIOnBoarding({required this.workspaceId}); - final String workspaceId; + const _LocalAIOnBoarding({ + required this.userProfile, + required this.member, + }); + final UserProfilePB userProfile; + final WorkspaceMemberPB member; @override Widget build(BuildContext context) { @@ -125,7 +139,7 @@ class _LocalAIOnBoarding extends StatelessWidget { return BillingGateGuard( builder: (context) { return BlocProvider( - create: (context) => LocalAIOnBoardingBloc(workspaceId) + create: (context) => LocalAIOnBoardingBloc(userProfile) ..add(const LocalAIOnBoardingEvent.started()), child: BlocBuilder( builder: (context, state) { @@ -133,16 +147,20 @@ class _LocalAIOnBoarding extends StatelessWidget { if (kDebugMode || state.isPurchaseAILocal) { return const LocalAISetting(); } else { - // Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan - return _UpgradeToAILocalPlan( - onTap: () { - context.read().add( - const SettingsDialogEvent.setSelectedPage( - SettingsPage.plan, - ), - ); - }, - ); + if (member.role.isOwner) { + // Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan + return _UpgradeToAILocalPlan( + onTap: () { + context.read().add( + const SettingsDialogEvent.setSelectedPage( + SettingsPage.plan, + ), + ); + }, + ); + } else { + return const _AskOwnerUpgradeToLocalAI(); + } } }, ), @@ -155,6 +173,18 @@ class _LocalAIOnBoarding extends StatelessWidget { } } +class _AskOwnerUpgradeToLocalAI extends StatelessWidget { + const _AskOwnerUpgradeToLocalAI(); + + @override + Widget build(BuildContext context) { + return FlowyText( + LocaleKeys.sideBar_askOwnerToUpgradeToLocalAI.tr(), + color: AFThemeExtension.of(context).strongText, + ); + } +} + class _UpgradeToAILocalPlan extends StatefulWidget { const _UpgradeToAILocalPlan({required this.onTap}); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 6f6d605673..5a3905ed21 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -120,7 +120,7 @@ class SettingsDialog extends StatelessWidget { return const SettingsShortcutsView(); case SettingsPage.ai: if (user.authenticator == AuthenticatorPB.AppFlowyCloud) { - return SettingsAIView(userProfile: user); + return SettingsAIView(userProfile: user, member: member); } else { return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud(); } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index a95a7d2bd3..e81c737826 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bincode", @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bytes", @@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "again", "anyhow", @@ -876,7 +876,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "collab-entity", "collab-rt-entity", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "futures-channel", "futures-util", @@ -1132,7 +1132,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bincode", @@ -1157,7 +1157,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "async-trait", @@ -1532,7 +1532,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "app-error", @@ -3051,7 +3051,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "futures-util", @@ -3068,7 +3068,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "app-error", @@ -3500,7 +3500,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bytes", @@ -6098,7 +6098,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index fcdc5490e0..cdfd51ef4f 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -53,7 +53,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 = "a371912c61d79fa946ec78f0cb852fdd7d391356" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "40bd6b784eced6fe43c95941420b3ce444b0a60c" } [dependencies] serde_json.workspace = true diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index f44e96c08a..2af380a87a 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/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=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bytes", @@ -800,7 +800,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "again", "anyhow", @@ -850,7 +850,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "collab-entity", "collab-rt-entity", @@ -862,7 +862,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "futures-channel", "futures-util", @@ -1115,7 +1115,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bincode", @@ -1140,7 +1140,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "async-trait", @@ -1522,7 +1522,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "app-error", @@ -3118,7 +3118,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "futures-util", @@ -3135,7 +3135,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "app-error", @@ -3572,7 +3572,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bytes", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" 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 bd8c4f9def..20a052adb0 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 = "a371912c61d79fa946ec78f0cb852fdd7d391356" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "40bd6b784eced6fe43c95941420b3ce444b0a60c" } [dependencies] serde_json.workspace = true diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index fc7a0156d1..49a40c640d 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -301,7 +301,8 @@ "askOwnerToUpgradeToAIMax": "Your workspace is running out of free AI responses. Please ask your workspace owner to upgrade the plan or purchase AI add-ons", "purchaseStorageSpace": "Purchase Storage Space", "purchaseAIResponse": "Purchase ", - "upgradeToAILocal": "AI offline on your device" + "askOwnerToUpgradeToLocalAI": "Ask workspace owner to enable AI On-device", + "upgradeToAILocal": "AI On-device on your device" }, "notifications": { "export": { @@ -654,7 +655,7 @@ "menuLabel": "AI Settings", "keys": { "enableAISearchTitle": "AI Search", - "aiSettingsDescription": "Select or configure AI models used on @:appName. For best performance we recommend using the default model options", + "aiSettingsDescription": "Choose your preferred model to power AppFlowy AI. Now includes GPT 4-o, Claude 3,5, Llama 3.1, and Mistral 7B", "loginToEnableAIFeature": "AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up", "llmModel": "Language Model", "llmModelType": "Language Model Type", @@ -778,7 +779,7 @@ }, "aiOnDevice": { "label": "AI On-device for Mac", - "description": "Unlock unlimited AI offline on your device", + "description": "Unlock unlimited AI On-device on your device", "activeDescription": "Next invoice due on {}", "canceledDescription": "AI On-device for Mac will be available until {}" }, diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a5098629f3..adf338ec7b 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=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bytes", @@ -718,7 +718,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "again", "anyhow", @@ -768,7 +768,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "collab-entity", "collab-rt-entity", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "futures-channel", "futures-util", @@ -993,7 +993,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bincode", @@ -1018,7 +1018,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "async-trait", @@ -1356,7 +1356,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "app-error", @@ -2730,7 +2730,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "futures-util", @@ -2747,7 +2747,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "app-error", @@ -3112,7 +3112,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "bytes", @@ -5307,7 +5307,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 1611828c0c..ac2666ddc4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -99,8 +99,8 @@ zip = "2.1.3" # 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 = "a371912c61d79fa946ec78f0cb852fdd7d391356" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a371912c61d79fa946ec78f0cb852fdd7d391356" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "40bd6b784eced6fe43c95941420b3ce444b0a60c" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "40bd6b784eced6fe43c95941420b3ce444b0a60c" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 21f17a0d98..04ce6c771f 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -54,6 +54,9 @@ pub struct ChatMessageMetaPB { #[pb(index = 3)] pub text: String, + + #[pb(index = 4)] + pub source: String, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index bb8af3f7a3..6ca6778c30 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -42,7 +42,7 @@ pub(crate) async fn stream_chat_message_handler( data: ChatMetadataData::new_text(metadata.text), id: metadata.id, name: metadata.name.clone(), - source: metadata.name, + source: metadata.source, }) .collect::>(); diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 3c71bff3b0..9b546d0182 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -304,6 +304,9 @@ pub enum ErrorCode { #[error("AI offline not started")] AIOfflineNotInstalled = 105, + + #[error("Invalid Request")] + InvalidRequest = 106, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs index bb4059164f..a436544e01 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -15,7 +15,7 @@ impl From for FlowyError { AppErrorCode::MissingPayload => ErrorCode::MissingPayload, AppErrorCode::OpenError => ErrorCode::Internal, AppErrorCode::InvalidUrl => ErrorCode::InvalidURL, - AppErrorCode::InvalidRequest => ErrorCode::InvalidParams, + AppErrorCode::InvalidRequest => ErrorCode::InvalidRequest, AppErrorCode::InvalidOAuthProvider => ErrorCode::InvalidAuthConfig, AppErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized, AppErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions,