From ab8e01bbf7040066e692e4d81a70a5c88a057df2 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:43:03 +0800 Subject: [PATCH] feat(flutter): pre-defined response formats (#7128) * feat: pre-defined response formats * chore: adjust bottom sheet * chore: rename and clean up enums * chore: move all mobile input actions to the bottom * chore: bump client-api * chore: connect to API * chore: apply suggestions from code review Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> * chore: code cleanup * chore: code cleanup * chore: update client-api * chore: expand page * chore: simplify logic for not displaying related questions * chore: remove hover effect view icon in select sources * chore: regenerate with different format * chore: remove error messages when sending new one * chore: code style * chore: bump client api * fix: image not displaying and hide editing options * chore: don't fetch related questions for image only * chore: fix clippy * chore: don't add related questions on regenerate * chore: bump editor * fix: expand sidebar page * chore: update client api --------- Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Co-authored-by: weidong fu --- .../ai_chat/application/chat_bloc.dart | 66 +- .../ai_chat/application/chat_entity.dart | 89 +- .../lib/plugins/ai_chat/chat_page.dart | 11 +- .../chat_input/chat_mention_page_menu.dart | 2 +- .../chat_input/desktop_ai_prompt_input.dart | 113 ++- .../chat_input/mobile_ai_prompt_input.dart | 96 +- .../chat_input/predefined_format_buttons.dart | 225 +++++ .../presentation/chat_related_question.dart | 2 +- .../ai_chat/presentation/layout_define.dart | 20 +- .../ai_change_format_bottom_sheet.dart | 193 ++++ .../message/ai_message_action_bar.dart | 208 ++++- .../message/ai_message_bubble.dart | 37 +- .../presentation/message/ai_text_message.dart | 3 + .../image_menu.dart | 34 +- .../layouts/image_browser_layout.dart | 3 +- .../image/resizeable_image.dart | 3 +- .../workspace/application/tabs/tabs_bloc.dart | 9 +- .../presentation/home/home_stack.dart | 23 +- .../widgets/image_viewer/image_provider.dart | 4 +- .../interactive_image_viewer.dart | 5 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../flowy_icons/16x/ai_retry_filled.svg | 53 +- .../flowy_icons/16x/ai_text_auto.svg | 6 + frontend/resources/translations/en.json | 19 +- frontend/rust-lib/Cargo.lock | 837 +++++++++++++----- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-ai-pub/src/cloud.rs | 4 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 35 +- frontend/rust-lib/flowy-ai/src/chat.rs | 58 +- frontend/rust-lib/flowy-ai/src/entities.rs | 70 +- .../rust-lib/flowy-ai/src/event_handler.rs | 38 +- .../src/middleware/chat_service_mw.rs | 6 +- .../src/deps_resolve/cloud_service_impl.rs | 7 +- .../flowy-server/src/af_cloud/impls/chat.rs | 21 +- .../rust-lib/flowy-server/src/default_impl.rs | 4 +- 36 files changed, 1914 insertions(+), 400 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/predefined_format_buttons.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart create mode 100644 frontend/resources/flowy_icons/16x/ai_text_auto.svg 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 c18389e262..d8cb97e7a3 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 @@ -59,7 +59,8 @@ class ChatBloc extends Bloc { bool isLoadingPreviousMessages = false; bool hasMorePreviousMessages = true; AnswerStream? answerStream; - int numSendMessage = 0; + bool isFetchingRelatedQuestions = false; + bool shouldFetchRelatedQuestions = false; @override Future close() async { @@ -165,14 +166,18 @@ class ChatBloc extends Bloc { }, sendMessage: ( String message, + PredefinedFormat? format, Map? metadata, ) { - numSendMessage += 1; - + _clearErrorMessages(); _clearRelatedQuestions(); - _startStreamingMessage(message, metadata); + _startStreamingMessage(message, format, metadata); lastSentMessage = null; + isFetchingRelatedQuestions = false; + shouldFetchRelatedQuestions = + format == null || format.imageFormat.hasText; + emit( state.copyWith( promptResponseState: PromptResponseState.sendingQuestion, @@ -231,11 +236,14 @@ class ChatBloc extends Bloc { ), ); }, - regenerateAnswer: (id) { + regenerateAnswer: (id, format) { _clearRelatedQuestions(); - _regenerateAnswer(id); + _regenerateAnswer(id, format); lastSentMessage = null; + isFetchingRelatedQuestions = false; + shouldFetchRelatedQuestions = false; + emit( state.copyWith( promptResponseState: PromptResponseState.sendingQuestion, @@ -319,7 +327,9 @@ class ChatBloc extends Bloc { // The answer stream will bet set to null after the streaming has // finished, got cancelled, or errored. In this case, don't retrieve // related questions. - if (answerStream == null || lastSentMessage == null) { + if (answerStream == null || + lastSentMessage == null || + !shouldFetchRelatedQuestions) { return; } @@ -328,17 +338,19 @@ class ChatBloc extends Bloc { messageId: lastSentMessage!.messageId, ); - // when previous numSendMessage is not equal to current numSendMessage, it means that the user - // has sent a new message. So we don't need to get related questions. - final preNumSendMessage = numSendMessage; + isFetchingRelatedQuestions = true; await AIEventGetRelatedQuestion(payload).send().fold( (list) { - if (!isClosed && preNumSendMessage == numSendMessage) { + // while fetching related questions, the user might enter a new + // question or regenerate a previous response. In such cases, don't + // display the relatedQuestions + if (!isClosed && isFetchingRelatedQuestions) { add( ChatEvent.didReceiveRelatedQuestions( list.items.map((e) => e.content).toList(), ), ); + isFetchingRelatedQuestions = false; } }, (err) => Log.error("Failed to get related questions: $err"), @@ -398,6 +410,7 @@ class ChatBloc extends Bloc { Future _startStreamingMessage( String message, + PredefinedFormat? format, Map? metadata, ) async { await answerStream?.dispose(); @@ -420,6 +433,9 @@ class ChatBloc extends Bloc { answerStreamPort: Int64(answerStream!.nativePort), metadata: await metadataPBFromMetadata(metadata), ); + if (format != null) { + payload.format = format.toPB(); + } // stream the question to the server await AIEventStreamMessage(payload).send().fold( @@ -460,7 +476,10 @@ class ChatBloc extends Bloc { ); } - void _regenerateAnswer(String answerMessageIdString) async { + void _regenerateAnswer( + String answerMessageIdString, + PredefinedFormat? format, + ) async { final id = temporaryMessageIDMap.entries .firstWhereOrNull((e) => e.value == answerMessageIdString) ?.key ?? @@ -479,6 +498,9 @@ class ChatBloc extends Bloc { answerMessageId: answerMessageId, answerStreamPort: Int64(answerStream!.nativePort), ); + if (format != null) { + payload.format = format.toPB(); + } await AIEventRegenerateResponse(payload).send().fold( (success) { @@ -558,6 +580,20 @@ class ChatBloc extends Bloc { ); } + void _clearErrorMessages() { + final errorMessages = chatController.messages + .where( + (message) => + onetimeMessageTypeFromMeta(message.metadata) == + OnetimeShotType.error, + ) + .toList(); + + for (final message in errorMessages) { + chatController.remove(message); + } + } + void _clearRelatedQuestions() { final relatedQuestionMessages = chatController.messages .where( @@ -586,13 +622,17 @@ class ChatEvent with _$ChatEvent { // send message const factory ChatEvent.sendMessage({ required String message, + PredefinedFormat? format, Map? metadata, }) = _SendMessage; const factory ChatEvent.finishSending() = _FinishSendMessage; const factory ChatEvent.failedSending() = _FailSendMessage; // regenerate - const factory ChatEvent.regenerateAnswer(String id) = _RegenerateAnswer; + const factory ChatEvent.regenerateAnswer( + String id, + PredefinedFormat? format, + ) = _RegenerateAnswer; // streaming answer const factory ChatEvent.stopStream() = _StopStream; 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 4494eca899..fe2ed44193 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 @@ -1,8 +1,11 @@ import 'dart:io'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:path/path.dart' as path; @@ -137,3 +140,87 @@ enum LoadChatMessageStatus { loadingRemote, ready, } + +class PredefinedFormat extends Equatable { + const PredefinedFormat({ + required this.imageFormat, + required this.textFormat, + }); + + const PredefinedFormat.auto() + : imageFormat = ImageFormat.text, + textFormat = TextFormat.auto; + + final ImageFormat imageFormat; + final TextFormat? textFormat; + + PredefinedFormatPB toPB() { + return PredefinedFormatPB( + imageFormat: switch (imageFormat) { + ImageFormat.text => ResponseImageFormatPB.TextOnly, + ImageFormat.image => ResponseImageFormatPB.ImageOnly, + ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage, + }, + textFormat: switch (textFormat) { + TextFormat.auto => ResponseTextFormatPB.Paragraph, + TextFormat.bulletList => ResponseTextFormatPB.BulletedList, + TextFormat.numberedList => ResponseTextFormatPB.NumberedList, + TextFormat.table => ResponseTextFormatPB.Table, + _ => null, + }, + ); + } + + @override + List get props => [imageFormat, textFormat]; +} + +enum ImageFormat { + text, + image, + textAndImage; + + bool get hasText => this == text || this == textAndImage; + + FlowySvgData get icon { + return switch (this) { + ImageFormat.text => FlowySvgs.ai_text_s, + ImageFormat.image => FlowySvgs.ai_image_s, + ImageFormat.textAndImage => FlowySvgs.ai_text_image_s, + }; + } + + String get i18n { + return switch (this) { + ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(), + ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(), + ImageFormat.textAndImage => + LocaleKeys.chat_changeFormat_textAndImage.tr(), + }; + } +} + +enum TextFormat { + auto, + bulletList, + numberedList, + table; + + FlowySvgData get icon { + return switch (this) { + TextFormat.auto => FlowySvgs.ai_paragraph_s, + TextFormat.bulletList => FlowySvgs.ai_list_s, + TextFormat.numberedList => FlowySvgs.ai_number_list_s, + TextFormat.table => FlowySvgs.ai_table_s, + }; + } + + String get i18n { + return switch (this) { + TextFormat.auto => LocaleKeys.chat_changeFormat_text.tr(), + TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(), + TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(), + TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(), + }; + } +} 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 dc6d9c649c..db27787292 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -214,7 +214,10 @@ class _ChatContentPage extends StatelessWidget { _onSelectMetadata(context, metadata), onRegenerate: () => context .read() - .add(ChatEvent.regenerateAnswer(message.id)), + .add(ChatEvent.regenerateAnswer(message.id, null)), + onChangeFormat: (format) => context + .read() + .add(ChatEvent.regenerateAnswer(message.id, format)), ); }, ); @@ -288,10 +291,11 @@ class _ChatContentPage extends StatelessWidget { onStopStreaming: () { chatBloc.add(const ChatEvent.stopStream()); }, - onSubmitted: (text, metadata) { + onSubmitted: (text, format, metadata) { chatBloc.add( ChatEvent.sendMessage( message: text, + format: format, metadata: metadata, ), ); @@ -310,10 +314,11 @@ class _ChatContentPage extends StatelessWidget { onStopStreaming: () { chatBloc.add(const ChatEvent.stopStream()); }, - onSubmitted: (text, metadata) { + onSubmitted: (text, format, metadata) { chatBloc.add( ChatEvent.sendMessage( message: text, + format: format, metadata: metadata, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_mention_page_menu.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_mention_page_menu.dart index 6f3eb2d6db..0a5d32838f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_mention_page_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_mention_page_menu.dart @@ -310,7 +310,7 @@ class MentionViewIcon extends StatelessWidget { if (view.icon.value.isNotEmpty) { return SizedBox( width: 16.0, - child: EmojiIconWidget( + child: RawEmojiIconWidget( emoji: view.icon.toEmojiIconData(), emojiSize: 14, ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart index 0e3ca31980..9e97b85aec 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart @@ -11,11 +11,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../application/chat_entity.dart'; import '../layout_define.dart'; import 'ai_prompt_buttons.dart'; import 'chat_input_file.dart'; import 'chat_input_span.dart'; import 'chat_mention_page_menu.dart'; +import 'predefined_format_buttons.dart'; import 'select_sources_menu.dart'; class DesktopAIPromptInput extends StatefulWidget { @@ -31,7 +33,8 @@ class DesktopAIPromptInput extends StatefulWidget { final String chatId; final bool isStreaming; final void Function() onStopStreaming; - final void Function(String, Map) onSubmitted; + final void Function(String, PredefinedFormat?, Map) + onSubmitted; final void Function(List) onUpdateSelectedSources; @override @@ -46,6 +49,8 @@ class _DesktopAIPromptInputState extends State { final focusNode = FocusNode(); final textController = TextEditingController(); + bool showPredefinedFormatSection = false; + PredefinedFormat predefinedFormat = const PredefinedFormat.auto(); late SendButtonState sendButtonState; @override @@ -115,6 +120,7 @@ class _DesktopAIPromptInputState extends State { ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.outline, width: focusNode.hasFocus ? 1.5 : 1.0, + strokeAlign: BorderSide.strokeAlignOutside, ), borderRadius: DesktopAIPromptSizes.promptFrameRadius, ), @@ -136,17 +142,35 @@ class _DesktopAIPromptInputState extends State { ), ), ), + const VSpace(4.0), Stack( children: [ - ConstrainedBox( - constraints: BoxConstraints( - minHeight: DesktopAIPromptSizes.textFieldMinHeight + - DesktopAIPromptSizes.actionBarHeight + - DesktopAIPromptSizes.actionBarPadding.vertical, - maxHeight: 300, - ), + Container( + constraints: getTextFieldConstraints(), child: inputTextField(), ), + if (showPredefinedFormatSection) + Positioned.fill( + bottom: null, + child: TextFieldTapRegion( + child: Padding( + padding: + const EdgeInsetsDirectional.only(start: 8.0), + child: ChangeFormatBar( + predefinedFormat: predefinedFormat, + spacing: DesktopAIPromptSizes + .predefinedFormatBarButtonSpacing, + iconSize: DesktopAIPromptSizes + .predefinedFormatIconHeight, + buttonSize: DesktopAIPromptSizes + .predefinedFormatButtonHeight, + onSelectPredefinedFormat: (format) { + setState(() => predefinedFormat = format); + }, + ), + ), + ), + ), Positioned.fill( top: null, child: TextFieldTapRegion( @@ -154,6 +178,19 @@ class _DesktopAIPromptInputState extends State { textController: textController, overlayController: overlayController, focusNode: focusNode, + showPredefinedFormats: showPredefinedFormatSection, + predefinedFormat: predefinedFormat.imageFormat, + predefinedTextFormat: predefinedFormat.textFormat, + onTogglePredefinedFormatSection: () { + setState(() { + showPredefinedFormatSection = + !showPredefinedFormatSection; + if (!showPredefinedFormatSection) { + predefinedFormat = + const PredefinedFormat.auto(); + } + }); + }, sendButtonState: sendButtonState, onSendPressed: handleSendPressed, onStopStreaming: widget.onStopStreaming, @@ -172,6 +209,18 @@ class _DesktopAIPromptInputState extends State { ); } + BoxConstraints getTextFieldConstraints() { + double minHeight = DesktopAIPromptSizes.textFieldMinHeight + + DesktopAIPromptSizes.actionBarHeight + + DesktopAIPromptSizes.actionBarPadding.vertical; + double maxHeight = 300; + if (showPredefinedFormatSection) { + minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight; + maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight; + } + return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight); + } + void cancelMentionPage() { if (overlayController.isShowing) { inputControlCubit.reset(); @@ -204,7 +253,11 @@ class _DesktopAIPromptInputState extends State { // get the attached files and mentioned pages final metadata = context.read().consumeMetadata(); - widget.onSubmitted(trimmedText, metadata); + widget.onSubmitted( + trimmedText, + showPredefinedFormatSection ? predefinedFormat : null, + metadata, + ); } void handleTextControllerChanged() { @@ -301,6 +354,7 @@ class _DesktopAIPromptInputState extends State { cubit: inputControlCubit, textController: textController, textFieldFocusNode: focusNode, + showPredefinedFormatSection: showPredefinedFormatSection, hintText: switch (state.aiType) { AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(), AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr() @@ -366,15 +420,17 @@ class _PromptTextField extends StatefulWidget { required this.cubit, required this.textController, required this.textFieldFocusNode, - // required this.onStartMentioningPage, + this.showPredefinedFormatSection = false, this.hintText = "", + // this.onStartMentioningPage, }); final ChatInputControlCubit cubit; final TextEditingController textController; final FocusNode textFieldFocusNode; - // final void Function() onStartMentioningPage; + final bool showPredefinedFormatSection; final String hintText; + // final void Function()? onStartMentioningPage; @override State<_PromptTextField> createState() => _PromptTextFieldState(); @@ -408,11 +464,7 @@ class _PromptTextFieldState extends State<_PromptTextField> { border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, - contentPadding: DesktopAIPromptSizes.textFieldContentPadding.add( - const EdgeInsets.only( - bottom: DesktopAIPromptSizes.actionBarHeight, - ), - ), + contentPadding: calculateContentPadding(), hintText: widget.hintText, hintStyle: Theme.of(context) .textTheme @@ -450,6 +502,16 @@ class _PromptTextFieldState extends State<_PromptTextField> { ); } + EdgeInsetsGeometry calculateContentPadding() { + final top = widget.showPredefinedFormatSection + ? DesktopAIPromptSizes.predefinedFormatButtonHeight + : 0.0; + const bottom = DesktopAIPromptSizes.actionBarHeight; + + return DesktopAIPromptSizes.textFieldContentPadding + .add(EdgeInsets.only(top: top, bottom: bottom)); + } + Map buildShortcuts() { if (isComposing) { return const {}; @@ -486,6 +548,10 @@ class _PromptBottomActions extends StatelessWidget { required this.overlayController, required this.focusNode, required this.sendButtonState, + required this.predefinedFormat, + required this.predefinedTextFormat, + required this.onTogglePredefinedFormatSection, + required this.showPredefinedFormats, required this.onSendPressed, required this.onStopStreaming, required this.onUpdateSelectedSources, @@ -494,6 +560,10 @@ class _PromptBottomActions extends StatelessWidget { final TextEditingController textController; final OverlayPortalController overlayController; final FocusNode focusNode; + final bool showPredefinedFormats; + final ImageFormat predefinedFormat; + final TextFormat? predefinedTextFormat; + final void Function() onTogglePredefinedFormatSection; final SendButtonState sendButtonState; final void Function() onSendPressed; final void Function() onStopStreaming; @@ -514,7 +584,7 @@ class _PromptBottomActions extends StatelessWidget { } return Row( children: [ - // predefinedFormatButton(), + _predefinedFormatButton(), const Spacer(), if (state.aiType == AIType.appflowyAI) ...[ _selectSourcesButton(context), @@ -540,6 +610,15 @@ class _PromptBottomActions extends StatelessWidget { ); } + Widget _predefinedFormatButton() { + return PromptInputDesktopToggleFormatButton( + showFormatBar: showPredefinedFormats, + predefinedFormat: predefinedFormat, + predefinedTextFormat: predefinedTextFormat, + onTap: onTogglePredefinedFormatSection, + ); + } + Widget _selectSourcesButton(BuildContext context) { return PromptInputDesktopSelectSourcesButton( onUpdateSelectedSources: onUpdateSelectedSources, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_ai_prompt_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_ai_prompt_input.dart index 7dbda11bfb..225aa8e5f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_ai_prompt_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_ai_prompt_input.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -13,6 +14,7 @@ import 'ai_prompt_buttons.dart'; import 'chat_input_file.dart'; import 'chat_input_span.dart'; import 'chat_mention_page_bottom_sheet.dart'; +import 'predefined_format_buttons.dart'; import 'select_sources_bottom_sheet.dart'; class MobileAIPromptInput extends StatefulWidget { @@ -28,7 +30,8 @@ class MobileAIPromptInput extends StatefulWidget { final String chatId; final bool isStreaming; final void Function() onStopStreaming; - final void Function(String, Map) onSubmitted; + final void Function(String, PredefinedFormat?, Map) + onSubmitted; final void Function(List) onUpdateSelectedSources; @override @@ -40,6 +43,8 @@ class _MobileAIPromptInputState extends State { final focusNode = FocusNode(); final textController = TextEditingController(); + bool showPredefinedFormatSection = false; + PredefinedFormat predefinedFormat = const PredefinedFormat.auto(); late SendButtonState sendButtonState; @override @@ -103,6 +108,7 @@ class _MobileAIPromptInputState extends State { borderRadius: MobileAIPromptSizes.promptFrameRadius, ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( constraints: BoxConstraints( @@ -117,24 +123,33 @@ class _MobileAIPromptInputState extends State { .add(AIPromptInputEvent.removeFile(file)), ), ), - Container( - constraints: const BoxConstraints( - minHeight: MobileAIPromptSizes.textFieldMinHeight, - maxHeight: 220, - ), - child: IntrinsicHeight( - child: Row( - children: [ - const HSpace(8.0), - leadingButtons(context), - Expanded( - child: inputTextField(context), - ), - sendButton(), - const HSpace(12.0), - ], + if (showPredefinedFormatSection) + TextFieldTapRegion( + child: Container( + padding: MobileAIPromptSizes.predefinedFormatBarPadding, + child: ChangeFormatBar( + predefinedFormat: predefinedFormat, + spacing: MobileAIPromptSizes + .predefinedFormatBarButtonSpacing, + iconSize: + MobileAIPromptSizes.predefinedFormatIconHeight, + buttonSize: + MobileAIPromptSizes.predefinedFormatButtonHeight, + onSelectPredefinedFormat: (format) { + setState(() => predefinedFormat = format); + }, + ), ), ), + inputTextField(context), + Row( + children: [ + const HSpace(8.0), + leadingButtons(context), + const Spacer(), + sendButton(), + const HSpace(12.0), + ], ), ], ), @@ -169,7 +184,11 @@ class _MobileAIPromptInputState extends State { // get the attached files and mentioned pages final metadata = context.read().consumeMetadata(); - widget.onSubmitted(trimmedText, metadata); + widget.onSubmitted( + trimmedText, + showPredefinedFormatSection ? predefinedFormat : null, + metadata, + ); } void handleTextControllerChange() { @@ -236,6 +255,7 @@ class _MobileAIPromptInputState extends State { return ExtendedTextField( controller: textController, focusNode: focusNode, + textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, @@ -252,7 +272,8 @@ class _MobileAIPromptInputState extends State { textCapitalization: TextCapitalization.sentences, minLines: 1, maxLines: null, - style: Theme.of(context).textTheme.bodyMedium, + style: + Theme.of(context).textTheme.bodyMedium?.copyWith(height: 20 / 14), specialTextSpanBuilder: ChatInputTextSpanBuilder( inputControlCubit: inputControlCubit, specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( @@ -267,10 +288,8 @@ class _MobileAIPromptInputState extends State { Widget leadingButtons(BuildContext context) { return Container( - alignment: Alignment.bottomCenter, - padding: const EdgeInsets.only(bottom: 8.0), + padding: const EdgeInsets.symmetric(vertical: 8.0), child: _LeadingActions( - textController: textController, // onMention: () { // textController.text += '@'; // if (!focusNode.hasFocus) { @@ -280,6 +299,16 @@ class _MobileAIPromptInputState extends State { // mentionPage(context); // }); // }, + showPredefinedFormatSection: showPredefinedFormatSection, + predefinedFormat: predefinedFormat, + onTogglePredefinedFormatSection: () { + setState(() { + showPredefinedFormatSection = !showPredefinedFormatSection; + if (!showPredefinedFormatSection) { + predefinedFormat = const PredefinedFormat.auto(); + } + }); + }, onUpdateSelectedSources: widget.onUpdateSelectedSources, ), ); @@ -288,7 +317,6 @@ class _MobileAIPromptInputState extends State { Widget sendButton() { return Container( alignment: Alignment.bottomCenter, - padding: const EdgeInsets.only(bottom: 8.0), child: PromptInputSendButton( buttonSize: MobileAIPromptSizes.sendButtonSize, iconSize: MobileAIPromptSizes.sendButtonSize, @@ -302,19 +330,33 @@ class _MobileAIPromptInputState extends State { class _LeadingActions extends StatelessWidget { const _LeadingActions({ - required this.textController, + required this.showPredefinedFormatSection, + required this.predefinedFormat, + required this.onTogglePredefinedFormatSection, required this.onUpdateSelectedSources, }); - final TextEditingController textController; + final bool showPredefinedFormatSection; + final PredefinedFormat predefinedFormat; + final void Function() onTogglePredefinedFormatSection; final void Function(List) onUpdateSelectedSources; @override Widget build(BuildContext context) { return Material( color: Theme.of(context).colorScheme.surface, - child: PromptInputMobileSelectSourcesButton( - onUpdateSelectedSources: onUpdateSelectedSources, + child: SeparatedRow( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const HSpace(4.0), + children: [ + PromptInputMobileSelectSourcesButton( + onUpdateSelectedSources: onUpdateSelectedSources, + ), + PromptInputMobileToggleFormatButton( + showFormatBar: showPredefinedFormatSection, + onTap: onTogglePredefinedFormatSection, + ), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/predefined_format_buttons.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/predefined_format_buttons.dart new file mode 100644 index 0000000000..dfd563e039 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/predefined_format_buttons.dart @@ -0,0 +1,225 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; + +import '../layout_define.dart'; + +class PromptInputDesktopToggleFormatButton extends StatelessWidget { + const PromptInputDesktopToggleFormatButton({ + super.key, + required this.showFormatBar, + required this.predefinedFormat, + required this.predefinedTextFormat, + required this.onTap, + }); + + final bool showFormatBar; + final ImageFormat predefinedFormat; + final TextFormat? predefinedTextFormat; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: DesktopAIPromptSizes.actionBarButtonSize, + child: FlowyHover( + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + child: Padding( + padding: const EdgeInsetsDirectional.all(6.0), + child: FlowyText( + _getDescription(), + fontSize: 12.0, + figmaLineHeight: 16.0, + ), + ), + ), + ), + ); + } + + String _getDescription() { + if (!showFormatBar) { + return LocaleKeys.chat_changeFormat_blankDescription.tr(); + } + + return switch ((predefinedFormat, predefinedTextFormat)) { + (ImageFormat.image, _) => predefinedFormat.i18n, + (ImageFormat.text, TextFormat.auto) => + LocaleKeys.chat_changeFormat_defaultDescription.tr(), + (ImageFormat.text, _) when predefinedTextFormat != null => + predefinedTextFormat!.i18n, + (ImageFormat.textAndImage, TextFormat.auto) => + LocaleKeys.chat_changeFormat_textWithImageDescription.tr(), + (ImageFormat.textAndImage, TextFormat.bulletList) => + LocaleKeys.chat_changeFormat_bulletWithImageDescription.tr(), + (ImageFormat.textAndImage, TextFormat.numberedList) => + LocaleKeys.chat_changeFormat_numberWithImageDescription.tr(), + (ImageFormat.textAndImage, TextFormat.table) => + LocaleKeys.chat_changeFormat_tableWithImageDescription.tr(), + _ => throw UnimplementedError(), + }; + } +} + +class ChangeFormatBar extends StatelessWidget { + const ChangeFormatBar({ + super.key, + required this.predefinedFormat, + required this.buttonSize, + required this.iconSize, + required this.spacing, + required this.onSelectPredefinedFormat, + }); + + final PredefinedFormat predefinedFormat; + final double buttonSize; + final double iconSize; + final double spacing; + final void Function(PredefinedFormat) onSelectPredefinedFormat; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: DesktopAIPromptSizes.predefinedFormatButtonHeight, + child: SeparatedRow( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => HSpace(spacing), + children: [ + _buildFormatButton(context, ImageFormat.text), + _buildFormatButton(context, ImageFormat.textAndImage), + _buildFormatButton(context, ImageFormat.image), + if (predefinedFormat.imageFormat.hasText) ...[ + _buildDivider(), + _buildTextFormatButton(context, TextFormat.auto), + _buildTextFormatButton(context, TextFormat.bulletList), + _buildTextFormatButton(context, TextFormat.numberedList), + _buildTextFormatButton(context, TextFormat.table), + ], + ], + ), + ); + } + + Widget _buildFormatButton(BuildContext context, ImageFormat format) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (format == predefinedFormat.imageFormat) { + return; + } + if (format.hasText) { + final textFormat = predefinedFormat.textFormat ?? TextFormat.auto; + onSelectPredefinedFormat( + PredefinedFormat(imageFormat: format, textFormat: textFormat), + ); + } else { + onSelectPredefinedFormat( + PredefinedFormat(imageFormat: format, textFormat: null), + ); + } + }, + child: FlowyTooltip( + message: format.i18n, + child: SizedBox.square( + dimension: buttonSize, + child: FlowyHover( + isSelected: () => format == predefinedFormat.imageFormat, + child: Center( + child: FlowySvg( + format.icon, + size: format == ImageFormat.textAndImage + ? Size(21.0 / 16.0 * iconSize, iconSize) + : Size.square(iconSize), + ), + ), + ), + ), + ), + ); + } + + Widget _buildDivider() { + return VerticalDivider( + indent: 6.0, + endIndent: 6.0, + width: 1.0 + spacing * 2, + ); + } + + Widget _buildTextFormatButton( + BuildContext context, + TextFormat format, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (format == predefinedFormat.textFormat) { + return; + } + onSelectPredefinedFormat( + PredefinedFormat( + imageFormat: predefinedFormat.imageFormat, + textFormat: format, + ), + ); + }, + child: FlowyTooltip( + message: format.i18n, + child: SizedBox.square( + dimension: buttonSize, + child: FlowyHover( + isSelected: () => format == predefinedFormat.textFormat, + child: Center( + child: FlowySvg( + format.icon, + size: Size.square(iconSize), + ), + ), + ), + ), + ), + ); + } +} + +class PromptInputMobileToggleFormatButton extends StatelessWidget { + const PromptInputMobileToggleFormatButton({ + super.key, + required this.showFormatBar, + required this.onTap, + }); + + final bool showFormatBar; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: 32.0, + child: FlowyButton( + radius: const BorderRadius.all(Radius.circular(8.0)), + margin: EdgeInsets.zero, + expandText: false, + text: showFormatBar + ? const FlowySvg( + FlowySvgs.ai_text_auto_s, + size: Size.square(24.0), + ) + : const FlowySvg( + FlowySvgs.ai_text_image_s, + size: Size(26.25, 20.0), + ), + onTap: onTap, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart index fd937cc27f..deba1cb96d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart @@ -16,7 +16,7 @@ class RelatedQuestionList extends StatelessWidget { required this.relatedQuestions, }); - final Function(String) onQuestionSelected; + final void Function(String) onQuestionSelected; final List relatedQuestions; @override diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart index 45947ca2d6..ad39830313 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart @@ -23,13 +23,17 @@ class DesktopAIPromptSizes { static const promptFrameRadius = BorderRadius.all(Radius.circular(12.0)); static const attachedFilesBarPadding = - EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0); + EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0); static const attachedFilesPreviewHeight = 48.0; static const attachedFilesPreviewSpacing = 12.0; - static const textFieldMinHeight = 40.0; + static const predefinedFormatButtonHeight = 28.0; + static const predefinedFormatIconHeight = 16.0; + static const predefinedFormatBarButtonSpacing = 4.0; + + static const textFieldMinHeight = 36.0; static const textFieldContentPadding = - EdgeInsetsDirectional.fromSTEB(14.0, 12.0, 14.0, 8.0); + EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0); static const actionBarHeight = 32.0; static const actionBarPadding = EdgeInsetsDirectional.fromSTEB(8, 0, 8, 4); @@ -49,8 +53,14 @@ class MobileAIPromptSizes { static const attachedFilesPreviewHeight = 56.0; static const attachedFilesPreviewSpacing = 8.0; - static const textFieldMinHeight = 48.0; - static const textFieldContentPadding = EdgeInsets.all(8.0); + static const predefinedFormatButtonHeight = 32.0; + static const predefinedFormatIconHeight = 20.0; + static const predefinedFormatBarButtonSpacing = 8.0; + static const predefinedFormatBarPadding = EdgeInsets.all(8.0); + + static const textFieldMinHeight = 32.0; + static const textFieldContentPadding = + EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0); static const mentionIconSize = 20.0; static const sendButtonSize = 32.0; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart new file mode 100644 index 0000000000..5faebed704 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart @@ -0,0 +1,193 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +Future showChangeFormatBottomSheet( + BuildContext context, +) { + return showMobileBottomSheet( + context, + showDragHandle: true, + builder: (context) => const _ChangeFormatBottomSheetContent(), + ); +} + +class _ChangeFormatBottomSheetContent extends StatefulWidget { + const _ChangeFormatBottomSheetContent(); + + @override + State<_ChangeFormatBottomSheetContent> createState() => + _ChangeFormatBottomSheetContentState(); +} + +class _ChangeFormatBottomSheetContentState + extends State<_ChangeFormatBottomSheetContent> { + PredefinedFormat predefinedFormat = const PredefinedFormat.auto(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _Header( + onCancel: () => Navigator.of(context).pop(), + onDone: () => Navigator.of(context).pop(predefinedFormat), + ), + const VSpace(4.0), + _Body( + predefinedFormat: predefinedFormat, + onSelectPredefinedFormat: (format) { + setState(() => predefinedFormat = format); + }, + ), + const VSpace(16.0), + ], + ); + } +} + +class _Header extends StatelessWidget { + const _Header({ + required this.onCancel, + required this.onDone, + }); + + final VoidCallback onCancel; + final VoidCallback onDone; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 44.0, + child: Stack( + children: [ + Align( + alignment: Alignment.centerLeft, + child: AppBarBackButton( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + onTap: onCancel, + ), + ), + Align( + child: Container( + constraints: const BoxConstraints(maxWidth: 250), + child: FlowyText( + LocaleKeys.chat_changeFormat_actionButton.tr(), + fontSize: 17.0, + fontWeight: FontWeight.w500, + overflow: TextOverflow.ellipsis, + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: AppBarDoneButton( + onTap: onDone, + ), + ), + ], + ), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + required this.predefinedFormat, + required this.onSelectPredefinedFormat, + }); + + final PredefinedFormat predefinedFormat; + final void Function(PredefinedFormat) onSelectPredefinedFormat; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildFormatButton(ImageFormat.text, true), + _buildFormatButton(ImageFormat.textAndImage), + _buildFormatButton(ImageFormat.image), + const VSpace(32.0), + Opacity( + opacity: predefinedFormat.imageFormat.hasText ? 1 : 0, + child: Column( + children: [ + _buildTextFormatButton(TextFormat.auto, true), + _buildTextFormatButton(TextFormat.bulletList), + _buildTextFormatButton(TextFormat.numberedList), + _buildTextFormatButton(TextFormat.table), + ], + ), + ), + ], + ); + } + + Widget _buildFormatButton( + ImageFormat format, [ + bool isFirst = false, + ]) { + return FlowyOptionTile.checkbox( + text: format.i18n, + isSelected: format == predefinedFormat.imageFormat, + showTopBorder: isFirst, + leftIcon: FlowySvg( + format.icon, + size: format == ImageFormat.textAndImage + ? const Size(21.0 / 16.0 * 20, 20) + : const Size.square(20), + ), + onTap: () { + if (format == predefinedFormat.imageFormat) { + return; + } + if (format.hasText) { + final textFormat = predefinedFormat.textFormat ?? TextFormat.auto; + onSelectPredefinedFormat( + PredefinedFormat(imageFormat: format, textFormat: textFormat), + ); + } else { + onSelectPredefinedFormat( + PredefinedFormat(imageFormat: format, textFormat: null), + ); + } + }, + ); + } + + Widget _buildTextFormatButton( + TextFormat format, [ + bool isFirst = false, + ]) { + return FlowyOptionTile.checkbox( + text: format.i18n, + isSelected: format == predefinedFormat.textFormat, + showTopBorder: isFirst, + leftIcon: FlowySvg( + format.icon, + size: const Size.square(20), + ), + onTap: () { + if (format == predefinedFormat.textFormat) { + return; + } + onSelectPredefinedFormat( + PredefinedFormat( + imageFormat: predefinedFormat.imageFormat, + textFormat: format, + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index f34611f265..d72f28d8d4 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -5,6 +5,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; @@ -29,24 +30,34 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart'; +import '../chat_input/predefined_format_buttons.dart'; import '../chat_input/select_sources_menu.dart'; import '../layout_define.dart'; import 'message_util.dart'; -class AIMessageActionBar extends StatelessWidget { +class AIMessageActionBar extends StatefulWidget { const AIMessageActionBar({ super.key, required this.message, required this.showDecoration, this.onRegenerate, + this.onChangeFormat, this.onOverrideVisibility, }); final Message message; final bool showDecoration; final void Function()? onRegenerate; + final void Function(PredefinedFormat)? onChangeFormat; final void Function(bool)? onOverrideVisibility; + @override + State createState() => _AIMessageActionBarState(); +} + +class _AIMessageActionBarState extends State { + final popoverMutex = PopoverMutex(); + @override Widget build(BuildContext context) { final isLightMode = Theme.of(context).isLightMode; @@ -58,7 +69,7 @@ class AIMessageActionBar extends StatelessWidget { children: _buildChildren(), ); - return showDecoration + return widget.showDecoration ? Container( padding: const EdgeInsets.all(2.0), decoration: BoxDecoration( @@ -67,6 +78,7 @@ class AIMessageActionBar extends StatelessWidget { color: isLightMode ? const Color(0x1F1F2329) : Theme.of(context).dividerColor, + strokeAlign: BorderSide.strokeAlignOutside, ), color: Theme.of(context).cardColor, boxShadow: [ @@ -103,17 +115,24 @@ class AIMessageActionBar extends StatelessWidget { List _buildChildren() { return [ CopyButton( - isInHoverBar: showDecoration, - textMessage: message as TextMessage, + isInHoverBar: widget.showDecoration, + textMessage: widget.message as TextMessage, ), RegenerateButton( - isInHoverBar: showDecoration, - onTap: () => onRegenerate?.call(), + isInHoverBar: widget.showDecoration, + onTap: () => widget.onRegenerate?.call(), + ), + ChangeFormatButton( + isInHoverBar: widget.showDecoration, + onRegenerate: widget.onChangeFormat, + popoverMutex: popoverMutex, + onOverrideVisibility: widget.onOverrideVisibility, ), SaveToPageButton( - textMessage: message as TextMessage, - isInHoverBar: showDecoration, - onOverrideVisibility: onOverrideVisibility, + textMessage: widget.message as TextMessage, + isInHoverBar: widget.showDecoration, + popoverMutex: popoverMutex, + onOverrideVisibility: widget.onOverrideVisibility, ), ]; } @@ -195,16 +214,186 @@ class RegenerateButton extends StatelessWidget { } } +class ChangeFormatButton extends StatefulWidget { + const ChangeFormatButton({ + super.key, + required this.isInHoverBar, + this.popoverMutex, + this.onRegenerate, + this.onOverrideVisibility, + }); + + final bool isInHoverBar; + final PopoverMutex? popoverMutex; + final void Function(PredefinedFormat)? onRegenerate; + final void Function(bool)? onOverrideVisibility; + + @override + State createState() => _ChangeFormatButtonState(); +} + +class _ChangeFormatButtonState extends State { + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + mutex: widget.popoverMutex, + triggerActions: PopoverTriggerFlags.none, + margin: EdgeInsets.zero, + offset: Offset(0, widget.isInHoverBar ? 8 : 4), + direction: PopoverDirection.bottomWithLeftAligned, + constraints: const BoxConstraints(), + onClose: () => widget.onOverrideVisibility?.call(false), + child: buildButton(context), + popupBuilder: (_) => _ChangeFormatPopoverContent( + onRegenerate: widget.onRegenerate, + ), + ); + } + + Widget buildButton(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.chat_changeFormat_actionButton.tr(), + child: FlowyIconButton( + width: 32.0, + height: DesktopAIConvoSizes.actionBarIconSize, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + radius: widget.isInHoverBar + ? DesktopAIConvoSizes.hoverActionBarIconRadius + : DesktopAIConvoSizes.actionBarIconRadius, + icon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + FlowySvgs.ai_retry_font_s, + color: Theme.of(context).hintColor, + size: const Size.square(16), + ), + FlowySvg( + FlowySvgs.ai_source_drop_down_s, + color: Theme.of(context).hintColor, + size: const Size.square(8), + ), + ], + ), + onPressed: () { + widget.onOverrideVisibility?.call(true); + popoverController.show(); + }, + ), + ); + } +} + +class _ChangeFormatPopoverContent extends StatefulWidget { + const _ChangeFormatPopoverContent({ + this.onRegenerate, + }); + + final void Function(PredefinedFormat)? onRegenerate; + + @override + State<_ChangeFormatPopoverContent> createState() => + _ChangeFormatPopoverContentState(); +} + +class _ChangeFormatPopoverContentState + extends State<_ChangeFormatPopoverContent> { + PredefinedFormat predefinedFormat = const PredefinedFormat.auto(); + + @override + Widget build(BuildContext context) { + final isLightMode = Theme.of(context).isLightMode; + return Container( + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + borderRadius: DesktopAIConvoSizes.hoverActionBarRadius, + border: Border.all( + color: isLightMode + ? const Color(0x1F1F2329) + : Theme.of(context).dividerColor, + strokeAlign: BorderSide.strokeAlignOutside, + ), + color: Theme.of(context).cardColor, + boxShadow: [ + BoxShadow( + offset: const Offset(0, 1), + blurRadius: 2, + spreadRadius: -2, + color: isLightMode + ? const Color(0x051F2329) + : Theme.of(context).shadowColor.withOpacity(0.02), + ), + BoxShadow( + offset: const Offset(0, 2), + blurRadius: 4, + color: isLightMode + ? const Color(0x051F2329) + : Theme.of(context).shadowColor.withOpacity(0.02), + ), + BoxShadow( + offset: const Offset(0, 2), + blurRadius: 8, + spreadRadius: 2, + color: isLightMode + ? const Color(0x051F2329) + : Theme.of(context).shadowColor.withOpacity(0.02), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ChangeFormatBar( + spacing: 2.0, + iconSize: 16.0, + buttonSize: DesktopAIPromptSizes.predefinedFormatButtonHeight, + predefinedFormat: predefinedFormat, + onSelectPredefinedFormat: (format) { + setState(() => predefinedFormat = format); + }, + ), + const HSpace(4.0), + FlowyTooltip( + message: LocaleKeys.chat_changeFormat_confirmButton.tr(), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => widget.onRegenerate?.call(predefinedFormat), + child: SizedBox.square( + dimension: DesktopAIPromptSizes.predefinedFormatButtonHeight, + child: Center( + child: FlowySvg( + FlowySvgs.ai_retry_filled_s, + color: Theme.of(context).colorScheme.primary, + size: const Size.square(20), + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} + class SaveToPageButton extends StatefulWidget { const SaveToPageButton({ super.key, required this.textMessage, required this.isInHoverBar, + this.popoverMutex, this.onOverrideVisibility, }); final TextMessage textMessage; final bool isInHoverBar; + final PopoverMutex? popoverMutex; final void Function(bool)? onOverrideVisibility; @override @@ -240,6 +429,7 @@ class _SaveToPageButtonState extends State { controller: popoverController, triggerActions: PopoverTriggerFlags.none, margin: EdgeInsets.zero, + mutex: widget.popoverMutex, offset: const Offset(8, 0), direction: PopoverDirection.rightWithBottomAligned, constraints: const BoxConstraints.tightFor(width: 300, height: 400), 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 cc4116d406..1e5598db9e 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 @@ -5,7 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_mention_page_bottom_sheet.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_entity.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'; @@ -20,8 +20,10 @@ import 'package:go_router/go_router.dart'; import 'package:universal_platform/universal_platform.dart'; import '../chat_avatar.dart'; +import '../chat_input/chat_mention_page_bottom_sheet.dart'; import '../layout_define.dart'; import 'ai_message_action_bar.dart'; +import 'ai_change_format_bottom_sheet.dart'; import 'message_util.dart'; /// Wraps an AI response message with the avatar and actions. On desktop, @@ -36,6 +38,7 @@ class ChatAIMessageBubble extends StatelessWidget { required this.showActions, this.isLastMessage = false, this.onRegenerate, + this.onChangeFormat, }); final Message message; @@ -43,6 +46,7 @@ class ChatAIMessageBubble extends StatelessWidget { final bool showActions; final bool isLastMessage; final void Function()? onRegenerate; + final void Function(PredefinedFormat)? onChangeFormat; @override Widget build(BuildContext context) { @@ -68,6 +72,7 @@ class ChatAIMessageBubble extends StatelessWidget { return ChatAIBottomInlineActions( message: message, onRegenerate: onRegenerate, + onChangeFormat: onChangeFormat, child: child, ); } @@ -76,6 +81,7 @@ class ChatAIMessageBubble extends StatelessWidget { return ChatAIMessageHover( message: message, onRegenerate: onRegenerate, + onChangeFormat: onChangeFormat, child: child, ); } @@ -84,6 +90,7 @@ class ChatAIMessageBubble extends StatelessWidget { return ChatAIMessagePopup( message: message, onRegenerate: onRegenerate, + onChangeFormat: onChangeFormat, child: child, ); } @@ -95,11 +102,13 @@ class ChatAIBottomInlineActions extends StatelessWidget { required this.child, required this.message, this.onRegenerate, + this.onChangeFormat, }); final Widget child; final Message message; final void Function()? onRegenerate; + final void Function(PredefinedFormat)? onChangeFormat; @override Widget build(BuildContext context) { @@ -117,6 +126,7 @@ class ChatAIBottomInlineActions extends StatelessWidget { message: message, showDecoration: false, onRegenerate: onRegenerate, + onChangeFormat: onChangeFormat, ), ), const VSpace(32.0), @@ -131,11 +141,13 @@ class ChatAIMessageHover extends StatefulWidget { required this.child, required this.message, this.onRegenerate, + this.onChangeFormat, }); final Widget child; final Message message; final void Function()? onRegenerate; + final void Function(PredefinedFormat)? onChangeFormat; @override State createState() => _ChatAIMessageHoverState(); @@ -217,6 +229,7 @@ class _ChatAIMessageHoverState extends State { message: widget.message, showDecoration: true, onRegenerate: widget.onRegenerate, + onChangeFormat: widget.onChangeFormat, onOverrideVisibility: (visibility) { overrideVisibility = visibility; }, @@ -288,11 +301,13 @@ class ChatAIMessagePopup extends StatelessWidget { required this.child, required this.message, this.onRegenerate, + this.onChangeFormat, }); final Widget child; final Message message; final void Function()? onRegenerate; + final void Function(PredefinedFormat)? onChangeFormat; @override Widget build(BuildContext context) { @@ -307,11 +322,12 @@ class ChatAIMessagePopup extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - const VSpace(16.0), _copyButton(context, bottomSheetContext), _divider(), _regenerateButton(context), _divider(), + _changeFormatButton(context), + _divider(), _saveToPageButton(context), ], ); @@ -366,6 +382,23 @@ class ChatAIMessagePopup extends StatelessWidget { ); } + Widget _changeFormatButton(BuildContext context) { + return MobileQuickActionButton( + onTap: () async { + final result = await showChangeFormatBottomSheet(context); + if (result != null) { + onChangeFormat?.call(result); + if (context.mounted) { + Navigator.of(context).pop(); + } + } + }, + icon: FlowySvgs.ai_retry_font_s, + iconSize: const Size.square(20), + text: LocaleKeys.chat_changeFormat_actionButton.tr(), + ); + } + Widget _saveToPageButton(BuildContext context) { return MobileQuickActionButton( onTap: () async { 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 632bf64ebc..42ba3dc125 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 @@ -34,6 +34,7 @@ class ChatAIMessageWidget extends StatelessWidget { required this.refSourceJsonString, this.onSelectedMetadata, this.onRegenerate, + this.onChangeFormat, this.isLastMessage = false, this.isStreaming = false, }); @@ -48,6 +49,7 @@ class ChatAIMessageWidget extends StatelessWidget { final String? refSourceJsonString; final void Function(ChatMessageRefSource metadata)? onSelectedMetadata; final void Function()? onRegenerate; + final void Function(PredefinedFormat)? onChangeFormat; final bool isStreaming; final bool isLastMessage; @@ -87,6 +89,7 @@ class ChatAIMessageWidget extends StatelessWidget { state.text.isNotEmpty && !isStreaming, onRegenerate: onRegenerate, + onChangeFormat: onChangeFormat, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart index bffb651d29..cae8d66985 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart @@ -2,12 +2,12 @@ import 'dart:ui'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; @@ -68,14 +68,16 @@ class _ImageMenuState extends State { onTap: copyImageLink, ), const HSpace(4), - _ImageAlignButton(node: widget.node, state: widget.state), - const _Divider(), - MenuBlockButton( - tooltip: LocaleKeys.button_delete.tr(), - iconData: FlowySvgs.trash_s, - onTap: deleteImage, - ), - const HSpace(4), + if (widget.state.editorState.editable) ...[ + _ImageAlignButton(node: widget.node, state: widget.state), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.button_delete.tr(), + iconData: FlowySvgs.trash_s, + onTap: deleteImage, + ), + const HSpace(4), + ], ], ), ); @@ -126,7 +128,7 @@ class _ImageMenuState extends State { showDialog( context: context, builder: (_) => InteractiveImageViewer( - userProfile: context.read().state.userProfilePB, + userProfile: context.read().userProfile, imageProvider: AFBlockImageProvider( images: [ ImageBlockData( @@ -136,11 +138,13 @@ class _ImageMenuState extends State { ), ), ], - onDeleteImage: (_) async { - final transaction = widget.state.editorState.transaction; - transaction.deleteNode(widget.node); - await widget.state.editorState.apply(transaction); - }, + onDeleteImage: widget.state.editorState.editable + ? (_) async { + final transaction = widget.state.editorState.transaction; + transaction.deleteNode(widget.node); + await widget.state.editorState.apply(transaction); + } + : null, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart index 9f6b10cf3c..1105480bf3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -53,7 +54,7 @@ class _ImageBrowserLayoutState extends State { @override void initState() { super.initState(); - _userProfile = context.read().state.userProfilePB; + _userProfile = context.read()?.userProfile; } @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 607e1d4d06..97f6365337 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -5,6 +5,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -60,7 +61,7 @@ class _ResizableImageState extends State { void initState() { super.initState(); imageWidth = widget.width; - _userProfilePB = context.read()?.state.userProfilePB; + _userProfilePB = context.read()?.userProfile; } @override diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart index 1a9af7c028..3562987303 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart @@ -173,12 +173,9 @@ class TabsBloc extends Bloc { }, expandSecondaryPlugin: () { final pageManager = state.currentPageManager; - pageManager.setPlugin( - pageManager.secondaryNotifier.plugin, - true, - false, - ); - pageManager.hideSecondaryPlugin(); + pageManager + ..hideSecondaryPlugin() + ..expandSecondaryPlugin(); _setLatestOpenView(); }, switchWorkspace: (workspaceId) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 9108098f94..6c9c2faebf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -621,10 +621,12 @@ class PageNotifier extends ChangeNotifier { ]) => _plugin.widgetBuilder.tabBarItem(pluginId, shortForm); - /// This is the only place where the plugin is set. - /// No need compare the old plugin with the new plugin. Just set it. - void setPlugin(Plugin newPlugin, bool setLatest) { - if (newPlugin.id != plugin.id) { + void setPlugin( + Plugin newPlugin, { + required bool setLatest, + bool disposeExisting = true, + }) { + if (newPlugin.id != plugin.id && disposeExisting) { _plugin.dispose(); } @@ -660,12 +662,21 @@ class PageManager { if (init) { newPlugin.init(); } - _notifier.setPlugin(newPlugin, setLatest); + _notifier.setPlugin(newPlugin, setLatest: setLatest); } void setSecondaryPlugin(Plugin newPlugin) { newPlugin.init(); - _secondaryNotifier.setPlugin(newPlugin, false); + _secondaryNotifier.setPlugin(newPlugin, setLatest: false); + } + + void expandSecondaryPlugin() { + _notifier.setPlugin(_secondaryNotifier.plugin, setLatest: true); + _secondaryNotifier.setPlugin( + BlankPagePlugin(), + setLatest: false, + disposeExisting: false, + ); } void showSecondaryPlugin() { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/image_provider.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/image_provider.dart index 7fb9f1edbe..b69c56abf2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/image_provider.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/image_provider.dart @@ -29,13 +29,13 @@ class AFBlockImageProvider implements AFImageProvider { const AFBlockImageProvider({ required this.images, this.initialIndex = 0, - required this.onDeleteImage, + this.onDeleteImage, }); final List images; @override - final Function(int) onDeleteImage; + final Function(int)? onDeleteImage; @override final int initialIndex; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart index 1678be5a16..143c6b1ad3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart @@ -140,8 +140,9 @@ class _InteractiveImageViewerState extends State { final scaleStep = scale / currentScale; _zoom(scaleStep, size); }, - onDelete: () => - widget.imageProvider.onDeleteImage?.call(currentIndex), + onDelete: widget.imageProvider.onDeleteImage == null + ? null + : () => widget.imageProvider.onDeleteImage?.call(currentIndex), ), ], ), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 006675314e..da064fe623 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -61,8 +61,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2490f69" - resolved-ref: "2490f698c39d59ef81a03bbe192088f36b75e968" + ref: "448174b" + resolved-ref: "448174bb11ae4cfb3bb093522ef02f10f856abdf" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 52f0dbe7e4..ede0038f3f 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -174,7 +174,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "2490f69" + ref: "448174b" appflowy_editor_plugins: git: diff --git a/frontend/resources/flowy_icons/16x/ai_retry_filled.svg b/frontend/resources/flowy_icons/16x/ai_retry_filled.svg index 021f724b01..37fcdaea43 100644 --- a/frontend/resources/flowy_icons/16x/ai_retry_filled.svg +++ b/frontend/resources/flowy_icons/16x/ai_retry_filled.svg @@ -1,11 +1,50 @@ - - - - + + + + + - - - + + + diff --git a/frontend/resources/flowy_icons/16x/ai_text_auto.svg b/frontend/resources/flowy_icons/16x/ai_text_auto.svg new file mode 100644 index 0000000000..b09fd45305 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_text_auto.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8df6426078..9fe043ede1 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -221,7 +221,24 @@ "addToNewPage": "Create new page", "addToNewPageName": "Messages extracted from \"{}\"", "addToNewPageSuccessToast": "Message added to", - "openPagePreviewFailedToast": "Failed to open page" + "openPagePreviewFailedToast": "Failed to open page", + "changeFormat": { + "actionButton": "Change format", + "confirmButton": "Regenerate with this format", + "textOnly": "Text", + "imageOnly": "Image only", + "textAndImage": "Text and Image", + "text": "Paragraph", + "bullet": "Bullet list", + "number": "Numbered list", + "table": "Table", + "blankDescription": "Format response", + "defaultDescription": "Auto", + "textWithImageDescription": "@:chat.changeFormat.text with image", + "numberWithImageDescription": "@:chat.changeFormat.number with image", + "bulletWithImageDescription": "@:chat.changeFormat.bullet with image", + "tableWithImageDescription": "@:chat.changeFormat.table with image" + } }, "trash": { "text": "Trash", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 34d76452a6..2505cbb93d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -163,16 +163,16 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "bincode", "getrandom 0.2.10", - "reqwest", + "reqwest 0.12.9", "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", "tokio", "tsify", "url", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "bytes", @@ -192,7 +192,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -203,7 +203,7 @@ dependencies = [ "anyhow", "appflowy-plugin", "bytes", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "tokio", @@ -227,7 +227,7 @@ dependencies = [ "parking_lot 0.12.1", "serde", "serde_json", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-stream", "tracing", @@ -302,7 +302,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -324,7 +324,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -335,7 +335,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -349,7 +349,7 @@ dependencies = [ "crc32fast", "futures-lite", "pin-project", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-util", ] @@ -360,6 +360,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atomic_refcell" version = "0.1.11" @@ -383,9 +389,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "itoa", "matchit", "memchr", @@ -394,7 +400,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", @@ -409,8 +415,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "mime", "rustversion", "tower-layer", @@ -480,9 +486,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -562,7 +568,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", "syn_derive", ] @@ -710,7 +716,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -780,7 +786,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "again", "anyhow", @@ -812,7 +818,7 @@ dependencies = [ "pin-project", "prost 0.13.3", "rayon", - "reqwest", + "reqwest 0.12.9", "scraper 0.17.1", "semver", "serde", @@ -820,10 +826,11 @@ dependencies = [ "serde_repr", "serde_urlencoded", "shared-entity", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-retry", "tokio-stream", + "tokio-tungstenite", "tokio-util", "tracing", "url", @@ -836,7 +843,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "collab-entity", "collab-rt-entity", @@ -849,15 +856,14 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "futures-channel", "futures-util", - "http", "httparse", "js-sys", "percent-encoding", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-tungstenite", "wasm-bindgen", @@ -905,7 +911,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-stream", "tracing", @@ -945,7 +951,7 @@ dependencies = [ "sha2", "strum", "strum_macros 0.25.2", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-stream", "tokio-util", @@ -968,7 +974,7 @@ dependencies = [ "nanoid", "serde", "serde_json", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-stream", "tracing", @@ -990,7 +996,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", "uuid", "walkdir", ] @@ -1010,7 +1016,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-stream", "tracing", @@ -1047,7 +1053,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-util", "tracing", @@ -1106,7 +1112,7 @@ dependencies = [ "serde_json", "similar 2.2.1", "smallvec", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-retry", "tokio-stream", @@ -1122,7 +1128,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "bincode", @@ -1139,7 +1145,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", "tokio-tungstenite", "yrs", ] @@ -1147,7 +1153,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "async-trait", @@ -1155,7 +1161,7 @@ dependencies = [ "collab", "collab-entity", "serde", - "thiserror", + "thiserror 1.0.64", "tokio", "tracing", "yrs", @@ -1252,9 +1258,9 @@ checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "cookie" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "percent-encoding", "time", @@ -1263,12 +1269,13 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" dependencies = [ "cookie", - "idna 0.3.0", + "document-features", + "idna 1.0.3", "log", "publicsuffix", "serde", @@ -1393,7 +1400,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1404,7 +1411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -1458,7 +1465,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -1469,7 +1476,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -1541,7 +1548,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "app-error", @@ -1555,7 +1562,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", "tracing", "uuid", "validator 0.19.0", @@ -1586,7 +1593,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -1618,7 +1625,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -1667,7 +1674,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -1687,7 +1694,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -1709,7 +1716,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", ] [[package]] @@ -1939,7 +1955,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -2021,7 +2037,7 @@ dependencies = [ "notify", "pin-project", "protobuf", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "sha2", @@ -2298,12 +2314,12 @@ dependencies = [ "lib-dispatch", "protobuf", "r2d2", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "serde_repr", "tantivy", - "thiserror", + "thiserror 1.0.64", "tokio", "url", "validator 0.18.1", @@ -2462,18 +2478,18 @@ dependencies = [ "futures", "futures-util", "hex", - "hyper", + "hyper 0.14.27", "lazy_static", "lib-dispatch", "lib-infra", "mime_guess", "postgrest", "rand 0.8.5", - "reqwest", + "reqwest 0.11.27", "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.64", "tokio", "tokio-retry", "tokio-stream", @@ -2510,7 +2526,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror", + "thiserror 1.0.64", "tracing", ] @@ -2784,7 +2800,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -2954,14 +2970,14 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "futures-util", "getrandom 0.2.10", "gotrue-entity", "infra", - "reqwest", + "reqwest 0.12.9", "serde", "serde_json", "tokio", @@ -2971,7 +2987,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "app-error", @@ -2993,7 +3009,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.9", "indexmap 1.9.3", "slab", "tokio", @@ -3001,6 +3017,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.2.0", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3109,6 +3144,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -3116,7 +3162,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.9", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.2.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -3157,9 +3226,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.21", + "http 0.2.9", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -3171,6 +3240,26 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + [[package]] name = "hyper-rustls" version = "0.24.1" @@ -3178,11 +3267,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ "futures-util", - "http", - "hyper", - "rustls", + "http 0.2.9", + "hyper 0.14.27", + "rustls 0.21.7", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper 1.5.2", + "hyper-util", + "rustls 0.23.20", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.1", + "tower-service", + "webpki-roots 0.26.7", ] [[package]] @@ -3191,7 +3298,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.27", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -3204,12 +3311,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.27", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.5.2", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -3348,7 +3490,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -3456,13 +3598,13 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "bytes", "futures", "pin-project", - "reqwest", + "reqwest 0.12.9", "serde", "serde_json", "tokio", @@ -3567,7 +3709,7 @@ checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ "base64 0.21.5", "pem", - "ring", + "ring 0.16.20", "serde", "serde_json", "simple_asn1", @@ -3687,9 +3829,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -3755,6 +3897,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.10" @@ -3853,7 +4001,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -3864,7 +4012,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -3877,7 +4025,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -4050,7 +4198,7 @@ dependencies = [ "rustc_version", "smallvec", "tagptr", - "thiserror", + "thiserror 1.0.64", "triomphe", "uuid", ] @@ -4248,7 +4396,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -4422,7 +4570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.64", "ucd-trie", ] @@ -4446,7 +4594,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -4476,7 +4624,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", ] @@ -4496,7 +4644,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", ] @@ -4564,19 +4711,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" @@ -4621,7 +4755,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -4689,7 +4823,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7" dependencies = [ - "reqwest", + "reqwest 0.11.27", ] [[package]] @@ -4717,7 +4851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8832c0f9be7e3cae60727e6256cfd2cd3c3e2b6cd5dad4190ecb2fd658c9030b" dependencies = [ "proc-macro2", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -4772,7 +4906,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -4783,9 +4917,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.75" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -4827,7 +4961,7 @@ dependencies = [ "prost 0.12.3", "prost-types", "regex", - "syn 2.0.47", + "syn 2.0.94", "tempfile", "which", ] @@ -4842,7 +4976,7 @@ dependencies = [ "itertools 0.10.5", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -4855,7 +4989,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -5034,6 +5168,58 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.0", + "rustls 0.23.20", + "socket2 0.5.5", + "thiserror 2.0.9", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom 0.2.10", + "rand 0.8.5", + "ring 0.17.8", + "rustc-hash 2.1.0", + "rustls 0.23.20", + "rustls-pki-types", + "slab", + "thiserror 2.0.9", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.5", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.35" @@ -5337,17 +5523,15 @@ checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.5", "bytes", - "cookie", - "cookie_store", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls", - "hyper-tls", + "h2 0.3.21", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", + "hyper-rustls 0.24.1", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -5357,16 +5541,16 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", - "rustls-pemfile", + "rustls 0.21.7", + "rustls-pemfile 1.0.3", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", @@ -5374,10 +5558,63 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 0.25.2", "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-rustls 0.27.5", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.20", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.1", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.26.7", + "windows-registry", +] + [[package]] name = "ring" version = "0.16.20" @@ -5387,12 +5624,27 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.10", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.42" @@ -5478,6 +5730,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + [[package]] name = "rustc_version" version = "0.4.1" @@ -5507,11 +5765,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", - "ring", - "rustls-webpki", + "ring 0.16.20", + "rustls-webpki 0.101.4", "sct", ] +[[package]] +name = "rustls" +version = "0.23.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +dependencies = [ + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.3" @@ -5521,14 +5793,43 @@ dependencies = [ "base64 0.21.5", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +dependencies = [ + "web-time", +] + [[package]] name = "rustls-webpki" version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", ] [[package]] @@ -5641,8 +5942,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -5719,7 +6020,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -5730,7 +6031,7 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -5753,7 +6054,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -5839,7 +6140,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ea131f0baab67defe7591067357eced490072372#ea131f0baab67defe7591067357eced490072372" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2bd6da228d3e3f0f258c982b7a2a3571718d3688#2bd6da228d3e3f0f258c982b7a2a3571718d3688" dependencies = [ "anyhow", "app-error", @@ -5853,11 +6154,11 @@ dependencies = [ "infra", "log", "pin-project", - "reqwest", + "reqwest 0.12.9", "serde", "serde_json", "serde_repr", - "thiserror", + "thiserror 1.0.64", "tracing", "uuid", "validator 0.19.0", @@ -5910,7 +6211,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.64", "time", ] @@ -5997,6 +6298,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -6080,7 +6387,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -6093,7 +6400,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -6115,9 +6422,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.47" +version = "2.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" +checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" dependencies = [ "proc-macro2", "quote", @@ -6133,7 +6440,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -6142,6 +6449,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -6150,7 +6466,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -6176,7 +6492,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.4.0", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -6189,6 +6516,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -6227,7 +6564,7 @@ dependencies = [ "rayon", "regex", "rust-stemmers", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "serde_json", "sketches-ddsketch", @@ -6240,7 +6577,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror", + "thiserror 1.0.64", "time", "uuid", "winapi", @@ -6413,7 +6750,16 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.64", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", ] [[package]] @@ -6424,7 +6770,18 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", ] [[package]] @@ -6542,7 +6899,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -6598,7 +6955,17 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.7", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls 0.23.20", "tokio", ] @@ -6710,10 +7077,10 @@ dependencies = [ "axum", "base64 0.21.5", "bytes", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.21", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-timeout", "percent-encoding", "pin-project", @@ -6777,7 +7144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.64", "time", "tracing-subscriber", ] @@ -6790,7 +7157,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -6919,7 +7286,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -6931,13 +7298,13 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 0.2.9", "httparse", "log", "native-tls", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.64", "url", "utf-8", ] @@ -7074,6 +7441,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -7164,7 +7537,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -7178,7 +7551,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -7251,7 +7624,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", "wasm-bindgen-shared", ] @@ -7285,7 +7658,7 @@ checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7334,12 +7707,31 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +[[package]] +name = "webpki-roots" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -7409,7 +7801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -7418,7 +7810,37 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -7436,7 +7858,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -7456,17 +7878,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -7477,9 +7900,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -7489,9 +7912,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -7501,9 +7924,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -7513,9 +7942,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -7525,9 +7954,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -7537,9 +7966,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -7549,9 +7978,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -7633,7 +8062,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", "synstructure", ] @@ -7652,7 +8081,7 @@ dependencies = [ "serde_json", "smallstr", "smallvec", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -7672,7 +8101,7 @@ checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -7692,7 +8121,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", "synstructure", ] @@ -7713,7 +8142,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -7735,7 +8164,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.47", + "syn 2.0.94", ] [[package]] @@ -7780,7 +8209,7 @@ dependencies = [ "pbkdf2 0.12.2", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.64", "time", "zeroize", "zopfli", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index fd9b7ac1e9..d437f559db 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -103,8 +103,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 = "ea131f0baab67defe7591067357eced490072372" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ea131f0baab67defe7591067357eced490072372" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2bd6da228d3e3f0f258c982b7a2a3571718d3688" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2bd6da228d3e3f0f258c982b7a2a3571718d3688" } [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 a3fd90e07c..67d31d0ded 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -1,7 +1,8 @@ use bytes::Bytes; pub use client_api::entity::ai_dto::{ AppFlowyOfflineAI, CompletionType, CreateChatContext, LLMModel, LocalAIConfig, ModelInfo, - RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage, + OutputContent, OutputLayout, RelatedQuestion, RepeatedRelatedQuestion, ResponseFormat, + StringOrMessage, }; pub use client_api::entity::billing_dto::SubscriptionPlan; pub use client_api::entity::chat_dto::{ @@ -53,6 +54,7 @@ pub trait ChatCloudService: Send + Sync + 'static { workspace_id: &str, chat_id: &str, message_id: i64, + format: ResponseFormat, ) -> Result; async fn get_answer( diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index a77764fcc9..c88a26504c 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -1,6 +1,7 @@ use crate::chat::Chat; use crate::entities::{ - ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, FilePB, RepeatedRelatedQuestionPB, + ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, FilePB, PredefinedFormatPB, + RepeatedRelatedQuestionPB, StreamMessageParams, }; use crate::local_ai::local_llm_chat::LocalAIController; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; @@ -9,9 +10,7 @@ use std::collections::HashMap; use appflowy_plugin::manager::PluginManager; use dashmap::DashMap; -use flowy_ai_pub::cloud::{ - ChatCloudService, ChatMessageMetadata, ChatMessageType, ChatSettings, UpdateChatParams, -}; +use flowy_ai_pub::cloud::{ChatCloudService, ChatSettings, UpdateChatParams}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; @@ -212,28 +211,15 @@ impl AIManager { Ok(chat) } - pub async fn stream_chat_message( - &self, - chat_id: &str, - message: &str, - message_type: ChatMessageType, - answer_stream_port: i64, - question_stream_port: i64, - metadata: Vec, + pub async fn stream_chat_message<'a>( + &'a self, + params: &'a StreamMessageParams<'a>, ) -> Result { - let chat = self.get_or_create_chat_instance(chat_id).await?; - let question = chat - .stream_chat_message( - message, - message_type, - answer_stream_port, - question_stream_port, - metadata, - ) - .await?; + let chat = self.get_or_create_chat_instance(params.chat_id).await?; + let question = chat.stream_chat_message(params).await?; let _ = self .external_service - .notify_did_send_message(chat_id, message) + .notify_did_send_message(params.chat_id, params.message) .await; Ok(question) } @@ -243,13 +229,14 @@ impl AIManager { chat_id: &str, answer_message_id: i64, answer_stream_port: i64, + format: Option, ) -> 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) + .stream_regenerate_response(question_message_id, answer_stream_port, format) .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 052e48a7a7..27eecf8638 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -1,6 +1,7 @@ use crate::ai_manager::AIUserService; use crate::entities::{ - ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB, + ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, PredefinedFormatPB, + RepeatedRelatedQuestionPB, StreamMessageParams, }; use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::notification::{chat_notification_builder, ChatNotification}; @@ -11,8 +12,7 @@ use crate::persistence::{ use crate::stream_message::StreamMessage; use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ - ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, MessageCursor, - QuestionStreamValue, + ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; @@ -81,20 +81,17 @@ impl Chat { } #[instrument(level = "info", skip_all, err)] - pub async fn stream_chat_message( - &self, - message: &str, - message_type: ChatMessageType, - answer_stream_port: i64, - question_stream_port: i64, - metadata: Vec, + pub async fn stream_chat_message<'a>( + &'a self, + params: &'a StreamMessageParams<'a>, ) -> Result { trace!( - "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, metadata={:?}", + "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, metadata={:?}, format={:?}", self.chat_id, - message, - message_type, - metadata + params.message, + params.message_type, + params.metadata, + params.format, ); // clear @@ -103,22 +100,22 @@ impl Chat { .store(false, std::sync::atomic::Ordering::SeqCst); self.stream_buffer.lock().await.clear(); - let mut question_sink = IsolateSink::new(Isolate::new(question_stream_port)); + let mut question_sink = IsolateSink::new(Isolate::new(params.question_stream_port)); let answer_stream_buffer = self.stream_buffer.clone(); let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; let _ = question_sink - .send(StreamMessage::Text(message.to_string()).to_string()) + .send(StreamMessage::Text(params.message.to_string()).to_string()) .await; let question = self .chat_service .create_question( &workspace_id, &self.chat_id, - message, - message_type, - &metadata, + params.message, + params.message_type.clone(), + ¶ms.metadata, ) .await .map_err(|err| { @@ -131,7 +128,7 @@ impl Chat { .await; if let Err(err) = self .chat_service - .index_message_metadata(&self.chat_id, &metadata, &mut question_sink) + .index_message_metadata(&self.chat_id, ¶ms.metadata, &mut question_sink) .await { error!("Failed to index file: {}", err); @@ -141,12 +138,15 @@ impl Chat { // Save message to disk save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; + let format = params.format.clone().unwrap_or_default().into(); + self.stream_response( - answer_stream_port, + params.answer_stream_port, answer_stream_buffer, uid, workspace_id, question.message_id, + format, ); let question_pb = ChatMessagePB::from(question); @@ -158,6 +158,7 @@ impl Chat { &self, question_id: i64, answer_stream_port: i64, + format: Option, ) -> FlowyResult<()> { trace!( "[Chat] regenerate and stream chat message: chat_id={}", @@ -170,6 +171,8 @@ impl Chat { .store(false, std::sync::atomic::Ordering::SeqCst); self.stream_buffer.lock().await.clear(); + let format = format.unwrap_or_default().into(); + let answer_stream_buffer = self.stream_buffer.clone(); let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; @@ -180,6 +183,7 @@ impl Chat { uid, workspace_id, question_id, + format, ); Ok(()) @@ -192,6 +196,7 @@ impl Chat { uid: i64, workspace_id: String, question_id: i64, + format: ResponseFormat, ) { let stop_stream = self.stop_stream.clone(); let chat_id = self.chat_id.clone(); @@ -200,7 +205,7 @@ impl Chat { tokio::spawn(async move { let mut answer_sink = IsolateSink::new(Isolate::new(answer_stream_port)); match cloud_service - .stream_answer(&workspace_id, &chat_id, question_id) + .stream_answer(&workspace_id, &chat_id, question_id, format) .await { Ok(mut stream) => { @@ -214,14 +219,21 @@ impl Chat { match message { QuestionStreamValue::Answer { value } => { answer_stream_buffer.lock().await.push_str(&value); - let _ = answer_sink.send(format!("data:{}", value)).await; + // trace!("[Chat] stream answer: {}", value); + if let Err(err) = answer_sink.send(format!("data:{}", value)).await { + error!("Failed to stream answer: {}", err); + } }, QuestionStreamValue::Metadata { value } => { if let Ok(s) = serde_json::to_string(&value) { + // trace!("[Chat] stream metadata: {}", s); answer_stream_buffer.lock().await.set_metadata(value); let _ = answer_sink.send(format!("metadata:{}", s)).await; } }, + QuestionStreamValue::KeepAlive => { + // trace!("[Chat] stream keep alive"); + }, } }, Err(err) => { diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 4eb80dd071..f0d3726b06 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -4,7 +4,8 @@ use std::collections::HashMap; use crate::local_ai::local_llm_resource::PendingResource; use flowy_ai_pub::cloud::{ - ChatMessage, LLMModel, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, + ChatMessage, ChatMessageMetadata, ChatMessageType, LLMModel, OutputContent, OutputLayout, + RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; @@ -67,10 +68,24 @@ pub struct StreamChatPayloadPB { #[pb(index = 5)] pub question_stream_port: i64, - #[pb(index = 6)] + #[pb(index = 6, one_of)] + pub format: Option, + + #[pb(index = 7)] pub metadata: Vec, } +#[derive(Default, Debug)] +pub struct StreamMessageParams<'a> { + pub chat_id: &'a str, + pub message: &'a str, + pub message_type: ChatMessageType, + pub answer_stream_port: i64, + pub question_stream_port: i64, + pub format: Option, + pub metadata: Vec, +} + #[derive(Default, ProtoBuf, Validate, Clone, Debug)] pub struct RegenerateResponsePB { #[pb(index = 1)] @@ -82,6 +97,9 @@ pub struct RegenerateResponsePB { #[pb(index = 3)] pub answer_stream_port: i64, + + #[pb(index = 4, one_of)] + pub format: Option, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] @@ -554,3 +572,51 @@ pub struct UpdateChatSettingsPB { #[pb(index = 2)] pub rag_ids: Vec, } + +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct PredefinedFormatPB { + #[pb(index = 1)] + pub image_format: ResponseImageFormatPB, + + #[pb(index = 2, one_of)] + pub text_format: Option, +} + +#[derive(Debug, Default, Clone, ProtoBuf_Enum)] +pub enum ResponseImageFormatPB { + #[default] + TextOnly = 0, + ImageOnly = 1, + TextAndImage = 2, +} + +#[derive(Debug, Default, Clone, ProtoBuf_Enum)] +pub enum ResponseTextFormatPB { + #[default] + Paragraph = 0, + BulletedList = 1, + NumberedList = 2, + Table = 3, +} + +impl From for ResponseFormat { + fn from(value: PredefinedFormatPB) -> Self { + Self { + output_layout: match value.text_format { + Some(format) => match format { + ResponseTextFormatPB::Paragraph => OutputLayout::Paragraph, + ResponseTextFormatPB::BulletedList => OutputLayout::BulletList, + ResponseTextFormatPB::NumberedList => OutputLayout::NumberedList, + ResponseTextFormatPB::Table => OutputLayout::SimpleTable, + }, + None => OutputLayout::Paragraph, + }, + output_content: match value.image_format { + ResponseImageFormatPB::TextOnly => OutputContent::TEXT, + ResponseImageFormatPB::ImageOnly => OutputContent::IMAGE, + ResponseImageFormatPB::TextAndImage => OutputContent::RichTextImage, + }, + output_content_metadata: None, + } + } +} diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index 419ff5403f..d8ebd0e93b 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -32,13 +32,22 @@ pub(crate) async fn stream_chat_message_handler( let data = data.into_inner(); data.validate()?; - let message_type = match data.message_type { + let StreamChatPayloadPB { + chat_id, + message, + message_type, + answer_stream_port, + question_stream_port, + format, + metadata, + } = data; + + let message_type = match message_type { ChatMessageTypePB::System => ChatMessageType::System, ChatMessageTypePB::User => ChatMessageType::User, }; - let metadata = data - .metadata + let metadata = metadata .into_iter() .map(|metadata| { let (content_type, content_len) = match metadata.loader_type { @@ -63,17 +72,19 @@ pub(crate) async fn stream_chat_message_handler( .collect::>(); trace!("Stream chat message with metadata: {:?}", metadata); + + let params = StreamMessageParams { + chat_id: &chat_id, + message: &message, + message_type, + answer_stream_port, + question_stream_port, + format, + metadata, + }; + let ai_manager = upgrade_ai_manager(ai_manager)?; - let result = ai_manager - .stream_chat_message( - &data.chat_id, - &data.message, - message_type, - data.answer_stream_port, - data.question_stream_port, - metadata, - ) - .await?; + let result = ai_manager.stream_chat_message(¶ms).await?; data_result_ok(result) } @@ -90,6 +101,7 @@ pub(crate) async fn regenerate_response_handler( &data.chat_id, data.answer_message_id, data.answer_stream_port, + data.format, ) .await?; Ok(()) 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 251b049317..c8c3dda821 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 @@ -11,7 +11,8 @@ use std::collections::HashMap; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, CompletionType, LocalAIConfig, MessageCursor, RelatedQuestion, RepeatedChatMessage, - RepeatedRelatedQuestion, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, + UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{stream, Sink, StreamExt, TryStreamExt}; @@ -155,6 +156,7 @@ impl ChatCloudService for AICloudServiceMiddleware { workspace_id: &str, chat_id: &str, question_id: i64, + format: ResponseFormat, ) -> Result { if self.local_llm_controller.is_running() { let row = self.get_message_record(question_id)?; @@ -172,7 +174,7 @@ impl ChatCloudService for AICloudServiceMiddleware { } else { self .cloud_service - .stream_answer(workspace_id, chat_id, question_id) + .stream_answer(workspace_id, chat_id, question_id, format) .await } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index f9a79ecc2e..c34b40023c 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -22,8 +22,8 @@ use collab_integrate::collab_builder::{ }; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, LocalAIConfig, - MessageCursor, RepeatedChatMessage, StreamAnswer, StreamComplete, SubscriptionPlan, - UpdateChatParams, + MessageCursor, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, + SubscriptionPlan, UpdateChatParams, }; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, @@ -704,13 +704,14 @@ impl ChatCloudService for ServerProvider { workspace_id: &str, chat_id: &str, message_id: i64, + format: ResponseFormat, ) -> Result { let workspace_id = workspace_id.to_string(); let chat_id = chat_id.to_string(); let server = self.get_server()?; server .chat_service() - .stream_answer(&workspace_id, &chat_id, message_id) + .stream_answer(&workspace_id, &chat_id, message_id, format) .await } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index f782563280..a7fe19980d 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -1,5 +1,7 @@ use crate::af_cloud::AFServer; -use client_api::entity::ai_dto::{CompleteTextParams, CompletionType, RepeatedRelatedQuestion}; +use client_api::entity::ai_dto::{ + ChatQuestionQuery, CompleteTextParams, CompletionType, RepeatedRelatedQuestion, ResponseFormat, +}; use client_api::entity::chat_dto::{ CreateAnswerMessageParams, CreateChatMessageParams, CreateChatParams, MessageCursor, RepeatedChatMessage, @@ -15,6 +17,7 @@ use lib_infra::util::{get_operating_system, OperatingSystem}; use serde_json::Value; use std::collections::HashMap; use std::path::Path; +use tracing::trace; pub(crate) struct AFCloudChatCloudServiceImpl { pub inner: T, @@ -97,10 +100,24 @@ where workspace_id: &str, chat_id: &str, message_id: i64, + format: ResponseFormat, ) -> Result { + trace!( + "stream_answer: workspace_id={}, chat_id={}, format={:?}", + workspace_id, + chat_id, + format + ); let try_get_client = self.inner.try_get_client(); let result = try_get_client? - .stream_answer_v2(workspace_id, chat_id, message_id) + .stream_answer_v3( + workspace_id, + ChatQuestionQuery { + chat_id: chat_id.to_string(), + question_id: message_id, + format, + }, + ) .await; let stream = result.map_err(FlowyError::from)?.map_err(FlowyError::from); diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index 7b7a201bd5..7fe409d781 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -1,7 +1,8 @@ use client_api::entity::ai_dto::{CompletionType, LocalAIConfig, RepeatedRelatedQuestion}; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, MessageCursor, - RepeatedChatMessage, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, + RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan, + UpdateChatParams, }; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; @@ -50,6 +51,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { _workspace_id: &str, _chat_id: &str, _message_id: i64, + _format: ResponseFormat, ) -> Result { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) }