diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart index 5efa85ff72..1c1121d7f5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart @@ -34,7 +34,7 @@ class MobileQuickActionButton extends StatelessWidget { enable ? null : const WidgetStatePropertyAll(Colors.transparent), splashColor: Colors.transparent, child: Container( - height: 52, + height: 44, padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ 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 5a3f705943..1822047d11 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 @@ -30,7 +30,8 @@ class ChatBloc extends Bloc { super(ChatState.initial()) { _startListening(); _dispatch(); - _init(); + _loadMessages(); + _loadSetting(); } final String chatId; @@ -59,16 +60,6 @@ class ChatBloc extends Bloc { AnswerStream? answerStream; int numSendMessage = 0; - /// a counter used to determine whether the initial loading state should be - /// set to finish. It should hit two before we emit: one for the local fetch - /// and another for the server fetch. - /// - /// This is to work around a bug where if an ai chat that is not yet on the - /// user local storage is opened but has messages in the server, it will - /// remain stuck on the welcome screen until the user switches to another page - /// then come back. - int initialFetchCounter = 0; - @override Future close() async { await answerStream?.dispose(); @@ -86,14 +77,23 @@ class ChatBloc extends Bloc { await chatController.insert(message, index: 0); } - if (initialFetchCounter < 2) { - initialFetchCounter++; - } - - if (state.loadingState.isLoading && initialFetchCounter >= 2) { - emit( - state.copyWith(loadingState: const ChatLoadingState.finish()), - ); + switch (state.loadingState) { + case LoadChatMessageStatus.loading + when chatController.messages.isEmpty: + emit( + state.copyWith( + loadingState: LoadChatMessageStatus.loadingRemote, + ), + ); + break; + case LoadChatMessageStatus.loading: + case LoadChatMessageStatus.loadingRemote: + emit( + state.copyWith(loadingState: LoadChatMessageStatus.ready), + ); + break; + default: + break; } }, loadPreviousMessages: () { @@ -165,18 +165,7 @@ class ChatBloc extends Bloc { ) { numSendMessage += 1; - final relatedQuestionMessages = chatController.messages - .where( - (message) => - onetimeMessageTypeFromMeta(message.metadata) == - OnetimeShotType.relatedQuestion, - ) - .toList(); - - for (final message in relatedQuestionMessages) { - chatController.remove(message); - } - + _clearRelatedQuestions(); _startStreamingMessage(message, metadata); lastSentMessage = null; @@ -229,7 +218,6 @@ class ChatBloc extends Bloc { answerStreamMessageId = ''; }, startAnswerStreaming: (Message message) { - chatController.insert(message); emit( state.copyWith( promptResponseState: PromptResponseState.streamingAnswer, @@ -247,6 +235,17 @@ class ChatBloc extends Bloc { ), ); }, + regenerateAnswer: (id) { + _clearRelatedQuestions(); + _regenerateAnswer(id); + lastSentMessage = null; + + emit( + state.copyWith( + promptResponseState: PromptResponseState.sendingQuestion, + ), + ); + }, didReceiveChatSettings: (settings) { emit( state.copyWith(selectedSourceIds: settings.ragIds), @@ -348,10 +347,10 @@ class ChatBloc extends Bloc { ); } - void _init() async { + void _loadSetting() async { final getChatSettingsPayload = AIEventGetChatSettings(ChatId(value: chatId)); - final getChatSettingsFuture = getChatSettingsPayload.send().fold( + await getChatSettingsPayload.send().fold( (settings) { if (!isClosed) { add(ChatEvent.didReceiveChatSettings(settings: settings)); @@ -359,13 +358,14 @@ class ChatBloc extends Bloc { }, Log.error, ); + } + void _loadMessages() async { final loadMessagesPayload = LoadNextChatMessagePB( chatId: chatId, limit: Int64(10), ); - final loadMessagesFuture = - AIEventLoadNextMessage(loadMessagesPayload).send().fold( + await AIEventLoadNextMessage(loadMessagesPayload).send().fold( (list) { if (!isClosed) { final messages = list.messages.map(_createTextMessage).toList(); @@ -374,8 +374,6 @@ class ChatBloc extends Bloc { }, (err) => Log.error("Failed to load messages: $err"), ); - - await Future.wait([getChatSettingsFuture, loadMessagesFuture]); } bool _isOneTimeMessage(Message message) { @@ -433,6 +431,7 @@ class ChatBloc extends Bloc { ); add(ChatEvent.finishSending(question)); + add(ChatEvent.receiveMessage(streamAnswer)); add(ChatEvent.startAnswerStreaming(streamAnswer)); } }, @@ -460,6 +459,37 @@ class ChatBloc extends Bloc { ); } + void _regenerateAnswer(String answerMessageIdString) async { + final answerMessageId = Int64.tryParseInt(answerMessageIdString); + if (answerMessageId == null) { + return; + } + + await answerStream?.dispose(); + answerStream = AnswerStream(); + + final payload = RegenerateResponsePB( + chatId: chatId, + answerMessageId: answerMessageId, + answerStreamPort: Int64(answerStream!.nativePort), + ); + + await AIEventRegenerateResponse(payload).send().fold( + (success) { + if (!isClosed) { + final streamAnswer = _createAnswerStreamMessage( + answerStream!, + answerMessageId - 1, + ); + + add(ChatEvent.receiveMessage(streamAnswer)); + add(ChatEvent.startAnswerStreaming(streamAnswer)); + } + }, + (err) => Log.error("Failed to send message: ${err.msg}"), + ); + } + Message _createAnswerStreamMessage( AnswerStream stream, Int64 questionMessageId, @@ -518,6 +548,20 @@ class ChatBloc extends Bloc { }, ); } + + void _clearRelatedQuestions() { + final relatedQuestionMessages = chatController.messages + .where( + (message) => + onetimeMessageTypeFromMeta(message.metadata) == + OnetimeShotType.relatedQuestion, + ) + .toList(); + + for (final message in relatedQuestionMessages) { + chatController.remove(message); + } + } } @freezed @@ -539,6 +583,9 @@ class ChatEvent with _$ChatEvent { _FinishSendMessage; const factory ChatEvent.failedSending() = _FailSendMessage; + // regenerate + const factory ChatEvent.regenerateAnswer(String id) = _RegenerateAnswer; + // streaming answer const factory ChatEvent.startAnswerStreaming(Message message) = _StartAnswerStreaming; @@ -567,13 +614,13 @@ class ChatEvent with _$ChatEvent { class ChatState with _$ChatState { const factory ChatState({ required List selectedSourceIds, - required ChatLoadingState loadingState, + required LoadChatMessageStatus loadingState, required PromptResponseState promptResponseState, }) = _ChatState; factory ChatState.initial() => const ChatState( selectedSourceIds: [], - loadingState: ChatLoadingState.loading(), + loadingState: LoadChatMessageStatus.loading, promptResponseState: PromptResponseState.ready, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart index e28c93a6a5..c3ad6ce1cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart @@ -132,3 +132,9 @@ const onetimeShotType = "OnetimeShotType"; OnetimeShotType? onetimeMessageTypeFromMeta(Map? metadata) { return metadata?[onetimeShotType]; } + +enum LoadChatMessageStatus { + loading, + loadingRemote, + ready, +} 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 ed9757e342..179f1e003d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -120,20 +120,13 @@ class _ChatContentPage extends StatelessWidget { behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), child: BlocBuilder( builder: (context, state) { - return state.loadingState.when( - loading: () { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - }, - finish: (_) { - final chatController = - context.read().chatController; - return Column( + return switch (state.loadingState) { + LoadChatMessageStatus.ready => Column( children: [ Expanded( child: Chat( - chatController: chatController, + chatController: + context.read().chatController, user: User(id: userProfile.id.toString()), darkTheme: ChatTheme.fromThemeData(Theme.of(context)), theme: ChatTheme.fromThemeData(Theme.of(context)), @@ -148,9 +141,9 @@ class _ChatContentPage extends StatelessWidget { ), _buildInput(context), ], - ); - }, - ); + ), + _ => const Center(child: CircularProgressIndicator.adaptive()), + }; }, ), ), @@ -223,6 +216,8 @@ class _ChatContentPage extends StatelessWidget { isLastMessage: isLastMessage, onSelectedMetadata: (metadata) => _onSelectMetadata(context, metadata), + onRegenerate: (id) => + context.read().add(ChatEvent.regenerateAnswer(id)), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart index 9da09d8c1e..ea10592932 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -30,12 +30,14 @@ class ChatAIMessageBubble extends StatelessWidget { required this.child, required this.showActions, this.isLastMessage = false, + this.onRegenerate, }); final Message message; final Widget child; final bool showActions; final bool isLastMessage; + final void Function(String)? onRegenerate; @override Widget build(BuildContext context) { @@ -60,6 +62,7 @@ class ChatAIMessageBubble extends StatelessWidget { Widget _wrapBottomActions(Widget child) { return ChatAIBottomInlineActions( message: message, + onRegenerate: onRegenerate, child: child, ); } @@ -67,6 +70,7 @@ class ChatAIMessageBubble extends StatelessWidget { Widget _wrapHover(Widget child) { return ChatAIMessageHover( message: message, + onRegenerate: onRegenerate, child: child, ); } @@ -74,6 +78,7 @@ class ChatAIMessageBubble extends StatelessWidget { Widget _wrapPopMenu(Widget child) { return ChatAIMessagePopup( message: message, + onRegenerate: onRegenerate, child: child, ); } @@ -84,10 +89,12 @@ class ChatAIBottomInlineActions extends StatelessWidget { super.key, required this.child, required this.message, + this.onRegenerate, }); final Widget child; final Message message; + final void Function(String)? onRegenerate; @override Widget build(BuildContext context) { @@ -107,6 +114,9 @@ class ChatAIBottomInlineActions extends StatelessWidget { CopyButton( textMessage: message as TextMessage, ), + RegenerateButton( + onTap: () => onRegenerate?.call(message.id), + ), ], ), ), @@ -121,10 +131,12 @@ class ChatAIMessageHover extends StatefulWidget { super.key, required this.child, required this.message, + this.onRegenerate, }); final Widget child; final Message message; + final void Function(String)? onRegenerate; @override State createState() => _ChatAIMessageHoverState(); @@ -206,6 +218,10 @@ class _ChatAIMessageHoverState extends State { CopyButton( textMessage: widget.message as TextMessage, ), + RegenerateButton( + onTap: () => + widget.onRegenerate?.call(widget.message.id), + ), ], ) : null, @@ -370,15 +386,44 @@ class CopyButton extends StatelessWidget { } } +class RegenerateButton extends StatelessWidget { + const RegenerateButton({ + super.key, + required this.onTap, + }); + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.chat_regenerate.tr(), + child: FlowyIconButton( + width: DesktopAIConvoSizes.actionBarIconSize, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + radius: DesktopAIConvoSizes.actionBarIconRadius, + icon: FlowySvg( + FlowySvgs.ai_undo_s, + color: Theme.of(context).hintColor, + size: const Size.square(16), + ), + onPressed: onTap, + ), + ); + } +} + class ChatAIMessagePopup extends StatelessWidget { const ChatAIMessagePopup({ super.key, required this.child, required this.message, + this.onRegenerate, }); final Widget child; final Message message; + final void Function(String)? onRegenerate; @override Widget build(BuildContext context) { @@ -393,33 +438,11 @@ class ChatAIMessagePopup extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - MobileQuickActionButton( - onTap: () async { - if (message is! TextMessage) { - return; - } - final textMessage = message as TextMessage; - final document = customMarkdownToDocument(textMessage.text); - await getIt().setData( - ClipboardServiceData( - plainText: textMessage.text, - inAppJson: jsonEncode(document.toJson()), - ), - ); - if (bottomSheetContext.mounted) { - Navigator.of(bottomSheetContext).pop(); - } - if (context.mounted) { - showToastNotification( - context, - message: LocaleKeys.grid_url_copiedNotification.tr(), - ); - } - }, - icon: FlowySvgs.copy_s, - iconSize: const Size.square(20), - text: LocaleKeys.button_copy.tr(), - ), + const VSpace(16.0), + _copyButton(context, bottomSheetContext), + const Divider(height: 8.5, thickness: 0.5), + _regenerateButton(context), + const Divider(height: 8.5, thickness: 0.5), ], ); }, @@ -428,4 +451,46 @@ class ChatAIMessagePopup extends StatelessWidget { child: child, ); } + + Widget _copyButton(BuildContext context, BuildContext bottomSheetContext) { + return MobileQuickActionButton( + onTap: () async { + if (message is! TextMessage) { + return; + } + final textMessage = message as TextMessage; + final document = customMarkdownToDocument(textMessage.text); + await getIt().setData( + ClipboardServiceData( + plainText: textMessage.text, + inAppJson: jsonEncode(document.toJson()), + ), + ); + if (bottomSheetContext.mounted) { + Navigator.of(bottomSheetContext).pop(); + } + if (context.mounted) { + showToastNotification( + context, + message: LocaleKeys.grid_url_copiedNotification.tr(), + ); + } + }, + icon: FlowySvgs.copy_s, + iconSize: const Size.square(20), + text: LocaleKeys.button_copy.tr(), + ); + } + + Widget _regenerateButton(BuildContext context) { + return MobileQuickActionButton( + onTap: () { + onRegenerate?.call(message.id); + Navigator.of(context).pop(); + }, + icon: FlowySvgs.ai_undo_s, + iconSize: const Size.square(20), + text: LocaleKeys.chat_regenerate.tr(), + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index c3c831ac61..f70de62f5b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -33,6 +33,7 @@ class ChatAIMessageWidget extends StatelessWidget { required this.chatId, required this.refSourceJsonString, this.onSelectedMetadata, + this.onRegenerate, this.isLastMessage = false, }); @@ -45,6 +46,7 @@ class ChatAIMessageWidget extends StatelessWidget { final String chatId; final String? refSourceJsonString; final void Function(ChatMessageRefSource metadata)? onSelectedMetadata; + final void Function(String messageId)? onRegenerate; final bool isLastMessage; @override @@ -80,6 +82,7 @@ class ChatAIMessageWidget extends StatelessWidget { message: message, isLastMessage: isLastMessage, showActions: stream == null && state.text.isNotEmpty, + onRegenerate: onRegenerate, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -101,11 +104,6 @@ class ChatAIMessageWidget extends StatelessWidget { onError: (error) { return ChatErrorMessageWidget( errorMessage: LocaleKeys.chat_aiServerUnavailable.tr(), - onRetry: () { - context - .read() - .add(const ChatAIMessageEvent.retry()); - }, ); }, onAIResponseLimit: () { diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 0da329aefe..1038047111 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -172,7 +172,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" 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=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bytes", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "again", "anyhow", @@ -944,7 +944,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "collab-entity", "collab-rt-entity", @@ -957,7 +957,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "futures-channel", "futures-util", @@ -1257,7 +1257,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bincode", @@ -1282,7 +1282,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "async-trait", @@ -1679,7 +1679,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "app-error", @@ -3268,7 +3268,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "futures-util", @@ -3285,7 +3285,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "app-error", @@ -3840,7 +3840,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bytes", @@ -6576,7 +6576,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 880141e7da..ff1c679503 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -59,7 +59,7 @@ collab-importer = { version = "0.1" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "abf827f2a6bebc70ef8829909dcf6acb3ce24715" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9f57fa63d5ee039f50b0d27b860679deb14fee2e" } [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 c614317cb6..1a8ac21464 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" 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=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bytes", @@ -877,7 +877,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "again", "anyhow", @@ -933,7 +933,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "collab-entity", "collab-rt-entity", @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "futures-channel", "futures-util", @@ -1255,7 +1255,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bincode", @@ -1280,7 +1280,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "async-trait", @@ -1684,7 +1684,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "app-error", @@ -3350,7 +3350,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "futures-util", @@ -3367,7 +3367,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "app-error", @@ -3927,7 +3927,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bytes", @@ -6656,7 +6656,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" 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 18b9b7ad51..6c5370c6dc 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -58,7 +58,7 @@ collab-importer = { version = "0.1" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "abf827f2a6bebc70ef8829909dcf6acb3ce24715" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9f57fa63d5ee039f50b0d27b860679deb14fee2e" } [dependencies] serde_json.workspace = true diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 2b2ea51b8f..c2dbfff3c3 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -180,8 +180,8 @@ "inputLocalAIMessageHint": "Ask @:appName Local AI", "unsupportedCloudPrompt": "This feature is only available when using @:appName Cloud", "relatedQuestion": "Suggested", - "serverUnavailable": "Service Temporarily Unavailable. Please try again later.", - "aiServerUnavailable": "Connection lost. Please check your internet and", + "serverUnavailable": "Connection lost. Please check your internet and", + "aiServerUnavailable": "The AI service is temporarily unavailable. Please try again later.", "retry": "Retry", "clickToRetry": "Click to retry", "regenerateAnswer": "Regenerate", @@ -205,7 +205,8 @@ "questionDetail": "Hi {}! How can I help you today?", "indexingFile": "Indexing {}", "generatingResponse": "Generating response", - "selectSources": "Select Sources" + "selectSources": "Select Sources", + "regenerate": "Try again" }, "trash": { "text": "Trash", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 481d681171..d777905546 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" 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=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bytes", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "again", "anyhow", @@ -836,7 +836,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "collab-entity", "collab-rt-entity", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "futures-channel", "futures-util", @@ -1118,7 +1118,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bincode", @@ -1143,7 +1143,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "async-trait", @@ -1389,7 +1389,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1538,7 +1538,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "app-error", @@ -2982,7 +2982,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "futures-util", @@ -2999,7 +2999,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "app-error", @@ -3484,7 +3484,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "bytes", @@ -4499,7 +4499,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4519,7 +4519,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -4587,19 +4586,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.47", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -5855,7 +5841,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=abf827f2a6bebc70ef8829909dcf6acb3ce24715#abf827f2a6bebc70ef8829909dcf6acb3ce24715" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9f57fa63d5ee039f50b0d27b860679deb14fee2e#9f57fa63d5ee039f50b0d27b860679deb14fee2e" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index c8fdd82085..be7c29c315 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -107,8 +107,8 @@ dashmap = "6.0.1" # 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 = "abf827f2a6bebc70ef8829909dcf6acb3ce24715" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "abf827f2a6bebc70ef8829909dcf6acb3ce24715" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9f57fa63d5ee039f50b0d27b860679deb14fee2e" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9f57fa63d5ee039f50b0d27b860679deb14fee2e" } [profile.dev] opt-level = 0 diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index e5e6920c27..a3fd90e07c 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -70,6 +70,13 @@ pub trait ChatCloudService: Send + Sync + 'static { limit: u64, ) -> Result; + async fn get_question_from_answer_id( + &self, + workspace_id: &str, + chat_id: &str, + answer_message_id: i64, + ) -> Result; + async fn get_related_message( &self, workspace_id: &str, diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index a0993c3a27..91e994763b 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -37,6 +37,8 @@ pub trait AIQueryService: Send + Sync + 'static { parent_view_id: &str, chat_id: &str, ) -> Result, FlowyError>; + + async fn sync_rag_documents(&self, rag_ids: Vec) -> Result<(), FlowyError>; } pub struct AIManager { @@ -102,6 +104,7 @@ impl AIManager { if self.local_ai_controller.is_running() { self.local_ai_controller.open_chat(chat_id); } + Ok(()) } @@ -193,6 +196,22 @@ impl AIManager { Ok(question) } + pub async fn stream_regenerate_response( + &self, + chat_id: &str, + answer_message_id: i64, + answer_stream_port: i64, + ) -> FlowyResult<()> { + let chat = self.get_or_create_chat_instance(chat_id).await?; + let question_message_id = chat + .get_question_id_from_answer_id(answer_message_id) + .await?; + chat + .stream_regenerate_response(question_message_id, answer_stream_port) + .await?; + Ok(()) + } + pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result, FlowyError> { let chat = self.chats.get(chat_id).as_deref().cloned(); match chat { diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 12d3b6c3d7..052e48a7a7 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -4,7 +4,10 @@ use crate::entities::{ }; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::notification::{chat_notification_builder, ChatNotification}; -use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable}; +use crate::persistence::{ + insert_chat_messages, select_chat_messages, select_message_where_match_reply_message_id, + ChatMessageTable, +}; use crate::stream_message::StreamMessage; use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ @@ -138,9 +141,60 @@ impl Chat { // Save message to disk save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; + self.stream_response( + answer_stream_port, + answer_stream_buffer, + uid, + workspace_id, + question.message_id, + ); + + let question_pb = ChatMessagePB::from(question); + Ok(question_pb) + } + + #[instrument(level = "info", skip_all, err)] + pub async fn stream_regenerate_response( + &self, + question_id: i64, + answer_stream_port: i64, + ) -> FlowyResult<()> { + trace!( + "[Chat] regenerate and stream chat message: chat_id={}", + self.chat_id, + ); + + // clear + self + .stop_stream + .store(false, std::sync::atomic::Ordering::SeqCst); + self.stream_buffer.lock().await.clear(); + + let answer_stream_buffer = self.stream_buffer.clone(); + let uid = self.user_service.user_id()?; + let workspace_id = self.user_service.workspace_id()?; + + self.stream_response( + answer_stream_port, + answer_stream_buffer, + uid, + workspace_id, + question_id, + ); + + Ok(()) + } + + fn stream_response( + &self, + answer_stream_port: i64, + answer_stream_buffer: Arc>, + uid: i64, + workspace_id: String, + question_id: i64, + ) { let stop_stream = self.stop_stream.clone(); let chat_id = self.chat_id.clone(); - let question_id = question.message_id; let cloud_service = self.chat_service.clone(); let user_service = self.user_service.clone(); tokio::spawn(async move { @@ -217,9 +271,6 @@ impl Chat { save_and_notify_message(uid, &chat_id, &user_service, answer)?; Ok::<(), FlowyError>(()) }); - - let question_pb = ChatMessagePB::from(question); - Ok(question_pb) } /// Load chat messages for a given `chat_id`. @@ -393,6 +444,30 @@ impl Chat { Ok(()) } + pub async fn get_question_id_from_answer_id( + &self, + answer_message_id: i64, + ) -> Result { + let conn = self.user_service.sqlite_connection(self.uid)?; + + let local_result = select_message_where_match_reply_message_id(conn, answer_message_id)? + .map(|message| message.message_id); + + if let Some(message_id) = local_result { + return Ok(message_id); + } + + let workspace_id = self.user_service.workspace_id()?; + let chat_id = self.chat_id.clone(); + let cloud_service = self.chat_service.clone(); + + let question = cloud_service + .get_question_from_answer_id(&workspace_id, &chat_id, answer_message_id) + .await?; + + Ok(question.message_id) + } + pub async fn get_related_question( &self, message_id: i64, diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index aba82d4b85..4eb80dd071 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -71,6 +71,19 @@ pub struct StreamChatPayloadPB { pub metadata: Vec, } +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct RegenerateResponsePB { + #[pb(index = 1)] + #[validate(custom(function = "required_not_empty_str"))] + pub chat_id: String, + + #[pb(index = 2)] + pub answer_message_id: i64, + + #[pb(index = 3)] + pub answer_stream_port: i64, +} + #[derive(Default, ProtoBuf, Validate, Clone, Debug)] pub struct ChatMessageMetaPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 748b4231ca..419ff5403f 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -77,6 +77,24 @@ pub(crate) async fn stream_chat_message_handler( data_result_ok(result) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn regenerate_response_handler( + data: AFPluginData, + ai_manager: AFPluginState>, +) -> FlowyResult<()> { + let data = data.try_into_inner()?; + + let ai_manager = upgrade_ai_manager(ai_manager)?; + ai_manager + .stream_regenerate_response( + &data.chat_id, + data.answer_message_id, + data.answer_stream_port, + ) + .await?; + Ok(()) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn load_prev_message_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index a12e81dc13..c27e94c334 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -59,6 +59,7 @@ pub fn init(ai_manager: Weak) -> AFPlugin { .event(AIEvent::GetChatInfo, create_chat_context_handler) .event(AIEvent::GetChatSettings, get_chat_settings_handler) .event(AIEvent::UpdateChatSettings, update_chat_settings_handler) + .event(AIEvent::RegenerateResponse, regenerate_response_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -150,4 +151,7 @@ pub enum AIEvent { #[event(input = "UpdateChatSettingsPB")] UpdateChatSettings = 26, + + #[event(input = "RegenerateResponsePB")] + RegenerateResponse = 27, } diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 35ee1e191c..251b049317 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -224,6 +224,18 @@ impl ChatCloudService for AICloudServiceMiddleware { .await } + async fn get_question_from_answer_id( + &self, + workspace_id: &str, + chat_id: &str, + answer_id: i64, + ) -> Result { + self + .cloud_service + .get_question_from_answer_id(workspace_id, chat_id, answer_id) + .await + } + async fn get_related_message( &self, workspace_id: &str, diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs index 91a806730b..aa4dd8215d 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs +++ b/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs @@ -82,3 +82,13 @@ pub fn select_single_message( .optional()?; Ok(message) } + +pub fn select_message_where_match_reply_message_id( + mut conn: DBConnection, + answer_message_id_val: i64, +) -> QueryResult> { + dsl::chat_message_table + .filter(chat_message_table::reply_message_id.eq(answer_message_id_val)) + .first::(&mut *conn) + .optional() +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index 8b08109b13..7618caf737 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -56,6 +56,15 @@ impl AIQueryService for ChatQueryServiceImpl { Ok(ids) } + + async fn sync_rag_documents(&self, rag_ids: Vec) -> Result<(), FlowyError> { + for rag_id in rag_ids.iter() { + if let Some(_query_collab) = self.folder_query.get_collab(rag_id).await { + // TODO(nathan): sync + } + } + Ok(()) + } } struct ChatUserServiceImpl(Weak); diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index f7f3692e12..27b02b8c77 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -34,7 +34,7 @@ use tokio::sync::RwLock; use crate::integrate::server::ServerProvider; use collab_plugins::local_storage::kv::KVTransactionDB; -use flowy_folder_pub::query::FolderQueryService; +use flowy_folder_pub::query::{FolderQueryService, QueryCollab}; use lib_infra::async_trait::async_trait; pub struct FolderDepsResolver(); @@ -707,4 +707,9 @@ impl FolderQueryService for FolderQueryServiceImpl { }, } } + + async fn get_collab(&self, _object_id: &str) -> Option { + // TODO(nathan): return query collab + None + } } 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 a40aca5ba2..d5b2d07123 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -715,6 +715,19 @@ impl ChatCloudService for ServerProvider { .await } + async fn get_question_from_answer_id( + &self, + workspace_id: &str, + chat_id: &str, + answer_message_id: i64, + ) -> Result { + self + .get_server()? + .chat_service() + .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) + .await + } + async fn get_related_message( &self, workspace_id: &str, diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index ecdaaa0336..a9d189ff78 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -356,6 +356,9 @@ pub enum ErrorCode { #[error("Requested namespace has one or more invalid characters")] CustomNamespaceInvalidCharacter = 122, + + #[error("Requested namespace has one or more invalid characters")] + AIServiceUnavailable = 123, } 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 2eb1e5f348..74e243de93 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -1,6 +1,5 @@ -use client_api::error::{AppResponseError, ErrorCode as AppErrorCode}; - use crate::{ErrorCode, FlowyError}; +use client_api::error::{AppResponseError, ErrorCode as AppErrorCode}; impl From for FlowyError { fn from(error: AppResponseError) -> Self { @@ -37,6 +36,7 @@ impl From for FlowyError { AppErrorCode::PublishNameInvalidCharacter => ErrorCode::PublishNameInvalidCharacter, AppErrorCode::PublishNameTooLong => ErrorCode::PublishNameTooLong, AppErrorCode::CustomNamespaceInvalidCharacter => ErrorCode::CustomNamespaceInvalidCharacter, + AppErrorCode::AIServiceUnavailable => ErrorCode::AIServiceUnavailable, _ => ErrorCode::Internal, }; diff --git a/frontend/rust-lib/flowy-folder-pub/src/query.rs b/frontend/rust-lib/flowy-folder-pub/src/query.rs index 73058e1ef2..52365a2803 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/query.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/query.rs @@ -1,6 +1,14 @@ +use collab::entity::EncodedCollab; +use collab_entity::CollabType; use collab_folder::ViewLayout; use lib_infra::async_trait::async_trait; +pub struct QueryCollab { + pub name: String, + pub collab_type: CollabType, + pub encoded_collab: EncodedCollab, +} + #[async_trait] pub trait FolderQueryService: Send + Sync + 'static { async fn get_sibling_ids_with_view_layout( @@ -8,4 +16,6 @@ pub trait FolderQueryService: Send + Sync + 'static { parent_view_id: &str, view_layout: ViewLayout, ) -> Vec; + + async fn get_collab(&self, object_id: &str) -> Option; } 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 f8efb9b137..f782563280 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 @@ -99,11 +99,11 @@ where message_id: i64, ) -> Result { let try_get_client = self.inner.try_get_client(); - let stream = try_get_client? + let result = try_get_client? .stream_answer_v2(workspace_id, chat_id, message_id) - .await - .map_err(FlowyError::from)? - .map_err(FlowyError::from); + .await; + + let stream = result.map_err(FlowyError::from)?.map_err(FlowyError::from); Ok(stream.boxed()) } @@ -137,6 +137,22 @@ where Ok(resp) } + async fn get_question_from_answer_id( + &self, + workspace_id: &str, + chat_id: &str, + answer_message_id: i64, + ) -> Result { + let try_get_client = self.inner.try_get_client()?; + let resp = try_get_client + .get_question_message_from_answer_id(workspace_id, chat_id, answer_message_id) + .await + .map_err(FlowyError::from)? + .ok_or_else(FlowyError::record_not_found)?; + + Ok(resp) + } + async fn get_related_message( &self, workspace_id: &str, diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index bfde5217b0..7b7a201bd5 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -64,6 +64,15 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) } + async fn get_question_from_answer_id( + &self, + _workspace_id: &str, + _chat_id: &str, + _answer_id: i64, + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + async fn get_related_message( &self, _workspace_id: &str,