From 34a858e94836466447f717cc2f0cd142ce34bbb7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 09:02:06 +0800 Subject: [PATCH 1/3] chore: show plugin version --- .../ai/download_offline_ai_app_bloc.dart | 41 ------- .../ai/local_ai_setting_panel_bloc.dart | 2 +- .../settings/ai/plugin_state_bloc.dart | 8 +- .../pages/setting_ai_view/plugin_state.dart | 105 ++---------------- frontend/resources/translations/ar-SA.json | 4 +- frontend/resources/translations/en.json | 4 +- frontend/resources/translations/ko-KR.json | 4 +- frontend/rust-lib/Cargo.lock | 6 +- frontend/rust-lib/Cargo.toml | 6 +- frontend/rust-lib/flowy-ai/src/entities.rs | 17 ++- .../rust-lib/flowy-ai/src/event_handler.rs | 9 -- frontend/rust-lib/flowy-ai/src/event_map.rs | 4 - .../flowy-ai/src/local_ai/controller.rs | 22 ++-- 13 files changed, 49 insertions(+), 183 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart deleted file mode 100644 index 185a8c049f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:url_launcher/url_launcher.dart' show launchUrl; -part 'download_offline_ai_app_bloc.freezed.dart'; - -class DownloadOfflineAIBloc - extends Bloc { - DownloadOfflineAIBloc() : super(const DownloadOfflineAIState()) { - on(_handleEvent); - } - - Future _handleEvent( - DownloadOfflineAIEvent event, - Emitter emit, - ) async { - await event.when( - started: () async { - final result = await AIEventGetLocalAIDownloadLink().send(); - await result.fold( - (app) async { - await launchUrl(Uri.parse(app.link)); - }, - (err) {}, - ); - }, - ); - } -} - -@freezed -class DownloadOfflineAIEvent with _$DownloadOfflineAIEvent { - const factory DownloadOfflineAIEvent.started() = _Started; -} - -@freezed -class DownloadOfflineAIState with _$DownloadOfflineAIState { - const factory DownloadOfflineAIState() = _DownloadOfflineAIState; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart index f6d5ef949d..60c68b70c6 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_setting_panel_bloc.dart @@ -44,7 +44,7 @@ class LocalAISettingPanelBloc ) async { event.when( updateAIState: (LocalAIPB pluginState) { - if (pluginState.isPluginExecutableReady) { + if (pluginState.pluginDownloaded) { emit( state.copyWith( runningState: pluginState.state, 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 index 4c3130ea00..d91d7151ab 100644 --- 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 @@ -91,7 +91,11 @@ class PluginStateBloc extends Bloc { ); break; case RunningStatePB.Running: - emit(const PluginStateState(action: PluginStateAction.running())); + emit( + PluginStateState( + action: PluginStateAction.running(aiState.pluginVersion), + ), + ); break; case RunningStatePB.Stopped: emit( @@ -140,7 +144,7 @@ class PluginStateAction with _$PluginStateAction { const factory PluginStateAction.unknown() = _Unknown; const factory PluginStateAction.readToRun() = _ReadyToRun; const factory PluginStateAction.initializingPlugin() = _InitializingPlugin; - const factory PluginStateAction.running() = _PluginRunning; + const factory PluginStateAction.running(String version) = _PluginRunning; const factory PluginStateAction.restartPlugin() = _RestartPlugin; const factory PluginStateAction.lackOfResource(String desc) = _LackOfResource; } 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 ab9303b429..b8461eb141 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,15 +1,11 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/download_offline_ai_app_bloc.dart'; import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.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/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -27,7 +23,7 @@ class PluginStateIndicator extends StatelessWidget { unknown: () => const SizedBox.shrink(), readToRun: () => const _PrepareRunning(), initializingPlugin: () => const InitLocalAIIndicator(), - running: () => const _LocalAIRunning(), + running: (version) => _LocalAIRunning(version: version), restartPlugin: () => const _RestartPluginButton(), lackOfResource: (desc) => _LackOfResource(desc: desc), ); @@ -88,7 +84,9 @@ class _RestartPluginButton extends StatelessWidget { } class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning(); + const _LocalAIRunning({required this.version}); + + final String version; @override Widget build(BuildContext context) { @@ -115,7 +113,11 @@ class _LocalAIRunning extends StatelessWidget { const HSpace(6), Flexible( child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), + LocaleKeys.settings_aiPage_keys_localAIRunning.tr( + args: [ + version, + ], + ), fontSize: 11, color: const Color(0xFF1E4620), maxLines: 3, @@ -131,95 +133,6 @@ class _LocalAIRunning extends StatelessWidget { } } -class OpenOrDownloadOfflineAIApp extends StatelessWidget { - const OpenOrDownloadOfflineAIApp({required this.onRetry, super.key}); - - final VoidCallback onRetry; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DownloadOfflineAIBloc(), - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - maxLines: 3, - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: - "${LocaleKeys.settings_aiPage_keys_offlineAIInstruction1.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction2.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - height: 1.5, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString( - "https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-offline", - ), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction3.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - "${LocaleKeys.settings_aiPage_keys_offlineAIDownload1.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload2.tr()} ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: FontSizes.s14, - color: Theme.of(context).colorScheme.primary, - height: 1.5, - ), - recognizer: TapGestureRecognizer() - ..onTap = - () => context.read().add( - const DownloadOfflineAIEvent.started(), - ), - ), - TextSpan( - text: - " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload3.tr()} ", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(height: 1.5), - ), - ], - ), - ), - ], - ); - }, - ), - ); - } -} - class _LackOfResource extends StatelessWidget { const _LackOfResource({required this.desc}); diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 62708e664f..bb4534d855 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -857,7 +857,7 @@ "localAIStart": "بدأت الدردشة المحلية بالذكاء الاصطناعي...", "localAILoading": "جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...", "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", - "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", + "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل. الإصدار: {}", "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", "localAINotReadyTextFieldPrompt": "لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي", "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", @@ -3217,4 +3217,4 @@ "rewrite": "إعادة كتابة", "insertBelow": "أدخل أدناه" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 2b22da596c..8129157941 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -857,7 +857,7 @@ "localAIStart": "Local AI is starting. If it’s slow, try toggling it off and on", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", - "localAIRunning": "Local AI is running", + "localAIRunning": "Local AI is running. Version: {}", "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", @@ -3202,4 +3202,4 @@ "rewrite": "Rewrite", "insertBelow": "Insert below" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index f956327073..7ed8373c72 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -851,7 +851,7 @@ "localAIStart": "로컬 AI가 시작 중입니다. 느리다면 껐다가 다시 켜보세요", "localAILoading": "로컬 AI 채팅 모델이 로드 중입니다...", "localAIStopped": "로컬 AI가 중지되었습니다", - "localAIRunning": "로컬 AI가 실행 중입니다", + "localAIRunning": "로컬 AI가 실행 중입니다. 버전: {}", "localAIInitializing": "로컬 AI가 로드 중이며 장치에 따라 몇 분이 소요될 수 있습니다", "localAINotReadyTextFieldPrompt": "로컬 AI가 로드되는 동안 편집할 수 없습니다", "failToLoadLocalAI": "로컬 AI를 시작하지 못했습니다", @@ -3185,4 +3185,4 @@ "rewrite": "다시 작성", "insertBelow": "아래에 삽입" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 01b9251e23..6bc02bda8a 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" dependencies = [ "af-plugin", "anyhow", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" dependencies = [ "anyhow", "futures-util", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=a7d4ca96ec30cad941b67acb1e0e426d689c1270#a7d4ca96ec30cad941b67acb1e0e426d689c1270" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b4a0a34b14..6baa7dea3e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "a7d4ca96ec30cad941b67acb1e0e426d689c1270" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index b24d0cb13e..30a3e5dd28 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -585,20 +585,17 @@ pub struct LocalAIPB { #[pb(index = 1)] pub enabled: bool, - #[pb(index = 2)] - pub is_plugin_executable_ready: bool, - - #[pb(index = 3, one_of)] + #[pb(index = 2, one_of)] pub lack_of_resource: Option, - #[pb(index = 4)] + #[pb(index = 3)] pub state: RunningStatePB, -} -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LocalAIAppLinkPB { - #[pb(index = 1)] - pub link: String, + #[pb(index = 4, one_of)] + pub plugin_version: Option, + + #[pb(index = 5)] + pub plugin_downloaded: bool, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index ec8b7b4964..3150d3e378 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -301,15 +301,6 @@ pub(crate) async fn get_local_ai_state_handler( data_result_ok(state) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_offline_app_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let link = ai_manager.local_ai.get_plugin_download_link().await?; - data_result_ok(LocalAIAppLinkPB { link }) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn create_chat_context_handler( data: AFPluginData, diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index 0e24ca6a21..51c49eaabb 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -34,7 +34,6 @@ pub fn init(ai_manager: Weak) -> AFPlugin { .event(AIEvent::RestartLocalAI, restart_local_ai_handler) .event(AIEvent::ToggleLocalAI, toggle_local_ai_handler) .event(AIEvent::GetLocalAIState, get_local_ai_state_handler) - .event(AIEvent::GetLocalAIDownloadLink, get_offline_app_handler) .event(AIEvent::GetLocalAISetting, get_local_ai_setting_handler) .event( AIEvent::UpdateLocalAISetting, @@ -97,9 +96,6 @@ pub enum AIEvent { #[event(output = "LocalAIPB")] GetLocalAIState = 19, - #[event(output = "LocalAIAppLinkPB")] - GetLocalAIDownloadLink = 22, - #[event(input = "CreateChatContextPB")] CreateChatContext = 23, diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index c3f57e6fac..1bbe356e2b 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -98,11 +98,11 @@ impl LocalAIController { if let Ok(workspace_id) = cloned_user_service.workspace_id() { let key = local_ai_enabled_key(&workspace_id); info!("[AI Plugin] state: {:?}", state); - let mut ready = false; + let mut plugin_downloaded = false; let mut lack_of_resource = None; let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { - ready = is_plugin_ready(); + plugin_downloaded = is_plugin_ready(); lack_of_resource = cloned_llm_res.get_lack_of_resource().await; } @@ -113,9 +113,10 @@ impl LocalAIController { ) .payload(LocalAIPB { enabled, - is_plugin_executable_ready: ready, + plugin_downloaded, lack_of_resource, state: new_state, + plugin_version: None, }) .send(); } @@ -274,12 +275,14 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let mut is_plugin_executable_ready = false; + let mut plugin_downloaded = false; let mut state = RunningState::ReadyToConnect; let mut lack_of_resource = None; + let mut plugin_version = None; if enabled { - is_plugin_executable_ready = is_plugin_ready(); + plugin_downloaded = is_plugin_ready(); state = self.ai_plugin.get_plugin_running_state(); + plugin_version = self.ai_plugin.plugin_info().await.ok().map(|v| v.version); lack_of_resource = self.resource.get_lack_of_resource().await; } let elapsed = start.elapsed(); @@ -290,9 +293,10 @@ impl LocalAIController { ); LocalAIPB { enabled, - is_plugin_executable_ready, + plugin_downloaded, state: RunningStatePB::from(state), lack_of_resource, + plugin_version, } } @@ -442,9 +446,10 @@ impl LocalAIController { ) .payload(LocalAIPB { enabled, - is_plugin_executable_ready: true, + plugin_downloaded: true, state: RunningStatePB::Stopped, lack_of_resource: None, + plugin_version: None, }) .send(); } @@ -466,9 +471,10 @@ async fn initialize_ai_plugin( ) .payload(LocalAIPB { enabled: true, - is_plugin_executable_ready: true, + plugin_downloaded: true, state: RunningStatePB::ReadyToRun, lack_of_resource: lack_of_resource.clone(), + plugin_version: None, }) .send(); From af5c4bfe76c65882f130acac1577631c38db96c9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 09:09:40 +0800 Subject: [PATCH 2/3] chore: show plugin version --- .../pages/setting_ai_view/plugin_state.dart | 21 ++- frontend/resources/translations/ar-SA.json | 2 +- frontend/resources/translations/en.json | 2 +- frontend/resources/translations/ko-KR.json | 2 +- .../flowy-ai/src/local_ai/controller.rs | 133 ++++++++++++------ 5 files changed, 109 insertions(+), 51 deletions(-) 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 b8461eb141..12ce391a29 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 @@ -23,7 +23,10 @@ class PluginStateIndicator extends StatelessWidget { unknown: () => const SizedBox.shrink(), readToRun: () => const _PrepareRunning(), initializingPlugin: () => const InitLocalAIIndicator(), - running: (version) => _LocalAIRunning(version: version), + running: (version) => _LocalAIRunning( + key: ValueKey(version), + version: version, + ), restartPlugin: () => const _RestartPluginButton(), lackOfResource: (desc) => _LackOfResource(desc: desc), ); @@ -84,7 +87,7 @@ class _RestartPluginButton extends StatelessWidget { } class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning({required this.version}); + const _LocalAIRunning({required this.version, super.key}); final String version; @@ -111,13 +114,17 @@ class _LocalAIRunning extends StatelessWidget { color: Color(0xFF2E7D32), ), const HSpace(6), + if (version.isNotEmpty) + Flexible( + child: FlowyText( + "($version) ", + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ), Flexible( child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIRunning.tr( - args: [ - version, - ], - ), + LocaleKeys.settings_aiPage_keys_localAIRunning.tr(), fontSize: 11, color: const Color(0xFF1E4620), maxLines: 3, diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index bb4534d855..e6a8be240c 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -857,7 +857,7 @@ "localAIStart": "بدأت الدردشة المحلية بالذكاء الاصطناعي...", "localAILoading": "جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...", "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", - "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل. الإصدار: {}", + "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", "localAINotReadyTextFieldPrompt": "لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي", "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8129157941..59877db7a6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -857,7 +857,7 @@ "localAIStart": "Local AI is starting. If it’s slow, try toggling it off and on", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", - "localAIRunning": "Local AI is running. Version: {}", + "localAIRunning": "Local AI is running", "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 7ed8373c72..635efe92de 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -851,7 +851,7 @@ "localAIStart": "로컬 AI가 시작 중입니다. 느리다면 껐다가 다시 켜보세요", "localAILoading": "로컬 AI 채팅 모델이 로드 중입니다...", "localAIStopped": "로컬 AI가 중지되었습니다", - "localAIRunning": "로컬 AI가 실행 중입니다. 버전: {}", + "localAIRunning": "로컬 AI가 실행 중입니다", "localAIInitializing": "로컬 AI가 로드 중이며 장치에 따라 몇 분이 소요될 수 있습니다", "localAINotReadyTextFieldPrompt": "로컬 AI가 로드되는 동안 편집할 수 없습니다", "failToLoadLocalAI": "로컬 AI를 시작하지 못했습니다", diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs index 1bbe356e2b..03b014c89d 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs @@ -77,62 +77,88 @@ impl LocalAIController { "[AI Plugin] init local ai controller, thread: {:?}", std::thread::current().id() ); + + // Create the core plugin and resource controller let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); let res_impl = LLMResourceServiceImpl { user_service: user_service.clone(), cloud_service: cloud_service.clone(), store_preferences: store_preferences.clone(), }; - let local_ai_resource = Arc::new(LocalAIResourceController::new( user_service.clone(), res_impl, )); - let current_chat_id = ArcSwapOption::default(); + // Subscribe to state changes let mut running_state_rx = local_ai.subscribe_running_state(); - let cloned_llm_res = local_ai_resource.clone(); - let cloned_store_preferences = store_preferences.clone(); - let cloned_user_service = user_service.clone(); + + let cloned_llm_res = Arc::clone(&local_ai_resource); + let cloned_store_preferences = Arc::clone(&store_preferences); + let cloned_local_ai = Arc::clone(&local_ai); + let cloned_user_service = Arc::clone(&user_service); + + // Spawn a background task to listen for plugin state changes tokio::spawn(async move { while let Some(state) = running_state_rx.next().await { - if let Ok(workspace_id) = cloned_user_service.workspace_id() { - let key = local_ai_enabled_key(&workspace_id); - info!("[AI Plugin] state: {:?}", state); - let mut plugin_downloaded = false; - let mut lack_of_resource = None; - let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); - if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { - plugin_downloaded = is_plugin_ready(); - lack_of_resource = cloned_llm_res.get_lack_of_resource().await; - } + // Skip if we can’t get workspace_id + let Ok(workspace_id) = cloned_user_service.workspace_id() else { + continue; + }; - let new_state = RunningStatePB::from(state); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - plugin_downloaded, - lack_of_resource, - state: new_state, - plugin_version: None, - }) - .send(); - } + let key = local_ai_enabled_key(&workspace_id); + info!("[AI Plugin] state: {:?}", state); + + // Read whether plugin is enabled from store; default to true + let enabled = cloned_store_preferences.get_bool(&key).unwrap_or(true); + + // Only check resource status if the plugin isn’t in "UnexpectedStop" and is enabled + let (plugin_downloaded, lack_of_resource) = + if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { + // Possibly check plugin readiness and resource concurrency in parallel, + // but here we do it sequentially for clarity. + let downloaded = is_plugin_ready(); + let resource_lack = cloned_llm_res.get_lack_of_resource().await; + (downloaded, resource_lack) + } else { + (false, None) + }; + + // If plugin is running, retrieve version + let plugin_version = if matches!(state, RunningState::Running { .. }) { + match cloned_local_ai.plugin_info().await { + Ok(info) => Some(info.version), + Err(_) => None, + } + } else { + None + }; + + // Broadcast the new local AI state + let new_state = RunningStatePB::from(state); + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalAIState, + ) + .payload(LocalAIPB { + enabled, + plugin_downloaded, + lack_of_resource, + state: new_state, + plugin_version, + }) + .send(); } }); Self { ai_plugin: local_ai, resource: local_ai_resource, - current_chat_id, + current_chat_id: ArcSwapOption::default(), store_preferences, user_service, cloud_service, } } - #[instrument(level = "debug", skip_all)] pub async fn observe_plugin_resource(&self) { debug!( @@ -275,22 +301,48 @@ impl LocalAIController { pub async fn get_local_ai_state(&self) -> LocalAIPB { let start = std::time::Instant::now(); let enabled = self.is_enabled(); - let mut plugin_downloaded = false; - let mut state = RunningState::ReadyToConnect; - let mut lack_of_resource = None; - let mut plugin_version = None; - if enabled { - plugin_downloaded = is_plugin_ready(); - state = self.ai_plugin.get_plugin_running_state(); - plugin_version = self.ai_plugin.plugin_info().await.ok().map(|v| v.version); - lack_of_resource = self.resource.get_lack_of_resource().await; + + // If not enabled, return immediately. + if !enabled { + debug!( + "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", + start.elapsed(), + std::thread::current().id() + ); + return LocalAIPB { + enabled: false, + plugin_downloaded: false, + state: RunningStatePB::from(RunningState::ReadyToConnect), + lack_of_resource: None, + plugin_version: None, + }; } + + let plugin_downloaded = is_plugin_ready(); + let state = self.ai_plugin.get_plugin_running_state(); + + // If the plugin is running, run both requests in parallel. + // Otherwise, only fetch the resource info. + let (plugin_version, lack_of_resource) = if matches!(state, RunningState::Running { .. }) { + // Launch both futures at once + let plugin_info_fut = self.ai_plugin.plugin_info(); + let resource_fut = self.resource.get_lack_of_resource(); + + let (plugin_info_res, resource_res) = tokio::join!(plugin_info_fut, resource_fut); + let plugin_version = plugin_info_res.ok().map(|info| info.version); + (plugin_version, resource_res) + } else { + let resource_res = self.resource.get_lack_of_resource().await; + (None, resource_res) + }; + let elapsed = start.elapsed(); debug!( "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", elapsed, std::thread::current().id() ); + LocalAIPB { enabled, plugin_downloaded, @@ -299,7 +351,6 @@ impl LocalAIController { plugin_version, } } - #[instrument(level = "debug", skip_all)] pub async fn restart_plugin(&self) { if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None).await { From 5ae3f423136c31452051e967c06141795640b177 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 11:28:29 +0800 Subject: [PATCH 3/3] chore: fix windows build --- frontend/rust-lib/Cargo.lock | 6 +++--- frontend/rust-lib/Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 6bc02bda8a..69d6220409 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -345,7 +345,7 @@ dependencies = [ [[package]] name = "af-local-ai" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" dependencies = [ "af-plugin", "anyhow", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "af-mcp" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" dependencies = [ "anyhow", "futures-util", @@ -379,7 +379,7 @@ dependencies = [ [[package]] name = "af-plugin" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=593bc3fbedf2a2c7e00ec28b10f2268c1983be65#593bc3fbedf2a2c7e00ec28b10f2268c1983be65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=4b3d50cbec2f58be2ac385231b8f585f1555e282#4b3d50cbec2f58be2ac385231b8f585f1555e282" dependencies = [ "anyhow", "cfg-if", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 6baa7dea3e..9245232f29 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -152,6 +152,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "593bc3fbedf2a2c7e00ec28b10f2268c1983be65" } +af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } +af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" } +af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "4b3d50cbec2f58be2ac385231b8f585f1555e282" }