chore: show plugin state (#5740)

* chore: show plugin state

* chore: flutter analyzer

* chore: update
This commit is contained in:
Nathan.fooo 2024-07-15 22:45:53 +08:00 committed by GitHub
parent ff23165d3e
commit c6ad57f11d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 251 additions and 55 deletions

View File

@ -15,7 +15,6 @@ extension AppFlowyAuthTest on WidgetTester {
Future<void> tapGoogleLoginInButton() async {
await tapButton(
find.byKey(const Key('signInWithGoogleButton')),
milliseconds: 3000,
);
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -9,31 +10,69 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
ChatFileBloc({
required String chatId,
dynamic message,
}) : super(ChatFileState.initial(message)) {
}) : listener = LocalLLMListener(),
super(ChatFileState.initial(message)) {
listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(ChatFileEvent.updateLocalAIState(pluginState));
}
},
);
on<ChatFileEvent>(
(event, emit) async {
await event.when(
initial: () async {},
initial: () async {
final result = await ChatEventGetPluginState().send();
result.fold(
(pluginState) {
if (!isClosed) {
add(ChatFileEvent.updateLocalAIState(pluginState));
}
},
(err) {},
);
},
newFile: (String filePath) {
final payload = ChatFilePB(filePath: filePath, chatId: chatId);
ChatEventChatWithFile(payload).send();
},
updateLocalAIState: (PluginStatePB pluginState) {
emit(
state.copyWith(
supportChatWithFile:
pluginState.state == RunningStatePB.Running,
),
);
},
);
},
);
}
final LocalLLMListener listener;
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
}
@freezed
class ChatFileEvent with _$ChatFileEvent {
const factory ChatFileEvent.initial() = Initial;
const factory ChatFileEvent.newFile(String filePath) = _NewFile;
const factory ChatFileEvent.updateLocalAIState(PluginStatePB pluginState) =
_UpdateLocalAIState;
}
@freezed
class ChatFileState with _$ChatFileState {
const factory ChatFileState({
required String text,
@Default(false) bool supportChatWithFile,
}) = _ChatFileState;
factory ChatFileState.initial(dynamic text) {

View File

@ -71,19 +71,24 @@ class AIChatPage extends StatelessWidget {
create: (context) => ChatFileBloc(chatId: view.id.toString()),
child: BlocBuilder<ChatFileBloc, ChatFileState>(
builder: (context, state) {
return DropTarget(
onDragDone: (DropDoneDetails detail) async {
for (final file in detail.files) {
context
.read<ChatFileBloc>()
.add(ChatFileEvent.newFile(file.path));
}
},
child: _ChatContentPage(
view: view,
userProfile: userProfile,
),
);
return state.supportChatWithFile
? DropTarget(
onDragDone: (DropDoneDetails detail) async {
for (final file in detail.files) {
context
.read<ChatFileBloc>()
.add(ChatFileEvent.newFile(file.path));
}
},
child: _ChatContentPage(
view: view,
userProfile: userProfile,
),
)
: _ChatContentPage(
view: view,
userProfile: userProfile,
);
},
),
);

View File

@ -20,7 +20,7 @@ class LocalAISettingBloc
listener.start(
stateCallback: (newState) {
if (!isClosed) {
add(LocalAISettingEvent.updateLLMRunningState(newState));
add(LocalAISettingEvent.updateLLMRunningState(newState.state));
}
},
);
@ -51,7 +51,11 @@ class LocalAISettingBloc
);
},
(err) {
emit(state.copyWith(fetchModelInfoState: LoadingState.finish(error: err)));
emit(
state.copyWith(
fetchModelInfoState: LoadingState.finish(error: err),
),
);
},
);
},
@ -184,6 +188,12 @@ class LocalAISettingBloc
add(LocalAISettingEvent.didLoadModelInfo(result));
}
}
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
}
@freezed

View File

