mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
81532d014e
commit
7c3dd5375d
@ -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
|
||||
|
@ -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,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -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(),
|
||||
|
@ -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<()>>,
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
33
frontend/rust-lib/flowy-chat/src/local_ai/path.rs
Normal file
33
frontend/rust-lib/flowy-chat/src/local_ai/path.rs
Normal 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));
|
||||
}
|
||||
}
|
@ -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,
|
||||
))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user