From a2e211555e3d06c44ed88d9631d55979c3275bff Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Fri, 26 Jul 2024 07:58:54 +0800 Subject: [PATCH] chore: chat with pdf ui (#5811) * chore: chat with pdf ui * chore: only enable local ai on macos * chore: add todo * chore: adjust UI * chore: clippy --- .../application/chat_ai_message_bloc.dart | 9 +- .../ai_chat/application/chat_bloc.dart | 4 +- .../ai_chat/application/chat_file_bloc.dart | 67 +++++++++++++-- .../lib/plugins/ai_chat/chat_page.dart | 82 ++++++++++++++----- .../ai_chat/presentation/chat_input.dart | 63 +------------- .../presentation/chat_related_question.dart | 2 +- .../lib/shared/feature_flags.dart | 3 +- .../setting_ai_view/settings_ai_view.dart | 3 +- .../settings/pages/settings_billing_view.dart | 36 ++++---- .../settings/pages/settings_plan_view.dart | 70 +++++++++------- frontend/resources/translations/en.json | 5 +- .../rust-lib/flowy-chat/src/event_handler.rs | 21 ++++- .../flowy-chat/src/local_ai/local_llm_chat.rs | 3 +- .../rust-lib/flowy-document/src/manager.rs | 3 +- frontend/rust-lib/flowy-error/src/code.rs | 3 + 15 files changed, 225 insertions(+), 149 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart index de52749b8d..e68ad2eb8d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart @@ -17,7 +17,7 @@ class ChatAIMessageBloc extends Bloc { required this.questionId, }) : super(ChatAIMessageState.initial(message)) { if (state.stream != null) { - _subscription = state.stream!.listen( + state.stream!.listen( onData: (text) { if (!isClosed) { add(ChatAIMessageEvent.updateText(text)); @@ -108,13 +108,6 @@ class ChatAIMessageBloc extends Bloc { ); } - @override - Future close() { - _subscription?.cancel(); - return super.close(); - } - - StreamSubscription? _subscription; final String chatId; final Int64? questionId; } 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 01a8e561ad..4a14608d50 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 @@ -586,7 +586,7 @@ class AnswerStream { _port.close(); } - StreamSubscription listen({ + void listen({ void Function(String text)? onData, void Function()? onStart, void Function()? onEnd, @@ -602,7 +602,5 @@ class AnswerStream { if (_onStart != null) { _onStart!(); } - - return _subscription; } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart index c257a4f95f..b2e1dd397d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -13,6 +15,11 @@ class ChatFileBloc extends Bloc { }) : listener = LocalLLMListener(), super(const ChatFileState()) { listener.start( + stateCallback: (pluginState) { + if (!isClosed) { + add(ChatFileEvent.updatePluginState(pluginState)); + } + }, chatStateCallback: (chatState) { if (!isClosed) { add(ChatFileEvent.updateChatState(chatState)); @@ -38,18 +45,55 @@ class ChatFileBloc extends Bloc { }, ); }, - newFile: (String filePath) { + newFile: (String filePath, String fileName) async { + emit( + state.copyWith( + indexFileIndicator: IndexFileIndicator.indexing(fileName), + ), + ); final payload = ChatFilePB(filePath: filePath, chatId: chatId); - ChatEventChatWithFile(payload).send(); + unawaited( + ChatEventChatWithFile(payload).send().then((result) { + if (!isClosed) { + result.fold((_) { + add( + ChatFileEvent.updateIndexFile( + IndexFileIndicator.finish(fileName), + ), + ); + }, (err) { + add( + ChatFileEvent.updateIndexFile( + IndexFileIndicator.error(err.msg), + ), + ); + }); + } + }), + ); }, updateChatState: (LocalAIChatPB chatState) { // Only user enable chat with file and the plugin is already running final supportChatWithFile = chatState.fileEnabled && chatState.pluginState.state == RunningStatePB.Running; emit( - state.copyWith(supportChatWithFile: supportChatWithFile), + state.copyWith( + supportChatWithFile: supportChatWithFile, + chatState: chatState, + ), ); }, + updateIndexFile: (IndexFileIndicator indicator) { + emit( + state.copyWith(indexFileIndicator: indicator), + ); + }, + updatePluginState: (LocalAIPluginStatePB chatState) { + final fileEnabled = state.chatState?.fileEnabled ?? false; + final supportChatWithFile = + fileEnabled && chatState.state == RunningStatePB.Running; + emit(state.copyWith(supportChatWithFile: supportChatWithFile)); + }, ); }, ); @@ -67,20 +111,29 @@ class ChatFileBloc extends Bloc { @freezed class ChatFileEvent with _$ChatFileEvent { const factory ChatFileEvent.initial() = Initial; - const factory ChatFileEvent.newFile(String filePath) = _NewFile; + const factory ChatFileEvent.newFile(String filePath, String fileName) = + _NewFile; const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) = _UpdateChatState; + const factory ChatFileEvent.updatePluginState( + LocalAIPluginStatePB chatState, + ) = _UpdatePluginState; + const factory ChatFileEvent.updateIndexFile(IndexFileIndicator indicator) = + _UpdateIndexFile; } @freezed class ChatFileState with _$ChatFileState { const factory ChatFileState({ @Default(false) bool supportChatWithFile, + IndexFileIndicator? indexFileIndicator, + LocalAIChatPB? chatState, }) = _ChatFileState; } @freezed -class LocalAIChatFileIndicator with _$LocalAIChatFileIndicator { - const factory LocalAIChatFileIndicator.ready(bool isEnabled) = _Ready; - const factory LocalAIChatFileIndicator.loading() = _Loading; +class IndexFileIndicator with _$IndexFileIndicator { + const factory IndexFileIndicator.finish(String fileName) = _Finish; + const factory IndexFileIndicator.indexing(String fileName) = _Indexing; + const factory IndexFileIndicator.error(String error) = _Error; } 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 4bab926d46..1fe467cd70 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,5 +1,7 @@ import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -71,7 +73,8 @@ class AIChatPage extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => ChatFileBloc(chatId: view.id.toString()), + create: (_) => ChatFileBloc(chatId: view.id.toString()) + ..add(const ChatFileEvent.initial()), ), BlocProvider( create: (_) => ChatBloc( @@ -81,28 +84,40 @@ class AIChatPage extends StatelessWidget { ), BlocProvider(create: (_) => ChatInputBloc()), ], - child: BlocBuilder( - builder: (context, state) { - Widget child = _ChatContentPage( - view: view, - userProfile: userProfile, - ); - - // If the chat supports file upload, wrap the chat content with a drop target - if (state.supportChatWithFile) { - child = DropTarget( + child: BlocListener( + listenWhen: (previous, current) => + previous.indexFileIndicator != current.indexFileIndicator, + listener: (context, state) { + _handleIndexIndicator(state.indexFileIndicator, context); + }, + child: BlocBuilder( + builder: (context, state) { + return DropTarget( onDragDone: (DropDoneDetails detail) async { - for (final file in detail.files) { - context - .read() - .add(ChatFileEvent.newFile(file.path)); + if (state.supportChatWithFile) { + await showConfirmDialog( + context: context, + style: ConfirmPopupStyle.cancelAndOk, + title: LocaleKeys.chat_chatWithFilePrompt.tr(), + confirmLabel: LocaleKeys.button_confirm.tr(), + onConfirm: () { + for (final file in detail.files) { + context + .read() + .add(ChatFileEvent.newFile(file.path, file.name)); + } + }, + description: '', + ); } }, - child: child, + child: _ChatContentPage( + view: view, + userProfile: userProfile, + ), ); - } - return child; - }, + }, + ), ), ); } @@ -114,6 +129,35 @@ class AIChatPage extends StatelessWidget { ), ); } + + void _handleIndexIndicator( + IndexFileIndicator? indicator, + BuildContext context, + ) { + if (indicator != null) { + indicator.when( + finish: (fileName) { + showSnackBarMessage( + context, + LocaleKeys.chat_indexFileSuccess.tr(args: [fileName]), + ); + }, + indexing: (fileName) { + showSnackBarMessage( + context, + LocaleKeys.chat_indexingFile.tr(args: [fileName]), + duration: const Duration(seconds: 2), + ); + }, + error: (err) { + showSnackBarMessage( + context, + err, + ); + }, + ); + } + } } class _ChatContentPage extends StatefulWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart index fdea08b0b1..3f6846b8b5 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart @@ -67,8 +67,7 @@ class _ChatInputState extends State { void initState() { super.initState(); - _textController = - widget.options.textEditingController ?? InputTextFieldController(); + _textController = InputTextFieldController(); _handleSendButtonVisibilityModeChange(); } @@ -85,9 +84,7 @@ class _ChatInputState extends State { final partialText = types.PartialText(text: trimmedText); widget.onSendPressed(partialText); - if (widget.options.inputClearMode == InputClearMode.always) { - _textController.clear(); - } + _textController.clear(); } } @@ -106,7 +103,6 @@ class _ChatInputState extends State { const inputPadding = EdgeInsets.all(6); return Focus( - autofocus: !widget.options.autofocus, child: Padding( padding: inputPadding, child: Material( @@ -148,15 +144,11 @@ class _ChatInputState extends State { style: TextStyle( color: AFThemeExtension.of(context).textColor, ), - autocorrect: widget.options.autocorrect, - autofocus: widget.options.autofocus, - enableSuggestions: widget.options.enableSuggestions, - keyboardType: widget.options.keyboardType, + keyboardType: TextInputType.multiline, textCapitalization: TextCapitalization.sentences, maxLines: 10, minLines: 1, - onChanged: widget.options.onTextChanged, - onTap: widget.options.onTextFieldTap, + onChanged: (_) {}, ), ); } @@ -207,53 +199,6 @@ class _ChatInputState extends State { ); } -@immutable -class InputOptions { - const InputOptions({ - this.inputClearMode = InputClearMode.always, - this.keyboardType = TextInputType.multiline, - this.onTextChanged, - this.onTextFieldTap, - this.textEditingController, - this.autocorrect = true, - this.autofocus = false, - this.enableSuggestions = true, - this.enabled = true, - }); - - /// Controls the [ChatInput] clear behavior. Defaults to [InputClearMode.always]. - final InputClearMode inputClearMode; - - /// Controls the [ChatInput] keyboard type. Defaults to [TextInputType.multiline]. - final TextInputType keyboardType; - - /// Will be called whenever the text inside [TextField] changes. - final void Function(String)? onTextChanged; - - /// Will be called on [TextField] tap. - final VoidCallback? onTextFieldTap; - - /// Custom [TextEditingController]. If not provided, defaults to the - /// [InputTextFieldController], which extends [TextEditingController] and has - /// additional fatures like markdown support. If you want to keep additional - /// features but still need some methods from the default [TextEditingController], - /// you can create your own [InputTextFieldController] (imported from this lib) - /// and pass it here. - final TextEditingController? textEditingController; - - /// Controls the [TextInput] autocorrect behavior. Defaults to [true]. - final bool autocorrect; - - /// Whether [TextInput] should have focus. Defaults to [false]. - final bool autofocus; - - /// Controls the [TextInput] enableSuggestions behavior. Defaults to [true]. - final bool enableSuggestions; - - /// Controls the [TextInput] enabled behavior. Defaults to [true]. - final bool enabled; -} - final isMobile = defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS; 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 a37a0824ed..02b77664e8 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 @@ -6,7 +6,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; - class RelatedQuestionList extends StatelessWidget { const RelatedQuestionList({ required this.chatId, @@ -97,6 +96,7 @@ class _RelatedQuestionItemState extends State { style: TextStyle( color: _isHovered ? Theme.of(context).colorScheme.primary : null, fontSize: 14, + height: 1.5, ), ), onTap: () { diff --git a/frontend/appflowy_flutter/lib/shared/feature_flags.dart b/frontend/appflowy_flutter/lib/shared/feature_flags.dart index ff780e2647..47dacc85b5 100644 --- a/frontend/appflowy_flutter/lib/shared/feature_flags.dart +++ b/frontend/appflowy_flutter/lib/shared/feature_flags.dart @@ -4,6 +4,7 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; typedef FeatureFlagMap = Map; @@ -91,7 +92,7 @@ enum FeatureFlag { bool get isOn { if ([ - // if (kDebugMode) FeatureFlag.planBilling, + if (kDebugMode) FeatureFlag.planBilling, // release this feature in version 0.6.1 FeatureFlag.spaceDesign, // release this feature in version 0.5.9 diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 09b6eef77c..5a62c9b073 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/m import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -128,7 +129,7 @@ class _LocalAIOnBoarding extends StatelessWidget { child: BlocBuilder( builder: (context, state) { // Show the local AI settings if the user has purchased the AI Local plan - if (state.isPurchaseAILocal) { + if (kDebugMode || state.isPurchaseAILocal) { return const LocalAISetting(); } else { // Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart index 47bfc2a7b9..aa4e5f2465 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:appflowy/util/int64_extension.dart'; @@ -209,22 +211,26 @@ class _SettingsBillingViewState extends State { ), ), const SettingsDashedDivider(), - _AITile( - plan: SubscriptionPlanPB.AiLocal, - label: LocaleKeys - .settings_billingPage_addons_aiOnDevice_label - .tr(), - description: LocaleKeys - .settings_billingPage_addons_aiOnDevice_description, - activeDescription: LocaleKeys - .settings_billingPage_addons_aiOnDevice_activeDescription, - canceledDescription: LocaleKeys - .settings_billingPage_addons_aiOnDevice_canceledDescription, - subscriptionInfo: - state.subscriptionInfo.addOns.firstWhereOrNull( - (a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal, + + // Currently, the AI Local tile is only available on macOS + // TODO(nathan): enable windows and linux + if (Platform.isMacOS) + _AITile( + plan: SubscriptionPlanPB.AiLocal, + label: LocaleKeys + .settings_billingPage_addons_aiOnDevice_label + .tr(), + description: LocaleKeys + .settings_billingPage_addons_aiOnDevice_description, + activeDescription: LocaleKeys + .settings_billingPage_addons_aiOnDevice_activeDescription, + canceledDescription: LocaleKeys + .settings_billingPage_addons_aiOnDevice_canceledDescription, + subscriptionInfo: + state.subscriptionInfo.addOns.firstWhereOrNull( + (a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal, + ), ), - ), ], ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart index 0aaf2148a7..ac1fd2ab09 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -136,38 +138,46 @@ class _SettingsPlanViewState extends State { ), ), const HSpace(8), - Flexible( - child: _AddOnBox( - title: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_title - .tr(), - description: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_description - .tr(), - price: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_price - .tr( - args: [SubscriptionPlanPB.AiLocal.priceAnnualBilling], + + // Currently, the AI Local tile is only available on macOS + // TODO(nathan): enable windows and linux + if (Platform.isMacOS) + Flexible( + child: _AddOnBox( + title: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_title + .tr(), + description: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_description + .tr(), + price: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_price + .tr( + args: [ + SubscriptionPlanPB.AiLocal.priceAnnualBilling, + ], + ), + priceInfo: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_priceInfo + .tr(), + billingInfo: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_billingInfo + .tr( + args: [ + SubscriptionPlanPB.AiLocal.priceMonthBilling, + ], + ), + buttonText: state.subscriptionInfo.hasAIOnDevice + ? LocaleKeys + .settings_planPage_planUsage_addons_activeLabel + .tr() + : LocaleKeys + .settings_planPage_planUsage_addons_addLabel + .tr(), + isActive: state.subscriptionInfo.hasAIOnDevice, + plan: SubscriptionPlanPB.AiLocal, ), - priceInfo: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_priceInfo - .tr(), - billingInfo: LocaleKeys - .settings_planPage_planUsage_addons_aiOnDevice_billingInfo - .tr( - args: [SubscriptionPlanPB.AiLocal.priceMonthBilling], - ), - buttonText: state.subscriptionInfo.hasAIOnDevice - ? LocaleKeys - .settings_planPage_planUsage_addons_activeLabel - .tr() - : LocaleKeys - .settings_planPage_planUsage_addons_addLabel - .tr(), - isActive: state.subscriptionInfo.hasAIOnDevice, - plan: SubscriptionPlanPB.AiLocal, ), - ), ], ), ], diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a3958a1aff..30d5685319 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -169,7 +169,10 @@ "question2": "Explain the GTD method", "question3": "Why use Rust", "question4": "Recipe with what's in my kitchen", - "aiMistakePrompt": "AI can make mistakes. Check important info." + "aiMistakePrompt": "AI can make mistakes. Check important info.", + "chatWithFilePrompt": "Do you want to chat with the file?", + "indexFileSuccess": "Indexing file successfully", + "indexingFile": "Indexing {}" }, "trash": { "text": "Trash", diff --git a/frontend/rust-lib/flowy-chat/src/event_handler.rs b/frontend/rust-lib/flowy-chat/src/event_handler.rs index 63c54a1f29..59eb74a058 100644 --- a/frontend/rust-lib/flowy-chat/src/event_handler.rs +++ b/frontend/rust-lib/flowy-chat/src/event_handler.rs @@ -12,7 +12,7 @@ use crate::entities::*; use crate::local_ai::local_llm_chat::LLMModelInfo; use crate::notification::{make_notification, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY}; use crate::tools::AITools; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use lib_infra::isolate_stream::IsolateSink; @@ -208,6 +208,25 @@ pub(crate) async fn chat_file_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let file_path = PathBuf::from(&data.file_path); + + let allowed_extensions = ["pdf", "md", "txt"]; + let extension = file_path + .extension() + .and_then(|ext| ext.to_str()) + .ok_or_else(|| { + FlowyError::new( + ErrorCode::UnsupportedFileFormat, + "Can't find file extension", + ) + })?; + + if !allowed_extensions.contains(&extension) { + return Err(FlowyError::new( + ErrorCode::UnsupportedFileFormat, + "Only support pdf,md and txt", + )); + } + let (tx, rx) = oneshot::channel::>(); tokio::spawn(async move { let chat_manager = upgrade_chat_manager(chat_manager)?; diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/local_llm_chat.rs b/frontend/rust-lib/flowy-chat/src/local_ai/local_llm_chat.rs index 09c5d36f41..f08a62c230 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/local_llm_chat.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/local_llm_chat.rs @@ -150,7 +150,8 @@ impl LocalAIController { pub fn is_rag_enabled(&self) -> bool { self .store_preferences - .get_bool_or_default(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED) + .get_bool(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED) + .unwrap_or(true) } pub fn open_chat(&self, chat_id: &str) { diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 79d2edd035..b284189f19 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -151,7 +151,6 @@ impl DocumentManager { } } - #[tracing::instrument(level = "info", skip(self), err)] pub async fn get_document(&self, doc_id: &str) -> FlowyResult> { if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { return Ok(doc); @@ -160,7 +159,7 @@ impl DocumentManager { if let Some(doc) = self.restore_document_from_removing(doc_id) { return Ok(doc); } - return Err(FlowyError::internal().with_context("Call open document first")); + Err(FlowyError::internal().with_context("Call open document first")) } /// Returns Document for given object id diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 6376b392ba..64b1ca6104 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -298,6 +298,9 @@ pub enum ErrorCode { #[error("Response timeout")] ResponseTimeout = 103, + + #[error("Unsupported file format")] + UnsupportedFileFormat = 104, } impl ErrorCode {