@ -9,7 +9,7 @@ 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);
typedef PluginStateCallback = void Function(PluginStatePB state);
class LocalLLMListener {
LocalLLMListener() {
@ -39,7 +39,7 @@ class LocalLLMListener {
result.map((r) {
switch (ty) {
case ChatNotification.UpdateChatPluginState:
stateCallback?.call(PluginStatePB.fromBuffer(r).state);
stateCallback?.call(PluginStatePB.fromBuffer(r));
break;
default:
break;

View File

@ -1,24 +1,67 @@
import 'dart:async';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
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<PluginStateEvent, PluginState> {
class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
PluginStateBloc()
: super(
const PluginState(runningState: RunningStatePB.Connecting),
) {
: listener = LocalLLMListener(),
super(const PluginStateState(action: PluginStateAction.init())) {
listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(PluginStateEvent.updateState(pluginState));
}
},
);
on<PluginStateEvent>(_handleEvent);
}
final LocalLLMListener listener;
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
Future<void> _handleEvent(
PluginStateEvent event,
Emitter<PluginState> emit,
Emitter<PluginStateState> emit,
) async {
await event.when(
started: () async {},
started: () async {
final result = await ChatEventGetPluginState().send();
result.fold(
(pluginState) {
if (!isClosed) {
add(PluginStateEvent.updateState(pluginState));
}
},
(err) => Log.error(err.toString()),
);
},
updateState: (PluginStatePB pluginState) {
switch (pluginState.state) {
case RunningStatePB.Running:
emit(const PluginStateState(action: PluginStateAction.ready()));
break;
default:
emit(
state.copyWith(action: const PluginStateAction.reloadRequired()),
);
break;
}
},
restartLocalAI: () {
ChatEventRestartLocalAI().send();
},
);
}
}
@ -26,11 +69,20 @@ class PluginStateBloc extends Bloc<PluginStateEvent, PluginState> {
@freezed
class PluginStateEvent with _$PluginStateEvent {
const factory PluginStateEvent.started() = _Started;
const factory PluginStateEvent.updateState(PluginStatePB pluginState) =
_UpdatePluginState;
const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI;
}
@freezed
class PluginState with _$PluginState {
const factory PluginState({
required RunningStatePB runningState,
}) = _PluginState;
class PluginStateState with _$PluginStateState {
const factory PluginStateState({required PluginStateAction action}) =
_PluginStateState;
}
@freezed
class PluginStateAction with _$PluginStateAction {
const factory PluginStateAction.init() = _Init;
const factory PluginStateAction.ready() = _Ready;
const factory PluginStateAction.reloadRequired() = _ReloadRequired;
}

View File

@ -2,9 +2,10 @@ 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/button.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CheckPluginStateIndicator extends StatelessWidget {
@ -12,17 +13,82 @@ class CheckPluginStateIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFFEDF7ED),
borderRadius: BorderRadius.all(
Radius.circular(4),
),
return BlocProvider(
create: (context) =>
PluginStateBloc()..add(const PluginStateEvent.started()),
child: BlocBuilder<PluginStateBloc, PluginStateState>(
builder: (context, state) {
return state.action.when(
init: () => const _InitPlugin(),
ready: () => const _ReadyToUse(),
reloadRequired: () => const _ReloadButton(),
);
},
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: BlocProvider(
create: (context) => PluginStateBloc(),
);
}
}
class _InitPlugin extends StatelessWidget {
const _InitPlugin();
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 20,
child: CircularProgressIndicator.adaptive(),
);
}
}
class _ReloadButton extends StatelessWidget {
const _ReloadButton();
@override
Widget build(BuildContext context) {
return Row(
children: [
const FlowySvg(
FlowySvgs.download_warn_s,
color: Color(0xFFC62828),
),
const HSpace(6),
FlowyText(LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr()),
const Spacer(),
SizedBox(
height: 30,
child: FlowyButton(
useIntrinsicWidth: true,
text:
FlowyText(LocaleKeys.settings_aiPage_keys_restartLocalAI.tr()),
onTap: () {
context.read<PluginStateBloc>().add(
const PluginStateEvent.restartLocalAI(),
);
},
),
),
],
);
}
}
class _ReadyToUse extends StatelessWidget {
const _ReadyToUse();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: DecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFFEDF7ED),
borderRadius: BorderRadius.all(
Radius.circular(4),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
const HSpace(8),

View File

@ -206,7 +206,7 @@ dependencies = [
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=346020270a0a3d3c82a60b545cd3f52144d56beb#346020270a0a3d3c82a60b545cd3f52144d56beb"
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=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=346020270a0a3d3c82a60b545cd3f52144d56beb#346020270a0a3d3c82a60b545cd3f52144d56beb"
dependencies = [
"anyhow",
"cfg-if",

View File

@ -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 = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }

View File

@ -197,7 +197,7 @@ dependencies = [
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=346020270a0a3d3c82a60b545cd3f52144d56beb#346020270a0a3d3c82a60b545cd3f52144d56beb"
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=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=346020270a0a3d3c82a60b545cd3f52144d56beb#346020270a0a3d3c82a60b545cd3f52144d56beb"
dependencies = [
"anyhow",
"cfg-if",

View File

@ -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 = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }

View File

@ -632,6 +632,8 @@
"localAILoaded": "Local AI Model successfully added and ready to use",
"localAILoading": "Local AI Model is loading...",
"localAIStopped": "Local AI Model stopped",
"failToLoadLocalAI": "Failed to start local AI",
"restartLocalAI": "Restart Local AI",
"title": "AI API Keys",
"openAILabel": "OpenAI API key",
"openAITooltip": "You can find your Secret API key on the API key page",

View File

@ -197,7 +197,7 @@ dependencies = [
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=346020270a0a3d3c82a60b545cd3f52144d56beb#346020270a0a3d3c82a60b545cd3f52144d56beb"
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=b803d8a8fd6b1587449e869b3a7a1ac687cf630b#b803d8a8fd6b1587449e869b3a7a1ac687cf630b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=346020270a0a3d3c82a60b545cd3f52144d56beb#346020270a0a3d3c82a60b545cd3f52144d56beb"
dependencies = [
"anyhow",
"cfg-if",

View File

@ -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 = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "b803d8a8fd6b1587449e869b3a7a1ac687cf630b" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }

View File

@ -234,3 +234,12 @@ pub(crate) async fn get_plugin_state_handler(
let state = chat_manager.local_ai_controller.get_plugin_state();
data_result_ok(state)
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn restart_local_ai_handler(
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> Result<(), FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
chat_manager.local_ai_controller.restart();
Ok(())
}

View File

@ -41,6 +41,7 @@ pub fn init(chat_manager: Weak<ChatManager>) -> AFPlugin {
cancel_download_llm_resource_handler,
)
.event(ChatEvent::GetPluginState, get_plugin_state_handler)
.event(ChatEvent::RestartLocalAI, restart_local_ai_handler)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@ -91,4 +92,7 @@ pub enum ChatEvent {
#[event(output = "PluginStatePB")]
GetPluginState = 14,
#[event()]
RestartLocalAI = 15,
}

View File

@ -59,8 +59,8 @@ impl LocalAIController {
let mut rx = llm_chat.subscribe_running_state();
tokio::spawn(async move {
while let Some(state) = rx.next().await {
info!("[AI Plugin] state: {:?}", state);
let new_state = RunningStatePB::from(state);
info!("[AI Plugin] state: {:?}", new_state);
send_notification(
"appflowy_chat_plugin",
ChatNotification::UpdateChatPluginState,
@ -84,7 +84,9 @@ impl LocalAIController {
tokio::spawn(async move {
while rx.recv().await.is_some() {
if let Ok(chat_config) = cloned_llm_res.get_ai_plugin_config() {
initialize_chat_plugin(&cloned_llm_chat, chat_config).unwrap();
if let Err(err) = initialize_chat_plugin(&cloned_llm_chat, chat_config) {
error!("[AI Plugin] failed to setup plugin: {:?}", err);
}
}
}
});
@ -184,6 +186,14 @@ impl LocalAIController {
state: RunningStatePB::from(state),
}
}
pub fn restart(&self) {
if let Ok(chat_config) = self.llm_res.get_ai_plugin_config() {
if let Err(err) = initialize_chat_plugin(&self.llm_chat, chat_config) {
error!("[AI Plugin] failed to setup plugin: {:?}", err);
}
}
}
}
fn initialize_chat_plugin(