From c8cdb86b95c8be52ae7e8504fa5b0ccf25234139 Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 11 Jul 2024 21:54:07 +0800 Subject: [PATCH 01/13] chore: download file --- .../ai_chat/application/chat_bloc.dart | 3 +- .../lib/plugins/ai_chat/chat_page.dart | 53 +++- .../settings/ai/setting_local_ai_bloc.dart | 195 ------------ .../settings/pages/settings_ai_view.dart | 279 ------------------ .../settings/settings_dialog.dart | 2 +- .../shared/af_dropdown_menu_entry.dart | 3 +- .../presentation/widgets/dialogs.dart | 4 +- frontend/appflowy_flutter/pubspec.lock | 8 + frontend/appflowy_flutter/pubspec.yaml | 1 + frontend/appflowy_tauri/src-tauri/Cargo.lock | 31 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 6 +- .../appflowy_web_app/src-tauri/Cargo.lock | 31 +- .../appflowy_web_app/src-tauri/Cargo.toml | 6 +- frontend/resources/translations/en.json | 8 +- frontend/rust-lib/Cargo.lock | 53 ++-- frontend/rust-lib/Cargo.toml | 8 +- frontend/rust-lib/flowy-chat-pub/src/cloud.rs | 13 +- frontend/rust-lib/flowy-chat/Cargo.toml | 3 + frontend/rust-lib/flowy-chat/src/chat.rs | 34 ++- .../rust-lib/flowy-chat/src/chat_manager.rs | 89 +++--- frontend/rust-lib/flowy-chat/src/entities.rs | 92 ++++-- .../rust-lib/flowy-chat/src/event_handler.rs | 90 +++++- frontend/rust-lib/flowy-chat/src/event_map.rs | 42 ++- frontend/rust-lib/flowy-chat/src/lib.rs | 1 + .../flowy-chat/src/local_ai/llm_resource.rs | 247 ++++++++++++++++ .../flowy-chat/src/local_ai/local_llm_chat.rs | 194 ++++++++++++ .../rust-lib/flowy-chat/src/local_ai/mod.rs | 3 + .../flowy-chat/src/local_ai/request.rs | 165 +++++++++++ .../src/middleware/chat_service_mw.rs | 165 ++++------- .../rust-lib/flowy-chat/src/notification.rs | 2 + .../flowy-core/src/deps_resolve/chat_deps.rs | 5 + .../flowy-core/src/integrate/trait_impls.rs | 25 +- frontend/rust-lib/flowy-core/src/lib.rs | 12 +- frontend/rust-lib/flowy-error/src/errors.rs | 8 +- .../flowy-server/src/af_cloud/impls/chat.rs | 35 ++- .../rust-lib/flowy-server/src/default_impl.rs | 19 +- .../src/services/authenticate_user.rs | 5 + 37 files changed, 1181 insertions(+), 759 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/setting_local_ai_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_ai_view.dart create mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs create mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/local_llm_chat.rs create mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/mod.rs create mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/request.rs 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 8f3b5a14f84d3..2dc4926cc8b0e 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 @@ -7,6 +7,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -480,7 +481,7 @@ class ChatState with _$ChatState { @freezed class LoadingState with _$LoadingState { const factory LoadingState.loading() = _Loading; - const factory LoadingState.finish() = _Finish; + const factory LoadingState.finish({FlowyError? error}) = _Finish; } enum OnetimeShotType { 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 c6a5e832c3e56..2c91e7fc11e9c 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,3 +1,7 @@ +import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; +import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -50,7 +54,7 @@ class AIChatUILayout { } } -class AIChatPage extends StatefulWidget { +class AIChatPage extends StatelessWidget { const AIChatPage({ super.key, required this.view, @@ -63,10 +67,53 @@ class AIChatPage extends StatefulWidget { final UserProfilePB userProfile; @override - State createState() => _AIChatPageState(); + Widget build(BuildContext context) { + if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) { + return BlocProvider( + create: (context) => ChatFileBloc(chatId: view.id.toString()), + child: BlocBuilder( + builder: (context, state) { + return DropTarget( + onDragDone: (DropDoneDetails detail) async { + for (final file in detail.files) { + context + .read() + .add(ChatFileEvent.newFile(file.path)); + } + }, + child: _ChatContentPage( + view: view, + userProfile: userProfile, + ), + ); + }, + ), + ); + } + + return Center( + child: FlowyText( + LocaleKeys.chat_unsupportedCloudPrompt.tr(), + fontSize: 20, + ), + ); + } +} + +class _ChatContentPage extends StatefulWidget { + const _ChatContentPage({ + required this.view, + required this.userProfile, + }); + + final UserProfilePB userProfile; + final ViewPB view; + + @override + State<_ChatContentPage> createState() => _ChatContentPageState(); } -class _AIChatPageState extends State { +class _ChatContentPageState extends State<_ChatContentPage> { late types.User _user; @override diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/setting_local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/setting_local_ai_bloc.dart deleted file mode 100644 index e8d9c5eb31fe9..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/setting_local_ai_bloc.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:path/path.dart' as path; -import 'package:protobuf/protobuf.dart'; - -part 'setting_local_ai_bloc.freezed.dart'; - -class SettingsAILocalBloc - extends Bloc { - SettingsAILocalBloc() : super(const SettingsAILocalState()) { - on(_handleEvent); - } - - /// Handles incoming events and dispatches them to the appropriate handler. - Future _handleEvent( - SettingsAILocalEvent event, - Emitter emit, - ) async { - await event.when( - started: _handleStarted, - didUpdateAISetting: (settings) async { - _handleDidUpdateAISetting(settings, emit); - }, - updateChatBin: (chatBinPath) async { - await _handleUpdatePath( - filePath: chatBinPath, - emit: emit, - stateUpdater: () => state.copyWith( - chatBinPath: chatBinPath.trim(), - chatBinPathError: null, - ), - errorUpdater: (error) => state.copyWith(chatBinPathError: error), - ); - }, - updateChatModelPath: (chatModelPath) async { - await _handleUpdatePath( - filePath: chatModelPath, - emit: emit, - stateUpdater: () => state.copyWith( - chatModelPath: chatModelPath.trim(), - chatModelPathError: null, - ), - errorUpdater: (error) => state.copyWith(chatModelPathError: error), - ); - }, - toggleLocalAI: () async { - emit(state.copyWith(localAIEnabled: !state.localAIEnabled)); - }, - saveSetting: () async { - _handleSaveSetting(); - }, - ); - } - - /// Handles the event to fetch local AI settings when the application starts. - Future _handleStarted() async { - final result = await ChatEventGetLocalAISetting().send(); - result.fold( - (setting) { - if (!isClosed) { - add(SettingsAILocalEvent.didUpdateAISetting(setting)); - } - }, - (err) => Log.error('Failed to get local AI setting: $err'), - ); - } - - /// Handles the event to update the AI settings in the state. - void _handleDidUpdateAISetting( - LocalLLMSettingPB settings, - Emitter emit, - ) { - final newState = state.copyWith( - aiSettings: settings, - chatBinPath: settings.chatBinPath, - chatModelPath: settings.chatModelPath, - localAIEnabled: settings.enabled, - loadingState: const LoadingState.finish(), - ); - emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState))); - } - - /// Handles updating file paths (both chat binary and chat model paths). - Future _handleUpdatePath({ - required String filePath, - required Emitter emit, - required SettingsAILocalState Function() stateUpdater, - required SettingsAILocalState Function(String) errorUpdater, - }) async { - filePath = filePath.trim(); - if (filePath.isEmpty) { - emit(stateUpdater()); - return; - } - - final validationError = await _validatePath(filePath); - if (validationError != null) { - emit(errorUpdater(validationError)); - return; - } - - final newState = stateUpdater(); - emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState))); - } - - /// Validates the provided file path. - Future _validatePath(String filePath) async { - if (!isAbsolutePath(filePath)) { - return "$filePath must be absolute"; - } - - if (!await pathExists(filePath)) { - return "$filePath does not exist"; - } - return null; - } - - /// Handles saving the updated settings. - void _handleSaveSetting() { - if (state.aiSettings == null) return; - state.aiSettings!.freeze(); - final newSetting = state.aiSettings!.rebuild((value) { - value - ..chatBinPath = state.chatBinPath ?? value.chatBinPath - ..chatModelPath = state.chatModelPath ?? value.chatModelPath - ..enabled = state.localAIEnabled; - }); - - ChatEventUpdateLocalAISetting(newSetting).send().then((result) { - result.fold( - (_) { - if (!isClosed) { - add(SettingsAILocalEvent.didUpdateAISetting(newSetting)); - } - }, - (err) => Log.error('Failed to update local AI setting: $err'), - ); - }); - } - - /// Determines if the save button should be enabled based on the state. - bool _saveButtonEnabled(SettingsAILocalState newState) { - return newState.chatBinPathError == null && - newState.chatModelPathError == null && - newState.chatBinPath != null && - newState.chatModelPath != null; - } -} - -@freezed -class SettingsAILocalEvent with _$SettingsAILocalEvent { - const factory SettingsAILocalEvent.started() = _Started; - const factory SettingsAILocalEvent.didUpdateAISetting( - LocalLLMSettingPB settings, - ) = _GetAISetting; - const factory SettingsAILocalEvent.updateChatBin(String chatBinPath) = - _UpdateChatBin; - const factory SettingsAILocalEvent.updateChatModelPath(String chatModelPath) = - _UpdateChatModelPath; - const factory SettingsAILocalEvent.toggleLocalAI() = _EnableLocalAI; - const factory SettingsAILocalEvent.saveSetting() = _SaveSetting; -} - -@freezed -class SettingsAILocalState with _$SettingsAILocalState { - const factory SettingsAILocalState({ - LocalLLMSettingPB? aiSettings, - String? chatBinPath, - String? chatBinPathError, - String? chatModelPath, - String? chatModelPathError, - @Default(false) bool localAIEnabled, - @Default(false) bool saveButtonEnabled, - @Default(LoadingState.loading()) LoadingState loadingState, - }) = _SettingsAILocalState; -} - -/// Checks if a given file path is absolute. -bool isAbsolutePath(String filePath) { - return path.isAbsolute(filePath); -} - -/// Checks if a given file or directory path exists. -Future pathExists(String filePath) async { - final file = File(filePath); - final directory = Directory(filePath); - - return await file.exists() || await directory.exists(); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_ai_view.dart deleted file mode 100644 index 1fba43b3dcea7..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_ai_view.dart +++ /dev/null @@ -1,279 +0,0 @@ -import 'package:appflowy/workspace/application/settings/ai/setting_local_ai_bloc.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget { - const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30), - child: FlowyText( - LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(), - maxLines: null, - fontSize: 16, - lineHeight: 1.6, - ), - ); - } -} - -class SettingsAIView extends StatelessWidget { - const SettingsAIView({super.key, required this.userProfile}); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => - SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()), - child: BlocBuilder( - builder: (context, state) { - return SettingsBody( - title: LocaleKeys.settings_aiPage_title.tr(), - description: - LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), - children: const [ - AIModelSelection(), - _AISearchToggle(value: false), - // Disable local AI configuration for now. It's not ready for production. - LocalAIConfiguration(), - ], - ); - }, - ), - ); - } -} - -class AIModelSelection extends StatelessWidget { - const AIModelSelection({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: FlowyText.medium( - LocaleKeys.settings_aiPage_keys_llmModel.tr(), - ), - ), - const Spacer(), - Flexible( - child: BlocBuilder( - builder: (context, state) { - return SettingsDropdown( - key: const Key('AIModelDropdown'), - onChanged: (model) => context - .read() - .add(SettingsAIEvent.selectModel(model)), - selectedOption: state.userProfile.aiModel, - options: _availableModels - .map( - (format) => buildDropdownMenuEntry( - context, - value: format, - label: _titleForAIModel(format), - ), - ) - .toList(), - ); - }, - ), - ), - ], - ); - } -} - -List _availableModels = [ - AIModelPB.DefaultModel, - AIModelPB.Claude3Opus, - AIModelPB.Claude3Sonnet, - AIModelPB.GPT35, - AIModelPB.GPT4o, -]; - -String _titleForAIModel(AIModelPB model) { - switch (model) { - case AIModelPB.DefaultModel: - return "Default"; - case AIModelPB.Claude3Opus: - return "Claude 3 Opus"; - case AIModelPB.Claude3Sonnet: - return "Claude 3 Sonnet"; - case AIModelPB.GPT35: - return "GPT-3.5"; - case AIModelPB.GPT4o: - return "GPT-4o"; - case AIModelPB.LocalAIModel: - return "Local"; - default: - Log.error("Unknown AI model: $model, fallback to default"); - return "Default"; - } -} - -class _AISearchToggle extends StatelessWidget { - const _AISearchToggle({required this.value}); - - final bool value; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - FlowyText.medium( - LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(), - ), - const Spacer(), - BlocBuilder( - builder: (context, state) { - if (state.aiSettings == null) { - return const Padding( - padding: EdgeInsets.only(top: 6), - child: SizedBox( - height: 26, - width: 26, - child: CircularProgressIndicator.adaptive(), - ), - ); - } else { - return Toggle( - value: state.enableSearchIndexing, - onChanged: (_) => context - .read() - .add(const SettingsAIEvent.toggleAISearch()), - ); - } - }, - ), - ], - ); - } -} - -class LocalAIConfiguration extends StatelessWidget { - const LocalAIConfiguration({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - SettingsAILocalBloc()..add(const SettingsAILocalEvent.started()), - child: BlocBuilder( - builder: (context, state) { - return state.loadingState.when( - loading: () { - return const SizedBox.shrink(); - }, - finish: () { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AIConfigurateTextField( - title: 'chat bin path', - hitText: '', - errorText: state.chatBinPathError ?? '', - value: state.aiSettings?.chatBinPath ?? '', - onChanged: (value) { - context.read().add( - SettingsAILocalEvent.updateChatBin(value), - ); - }, - ), - const VSpace(16), - AIConfigurateTextField( - title: 'chat model path', - hitText: '', - errorText: state.chatModelPathError ?? '', - value: state.aiSettings?.chatModelPath ?? '', - onChanged: (value) { - context.read().add( - SettingsAILocalEvent.updateChatModelPath(value), - ); - }, - ), - const VSpace(16), - Toggle( - value: state.localAIEnabled, - onChanged: (_) => context - .read() - .add(const SettingsAILocalEvent.toggleLocalAI()), - ), - const VSpace(16), - FlowyButton( - disable: !state.saveButtonEnabled, - text: const FlowyText("save"), - onTap: () { - context.read().add( - const SettingsAILocalEvent.saveSetting(), - ); - }, - ), - ], - ); - }, - ); - }, - ), - ); - } -} - -class AIConfigurateTextField extends StatelessWidget { - const AIConfigurateTextField({ - required this.title, - required this.hitText, - required this.errorText, - required this.value, - required this.onChanged, - super.key, - }); - - final String title; - final String hitText; - final String errorText; - final String value; - final void Function(String) onChanged; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - title, - ), - const VSpace(8), - RoundedInputField( - hintText: hitText, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - normalBorderColor: Theme.of(context).colorScheme.outline, - errorBorderColor: Theme.of(context).colorScheme.error, - cursorColor: Theme.of(context).colorScheme.primary, - errorText: errorText, - initialValue: value, - onChanged: onChanged, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index de7f167a4597b..9e79e3eb39c08 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -4,7 +4,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/settings_ai_view.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index c58e3ecc26ae1..919db8329b6c9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -13,6 +13,7 @@ DropdownMenuEntry buildDropdownMenuEntry( Widget? leadingWidget, Widget? trailingWidget, String? fontFamily, + EdgeInsets padding = const EdgeInsets.symmetric(vertical: 4), }) { final fontFamilyUsed = fontFamily != null ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily @@ -32,7 +33,7 @@ DropdownMenuEntry buildDropdownMenuEntry( label: label, leadingIcon: leadingWidget, labelWidget: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: padding, child: FlowyText.regular( label, fontSize: 14, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 7576530c17eb1..0ffd2fb0959bc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -183,6 +183,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { this.title, this.message, this.maxWidth, + this.titleUpperCase = true, }); final VoidCallback? onOkPressed; @@ -192,6 +193,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { final String? title; final String? message; final double? maxWidth; + final bool titleUpperCase; @override Widget build(BuildContext context) { @@ -203,7 +205,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { children: [ if (title != null) ...[ FlowyText.medium( - title!.toUpperCase(), + titleUpperCase ? title!.toUpperCase() : title!, fontSize: FontSizes.s16, maxLines: 3, ), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index defd58dadf911..c6ec6e7556cf4 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -410,6 +410,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d + url: "https://pub.dev" + source: hosted + version: "0.4.4" device_info_plus: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 51ec3d483f03f..97f56d1356e74 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -141,6 +141,7 @@ dependencies: shimmer: ^3.0.0 isolates: ^3.0.3+8 markdown_widget: ^2.3.2+6 + desktop_drop: ^0.4.4 # Window Manager for MacOS and Linux window_manager: ^0.3.9 diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index a5e79afdd4b65..2543e2572d032 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bincode", @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bytes", @@ -206,7 +206,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" dependencies = [ "anyhow", "appflowy-plugin", @@ -221,7 +221,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" dependencies = [ "anyhow", "cfg-if", @@ -806,7 +806,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "again", "anyhow", @@ -855,7 +855,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "collab-entity", "collab-rt-entity", @@ -867,7 +867,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "futures-channel", "futures-util", @@ -1107,7 +1107,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bincode", @@ -1132,7 +1132,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "async-trait", @@ -1486,7 +1486,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", @@ -1883,6 +1883,7 @@ dependencies = [ "anyhow", "appflowy-local-ai", "appflowy-plugin", + "base64 0.21.5", "bytes", "dashmap", "flowy-chat-pub", @@ -1897,8 +1898,10 @@ dependencies = [ "log", "parking_lot 0.12.1", "protobuf", + "reqwest", "serde", "serde_json", + "sha2", "strum_macros 0.21.1", "tokio", "tokio-stream", @@ -2937,7 +2940,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "futures-util", @@ -2954,7 +2957,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", @@ -3386,7 +3389,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bytes", @@ -5879,7 +5882,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index ce2c1ab729f0b..ca5d5d481a2b4 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" } [dependencies] serde_json.workspace = true @@ -127,5 +127,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 9ed5393c3f3af..f4332a74efb1d 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bytes", @@ -197,7 +197,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" dependencies = [ "anyhow", "appflowy-plugin", @@ -212,7 +212,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" dependencies = [ "anyhow", "cfg-if", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "again", "anyhow", @@ -829,7 +829,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "collab-entity", "collab-rt-entity", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "futures-channel", "futures-util", @@ -1090,7 +1090,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bincode", @@ -1115,7 +1115,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "async-trait", @@ -1476,7 +1476,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", @@ -1923,6 +1923,7 @@ dependencies = [ "anyhow", "appflowy-local-ai", "appflowy-plugin", + "base64 0.21.7", "bytes", "dashmap", "flowy-chat-pub", @@ -1937,8 +1938,10 @@ dependencies = [ "log", "parking_lot 0.12.1", "protobuf", + "reqwest", "serde", "serde_json", + "sha2", "strum_macros 0.21.1", "tokio", "tokio-stream", @@ -3014,7 +3017,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "futures-util", @@ -3031,7 +3034,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", @@ -3468,7 +3471,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bytes", @@ -5977,7 +5980,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 9a95bc584a233..2d90a5e865382 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" } [dependencies] serde_json.workspace = true @@ -128,6 +128,6 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 6c37938d97a2b..e551a4d80932e 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -615,6 +615,12 @@ "aiSettingsDescription": "Select or configure Ai models used on @:appName. For best performance we recommend using the default model options", "loginToEnableAIFeature": "AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up", "llmModel": "Language Model", + "llmModelType": "Language Model Type", + "downloadLLMPrompt": "Download {} local model", + "downloadLLMPromptDetail": "Downloading {} local model will take up to {} of storage. Do you want to continue?", + "downloadAIModelButton": "Download AI model", + "downloadingModel": "Downloading", + "downloadingModelPrompt": "Do not navigate away or close the app while file is downloading", "title": "AI API Keys", "openAILabel": "OpenAI API key", "openAITooltip": "You can find your Secret API key on the API key page", @@ -2031,4 +2037,4 @@ "movePageToSpace": "Move page to space", "switchSpace": "Switch space" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index b84e18e1db2be..447b03f80f077 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bytes", @@ -197,7 +197,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" dependencies = [ "anyhow", "appflowy-plugin", @@ -212,7 +212,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" dependencies = [ "anyhow", "cfg-if", @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "again", "anyhow", @@ -747,7 +747,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "collab-entity", "collab-rt-entity", @@ -759,7 +759,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "futures-channel", "futures-util", @@ -968,7 +968,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bincode", @@ -993,7 +993,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "async-trait", @@ -1210,7 +1210,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1310,7 +1310,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", @@ -1703,6 +1703,7 @@ dependencies = [ "anyhow", "appflowy-local-ai", "appflowy-plugin", + "base64 0.21.5", "bytes", "dashmap", "dotenv", @@ -1718,8 +1719,10 @@ dependencies = [ "log", "parking_lot 0.12.1", "protobuf", + "reqwest", "serde", "serde_json", + "sha2", "simsimd", "strum_macros 0.21.1", "tokio", @@ -2613,7 +2616,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "futures-util", @@ -2630,7 +2633,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", @@ -2995,7 +2998,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "bytes", @@ -3874,7 +3877,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", ] @@ -3894,7 +3897,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", ] @@ -3962,19 +3964,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" @@ -4178,7 +4167,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -4199,7 +4188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.47", @@ -5096,7 +5085,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index acae7d44cdc21..3a3a4fb63ebc4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -97,8 +97,8 @@ validator = { version = "0.16.1", features = ["derive"] } # 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 = "9884d93aa2805013f36a79c1757174a0b5063065" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" } [profile.dev] opt-level = 1 @@ -149,5 +149,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } diff --git a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs index fae11d979e917..8a5a424951a2a 100644 --- a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs @@ -1,6 +1,7 @@ use bytes::Bytes; pub use client_api::entity::ai_dto::{ - CompletionType, RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage, + AppFlowyAIPlugin, CompletionType, LLMModel, LocalAIConfig, RelatedQuestion, + RepeatedRelatedQuestion, StringOrMessage, }; pub use client_api::entity::{ ChatAuthorType, ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage, @@ -10,6 +11,7 @@ use flowy_error::FlowyError; use futures::stream::BoxStream; use lib_infra::async_trait::async_trait; use lib_infra::future::FutureResult; +use std::path::PathBuf; pub type ChatMessageStream = BoxStream<'static, Result>; pub type StreamAnswer = BoxStream<'static, Result>; @@ -74,4 +76,13 @@ pub trait ChatCloudService: Send + Sync + 'static { text: &str, complete_type: CompletionType, ) -> Result; + + async fn index_file( + &self, + workspace_id: &str, + file_path: PathBuf, + chat_id: &str, + ) -> Result<(), FlowyError>; + + async fn get_local_ai_config(&self, workspace_id: &str) -> Result; } diff --git a/frontend/rust-lib/flowy-chat/Cargo.toml b/frontend/rust-lib/flowy-chat/Cargo.toml index a7827083308db..9cd9a28031368 100644 --- a/frontend/rust-lib/flowy-chat/Cargo.toml +++ b/frontend/rust-lib/flowy-chat/Cargo.toml @@ -35,6 +35,9 @@ tokio-stream = "0.1.15" parking_lot.workspace = true appflowy-local-ai = { version = "0.1.0", features = ["verbose"] } appflowy-plugin = { version = "0.1.0", features = ["verbose"] } +reqwest = "0.11.27" +sha2 = "0.10.7" +base64 = "0.21.5" [dev-dependencies] dotenv = "0.15.0" diff --git a/frontend/rust-lib/flowy-chat/src/chat.rs b/frontend/rust-lib/flowy-chat/src/chat.rs index def485326efbc..6f3cff2a8d0ea 100644 --- a/frontend/rust-lib/flowy-chat/src/chat.rs +++ b/frontend/rust-lib/flowy-chat/src/chat.rs @@ -2,7 +2,7 @@ use crate::chat_manager::ChatUserService; use crate::entities::{ ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB, }; -use crate::middleware::chat_service_mw::ChatService; +use crate::middleware::chat_service_mw::ChatServiceMiddleware; use crate::notification::{send_notification, ChatNotification}; use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable}; use allo_isolate::Isolate; @@ -11,6 +11,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicI64}; use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; @@ -26,7 +27,7 @@ pub struct Chat { chat_id: String, uid: i64, user_service: Arc, - chat_service: Arc, + chat_service: Arc, prev_message_state: Arc>, latest_message_id: Arc, stop_stream: Arc, @@ -38,7 +39,7 @@ impl Chat { uid: i64, chat_id: String, user_service: Arc, - chat_service: Arc, + chat_service: Arc, ) -> Chat { Chat { uid, @@ -435,6 +436,33 @@ impl Chat { Ok(messages) } + + #[instrument(level = "debug", skip_all, err)] + pub async fn index_file(&self, file_path: PathBuf) -> FlowyResult<()> { + if !file_path.exists() { + return Err( + FlowyError::record_not_found().with_context(format!("{:?} not exist", file_path)), + ); + } + + if !file_path.is_file() { + return Err( + FlowyError::invalid_data().with_context(format!("{:?} is not a file ", file_path)), + ); + } + + trace!( + "[Chat] index file: chat_id={}, file_path={:?}", + self.chat_id, + file_path + ); + self + .chat_service + .index_file(&self.user_service.workspace_id()?, file_path, &self.chat_id) + .await?; + + Ok(()) + } } fn save_chat_message( diff --git a/frontend/rust-lib/flowy-chat/src/chat_manager.rs b/frontend/rust-lib/flowy-chat/src/chat_manager.rs index b900eb3e5c851..0e3fc0d27f3dc 100644 --- a/frontend/rust-lib/flowy-chat/src/chat_manager.rs +++ b/frontend/rust-lib/flowy-chat/src/chat_manager.rs @@ -1,30 +1,36 @@ use crate::chat::Chat; use crate::entities::{ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB}; -use crate::middleware::chat_service_mw::ChatService; +use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting, LocalLLMController}; +use crate::middleware::chat_service_mw::ChatServiceMiddleware; use crate::persistence::{insert_chat, ChatTable}; -use appflowy_local_ai::llm_chat::{LocalChatLLMChat, LocalLLMSetting}; +use allo_isolate::Isolate; +use appflowy_local_ai::llm_chat::LocalLLMSetting; use appflowy_plugin::manager::PluginManager; use dashmap::DashMap; -use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageType}; +use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageType, LLMModel}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; +use lib_infra::isolate_stream::IsolateSink; use lib_infra::util::timestamp; +use std::path::PathBuf; use std::sync::Arc; -use tracing::trace; +use tracing::{error, info, trace}; pub trait ChatUserService: Send + Sync + 'static { fn user_id(&self) -> Result; fn device_id(&self) -> Result; fn workspace_id(&self) -> Result; fn sqlite_connection(&self, uid: i64) -> Result; + fn user_data_dir(&self) -> Result; } pub struct ChatManager { - pub chat_service: Arc, + pub chat_service_wm: Arc, pub user_service: Arc, chats: Arc>>, store_preferences: Arc, + pub llm_controller: Arc, } const LOCAL_AI_SETTING_KEY: &str = "local_ai_setting"; @@ -35,42 +41,36 @@ impl ChatManager { store_preferences: Arc, ) -> ChatManager { let user_service = Arc::new(user_service); - let local_ai_setting = store_preferences - .get_object::(LOCAL_AI_SETTING_KEY) - .unwrap_or_default(); let plugin_manager = Arc::new(PluginManager::new()); + let llm_controller = Arc::new(LocalLLMController::new( + plugin_manager.clone(), + store_preferences.clone(), + user_service.clone(), + cloud_service.clone(), + )); + + if llm_controller.is_ready() { + if let Err(err) = llm_controller.initialize() { + error!("[Chat Plugin] failed to initialize local ai: {:?}", err); + } + } - // setup local AI chat plugin - let local_llm_ctrl = Arc::new(LocalChatLLMChat::new(plugin_manager)); // setup local chat service - let chat_service = Arc::new(ChatService::new( + let chat_service_wm = Arc::new(ChatServiceMiddleware::new( user_service.clone(), cloud_service, - local_llm_ctrl, - local_ai_setting, + llm_controller.clone(), )); Self { - chat_service, + chat_service_wm, user_service, chats: Arc::new(DashMap::new()), store_preferences, + llm_controller, } } - pub fn update_local_ai_setting(&self, setting: LocalLLMSetting) -> FlowyResult<()> { - self.chat_service.update_local_ai_setting(setting.clone())?; - self - .store_preferences - .set_object(LOCAL_AI_SETTING_KEY, setting)?; - Ok(()) - } - - pub fn get_local_ai_setting(&self) -> FlowyResult { - let setting = self.chat_service.get_local_ai_setting(); - Ok(setting) - } - pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> { trace!("open chat: {}", chat_id); self.chats.entry(chat_id.to_string()).or_insert_with(|| { @@ -78,24 +78,33 @@ impl ChatManager { self.user_service.user_id().unwrap(), chat_id.to_string(), self.user_service.clone(), - self.chat_service.clone(), + self.chat_service_wm.clone(), )) }); - self.chat_service.notify_open_chat(chat_id); + trace!("[Chat Plugin] notify open chat: {}", chat_id); + self.llm_controller.open_chat(chat_id); Ok(()) } pub async fn close_chat(&self, chat_id: &str) -> Result<(), FlowyError> { trace!("close chat: {}", chat_id); - self.chat_service.notify_close_chat(chat_id); + + if self.llm_controller.is_ready() { + info!("[Chat Plugin] notify close chat: {}", chat_id); + self.llm_controller.close_chat(chat_id); + } Ok(()) } pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> { if let Some((_, chat)) = self.chats.remove(chat_id) { chat.close(); - self.chat_service.notify_close_chat(chat_id); + + if self.llm_controller.is_ready() { + info!("[Chat Plugin] notify close chat: {}", chat_id); + self.llm_controller.close_chat(chat_id); + } } Ok(()) } @@ -103,7 +112,7 @@ impl ChatManager { pub async fn create_chat(&self, uid: &i64, chat_id: &str) -> Result, FlowyError> { let workspace_id = self.user_service.workspace_id()?; self - .chat_service + .chat_service_wm .create_chat(uid, &workspace_id, chat_id) .await?; save_chat(self.user_service.sqlite_connection(*uid)?, chat_id)?; @@ -112,7 +121,7 @@ impl ChatManager { self.user_service.user_id().unwrap(), chat_id.to_string(), self.user_service.clone(), - self.chat_service.clone(), + self.chat_service_wm.clone(), )); self.chats.insert(chat_id.to_string(), chat.clone()); Ok(chat) @@ -140,7 +149,7 @@ impl ChatManager { self.user_service.user_id().unwrap(), chat_id.to_string(), self.user_service.clone(), - self.chat_service.clone(), + self.chat_service_wm.clone(), )); self.chats.insert(chat_id.to_string(), chat.clone()); Ok(chat) @@ -215,6 +224,18 @@ impl ChatManager { chat.stop_stream_message().await; Ok(()) } + + pub async fn chat_with_file(&self, chat_id: &str, file_path: PathBuf) -> FlowyResult<()> { + let chat = self.get_or_create_chat_instance(chat_id).await?; + chat.index_file(file_path).await?; + Ok(()) + } + + pub async fn download_ai_resources(&self, progress_port: i64) -> FlowyResult<()> { + let text_sink = IsolateSink::new(Isolate::new(progress_port)); + self.llm_controller.start_downloading(text_sink)?; + Ok(()) + } } fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-chat/src/entities.rs b/frontend/rust-lib/flowy-chat/src/entities.rs index da4dcba029fe3..564042156890c 100644 --- a/frontend/rust-lib/flowy-chat/src/entities.rs +++ b/frontend/rust-lib/flowy-chat/src/entities.rs @@ -1,6 +1,7 @@ +use crate::local_ai::local_llm_chat::LLMModelInfo; use appflowy_local_ai::llm_chat::LocalLLMSetting; use flowy_chat_pub::cloud::{ - ChatMessage, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, + ChatMessage, LLMModel, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; @@ -208,33 +209,49 @@ impl From for RepeatedRelatedQuestionPB { } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct LocalLLMSettingPB { +pub struct LLMModelInfoPB { #[pb(index = 1)] - pub chat_bin_path: String, + pub selected_model: LLMModelPB, #[pb(index = 2)] - pub chat_model_path: String, - - #[pb(index = 3)] - pub enabled: bool, + pub models: Vec, } -impl From for LocalLLMSettingPB { - fn from(value: LocalLLMSetting) -> Self { - LocalLLMSettingPB { - chat_bin_path: value.chat_bin_path, - chat_model_path: value.chat_model_path, - enabled: value.enabled, +impl From for LLMModelInfoPB { + fn from(value: LLMModelInfo) -> Self { + LLMModelInfoPB { + selected_model: LLMModelPB::from(value.selected_model), + models: value.models.into_iter().map(LLMModelPB::from).collect(), } } } -impl From for LocalLLMSetting { - fn from(value: LocalLLMSettingPB) -> Self { - LocalLLMSetting { - chat_bin_path: value.chat_bin_path, - chat_model_path: value.chat_model_path, - enabled: value.enabled, +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct LLMModelPB { + #[pb(index = 1)] + pub llm_id: i64, + + #[pb(index = 2)] + pub embedding_model: String, + + #[pb(index = 3)] + pub chat_model: String, + + #[pb(index = 4)] + pub requirement: String, + + #[pb(index = 5)] + pub file_size: i64, +} + +impl From for LLMModelPB { + fn from(value: LLMModel) -> Self { + LLMModelPB { + llm_id: value.llm_id, + embedding_model: value.embedding_model.name, + chat_model: value.chat_model.name, + requirement: value.chat_model.requirements, + file_size: value.chat_model.file_size, } } } @@ -283,3 +300,40 @@ pub enum ModelTypePB { #[default] RemoteAI = 1, } + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct ChatFilePB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub file_path: String, + + #[pb(index = 2)] + #[validate(custom = "required_not_empty_str")] + pub chat_id: String, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct DownloadLLMPB { + #[pb(index = 1)] + pub progress_stream: i64, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct DownloadTaskPB { + #[pb(index = 1)] + pub task_id: String, +} +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct LocalModelStatePB { + #[pb(index = 1)] + pub model_name: String, + + #[pb(index = 2)] + pub model_size: String, + + #[pb(index = 3)] + pub need_download: bool, + + #[pb(index = 4)] + pub requirements: String, +} diff --git a/frontend/rust-lib/flowy-chat/src/event_handler.rs b/frontend/rust-lib/flowy-chat/src/event_handler.rs index 924687ca5d368..96cc0943281c3 100644 --- a/frontend/rust-lib/flowy-chat/src/event_handler.rs +++ b/frontend/rust-lib/flowy-chat/src/event_handler.rs @@ -1,14 +1,18 @@ use flowy_chat_pub::cloud::ChatMessageType; +use std::path::PathBuf; +use allo_isolate::Isolate; use std::sync::{Arc, Weak}; +use tokio::sync::oneshot; use validator::Validate; +use crate::chat_manager::ChatManager; +use crate::entities::*; +use crate::local_ai::local_llm_chat::LLMModelInfo; use crate::tools::AITools; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; - -use crate::chat_manager::ChatManager; -use crate::entities::*; +use lib_infra::isolate_stream::IsolateSink; fn upgrade_chat_manager( chat_manager: AFPluginState>, @@ -120,24 +124,41 @@ pub(crate) async fn stop_stream_handler( } #[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_local_ai_setting_handler( +pub(crate) async fn get_local_ai_model_info_handler( chat_manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let chat_manager = upgrade_chat_manager(chat_manager)?; - let setting = chat_manager.get_local_ai_setting()?; - let pb = setting.into(); - data_result_ok(pb) + let (tx, rx) = oneshot::channel::>(); + tokio::spawn(async move { + let model_info = chat_manager.llm_controller.model_info().await; + let _ = tx.send(model_info); + }); + + let model_info = rx.await??; + data_result_ok(model_info.into()) } #[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn update_local_ai_setting_handler( - data: AFPluginData, +pub(crate) async fn update_local_llm_model_handler( + data: AFPluginData, chat_manager: AFPluginState>, -) -> Result<(), FlowyError> { +) -> DataResult { let data = data.into_inner(); let chat_manager = upgrade_chat_manager(chat_manager)?; - chat_manager.update_local_ai_setting(data.into())?; - Ok(()) + let state = chat_manager + .llm_controller + .use_local_llm(data.llm_id) + .await?; + data_result_ok(state) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_local_llm_state_handler( + chat_manager: AFPluginState>, +) -> DataResult { + let chat_manager = upgrade_chat_manager(chat_manager)?; + let state = chat_manager.llm_controller.get_local_llm_state().await?; + data_result_ok(state) } pub(crate) async fn start_complete_text_handler( @@ -157,3 +178,46 @@ pub(crate) async fn stop_complete_text_handler( tools.cancel_complete_task(&data.task_id).await; Ok(()) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn chat_file_handler( + data: AFPluginData, + chat_manager: AFPluginState>, +) -> Result<(), FlowyError> { + let data = data.try_into_inner()?; + let file_path = PathBuf::from(&data.file_path); + let (tx, rx) = oneshot::channel::>(); + tokio::spawn(async move { + let chat_manager = upgrade_chat_manager(chat_manager)?; + chat_manager + .chat_with_file(&data.chat_id, file_path) + .await?; + let _ = tx.send(Ok(())); + Ok::<_, FlowyError>(()) + }); + + rx.await? +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn download_llm_resource_handler( + data: AFPluginData, + chat_manager: AFPluginState>, +) -> DataResult { + let data = data.try_into_inner()?; + let chat_manager = upgrade_chat_manager(chat_manager)?; + let text_sink = IsolateSink::new(Isolate::new(data.progress_stream)); + let task_id = chat_manager.llm_controller.start_downloading(text_sink)?; + data_result_ok(DownloadTaskPB { task_id }) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn cancel_download_llm_resource_handler( + data: AFPluginData, + chat_manager: AFPluginState>, +) -> Result<(), FlowyError> { + let data = data.into_inner(); + let chat_manager = upgrade_chat_manager(chat_manager)?; + chat_manager.llm_controller.cancel_download(&data.task_id)?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-chat/src/event_map.rs b/frontend/rust-lib/flowy-chat/src/event_map.rs index 52aad53503ec0..69f3d49ee16c8 100644 --- a/frontend/rust-lib/flowy-chat/src/event_map.rs +++ b/frontend/rust-lib/flowy-chat/src/event_map.rs @@ -11,7 +11,7 @@ use crate::event_handler::*; pub fn init(chat_manager: Weak) -> AFPlugin { let user_service = Arc::downgrade(&chat_manager.upgrade().unwrap().user_service); - let cloud_service = Arc::downgrade(&chat_manager.upgrade().unwrap().chat_service); + let cloud_service = Arc::downgrade(&chat_manager.upgrade().unwrap().chat_service_wm); let ai_tools = Arc::new(AITools::new(cloud_service, user_service)); AFPlugin::new() .name("Flowy-Chat") @@ -23,13 +23,23 @@ pub fn init(chat_manager: Weak) -> AFPlugin { .event(ChatEvent::GetRelatedQuestion, get_related_question_handler) .event(ChatEvent::GetAnswerForQuestion, get_answer_handler) .event(ChatEvent::StopStream, stop_stream_handler) - .event(ChatEvent::GetLocalAISetting, get_local_ai_setting_handler) .event( - ChatEvent::UpdateLocalAISetting, - update_local_ai_setting_handler, + ChatEvent::GetLocalAIModelInfo, + get_local_ai_model_info_handler, ) + .event(ChatEvent::UpdateLocalLLM, update_local_llm_model_handler) + .event(ChatEvent::GetLocalLLMState, get_local_llm_state_handler) .event(ChatEvent::CompleteText, start_complete_text_handler) .event(ChatEvent::StopCompleteText, stop_complete_text_handler) + .event(ChatEvent::ChatWithFile, chat_file_handler) + .event( + ChatEvent::DownloadLLMResource, + download_llm_resource_handler, + ) + .event( + ChatEvent::CancelDownloadLLMResource, + cancel_download_llm_resource_handler, + ) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -54,15 +64,27 @@ pub enum ChatEvent { #[event(input = "ChatMessageIdPB", output = "ChatMessagePB")] GetAnswerForQuestion = 5, - #[event(input = "LocalLLMSettingPB")] - UpdateLocalAISetting = 6, + #[event(input = "LLMModelPB", output = "LocalModelStatePB")] + UpdateLocalLLM = 6, - #[event(output = "LocalLLMSettingPB")] - GetLocalAISetting = 7, + #[event(output = "LocalModelStatePB")] + GetLocalLLMState = 7, + + #[event(output = "LLMModelInfoPB")] + GetLocalAIModelInfo = 8, #[event(input = "CompleteTextPB", output = "CompleteTextTaskPB")] - CompleteText = 8, + CompleteText = 9, #[event(input = "CompleteTextTaskPB")] - StopCompleteText = 9, + StopCompleteText = 10, + + #[event(input = "ChatFilePB")] + ChatWithFile = 11, + + #[event(input = "DownloadLLMPB", output = "DownloadTaskPB")] + DownloadLLMResource = 12, + + #[event(input = "DownloadTaskPB")] + CancelDownloadLLMResource = 13, } diff --git a/frontend/rust-lib/flowy-chat/src/lib.rs b/frontend/rust-lib/flowy-chat/src/lib.rs index ca5e9b1f2468f..2b4458d5ea7bf 100644 --- a/frontend/rust-lib/flowy-chat/src/lib.rs +++ b/frontend/rust-lib/flowy-chat/src/lib.rs @@ -4,6 +4,7 @@ pub mod event_map; mod chat; pub mod chat_manager; pub mod entities; +mod local_ai; mod middleware; pub mod notification; mod persistence; diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs new file mode 100644 index 0000000000000..1fbbcdbc6b317 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs @@ -0,0 +1,247 @@ +use reqwest::{Client, Response, StatusCode}; +use sha2::{Digest, Sha256}; + +use crate::chat_manager::ChatUserService; +use crate::entities::LocalModelStatePB; +use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting}; +use crate::notification::{send_notification, ChatNotification}; +use anyhow::anyhow; +use appflowy_local_ai::llm_chat::ChatPluginConfig; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use flowy_chat_pub::cloud::{LLMModel, LocalAIConfig}; +use flowy_error::{FlowyError, FlowyResult}; +use lib_infra::async_trait::async_trait; +use parking_lot::RwLock; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::fs::{self, File}; +use tokio::io::SeekFrom; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; +use tracing::{debug, info, instrument, trace}; + +#[async_trait] +pub trait LLMResourceService: Send + Sync + 'static { + async fn get_local_ai_config(&self) -> Result; + fn store(&self, setting: LLMSetting) -> Result<(), anyhow::Error>; + fn retrieve(&self) -> Option; +} + +pub struct LLMResourceController { + client: Client, + user_service: Arc, + resource_service: Arc, + llm_setting: RwLock>, + // The ai_config will be set when user try to get latest local ai config from server + ai_config: RwLock>, +} + +impl LLMResourceController { + pub fn new( + user_service: Arc, + resource_service: impl LLMResourceService, + ) -> Self { + let llm_setting = RwLock::new(resource_service.retrieve()); + Self { + client: Client::new(), + user_service, + resource_service: Arc::new(resource_service), + llm_setting, + ai_config: Default::default(), + } + } + + /// Returns true when all resources are downloaded and ready to use. + pub fn is_ready(&self) -> bool { + self.is_resource_ready().unwrap_or(false) + } + + #[instrument(level = "debug", skip_all, err)] + pub async fn model_info(&self) -> FlowyResult { + let ai_config = self + .resource_service + .get_local_ai_config() + .await + .map_err(|err| { + FlowyError::local_ai().with_context(format!("Can't retrieve model info:{}", err)) + })?; + + if ai_config.models.is_empty() { + return Err(FlowyError::local_ai().with_context("No model found")); + } + + *self.ai_config.write() = Some(ai_config.clone()); + let selected_config = ai_config.models[0].clone(); + Ok(LLMModelInfo { + selected_model: selected_config, + models: ai_config.models, + }) + } + + #[instrument(level = "info", skip_all, err)] + pub fn use_local_llm(&self, llm_id: i64) -> FlowyResult { + let (package, llm_config) = self + .ai_config + .read() + .as_ref() + .and_then(|config| { + config + .models + .iter() + .find(|model| model.llm_id == llm_id) + .cloned() + .map(|model| (config.plugin.clone(), model)) + }) + .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; + + let llm_setting = LLMSetting { + plugin: package, + llm_model: llm_config.clone(), + }; + + trace!("Selected local ai setting: {:?}", llm_setting); + *self.llm_setting.write() = Some(llm_setting.clone()); + self.resource_service.store(llm_setting)?; + self.get_local_llm_state() + } + + pub fn get_local_llm_state(&self) -> FlowyResult { + let state = self + .check_resource() + .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; + Ok(state) + } + + #[instrument(level = "debug", skip_all)] + fn check_resource(&self) -> Option { + trace!("Checking local ai resources"); + let llm_model = self + .llm_setting + .read() + .as_ref() + .map(|setting| setting.llm_model.clone())?; + let need_download = !self.is_resource_ready().ok()?; + let payload = LocalModelStatePB { + model_name: llm_model.chat_model.name, + model_size: bytes_to_readable_size(llm_model.chat_model.file_size as u64), + need_download, + requirements: llm_model.chat_model.requirements, + }; + + if need_download { + info!("Local AI resources are not ready, notify client to download ai resources"); + // notify client it needs to download ai resource + send_notification("local_ai_resource", ChatNotification::LocalAIResourceNeeded) + .payload(payload.clone()) + .send(); + } + debug!("Local AI resources state: {:?}", payload); + Some(payload) + } + + /// Returns true when all resources are downloaded and ready to use. + pub fn is_resource_ready(&self) -> FlowyResult { + match self.llm_setting.read().as_ref() { + None => Err(FlowyError::local_ai().with_context("Can't find any llm config")), + Some(llm_setting) => { + let llm_dir = self.user_service.user_data_dir()?; + + let plugin_needed = should_download_plugin(&llm_dir, llm_setting); + if plugin_needed { + return Ok(false); + } + + let model_needed = should_download_model(&llm_dir, llm_setting); + if model_needed { + return Ok(false); + } + + Ok(false) + }, + } + } + + pub fn start_downloading(&self) -> FlowyResult { + info!("Start downloading local ai resources"); + // + Ok("".to_string()) + } + + pub fn cancel_download(&self, task_id: &str) -> FlowyResult<()> { + Ok(()) + } + + pub fn get_chat_config(&self) -> FlowyResult { + if !self.is_resource_ready()? { + let _ = self.check_resource(); + return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); + } + + // let mut config = ChatPluginConfig::new( + // setting.chat_bin_path.clone(), + // setting.chat_model_path.clone(), + // )?; + // + // let persist_directory = user_data_dir.join("chat_plugin_rag"); + // if !persist_directory.exists() { + // std::fs::create_dir_all(&persist_directory)?; + // } + // + // // Enable RAG when the embedding model path is set + // if let Err(err) = config.set_rag_enabled( + // &PathBuf::from(&setting.embedding_model_path), + // &persist_directory, + // ) { + // error!( + // "[Chat Plugin] failed to enable rag: {:?}, embedding_model_path: {:?}", + // err, setting.embedding_model_path + // ); + // } + // + // if cfg!(debug_assertions) { + // config = config.with_verbose(true); + // } + // Ok(config) + todo!() + } + + fn llm_dir(&self) -> FlowyResult { + let user_data_dir = self.user_service.user_data_dir()?; + Ok(user_data_dir.join("llm")) + } +} + +pub fn should_download_plugin(llm_dir: &PathBuf, llm_setting: &LLMSetting) -> bool { + let plugin_path = llm_dir.join(format!( + "{}-{}", + llm_setting.plugin.version, llm_setting.plugin.name + )); + !plugin_path.exists() +} + +pub fn should_download_model(llm_dir: &PathBuf, llm_setting: &LLMSetting) -> bool { + let chat_model = llm_dir.join(&llm_setting.llm_model.chat_model.file_name); + if !chat_model.exists() { + return true; + } + + let embedding_model = llm_dir.join(&llm_setting.llm_model.embedding_model.file_name); + if !embedding_model.exists() { + return true; + } + + false +} + +pub fn bytes_to_readable_size(bytes: u64) -> String { + const GB: u64 = 1_000_000_000; + const MB: u64 = 1_000_000; + + if bytes >= GB { + let size_in_gb = bytes as f64 / GB as f64; + format!("{:.2} GB", size_in_gb) + } else { + let size_in_mb = bytes as f64 / MB as f64; + format!("{:.2} MB", size_in_mb) + } +} 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 new file mode 100644 index 0000000000000..7f3376e1f3ee6 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/local_ai/local_llm_chat.rs @@ -0,0 +1,194 @@ +use crate::chat_manager::ChatUserService; +use crate::entities::{ChatStatePB, LocalModelStatePB, ModelTypePB}; +use crate::local_ai::llm_resource::{LLMResourceController, LLMResourceService}; +use crate::notification::{send_notification, ChatNotification}; +use anyhow::Error; +use appflowy_local_ai::llm_chat::{ChatPluginConfig, LocalChatLLMChat}; +use appflowy_plugin::manager::PluginManager; +use appflowy_plugin::util::is_apple_silicon; +use flowy_chat_pub::cloud::{AppFlowyAIPlugin, ChatCloudService, LLMModel, LocalAIConfig}; +use flowy_error::FlowyResult; +use flowy_sqlite::kv::KVStorePreferences; +use futures::Sink; +use lib_infra::async_trait::async_trait; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::path::PathBuf; +use std::sync::Arc; +use tracing::{error, info, trace}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LLMSetting { + pub plugin: AppFlowyAIPlugin, + pub llm_model: LLMModel, +} + +pub struct LLMModelInfo { + pub selected_model: LLMModel, + pub models: Vec, +} + +const LOCAL_AI_SETTING_KEY: &str = "local_ai_setting"; +pub struct LocalLLMController { + llm_chat: Arc, + llm_res: Arc, +} + +impl Deref for LocalLLMController { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.llm_chat + } +} + +impl LocalLLMController { + pub fn new( + plugin_manager: Arc, + store_preferences: Arc, + user_service: Arc, + cloud_service: Arc, + ) -> Self { + let llm_chat = Arc::new(LocalChatLLMChat::new(plugin_manager)); + let mut rx = llm_chat.subscribe_running_state(); + tokio::spawn(async move { + while let Ok(state) = rx.recv().await { + info!("[Chat Plugin] state: {:?}", state); + } + }); + + let res_impl = LLMResourceServiceImpl { + user_service: user_service.clone(), + cloud_service, + store_preferences, + }; + let llm_res = Arc::new(LLMResourceController::new(user_service, res_impl)); + Self { llm_chat, llm_res } + } + pub async fn model_info(&self) -> FlowyResult { + self.llm_res.model_info().await + } + + pub fn initialize(&self) -> FlowyResult<()> { + let mut chat_config = self.llm_res.get_chat_config()?; + let llm_chat = self.llm_chat.clone(); + tokio::spawn(async move { + trace!("[Chat Plugin] setup local chat: {:?}", chat_config); + if is_apple_silicon().await.unwrap_or(false) { + chat_config = chat_config.with_device("gpu"); + } + match llm_chat.init_chat_plugin(chat_config).await { + Ok(_) => { + send_notification("appflowy_chat_plugin", ChatNotification::ChatStateUpdated).payload( + ChatStatePB { + model_type: ModelTypePB::LocalAI, + available: true, + }, + ); + }, + Err(err) => { + send_notification("appflowy_chat_plugin", ChatNotification::ChatStateUpdated).payload( + ChatStatePB { + model_type: ModelTypePB::LocalAI, + available: false, + }, + ); + error!("[Chat Plugin] failed to setup plugin: {:?}", err); + }, + } + }); + Ok(()) + } + + /// Returns true if the local AI is enabled and ready to use. + pub fn is_ready(&self) -> bool { + self.llm_res.is_ready() + } + + pub fn open_chat(&self, chat_id: &str) { + if !self.is_ready() { + return; + } + + let chat_id = chat_id.to_string(); + let weak_ctrl = Arc::downgrade(&self.llm_chat); + tokio::spawn(async move { + if let Some(ctrl) = weak_ctrl.upgrade() { + if let Err(err) = ctrl.create_chat(&chat_id).await { + error!("[Chat Plugin] failed to open chat: {:?}", err); + } + } + }); + } + + pub fn close_chat(&self, chat_id: &str) { + let weak_ctrl = Arc::downgrade(&self.llm_chat); + let chat_id = chat_id.to_string(); + tokio::spawn(async move { + if let Some(ctrl) = weak_ctrl.upgrade() { + if let Err(err) = ctrl.close_chat(&chat_id).await { + error!("[Chat Plugin] failed to close chat: {:?}", err); + } + } + }); + } + + pub async fn use_local_llm(&self, llm_id: i64) -> FlowyResult { + let llm_chat = self.llm_chat.clone(); + match llm_chat.destroy_chat_plugin().await { + Ok(_) => info!("[Chat Plugin] destroy plugin successfully"), + Err(err) => error!("[Chat Plugin] failed to destroy plugin: {:?}", err), + } + let state = self.llm_res.use_local_llm(llm_id)?; + // Re-initialize the plugin if the setting is updated and ready to use + if self.llm_res.is_ready() { + self.initialize()?; + } + Ok(state) + } + + pub async fn get_local_llm_state(&self) -> FlowyResult { + self.llm_res.get_local_llm_state() + } + + pub fn start_downloading>(&self, progress_sink: T) -> FlowyResult { + let task_id = self.llm_res.start_downloading()?; + Ok(task_id) + } + + pub fn cancel_download(&self, task_id: &str) -> FlowyResult<()> { + self.llm_res.cancel_download(task_id)?; + Ok(()) + } +} + +pub struct LLMResourceServiceImpl { + user_service: Arc, + cloud_service: Arc, + store_preferences: Arc, +} +#[async_trait] +impl LLMResourceService for LLMResourceServiceImpl { + async fn get_local_ai_config(&self) -> Result { + let workspace_id = self.user_service.workspace_id()?; + let config = self + .cloud_service + .get_local_ai_config(&workspace_id) + .await?; + Ok(config) + } + + fn store(&self, setting: LLMSetting) -> Result<(), Error> { + self + .store_preferences + .set_object(LOCAL_AI_SETTING_KEY, setting)?; + Ok(()) + } + + fn retrieve(&self) -> Option { + self + .store_preferences + .get_object::(LOCAL_AI_SETTING_KEY) + } +} diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs b/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs new file mode 100644 index 0000000000000..cd63acf0cce65 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs @@ -0,0 +1,3 @@ +pub mod llm_resource; +pub mod local_llm_chat; +mod request; diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/request.rs new file mode 100644 index 0000000000000..e94580a7666c3 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/local_ai/request.rs @@ -0,0 +1,165 @@ +use reqwest::{Client, Response, StatusCode}; +use sha2::{Digest, Sha256}; + +use crate::chat_manager::ChatUserService; +use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting}; +use anyhow::anyhow; +use appflowy_local_ai::llm_chat::ChatPluginConfig; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use flowy_chat_pub::cloud::{LLMModel, LocalAIConfig}; +use flowy_error::{FlowyError, FlowyResult}; +use lib_infra::async_trait::async_trait; +use parking_lot::RwLock; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::fs::{self, File}; +use tokio::io::SeekFrom; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; +use tracing::{instrument, trace}; + +type ProgressCallback = Arc; + +pub async fn retrieve_model( + url: &str, + model_path: &Path, + model_filename: &str, + progress_callback: ProgressCallback, +) -> Result> { + let model_download_path = + download_model(url, model_filename, model_path, None, progress_callback).await?; + Ok(model_download_path) +} + +async fn make_request( + client: &Client, + url: &str, + offset: Option, +) -> Result { + let mut request = client.get(url); + if let Some(offset) = offset { + println!( + "\nDownload interrupted, resuming from byte position {}", + offset + ); + request = request.header("Range", format!("bytes={}-", offset)); + } + let response = request.send().await?; + if !(response.status().is_success() || response.status() == StatusCode::PARTIAL_CONTENT) { + return Err(anyhow!(response.text().await?)); + } + Ok(response) +} + +async fn download_model( + url: &str, + model_filename: &str, + model_path: &Path, + expected_size: Option, + progress_callback: ProgressCallback, +) -> Result { + let client = Client::new(); + // Make the initial request + let mut response = make_request(&client, url, None).await?; + let total_size_in_bytes = response.content_length().unwrap_or(0); + let partial_path = model_path.join(format!("{}.part", model_filename)); + let download_path = model_path.join(model_filename); + let mut part_file = File::create(&partial_path).await?; + let mut downloaded: u64 = 0; + while let Some(chunk) = response.chunk().await? { + part_file.write_all(&chunk).await?; + downloaded += chunk.len() as u64; + progress_callback(downloaded, total_size_in_bytes); + } + + // Verify file integrity + let file_size = part_file.metadata().await?.len(); + trace!("Downloaded file:{}, size: {}", model_filename, file_size); + if let Some(expected_size) = expected_size { + if file_size != expected_size { + return Err(anyhow!( + "Expected file size of {} bytes, got {}", + expected_size, + file_size + )); + } + } + + let header_md5 = response + .headers() + .get("Content-MD5") + .map(|value| value.to_str().ok()) + .flatten() + .map(|value| STANDARD.decode(value).ok()) + .flatten(); + part_file.seek(SeekFrom::Start(0)).await?; + let mut hasher = Sha256::new(); + let block_size = 2_usize.pow(20); // 1 MB + let mut buffer = vec![0; block_size]; + while let Ok(bytes_read) = part_file.read(&mut buffer).await { + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + let calculated_md5 = hasher.finalize(); + if let Some(header_md5) = header_md5 { + if calculated_md5.as_slice() != header_md5.as_slice() { + // remove partial file + fs::remove_file(&partial_path).await?; + + return Err(anyhow!( + "MD5 mismatch: expected {:?}, got {:?}", + header_md5, + calculated_md5 + )); + } + } + + fs::rename(&partial_path, &download_path).await?; + trace!("Model downloaded to {:?}", download_path); + Ok(download_path) +} + +// #[cfg(test)] +// mod test { +// use super::*; +// #[tokio::test] +// async fn retrieve_gpt4all_model_test() { +// let file_name = "all-MiniLM-L6-v2-f16.gguf"; +// let path = Path::new("."); +// retrieve_model( +// "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf", +// &path, +// file_name, +// Arc::new(|a, b| { +// println!("{}/{}", a, b); +// }), +// ) +// .await +// .unwrap(); +// +// let file_path = path.join(file_name); +// assert!(file_path.exists()); +// std::fs::remove_file(file_path).unwrap(); +// } +// +// #[tokio::test] +// async fn retrieve_hugging_face_model_test() { +// let path = Path::new("."); +// let file_name = "all-MiniLM-L6-v2-Q3_K_L.gguf"; +// retrieve_model( +// "https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-Q3_K_L.gguf?download=true", +// &path, +// file_name, +// Arc::new(|a, b| { +// println!("{}/{}", a, b); +// }), +// ) +// .await +// .unwrap(); +// let file_path = path.join(file_name); +// assert!(file_path.exists()); +// std::fs::remove_file(file_path).unwrap(); +// } +// } diff --git a/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs index e3b42a89040c3..8d1d8b912a210 100644 --- a/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs @@ -1,97 +1,41 @@ use crate::chat_manager::ChatUserService; use crate::entities::{ChatStatePB, ModelTypePB}; +use crate::local_ai::local_llm_chat::LocalLLMController; use crate::notification::{send_notification, ChatNotification}; use crate::persistence::select_single_message; -use appflowy_local_ai::llm_chat::{LocalChatLLMChat, LocalLLMSetting}; use appflowy_plugin::error::PluginError; -use appflowy_plugin::util::is_apple_silicon; + use flowy_chat_pub::cloud::{ - ChatCloudService, ChatMessage, ChatMessageType, CompletionType, MessageCursor, + ChatCloudService, ChatMessage, ChatMessageType, CompletionType, LocalAIConfig, MessageCursor, RepeatedChatMessage, RepeatedRelatedQuestion, StreamAnswer, StreamComplete, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{stream, StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use lib_infra::future::FutureResult; -use parking_lot::RwLock; + +use std::path::PathBuf; use std::sync::Arc; -use tracing::{error, info, trace}; -pub struct ChatService { +pub struct ChatServiceMiddleware { pub cloud_service: Arc, user_service: Arc, - local_llm_chat: Arc, - local_llm_setting: Arc>, + local_llm_controller: Arc, } -impl ChatService { +impl ChatServiceMiddleware { pub fn new( user_service: Arc, cloud_service: Arc, - local_llm_ctrl: Arc, - local_llm_setting: LocalLLMSetting, + local_llm_controller: Arc, ) -> Self { - if local_llm_setting.enabled { - setup_local_chat(&local_llm_setting, local_llm_ctrl.clone()); - } - - let mut rx = local_llm_ctrl.subscribe_running_state(); - tokio::spawn(async move { - while let Ok(state) = rx.recv().await { - info!("[Chat Plugin] state: {:?}", state); - } - }); - Self { user_service, cloud_service, - local_llm_chat: local_llm_ctrl, - local_llm_setting: Arc::new(RwLock::new(local_llm_setting)), - } - } - - pub fn notify_open_chat(&self, chat_id: &str) { - if self.local_llm_setting.read().enabled { - trace!("[Chat Plugin] notify open chat: {}", chat_id); - let chat_id = chat_id.to_string(); - let weak_ctrl = Arc::downgrade(&self.local_llm_chat); - tokio::spawn(async move { - if let Some(ctrl) = weak_ctrl.upgrade() { - if let Err(err) = ctrl.create_chat(&chat_id).await { - error!("[Chat Plugin] failed to open chat: {:?}", err); - } - } - }); + local_llm_controller, } } - pub fn notify_close_chat(&self, chat_id: &str) { - if self.local_llm_setting.read().enabled { - trace!("[Chat Plugin] notify close chat: {}", chat_id); - let weak_ctrl = Arc::downgrade(&self.local_llm_chat); - let chat_id = chat_id.to_string(); - tokio::spawn(async move { - if let Some(ctrl) = weak_ctrl.upgrade() { - if let Err(err) = ctrl.close_chat(&chat_id).await { - error!("[Chat Plugin] failed to close chat: {:?}", err); - } - } - }); - } - } - - pub fn get_local_ai_setting(&self) -> LocalLLMSetting { - self.local_llm_setting.read().clone() - } - - pub fn update_local_ai_setting(&self, setting: LocalLLMSetting) -> FlowyResult<()> { - setting.validate()?; - - setup_local_chat(&setting, self.local_llm_chat.clone()); - *self.local_llm_setting.write() = setting; - Ok(()) - } - fn get_message_content(&self, message_id: i64) -> FlowyResult { let uid = self.user_service.user_id()?; let conn = self.user_service.sqlite_connection(uid)?; @@ -120,7 +64,7 @@ impl ChatService { } #[async_trait] -impl ChatCloudService for ChatService { +impl ChatCloudService for ChatServiceMiddleware { fn create_chat( &self, uid: &i64, @@ -160,9 +104,13 @@ impl ChatCloudService for ChatService { chat_id: &str, message_id: i64, ) -> Result { - if self.local_llm_setting.read().enabled { + if self.local_llm_controller.is_ready() { let content = self.get_message_content(message_id)?; - match self.local_llm_chat.stream_question(chat_id, &content).await { + match self + .local_llm_controller + .stream_question(chat_id, &content) + .await + { Ok(stream) => Ok( stream .map_err(|err| FlowyError::local_ai().with_context(err)) @@ -187,9 +135,13 @@ impl ChatCloudService for ChatService { chat_id: &str, question_message_id: i64, ) -> Result { - if self.local_llm_setting.read().enabled { + if self.local_llm_controller.is_ready() { let content = self.get_message_content(question_message_id)?; - match self.local_llm_chat.ask_question(chat_id, &content).await { + match self + .local_llm_controller + .ask_question(chat_id, &content) + .await + { Ok(answer) => { let message = self .cloud_service @@ -228,7 +180,7 @@ impl ChatCloudService for ChatService { chat_id: &str, message_id: i64, ) -> FutureResult { - if self.local_llm_setting.read().enabled { + if self.local_llm_controller.is_ready() { FutureResult::new(async move { Ok(RepeatedRelatedQuestion { message_id, @@ -248,8 +200,10 @@ impl ChatCloudService for ChatService { text: &str, complete_type: CompletionType, ) -> Result { - if self.local_llm_setting.read().enabled { - todo!() + if self.local_llm_controller.is_ready() { + return Err( + FlowyError::not_support().with_context("completion with local ai is not supported yet"), + ); } else { self .cloud_service @@ -257,48 +211,29 @@ impl ChatCloudService for ChatService { .await } } -} - -fn setup_local_chat(local_llm_setting: &LocalLLMSetting, llm_chat_ctrl: Arc) { - if local_llm_setting.enabled { - if let Ok(mut config) = local_llm_setting.chat_config() { - tokio::spawn(async move { - trace!("[Chat Plugin] setup local chat: {:?}", config); - if is_apple_silicon().await.unwrap_or(false) { - config = config.with_device("gpu"); - } - - if cfg!(debug_assertions) { - config = config.with_verbose(true); - } - match llm_chat_ctrl.init_chat_plugin(config).await { - Ok(_) => { - send_notification("appflowy_chat_plugin", ChatNotification::ChatStateUpdated).payload( - ChatStatePB { - model_type: ModelTypePB::LocalAI, - available: true, - }, - ); - }, - Err(err) => { - send_notification("appflowy_chat_plugin", ChatNotification::ChatStateUpdated).payload( - ChatStatePB { - model_type: ModelTypePB::LocalAI, - available: false, - }, - ); - error!("[Chat Plugin] failed to setup plugin: {:?}", err); - }, - } - }); + async fn index_file( + &self, + workspace_id: &str, + file_path: PathBuf, + chat_id: &str, + ) -> Result<(), FlowyError> { + if self.local_llm_controller.is_ready() { + self + .local_llm_controller + .index_file(chat_id, file_path) + .await + .map_err(|err| FlowyError::local_ai().with_context(err))?; + Ok(()) + } else { + self + .cloud_service + .index_file(workspace_id, file_path, chat_id) + .await } - } else { - tokio::spawn(async move { - match llm_chat_ctrl.destroy_chat_plugin().await { - Ok(_) => info!("[Chat Plugin] destroy plugin successfully"), - Err(err) => error!("[Chat Plugin] failed to destroy plugin: {:?}", err), - } - }); + } + + async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + self.cloud_service.get_local_ai_config(workspace_id).await } } diff --git a/frontend/rust-lib/flowy-chat/src/notification.rs b/frontend/rust-lib/flowy-chat/src/notification.rs index 10101027aefce..c781040642377 100644 --- a/frontend/rust-lib/flowy-chat/src/notification.rs +++ b/frontend/rust-lib/flowy-chat/src/notification.rs @@ -13,6 +13,7 @@ pub enum ChatNotification { StreamChatMessageError = 4, FinishStreaming = 5, ChatStateUpdated = 6, + LocalAIResourceNeeded = 7, } impl std::convert::From for i32 { @@ -29,6 +30,7 @@ impl std::convert::From for ChatNotification { 4 => ChatNotification::StreamChatMessageError, 5 => ChatNotification::FinishStreaming, 6 => ChatNotification::ChatStateUpdated, + 7 => ChatNotification::LocalAIResourceNeeded, _ => ChatNotification::Unknown, } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index 32a31fb274299..be503e4afdf5d 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -4,6 +4,7 @@ use flowy_error::FlowyError; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; use flowy_user::services::authenticate_user::AuthenticateUser; +use std::path::PathBuf; use std::sync::{Arc, Weak}; pub struct ChatDepsResolver; @@ -50,4 +51,8 @@ impl ChatUserService for ChatUserServiceImpl { fn sqlite_connection(&self, uid: i64) -> Result { self.upgrade_user()?.get_sqlite_connection(uid) } + + fn user_data_dir(&self) -> Result { + self.upgrade_user()?.get_user_data_dir() + } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index b5decb151b7c4..691e44a4a9024 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -1,5 +1,6 @@ use client_api::entity::search_dto::SearchDocumentResponseItem; use flowy_search_pub::cloud::SearchCloudService; +use std::path::PathBuf; use std::sync::Arc; use anyhow::Error; @@ -18,7 +19,8 @@ use collab_integrate::collab_builder::{ CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, }; use flowy_chat_pub::cloud::{ - ChatCloudService, ChatMessage, MessageCursor, RepeatedChatMessage, StreamAnswer, StreamComplete, + ChatCloudService, ChatMessage, LocalAIConfig, MessageCursor, RepeatedChatMessage, StreamAnswer, + StreamComplete, }; use flowy_database_pub::cloud::{ CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, @@ -667,6 +669,27 @@ impl ChatCloudService for ServerProvider { .stream_complete(&workspace_id, &text, complete_type) .await } + + async fn index_file( + &self, + workspace_id: &str, + file_path: PathBuf, + chat_id: &str, + ) -> Result<(), FlowyError> { + self + .get_server()? + .chat_service() + .index_file(workspace_id, file_path, chat_id) + .await + } + + async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + self + .get_server()? + .chat_service() + .get_local_ai_config(workspace_id) + .await + } } #[async_trait] diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 624f4234932bd..ee82157d58a41 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -86,11 +86,13 @@ impl AppFlowyCore { init_log(&config, &platform, stream_log_sender); } - info!( - "💡{:?}, platform: {:?}", - System::long_os_version(), - platform - ); + if sysinfo::IS_SUPPORTED_SYSTEM { + info!( + "💡{:?}, platform: {:?}", + System::long_os_version(), + platform + ); + } Self::init(config, runtime).await } diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index e7c6f5a9a692c..349c07fa59ed3 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -1,5 +1,5 @@ use std::convert::TryInto; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use protobuf::ProtobufError; use thiserror::Error; @@ -42,8 +42,8 @@ impl FlowyError { payload: vec![], } } - pub fn with_context(mut self, error: T) -> Self { - self.msg = format!("{:?}", error); + pub fn with_context(mut self, error: T) -> Self { + self.msg = format!("{}", error.to_string()); self } @@ -137,7 +137,7 @@ pub fn internal_error(e: T) -> FlowyError where T: std::fmt::Debug, { - FlowyError::internal().with_context(e) + FlowyError::internal().with_context(format!("{:?}", e)) } impl std::convert::From for FlowyError { 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 d07f53a4a8474..e69201a390271 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 @@ -5,12 +5,14 @@ use client_api::entity::{ RepeatedChatMessage, }; use flowy_chat_pub::cloud::{ - ChatCloudService, ChatMessage, ChatMessageType, StreamAnswer, StreamComplete, + ChatCloudService, ChatMessage, ChatMessageType, LocalAIConfig, StreamAnswer, StreamComplete, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use lib_infra::future::FutureResult; +use lib_infra::util::{get_operating_system, OperatingSystem}; +use std::path::PathBuf; pub(crate) struct AFCloudChatCloudServiceImpl { pub inner: T, @@ -182,4 +184,35 @@ where .map_err(FlowyError::from)?; Ok(stream.boxed()) } + + async fn index_file( + &self, + _workspace_id: &str, + _file_path: PathBuf, + _chat_id: &str, + ) -> Result<(), FlowyError> { + return Err( + FlowyError::not_support() + .with_context("indexing file with appflowy cloud is not suppotred yet"), + ); + } + + async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + let system = get_operating_system(); + let platform = match system { + OperatingSystem::MacOS => "macos", + _ => { + return Err( + FlowyError::not_support() + .with_context("local ai is not supported on this operating system"), + ); + }, + }; + let config = self + .inner + .try_get_client()? + .get_local_ai_config(workspace_id, platform) + .await?; + Ok(config) + } } diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index d40100cc15ead..955cf653dadf4 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -1,9 +1,10 @@ -use client_api::entity::ai_dto::{CompletionType, RepeatedRelatedQuestion}; +use client_api::entity::ai_dto::{CompletionType, LocalAIConfig, RepeatedRelatedQuestion}; use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage}; use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, StreamAnswer, StreamComplete}; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; use lib_infra::future::FutureResult; +use std::path::PathBuf; pub(crate) struct DefaultChatCloudServiceImpl; @@ -93,4 +94,20 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { ) -> Result { Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) } + + async fn index_file( + &self, + _workspace_id: &str, + _file_path: PathBuf, + _chat_id: &str, + ) -> Result<(), FlowyError> { + Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) + } + + async fn get_local_ai_config(&self, _workspace_id: &str) -> Result { + Err( + FlowyError::not_support() + .with_context("Get local ai config is not supported in local server."), + ) + } } diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 420ffdeda6bc8..0065bddd14330 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -88,6 +88,11 @@ impl AuthenticateUser { PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes") } + pub fn get_user_data_dir(&self) -> FlowyResult { + let uid = self.user_id()?; + Ok(PathBuf::from(self.user_paths.user_data_dir(uid))) + } + pub fn get_application_root_dir(&self) -> &str { self.user_paths.root() } From df154a02f4baabe6b0122ca9c24d964d47dc4d76 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 12 Jul 2024 20:58:45 +0800 Subject: [PATCH 02/13] chore: config download ui --- .../lib/plugins/ai_chat/chat_page.dart | 2 - frontend/appflowy_tauri/src-tauri/Cargo.lock | 38 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 2 +- .../appflowy_web_app/src-tauri/Cargo.lock | 38 +- .../appflowy_web_app/src-tauri/Cargo.toml | 2 +- frontend/resources/translations/en.json | 2 +- frontend/rust-lib/Cargo.lock | 32 +- frontend/rust-lib/Cargo.toml | 5 +- frontend/rust-lib/flowy-chat-pub/src/cloud.rs | 2 +- frontend/rust-lib/flowy-chat/Cargo.toml | 3 + .../rust-lib/flowy-chat/src/chat_manager.rs | 6 - frontend/rust-lib/flowy-chat/src/entities.rs | 27 ++ .../rust-lib/flowy-chat/src/event_handler.rs | 19 +- frontend/rust-lib/flowy-chat/src/event_map.rs | 12 +- .../flowy-chat/src/local_ai/llm_resource.rs | 454 ++++++++++++++---- .../flowy-chat/src/local_ai/local_llm_chat.rs | 21 +- .../rust-lib/flowy-chat/src/local_ai/mod.rs | 3 +- .../flowy-chat/src/local_ai/model_request.rs | 151 ++++++ .../flowy-chat/src/local_ai/plugin_request.rs | 102 ++++ .../flowy-chat/src/local_ai/request.rs | 165 ------- .../rust-lib/lib-infra/src/isolate_stream.rs | 5 +- 21 files changed, 731 insertions(+), 360 deletions(-) create mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs create mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs delete mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/request.rs 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 2c91e7fc11e9c..9c6a0eda0e9e3 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,6 +1,4 @@ import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; -import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 2543e2572d032..603714edbc7c2 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bincode", @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bytes", @@ -806,7 +806,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "again", "anyhow", @@ -855,7 +855,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "collab-entity", "collab-rt-entity", @@ -867,7 +867,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "futures-channel", "futures-util", @@ -1107,7 +1107,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bincode", @@ -1132,7 +1132,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "async-trait", @@ -1375,7 +1375,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1486,7 +1486,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -1893,9 +1893,11 @@ dependencies = [ "flowy-notification", "flowy-sqlite", "futures", + "futures-util", "lib-dispatch", "lib-infra", "log", + "md5", "parking_lot 0.12.1", "protobuf", "reqwest", @@ -1905,6 +1907,7 @@ dependencies = [ "strum_macros 0.21.1", "tokio", "tokio-stream", + "tokio-util", "tracing", "uuid", "validator", @@ -2940,7 +2943,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "futures-util", @@ -2957,7 +2960,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -3389,7 +3392,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bytes", @@ -4897,7 +4900,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -4918,7 +4921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.47", @@ -5882,7 +5885,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -6915,16 +6918,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index ca5d5d481a2b4..c9cd46684f2d2 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "bb84d4ad059a723de031e1f286606bd1bfcb1806" } [dependencies] serde_json.workspace = true diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index f4332a74efb1d..6e8ca813611d6 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bytes", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "again", "anyhow", @@ -829,7 +829,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "collab-entity", "collab-rt-entity", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "futures-channel", "futures-util", @@ -1090,7 +1090,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bincode", @@ -1115,7 +1115,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "async-trait", @@ -1365,7 +1365,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.10", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1476,7 +1476,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -1933,9 +1933,11 @@ dependencies = [ "flowy-notification", "flowy-sqlite", "futures", + "futures-util", "lib-dispatch", "lib-infra", "log", + "md5", "parking_lot 0.12.1", "protobuf", "reqwest", @@ -1945,6 +1947,7 @@ dependencies = [ "strum_macros 0.21.1", "tokio", "tokio-stream", + "tokio-util", "tracing", "uuid", "validator", @@ -3017,7 +3020,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "futures-util", @@ -3034,7 +3037,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -3471,7 +3474,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bytes", @@ -4981,7 +4984,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -5002,7 +5005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.55", @@ -5980,7 +5983,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -7047,16 +7050,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 2d90a5e865382..1ef5f6d6b7919 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "bb84d4ad059a723de031e1f286606bd1bfcb1806" } [dependencies] serde_json.workspace = true diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e551a4d80932e..5c5fbd30a5140 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -620,7 +620,7 @@ "downloadLLMPromptDetail": "Downloading {} local model will take up to {} of storage. Do you want to continue?", "downloadAIModelButton": "Download AI model", "downloadingModel": "Downloading", - "downloadingModelPrompt": "Do not navigate away or close the app while file is downloading", + "downloadModelSuccess": "Local AI Model successfully added and ready to use", "title": "AI API Keys", "openAILabel": "OpenAI API key", "openAITooltip": "You can find your Secret API key on the API key page", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 447b03f80f077..58fa105599a33 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bytes", @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "again", "anyhow", @@ -747,7 +747,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "collab-entity", "collab-rt-entity", @@ -759,7 +759,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "futures-channel", "futures-util", @@ -968,7 +968,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bincode", @@ -993,7 +993,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "async-trait", @@ -1310,7 +1310,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -1714,9 +1714,11 @@ dependencies = [ "flowy-notification", "flowy-sqlite", "futures", + "futures-util", "lib-dispatch", "lib-infra", "log", + "md5", "parking_lot 0.12.1", "protobuf", "reqwest", @@ -1727,6 +1729,7 @@ dependencies = [ "strum_macros 0.21.1", "tokio", "tokio-stream", + "tokio-util", "tracing", "tracing-subscriber", "uuid", @@ -2616,7 +2619,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "futures-util", @@ -2633,7 +2636,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -2998,7 +3001,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "bytes", @@ -5085,7 +5088,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836#2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=bb84d4ad059a723de031e1f286606bd1bfcb1806#bb84d4ad059a723de031e1f286606bd1bfcb1806" dependencies = [ "anyhow", "app-error", @@ -5818,16 +5821,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 3a3a4fb63ebc4..b5c6a3461a98c 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -91,14 +91,15 @@ collab-plugins = { version = "0.2" } collab-user = { version = "0.2" } yrs = "0.18.8" validator = { version = "0.16.1", features = ["derive"] } +tokio-util = "0.7.11" # Please using the following command to update the revision id # Current directory: frontend # 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 = "2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2a4fb4cf18c3f0b46338cf45f27c4023cfd3a836" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "bb84d4ad059a723de031e1f286606bd1bfcb1806" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "bb84d4ad059a723de031e1f286606bd1bfcb1806" } [profile.dev] opt-level = 1 diff --git a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs index 8a5a424951a2a..0054891140877 100644 --- a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs @@ -1,6 +1,6 @@ use bytes::Bytes; pub use client_api::entity::ai_dto::{ - AppFlowyAIPlugin, CompletionType, LLMModel, LocalAIConfig, RelatedQuestion, + AppFlowyAIPlugin, CompletionType, LLMModel, LocalAIConfig, ModelInfo, RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage, }; pub use client_api::entity::{ diff --git a/frontend/rust-lib/flowy-chat/Cargo.toml b/frontend/rust-lib/flowy-chat/Cargo.toml index 9cd9a28031368..5b0d8ae8513ee 100644 --- a/frontend/rust-lib/flowy-chat/Cargo.toml +++ b/frontend/rust-lib/flowy-chat/Cargo.toml @@ -32,12 +32,15 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } anyhow = "1.0.86" tokio-stream = "0.1.15" +tokio-util.workspace = true parking_lot.workspace = true appflowy-local-ai = { version = "0.1.0", features = ["verbose"] } appflowy-plugin = { version = "0.1.0", features = ["verbose"] } reqwest = "0.11.27" sha2 = "0.10.7" base64 = "0.21.5" +futures-util = "0.3.30" +md5 = "0.7.0" [dev-dependencies] dotenv = "0.15.0" diff --git a/frontend/rust-lib/flowy-chat/src/chat_manager.rs b/frontend/rust-lib/flowy-chat/src/chat_manager.rs index 0e3fc0d27f3dc..4c422e7b9cf88 100644 --- a/frontend/rust-lib/flowy-chat/src/chat_manager.rs +++ b/frontend/rust-lib/flowy-chat/src/chat_manager.rs @@ -230,12 +230,6 @@ impl ChatManager { chat.index_file(file_path).await?; Ok(()) } - - pub async fn download_ai_resources(&self, progress_port: i64) -> FlowyResult<()> { - let text_sink = IsolateSink::new(Isolate::new(progress_port)); - self.llm_controller.start_downloading(text_sink)?; - Ok(()) - } } fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-chat/src/entities.rs b/frontend/rust-lib/flowy-chat/src/entities.rs index 564042156890c..3d3c41a148baf 100644 --- a/frontend/rust-lib/flowy-chat/src/entities.rs +++ b/frontend/rust-lib/flowy-chat/src/entities.rs @@ -336,4 +336,31 @@ pub struct LocalModelStatePB { #[pb(index = 4)] pub requirements: String, + + #[pb(index = 5)] + pub is_downloading: bool, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct LocalModelResourcePB { + #[pb(index = 1)] + pub is_ready: bool, + + #[pb(index = 2)] + pub pending_resources: Vec, + + #[pb(index = 3)] + pub is_downloading: bool, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct PendingResourcePB { + #[pb(index = 1)] + pub model_name: String, + + #[pb(index = 2)] + pub model_size: i64, + + #[pb(index = 3)] + pub requirements: String, } diff --git a/frontend/rust-lib/flowy-chat/src/event_handler.rs b/frontend/rust-lib/flowy-chat/src/event_handler.rs index 96cc0943281c3..8b3dbbed547ca 100644 --- a/frontend/rust-lib/flowy-chat/src/event_handler.rs +++ b/frontend/rust-lib/flowy-chat/src/event_handler.rs @@ -124,13 +124,13 @@ pub(crate) async fn stop_stream_handler( } #[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_local_ai_model_info_handler( +pub(crate) async fn refresh_local_ai_info_handler( chat_manager: AFPluginState>, ) -> DataResult { let chat_manager = upgrade_chat_manager(chat_manager)?; let (tx, rx) = oneshot::channel::>(); tokio::spawn(async move { - let model_info = chat_manager.llm_controller.model_info().await; + let model_info = chat_manager.llm_controller.refresh().await; let _ = tx.send(model_info); }); @@ -142,7 +142,7 @@ pub(crate) async fn get_local_ai_model_info_handler( pub(crate) async fn update_local_llm_model_handler( data: AFPluginData, chat_manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let data = data.into_inner(); let chat_manager = upgrade_chat_manager(chat_manager)?; let state = chat_manager @@ -155,7 +155,7 @@ pub(crate) async fn update_local_llm_model_handler( #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_local_llm_state_handler( chat_manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let chat_manager = upgrade_chat_manager(chat_manager)?; let state = chat_manager.llm_controller.get_local_llm_state().await?; data_result_ok(state) @@ -204,20 +204,21 @@ pub(crate) async fn download_llm_resource_handler( data: AFPluginData, chat_manager: AFPluginState>, ) -> DataResult { - let data = data.try_into_inner()?; + let data = data.into_inner(); let chat_manager = upgrade_chat_manager(chat_manager)?; let text_sink = IsolateSink::new(Isolate::new(data.progress_stream)); - let task_id = chat_manager.llm_controller.start_downloading(text_sink)?; + let task_id = chat_manager + .llm_controller + .start_downloading(text_sink) + .await?; data_result_ok(DownloadTaskPB { task_id }) } #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn cancel_download_llm_resource_handler( - data: AFPluginData, chat_manager: AFPluginState>, ) -> Result<(), FlowyError> { - let data = data.into_inner(); let chat_manager = upgrade_chat_manager(chat_manager)?; - chat_manager.llm_controller.cancel_download(&data.task_id)?; + chat_manager.llm_controller.cancel_download()?; Ok(()) } diff --git a/frontend/rust-lib/flowy-chat/src/event_map.rs b/frontend/rust-lib/flowy-chat/src/event_map.rs index 69f3d49ee16c8..98ee1831f0bca 100644 --- a/frontend/rust-lib/flowy-chat/src/event_map.rs +++ b/frontend/rust-lib/flowy-chat/src/event_map.rs @@ -24,8 +24,8 @@ pub fn init(chat_manager: Weak) -> AFPlugin { .event(ChatEvent::GetAnswerForQuestion, get_answer_handler) .event(ChatEvent::StopStream, stop_stream_handler) .event( - ChatEvent::GetLocalAIModelInfo, - get_local_ai_model_info_handler, + ChatEvent::RefreshLocalAIModelInfo, + refresh_local_ai_info_handler, ) .event(ChatEvent::UpdateLocalLLM, update_local_llm_model_handler) .event(ChatEvent::GetLocalLLMState, get_local_llm_state_handler) @@ -64,14 +64,14 @@ pub enum ChatEvent { #[event(input = "ChatMessageIdPB", output = "ChatMessagePB")] GetAnswerForQuestion = 5, - #[event(input = "LLMModelPB", output = "LocalModelStatePB")] + #[event(input = "LLMModelPB", output = "LocalModelResourcePB")] UpdateLocalLLM = 6, - #[event(output = "LocalModelStatePB")] + #[event(output = "LocalModelResourcePB")] GetLocalLLMState = 7, #[event(output = "LLMModelInfoPB")] - GetLocalAIModelInfo = 8, + RefreshLocalAIModelInfo = 8, #[event(input = "CompleteTextPB", output = "CompleteTextTaskPB")] CompleteText = 9, @@ -85,6 +85,6 @@ pub enum ChatEvent { #[event(input = "DownloadLLMPB", output = "DownloadTaskPB")] DownloadLLMResource = 12, - #[event(input = "DownloadTaskPB")] + #[event()] CancelDownloadLLMResource = 13, } diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs index 1fbbcdbc6b317..169288332c7c7 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs @@ -2,23 +2,29 @@ use reqwest::{Client, Response, StatusCode}; use sha2::{Digest, Sha256}; use crate::chat_manager::ChatUserService; -use crate::entities::LocalModelStatePB; +use crate::entities::{LocalModelResourcePB, LocalModelStatePB, PendingResourcePB}; use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting}; +use crate::local_ai::model_request::download_model; +use crate::local_ai::plugin_request::download_plugin; use crate::notification::{send_notification, ChatNotification}; use anyhow::anyhow; use appflowy_local_ai::llm_chat::ChatPluginConfig; use base64::engine::general_purpose::STANDARD; use base64::Engine; -use flowy_chat_pub::cloud::{LLMModel, LocalAIConfig}; +use flowy_chat_pub::cloud::{LLMModel, LocalAIConfig, ModelInfo}; use flowy_error::{FlowyError, FlowyResult}; +use futures::Sink; +use futures_util::SinkExt; use lib_infra::async_trait::async_trait; +use lib_infra::file_util::unzip_and_replace; use parking_lot::RwLock; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::fs::{self, File}; use tokio::io::SeekFrom; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; -use tracing::{debug, info, instrument, trace}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, instrument, trace}; #[async_trait] pub trait LLMResourceService: Send + Sync + 'static { @@ -27,6 +33,31 @@ pub trait LLMResourceService: Send + Sync + 'static { fn retrieve(&self) -> Option; } +const PLUGIN_DIR: &str = "plugin"; +const LLM_MODEL_DIR: &str = "models"; +const DOWNLOAD_FINISH: &str = "finish"; + +pub enum PendingResource { + PluginRes, + ModelInfoRes(Vec), +} +#[derive(Clone)] +pub struct DownloadTask { + cancel_token: CancellationToken, + tx: tokio::sync::broadcast::Sender, +} +impl DownloadTask { + pub fn new() -> Self { + let (tx, _) = tokio::sync::broadcast::channel(5); + let cancel_token = CancellationToken::new(); + Self { cancel_token, tx } + } + + pub fn cancel(&self) { + self.cancel_token.cancel(); + } +} + pub struct LLMResourceController { client: Client, user_service: Arc, @@ -34,6 +65,7 @@ pub struct LLMResourceController { llm_setting: RwLock>, // The ai_config will be set when user try to get latest local ai config from server ai_config: RwLock>, + download_task: Arc>>, } impl LLMResourceController { @@ -48,38 +80,44 @@ impl LLMResourceController { resource_service: Arc::new(resource_service), llm_setting, ai_config: Default::default(), + download_task: Default::default(), } } /// Returns true when all resources are downloaded and ready to use. pub fn is_ready(&self) -> bool { - self.is_resource_ready().unwrap_or(false) + match self.calculate_pending_resources() { + Ok(res) => res.is_empty(), + Err(_) => false, + } } + /// Retrieves model information and updates the current model settings. #[instrument(level = "debug", skip_all, err)] - pub async fn model_info(&self) -> FlowyResult { - let ai_config = self - .resource_service - .get_local_ai_config() - .await - .map_err(|err| { - FlowyError::local_ai().with_context(format!("Can't retrieve model info:{}", err)) - })?; - + pub async fn refresh_llm_resource(&self) -> FlowyResult { + let ai_config = self.fetch_ai_config().await?; if ai_config.models.is_empty() { return Err(FlowyError::local_ai().with_context("No model found")); } *self.ai_config.write() = Some(ai_config.clone()); - let selected_config = ai_config.models[0].clone(); + let selected_model = self.select_model(&ai_config)?; + + let llm_setting = LLMSetting { + plugin: ai_config.plugin.clone(), + llm_model: selected_model.clone(), + }; + self.llm_setting.write().replace(llm_setting.clone()); + self.resource_service.store(llm_setting)?; + Ok(LLMModelInfo { - selected_model: selected_config, + selected_model, models: ai_config.models, }) } #[instrument(level = "info", skip_all, err)] - pub fn use_local_llm(&self, llm_id: i64) -> FlowyResult { + pub fn use_local_llm(&self, llm_id: i64) -> FlowyResult { let (package, llm_config) = self .ai_config .read() @@ -99,13 +137,13 @@ impl LLMResourceController { llm_model: llm_config.clone(), }; - trace!("Selected local ai setting: {:?}", llm_setting); + trace!("[LLM Resource] Selected AI setting: {:?}", llm_setting); *self.llm_setting.write() = Some(llm_setting.clone()); self.resource_service.store(llm_setting)?; self.get_local_llm_state() } - pub fn get_local_llm_state(&self) -> FlowyResult { + pub fn get_local_llm_state(&self) -> FlowyResult { let state = self .check_resource() .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; @@ -113,124 +151,334 @@ impl LLMResourceController { } #[instrument(level = "debug", skip_all)] - fn check_resource(&self) -> Option { - trace!("Checking local ai resources"); - let llm_model = self - .llm_setting - .read() - .as_ref() - .map(|setting| setting.llm_model.clone())?; - let need_download = !self.is_resource_ready().ok()?; - let payload = LocalModelStatePB { - model_name: llm_model.chat_model.name, - model_size: bytes_to_readable_size(llm_model.chat_model.file_size as u64), - need_download, - requirements: llm_model.chat_model.requirements, + fn check_resource(&self) -> Option { + trace!("[LLM Resource] Checking local ai resources"); + + let pending_resources = self.calculate_pending_resources().ok()?; + let is_ready = pending_resources.is_empty(); + let is_downloading = self.download_task.read().is_some(); + let pending_resources: Vec<_> = pending_resources + .into_iter() + .filter_map(|res| match res { + PendingResource::PluginRes => None, + PendingResource::ModelInfoRes(model_infos) => Some( + model_infos + .into_iter() + .map(|model_info| PendingResourcePB { + model_name: model_info.name, + model_size: model_info.file_size, + requirements: model_info.requirements, + }) + .collect::>(), + ), + }) + .flatten() + .collect(); + + let resource = LocalModelResourcePB { + is_ready, + pending_resources, + is_downloading, }; - if need_download { - info!("Local AI resources are not ready, notify client to download ai resources"); - // notify client it needs to download ai resource - send_notification("local_ai_resource", ChatNotification::LocalAIResourceNeeded) - .payload(payload.clone()) - .send(); - } - debug!("Local AI resources state: {:?}", payload); - Some(payload) + debug!("[LLM Resource] Local AI resources state: {:?}", resource); + Some(resource) } /// Returns true when all resources are downloaded and ready to use. - pub fn is_resource_ready(&self) -> FlowyResult { + pub fn calculate_pending_resources(&self) -> FlowyResult> { match self.llm_setting.read().as_ref() { None => Err(FlowyError::local_ai().with_context("Can't find any llm config")), Some(llm_setting) => { - let llm_dir = self.user_service.user_data_dir()?; + let mut resources = vec![]; + let plugin_path = self.plugin_path(&llm_setting.plugin.etag)?; - let plugin_needed = should_download_plugin(&llm_dir, llm_setting); - if plugin_needed { - return Ok(false); + if !plugin_path.exists() { + trace!("[LLM Resource] Plugin file not found: {:?}", plugin_path); + resources.push(PendingResource::PluginRes); } - let model_needed = should_download_model(&llm_dir, llm_setting); - if model_needed { - return Ok(false); + let chat_model = self.model_path(&llm_setting.llm_model.chat_model.file_name)?; + if !chat_model.exists() { + resources.push(PendingResource::ModelInfoRes(vec![llm_setting + .llm_model + .chat_model + .clone()])); } - Ok(false) + let embedding_model = self.model_path(&llm_setting.llm_model.embedding_model.file_name)?; + if !embedding_model.exists() { + resources.push(PendingResource::ModelInfoRes(vec![llm_setting + .llm_model + .embedding_model + .clone()])); + } + + Ok(resources) }, } } - pub fn start_downloading(&self) -> FlowyResult { - info!("Start downloading local ai resources"); - // - Ok("".to_string()) + #[instrument(level = "info", skip_all, err)] + pub async fn start_downloading(&self, mut progress_sink: T) -> FlowyResult + where + T: Sink + Unpin + Sync + Send + 'static, + { + let task_id = uuid::Uuid::new_v4().to_string(); + let weak_download_task = Arc::downgrade(&self.download_task); + + // notify download progress to client. + let progress_notify = |mut rx: tokio::sync::broadcast::Receiver| { + tokio::spawn(async move { + while let Ok(value) = rx.recv().await { + if value == DOWNLOAD_FINISH { + if let Some(download_task) = weak_download_task.upgrade() { + if let Some(task) = download_task.write().take() { + task.cancel(); + } + } + break; + } + + if let Err(err) = progress_sink.send(value).await { + error!("Failed to send progress: {:?}", err); + break; + } + } + }); + }; + + // return immediately if download task already exists + if let Some(download_task) = self.download_task.read().as_ref() { + trace!( + "Download task already exists, return the task id: {}", + task_id + ); + progress_notify(download_task.tx.subscribe()); + return Ok(task_id); + } + + // If download task is not exists, create a new download task. + info!("[LLM Resource] Start new download task"); + let llm_setting = self + .llm_setting + .read() + .clone() + .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; + + let download_task = DownloadTask::new(); + *self.download_task.write() = Some(download_task.clone()); + progress_notify(download_task.tx.subscribe()); + + let plugin_dir = self.user_plugin_folder()?; + if !plugin_dir.exists() { + fs::create_dir_all(&plugin_dir).await.map_err(|err| { + FlowyError::local_ai().with_context(format!("Failed to create plugin dir: {:?}", err)) + })?; + } + + let model_dir = self.user_model_folder()?; + if !model_dir.exists() { + fs::create_dir_all(&model_dir).await.map_err(|err| { + FlowyError::local_ai().with_context(format!("Failed to create model dir: {:?}", err)) + })?; + } + + tokio::spawn(async move { + let plugin_file_etag_dir = plugin_dir.join(&llm_setting.plugin.etag); + // We use the ETag as the identifier for the plugin file. If a file with the given ETag + // already exists, skip downloading it. + if !plugin_file_etag_dir.exists() { + let plugin_progress_tx = download_task.tx.clone(); + info!( + "[LLM Resource] Downloading plugin: {:?}", + llm_setting.plugin.etag + ); + let file_name = format!("{}.zip", llm_setting.plugin.etag); + let zip_plugin_file = download_plugin( + &llm_setting.plugin.url, + &plugin_dir, + &file_name, + Some(download_task.cancel_token.clone()), + Some(Arc::new(move |downloaded, total_size| { + let progress = (downloaded as f64 / total_size as f64).clamp(0.0, 1.0); + trace!("Plugin download progress: {}", progress); + let _ = plugin_progress_tx.send(format!("plugin:progress:{}", progress)); + })), + ) + .await?; + + // unzip file + info!( + "[LLM Resource] unzip {:?} to {:?}", + zip_plugin_file, plugin_file_etag_dir + ); + unzip_and_replace(&zip_plugin_file, &plugin_file_etag_dir)?; + + // delete zip file + info!("[LLM Resource] Delete zip file: {:?}", file_name); + if let Err(err) = fs::remove_file(&zip_plugin_file).await { + error!("Failed to delete zip file: {:?}", err); + } + } + + // After download the plugin, start downloading models + let chat_model_file = ( + model_dir.join(&llm_setting.llm_model.chat_model.file_name), + llm_setting.llm_model.chat_model.file_name, + llm_setting.llm_model.chat_model.name, + llm_setting.llm_model.chat_model.download_url, + ); + let embedding_model_file = ( + model_dir.join(&llm_setting.llm_model.embedding_model.file_name), + llm_setting.llm_model.embedding_model.file_name, + llm_setting.llm_model.embedding_model.name, + llm_setting.llm_model.embedding_model.download_url, + ); + for (file_path, file_name, model_name, url) in [chat_model_file, embedding_model_file] { + if file_path.exists() { + continue; + } + + info!("[LLM Resource] Downloading model: {:?}", file_name); + let plugin_progress_tx = download_task.tx.clone(); + let cloned_model_name = model_name.clone(); + match download_model( + &url, + &model_dir, + &file_name, + Some(Arc::new(move |downloaded, total_size| { + let progress = (downloaded as f64 / total_size as f64).clamp(0.0, 1.0); + let _ = plugin_progress_tx.send(format!("{}:progress:{}", cloned_model_name, progress)); + })), + Some(download_task.cancel_token.clone()), + ) + .await + { + Ok(_) => info!("[LLM Resource] Downloaded model: {:?}", file_name), + Err(err) => { + error!( + "[LLM Resource] Failed to download model for given url: {:?}, error: {:?}", + url, err + ); + download_task + .tx + .send(format!("error:failed to download {}", model_name))?; + continue; + }, + } + } + download_task.tx.send(DOWNLOAD_FINISH.to_string())?; + Ok::<_, anyhow::Error>(()) + }); + + Ok(task_id) } - pub fn cancel_download(&self, task_id: &str) -> FlowyResult<()> { + pub fn cancel_download(&self) -> FlowyResult<()> { + if let Some(cancel_token) = self.download_task.write().take() { + info!("[LLM Resource] Cancel download"); + cancel_token.cancel(); + } + Ok(()) } pub fn get_chat_config(&self) -> FlowyResult { - if !self.is_resource_ready()? { - let _ = self.check_resource(); + if !self.is_ready() { return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); } - // let mut config = ChatPluginConfig::new( - // setting.chat_bin_path.clone(), - // setting.chat_model_path.clone(), - // )?; - // - // let persist_directory = user_data_dir.join("chat_plugin_rag"); - // if !persist_directory.exists() { - // std::fs::create_dir_all(&persist_directory)?; - // } - // - // // Enable RAG when the embedding model path is set - // if let Err(err) = config.set_rag_enabled( - // &PathBuf::from(&setting.embedding_model_path), - // &persist_directory, - // ) { - // error!( - // "[Chat Plugin] failed to enable rag: {:?}, embedding_model_path: {:?}", - // err, setting.embedding_model_path - // ); - // } - // - // if cfg!(debug_assertions) { - // config = config.with_verbose(true); - // } - // Ok(config) - todo!() - } - - fn llm_dir(&self) -> FlowyResult { - let user_data_dir = self.user_service.user_data_dir()?; - Ok(user_data_dir.join("llm")) + let llm_setting = self + .llm_setting + .read() + .as_ref() + .cloned() + .ok_or_else(|| FlowyError::local_ai().with_context("No local llm setting found"))?; + + let model_dir = self.user_model_folder()?; + let resource_dir = self.resource_dir()?; + + let chat_bin_path = self + .plugin_path(&llm_setting.plugin.etag)? + .join("chat_plugin"); + let chat_model_path = model_dir.join(&llm_setting.llm_model.chat_model.file_name); + let embedding_model_path = model_dir.join(&llm_setting.llm_model.embedding_model.file_name); + + let mut config = ChatPluginConfig::new(chat_bin_path, chat_model_path)?; + + let persist_directory = resource_dir.join("rag"); + if !persist_directory.exists() { + std::fs::create_dir_all(&persist_directory)?; + } + + // Enable RAG when the embedding model path is set + config.set_rag_enabled(&embedding_model_path, &persist_directory)?; + + if cfg!(debug_assertions) { + config = config.with_verbose(true); + } + Ok(config) } -} -pub fn should_download_plugin(llm_dir: &PathBuf, llm_setting: &LLMSetting) -> bool { - let plugin_path = llm_dir.join(format!( - "{}-{}", - llm_setting.plugin.version, llm_setting.plugin.name - )); - !plugin_path.exists() -} + /// Fetches the local AI configuration from the resource service. + async fn fetch_ai_config(&self) -> FlowyResult { + self + .resource_service + .get_local_ai_config() + .await + .map_err(|err| { + FlowyError::local_ai().with_context(format!("Can't retrieve model info: {}", err)) + }) + } -pub fn should_download_model(llm_dir: &PathBuf, llm_setting: &LLMSetting) -> bool { - let chat_model = llm_dir.join(&llm_setting.llm_model.chat_model.file_name); - if !chat_model.exists() { - return true; + /// Selects the appropriate model based on the current settings or defaults to the first model. + fn select_model(&self, ai_config: &LocalAIConfig) -> FlowyResult { + let selected_model = match self.llm_setting.read().as_ref() { + None => ai_config.models[0].clone(), + Some(llm_setting) => { + match ai_config + .models + .iter() + .find(|model| model.llm_id == llm_setting.llm_model.llm_id) + { + None => ai_config.models[0].clone(), + Some(llm_model) => { + if llm_model != &llm_setting.llm_model { + info!( + "[LLM Resource] existing model is different from remote, replace with remote model" + ); + } + llm_model.clone() + }, + } + }, + }; + Ok(selected_model) + } + + fn user_plugin_folder(&self) -> FlowyResult { + self.resource_dir().map(|dir| dir.join(PLUGIN_DIR)) + } + + fn user_model_folder(&self) -> FlowyResult { + self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR)) } - let embedding_model = llm_dir.join(&llm_setting.llm_model.embedding_model.file_name); - if !embedding_model.exists() { - return true; + fn plugin_path(&self, etag: &str) -> FlowyResult { + self.user_plugin_folder().map(|dir| dir.join(etag)) } - false + fn model_path(&self, model_file_name: &str) -> FlowyResult { + self + .user_model_folder() + .map(|dir| dir.join(model_file_name)) + } + + fn resource_dir(&self) -> FlowyResult { + let user_data_dir = self.user_service.user_data_dir()?; + Ok(user_data_dir.join("llm")) + } } pub fn bytes_to_readable_size(bytes: u64) -> String { 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 7f3376e1f3ee6..340721a87f425 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 @@ -1,5 +1,5 @@ use crate::chat_manager::ChatUserService; -use crate::entities::{ChatStatePB, LocalModelStatePB, ModelTypePB}; +use crate::entities::{ChatStatePB, LocalModelResourcePB, LocalModelStatePB, ModelTypePB}; use crate::local_ai::llm_resource::{LLMResourceController, LLMResourceService}; use crate::notification::{send_notification, ChatNotification}; use anyhow::Error; @@ -66,8 +66,8 @@ impl LocalLLMController { let llm_res = Arc::new(LLMResourceController::new(user_service, res_impl)); Self { llm_chat, llm_res } } - pub async fn model_info(&self) -> FlowyResult { - self.llm_res.model_info().await + pub async fn refresh(&self) -> FlowyResult { + self.llm_res.refresh_llm_resource().await } pub fn initialize(&self) -> FlowyResult<()> { @@ -134,7 +134,7 @@ impl LocalLLMController { }); } - pub async fn use_local_llm(&self, llm_id: i64) -> FlowyResult { + pub async fn use_local_llm(&self, llm_id: i64) -> FlowyResult { let llm_chat = self.llm_chat.clone(); match llm_chat.destroy_chat_plugin().await { Ok(_) => info!("[Chat Plugin] destroy plugin successfully"), @@ -148,17 +148,20 @@ impl LocalLLMController { Ok(state) } - pub async fn get_local_llm_state(&self) -> FlowyResult { + pub async fn get_local_llm_state(&self) -> FlowyResult { self.llm_res.get_local_llm_state() } - pub fn start_downloading>(&self, progress_sink: T) -> FlowyResult { - let task_id = self.llm_res.start_downloading()?; + pub async fn start_downloading(&self, progress_sink: T) -> FlowyResult + where + T: Sink + Unpin + Sync + Send + 'static, + { + let task_id = self.llm_res.start_downloading(progress_sink).await?; Ok(task_id) } - pub fn cancel_download(&self, task_id: &str) -> FlowyResult<()> { - self.llm_res.cancel_download(task_id)?; + pub fn cancel_download(&self) -> FlowyResult<()> { + self.llm_res.cancel_download()?; Ok(()) } } diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs b/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs index cd63acf0cce65..cf6573c8003f4 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs @@ -1,3 +1,4 @@ pub mod llm_resource; pub mod local_llm_chat; -mod request; +mod model_request; +mod plugin_request; diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs new file mode 100644 index 0000000000000..47d2c777a0f9d --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs @@ -0,0 +1,151 @@ +use anyhow::{anyhow, Result}; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use reqwest::{Client, Response, StatusCode}; +use sha2::{Digest, Sha256}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::fs::{self, File}; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; +use tokio::sync::watch; +use tokio_util::sync::CancellationToken; +use tracing::{instrument, trace}; + +type ProgressCallback = Arc; + +#[instrument(level = "trace", skip_all, err)] +pub async fn download_model( + url: &str, + model_path: &Path, + model_filename: &str, + progress_callback: Option, + cancel_token: Option, +) -> Result { + let client = Client::new(); + let mut response = make_request(&client, url, None).await?; + let total_size_in_bytes = response.content_length().unwrap_or(0); + let partial_path = model_path.join(format!("{}.part", model_filename)); + let download_path = model_path.join(model_filename); + let mut part_file = File::create(&partial_path).await?; + let mut downloaded: u64 = 0; + + while let Some(chunk) = response.chunk().await? { + if let Some(cancel_token) = &cancel_token { + if cancel_token.is_cancelled() { + trace!("Download canceled by client"); + fs::remove_file(&partial_path).await?; + return Err(anyhow!("Download canceled")); + } + } + + part_file.write_all(&chunk).await?; + downloaded += chunk.len() as u64; + + if let Some(progress_callback) = &progress_callback { + progress_callback(downloaded, total_size_in_bytes); + } + } + + // Verify file integrity + let header_sha256 = response + .headers() + .get("SHA256") + .map(|value| value.to_str().ok()) + .flatten() + .map(|value| STANDARD.decode(value).ok()) + .flatten(); + + part_file.seek(tokio::io::SeekFrom::Start(0)).await?; + let mut hasher = Sha256::new(); + let block_size = 2_usize.pow(20); // 1 MB + let mut buffer = vec![0; block_size]; + while let Ok(bytes_read) = part_file.read(&mut buffer).await { + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + let calculated_sha256 = hasher.finalize(); + if let Some(header_sha256) = header_sha256 { + if calculated_sha256.as_slice() != header_sha256.as_slice() { + trace!( + "Header Sha256: {:?}, calculated Sha256:{:?}", + header_sha256, + calculated_sha256 + ); + + fs::remove_file(&partial_path).await?; + return Err(anyhow!( + "Sha256 mismatch: expected {:?}, got {:?}", + header_sha256, + calculated_sha256 + )); + } + } + + fs::rename(&partial_path, &download_path).await?; + Ok(download_path) +} + +async fn make_request( + client: &Client, + url: &str, + offset: Option, +) -> Result { + let mut request = client.get(url); + if let Some(offset) = offset { + println!( + "\nDownload interrupted, resuming from byte position {}", + offset + ); + request = request.header("Range", format!("bytes={}-", offset)); + } + let response = request.send().await?; + if !(response.status().is_success() || response.status() == StatusCode::PARTIAL_CONTENT) { + return Err(anyhow!(response.text().await?)); + } + Ok(response) +} + +#[cfg(test)] +mod test { + use super::*; + use std::env::temp_dir; + #[tokio::test] + async fn retrieve_gpt4all_model_test() { + for url in [ + "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf", + "https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-Q3_K_L.gguf?download=true", + // "https://huggingface.co/MaziyarPanahi/Mistral-7B-Instruct-v0.3-GGUF/resolve/main/Mistral-7B-Instruct-v0.3.Q4_K_M.gguf?download=true", + ] { + let temp_dir = temp_dir().join("download_llm"); + if !temp_dir.exists() { + fs::create_dir(&temp_dir).await.unwrap(); + } + let file_name = "llm_model.gguf"; + let cancel_token = CancellationToken::new(); + let token = cancel_token.clone(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; + token.cancel(); + }); + + let download_file = download_model( + url, + &temp_dir, + file_name, + Some(Arc::new(|a, b| { + println!("{}/{}", a, b); + })), + Some(cancel_token), + ).await.unwrap(); + + let file_path = temp_dir.join(file_name); + assert_eq!(download_file, file_path); + + println!("File path: {:?}", file_path); + assert!(file_path.exists()); + std::fs::remove_file(file_path).unwrap(); + } + } +} diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs new file mode 100644 index 0000000000000..72f625af19782 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs @@ -0,0 +1,102 @@ +use anyhow::anyhow; +use futures_util::StreamExt; +use md5::Context; +use reqwest::Client; +use sha2::{Digest, Sha256}; +use std::error::Error; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::fs; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; +use tokio_util::sync::CancellationToken; +use tracing::trace; + +type ProgressCallback = Arc; + +pub async fn download_plugin( + url: &str, + plugin_dir: &PathBuf, + file_name: &str, + cancel_token: Option, + progress_callback: Option, +) -> Result { + let client = Client::new(); + let response = client.get(url).send().await?; + + if !response.status().is_success() { + return Err(anyhow!("Failed to download file: {}", response.status())); + } + + let total_size = response + .content_length() + .ok_or(anyhow!("Failed to get content length"))?; + + // Create paths for the partial and final files + let partial_path = plugin_dir.join(format!("{}.part", file_name)); + let final_path = plugin_dir.join(file_name); + let mut part_file = File::create(&partial_path).await?; + let mut stream = response.bytes_stream(); + let mut downloaded: u64 = 0; + + while let Some(chunk) = stream.next().await { + if let Some(cancel_token) = &cancel_token { + if cancel_token.is_cancelled() { + trace!("Download canceled"); + fs::remove_file(&partial_path).await?; + return Err(anyhow!("Download canceled")); + } + } + + let bytes = chunk?; + part_file.write_all(&bytes).await?; + downloaded += bytes.len() as u64; + + // Call the progress callback + if let Some(progress_callback) = &progress_callback { + progress_callback(downloaded, total_size); + } + } + + // Ensure all data is written to disk + part_file.sync_all().await?; + + // Move the temporary file to the final destination + fs::rename(&partial_path, &final_path).await?; + trace!("Plugin downloaded to {:?}", final_path); + Ok(final_path) +} + +#[cfg(test)] +mod test { + use super::*; + use std::env::temp_dir; + + #[tokio::test] + async fn download_plugin_test() { + let url = "https://appflowy-local-ai.s3.amazonaws.com/windows-latest/AppFlowyLLM_release.zip?AWSAccessKeyId=AKIAVQA4ULIFKSXHI6PI&Signature=RyHlKjiB5AFSv2S7NFMt7Kr8cyo%3D&Expires=1720788887"; + if url.is_empty() { + return; + } + + let progress_callback: ProgressCallback = Arc::new(|downloaded, total_size| { + let progress = (downloaded as f64 / total_size as f64) * 100.0; + println!("Download progress: {:.2}%", progress); + }); + + let temp_dir = temp_dir().join("download_plugin"); + if !temp_dir.exists() { + std::fs::create_dir(&temp_dir).unwrap(); + } + + download_plugin( + url, + &temp_dir, + "AppFlowyLLM.zip", + None, + Some(progress_callback), + ) + .await + .unwrap(); + } +} diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/request.rs deleted file mode 100644 index e94580a7666c3..0000000000000 --- a/frontend/rust-lib/flowy-chat/src/local_ai/request.rs +++ /dev/null @@ -1,165 +0,0 @@ -use reqwest::{Client, Response, StatusCode}; -use sha2::{Digest, Sha256}; - -use crate::chat_manager::ChatUserService; -use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting}; -use anyhow::anyhow; -use appflowy_local_ai::llm_chat::ChatPluginConfig; -use base64::engine::general_purpose::STANDARD; -use base64::Engine; -use flowy_chat_pub::cloud::{LLMModel, LocalAIConfig}; -use flowy_error::{FlowyError, FlowyResult}; -use lib_infra::async_trait::async_trait; -use parking_lot::RwLock; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use tokio::fs::{self, File}; -use tokio::io::SeekFrom; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; -use tracing::{instrument, trace}; - -type ProgressCallback = Arc; - -pub async fn retrieve_model( - url: &str, - model_path: &Path, - model_filename: &str, - progress_callback: ProgressCallback, -) -> Result> { - let model_download_path = - download_model(url, model_filename, model_path, None, progress_callback).await?; - Ok(model_download_path) -} - -async fn make_request( - client: &Client, - url: &str, - offset: Option, -) -> Result { - let mut request = client.get(url); - if let Some(offset) = offset { - println!( - "\nDownload interrupted, resuming from byte position {}", - offset - ); - request = request.header("Range", format!("bytes={}-", offset)); - } - let response = request.send().await?; - if !(response.status().is_success() || response.status() == StatusCode::PARTIAL_CONTENT) { - return Err(anyhow!(response.text().await?)); - } - Ok(response) -} - -async fn download_model( - url: &str, - model_filename: &str, - model_path: &Path, - expected_size: Option, - progress_callback: ProgressCallback, -) -> Result { - let client = Client::new(); - // Make the initial request - let mut response = make_request(&client, url, None).await?; - let total_size_in_bytes = response.content_length().unwrap_or(0); - let partial_path = model_path.join(format!("{}.part", model_filename)); - let download_path = model_path.join(model_filename); - let mut part_file = File::create(&partial_path).await?; - let mut downloaded: u64 = 0; - while let Some(chunk) = response.chunk().await? { - part_file.write_all(&chunk).await?; - downloaded += chunk.len() as u64; - progress_callback(downloaded, total_size_in_bytes); - } - - // Verify file integrity - let file_size = part_file.metadata().await?.len(); - trace!("Downloaded file:{}, size: {}", model_filename, file_size); - if let Some(expected_size) = expected_size { - if file_size != expected_size { - return Err(anyhow!( - "Expected file size of {} bytes, got {}", - expected_size, - file_size - )); - } - } - - let header_md5 = response - .headers() - .get("Content-MD5") - .map(|value| value.to_str().ok()) - .flatten() - .map(|value| STANDARD.decode(value).ok()) - .flatten(); - part_file.seek(SeekFrom::Start(0)).await?; - let mut hasher = Sha256::new(); - let block_size = 2_usize.pow(20); // 1 MB - let mut buffer = vec![0; block_size]; - while let Ok(bytes_read) = part_file.read(&mut buffer).await { - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - let calculated_md5 = hasher.finalize(); - if let Some(header_md5) = header_md5 { - if calculated_md5.as_slice() != header_md5.as_slice() { - // remove partial file - fs::remove_file(&partial_path).await?; - - return Err(anyhow!( - "MD5 mismatch: expected {:?}, got {:?}", - header_md5, - calculated_md5 - )); - } - } - - fs::rename(&partial_path, &download_path).await?; - trace!("Model downloaded to {:?}", download_path); - Ok(download_path) -} - -// #[cfg(test)] -// mod test { -// use super::*; -// #[tokio::test] -// async fn retrieve_gpt4all_model_test() { -// let file_name = "all-MiniLM-L6-v2-f16.gguf"; -// let path = Path::new("."); -// retrieve_model( -// "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf", -// &path, -// file_name, -// Arc::new(|a, b| { -// println!("{}/{}", a, b); -// }), -// ) -// .await -// .unwrap(); -// -// let file_path = path.join(file_name); -// assert!(file_path.exists()); -// std::fs::remove_file(file_path).unwrap(); -// } -// -// #[tokio::test] -// async fn retrieve_hugging_face_model_test() { -// let path = Path::new("."); -// let file_name = "all-MiniLM-L6-v2-Q3_K_L.gguf"; -// retrieve_model( -// "https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-Q3_K_L.gguf?download=true", -// &path, -// file_name, -// Arc::new(|a, b| { -// println!("{}/{}", a, b); -// }), -// ) -// .await -// .unwrap(); -// let file_path = path.join(file_name); -// assert!(file_path.exists()); -// std::fs::remove_file(file_path).unwrap(); -// } -// } diff --git a/frontend/rust-lib/lib-infra/src/isolate_stream.rs b/frontend/rust-lib/lib-infra/src/isolate_stream.rs index 19f692dda4f58..3f2be5477bb54 100644 --- a/frontend/rust-lib/lib-infra/src/isolate_stream.rs +++ b/frontend/rust-lib/lib-infra/src/isolate_stream.rs @@ -1,4 +1,5 @@ use allo_isolate::{IntoDart, Isolate}; +use anyhow::anyhow; use futures::Sink; use pin_project::pin_project; use std::pin::Pin; @@ -19,7 +20,7 @@ impl Sink for IsolateSink where T: IntoDart, { - type Error = (); + type Error = anyhow::Error; fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) @@ -30,7 +31,7 @@ where if this.isolate.post(item) { Ok(()) } else { - Err(()) + Err(anyhow!("failed to post message")) } } From cb60cc0257fdb378422c1121f12d12e5342cdfbf Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 12 Jul 2024 22:20:34 +0800 Subject: [PATCH 03/13] chore: update zip --- frontend/appflowy_tauri/src-tauri/Cargo.toml | 1 + frontend/rust-lib/Cargo.lock | 347 ++++++++++++++---- frontend/rust-lib/Cargo.toml | 1 + .../event-integration-test/Cargo.toml | 2 +- frontend/rust-lib/flowy-chat/Cargo.toml | 1 + .../flowy-chat/src/local_ai/llm_resource.rs | 13 - .../rust-lib/flowy-chat/src/local_ai/mod.rs | 2 + .../flowy-chat/src/local_ai/plugin_request.rs | 5 +- .../rust-lib/flowy-chat/src/local_ai/util.rs | 53 +++ frontend/rust-lib/flowy-error/Cargo.toml | 2 +- frontend/rust-lib/flowy-search/Cargo.toml | 2 +- .../flowy-search/src/folder/indexer.rs | 8 +- 12 files changed, 344 insertions(+), 93 deletions(-) create mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/util.rs diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index c9cd46684f2d2..d040b4ad9ed76 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -30,6 +30,7 @@ tokio-stream = "0.1.14" async-trait = "0.1.74" chrono = { version = "0.4.31", default-features = false, features = ["clock"] } yrs = "0.18.8" +zip = "2.1.3" # Please use the following script to update collab. # Working directory: frontend # diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 58fa105599a33..366a5eee87d3d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -228,6 +228,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -373,6 +382,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -438,9 +453,9 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bitpacking" -version = "0.8.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" dependencies = [ "crunchy", ] @@ -544,9 +559,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" @@ -1082,6 +1097,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "cookie" version = "0.17.0" @@ -1135,11 +1156,26 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1210,7 +1246,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1337,6 +1373,12 @@ dependencies = [ "regex", ] +[[package]] +name = "deflate64" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" + [[package]] name = "delegate-display" version = "2.1.1" @@ -1351,10 +1393,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ + "powerfmt", "serde", ] @@ -1369,6 +1412,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1449,6 +1503,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -1587,7 +1652,7 @@ dependencies = [ "tracing", "uuid", "walkdir", - "zip", + "zip 2.1.3", ] [[package]] @@ -1678,9 +1743,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1734,6 +1799,7 @@ dependencies = [ "tracing-subscriber", "uuid", "validator", + "zip 2.1.3", ] [[package]] @@ -2363,12 +2429,12 @@ dependencies = [ [[package]] name = "fs4" -version = "0.6.6" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3057,6 +3123,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -3163,7 +3238,7 @@ dependencies = [ "tracing", "validator", "walkdir", - "zip", + "zip 0.6.6", ] [[package]] @@ -3256,6 +3331,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.21" @@ -3278,9 +3359,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.11.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ "hashbrown 0.14.3", ] @@ -3291,6 +3372,16 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "912b45c753ff5f7f5208307e8ace7d2a2e30d024e26d3509f3dce546c044ce15" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "mac" version = "0.1.1" @@ -3406,9 +3497,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.7.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] @@ -3570,6 +3661,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.45" @@ -3587,6 +3684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3701,9 +3799,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "ownedbytes" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" +checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" dependencies = [ "stable_deref_trait", ] @@ -3880,7 +3978,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3900,6 +3998,7 @@ 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", ] @@ -3967,6 +4066,19 @@ 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" @@ -4082,6 +4194,12 @@ dependencies = [ "reqwest", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -4170,7 +4288,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4191,7 +4309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -4468,6 +4586,16 @@ dependencies = [ "getrandom 0.2.10", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -4583,6 +4711,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -5050,9 +5184,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -5124,6 +5258,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.4" @@ -5402,14 +5542,13 @@ dependencies = [ [[package]] name = "tantivy" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2" +checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" dependencies = [ "aho-corasick", "arc-swap", - "async-trait", - "base64 0.21.5", + "base64 0.22.1", "bitpacking", "byteorder", "census", @@ -5417,16 +5556,16 @@ dependencies = [ "crossbeam-channel", "downcast-rs", "fastdivide", + "fnv", "fs4", "htmlescape", - "itertools 0.11.0", + "itertools 0.12.1", "levenshtein_automata", "log", "lru", "lz4_flex", "measure_time", "memmap2", - "murmurhash32", "num_cpus", "once_cell", "oneshot", @@ -5454,22 +5593,22 @@ dependencies = [ [[package]] name = "tantivy-bitpacker" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" +checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" +checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" dependencies = [ + "downcast-rs", "fastdivide", - "fnv", - "itertools 0.11.0", + "itertools 0.12.1", "serde", "tantivy-bitpacker", "tantivy-common", @@ -5479,9 +5618,9 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" +checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" dependencies = [ "async-trait", "byteorder", @@ -5492,50 +5631,52 @@ dependencies = [ [[package]] name = "tantivy-fst" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" dependencies = [ "byteorder", - "regex-syntax 0.6.29", + "regex-syntax 0.8.4", "utf8-ranges", ] [[package]] name = "tantivy-query-grammar" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" +checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" dependencies = [ "nom", ] [[package]] name = "tantivy-sstable" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" +checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" dependencies = [ + "tantivy-bitpacker", "tantivy-common", "tantivy-fst", - "zstd 0.12.4", + "zstd 0.13.2", ] [[package]] name = "tantivy-stacker" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" +checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" dependencies = [ "murmurhash32", + "rand_distr", "tantivy-common", ] [[package]] name = "tantivy-tokenizer-api" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" +checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" dependencies = [ "serde", ] @@ -5654,12 +5795,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", + "powerfmt", "serde", "time-core", "time-macros", @@ -5667,16 +5810,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -6751,6 +6895,26 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "zip" version = "0.6.6" @@ -6760,7 +6924,7 @@ dependencies = [ "aes", "byteorder", "bzip2", - "constant_time_eq", + "constant_time_eq 0.1.5", "crc32fast", "crossbeam-utils", "flate2", @@ -6771,6 +6935,49 @@ dependencies = [ "zstd 0.11.2+zstd.1.5.2", ] +[[package]] +name = "zip" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq 0.3.0", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap 2.1.0", + "lzma-rs", + "memchr", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd 0.13.2", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" @@ -6782,11 +6989,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.12.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe 6.0.6", + "zstd-safe 7.2.0", ] [[package]] @@ -6801,21 +7008,19 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "6.0.6" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ - "libc", "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b5c6a3461a98c..c2bb3d7184cdc 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -92,6 +92,7 @@ collab-user = { version = "0.2" } yrs = "0.18.8" validator = { version = "0.16.1", features = ["derive"] } tokio-util = "0.7.11" +zip = "2.1.3" # Please using the following command to update the revision id # Current directory: frontend diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index a644709791345..b836e8a40ca6f 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -55,7 +55,7 @@ uuid.workspace = true assert-json-diff = "2.0.2" tokio-postgres = { version = "0.7.8" } chrono = "0.4.31" -zip = "0.6.6" +zip.workspace = true walkdir = "2.5.0" futures = "0.3.30" flowy-chat-pub = { workspace = true } diff --git a/frontend/rust-lib/flowy-chat/Cargo.toml b/frontend/rust-lib/flowy-chat/Cargo.toml index 5b0d8ae8513ee..d57b7cc0bd3b6 100644 --- a/frontend/rust-lib/flowy-chat/Cargo.toml +++ b/frontend/rust-lib/flowy-chat/Cargo.toml @@ -41,6 +41,7 @@ sha2 = "0.10.7" base64 = "0.21.5" futures-util = "0.3.30" md5 = "0.7.0" +zip.workspace = true [dev-dependencies] dotenv = "0.15.0" diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs index 169288332c7c7..8f1de4d77be3b 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs @@ -480,16 +480,3 @@ impl LLMResourceController { Ok(user_data_dir.join("llm")) } } - -pub fn bytes_to_readable_size(bytes: u64) -> String { - const GB: u64 = 1_000_000_000; - const MB: u64 = 1_000_000; - - if bytes >= GB { - let size_in_gb = bytes as f64 / GB as f64; - format!("{:.2} GB", size_in_gb) - } else { - let size_in_mb = bytes as f64 / MB as f64; - format!("{:.2} MB", size_in_mb) - } -} diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs b/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs index cf6573c8003f4..ca3630bd56ac3 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs @@ -2,3 +2,5 @@ pub mod llm_resource; pub mod local_llm_chat; mod model_request; mod plugin_request; + +mod util; diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs index 72f625af19782..32176566fc47c 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs @@ -74,7 +74,7 @@ mod test { #[tokio::test] async fn download_plugin_test() { - let url = "https://appflowy-local-ai.s3.amazonaws.com/windows-latest/AppFlowyLLM_release.zip?AWSAccessKeyId=AKIAVQA4ULIFKSXHI6PI&Signature=RyHlKjiB5AFSv2S7NFMt7Kr8cyo%3D&Expires=1720788887"; + let url = "https://appflowy-local-ai.s3.amazonaws.com/macos-latest/AppFlowyLLM_release.zip?AWSAccessKeyId=AKIAVQA4ULIFKSXHI6PI&Signature=KKXVOOJUG1TSVGZeuJV1MZ4k49o%3D&Expires=1720828964"; if url.is_empty() { return; } @@ -89,7 +89,7 @@ mod test { std::fs::create_dir(&temp_dir).unwrap(); } - download_plugin( + let path = download_plugin( url, &temp_dir, "AppFlowyLLM.zip", @@ -98,5 +98,6 @@ mod test { ) .await .unwrap(); + println!("Downloaded plugin to {:?}", path); } } diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/util.rs b/frontend/rust-lib/flowy-chat/src/local_ai/util.rs new file mode 100644 index 0000000000000..4859168c75b0f --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/local_ai/util.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use sha2::Digest; +use std::fs; +use std::fs::{create_dir_all, remove_dir_all, File}; +use std::io::{BufReader, Read, Write}; +use std::path::Path; +use zip::ZipArchive; + +pub fn bytes_to_readable_size(bytes: u64) -> String { + const GB: u64 = 1_000_000_000; + const MB: u64 = 1_000_000; + + if bytes >= GB { + let size_in_gb = bytes as f64 / GB as f64; + format!("{:.2} GB", size_in_gb) + } else { + let size_in_mb = bytes as f64 / MB as f64; + format!("{:.2} MB", size_in_mb) + } +} +async fn unzip_file(zip_path: impl AsRef, destination_path: &Path) -> Result<()> { + // Remove the destination directory if it exists and create a new one + if destination_path.exists() { + remove_dir_all(destination_path)?; + } + create_dir_all(destination_path)?; + + // Open the zip file + let file = File::open(zip_path)?; + let mut archive = ZipArchive::new(BufReader::new(file))?; + + // Iterate over each file in the archive and extract it + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let outpath = destination_path.join(file.name()); + + if file.name().ends_with('/') { + create_dir_all(&outpath)?; + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + create_dir_all(&p)?; + } + } + let mut outfile = fs::File::create(&outpath)?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + outfile.write_all(&buffer)?; + } + } + + Ok(()) +} diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index cb3864c93c0d1..2eda43b392076 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -32,7 +32,7 @@ collab-document = { workspace = true, optional = true } collab-plugins = { workspace = true, optional = true } collab-folder = { workspace = true, optional = true } client-api = { workspace = true, optional = true } -tantivy = { version = "0.21.1", optional = true } +tantivy = { version = "0.22.0", optional = true } [features] impl_from_dispatch_error = ["lib-dispatch"] diff --git a/frontend/rust-lib/flowy-search/Cargo.toml b/frontend/rust-lib/flowy-search/Cargo.toml index dbd2b3ecf14a4..a98ab15a38a54 100644 --- a/frontend/rust-lib/flowy-search/Cargo.toml +++ b/frontend/rust-lib/flowy-search/Cargo.toml @@ -36,7 +36,7 @@ tracing.workspace = true async-stream = "0.3.4" strsim = "0.11.0" strum_macros = "0.26.1" -tantivy = { version = "0.21.1" } +tantivy = { version = "0.22.0" } tempfile = "3.9.0" validator = { version = "0.16.0", features = ["derive"] } diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 5831e0871aab8..662406778857a 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -22,8 +22,8 @@ use flowy_user::services::authenticate_user::AuthenticateUser; use lib_dispatch::prelude::af_spawn; use strsim::levenshtein; use tantivy::{ - collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Index, - IndexReader, IndexWriter, Term, + collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document, + Index, IndexReader, IndexWriter, TantivyDocument, Term, }; use super::entities::FolderIndexData; @@ -233,10 +233,10 @@ impl FolderIndexManagerImpl { let mut search_results: Vec = vec![]; let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?; for (_score, doc_address) in top_docs { - let retrieved_doc = searcher.doc(doc_address)?; + let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?; let mut content = HashMap::new(); - let named_doc = folder_schema.schema.to_named_doc(&retrieved_doc); + let named_doc = retrieved_doc.to_named_doc(&folder_schema.schema); for (k, v) in named_doc.0 { content.insert(k, v[0].clone()); } From a5c40dccbecb5689eddd348865cee952887378bb Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 12 Jul 2024 22:24:11 +0800 Subject: [PATCH 04/13] chore: config download ui --- .../ai_chat/application/chat_file_bloc.dart | 44 +++ .../settings/ai/download_model_bloc.dart | 136 +++++++ .../settings/ai/local_ai_bloc.dart | 206 +++++++++++ .../settings/ai/local_llm_listener.dart | 1 + .../pages/setting_ai_view/downloading.dart | 109 ++++++ .../setting_ai_view/local_ai_config.dart | 344 ++++++++++++++++++ .../setting_ai_view/model_selection.dart | 85 +++++ .../setting_ai_view/settings_ai_view.dart | 100 +++++ .../flowy_icons/16x/download_success.svg | 3 + .../flowy_icons/16x/download_warn.svg | 8 + .../flowy_icons/16x/local_model_download.svg | 5 + .../flowy-chat/src/local_ai/plugin_request.rs | 2 + .../rust-lib/flowy-chat/src/local_ai/util.rs | 4 +- 13 files changed, 1045 insertions(+), 2 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart create mode 100644 frontend/resources/flowy_icons/16x/download_success.svg create mode 100644 frontend/resources/flowy_icons/16x/download_warn.svg create mode 100644 frontend/resources/flowy_icons/16x/local_model_download.svg 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 new file mode 100644 index 0000000000000..7f81dbcb53815 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart @@ -0,0 +1,44 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'chat_file_bloc.freezed.dart'; + +class ChatFileBloc extends Bloc { + ChatFileBloc({ + required String chatId, + dynamic message, + }) : super(ChatFileState.initial(message)) { + on( + (event, emit) async { + await event.when( + initial: () async {}, + newFile: (String filePath) { + final payload = ChatFilePB(filePath: filePath, chatId: chatId); + ChatEventChatWithFile(payload).send(); + }, + ); + }, + ); + } +} + +@freezed +class ChatFileEvent with _$ChatFileEvent { + const factory ChatFileEvent.initial() = Initial; + const factory ChatFileEvent.newFile(String filePath) = _NewFile; +} + +@freezed +class ChatFileState with _$ChatFileState { + const factory ChatFileState({ + required String text, + }) = _ChatFileState; + + factory ChatFileState.initial(dynamic text) { + return ChatFileState( + text: text is String ? text : "", + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart new file mode 100644 index 0000000000000..f26d29ef978e7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart @@ -0,0 +1,136 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:isolate'; + +import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:fixnum/fixnum.dart'; +part 'download_model_bloc.freezed.dart'; + +class DownloadModelBloc extends Bloc { + DownloadModelBloc(LLMModelPB model) + : super(DownloadModelState(model: model)) { + on(_handleEvent); + } + + Future _handleEvent( + DownloadModelEvent event, + Emitter emit, + ) async { + await event.when( + started: () async { + final downloadStream = DownloadingStream(); + downloadStream.listen( + onModelPercentage: (name, percent) { + if (!isClosed) { + add( + DownloadModelEvent.updatePercent(name, percent), + ); + } + }, + onPluginPercentage: (percent) { + if (!isClosed) { + add(DownloadModelEvent.updatePercent("AppFlowy Plugin", percent)); + } + }, + onFinish: () { + emit(state.copyWith(isFinish: true)); + }, + onError: (err) { + emit(state.copyWith(downloadError: err)); + }, + ); + + final payload = + DownloadLLMPB(progressStream: Int64(downloadStream.nativePort)); + final result = await ChatEventDownloadLLMResource(payload).send(); + result.fold((_) { + emit( + state.copyWith( + downloadStream: downloadStream, + loadingState: const LoadingState.finish(), + downloadError: null, + ), + ); + }, (err) { + emit(state.copyWith(loadingState: LoadingState.finish(error: err))); + }); + }, + updatePercent: (String object, double percent) { + emit(state.copyWith(object: object, percent: percent)); + }, + ); + } +} + +@freezed +class DownloadModelEvent with _$DownloadModelEvent { + const factory DownloadModelEvent.started() = _Started; + const factory DownloadModelEvent.updatePercent( + String object, + double percent, + ) = _UpdatePercent; +} + +@freezed +class DownloadModelState with _$DownloadModelState { + const factory DownloadModelState({ + required LLMModelPB model, + DownloadingStream? downloadStream, + String? downloadError, + @Default("") String object, + @Default(0) double percent, + @Default(false) bool isFinish, + @Default(LoadingState.loading()) LoadingState loadingState, + }) = _DownloadModelState; +} + +class DownloadingStream { + DownloadingStream() { + _port.handler = _controller.add; + } + + final RawReceivePort _port = RawReceivePort(); + StreamSubscription? _sub; + final StreamController _controller = StreamController.broadcast(); + int get nativePort => _port.sendPort.nativePort; + + Future dispose() async { + await _sub?.cancel(); + await _controller.close(); + _port.close(); + } + + void listen({ + void Function(String modelName, double percent)? onModelPercentage, + void Function(double percent)? onPluginPercentage, + void Function(String data)? onError, + void Function()? onFinish, + }) { + _sub = _controller.stream.listen((text) { + if (text.contains(':progress:')) { + final progressIndex = text.indexOf(':progress:'); + final modelName = text.substring(0, progressIndex); + final progressValue = text + .substring(progressIndex + 10); // 10 is the length of ":progress:" + final percent = double.tryParse(progressValue); + if (percent != null) { + onModelPercentage?.call(modelName, percent); + } + } else if (text.startsWith('plugin:progress:')) { + final percent = double.tryParse(text.substring(16)); + if (percent != null) { + onPluginPercentage?.call(percent); + } + } else if (text.startsWith('finish')) { + onFinish?.call(); + } else if (text.startsWith('error:')) { + // substring 6 to remove "error:" + onError?.call(text.substring(6)); + } + }); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart new file mode 100644 index 0000000000000..11d9c27edaf85 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'local_ai_bloc.freezed.dart'; + +class LocalAIConfigBloc extends Bloc { + LocalAIConfigBloc() : super(const LocalAIConfigState()) { + on(_handleEvent); + } + + /// Handles incoming events and dispatches them to the appropriate handler. + Future _handleEvent( + LocalAIConfigEvent event, + Emitter emit, + ) async { + await event.when( + started: _handleStarted, + didLoadModelInfo: (FlowyResult result) { + result.fold( + (modelInfo) { + _fetchCurremtLLMState(); + emit( + state.copyWith( + modelInfo: modelInfo, + models: modelInfo.models, + selectedLLMModel: modelInfo.selectedModel, + loadingState: const LoadingState.finish(), + ), + ); + }, + (err) { + emit(state.copyWith(loadingState: LoadingState.finish(error: err))); + }, + ); + }, + selectLLMConfig: (LLMModelPB llmModel) async { + final result = await ChatEventUpdateLocalLLM(llmModel).send(); + result.fold( + (llmResource) { + if (llmResource.pendingResources.isNotEmpty) { + emit( + state.copyWith( + selectedLLMModel: llmModel, + localAIInfo: LocalAIInfo.requestDownload( + llmResource, + llmModel, + ), + llmModelLoadingState: const LoadingState.finish(), + ), + ); + } else { + emit( + state.copyWith( + selectedLLMModel: llmModel, + llmModelLoadingState: const LoadingState.finish(), + ), + ); + } + }, + (err) { + emit( + state.copyWith( + llmModelLoadingState: LoadingState.finish(error: err), + ), + ); + }, + ); + }, + refreshLLMState: (LocalModelResourcePB llmResource) { + if (state.selectedLLMModel == null) { + Log.error( + 'Unexpected null selected config. It should be set already'); + return; + } + + if (llmResource.pendingResources.isEmpty) { + emit( + state.copyWith( + localAIInfo: const LocalAIInfo.readyToUse(), + ), + ); + } else { + if (state.selectedLLMModel != null) { + if (llmResource.isDownloading) { + emit( + state.copyWith( + localAIInfo: LocalAIInfo.downloading(state.selectedLLMModel!), + llmModelLoadingState: const LoadingState.finish(), + ), + ); + return; + } else { + emit( + state.copyWith( + localAIInfo: LocalAIInfo.downloadNeeded( + llmResource, + state.selectedLLMModel!, + ), + llmModelLoadingState: const LoadingState.finish(), + ), + ); + } + } + } + }, + startDownloadModel: (LLMModelPB llmModel) { + emit( + state.copyWith( + localAIInfo: LocalAIInfo.downloading(llmModel), + llmModelLoadingState: const LoadingState.finish(), + ), + ); + }, + cancelDownload: () async { + final _ = await ChatEventCancelDownloadLLMResource().send(); + _fetchCurremtLLMState(); + }, + finishDownload: () { + emit( + state.copyWith(localAIInfo: const LocalAIInfo.finishDownload()), + ); + }, + ); + } + + void _fetchCurremtLLMState() async { + final result = await ChatEventGetLocalLLMState().send(); + result.fold( + (llmResource) { + if (!isClosed) { + add(LocalAIConfigEvent.refreshLLMState(llmResource)); + } + }, + (err) { + Log.error(err); + }, + ); + } + + /// Handles the event to fetch local AI settings when the application starts. + Future _handleStarted() async { + final result = await ChatEventRefreshLocalAIModelInfo().send(); + if (!isClosed) { + add(LocalAIConfigEvent.didLoadModelInfo(result)); + } + } +} + +@freezed +class LocalAIConfigEvent with _$LocalAIConfigEvent { + const factory LocalAIConfigEvent.started() = _Started; + const factory LocalAIConfigEvent.didLoadModelInfo( + FlowyResult result, + ) = _ModelInfo; + const factory LocalAIConfigEvent.selectLLMConfig(LLMModelPB config) = + _SelectLLMConfig; + + const factory LocalAIConfigEvent.refreshLLMState( + LocalModelResourcePB llmResource, + ) = _RefreshLLMResource; + const factory LocalAIConfigEvent.startDownloadModel(LLMModelPB llmModel) = + _StartDownloadModel; + + const factory LocalAIConfigEvent.cancelDownload() = _CancelDownload; + const factory LocalAIConfigEvent.finishDownload() = _FinishDownload; +} + +@freezed +class LocalAIConfigState with _$LocalAIConfigState { + const factory LocalAIConfigState({ + LLMModelInfoPB? modelInfo, + LLMModelPB? selectedLLMModel, + LocalAIInfo? localAIInfo, + @Default(LoadingState.loading()) LoadingState llmModelLoadingState, + @Default([]) List models, + @Default(LoadingState.loading()) LoadingState loadingState, + }) = _LocalAIConfigState; +} + +@freezed +class LocalAIInfo with _$LocalAIInfo { + // when user select a new model, it will call requestDownload + const factory LocalAIInfo.requestDownload( + LocalModelResourcePB llmResource, + LLMModelPB llmModel, + ) = _RequestDownload; + + // when user comes back to the setting page, it will auto detect current llm state + const factory LocalAIInfo.downloadNeeded( + LocalModelResourcePB llmResource, + LLMModelPB llmModel, + ) = _DownloadNeeded; + + // when start downloading the model + const factory LocalAIInfo.downloading(LLMModelPB llmModel) = _Downloading; + const factory LocalAIInfo.finishDownload() = _Finish; + const factory LocalAIInfo.readyToUse() = _ReadyToUse; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart new file mode 100644 index 0000000000000..a646a39474dbc --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart @@ -0,0 +1 @@ +class LocalLLMListener {} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading.dart new file mode 100644 index 0000000000000..5c01d1441fe6a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading.dart @@ -0,0 +1,109 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/download_model_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; + +class DownloadingIndicator extends StatelessWidget { + const DownloadingIndicator({ + required this.llmModel, + required this.onCancel, + required this.onFinish, + super.key, + }); + final LLMModelPB llmModel; + final VoidCallback onCancel; + final VoidCallback onFinish; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + DownloadModelBloc(llmModel)..add(const DownloadModelEvent.started()), + child: BlocListener( + listener: (context, state) { + if (state.isFinish) { + onFinish(); + } + }, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + // const DownloadingPrompt(), + // const VSpace(12), + DownloadingProgressBar(onCancel: onCancel), + ], + ), + ), + ), + ), + ); + } +} + +class DownloadingProgressBar extends StatelessWidget { + const DownloadingProgressBar({required this.onCancel, super.key}); + + final VoidCallback onCancel; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText( + "${LocaleKeys.settings_aiPage_keys_downloadingModel.tr()}: ${state.object}", + fontSize: 11, + ), + IntrinsicHeight( + child: Row( + children: [ + Expanded( + child: LinearPercentIndicator( + lineHeight: 9.0, + percent: state.percent, + padding: EdgeInsets.zero, + progressColor: AFThemeExtension.of(context).success, + backgroundColor: + AFThemeExtension.of(context).progressBarBGColor, + barRadius: const Radius.circular(8), + trailing: FlowyText( + "${(state.percent * 100).toStringAsFixed(0)}%", + fontSize: 11, + color: AFThemeExtension.of(context).success, + ), + ), + ), + const HSpace(12), + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.button_cancel.tr(), + fontSize: 11, + ), + onTap: onCancel, + ) + ], + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart new file mode 100644 index 0000000000000..64559e9cc9841 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart @@ -0,0 +1,344 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/downloading.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class LocalModelConfig extends StatelessWidget { + const LocalModelConfig({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.aiSettings == null) { + return const SizedBox.shrink(); + } + + if (state.aiSettings!.aiModel != AIModelPB.LocalAIModel) { + return const SizedBox.shrink(); + } + + return BlocProvider( + create: (context) => + LocalAIConfigBloc()..add(const LocalAIConfigEvent.started()), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FlowyText.medium( + LocaleKeys.settings_aiPage_keys_llmModel.tr(), + fontSize: 14, + ), + ), + const Spacer(), + BlocBuilder( + builder: (context, state) { + return state.loadingState.when( + loading: () => + const CircularProgressIndicator.adaptive(), + finish: (err) { + return (err == null) + ? const _SelectLocalModelDropdownMenu() + : const SizedBox.shrink(); + }, + ); + }, + ), + ], + ), + const IntrinsicHeight(child: _LocalLLMInfoWidget()), + ], + ), + ), + ); + }, + ); + } +} + +class _SelectLocalModelDropdownMenu extends StatelessWidget { + const _SelectLocalModelDropdownMenu(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Flexible( + child: SettingsDropdown( + key: const Key('_SelectLocalModelDropdownMenu'), + onChanged: (model) => context.read().add( + LocalAIConfigEvent.selectLLMConfig(model), + ), + selectedOption: state.selectedLLMModel!, + options: state.models + .map( + (llm) => buildDropdownMenuEntry( + context, + value: llm, + label: llm.chatModel, + padding: const EdgeInsets.symmetric(vertical: 8), + ), + ) + .toList(), + ), + ); + }, + ); + } +} + +class _LocalLLMInfoWidget extends StatelessWidget { + const _LocalLLMInfoWidget(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final error = errorFromState(state); + if (error == null) { + // If the error is null, handle selected llm model. + if (state.localAIInfo != null) { + return state.localAIInfo!.when( + requestDownload: ( + LocalModelResourcePB llmResource, + LLMModelPB llmModel, + ) { + _showDownloadDialog(context, llmResource, llmModel); + return const SizedBox.shrink(); + }, + downloadNeeded: ( + LocalModelResourcePB llmResource, + LLMModelPB llmModel, + ) { + return Padding( + padding: const EdgeInsets.only(top: 14), + child: _ModelNotExistIndicator( + llmResource: llmResource, + llmModel: llmModel, + ), + ); + }, + downloading: (llmModel) { + return Padding( + padding: const EdgeInsets.only(top: 14), + child: DownloadingIndicator( + key: UniqueKey(), + llmModel: llmModel, + onFinish: () { + context.read().add( + const LocalAIConfigEvent.finishDownload(), + ); + }, + onCancel: () { + context.read().add( + const LocalAIConfigEvent.cancelDownload(), + ); + }, + ), + ); + }, + readyToUse: () => const SizedBox.shrink(), + finishDownload: () => const _FinishDownloadIndicator(), + ); + } else { + return const SizedBox.shrink(); + } + } else { + return FlowyText( + error.msg, + maxLines: 10, + ); + } + }, + ); + } + + void _showDownloadDialog( + BuildContext context, + LocalModelResourcePB llmResource, + LLMModelPB llmModel, + ) { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + showDialog( + context: context, + barrierDismissible: false, + useRootNavigator: false, + builder: (dialogContext) { + return _LLMModelDownloadDialog( + llmResource: llmResource, + onOkPressed: () { + context.read().add( + LocalAIConfigEvent.startDownloadModel( + llmModel, + ), + ); + }, + onCancelPressed: () { + context.read().add( + const LocalAIConfigEvent.cancelDownload(), + ); + }, + ); + }, + ); + }, + debugLabel: 'localModel.download', + ); + } + + FlowyError? errorFromState(LocalAIConfigState state) { + final err = state.loadingState.when( + loading: () => null, + finish: (err) => err, + ); + + if (err == null) { + state.llmModelLoadingState.when( + loading: () => null, + finish: (err) => err, + ); + } + + return err; + } +} + +class _LLMModelDownloadDialog extends StatelessWidget { + const _LLMModelDownloadDialog({ + required this.llmResource, + required this.onOkPressed, + required this.onCancelPressed, + }); + final LocalModelResourcePB llmResource; + final VoidCallback onOkPressed; + final VoidCallback onCancelPressed; + + @override + Widget build(BuildContext context) { + return NavigatorOkCancelDialog( + title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr( + args: [ + llmResource.pendingResources[0].modelName, + ], + ), + message: LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr( + args: [ + llmResource.pendingResources[0].modelName, + llmResource.pendingResources[0].modelSize.toString(), + ], + ), + okTitle: LocaleKeys.button_confirm.tr(), + cancelTitle: LocaleKeys.button_cancel.tr(), + onOkPressed: onOkPressed, + onCancelPressed: onCancelPressed, + titleUpperCase: false, + ); + } +} + +class _ModelNotExistIndicator extends StatelessWidget { + const _ModelNotExistIndicator({ + required this.llmResource, + required this.llmModel, + }); + final LocalModelResourcePB llmResource; + final LLMModelPB llmModel; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Row( + children: [ + const Spacer(), + IntrinsicWidth( + child: SizedBox( + height: 30, + child: FlowyButton( + text: FlowyText( + LocaleKeys.settings_aiPage_keys_downloadAIModelButton.tr(), + fontSize: 14, + color: const Color(0xFF005483), + ), + leftIcon: const FlowySvg( + FlowySvgs.local_model_download_s, + color: Color(0xFF005483), + ), + onTap: () { + showDialog( + context: context, + barrierDismissible: false, + useRootNavigator: false, + builder: (dialogContext) { + return _LLMModelDownloadDialog( + llmResource: llmResource, + onOkPressed: () { + context.read().add( + LocalAIConfigEvent.startDownloadModel( + llmModel, + ), + ); + }, + onCancelPressed: () { + context.read().add( + const LocalAIConfigEvent.cancelDownload(), + ); + }, + ); + }, + ); + }, + ), + ), + ), + ], + ); + }, + ); + } +} + +class _FinishDownloadIndicator extends StatelessWidget { + const _FinishDownloadIndicator(); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration(color: Color(0xFFEDF7ED)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: Row( + children: [ + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + FlowyText( + LocaleKeys.settings_aiPage_keys_downloadModelSuccess.tr(), + fontSize: 11, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart new file mode 100644 index 0000000000000..d37698faa7df1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AIModelSelection extends StatelessWidget { + const AIModelSelection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FlowyText.medium( + LocaleKeys.settings_aiPage_keys_llmModelType.tr(), + fontSize: 14, + ), + ), + const Spacer(), + Flexible( + child: SettingsDropdown( + key: const Key('_AIModelSelection'), + onChanged: (model) => context + .read() + .add(SettingsAIEvent.selectModel(model)), + selectedOption: state.userProfile.aiModel, + options: _availableModels + .map( + (format) => buildDropdownMenuEntry( + context, + value: format, + label: _titleForAIModel(format), + ), + ) + .toList(), + ), + ), + ], + ), + ); + }, + ); + } +} + +List _availableModels = [ + AIModelPB.DefaultModel, + AIModelPB.Claude3Opus, + AIModelPB.Claude3Sonnet, + AIModelPB.GPT35, + AIModelPB.GPT4o, + AIModelPB.LocalAIModel, +]; + +String _titleForAIModel(AIModelPB model) { + switch (model) { + case AIModelPB.DefaultModel: + return "Default"; + case AIModelPB.Claude3Opus: + return "Claude 3 Opus"; + case AIModelPB.Claude3Sonnet: + return "Claude 3 Sonnet"; + case AIModelPB.GPT35: + return "GPT-3.5"; + case AIModelPB.GPT4o: + return "GPT-4o"; + case AIModelPB.LocalAIModel: + return "Local"; + default: + Log.error("Unknown AI model: $model, fallback to default"); + return "Default"; + } +} 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 new file mode 100644 index 0000000000000..21792d28a04cf --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -0,0 +1,100 @@ +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget { + const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30), + child: FlowyText( + LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(), + maxLines: null, + fontSize: 16, + lineHeight: 1.6, + ), + ); + } +} + +class SettingsAIView extends StatelessWidget { + const SettingsAIView({super.key, required this.userProfile}); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => + SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()), + child: BlocBuilder( + builder: (context, state) { + return SettingsBody( + title: LocaleKeys.settings_aiPage_title.tr(), + description: + LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), + children: const [ + AIModelSelection(), + LocalModelConfig(), + _AISearchToggle(value: false), + ], + ); + }, + ), + ); + } +} + +class _AISearchToggle extends StatelessWidget { + const _AISearchToggle({required this.value}); + + final bool value; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + FlowyText.medium( + LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(), + ), + const Spacer(), + BlocBuilder( + builder: (context, state) { + if (state.aiSettings == null) { + return const Padding( + padding: EdgeInsets.only(top: 6), + child: SizedBox( + height: 26, + width: 26, + child: CircularProgressIndicator.adaptive(), + ), + ); + } else { + return Toggle( + value: state.enableSearchIndexing, + onChanged: (_) => context + .read() + .add(const SettingsAIEvent.toggleAISearch()), + ); + } + }, + ), + ], + ), + ], + ); + } +} diff --git a/frontend/resources/flowy_icons/16x/download_success.svg b/frontend/resources/flowy_icons/16x/download_success.svg new file mode 100644 index 0000000000000..d7cbd38d8262d --- /dev/null +++ b/frontend/resources/flowy_icons/16x/download_success.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/download_warn.svg b/frontend/resources/flowy_icons/16x/download_warn.svg new file mode 100644 index 0000000000000..8f4f3810af79f --- /dev/null +++ b/frontend/resources/flowy_icons/16x/download_warn.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/local_model_download.svg b/frontend/resources/flowy_icons/16x/local_model_download.svg new file mode 100644 index 0000000000000..270bcb25d1fc3 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/local_model_download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs index 32176566fc47c..bece238913696 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs @@ -70,6 +70,7 @@ pub async fn download_plugin( #[cfg(test)] mod test { use super::*; + use crate::local_ai::util::unzip_file; use std::env::temp_dir; #[tokio::test] @@ -99,5 +100,6 @@ mod test { .await .unwrap(); println!("Downloaded plugin to {:?}", path); + unzip_file(&path, &temp_dir).await.unwrap(); } } diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/util.rs b/frontend/rust-lib/flowy-chat/src/local_ai/util.rs index 4859168c75b0f..de338b7c3e883 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/util.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/util.rs @@ -18,10 +18,10 @@ pub fn bytes_to_readable_size(bytes: u64) -> String { format!("{:.2} MB", size_in_mb) } } -async fn unzip_file(zip_path: impl AsRef, destination_path: &Path) -> Result<()> { +pub async fn unzip_file(zip_path: impl AsRef, destination_path: &Path) -> Result<()> { // Remove the destination directory if it exists and create a new one if destination_path.exists() { - remove_dir_all(destination_path)?; + // remove_dir_all(destination_path)?; } create_dir_all(destination_path)?; From d48ed0a420edc3e6cc072e2a47021caea209c4bf Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 14 Jul 2024 12:50:55 +0800 Subject: [PATCH 05/13] chore: unzip file --- .../settings/ai/download_model_bloc.dart | 8 +- .../settings/ai/local_ai_bloc.dart | 36 +- .../settings/ai/local_llm_listener.dart | 55 ++- .../pages/setting_ai_view/downloading.dart | 3 +- .../setting_ai_view/local_ai_config.dart | 96 ++--- frontend/appflowy_tauri/src-tauri/Cargo.lock | 373 ++++++++++++++---- frontend/appflowy_tauri/src-tauri/Cargo.toml | 4 +- .../appflowy_web_app/src-tauri/Cargo.lock | 319 +++++++++++---- .../appflowy_web_app/src-tauri/Cargo.toml | 4 +- frontend/resources/translations/en.json | 6 +- frontend/rust-lib/Cargo.lock | 65 +-- frontend/rust-lib/Cargo.toml | 4 +- .../event-integration-test/tests/util.rs | 2 +- frontend/rust-lib/flowy-chat/Cargo.toml | 7 +- .../rust-lib/flowy-chat/src/chat_manager.rs | 12 +- frontend/rust-lib/flowy-chat/src/entities.rs | 34 +- .../flowy-chat/src/local_ai/llm_resource.rs | 61 ++- .../flowy-chat/src/local_ai/local_llm_chat.rs | 101 +++-- .../rust-lib/flowy-chat/src/local_ai/mod.rs | 3 - .../flowy-chat/src/local_ai/model_request.rs | 2 +- .../flowy-chat/src/local_ai/plugin_request.rs | 105 ----- .../rust-lib/flowy-chat/src/local_ai/util.rs | 53 --- .../src/middleware/chat_service_mw.rs | 14 +- .../rust-lib/flowy-chat/src/notification.rs | 4 +- .../flowy-search/tests/tantivy_test.rs | 4 +- 25 files changed, 872 insertions(+), 503 deletions(-) delete mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs delete mode 100644 frontend/rust-lib/flowy-chat/src/local_ai/util.rs diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart index f26d29ef978e7..782eb0e037099 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart @@ -37,10 +37,10 @@ class DownloadModelBloc extends Bloc { } }, onFinish: () { - emit(state.copyWith(isFinish: true)); + add(const DownloadModelEvent.downloadFinish()); }, onError: (err) { - emit(state.copyWith(downloadError: err)); + // emit(state.copyWith(downloadError: err)); }, ); @@ -62,6 +62,9 @@ class DownloadModelBloc extends Bloc { updatePercent: (String object, double percent) { emit(state.copyWith(object: object, percent: percent)); }, + downloadFinish: () { + emit(state.copyWith(isFinish: true)); + }, ); } } @@ -73,6 +76,7 @@ class DownloadModelEvent with _$DownloadModelEvent { String object, double percent, ) = _UpdatePercent; + const factory DownloadModelEvent.downloadFinish() = _DownloadFinish; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index 11d9c27edaf85..c7c68a0859d8c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; @@ -12,10 +13,23 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'local_ai_bloc.freezed.dart'; class LocalAIConfigBloc extends Bloc { - LocalAIConfigBloc() : super(const LocalAIConfigState()) { + LocalAIConfigBloc() + : listener = LocalLLMListener(), + super(const LocalAIConfigState()) { + listener.start( + stateCallback: (newState) { + if (!isClosed) { + Log.debug('Local LLM State: new state: $newState'); + add(LocalAIConfigEvent.updatellmRunningState(newState)); + } + }, + ); + on(_handleEvent); } + final LocalLLMListener listener; + /// Handles incoming events and dispatches them to the appropriate handler. Future _handleEvent( LocalAIConfigEvent event, @@ -45,6 +59,7 @@ class LocalAIConfigBloc extends Bloc { final result = await ChatEventUpdateLocalLLM(llmModel).send(); result.fold( (llmResource) { + // If all resources are downloaded, show reload plugin if (llmResource.pendingResources.isNotEmpty) { emit( state.copyWith( @@ -61,6 +76,7 @@ class LocalAIConfigBloc extends Bloc { state.copyWith( selectedLLMModel: llmModel, llmModelLoadingState: const LoadingState.finish(), + localAIInfo: const LocalAIInfo.pluginState(), ), ); } @@ -77,18 +93,21 @@ class LocalAIConfigBloc extends Bloc { refreshLLMState: (LocalModelResourcePB llmResource) { if (state.selectedLLMModel == null) { Log.error( - 'Unexpected null selected config. It should be set already'); + 'Unexpected null selected config. It should be set already', + ); return; } + // reload plugin if all resources are downloaded if (llmResource.pendingResources.isEmpty) { emit( state.copyWith( - localAIInfo: const LocalAIInfo.readyToUse(), + localAIInfo: const LocalAIInfo.pluginState(), ), ); } else { if (state.selectedLLMModel != null) { + // Go to download page if the selected model is downloading if (llmResource.isDownloading) { emit( state.copyWith( @@ -123,11 +142,14 @@ class LocalAIConfigBloc extends Bloc { final _ = await ChatEventCancelDownloadLLMResource().send(); _fetchCurremtLLMState(); }, - finishDownload: () { + finishDownload: () async { emit( state.copyWith(localAIInfo: const LocalAIInfo.finishDownload()), ); }, + updatellmRunningState: (RunningStatePB newRunningState) { + emit(state.copyWith(runningState: newRunningState)); + }, ); } @@ -171,6 +193,9 @@ class LocalAIConfigEvent with _$LocalAIConfigEvent { const factory LocalAIConfigEvent.cancelDownload() = _CancelDownload; const factory LocalAIConfigEvent.finishDownload() = _FinishDownload; + const factory LocalAIConfigEvent.updatellmRunningState( + RunningStatePB newRunningState, + ) = _RunningState; } @freezed @@ -182,6 +207,7 @@ class LocalAIConfigState with _$LocalAIConfigState { @Default(LoadingState.loading()) LoadingState llmModelLoadingState, @Default([]) List models, @Default(LoadingState.loading()) LoadingState loadingState, + @Default(RunningStatePB.Connecting) RunningStatePB runningState, }) = _LocalAIConfigState; } @@ -202,5 +228,5 @@ class LocalAIInfo with _$LocalAIInfo { // when start downloading the model const factory LocalAIInfo.downloading(LLMModelPB llmModel) = _Downloading; const factory LocalAIInfo.finishDownload() = _Finish; - const factory LocalAIInfo.readyToUse() = _ReadyToUse; + const factory LocalAIInfo.pluginState() = _PluginState; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart index a646a39474dbc..731eeef3cab2f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart @@ -1 +1,54 @@ -class LocalLLMListener {} +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy/plugins/ai_chat/application/chat_notification.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; +import 'package:appflowy_backend/rust_stream.dart'; +import 'package:appflowy_result/appflowy_result.dart'; + +typedef PluginStateCallback = void Function(RunningStatePB state); + +class LocalLLMListener { + LocalLLMListener() { + _parser = + ChatNotificationParser(id: "appflowy_chat_plugin", callback: _callback); + _subscription = RustStreamReceiver.listen( + (observable) => _parser?.parse(observable), + ); + } + + StreamSubscription? _subscription; + ChatNotificationParser? _parser; + + PluginStateCallback? stateCallback; + void Function()? finishStreamingCallback; + + void start({ + PluginStateCallback? stateCallback, + }) { + this.stateCallback = stateCallback; + } + + void _callback( + ChatNotification ty, + FlowyResult result, + ) { + result.map((r) { + switch (ty) { + case ChatNotification.UpdateChatPluginState: + stateCallback?.call(ChatPluginStatePB.fromBuffer(r).state); + break; + default: + break; + } + }); + } + + Future stop() async { + await _subscription?.cancel(); + _subscription = null; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading.dart index 5c01d1441fe6a..08611542a4bfd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/download_model_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; @@ -97,7 +96,7 @@ class DownloadingProgressBar extends StatelessWidget { fontSize: 11, ), onTap: onCancel, - ) + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart index 64559e9cc9841..14b0d013908c9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart @@ -1,10 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/downloading.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -115,7 +116,7 @@ class _LocalLLMInfoWidget extends StatelessWidget { if (error == null) { // If the error is null, handle selected llm model. if (state.localAIInfo != null) { - return state.localAIInfo!.when( + final child = state.localAIInfo!.when( requestDownload: ( LocalModelResourcePB llmResource, LLMModelPB llmModel, @@ -126,36 +127,30 @@ class _LocalLLMInfoWidget extends StatelessWidget { downloadNeeded: ( LocalModelResourcePB llmResource, LLMModelPB llmModel, - ) { - return Padding( - padding: const EdgeInsets.only(top: 14), - child: _ModelNotExistIndicator( - llmResource: llmResource, - llmModel: llmModel, - ), - ); - }, + ) => + _ModelNotExistIndicator( + llmResource: llmResource, + llmModel: llmModel, + ), downloading: (llmModel) { - return Padding( - padding: const EdgeInsets.only(top: 14), - child: DownloadingIndicator( - key: UniqueKey(), - llmModel: llmModel, - onFinish: () { - context.read().add( - const LocalAIConfigEvent.finishDownload(), - ); - }, - onCancel: () { - context.read().add( - const LocalAIConfigEvent.cancelDownload(), - ); - }, - ), + return DownloadingIndicator( + key: UniqueKey(), + llmModel: llmModel, + onFinish: () => context + .read() + .add(const LocalAIConfigEvent.finishDownload()), + onCancel: () => context + .read() + .add(const LocalAIConfigEvent.cancelDownload()), ); }, - readyToUse: () => const SizedBox.shrink(), - finishDownload: () => const _FinishDownloadIndicator(), + pluginState: () => const PluginStateIndicator(), + finishDownload: () => const InitLocalAIIndicator(), + ); + + return Padding( + padding: const EdgeInsets.only(top: 14), + child: child, ); } else { return const SizedBox.shrink(); @@ -236,15 +231,17 @@ class _LLMModelDownloadDialog extends StatelessWidget { return NavigatorOkCancelDialog( title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr( args: [ - llmResource.pendingResources[0].modelName, - ], - ), - message: LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr( - args: [ - llmResource.pendingResources[0].modelName, - llmResource.pendingResources[0].modelSize.toString(), + llmResource.pendingResources[0].name, ], ), + message: llmResource.pendingResources[0].fileSize == 0 + ? "" + : LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr( + args: [ + llmResource.pendingResources[0].name, + llmResource.pendingResources[0].fileSize.toString(), + ], + ), okTitle: LocaleKeys.button_confirm.tr(), cancelTitle: LocaleKeys.button_cancel.tr(), onOkPressed: onOkPressed, @@ -315,30 +312,3 @@ class _ModelNotExistIndicator extends StatelessWidget { ); } } - -class _FinishDownloadIndicator extends StatelessWidget { - const _FinishDownloadIndicator(); - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: const BoxDecoration(color: Color(0xFFEDF7ED)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - FlowyText( - LocaleKeys.settings_aiPage_keys_downloadModelSuccess.tr(), - fontSize: 11, - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 603714edbc7c2..487eb9b906c97 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -206,22 +206,26 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" dependencies = [ "anyhow", "appflowy-plugin", "bytes", + "reqwest", "serde", "serde_json", "tokio", "tokio-stream", + "tokio-util", "tracing", + "zip 2.1.3", + "zip-extensions", ] [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" dependencies = [ "anyhow", "cfg-if", @@ -235,6 +239,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "xattr 1.3.1", ] [[package]] @@ -264,6 +269,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -378,6 +392,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -443,9 +463,9 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bitpacking" -version = "0.8.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" dependencies = [ "crunchy", ] @@ -555,9 +575,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" @@ -1200,6 +1220,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "convert_case" version = "0.4.0" @@ -1283,11 +1309,26 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1375,7 +1416,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1513,6 +1554,12 @@ dependencies = [ "regex", ] +[[package]] +name = "deflate64" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" + [[package]] name = "delegate-display" version = "2.1.1" @@ -1525,6 +1572,16 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1536,6 +1593,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1660,6 +1728,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -1858,9 +1937,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide 0.7.1", @@ -1911,6 +1990,8 @@ dependencies = [ "tracing", "uuid", "validator", + "zip 2.1.3", + "zip-extensions", ] [[package]] @@ -2521,12 +2602,12 @@ dependencies = [ [[package]] name = "fs4" -version = "0.6.6" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3461,6 +3542,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -3651,7 +3741,7 @@ dependencies = [ "tracing", "validator", "walkdir", - "zip", + "zip 0.6.6", ] [[package]] @@ -3754,6 +3844,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.21" @@ -3778,9 +3874,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.11.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ "hashbrown 0.14.3", ] @@ -3791,6 +3887,16 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "912b45c753ff5f7f5208307e8ace7d2a2e30d024e26d3509f3dce546c044ce15" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "mac" version = "0.1.1" @@ -3914,15 +4020,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.7.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] @@ -4128,6 +4234,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.45" @@ -4156,6 +4268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4368,9 +4481,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "ownedbytes" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" +checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" dependencies = [ "stable_deref_trait", ] @@ -4802,6 +4915,12 @@ dependencies = [ "reqwest", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -4900,7 +5019,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4921,7 +5040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -5157,6 +5276,16 @@ dependencies = [ "getrandom 0.2.10", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -5277,6 +5406,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rend" version = "0.4.0" @@ -5847,9 +5982,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -5923,9 +6058,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simdutf8" @@ -6254,14 +6389,13 @@ dependencies = [ [[package]] name = "tantivy" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2" +checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" dependencies = [ "aho-corasick 1.0.2", "arc-swap", - "async-trait", - "base64 0.21.5", + "base64 0.22.1", "bitpacking", "byteorder", "census", @@ -6269,16 +6403,16 @@ dependencies = [ "crossbeam-channel", "downcast-rs", "fastdivide", + "fnv", "fs4", "htmlescape", - "itertools 0.11.0", + "itertools 0.12.1", "levenshtein_automata", "log", "lru", "lz4_flex", "measure_time", "memmap2", - "murmurhash32", "num_cpus", "once_cell", "oneshot", @@ -6306,22 +6440,22 @@ dependencies = [ [[package]] name = "tantivy-bitpacker" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" +checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" +checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" dependencies = [ + "downcast-rs", "fastdivide", - "fnv", - "itertools 0.11.0", + "itertools 0.12.1", "serde", "tantivy-bitpacker", "tantivy-common", @@ -6331,9 +6465,9 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" +checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" dependencies = [ "async-trait", "byteorder", @@ -6344,50 +6478,52 @@ dependencies = [ [[package]] name = "tantivy-fst" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" dependencies = [ "byteorder", - "regex-syntax 0.6.29", + "regex-syntax 0.8.4", "utf8-ranges", ] [[package]] name = "tantivy-query-grammar" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" +checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" dependencies = [ "nom", ] [[package]] name = "tantivy-sstable" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" +checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" dependencies = [ + "tantivy-bitpacker", "tantivy-common", "tantivy-fst", - "zstd 0.12.4", + "zstd 0.13.2", ] [[package]] name = "tantivy-stacker" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" +checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" dependencies = [ "murmurhash32", + "rand_distr", "tantivy-common", ] [[package]] name = "tantivy-tokenizer-api" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" +checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" dependencies = [ "serde", ] @@ -6464,7 +6600,7 @@ checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" dependencies = [ "filetime", "libc", - "xattr", + "xattr 0.2.3", ] [[package]] @@ -6782,11 +6918,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.22" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ + "deranged", "itoa 1.0.6", + "num-conv", + "powerfmt", "serde", "time-core", "time-macros", @@ -6794,16 +6933,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -6924,8 +7064,12 @@ checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", + "hashbrown 0.14.3", "pin-project-lite", + "slab", "tokio", ] @@ -8141,6 +8285,17 @@ dependencies = [ "libc", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "yrs" version = "0.18.8" @@ -8177,6 +8332,26 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "zip" version = "0.6.6" @@ -8186,7 +8361,7 @@ dependencies = [ "aes", "byteorder", "bzip2", - "constant_time_eq", + "constant_time_eq 0.1.5", "crc32fast", "crossbeam-utils", "flate2", @@ -8197,6 +8372,58 @@ dependencies = [ "zstd 0.11.2+zstd.1.5.2", ] +[[package]] +name = "zip" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq 0.3.0", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap 2.1.0", + "lzma-rs", + "memchr", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd 0.13.2", +] + +[[package]] +name = "zip-extensions" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341" +dependencies = [ + "zip 2.1.3", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" @@ -8208,11 +8435,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.12.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe 6.0.6", + "zstd-safe 7.2.0", ] [[package]] @@ -8227,21 +8454,19 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "6.0.6" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ - "libc", "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index d040b4ad9ed76..e048c224c7db0 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -128,5 +128,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 6e8ca813611d6..d0ed71a8ce820 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -197,22 +197,26 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" dependencies = [ "anyhow", "appflowy-plugin", "bytes", + "reqwest", "serde", "serde_json", "tokio", "tokio-stream", + "tokio-util", "tracing", + "zip 2.1.3", + "zip-extensions", ] [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" dependencies = [ "anyhow", "cfg-if", @@ -226,6 +230,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "xattr", ] [[package]] @@ -254,6 +259,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arboard" version = "3.3.2" @@ -388,6 +402,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -453,9 +473,9 @@ checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bitpacking" -version = "0.8.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" dependencies = [ "crunchy", ] @@ -544,9 +564,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" @@ -1183,6 +1203,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "convert_case" version = "0.4.0" @@ -1279,11 +1305,26 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1365,7 +1406,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.10", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1503,6 +1544,12 @@ dependencies = [ "regex", ] +[[package]] +name = "deflate64" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" + [[package]] name = "delegate-display" version = "2.1.1" @@ -1547,6 +1594,17 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1671,6 +1729,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "dlib" version = "0.5.2" @@ -1898,9 +1967,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1951,6 +2020,8 @@ dependencies = [ "tracing", "uuid", "validator", + "zip 2.1.3", + "zip-extensions", ] [[package]] @@ -2588,12 +2659,12 @@ dependencies = [ [[package]] name = "fs4" -version = "0.6.6" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3554,6 +3625,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -3738,7 +3818,7 @@ dependencies = [ "tracing", "validator", "walkdir", - "zip", + "zip 0.6.6", ] [[package]] @@ -3849,6 +3929,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.21" @@ -3873,9 +3959,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.11.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ "hashbrown 0.14.3", ] @@ -3886,6 +3972,16 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "mac" version = "0.1.1" @@ -3995,15 +4091,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.7.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] @@ -4234,6 +4330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4446,9 +4543,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "ownedbytes" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" +checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" dependencies = [ "stable_deref_trait", ] @@ -4984,7 +5081,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -5005,7 +5102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.55", @@ -5241,6 +5338,16 @@ dependencies = [ "getrandom 0.2.12", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -6361,14 +6468,13 @@ dependencies = [ [[package]] name = "tantivy" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2" +checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" dependencies = [ "aho-corasick", "arc-swap", - "async-trait", - "base64 0.21.7", + "base64 0.22.1", "bitpacking", "byteorder", "census", @@ -6376,16 +6482,16 @@ dependencies = [ "crossbeam-channel", "downcast-rs", "fastdivide", + "fnv", "fs4", "htmlescape", - "itertools 0.11.0", + "itertools 0.12.1", "levenshtein_automata", "log", "lru", "lz4_flex", "measure_time", "memmap2", - "murmurhash32", "num_cpus", "once_cell", "oneshot", @@ -6413,22 +6519,22 @@ dependencies = [ [[package]] name = "tantivy-bitpacker" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" +checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" +checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" dependencies = [ + "downcast-rs", "fastdivide", - "fnv", - "itertools 0.11.0", + "itertools 0.12.1", "serde", "tantivy-bitpacker", "tantivy-common", @@ -6438,9 +6544,9 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" +checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" dependencies = [ "async-trait", "byteorder", @@ -6451,50 +6557,52 @@ dependencies = [ [[package]] name = "tantivy-fst" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" dependencies = [ "byteorder", - "regex-syntax 0.6.29", + "regex-syntax 0.8.2", "utf8-ranges", ] [[package]] name = "tantivy-query-grammar" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" +checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" dependencies = [ "nom", ] [[package]] name = "tantivy-sstable" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" +checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" dependencies = [ + "tantivy-bitpacker", "tantivy-common", "tantivy-fst", - "zstd 0.12.4", + "zstd 0.13.2", ] [[package]] name = "tantivy-stacker" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" +checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" dependencies = [ "murmurhash32", + "rand_distr", "tantivy-common", ] [[package]] name = "tantivy-tokenizer-api" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" +checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" dependencies = [ "serde", ] @@ -6858,18 +6966,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", @@ -6910,9 +7018,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa 1.0.10", @@ -6931,9 +7039,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -7056,8 +7164,12 @@ checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", + "hashbrown 0.14.3", "pin-project-lite", + "slab", "tokio", ] @@ -8484,6 +8596,26 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "zip" version = "0.6.6" @@ -8493,7 +8625,7 @@ dependencies = [ "aes", "byteorder", "bzip2", - "constant_time_eq", + "constant_time_eq 0.1.5", "crc32fast", "crossbeam-utils", "flate2", @@ -8504,6 +8636,58 @@ dependencies = [ "zstd 0.11.2+zstd.1.5.2", ] +[[package]] +name = "zip" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq 0.3.0", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap 2.2.6", + "lzma-rs", + "memchr", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd 0.13.2", +] + +[[package]] +name = "zip-extensions" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341" +dependencies = [ + "zip 2.1.3", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" @@ -8515,11 +8699,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.12.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe 6.0.6", + "zstd-safe 7.2.0", ] [[package]] @@ -8534,19 +8718,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "6.0.6" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ - "libc", "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 1ef5f6d6b7919..44deac61c77c6 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -128,6 +128,6 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 5c5fbd30a5140..18c9f63252e9e 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -616,11 +616,13 @@ "loginToEnableAIFeature": "AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up", "llmModel": "Language Model", "llmModelType": "Language Model Type", - "downloadLLMPrompt": "Download {} local model", + "downloadLLMPrompt": "Download {}", "downloadLLMPromptDetail": "Downloading {} local model will take up to {} of storage. Do you want to continue?", "downloadAIModelButton": "Download AI model", "downloadingModel": "Downloading", - "downloadModelSuccess": "Local AI Model successfully added and ready to use", + "localAILoaded": "Local AI Model successfully added and ready to use", + "localAILoading": "Local AI Model is loading...", + "localAIStopped": "Local AI Model stopped", "title": "AI API Keys", "openAILabel": "OpenAI API key", "openAITooltip": "You can find your Secret API key on the API key page", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 366a5eee87d3d..5adab80113d6b 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -197,22 +197,26 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" dependencies = [ "anyhow", "appflowy-plugin", "bytes", + "reqwest", "serde", "serde_json", "tokio", "tokio-stream", + "tokio-util", "tracing", + "zip 2.1.3", + "zip-extensions", ] [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4ebcd3d00e7a5e24c27a1063f07446bad3c2c087#4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" dependencies = [ "anyhow", "cfg-if", @@ -226,6 +230,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "xattr", ] [[package]] @@ -1246,7 +1251,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1800,6 +1805,7 @@ dependencies = [ "uuid", "validator", "zip 2.1.3", + "zip-extensions", ] [[package]] @@ -3114,15 +3120,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -3978,7 +3975,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", ] @@ -3998,7 +3995,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", ] @@ -4066,19 +4062,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" @@ -4288,7 +4271,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -4309,7 +4292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.47", @@ -5971,8 +5954,12 @@ checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", + "hashbrown 0.14.3", "pin-project-lite", + "slab", "tokio", ] @@ -6859,6 +6846,17 @@ dependencies = [ "tap", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "yrs" version = "0.18.8" @@ -6964,6 +6962,15 @@ dependencies = [ "zstd 0.13.2", ] +[[package]] +name = "zip-extensions" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341" +dependencies = [ + "zip 2.1.3", +] + [[package]] name = "zopfli" version = "0.8.1" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index c2bb3d7184cdc..ba46dfc0e2b2b 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -151,5 +151,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4ebcd3d00e7a5e24c27a1063f07446bad3c2c087" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } diff --git a/frontend/rust-lib/event-integration-test/tests/util.rs b/frontend/rust-lib/event-integration-test/tests/util.rs index 0a63ccaac7ed5..ad1a01bcffc8b 100644 --- a/frontend/rust-lib/event-integration-test/tests/util.rs +++ b/frontend/rust-lib/event-integration-test/tests/util.rs @@ -190,7 +190,7 @@ pub fn zip(src_dir: PathBuf, output_file_path: PathBuf) -> io::Result<()> { .truncate(true) .open(&output_file_path)?; - let options = FileOptions::default().compression_method(CompressionMethod::Deflated); + let options = FileOptions::<()>::default().compression_method(CompressionMethod::Deflated); let mut zip = ZipWriter::new(file); diff --git a/frontend/rust-lib/flowy-chat/Cargo.toml b/frontend/rust-lib/flowy-chat/Cargo.toml index d57b7cc0bd3b6..91bee1a7ff3fa 100644 --- a/frontend/rust-lib/flowy-chat/Cargo.toml +++ b/frontend/rust-lib/flowy-chat/Cargo.toml @@ -32,16 +32,17 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } anyhow = "1.0.86" tokio-stream = "0.1.15" -tokio-util.workspace = true +tokio-util = { workspace = true, features = ["full"] } parking_lot.workspace = true appflowy-local-ai = { version = "0.1.0", features = ["verbose"] } -appflowy-plugin = { version = "0.1.0", features = ["verbose"] } +appflowy-plugin = { version = "0.1.0" } reqwest = "0.11.27" sha2 = "0.10.7" base64 = "0.21.5" futures-util = "0.3.30" md5 = "0.7.0" -zip.workspace = true +zip = { workspace = true, features = ["deflate"] } +zip-extensions = "0.8.0" [dev-dependencies] dotenv = "0.15.0" diff --git a/frontend/rust-lib/flowy-chat/src/chat_manager.rs b/frontend/rust-lib/flowy-chat/src/chat_manager.rs index 4c422e7b9cf88..0f3aea3835275 100644 --- a/frontend/rust-lib/flowy-chat/src/chat_manager.rs +++ b/frontend/rust-lib/flowy-chat/src/chat_manager.rs @@ -1,17 +1,16 @@ use crate::chat::Chat; use crate::entities::{ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB}; -use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting, LocalLLMController}; +use crate::local_ai::local_llm_chat::LocalLLMController; use crate::middleware::chat_service_mw::ChatServiceMiddleware; use crate::persistence::{insert_chat, ChatTable}; -use allo_isolate::Isolate; -use appflowy_local_ai::llm_chat::LocalLLMSetting; + use appflowy_plugin::manager::PluginManager; use dashmap::DashMap; -use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageType, LLMModel}; +use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageType}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; -use lib_infra::isolate_stream::IsolateSink; + use lib_infra::util::timestamp; use std::path::PathBuf; use std::sync::Arc; @@ -29,11 +28,9 @@ pub struct ChatManager { pub chat_service_wm: Arc, pub user_service: Arc, chats: Arc>>, - store_preferences: Arc, pub llm_controller: Arc, } -const LOCAL_AI_SETTING_KEY: &str = "local_ai_setting"; impl ChatManager { pub fn new( cloud_service: Arc, @@ -66,7 +63,6 @@ impl ChatManager { chat_service_wm, user_service, chats: Arc::new(DashMap::new()), - store_preferences, llm_controller, } } diff --git a/frontend/rust-lib/flowy-chat/src/entities.rs b/frontend/rust-lib/flowy-chat/src/entities.rs index 3d3c41a148baf..7af3da8db9c3e 100644 --- a/frontend/rust-lib/flowy-chat/src/entities.rs +++ b/frontend/rust-lib/flowy-chat/src/entities.rs @@ -1,5 +1,6 @@ use crate::local_ai::local_llm_chat::LLMModelInfo; -use appflowy_local_ai::llm_chat::LocalLLMSetting; +use appflowy_plugin::core::plugin::RunningState; + use flowy_chat_pub::cloud::{ ChatMessage, LLMModel, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, }; @@ -356,11 +357,38 @@ pub struct LocalModelResourcePB { #[derive(Default, ProtoBuf, Clone, Debug)] pub struct PendingResourcePB { #[pb(index = 1)] - pub model_name: String, + pub name: String, #[pb(index = 2)] - pub model_size: i64, + pub file_size: i64, #[pb(index = 3)] pub requirements: String, } + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct ChatPluginStatePB { + #[pb(index = 1)] + pub state: RunningStatePB, +} + +#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] +pub enum RunningStatePB { + #[default] + Connecting = 0, + Connected = 1, + Running = 2, + Stopped = 3, +} + +impl From for RunningStatePB { + fn from(value: RunningState) -> Self { + match value { + RunningState::Connecting => RunningStatePB::Connecting, + RunningState::Connected { .. } => RunningStatePB::Connected, + RunningState::Running { .. } => RunningStatePB::Running, + RunningState::Stopped { .. } => RunningStatePB::Stopped, + RunningState::UnexpectedStop { .. } => RunningStatePB::Stopped, + } + } +} diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs index 8f1de4d77be3b..12b8fbcfccbbb 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs @@ -1,30 +1,23 @@ -use reqwest::{Client, Response, StatusCode}; -use sha2::{Digest, Sha256}; - use crate::chat_manager::ChatUserService; -use crate::entities::{LocalModelResourcePB, LocalModelStatePB, PendingResourcePB}; +use crate::entities::{LocalModelResourcePB, PendingResourcePB}; use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting}; use crate::local_ai::model_request::download_model; -use crate::local_ai::plugin_request::download_plugin; -use crate::notification::{send_notification, ChatNotification}; -use anyhow::anyhow; -use appflowy_local_ai::llm_chat::ChatPluginConfig; -use base64::engine::general_purpose::STANDARD; -use base64::Engine; + +use appflowy_local_ai::chat_plugin::ChatPluginConfig; use flowy_chat_pub::cloud::{LLMModel, LocalAIConfig, ModelInfo}; use flowy_error::{FlowyError, FlowyResult}; use futures::Sink; use futures_util::SinkExt; use lib_infra::async_trait::async_trait; -use lib_infra::file_util::unzip_and_replace; use parking_lot::RwLock; -use std::path::{Path, PathBuf}; + +use appflowy_local_ai::plugin_request::download_plugin; +use std::path::PathBuf; use std::sync::Arc; -use tokio::fs::{self, File}; -use tokio::io::SeekFrom; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; +use tokio::fs::{self}; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, instrument, trace}; +use zip_extensions::zip_extract; #[async_trait] pub trait LLMResourceService: Send + Sync + 'static { @@ -59,28 +52,29 @@ impl DownloadTask { } pub struct LLMResourceController { - client: Client, user_service: Arc, resource_service: Arc, llm_setting: RwLock>, // The ai_config will be set when user try to get latest local ai config from server ai_config: RwLock>, download_task: Arc>>, + resource_notify: tokio::sync::mpsc::Sender<()>, } impl LLMResourceController { pub fn new( user_service: Arc, resource_service: impl LLMResourceService, + resource_notify: tokio::sync::mpsc::Sender<()>, ) -> Self { let llm_setting = RwLock::new(resource_service.retrieve()); Self { - client: Client::new(), user_service, resource_service: Arc::new(resource_service), llm_setting, ai_config: Default::default(), download_task: Default::default(), + resource_notify, } } @@ -160,13 +154,17 @@ impl LLMResourceController { let pending_resources: Vec<_> = pending_resources .into_iter() .filter_map(|res| match res { - PendingResource::PluginRes => None, + PendingResource::PluginRes => Some(vec![PendingResourcePB { + name: "AppFlowy Plugin".to_string(), + file_size: 0, + requirements: "".to_string(), + }]), PendingResource::ModelInfoRes(model_infos) => Some( model_infos .into_iter() .map(|model_info| PendingResourcePB { - model_name: model_info.name, - model_size: model_info.file_size, + name: model_info.name, + file_size: model_info.file_size, requirements: model_info.requirements, }) .collect::>(), @@ -226,12 +224,20 @@ impl LLMResourceController { { let task_id = uuid::Uuid::new_v4().to_string(); let weak_download_task = Arc::downgrade(&self.download_task); - + let resource_notify = self.resource_notify.clone(); // notify download progress to client. let progress_notify = |mut rx: tokio::sync::broadcast::Receiver| { tokio::spawn(async move { while let Ok(value) = rx.recv().await { - if value == DOWNLOAD_FINISH { + let is_finish = value == DOWNLOAD_FINISH; + if let Err(err) = progress_sink.send(value).await { + error!("Failed to send progress: {:?}", err); + break; + } + + if is_finish { + info!("notify download finish, need to reload resources"); + let _ = resource_notify.send(()).await; if let Some(download_task) = weak_download_task.upgrade() { if let Some(task) = download_task.write().take() { task.cancel(); @@ -239,11 +245,6 @@ impl LLMResourceController { } break; } - - if let Err(err) = progress_sink.send(value).await { - error!("Failed to send progress: {:?}", err); - break; - } } }); }; @@ -302,7 +303,6 @@ impl LLMResourceController { Some(download_task.cancel_token.clone()), Some(Arc::new(move |downloaded, total_size| { let progress = (downloaded as f64 / total_size as f64).clamp(0.0, 1.0); - trace!("Plugin download progress: {}", progress); let _ = plugin_progress_tx.send(format!("plugin:progress:{}", progress)); })), ) @@ -313,7 +313,7 @@ impl LLMResourceController { "[LLM Resource] unzip {:?} to {:?}", zip_plugin_file, plugin_file_etag_dir ); - unzip_and_replace(&zip_plugin_file, &plugin_file_etag_dir)?; + zip_extract(&zip_plugin_file, &plugin_file_etag_dir)?; // delete zip file info!("[LLM Resource] Delete zip file: {:?}", file_name); @@ -368,6 +368,7 @@ impl LLMResourceController { }, } } + info!("[LLM Resource] All resources downloaded"); download_task.tx.send(DOWNLOAD_FINISH.to_string())?; Ok::<_, anyhow::Error>(()) }); @@ -404,9 +405,7 @@ impl LLMResourceController { .join("chat_plugin"); let chat_model_path = model_dir.join(&llm_setting.llm_model.chat_model.file_name); let embedding_model_path = model_dir.join(&llm_setting.llm_model.embedding_model.file_name); - let mut config = ChatPluginConfig::new(chat_bin_path, chat_model_path)?; - let persist_directory = resource_dir.join("rag"); if !persist_directory.exists() { std::fs::create_dir_all(&persist_directory)?; 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 340721a87f425..9923110e0deb2 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 @@ -1,9 +1,11 @@ use crate::chat_manager::ChatUserService; -use crate::entities::{ChatStatePB, LocalModelResourcePB, LocalModelStatePB, ModelTypePB}; +use crate::entities::{ + ChatPluginStatePB, ChatStatePB, LocalModelResourcePB, ModelTypePB, RunningStatePB, +}; use crate::local_ai::llm_resource::{LLMResourceController, LLMResourceService}; use crate::notification::{send_notification, ChatNotification}; use anyhow::Error; -use appflowy_local_ai::llm_chat::{ChatPluginConfig, LocalChatLLMChat}; +use appflowy_local_ai::chat_plugin::{ChatPluginConfig, LocalChatLLMChat}; use appflowy_plugin::manager::PluginManager; use appflowy_plugin::util::is_apple_silicon; use flowy_chat_pub::cloud::{AppFlowyAIPlugin, ChatCloudService, LLMModel, LocalAIConfig}; @@ -11,11 +13,12 @@ use flowy_error::FlowyResult; use flowy_sqlite::kv::KVStorePreferences; use futures::Sink; use lib_infra::async_trait::async_trait; -use parking_lot::RwLock; + use serde::{Deserialize, Serialize}; use std::ops::Deref; -use std::path::PathBuf; + use std::sync::Arc; +use tokio_stream::StreamExt; use tracing::{error, info, trace}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -53,8 +56,14 @@ impl LocalLLMController { let llm_chat = Arc::new(LocalChatLLMChat::new(plugin_manager)); let mut rx = llm_chat.subscribe_running_state(); tokio::spawn(async move { - while let Ok(state) = rx.recv().await { - info!("[Chat Plugin] state: {:?}", state); + while let Some(state) = rx.next().await { + let new_state = RunningStatePB::from(state); + info!("[Chat Plugin] state: {:?}", new_state); + send_notification( + "appflowy_chat_plugin", + ChatNotification::UpdateChatPluginState, + ) + .payload(ChatPluginStatePB { state: new_state }); } }); @@ -63,7 +72,20 @@ impl LocalLLMController { cloud_service, store_preferences, }; - let llm_res = Arc::new(LLMResourceController::new(user_service, res_impl)); + + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + let llm_res = Arc::new(LLMResourceController::new(user_service, res_impl, tx)); + + let cloned_llm_chat = llm_chat.clone(); + let cloned_llm_res = llm_res.clone(); + tokio::spawn(async move { + while rx.recv().await.is_some() { + if let Ok(chat_config) = cloned_llm_res.get_chat_config() { + initialize_chat_plugin(&cloned_llm_chat, chat_config).unwrap(); + } + } + }); + Self { llm_chat, llm_res } } pub async fn refresh(&self) -> FlowyResult { @@ -71,33 +93,9 @@ impl LocalLLMController { } pub fn initialize(&self) -> FlowyResult<()> { - let mut chat_config = self.llm_res.get_chat_config()?; + let chat_config = self.llm_res.get_chat_config()?; let llm_chat = self.llm_chat.clone(); - tokio::spawn(async move { - trace!("[Chat Plugin] setup local chat: {:?}", chat_config); - if is_apple_silicon().await.unwrap_or(false) { - chat_config = chat_config.with_device("gpu"); - } - match llm_chat.init_chat_plugin(chat_config).await { - Ok(_) => { - send_notification("appflowy_chat_plugin", ChatNotification::ChatStateUpdated).payload( - ChatStatePB { - model_type: ModelTypePB::LocalAI, - available: true, - }, - ); - }, - Err(err) => { - send_notification("appflowy_chat_plugin", ChatNotification::ChatStateUpdated).payload( - ChatStatePB { - model_type: ModelTypePB::LocalAI, - available: false, - }, - ); - error!("[Chat Plugin] failed to setup plugin: {:?}", err); - }, - } - }); + initialize_chat_plugin(&llm_chat, chat_config)?; Ok(()) } @@ -166,6 +164,43 @@ impl LocalLLMController { } } +fn initialize_chat_plugin( + llm_chat: &Arc, + mut chat_config: ChatPluginConfig, +) -> FlowyResult<()> { + let llm_chat = llm_chat.clone(); + tokio::spawn(async move { + trace!("[Chat Plugin] config: {:?}", chat_config); + if is_apple_silicon().await.unwrap_or(false) { + chat_config = chat_config.with_device("gpu"); + } + match llm_chat.init_chat_plugin(chat_config).await { + Ok(_) => { + send_notification( + "appflowy_chat_plugin", + ChatNotification::UpdateChatPluginState, + ) + .payload(ChatStatePB { + model_type: ModelTypePB::LocalAI, + available: true, + }); + }, + Err(err) => { + send_notification( + "appflowy_chat_plugin", + ChatNotification::UpdateChatPluginState, + ) + .payload(ChatStatePB { + model_type: ModelTypePB::LocalAI, + available: false, + }); + error!("[Chat Plugin] failed to setup plugin: {:?}", err); + }, + } + }); + Ok(()) +} + pub struct LLMResourceServiceImpl { user_service: Arc, cloud_service: Arc, diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs b/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs index ca3630bd56ac3..cb94a2dc3e60d 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/mod.rs @@ -1,6 +1,3 @@ pub mod llm_resource; pub mod local_llm_chat; mod model_request; -mod plugin_request; - -mod util; diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs index 47d2c777a0f9d..81f221354f00f 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::fs::{self, File}; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; -use tokio::sync::watch; + use tokio_util::sync::CancellationToken; use tracing::{instrument, trace}; diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs deleted file mode 100644 index bece238913696..0000000000000 --- a/frontend/rust-lib/flowy-chat/src/local_ai/plugin_request.rs +++ /dev/null @@ -1,105 +0,0 @@ -use anyhow::anyhow; -use futures_util::StreamExt; -use md5::Context; -use reqwest::Client; -use sha2::{Digest, Sha256}; -use std::error::Error; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::fs; -use tokio::fs::File; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; -use tokio_util::sync::CancellationToken; -use tracing::trace; - -type ProgressCallback = Arc; - -pub async fn download_plugin( - url: &str, - plugin_dir: &PathBuf, - file_name: &str, - cancel_token: Option, - progress_callback: Option, -) -> Result { - let client = Client::new(); - let response = client.get(url).send().await?; - - if !response.status().is_success() { - return Err(anyhow!("Failed to download file: {}", response.status())); - } - - let total_size = response - .content_length() - .ok_or(anyhow!("Failed to get content length"))?; - - // Create paths for the partial and final files - let partial_path = plugin_dir.join(format!("{}.part", file_name)); - let final_path = plugin_dir.join(file_name); - let mut part_file = File::create(&partial_path).await?; - let mut stream = response.bytes_stream(); - let mut downloaded: u64 = 0; - - while let Some(chunk) = stream.next().await { - if let Some(cancel_token) = &cancel_token { - if cancel_token.is_cancelled() { - trace!("Download canceled"); - fs::remove_file(&partial_path).await?; - return Err(anyhow!("Download canceled")); - } - } - - let bytes = chunk?; - part_file.write_all(&bytes).await?; - downloaded += bytes.len() as u64; - - // Call the progress callback - if let Some(progress_callback) = &progress_callback { - progress_callback(downloaded, total_size); - } - } - - // Ensure all data is written to disk - part_file.sync_all().await?; - - // Move the temporary file to the final destination - fs::rename(&partial_path, &final_path).await?; - trace!("Plugin downloaded to {:?}", final_path); - Ok(final_path) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::local_ai::util::unzip_file; - use std::env::temp_dir; - - #[tokio::test] - async fn download_plugin_test() { - let url = "https://appflowy-local-ai.s3.amazonaws.com/macos-latest/AppFlowyLLM_release.zip?AWSAccessKeyId=AKIAVQA4ULIFKSXHI6PI&Signature=KKXVOOJUG1TSVGZeuJV1MZ4k49o%3D&Expires=1720828964"; - if url.is_empty() { - return; - } - - let progress_callback: ProgressCallback = Arc::new(|downloaded, total_size| { - let progress = (downloaded as f64 / total_size as f64) * 100.0; - println!("Download progress: {:.2}%", progress); - }); - - let temp_dir = temp_dir().join("download_plugin"); - if !temp_dir.exists() { - std::fs::create_dir(&temp_dir).unwrap(); - } - - let path = download_plugin( - url, - &temp_dir, - "AppFlowyLLM.zip", - None, - Some(progress_callback), - ) - .await - .unwrap(); - println!("Downloaded plugin to {:?}", path); - unzip_file(&path, &temp_dir).await.unwrap(); - } -} diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/util.rs b/frontend/rust-lib/flowy-chat/src/local_ai/util.rs deleted file mode 100644 index de338b7c3e883..0000000000000 --- a/frontend/rust-lib/flowy-chat/src/local_ai/util.rs +++ /dev/null @@ -1,53 +0,0 @@ -use anyhow::Result; -use sha2::Digest; -use std::fs; -use std::fs::{create_dir_all, remove_dir_all, File}; -use std::io::{BufReader, Read, Write}; -use std::path::Path; -use zip::ZipArchive; - -pub fn bytes_to_readable_size(bytes: u64) -> String { - const GB: u64 = 1_000_000_000; - const MB: u64 = 1_000_000; - - if bytes >= GB { - let size_in_gb = bytes as f64 / GB as f64; - format!("{:.2} GB", size_in_gb) - } else { - let size_in_mb = bytes as f64 / MB as f64; - format!("{:.2} MB", size_in_mb) - } -} -pub async fn unzip_file(zip_path: impl AsRef, destination_path: &Path) -> Result<()> { - // Remove the destination directory if it exists and create a new one - if destination_path.exists() { - // remove_dir_all(destination_path)?; - } - create_dir_all(destination_path)?; - - // Open the zip file - let file = File::open(zip_path)?; - let mut archive = ZipArchive::new(BufReader::new(file))?; - - // Iterate over each file in the archive and extract it - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - let outpath = destination_path.join(file.name()); - - if file.name().ends_with('/') { - create_dir_all(&outpath)?; - } else { - if let Some(p) = outpath.parent() { - if !p.exists() { - create_dir_all(&p)?; - } - } - let mut outfile = fs::File::create(&outpath)?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - outfile.write_all(&buffer)?; - } - } - - Ok(()) -} diff --git a/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs index 8d1d8b912a210..be205e755bf46 100644 --- a/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs @@ -53,12 +53,14 @@ impl ChatServiceMiddleware { err, PluginError::PluginNotConnected | PluginError::PeerDisconnect ) { - send_notification("appflowy_chat_plugin", ChatNotification::ChatStateUpdated).payload( - ChatStatePB { - model_type: ModelTypePB::LocalAI, - available: false, - }, - ); + send_notification( + "appflowy_chat_plugin", + ChatNotification::UpdateChatPluginState, + ) + .payload(ChatStatePB { + model_type: ModelTypePB::LocalAI, + available: false, + }); } } } diff --git a/frontend/rust-lib/flowy-chat/src/notification.rs b/frontend/rust-lib/flowy-chat/src/notification.rs index c781040642377..98c20a47c3d6c 100644 --- a/frontend/rust-lib/flowy-chat/src/notification.rs +++ b/frontend/rust-lib/flowy-chat/src/notification.rs @@ -12,7 +12,7 @@ pub enum ChatNotification { DidReceiveChatMessage = 3, StreamChatMessageError = 4, FinishStreaming = 5, - ChatStateUpdated = 6, + UpdateChatPluginState = 6, LocalAIResourceNeeded = 7, } @@ -29,7 +29,7 @@ impl std::convert::From for ChatNotification { 3 => ChatNotification::DidReceiveChatMessage, 4 => ChatNotification::StreamChatMessageError, 5 => ChatNotification::FinishStreaming, - 6 => ChatNotification::ChatStateUpdated, + 6 => ChatNotification::UpdateChatPluginState, 7 => ChatNotification::LocalAIResourceNeeded, _ => ChatNotification::Unknown, } diff --git a/frontend/rust-lib/flowy-search/tests/tantivy_test.rs b/frontend/rust-lib/flowy-search/tests/tantivy_test.rs index b07853c7de5fc..f68b06d6109ae 100644 --- a/frontend/rust-lib/flowy-search/tests/tantivy_test.rs +++ b/frontend/rust-lib/flowy-search/tests/tantivy_test.rs @@ -47,7 +47,7 @@ fn search_folder_test() { for (_score, doc_address) in top_docs { // Retrieve the actual content of documents given its `doc_address`. - let retrieved_doc = searcher.doc(doc_address).unwrap(); - println!("{}", schema.to_json(&retrieved_doc)); + let retrieved_doc: TantivyDocument = searcher.doc(doc_address).unwrap(); + println!("{}", retrieved_doc.to_json(&schema)); } } From 86e0b8e8a6256fe0678d639744020c3c67f3bb7b Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 14 Jul 2024 12:51:16 +0800 Subject: [PATCH 06/13] chore: unzip file --- .../settings/ai/init_local_ai_bloc.dart | 37 +++++++++ .../pages/setting_ai_view/init_local_ai.dart | 80 +++++++++++++++++++ .../pages/setting_ai_view/plugin_state.dart | 40 ++++++++++ 3 files changed, 157 insertions(+) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart new file mode 100644 index 0000000000000..dc02c5ff8d802 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'init_local_ai_bloc.freezed.dart'; + +class InitLocalAIBloc extends Bloc { + InitLocalAIBloc() + : super( + const InitLocalAIState(runningState: RunningStatePB.Connecting), + ) { + on(_handleEvent); + } + + Future _handleEvent( + InitLocalAIEvent event, + Emitter emit, + ) async { + await event.when( + started: () async {}, + ); + } +} + +@freezed +class InitLocalAIEvent with _$InitLocalAIEvent { + const factory InitLocalAIEvent.started() = _Started; +} + +@freezed +class InitLocalAIState with _$InitLocalAIState { + const factory InitLocalAIState({ + required RunningStatePB runningState, + }) = _InitLocalAIState; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart new file mode 100644 index 0000000000000..bd99df76d7c51 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart @@ -0,0 +1,80 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/init_local_ai_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class InitLocalAIIndicator extends StatelessWidget { + const InitLocalAIIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + InitLocalAIBloc()..add(const InitLocalAIEvent.started()), + child: DecoratedBox( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: BlocBuilder( + builder: (context, state) { + switch (state.runningState) { + case RunningStatePB.Connecting: + case RunningStatePB.Connected: + return Row( + children: [ + const HSpace(8), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoading.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ); + case RunningStatePB.Running: + return Row( + children: [ + const HSpace(8), + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ); + case RunningStatePB.Stopped: + return Row( + children: [ + const HSpace(8), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStopped.tr(), + fontSize: 11, + color: const Color(0xFFC62828), + ), + ], + ); + default: + return const SizedBox.shrink(); + } + }, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart new file mode 100644 index 0000000000000..bbbb906b5f6b1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -0,0 +1,40 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/widgets.dart'; + +class PluginStateIndicator extends StatelessWidget { + const PluginStateIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: Row( + children: [ + const HSpace(8), + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ), + ), + ); + } +} From 073612f6ccbf45a3ba9e467fd751c8cde3a38318 Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 14 Jul 2024 12:55:35 +0800 Subject: [PATCH 07/13] chore: rename --- .../settings/ai/init_local_ai_bloc.dart | 1 - .../settings/ai/local_ai_bloc.dart | 28 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart index dc02c5ff8d802..70abc24e0bfd7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index c7c68a0859d8c..c801a43c8d819 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -64,7 +64,7 @@ class LocalAIConfigBloc extends Bloc { emit( state.copyWith( selectedLLMModel: llmModel, - localAIInfo: LocalAIInfo.requestDownload( + localAIInfo: LocalAIProgress.requestDownload( llmResource, llmModel, ), @@ -76,7 +76,7 @@ class LocalAIConfigBloc extends Bloc { state.copyWith( selectedLLMModel: llmModel, llmModelLoadingState: const LoadingState.finish(), - localAIInfo: const LocalAIInfo.pluginState(), + localAIInfo: const LocalAIProgress.pluginState(), ), ); } @@ -102,7 +102,7 @@ class LocalAIConfigBloc extends Bloc { if (llmResource.pendingResources.isEmpty) { emit( state.copyWith( - localAIInfo: const LocalAIInfo.pluginState(), + localAIInfo: const LocalAIProgress.pluginState(), ), ); } else { @@ -111,7 +111,7 @@ class LocalAIConfigBloc extends Bloc { if (llmResource.isDownloading) { emit( state.copyWith( - localAIInfo: LocalAIInfo.downloading(state.selectedLLMModel!), + localAIInfo: LocalAIProgress.downloading(state.selectedLLMModel!), llmModelLoadingState: const LoadingState.finish(), ), ); @@ -119,7 +119,7 @@ class LocalAIConfigBloc extends Bloc { } else { emit( state.copyWith( - localAIInfo: LocalAIInfo.downloadNeeded( + localAIInfo: LocalAIProgress.downloadNeeded( llmResource, state.selectedLLMModel!, ), @@ -133,7 +133,7 @@ class LocalAIConfigBloc extends Bloc { startDownloadModel: (LLMModelPB llmModel) { emit( state.copyWith( - localAIInfo: LocalAIInfo.downloading(llmModel), + localAIInfo: LocalAIProgress.downloading(llmModel), llmModelLoadingState: const LoadingState.finish(), ), ); @@ -144,7 +144,7 @@ class LocalAIConfigBloc extends Bloc { }, finishDownload: () async { emit( - state.copyWith(localAIInfo: const LocalAIInfo.finishDownload()), + state.copyWith(localAIInfo: const LocalAIProgress.finishDownload()), ); }, updatellmRunningState: (RunningStatePB newRunningState) { @@ -203,7 +203,7 @@ class LocalAIConfigState with _$LocalAIConfigState { const factory LocalAIConfigState({ LLMModelInfoPB? modelInfo, LLMModelPB? selectedLLMModel, - LocalAIInfo? localAIInfo, + LocalAIProgress? localAIInfo, @Default(LoadingState.loading()) LoadingState llmModelLoadingState, @Default([]) List models, @Default(LoadingState.loading()) LoadingState loadingState, @@ -212,21 +212,21 @@ class LocalAIConfigState with _$LocalAIConfigState { } @freezed -class LocalAIInfo with _$LocalAIInfo { +class LocalAIProgress with _$LocalAIProgress { // when user select a new model, it will call requestDownload - const factory LocalAIInfo.requestDownload( + const factory LocalAIProgress.requestDownload( LocalModelResourcePB llmResource, LLMModelPB llmModel, ) = _RequestDownload; // when user comes back to the setting page, it will auto detect current llm state - const factory LocalAIInfo.downloadNeeded( + const factory LocalAIProgress.downloadNeeded( LocalModelResourcePB llmResource, LLMModelPB llmModel, ) = _DownloadNeeded; // when start downloading the model - const factory LocalAIInfo.downloading(LLMModelPB llmModel) = _Downloading; - const factory LocalAIInfo.finishDownload() = _Finish; - const factory LocalAIInfo.pluginState() = _PluginState; + const factory LocalAIProgress.downloading(LLMModelPB llmModel) = _Downloading; + const factory LocalAIProgress.finishDownload() = _Finish; + const factory LocalAIProgress.pluginState() = _PluginState; } From 652606ab8fa19058eb026258b830085f8567edd1 Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 14 Jul 2024 13:42:45 +0800 Subject: [PATCH 08/13] chore: disable local ai --- .../settings/ai/init_local_ai_bloc.dart | 36 ------ .../settings/ai/local_ai_bloc.dart | 99 ++++++++------- .../settings/ai/local_llm_listener.dart | 2 +- .../settings/ai/plugin_state_bloc.dart | 36 ++++++ .../pages/setting_ai_view/init_local_ai.dart | 113 +++++++++--------- .../setting_ai_view/local_ai_config.dart | 60 +++++----- .../setting_ai_view/model_selection.dart | 2 +- .../pages/setting_ai_view/plugin_state.dart | 37 +++--- frontend/appflowy_tauri/src-tauri/Cargo.lock | 4 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 4 +- .../appflowy_web_app/src-tauri/Cargo.lock | 4 +- .../appflowy_web_app/src-tauri/Cargo.toml | 4 +- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/Cargo.toml | 4 +- frontend/rust-lib/flowy-chat/src/entities.rs | 2 +- .../rust-lib/flowy-chat/src/event_handler.rs | 9 ++ frontend/rust-lib/flowy-chat/src/event_map.rs | 4 + .../flowy-chat/src/local_ai/local_llm_chat.rs | 12 +- 18 files changed, 234 insertions(+), 202 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart deleted file mode 100644 index 70abc24e0bfd7..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/init_local_ai_bloc.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -part 'init_local_ai_bloc.freezed.dart'; - -class InitLocalAIBloc extends Bloc { - InitLocalAIBloc() - : super( - const InitLocalAIState(runningState: RunningStatePB.Connecting), - ) { - on(_handleEvent); - } - - Future _handleEvent( - InitLocalAIEvent event, - Emitter emit, - ) async { - await event.when( - started: () async {}, - ); - } -} - -@freezed -class InitLocalAIEvent with _$InitLocalAIEvent { - const factory InitLocalAIEvent.started() = _Started; -} - -@freezed -class InitLocalAIState with _$InitLocalAIState { - const factory InitLocalAIState({ - required RunningStatePB runningState, - }) = _InitLocalAIState; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index c801a43c8d819..15d91d6b96339 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -12,28 +12,28 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'local_ai_bloc.freezed.dart'; -class LocalAIConfigBloc extends Bloc { - LocalAIConfigBloc() +class LocalAISettingBloc + extends Bloc { + LocalAISettingBloc() : listener = LocalLLMListener(), - super(const LocalAIConfigState()) { + super(const LocalAISettingState()) { listener.start( stateCallback: (newState) { if (!isClosed) { - Log.debug('Local LLM State: new state: $newState'); - add(LocalAIConfigEvent.updatellmRunningState(newState)); + add(LocalAISettingEvent.updateLLMRunningState(newState)); } }, ); - on(_handleEvent); + on(_handleEvent); } final LocalLLMListener listener; /// Handles incoming events and dispatches them to the appropriate handler. Future _handleEvent( - LocalAIConfigEvent event, - Emitter emit, + LocalAISettingEvent event, + Emitter emit, ) async { await event.when( started: _handleStarted, @@ -46,12 +46,12 @@ class LocalAIConfigBloc extends Bloc { modelInfo: modelInfo, models: modelInfo.models, selectedLLMModel: modelInfo.selectedModel, - loadingState: const LoadingState.finish(), + fetchModelInfoState: const LoadingState.finish(), ), ); }, (err) { - emit(state.copyWith(loadingState: LoadingState.finish(error: err))); + emit(state.copyWith(fetchModelInfoState: LoadingState.finish(error: err))); }, ); }, @@ -64,19 +64,19 @@ class LocalAIConfigBloc extends Bloc { emit( state.copyWith( selectedLLMModel: llmModel, - localAIInfo: LocalAIProgress.requestDownload( + localAIInfo: LocalAIProgress.showDownload( llmResource, llmModel, ), - llmModelLoadingState: const LoadingState.finish(), + selectLLMState: const LoadingState.finish(), ), ); } else { emit( state.copyWith( selectedLLMModel: llmModel, - llmModelLoadingState: const LoadingState.finish(), - localAIInfo: const LocalAIProgress.pluginState(), + selectLLMState: const LoadingState.finish(), + localAIInfo: const LocalAIProgress.checkPluginState(), ), ); } @@ -84,7 +84,7 @@ class LocalAIConfigBloc extends Bloc { (err) { emit( state.copyWith( - llmModelLoadingState: LoadingState.finish(error: err), + selectLLMState: LoadingState.finish(error: err), ), ); }, @@ -102,7 +102,7 @@ class LocalAIConfigBloc extends Bloc { if (llmResource.pendingResources.isEmpty) { emit( state.copyWith( - localAIInfo: const LocalAIProgress.pluginState(), + localAIInfo: const LocalAIProgress.checkPluginState(), ), ); } else { @@ -111,19 +111,20 @@ class LocalAIConfigBloc extends Bloc { if (llmResource.isDownloading) { emit( state.copyWith( - localAIInfo: LocalAIProgress.downloading(state.selectedLLMModel!), - llmModelLoadingState: const LoadingState.finish(), + localAIInfo: + LocalAIProgress.startDownloading(state.selectedLLMModel!), + selectLLMState: const LoadingState.finish(), ), ); return; } else { emit( state.copyWith( - localAIInfo: LocalAIProgress.downloadNeeded( + localAIInfo: LocalAIProgress.showDownload( llmResource, state.selectedLLMModel!, ), - llmModelLoadingState: const LoadingState.finish(), + selectLLMState: const LoadingState.finish(), ), ); } @@ -133,8 +134,8 @@ class LocalAIConfigBloc extends Bloc { startDownloadModel: (LLMModelPB llmModel) { emit( state.copyWith( - localAIInfo: LocalAIProgress.downloading(llmModel), - llmModelLoadingState: const LoadingState.finish(), + localAIInfo: LocalAIProgress.startDownloading(llmModel), + selectLLMState: const LoadingState.finish(), ), ); }, @@ -147,8 +148,17 @@ class LocalAIConfigBloc extends Bloc { state.copyWith(localAIInfo: const LocalAIProgress.finishDownload()), ); }, - updatellmRunningState: (RunningStatePB newRunningState) { - emit(state.copyWith(runningState: newRunningState)); + updateLLMRunningState: (RunningStatePB newRunningState) { + if (newRunningState == RunningStatePB.Stopped) { + emit( + state.copyWith( + runningState: newRunningState, + localAIInfo: const LocalAIProgress.checkPluginState(), + ), + ); + } else { + emit(state.copyWith(runningState: newRunningState)); + } }, ); } @@ -158,7 +168,7 @@ class LocalAIConfigBloc extends Bloc { result.fold( (llmResource) { if (!isClosed) { - add(LocalAIConfigEvent.refreshLLMState(llmResource)); + add(LocalAISettingEvent.refreshLLMState(llmResource)); } }, (err) { @@ -171,62 +181,63 @@ class LocalAIConfigBloc extends Bloc { Future _handleStarted() async { final result = await ChatEventRefreshLocalAIModelInfo().send(); if (!isClosed) { - add(LocalAIConfigEvent.didLoadModelInfo(result)); + add(LocalAISettingEvent.didLoadModelInfo(result)); } } } @freezed -class LocalAIConfigEvent with _$LocalAIConfigEvent { - const factory LocalAIConfigEvent.started() = _Started; - const factory LocalAIConfigEvent.didLoadModelInfo( +class LocalAISettingEvent with _$LocalAISettingEvent { + const factory LocalAISettingEvent.started() = _Started; + const factory LocalAISettingEvent.didLoadModelInfo( FlowyResult result, ) = _ModelInfo; - const factory LocalAIConfigEvent.selectLLMConfig(LLMModelPB config) = + const factory LocalAISettingEvent.selectLLMConfig(LLMModelPB config) = _SelectLLMConfig; - const factory LocalAIConfigEvent.refreshLLMState( + const factory LocalAISettingEvent.refreshLLMState( LocalModelResourcePB llmResource, ) = _RefreshLLMResource; - const factory LocalAIConfigEvent.startDownloadModel(LLMModelPB llmModel) = + const factory LocalAISettingEvent.startDownloadModel(LLMModelPB llmModel) = _StartDownloadModel; - const factory LocalAIConfigEvent.cancelDownload() = _CancelDownload; - const factory LocalAIConfigEvent.finishDownload() = _FinishDownload; - const factory LocalAIConfigEvent.updatellmRunningState( + const factory LocalAISettingEvent.cancelDownload() = _CancelDownload; + const factory LocalAISettingEvent.finishDownload() = _FinishDownload; + const factory LocalAISettingEvent.updateLLMRunningState( RunningStatePB newRunningState, ) = _RunningState; } @freezed -class LocalAIConfigState with _$LocalAIConfigState { - const factory LocalAIConfigState({ +class LocalAISettingState with _$LocalAISettingState { + const factory LocalAISettingState({ LLMModelInfoPB? modelInfo, LLMModelPB? selectedLLMModel, LocalAIProgress? localAIInfo, - @Default(LoadingState.loading()) LoadingState llmModelLoadingState, + @Default(LoadingState.loading()) LoadingState fetchModelInfoState, + @Default(LoadingState.loading()) LoadingState selectLLMState, @Default([]) List models, - @Default(LoadingState.loading()) LoadingState loadingState, @Default(RunningStatePB.Connecting) RunningStatePB runningState, - }) = _LocalAIConfigState; + }) = _LocalAISettingState; } @freezed class LocalAIProgress with _$LocalAIProgress { // when user select a new model, it will call requestDownload - const factory LocalAIProgress.requestDownload( + const factory LocalAIProgress.requestDownloadInfo( LocalModelResourcePB llmResource, LLMModelPB llmModel, ) = _RequestDownload; // when user comes back to the setting page, it will auto detect current llm state - const factory LocalAIProgress.downloadNeeded( + const factory LocalAIProgress.showDownload( LocalModelResourcePB llmResource, LLMModelPB llmModel, ) = _DownloadNeeded; // when start downloading the model - const factory LocalAIProgress.downloading(LLMModelPB llmModel) = _Downloading; + const factory LocalAIProgress.startDownloading(LLMModelPB llmModel) = + _Downloading; const factory LocalAIProgress.finishDownload() = _Finish; - const factory LocalAIProgress.pluginState() = _PluginState; + const factory LocalAIProgress.checkPluginState() = _PluginState; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart index 731eeef3cab2f..f68d14abedc3b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart @@ -39,7 +39,7 @@ class LocalLLMListener { result.map((r) { switch (ty) { case ChatNotification.UpdateChatPluginState: - stateCallback?.call(ChatPluginStatePB.fromBuffer(r).state); + stateCallback?.call(PluginStatePB.fromBuffer(r).state); break; default: break; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart new file mode 100644 index 0000000000000..e4f61b8e99a51 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'plugin_state_bloc.freezed.dart'; + +class PluginStateBloc extends Bloc { + PluginStateBloc() + : super( + const PluginState(runningState: RunningStatePB.Connecting), + ) { + on(_handleEvent); + } + + Future _handleEvent( + PluginStateEvent event, + Emitter emit, + ) async { + await event.when( + started: () async {}, + ); + } +} + +@freezed +class PluginStateEvent with _$PluginStateEvent { + const factory PluginStateEvent.started() = _Started; +} + +@freezed +class PluginState with _$PluginState { + const factory PluginState({ + required RunningStatePB runningState, + }) = _PluginState; +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart index bd99df76d7c51..7e0ca50092c80 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart @@ -1,6 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/init_local_ai_bloc.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -14,65 +13,61 @@ class InitLocalAIIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - InitLocalAIBloc()..add(const InitLocalAIEvent.started()), - child: DecoratedBox( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all( - Radius.circular(4), - ), + return DecoratedBox( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all( + Radius.circular(4), ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: BlocBuilder( - builder: (context, state) { - switch (state.runningState) { - case RunningStatePB.Connecting: - case RunningStatePB.Connected: - return Row( - children: [ - const HSpace(8), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAILoading.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - ), - ], - ); - case RunningStatePB.Running: - return Row( - children: [ - const HSpace(8), - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - ), - ], - ); - case RunningStatePB.Stopped: - return Row( - children: [ - const HSpace(8), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStopped.tr(), - fontSize: 11, - color: const Color(0xFFC62828), - ), - ], - ); - default: - return const SizedBox.shrink(); - } - }, - ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: BlocBuilder( + builder: (context, state) { + switch (state.runningState) { + case RunningStatePB.Connecting: + case RunningStatePB.Connected: + return Row( + children: [ + const HSpace(8), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoading.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ); + case RunningStatePB.Running: + return Row( + children: [ + const HSpace(8), + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ); + case RunningStatePB.Stopped: + return Row( + children: [ + const HSpace(8), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStopped.tr(), + fontSize: 11, + color: const Color(0xFFC62828), + ), + ], + ); + default: + return const SizedBox.shrink(); + } + }, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart index 14b0d013908c9..c011b379e68f0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart @@ -34,7 +34,7 @@ class LocalModelConfig extends StatelessWidget { return BlocProvider( create: (context) => - LocalAIConfigBloc()..add(const LocalAIConfigEvent.started()), + LocalAISettingBloc()..add(const LocalAISettingEvent.started()), child: Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Column( @@ -49,9 +49,9 @@ class LocalModelConfig extends StatelessWidget { ), ), const Spacer(), - BlocBuilder( + BlocBuilder( builder: (context, state) { - return state.loadingState.when( + return state.fetchModelInfoState.when( loading: () => const CircularProgressIndicator.adaptive(), finish: (err) { @@ -79,13 +79,13 @@ class _SelectLocalModelDropdownMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return Flexible( child: SettingsDropdown( key: const Key('_SelectLocalModelDropdownMenu'), - onChanged: (model) => context.read().add( - LocalAIConfigEvent.selectLLMConfig(model), + onChanged: (model) => context.read().add( + LocalAISettingEvent.selectLLMConfig(model), ), selectedOption: state.selectedLLMModel!, options: state.models @@ -110,42 +110,42 @@ class _LocalLLMInfoWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { final error = errorFromState(state); if (error == null) { // If the error is null, handle selected llm model. if (state.localAIInfo != null) { final child = state.localAIInfo!.when( - requestDownload: ( + requestDownloadInfo: ( LocalModelResourcePB llmResource, LLMModelPB llmModel, ) { _showDownloadDialog(context, llmResource, llmModel); return const SizedBox.shrink(); }, - downloadNeeded: ( + showDownload: ( LocalModelResourcePB llmResource, LLMModelPB llmModel, ) => - _ModelNotExistIndicator( + _ShowDownloadIndicator( llmResource: llmResource, llmModel: llmModel, ), - downloading: (llmModel) { + startDownloading: (llmModel) { return DownloadingIndicator( key: UniqueKey(), llmModel: llmModel, onFinish: () => context - .read() - .add(const LocalAIConfigEvent.finishDownload()), + .read() + .add(const LocalAISettingEvent.finishDownload()), onCancel: () => context - .read() - .add(const LocalAIConfigEvent.cancelDownload()), + .read() + .add(const LocalAISettingEvent.cancelDownload()), ); }, - pluginState: () => const PluginStateIndicator(), finishDownload: () => const InitLocalAIIndicator(), + checkPluginState: () => const CheckPluginStateIndicator(), ); return Padding( @@ -180,15 +180,15 @@ class _LocalLLMInfoWidget extends StatelessWidget { return _LLMModelDownloadDialog( llmResource: llmResource, onOkPressed: () { - context.read().add( - LocalAIConfigEvent.startDownloadModel( + context.read().add( + LocalAISettingEvent.startDownloadModel( llmModel, ), ); }, onCancelPressed: () { - context.read().add( - const LocalAIConfigEvent.cancelDownload(), + context.read().add( + const LocalAISettingEvent.cancelDownload(), ); }, ); @@ -199,14 +199,14 @@ class _LocalLLMInfoWidget extends StatelessWidget { ); } - FlowyError? errorFromState(LocalAIConfigState state) { - final err = state.loadingState.when( + FlowyError? errorFromState(LocalAISettingState state) { + final err = state.fetchModelInfoState.when( loading: () => null, finish: (err) => err, ); if (err == null) { - state.llmModelLoadingState.when( + state.selectLLMState.when( loading: () => null, finish: (err) => err, ); @@ -251,8 +251,8 @@ class _LLMModelDownloadDialog extends StatelessWidget { } } -class _ModelNotExistIndicator extends StatelessWidget { - const _ModelNotExistIndicator({ +class _ShowDownloadIndicator extends StatelessWidget { + const _ShowDownloadIndicator({ required this.llmResource, required this.llmModel, }); @@ -261,7 +261,7 @@ class _ModelNotExistIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return Row( children: [ @@ -288,15 +288,15 @@ class _ModelNotExistIndicator extends StatelessWidget { return _LLMModelDownloadDialog( llmResource: llmResource, onOkPressed: () { - context.read().add( - LocalAIConfigEvent.startDownloadModel( + context.read().add( + LocalAISettingEvent.startDownloadModel( llmModel, ), ); }, onCancelPressed: () { - context.read().add( - const LocalAIConfigEvent.cancelDownload(), + context.read().add( + const LocalAISettingEvent.cancelDownload(), ); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index d37698faa7df1..4a1115e8e1bff 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -61,7 +61,7 @@ List _availableModels = [ AIModelPB.Claude3Sonnet, AIModelPB.GPT35, AIModelPB.GPT4o, - AIModelPB.LocalAIModel, + // AIModelPB.LocalAIModel, ]; String _titleForAIModel(AIModelPB model) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart index bbbb906b5f6b1..66c000b892097 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -1,12 +1,14 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -class PluginStateIndicator extends StatelessWidget { - const PluginStateIndicator({super.key}); +class CheckPluginStateIndicator extends StatelessWidget { + const CheckPluginStateIndicator({super.key}); @override Widget build(BuildContext context) { @@ -19,20 +21,23 @@ class PluginStateIndicator extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: Row( - children: [ - const HSpace(8), - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), - fontSize: 11, - color: const Color(0xFF1E4620), - ), - ], + child: BlocProvider( + create: (context) => PluginStateBloc(), + child: Row( + children: [ + const HSpace(8), + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ), ), ), ); diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 487eb9b906c97..6d659028baeb2 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -206,7 +206,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" dependencies = [ "anyhow", "appflowy-plugin", @@ -225,7 +225,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index e048c224c7db0..f997401267869 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -128,5 +128,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index d0ed71a8ce820..9c2b95facc32c 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -197,7 +197,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" dependencies = [ "anyhow", "appflowy-plugin", @@ -216,7 +216,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 44deac61c77c6..af4547f3ce501 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -128,6 +128,6 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 5adab80113d6b..675417e64c02e 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -197,7 +197,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" dependencies = [ "anyhow", "appflowy-plugin", @@ -216,7 +216,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=ce9326c48d7a314cec6b8db3b050dfa7b0b66985#ce9326c48d7a314cec6b8db3b050dfa7b0b66985" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index ba46dfc0e2b2b..1e5585e617172 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -151,5 +151,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "ce9326c48d7a314cec6b8db3b050dfa7b0b66985" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } diff --git a/frontend/rust-lib/flowy-chat/src/entities.rs b/frontend/rust-lib/flowy-chat/src/entities.rs index 7af3da8db9c3e..67b93c8f64a31 100644 --- a/frontend/rust-lib/flowy-chat/src/entities.rs +++ b/frontend/rust-lib/flowy-chat/src/entities.rs @@ -367,7 +367,7 @@ pub struct PendingResourcePB { } #[derive(Default, ProtoBuf, Clone, Debug)] -pub struct ChatPluginStatePB { +pub struct PluginStatePB { #[pb(index = 1)] pub state: RunningStatePB, } diff --git a/frontend/rust-lib/flowy-chat/src/event_handler.rs b/frontend/rust-lib/flowy-chat/src/event_handler.rs index 8b3dbbed547ca..31c6abbf6e8b4 100644 --- a/frontend/rust-lib/flowy-chat/src/event_handler.rs +++ b/frontend/rust-lib/flowy-chat/src/event_handler.rs @@ -222,3 +222,12 @@ pub(crate) async fn cancel_download_llm_resource_handler( chat_manager.llm_controller.cancel_download()?; Ok(()) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_plugin_state_handler( + chat_manager: AFPluginState>, +) -> DataResult { + let chat_manager = upgrade_chat_manager(chat_manager)?; + let state = chat_manager.llm_controller.get_plugin_state(); + data_result_ok(state) +} diff --git a/frontend/rust-lib/flowy-chat/src/event_map.rs b/frontend/rust-lib/flowy-chat/src/event_map.rs index 98ee1831f0bca..72ac807b7dc81 100644 --- a/frontend/rust-lib/flowy-chat/src/event_map.rs +++ b/frontend/rust-lib/flowy-chat/src/event_map.rs @@ -40,6 +40,7 @@ pub fn init(chat_manager: Weak) -> AFPlugin { ChatEvent::CancelDownloadLLMResource, cancel_download_llm_resource_handler, ) + .event(ChatEvent::GetPluginState, get_plugin_state_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -87,4 +88,7 @@ pub enum ChatEvent { #[event()] CancelDownloadLLMResource = 13, + + #[event(output = "PluginStatePB")] + GetPluginState = 14, } 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 9923110e0deb2..7228990314372 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 @@ -1,6 +1,6 @@ use crate::chat_manager::ChatUserService; use crate::entities::{ - ChatPluginStatePB, ChatStatePB, LocalModelResourcePB, ModelTypePB, RunningStatePB, + ChatStatePB, LocalModelResourcePB, ModelTypePB, PluginStatePB, RunningStatePB, }; use crate::local_ai::llm_resource::{LLMResourceController, LLMResourceService}; use crate::notification::{send_notification, ChatNotification}; @@ -63,7 +63,8 @@ impl LocalLLMController { "appflowy_chat_plugin", ChatNotification::UpdateChatPluginState, ) - .payload(ChatPluginStatePB { state: new_state }); + .payload(PluginStatePB { state: new_state }) + .send(); } }); @@ -162,6 +163,13 @@ impl LocalLLMController { self.llm_res.cancel_download()?; Ok(()) } + + pub fn get_plugin_state(&self) -> PluginStatePB { + let state = self.llm_chat.get_plugin_running_state(); + PluginStatePB { + state: RunningStatePB::from(state), + } + } } fn initialize_chat_plugin( From cd06f62e8602fb9184c744ac931bdb7bccd38fc9 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 15 Jul 2024 09:39:04 +0800 Subject: [PATCH 09/13] chore: fmt --- .../settings/pages/setting_ai_view/settings_ai_view.dart | 9 +++++---- .../src/af_cloud/impls/user/cloud_service_impl.rs | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) 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 21792d28a04cf..2b8023f35c1ba 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 @@ -44,10 +44,11 @@ class SettingsAIView extends StatelessWidget { title: LocaleKeys.settings_aiPage_title.tr(), description: LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), - children: const [ - AIModelSelection(), - LocalModelConfig(), - _AISearchToggle(value: false), + children: [ + const AIModelSelection(), + if (state.aiSettings!.aiModel == AIModelPB.LocalAIModel) + const LocalModelConfig(), + const _AISearchToggle(value: false), ], ); }, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index f29dc52934e5c..055db1ed946ce 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -543,7 +543,9 @@ where let try_get_client = self.server.try_get_client(); FutureResult::new(async move { let client = try_get_client?; - client.cancel_subscription(&workspace_id, &SubscriptionPlan::Pro).await?; // TODO: + client + .cancel_subscription(&workspace_id, &SubscriptionPlan::Pro) + .await?; // TODO: Ok(()) }) } From 2cd5d7e07d3138e7bc9e64abcb568adef2cc6f1e Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 15 Jul 2024 10:25:33 +0800 Subject: [PATCH 10/13] chore: fix warning --- .../pages/setting_ai_view/model_selection.dart | 2 +- .../setting_ai_view/settings_ai_view.dart | 18 ++++++++++++------ .../rust-lib/flowy-chat/src/chat_manager.rs | 8 ++++---- .../flowy-chat/src/local_ai/llm_resource.rs | 7 +++++-- .../flowy-chat/src/local_ai/local_llm_chat.rs | 18 +++++++++--------- .../flowy-chat/src/local_ai/model_request.rs | 6 ++---- frontend/rust-lib/flowy-error/src/errors.rs | 2 +- .../af_cloud/impls/user/cloud_service_impl.rs | 6 +----- 8 files changed, 35 insertions(+), 32 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 4a1115e8e1bff..d37698faa7df1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -61,7 +61,7 @@ List _availableModels = [ AIModelPB.Claude3Sonnet, AIModelPB.GPT35, AIModelPB.GPT4o, - // AIModelPB.LocalAIModel, + AIModelPB.LocalAIModel, ]; String _titleForAIModel(AIModelPB model) { 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 2b8023f35c1ba..e156bbd696aa1 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 @@ -40,16 +40,22 @@ class SettingsAIView extends StatelessWidget { SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()), child: BlocBuilder( builder: (context, state) { + final children = [ + const AIModelSelection(), + ]; + + if (state.aiSettings != null && + state.aiSettings!.aiModel == AIModelPB.LocalAIModel) { + children.add(const LocalModelConfig()); + } + + children.add(const _AISearchToggle(value: false)); + return SettingsBody( title: LocaleKeys.settings_aiPage_title.tr(), description: LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), - children: [ - const AIModelSelection(), - if (state.aiSettings!.aiModel == AIModelPB.LocalAIModel) - const LocalModelConfig(), - const _AISearchToggle(value: false), - ], + children: children, ); }, ), diff --git a/frontend/rust-lib/flowy-chat/src/chat_manager.rs b/frontend/rust-lib/flowy-chat/src/chat_manager.rs index 0f3aea3835275..11c27f74c9368 100644 --- a/frontend/rust-lib/flowy-chat/src/chat_manager.rs +++ b/frontend/rust-lib/flowy-chat/src/chat_manager.rs @@ -48,7 +48,7 @@ impl ChatManager { if llm_controller.is_ready() { if let Err(err) = llm_controller.initialize() { - error!("[Chat Plugin] failed to initialize local ai: {:?}", err); + error!("[AI Plugin] failed to initialize local ai: {:?}", err); } } @@ -78,7 +78,7 @@ impl ChatManager { )) }); - trace!("[Chat Plugin] notify open chat: {}", chat_id); + trace!("[AI Plugin] notify open chat: {}", chat_id); self.llm_controller.open_chat(chat_id); Ok(()) } @@ -87,7 +87,7 @@ impl ChatManager { trace!("close chat: {}", chat_id); if self.llm_controller.is_ready() { - info!("[Chat Plugin] notify close chat: {}", chat_id); + info!("[AI Plugin] notify close chat: {}", chat_id); self.llm_controller.close_chat(chat_id); } Ok(()) @@ -98,7 +98,7 @@ impl ChatManager { chat.close(); if self.llm_controller.is_ready() { - info!("[Chat Plugin] notify close chat: {}", chat_id); + info!("[AI Plugin] notify close chat: {}", chat_id); self.llm_controller.close_chat(chat_id); } } diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs index 389025fc4fed6..c1a7b24d443ea 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs @@ -385,7 +385,8 @@ impl LLMResourceController { Ok(()) } - pub fn get_chat_config(&self) -> FlowyResult { + #[instrument(level = "debug", skip_all, err)] + pub fn get_ai_plugin_config(&self) -> FlowyResult { if !self.is_ready() { return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); } @@ -429,7 +430,9 @@ impl LLMResourceController { .get_local_ai_config() .await .map_err(|err| { - FlowyError::local_ai().with_context(format!("Can't retrieve model info: {}", err)) + error!("[LLM Resource] Failed to fetch local ai config: {:?}", err); + FlowyError::local_ai() + .with_context("Can't retrieve model info. Please try again later".to_string()) }) } 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 7228990314372..cc12f7ebc3cf1 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 @@ -58,7 +58,7 @@ impl LocalLLMController { tokio::spawn(async move { while let Some(state) = rx.next().await { let new_state = RunningStatePB::from(state); - info!("[Chat Plugin] state: {:?}", new_state); + info!("[AI Plugin] state: {:?}", new_state); send_notification( "appflowy_chat_plugin", ChatNotification::UpdateChatPluginState, @@ -81,7 +81,7 @@ impl LocalLLMController { let cloned_llm_res = llm_res.clone(); tokio::spawn(async move { while rx.recv().await.is_some() { - if let Ok(chat_config) = cloned_llm_res.get_chat_config() { + if let Ok(chat_config) = cloned_llm_res.get_ai_plugin_config() { initialize_chat_plugin(&cloned_llm_chat, chat_config).unwrap(); } } @@ -94,7 +94,7 @@ impl LocalLLMController { } pub fn initialize(&self) -> FlowyResult<()> { - let chat_config = self.llm_res.get_chat_config()?; + let chat_config = self.llm_res.get_ai_plugin_config()?; let llm_chat = self.llm_chat.clone(); initialize_chat_plugin(&llm_chat, chat_config)?; Ok(()) @@ -115,7 +115,7 @@ impl LocalLLMController { tokio::spawn(async move { if let Some(ctrl) = weak_ctrl.upgrade() { if let Err(err) = ctrl.create_chat(&chat_id).await { - error!("[Chat Plugin] failed to open chat: {:?}", err); + error!("[AI Plugin] failed to open chat: {:?}", err); } } }); @@ -127,7 +127,7 @@ impl LocalLLMController { tokio::spawn(async move { if let Some(ctrl) = weak_ctrl.upgrade() { if let Err(err) = ctrl.close_chat(&chat_id).await { - error!("[Chat Plugin] failed to close chat: {:?}", err); + error!("[AI Plugin] failed to close chat: {:?}", err); } } }); @@ -136,8 +136,8 @@ impl LocalLLMController { pub async fn use_local_llm(&self, llm_id: i64) -> FlowyResult { let llm_chat = self.llm_chat.clone(); match llm_chat.destroy_chat_plugin().await { - Ok(_) => info!("[Chat Plugin] destroy plugin successfully"), - Err(err) => error!("[Chat Plugin] failed to destroy plugin: {:?}", err), + Ok(_) => info!("[AI Plugin] destroy plugin successfully"), + Err(err) => error!("[AI Plugin] failed to destroy plugin: {:?}", err), } let state = self.llm_res.use_local_llm(llm_id)?; // Re-initialize the plugin if the setting is updated and ready to use @@ -178,7 +178,7 @@ fn initialize_chat_plugin( ) -> FlowyResult<()> { let llm_chat = llm_chat.clone(); tokio::spawn(async move { - trace!("[Chat Plugin] config: {:?}", chat_config); + trace!("[AI Plugin] config: {:?}", chat_config); if is_apple_silicon().await.unwrap_or(false) { chat_config = chat_config.with_device("gpu"); } @@ -202,7 +202,7 @@ fn initialize_chat_plugin( model_type: ModelTypePB::LocalAI, available: false, }); - error!("[Chat Plugin] failed to setup plugin: {:?}", err); + error!("[AI Plugin] failed to setup plugin: {:?}", err); }, } }); diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs b/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs index 81f221354f00f..1692b6ce57b27 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/model_request.rs @@ -50,10 +50,8 @@ pub async fn download_model( let header_sha256 = response .headers() .get("SHA256") - .map(|value| value.to_str().ok()) - .flatten() - .map(|value| STANDARD.decode(value).ok()) - .flatten(); + .and_then(|value| value.to_str().ok()) + .and_then(|value| STANDARD.decode(value).ok()); part_file.seek(tokio::io::SeekFrom::Start(0)).await?; let mut hasher = Sha256::new(); diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 349c07fa59ed3..a442a224caa20 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -43,7 +43,7 @@ impl FlowyError { } } pub fn with_context(mut self, error: T) -> Self { - self.msg = format!("{}", error.to_string()); + self.msg = format!("{}", error); self } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 055db1ed946ce..4cd25af7d62c6 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -727,11 +727,7 @@ fn to_workspace_subscription_plan( fn to_workspace_subscription(s: WorkspaceSubscriptionStatus) -> WorkspaceSubscription { WorkspaceSubscription { workspace_id: s.workspace_id, - subscription_plan: match s.workspace_plan { - // WorkspaceSubscriptionPlan::Pro => flowy_user_pub::entities::SubscriptionPlan::Pro, - // WorkspaceSubscriptionPlan::Team => flowy_user_pub::entities::SubscriptionPlan::Team, - _ => flowy_user_pub::entities::SubscriptionPlan::None, - }, + subscription_plan: flowy_user_pub::entities::SubscriptionPlan::None, recurring_interval: match s.recurring_interval { client_api::entity::billing_dto::RecurringInterval::Month => { flowy_user_pub::entities::RecurringInterval::Month From a457aa6172b39ff11619e261b3fc212a55ce5cdc Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 15 Jul 2024 10:55:50 +0800 Subject: [PATCH 11/13] chore: update --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 4 ++-- frontend/appflowy_tauri/src-tauri/Cargo.toml | 4 ++-- .../appflowy_web_app/src-tauri/Cargo.lock | 4 ++-- .../appflowy_web_app/src-tauri/Cargo.toml | 4 ++-- frontend/rust-lib/Cargo.lock | 4 ++-- frontend/rust-lib/Cargo.toml | 4 ++-- .../rust-lib/flowy-chat/src/chat_manager.rs | 24 +++++++++---------- .../rust-lib/flowy-chat/src/event_handler.rs | 15 +++++++----- .../flowy-chat/src/local_ai/llm_resource.rs | 6 ++--- .../flowy-chat/src/local_ai/local_llm_chat.rs | 10 ++++---- .../src/middleware/chat_service_mw.rs | 6 ++--- 11 files changed, 44 insertions(+), 41 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index af019e6067475..c77572fcac59e 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -206,7 +206,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b" dependencies = [ "anyhow", "appflowy-plugin", @@ -225,7 +225,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 1510239ac53d7..8ba790b3a9630 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -128,5 +128,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 94dd43e565f32..6f2f64dcde462 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -197,7 +197,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b" dependencies = [ "anyhow", "appflowy-plugin", @@ -216,7 +216,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 59ca2de731a76..e71778dcd4c86 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -128,6 +128,6 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index b68eb23dffc71..fb17ab7072352 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -197,7 +197,7 @@ dependencies = [ [[package]] name = "appflowy-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b" dependencies = [ "anyhow", "appflowy-plugin", @@ -216,7 +216,7 @@ dependencies = [ [[package]] name = "appflowy-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=aaf1b27999a25d23e231e19fa00bdeef04303b9e#aaf1b27999a25d23e231e19fa00bdeef04303b9e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index d9b1d767ebe45..ad3c11761dda4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -151,5 +151,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy- # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } -appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "aaf1b27999a25d23e231e19fa00bdeef04303b9e" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" } diff --git a/frontend/rust-lib/flowy-chat/src/chat_manager.rs b/frontend/rust-lib/flowy-chat/src/chat_manager.rs index 11c27f74c9368..b504a931041e9 100644 --- a/frontend/rust-lib/flowy-chat/src/chat_manager.rs +++ b/frontend/rust-lib/flowy-chat/src/chat_manager.rs @@ -1,6 +1,6 @@ use crate::chat::Chat; use crate::entities::{ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB}; -use crate::local_ai::local_llm_chat::LocalLLMController; +use crate::local_ai::local_llm_chat::LocalAIController; use crate::middleware::chat_service_mw::ChatServiceMiddleware; use crate::persistence::{insert_chat, ChatTable}; @@ -28,7 +28,7 @@ pub struct ChatManager { pub chat_service_wm: Arc, pub user_service: Arc, chats: Arc>>, - pub llm_controller: Arc, + pub local_ai_controller: Arc, } impl ChatManager { @@ -39,15 +39,15 @@ impl ChatManager { ) -> ChatManager { let user_service = Arc::new(user_service); let plugin_manager = Arc::new(PluginManager::new()); - let llm_controller = Arc::new(LocalLLMController::new( + let local_ai_controller = Arc::new(LocalAIController::new( plugin_manager.clone(), store_preferences.clone(), user_service.clone(), cloud_service.clone(), )); - if llm_controller.is_ready() { - if let Err(err) = llm_controller.initialize() { + if local_ai_controller.is_ready() { + if let Err(err) = local_ai_controller.initialize() { error!("[AI Plugin] failed to initialize local ai: {:?}", err); } } @@ -56,14 +56,14 @@ impl ChatManager { let chat_service_wm = Arc::new(ChatServiceMiddleware::new( user_service.clone(), cloud_service, - llm_controller.clone(), + local_ai_controller.clone(), )); Self { chat_service_wm, user_service, chats: Arc::new(DashMap::new()), - llm_controller, + local_ai_controller, } } @@ -79,16 +79,16 @@ impl ChatManager { }); trace!("[AI Plugin] notify open chat: {}", chat_id); - self.llm_controller.open_chat(chat_id); + self.local_ai_controller.open_chat(chat_id); Ok(()) } pub async fn close_chat(&self, chat_id: &str) -> Result<(), FlowyError> { trace!("close chat: {}", chat_id); - if self.llm_controller.is_ready() { + if self.local_ai_controller.is_ready() { info!("[AI Plugin] notify close chat: {}", chat_id); - self.llm_controller.close_chat(chat_id); + self.local_ai_controller.close_chat(chat_id); } Ok(()) } @@ -97,9 +97,9 @@ impl ChatManager { if let Some((_, chat)) = self.chats.remove(chat_id) { chat.close(); - if self.llm_controller.is_ready() { + if self.local_ai_controller.is_ready() { info!("[AI Plugin] notify close chat: {}", chat_id); - self.llm_controller.close_chat(chat_id); + self.local_ai_controller.close_chat(chat_id); } } Ok(()) diff --git a/frontend/rust-lib/flowy-chat/src/event_handler.rs b/frontend/rust-lib/flowy-chat/src/event_handler.rs index 31c6abbf6e8b4..fead2c476b581 100644 --- a/frontend/rust-lib/flowy-chat/src/event_handler.rs +++ b/frontend/rust-lib/flowy-chat/src/event_handler.rs @@ -130,7 +130,7 @@ pub(crate) async fn refresh_local_ai_info_handler( let chat_manager = upgrade_chat_manager(chat_manager)?; let (tx, rx) = oneshot::channel::>(); tokio::spawn(async move { - let model_info = chat_manager.llm_controller.refresh().await; + let model_info = chat_manager.local_ai_controller.refresh().await; let _ = tx.send(model_info); }); @@ -146,7 +146,7 @@ pub(crate) async fn update_local_llm_model_handler( let data = data.into_inner(); let chat_manager = upgrade_chat_manager(chat_manager)?; let state = chat_manager - .llm_controller + .local_ai_controller .use_local_llm(data.llm_id) .await?; data_result_ok(state) @@ -157,7 +157,10 @@ pub(crate) async fn get_local_llm_state_handler( chat_manager: AFPluginState>, ) -> DataResult { let chat_manager = upgrade_chat_manager(chat_manager)?; - let state = chat_manager.llm_controller.get_local_llm_state().await?; + let state = chat_manager + .local_ai_controller + .get_local_llm_state() + .await?; data_result_ok(state) } @@ -208,7 +211,7 @@ pub(crate) async fn download_llm_resource_handler( let chat_manager = upgrade_chat_manager(chat_manager)?; let text_sink = IsolateSink::new(Isolate::new(data.progress_stream)); let task_id = chat_manager - .llm_controller + .local_ai_controller .start_downloading(text_sink) .await?; data_result_ok(DownloadTaskPB { task_id }) @@ -219,7 +222,7 @@ pub(crate) async fn cancel_download_llm_resource_handler( chat_manager: AFPluginState>, ) -> Result<(), FlowyError> { let chat_manager = upgrade_chat_manager(chat_manager)?; - chat_manager.llm_controller.cancel_download()?; + chat_manager.local_ai_controller.cancel_download()?; Ok(()) } @@ -228,6 +231,6 @@ pub(crate) async fn get_plugin_state_handler( chat_manager: AFPluginState>, ) -> DataResult { let chat_manager = upgrade_chat_manager(chat_manager)?; - let state = chat_manager.llm_controller.get_plugin_state(); + let state = chat_manager.local_ai_controller.get_plugin_state(); data_result_ok(state) } diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs index c1a7b24d443ea..4772565cc5624 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs @@ -3,7 +3,7 @@ use crate::entities::{LocalModelResourcePB, PendingResourcePB}; use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting}; use crate::local_ai::model_request::download_model; -use appflowy_local_ai::chat_plugin::ChatPluginConfig; +use appflowy_local_ai::chat_plugin::AIPluginConfig; use flowy_chat_pub::cloud::{LLMModel, LocalAIConfig, ModelInfo}; use flowy_error::{FlowyError, FlowyResult}; use futures::Sink; @@ -386,7 +386,7 @@ impl LLMResourceController { } #[instrument(level = "debug", skip_all, err)] - pub fn get_ai_plugin_config(&self) -> FlowyResult { + pub fn get_ai_plugin_config(&self) -> FlowyResult { if !self.is_ready() { return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); } @@ -406,7 +406,7 @@ impl LLMResourceController { .join(llm_setting.plugin.name); let chat_model_path = model_dir.join(&llm_setting.llm_model.chat_model.file_name); let embedding_model_path = model_dir.join(&llm_setting.llm_model.embedding_model.file_name); - let mut config = ChatPluginConfig::new(bin_path, chat_model_path)?; + let mut config = AIPluginConfig::new(bin_path, chat_model_path)?; // let persist_directory = resource_dir.join("rag"); 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 cc12f7ebc3cf1..a97de00f7042b 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 @@ -5,7 +5,7 @@ use crate::entities::{ use crate::local_ai::llm_resource::{LLMResourceController, LLMResourceService}; use crate::notification::{send_notification, ChatNotification}; use anyhow::Error; -use appflowy_local_ai::chat_plugin::{ChatPluginConfig, LocalChatLLMChat}; +use appflowy_local_ai::chat_plugin::{AIPluginConfig, LocalChatLLMChat}; use appflowy_plugin::manager::PluginManager; use appflowy_plugin::util::is_apple_silicon; use flowy_chat_pub::cloud::{AppFlowyAIPlugin, ChatCloudService, LLMModel, LocalAIConfig}; @@ -33,12 +33,12 @@ pub struct LLMModelInfo { } const LOCAL_AI_SETTING_KEY: &str = "local_ai_setting"; -pub struct LocalLLMController { +pub struct LocalAIController { llm_chat: Arc, llm_res: Arc, } -impl Deref for LocalLLMController { +impl Deref for LocalAIController { type Target = Arc; fn deref(&self) -> &Self::Target { @@ -46,7 +46,7 @@ impl Deref for LocalLLMController { } } -impl LocalLLMController { +impl LocalAIController { pub fn new( plugin_manager: Arc, store_preferences: Arc, @@ -174,7 +174,7 @@ impl LocalLLMController { fn initialize_chat_plugin( llm_chat: &Arc, - mut chat_config: ChatPluginConfig, + mut chat_config: AIPluginConfig, ) -> FlowyResult<()> { let llm_chat = llm_chat.clone(); tokio::spawn(async move { diff --git a/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs index be205e755bf46..1ca4160ea87ea 100644 --- a/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-chat/src/middleware/chat_service_mw.rs @@ -1,6 +1,6 @@ use crate::chat_manager::ChatUserService; use crate::entities::{ChatStatePB, ModelTypePB}; -use crate::local_ai::local_llm_chat::LocalLLMController; +use crate::local_ai::local_llm_chat::LocalAIController; use crate::notification::{send_notification, ChatNotification}; use crate::persistence::select_single_message; use appflowy_plugin::error::PluginError; @@ -20,14 +20,14 @@ use std::sync::Arc; pub struct ChatServiceMiddleware { pub cloud_service: Arc, user_service: Arc, - local_llm_controller: Arc, + local_llm_controller: Arc, } impl ChatServiceMiddleware { pub fn new( user_service: Arc, cloud_service: Arc, - local_llm_controller: Arc, + local_llm_controller: Arc, ) -> Self { Self { user_service, From ed3e1cf3f3f8be5d4099fe3a4950b08a13ca08f5 Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 15 Jul 2024 13:17:38 +0800 Subject: [PATCH 12/13] chore: fix clippy --- .../flowy-chat/src/local_ai/llm_resource.rs | 38 ++++++++-------- .../flowy-chat/src/local_ai/local_llm_chat.rs | 4 +- frontend/rust-lib/lib-infra/src/file_util.rs | 45 ++++++++++--------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs index 4772565cc5624..7e4d7da8a6404 100644 --- a/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs +++ b/frontend/rust-lib/flowy-chat/src/local_ai/llm_resource.rs @@ -79,7 +79,7 @@ impl LLMResourceController { } /// Returns true when all resources are downloaded and ready to use. - pub fn is_ready(&self) -> bool { + pub fn is_resource_ready(&self) -> bool { match self.calculate_pending_resources() { Ok(res) => res.is_empty(), Err(_) => false, @@ -153,24 +153,21 @@ impl LLMResourceController { let is_downloading = self.download_task.read().is_some(); let pending_resources: Vec<_> = pending_resources .into_iter() - .filter_map(|res| match res { - PendingResource::PluginRes => Some(vec![PendingResourcePB { + .flat_map(|res| match res { + PendingResource::PluginRes => vec![PendingResourcePB { name: "AppFlowy Plugin".to_string(), file_size: 0, requirements: "".to_string(), - }]), - PendingResource::ModelInfoRes(model_infos) => Some( - model_infos - .into_iter() - .map(|model_info| PendingResourcePB { - name: model_info.name, - file_size: model_info.file_size, - requirements: model_info.requirements, - }) - .collect::>(), - ), + }], + PendingResource::ModelInfoRes(model_infos) => model_infos + .into_iter() + .map(|model_info| PendingResourcePB { + name: model_info.name, + file_size: model_info.file_size, + requirements: model_info.requirements, + }) + .collect::>(), }) - .flatten() .collect(); let resource = LocalModelResourcePB { @@ -343,14 +340,15 @@ impl LLMResourceController { info!("[LLM Resource] Downloading model: {:?}", file_name); let plugin_progress_tx = download_task.tx.clone(); let cloned_model_name = model_name.clone(); + let progress = Arc::new(move |downloaded, total_size| { + let progress = (downloaded as f64 / total_size as f64).clamp(0.0, 1.0); + let _ = plugin_progress_tx.send(format!("{}:progress:{}", cloned_model_name, progress)); + }); match download_model( &url, &model_dir, &file_name, - Some(Arc::new(move |downloaded, total_size| { - let progress = (downloaded as f64 / total_size as f64).clamp(0.0, 1.0); - let _ = plugin_progress_tx.send(format!("{}:progress:{}", cloned_model_name, progress)); - })), + Some(progress), Some(download_task.cancel_token.clone()), ) .await @@ -387,7 +385,7 @@ impl LLMResourceController { #[instrument(level = "debug", skip_all, err)] pub fn get_ai_plugin_config(&self) -> FlowyResult { - if !self.is_ready() { + if !self.is_resource_ready() { return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); } 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 a97de00f7042b..8fb3439cdd4ef 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 @@ -102,7 +102,7 @@ impl LocalAIController { /// Returns true if the local AI is enabled and ready to use. pub fn is_ready(&self) -> bool { - self.llm_res.is_ready() + self.llm_res.is_resource_ready() } pub fn open_chat(&self, chat_id: &str) { @@ -141,7 +141,7 @@ impl LocalAIController { } let state = self.llm_res.use_local_llm(llm_id)?; // Re-initialize the plugin if the setting is updated and ready to use - if self.llm_res.is_ready() { + if self.llm_res.is_resource_ready() { self.initialize()?; } Ok(state) diff --git a/frontend/rust-lib/lib-infra/src/file_util.rs b/frontend/rust-lib/lib-infra/src/file_util.rs index 2186c71eaab05..c8a464d5dd5b6 100644 --- a/frontend/rust-lib/lib-infra/src/file_util.rs +++ b/frontend/rust-lib/lib-infra/src/file_util.rs @@ -1,10 +1,10 @@ use anyhow::Context; use std::cmp::Ordering; use std::fs::File; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::SystemTime; use std::{fs, io}; - use tempfile::tempdir; use walkdir::WalkDir; use zip::write::FileOptions; @@ -69,11 +69,13 @@ where } pub fn zip_folder(src_path: impl AsRef, dest_path: &Path) -> io::Result<()> { - if !src_path.as_ref().exists() { + let src_path = src_path.as_ref(); + + if !src_path.exists() { return Err(io::ErrorKind::NotFound.into()); } - if src_path.as_ref() == dest_path { + if src_path == dest_path { return Err(io::ErrorKind::InvalidInput.into()); } @@ -81,36 +83,39 @@ pub fn zip_folder(src_path: impl AsRef, dest_path: &Path) -> io::Result<() let mut zip = ZipWriter::new(file); let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated); - for entry in WalkDir::new(&src_path) { + for entry in WalkDir::new(src_path) { let entry = entry?; let path = entry.path(); - let name = match path.strip_prefix(&src_path) { + let name = match path.strip_prefix(src_path) { Ok(n) => n, Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "Invalid path")), }; if path.is_file() { - zip.start_file( - name - .to_str() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid file name"))?, - options, - )?; - let mut f = File::open(path)?; - io::copy(&mut f, &mut zip)?; + let file_name = name + .to_str() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid file name"))?; + zip.start_file(file_name, options)?; + + let mut buffer = Vec::new(); + { + let mut f = File::open(path)?; + f.read_to_end(&mut buffer)?; + drop(f); + } + + zip.write_all(&buffer)?; } else if !name.as_os_str().is_empty() { - zip.add_directory( - name - .to_str() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid directory name"))?, - options, - )?; + let dir_name = name + .to_str() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid directory name"))?; + zip.add_directory(dir_name, options)?; } } + zip.finish()?; Ok(()) } - pub fn unzip_and_replace( zip_path: impl AsRef, target_folder: &Path, From 331fddf3c70e423895b6a75ef0b7307347792b8c Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 15 Jul 2024 15:23:09 +0800 Subject: [PATCH 13/13] chore: fix clippy --- .../shared/auth_operation.dart | 5 ++++- .../pages/setting_ai_view/model_selection.dart | 2 +- .../flowy-chat/src/local_ai/local_llm_chat.rs | 18 ++++++++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart index 9e205182a4adc..9c103ace8655c 100644 --- a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart @@ -13,7 +13,10 @@ import 'util.dart'; extension AppFlowyAuthTest on WidgetTester { Future tapGoogleLoginInButton() async { - await tapButton(find.byKey(const Key('signInWithGoogleButton'))); + await tapButton( + find.byKey(const Key('signInWithGoogleButton')), + milliseconds: 3000, + ); } /// Requires being on the SettingsPage.account of the SettingsDialog diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index d37698faa7df1..4a1115e8e1bff 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -61,7 +61,7 @@ List _availableModels = [ AIModelPB.Claude3Sonnet, AIModelPB.GPT35, AIModelPB.GPT4o, - AIModelPB.LocalAIModel, + // AIModelPB.LocalAIModel, ]; String _titleForAIModel(AIModelPB model) { 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 8fb3439cdd4ef..0b0aa68d52576 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 @@ -17,9 +17,10 @@ use lib_infra::async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::ops::Deref; +use parking_lot::Mutex; use std::sync::Arc; use tokio_stream::StreamExt; -use tracing::{error, info, trace}; +use tracing::{debug, error, info, trace}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LLMSetting { @@ -36,6 +37,7 @@ const LOCAL_AI_SETTING_KEY: &str = "local_ai_setting"; pub struct LocalAIController { llm_chat: Arc, llm_res: Arc, + current_chat_id: Mutex>, } impl Deref for LocalAIController { @@ -87,7 +89,11 @@ impl LocalAIController { } }); - Self { llm_chat, llm_res } + Self { + llm_chat, + llm_res, + current_chat_id: Default::default(), + } } pub async fn refresh(&self) -> FlowyResult { self.llm_res.refresh_llm_resource().await @@ -110,6 +116,14 @@ impl LocalAIController { return; } + // Only keep one chat open at a time. Since loading multiple models at the same time will cause + // memory issues. + if let Some(current_chat_id) = self.current_chat_id.lock().as_ref() { + debug!("[AI Plugin] close previous chat: {}", current_chat_id); + self.close_chat(current_chat_id); + } + + *self.current_chat_id.lock() = Some(chat_id.to_string()); let chat_id = chat_id.to_string(); let weak_ctrl = Arc::downgrade(&self.llm_chat); tokio::spawn(async move {