chore: switch to appflowy ai if local ai is not enable (#5839)

* chore: switch to appflowy ai if local ai is not enable

* chore: clippy

* chore: fix enable bug

* chore: fix compile

* chore: clippy
This commit is contained in:
Nathan.fooo 2024-07-31 11:47:09 +08:00 committed by GitHub
parent 81532d014e
commit 7c3dd5375d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 238 additions and 220 deletions

View File

@ -13,9 +13,9 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
: listener = LocalLLMListener(),
super(const ChatInputState(aiType: _AppFlowyAI())) {
listener.start(
chatStateCallback: (aiState) {
stateCallback: (pluginState) {
if (!isClosed) {
add(ChatInputEvent.updateState(aiState));
add(ChatInputEvent.updatePluginState(pluginState));
}
},
);
@ -37,12 +37,12 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
) async {
await event.when(
started: () async {
final result = await ChatEventGetLocalAIChatState().send();
final result = await ChatEventGetLocalAIPluginState().send();
result.fold(
(aiState) {
(pluginState) {
if (!isClosed) {
add(
ChatInputEvent.updateState(aiState),
ChatInputEvent.updatePluginState(pluginState),
);
}
},
@ -51,8 +51,8 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
},
);
},
updateState: (aiState) {
if (aiState.pluginState.state == RunningStatePB.Running) {
updatePluginState: (pluginState) {
if (pluginState.state == RunningStatePB.Running) {
emit(const ChatInputState(aiType: _LocalAI()));
} else {
emit(const ChatInputState(aiType: _AppFlowyAI()));
@ -65,8 +65,9 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
@freezed
class ChatInputEvent with _$ChatInputEvent {
const factory ChatInputEvent.started() = _Started;
const factory ChatInputEvent.updateState(LocalAIChatPB aiState) =
_UpdateAIState;
const factory ChatInputEvent.updatePluginState(
LocalAIPluginStatePB pluginState,
) = _UpdatePluginState;
}
@freezed

View File

@ -223,20 +223,20 @@ class OpenOrDownloadOfflineAIApp extends StatelessWidget {
],
),
),
const SizedBox(
height: 6,
), // Replaced VSpace with SizedBox for simplicity
SizedBox(
height: 30,
child: FlowyButton(
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(horizontal: 12),
text: FlowyText(
LocaleKeys.settings_aiPage_keys_activeOfflineAI.tr(),
),
onTap: onRetry,
),
),
// const SizedBox(
// height: 6,
// ), // Replaced VSpace with SizedBox for simplicity
// SizedBox(
// height: 30,
// child: FlowyButton(
// useIntrinsicWidth: true,
// margin: const EdgeInsets.symmetric(horizontal: 12),
// text: FlowyText(
// LocaleKeys.settings_aiPage_keys_activeOfflineAI.tr(),
// ),
// onTap: onRetry,
// ),
// ),
],
);
},

View File

@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/m
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -52,12 +53,11 @@ class SettingsAIView extends StatelessWidget {
];
children.add(const _AISearchToggle(value: false));
// TODO(nathan): enable local ai
// children.add(
// _LocalAIOnBoarding(
// workspaceId: userProfile.workspaceId,
// ),
// );
children.add(
_LocalAIOnBoarding(
workspaceId: userProfile.workspaceId,
),
);
return SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
@ -130,7 +130,7 @@ class _LocalAIOnBoarding extends StatelessWidget {
child: BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
builder: (context, state) {
// Show the local AI settings if the user has purchased the AI Local plan
if (state.isPurchaseAILocal) {
if (kDebugMode || state.isPurchaseAILocal) {
return const LocalAISetting();
} else {
// Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/util/int64_extension.dart';
@ -212,24 +214,23 @@ class _SettingsBillingViewState extends State<SettingsBillingView> {
// Currently, the AI Local tile is only available on macOS
// TODO(nathan): enable windows and linux
// TODO(nathan): enable local ai
// if (Platform.isMacOS)
// _AITile(
// plan: SubscriptionPlanPB.AiLocal,
// label: LocaleKeys
// .settings_billingPage_addons_aiOnDevice_label
// .tr(),
// description: LocaleKeys
// .settings_billingPage_addons_aiOnDevice_description,
// activeDescription: LocaleKeys
// .settings_billingPage_addons_aiOnDevice_activeDescription,
// canceledDescription: LocaleKeys
// .settings_billingPage_addons_aiOnDevice_canceledDescription,
// subscriptionInfo:
// state.subscriptionInfo.addOns.firstWhereOrNull(
// (a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
// ),
// ),
if (Platform.isMacOS)
_AITile(
plan: SubscriptionPlanPB.AiLocal,
label: LocaleKeys
.settings_billingPage_addons_aiOnDevice_label
.tr(),
description: LocaleKeys
.settings_billingPage_addons_aiOnDevice_description,
activeDescription: LocaleKeys
.settings_billingPage_addons_aiOnDevice_activeDescription,
canceledDescription: LocaleKeys
.settings_billingPage_addons_aiOnDevice_canceledDescription,
subscriptionInfo:
state.subscriptionInfo.addOns.firstWhereOrNull(
(a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
),
),
],
),
],

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@ -139,44 +141,43 @@ class _SettingsPlanViewState extends State<SettingsPlanView> {
// Currently, the AI Local tile is only available on macOS
// TODO(nathan): enable windows and linux
// TODO(nathan): enable local ai
// if (Platform.isMacOS)
// Flexible(
// child: _AddOnBox(
// title: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_title
// .tr(),
// description: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_description
// .tr(),
// price: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_price
// .tr(
// args: [
// SubscriptionPlanPB.AiLocal.priceAnnualBilling,
// ],
// ),
// priceInfo: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_priceInfo
// .tr(),
// billingInfo: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_billingInfo
// .tr(
// args: [
// SubscriptionPlanPB.AiLocal.priceMonthBilling,
// ],
// ),
// buttonText: state.subscriptionInfo.hasAIOnDevice
// ? LocaleKeys
// .settings_planPage_planUsage_addons_activeLabel
// .tr()
// : LocaleKeys
// .settings_planPage_planUsage_addons_addLabel
// .tr(),
// isActive: state.subscriptionInfo.hasAIOnDevice,
// plan: SubscriptionPlanPB.AiLocal,
// ),
// ),
if (Platform.isMacOS)
Flexible(
child: _AddOnBox(
title: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_title
.tr(),
description: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_description
.tr(),
price: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_price
.tr(
args: [
SubscriptionPlanPB.AiLocal.priceAnnualBilling,
],
),
priceInfo: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_priceInfo
.tr(),
billingInfo: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_billingInfo
.tr(
args: [
SubscriptionPlanPB.AiLocal.priceMonthBilling,
],
),
buttonText: state.subscriptionInfo.hasAIOnDevice
? LocaleKeys
.settings_planPage_planUsage_addons_activeLabel
.tr()
: LocaleKeys
.settings_planPage_planUsage_addons_addLabel
.tr(),
isActive: state.subscriptionInfo.hasAIOnDevice,
plan: SubscriptionPlanPB.AiLocal,
),
),
],
),
],

View File

@ -14,7 +14,7 @@ use flowy_sqlite::DBConnection;
use lib_infra::util::timestamp;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{error, info, trace};
use tracing::{info, trace};
pub trait ChatUserService: Send + Sync + 'static {
fn user_id(&self) -> Result<i64, FlowyError>;
@ -46,12 +46,6 @@ impl ChatManager {
cloud_service.clone(),
));
if local_ai_controller.can_init_plugin() {
if let Err(err) = local_ai_controller.initialize_ai_plugin(None) {
error!("[AI Plugin] failed to initialize local ai: {:?}", err);
}
}
// setup local chat service
let cloud_service_wm = Arc::new(CloudServiceMiddleware::new(
user_service.clone(),

View File

@ -16,8 +16,9 @@ use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use std::sync::Arc;
use tokio::select;
use tokio_stream::StreamExt;
use tracing::{debug, error, info, trace};
use tracing::{debug, error, info, instrument, trace};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LLMSetting {
@ -69,10 +70,11 @@ impl LocalAIController {
let current_chat_id = Mutex::new(None);
let mut running_state_rx = llm_chat.subscribe_running_state();
let offline_ai_ready = llm_res.is_offline_ai_ready();
let cloned_llm_res = llm_res.clone();
tokio::spawn(async move {
while let Some(state) = running_state_rx.next().await {
info!("[AI Plugin] state: {:?}", state);
let offline_ai_ready = cloned_llm_res.is_offline_app_ready();
let new_state = RunningStatePB::from(state);
make_notification(
APPFLOWY_AI_NOTIFICATION_KEY,
@ -96,31 +98,44 @@ impl LocalAIController {
let rag_enabled = this.is_rag_enabled();
let cloned_llm_chat = this.llm_chat.clone();
let cloned_llm_res = this.llm_res.clone();
let mut offline_ai_watch = this.llm_res.subscribe_offline_app_state();
tokio::spawn(async move {
while rx.recv().await.is_some() {
let init_fn = || {
if let Ok(chat_config) = cloned_llm_res.get_chat_config(rag_enabled) {
if let Err(err) = initialize_chat_plugin(&cloned_llm_chat, chat_config, None) {
if let Err(err) = initialize_ai_plugin(&cloned_llm_chat, chat_config, None) {
error!("[AI Plugin] failed to setup plugin: {:?}", err);
}
}
};
loop {
select! {
_ = offline_ai_watch.recv() => {
init_fn();
},
_ = rx.recv() => {
init_fn();
},
else => { break; }
}
}
});
if this.can_init_plugin() {
let result = this.llm_res.get_chat_config(this.is_rag_enabled());
if let Ok(chat_config) = result {
if let Err(err) = initialize_ai_plugin(&this.llm_chat, chat_config, None) {
error!("[AI Plugin] failed to setup plugin: {:?}", err);
}
}
}
this
}
pub async fn refresh(&self) -> FlowyResult<LLMModelInfo> {
self.llm_res.refresh_llm_resource().await
}
pub fn initialize_ai_plugin(
&self,
ret: Option<tokio::sync::oneshot::Sender<()>>,
) -> FlowyResult<()> {
let chat_config = self.llm_res.get_chat_config(self.is_rag_enabled())?;
initialize_chat_plugin(&self.llm_chat, chat_config, ret)?;
Ok(())
}
/// Returns true if the local AI is enabled and ready to use.
pub fn can_init_plugin(&self) -> bool {
self.is_enabled() && self.llm_res.is_resource_ready()
@ -199,7 +214,10 @@ 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_resource_ready() {
self.initialize_ai_plugin(None)?;
let chat_config = self.llm_res.get_chat_config(self.is_rag_enabled())?;
if let Err(err) = initialize_ai_plugin(&self.llm_chat, chat_config, None) {
error!("failed to setup plugin: {:?}", err);
}
}
Ok(state)
}
@ -226,7 +244,7 @@ impl LocalAIController {
}
pub fn get_chat_plugin_state(&self) -> LocalAIPluginStatePB {
let offline_ai_ready = self.llm_res.is_offline_ai_ready();
let offline_ai_ready = self.llm_res.is_offline_app_ready();
let state = self.llm_chat.get_plugin_running_state();
LocalAIPluginStatePB {
state: RunningStatePB::from(state),
@ -237,7 +255,7 @@ impl LocalAIController {
pub fn restart_chat_plugin(&self) {
let rag_enabled = self.is_rag_enabled();
if let Ok(chat_config) = self.llm_res.get_chat_config(rag_enabled) {
if let Err(err) = initialize_chat_plugin(&self.llm_chat, chat_config, None) {
if let Err(err) = initialize_ai_plugin(&self.llm_chat, chat_config, None) {
error!("[AI Plugin] failed to setup plugin: {:?}", err);
}
}
@ -268,7 +286,8 @@ impl LocalAIController {
if enabled {
let chat_enabled = self
.store_preferences
.get_bool_or_default(APPFLOWY_LOCAL_AI_CHAT_ENABLED);
.get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED)
.unwrap_or(true);
self.enable_chat_plugin(chat_enabled).await?;
} else {
self.enable_chat_plugin(false).await?;
@ -300,9 +319,11 @@ impl LocalAIController {
}
async fn enable_chat_plugin(&self, enabled: bool) -> FlowyResult<()> {
info!("[AI Plugin] enable chat plugin: {}", enabled);
if enabled {
let (tx, rx) = tokio::sync::oneshot::channel();
if let Err(err) = self.initialize_ai_plugin(Some(tx)) {
let chat_config = self.llm_res.get_chat_config(self.is_rag_enabled())?;
if let Err(err) = initialize_ai_plugin(&self.llm_chat, chat_config, Some(tx)) {
error!("[AI Plugin] failed to initialize local ai: {:?}", err);
}
let _ = rx.await;
@ -313,7 +334,8 @@ impl LocalAIController {
}
}
fn initialize_chat_plugin(
#[instrument(level = "debug", skip_all, err)]
fn initialize_ai_plugin(
llm_chat: &Arc<LocalChatLLMChat>,
mut chat_config: AIPluginConfig,
ret: Option<tokio::sync::oneshot::Sender<()>>,

View File

@ -15,8 +15,9 @@ use lib_infra::util::{get_operating_system, OperatingSystem};
use std::path::PathBuf;
use std::sync::Arc;
use crate::local_ai::path::offline_app_path;
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
use crate::local_ai::watch::{watch_path, WatchContext};
use crate::local_ai::watch::{watch_offline_app, WatchContext};
use tokio::fs::{self};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, instrument, trace, warn};
@ -69,7 +70,8 @@ pub struct LLMResourceController {
download_task: Arc<RwLock<Option<DownloadTask>>>,
resource_notify: tokio::sync::mpsc::Sender<()>,
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
offline_app_disk_watch: RwLock<Option<WatchContext>>,
#[allow(dead_code)]
offline_app_disk_watch: Option<WatchContext>,
offline_app_state_sender: tokio::sync::broadcast::Sender<WatchDiskEvent>,
}
@ -79,8 +81,31 @@ impl LLMResourceController {
resource_service: impl LLMResourceService,
resource_notify: tokio::sync::mpsc::Sender<()>,
) -> Self {
let (offline_app_ready_sender, _) = tokio::sync::broadcast::channel(1);
let (offline_app_state_sender, _) = tokio::sync::broadcast::channel(1);
let llm_setting = RwLock::new(resource_service.retrieve_setting());
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
let mut offline_app_disk_watch: Option<WatchContext> = None;
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
{
match watch_offline_app() {
Ok((new_watcher, mut rx)) => {
let sender = offline_app_state_sender.clone();
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
if let Err(err) = sender.send(event) {
error!("[LLM Resource] Failed to send offline app state: {:?}", err);
}
}
});
offline_app_disk_watch = Some(new_watcher);
},
Err(err) => {
error!("[LLM Resource] Failed to watch offline app path: {:?}", err);
},
}
}
Self {
user_service,
resource_service: Arc::new(resource_service),
@ -89,8 +114,8 @@ impl LLMResourceController {
download_task: Default::default(),
resource_notify,
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
offline_app_disk_watch: Default::default(),
offline_app_state_sender: offline_app_ready_sender,
offline_app_disk_watch,
offline_app_state_sender,
}
}
@ -100,32 +125,7 @@ impl LLMResourceController {
}
fn set_llm_setting(&self, llm_setting: LLMSetting) {
let offline_app_path = self.offline_app_path(&llm_setting.app.ai_plugin_name);
*self.llm_setting.write() = Some(llm_setting);
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
{
let is_diff = self
.offline_app_disk_watch
.read()
.as_ref()
.map(|watch_context| watch_context.path == offline_app_path)
.unwrap_or(true);
// If the offline app path is different from the current watch path, update the watch path.
if is_diff {
if let Ok((watcher, mut rx)) = watch_path(offline_app_path) {
let offline_app_ready_sender = self.offline_app_state_sender.clone();
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
info!("Offline app file changed: {:?}", event);
let _ = offline_app_ready_sender.send(event);
}
});
self.offline_app_disk_watch.write().replace(watcher);
}
}
}
}
/// Returns true when all resources are downloaded and ready to use.
@ -136,17 +136,8 @@ impl LLMResourceController {
}
}
pub fn is_offline_ai_ready(&self) -> bool {
match self.llm_setting.read().as_ref() {
None => {
trace!("[LLM Resource] No local ai setting found");
false
},
Some(setting) => {
let path = self.offline_app_path(&setting.app.ai_plugin_name);
path.exists()
},
}
pub fn is_offline_app_ready(&self) -> bool {
offline_app_path().exists()
}
pub async fn get_offline_ai_app_download_link(&self) -> FlowyResult<String> {
@ -256,9 +247,9 @@ impl LLMResourceController {
None => Err(FlowyError::local_ai().with_context("Can't find any llm config")),
Some(llm_setting) => {
let mut resources = vec![];
let plugin_path = self.offline_app_path(&llm_setting.app.ai_plugin_name);
if !plugin_path.exists() {
trace!("[LLM Resource] offline plugin not found: {:?}", plugin_path);
let app_path = offline_app_path();
if !app_path.exists() {
trace!("[LLM Resource] offline app not found: {:?}", app_path);
resources.push(PendingResource::OfflineApp);
}
@ -337,13 +328,6 @@ impl LLMResourceController {
*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| {
@ -352,43 +336,6 @@ impl LLMResourceController {
}
tokio::spawn(async move {
// let plugin_file_etag_dir = plugin_dir.join(&llm_setting.app.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.app.etag
// );
// let file_name = format!("{}.zip", llm_setting.app.etag);
// let zip_plugin_file = download_plugin(
// &llm_setting.app.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);
// let _ = plugin_progress_tx.send(format!("plugin:progress:{}", progress));
// })),
// Some(Duration::from_millis(100)),
// )
// .await?;
//
// // unzip file
// info!(
// "[LLM Resource] unzip {:?} to {:?}",
// 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);
// 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),
@ -473,7 +420,7 @@ impl LLMResourceController {
let model_dir = self.user_model_folder()?;
let bin_path = match get_operating_system() {
OperatingSystem::MacOS => {
let path = self.offline_app_path(&llm_setting.app.ai_plugin_name);
let path = offline_app_path();
if !path.exists() {
return Err(FlowyError::new(
ErrorCode::AIOfflineNotInstalled,
@ -560,10 +507,6 @@ impl LLMResourceController {
self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR))
}
pub(crate) fn offline_app_path(&self, plugin_name: &str) -> PathBuf {
PathBuf::from(format!("/usr/local/bin/{}", plugin_name))
}
fn model_path(&self, model_file_name: &str) -> FlowyResult<PathBuf> {
self
.user_model_folder()

View File

@ -2,5 +2,6 @@ pub mod local_llm_chat;
pub mod local_llm_resource;
mod model_request;
mod path;
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
pub mod watch;

View File

@ -0,0 +1,33 @@
use std::path::PathBuf;
pub(crate) fn install_path() -> Option<PathBuf> {
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
return None;
#[cfg(target_os = "windows")]
return None;
#[cfg(target_os = "macos")]
return Some(PathBuf::from("/usr/local/bin"));
#[cfg(target_os = "linux")]
return None;
}
pub(crate) fn offline_app_path() -> PathBuf {
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
return PathBuf::new();
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
{
let offline_app = "appflowy_ai_plugin";
#[cfg(target_os = "windows")]
return PathBuf::from(format!("/usr/local/bin/{}", offline_app));
#[cfg(target_os = "macos")]
return PathBuf::from(format!("/usr/local/bin/{}", offline_app));
#[cfg(target_os = "linux")]
return PathBuf::from(format!("/usr/local/bin/{}", offline_app));
}
}

View File

@ -1,9 +1,10 @@
use crate::local_ai::local_llm_resource::WatchDiskEvent;
use crate::local_ai::path::{install_path, offline_app_path};
use flowy_error::{FlowyError, FlowyResult};
use notify::{Event, RecursiveMode, Watcher};
use std::path::PathBuf;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
use tracing::error;
use tracing::{error, trace};
pub struct WatchContext {
#[allow(dead_code)]
@ -11,28 +12,49 @@ pub struct WatchContext {
pub path: PathBuf,
}
pub fn watch_path(path: PathBuf) -> FlowyResult<(WatchContext, UnboundedReceiver<WatchDiskEvent>)> {
pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver<WatchDiskEvent>)> {
let install_path = install_path().ok_or_else(|| {
FlowyError::internal().with_context("Unsupported platform for offline app watching")
})?;
trace!(
"[LLM Resource] Start watching offline app path: {:?}",
install_path,
);
let (tx, rx) = unbounded_channel();
let app_path = offline_app_path();
let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| match res {
Ok(event) => match event.kind {
notify::EventKind::Create(_) => {
if let Err(err) = tx.send(WatchDiskEvent::Create) {
error!("watch send error: {:?}", err)
Ok(event) => {
if event.paths.iter().any(|path| path == &app_path) {
trace!("watch event: {:?}", event);
match event.kind {
notify::EventKind::Create(_) => {
if let Err(err) = tx.send(WatchDiskEvent::Create) {
error!("watch send error: {:?}", err)
}
},
notify::EventKind::Remove(_) => {
if let Err(err) = tx.send(WatchDiskEvent::Remove) {
error!("watch send error: {:?}", err)
}
},
_ => {
trace!("unhandle watch event: {:?}", event);
},
}
},
notify::EventKind::Remove(_) => {
if let Err(err) = tx.send(WatchDiskEvent::Remove) {
error!("watch send error: {:?}", err)
}
},
_ => {},
}
},
Err(e) => error!("watch error: {:?}", e),
})
.map_err(|err| FlowyError::internal().with_context(err))?;
watcher
.watch(&path, RecursiveMode::Recursive)
.watch(&install_path, RecursiveMode::NonRecursive)
.map_err(|err| FlowyError::internal().with_context(err))?;
Ok((WatchContext { watcher, path }, rx))
Ok((
WatchContext {
watcher,
path: install_path,
},
rx,
))
}