From f76ce2be1481a4195b462632da6cd096bf5fdef0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 30 Mar 2025 12:06:08 +0800 Subject: [PATCH] chore: show not ready state when using ai writer with local ai --- .../lib/ai/service/appflowy_ai_service.dart | 10 ++++++++++ .../ai/ai_writer_block_component.dart | 18 +++++++++++++++++ .../ai/operations/ai_writer_cubit.dart | 20 +++++++++++++++++++ .../ai_writer_test/ai_writer_bloc_test.dart | 4 ++++ frontend/resources/translations/en.json | 3 ++- frontend/rust-lib/flowy-ai/src/completion.rs | 15 ++++++++++---- 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart index 18b1c71029..f2d666a586 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -28,6 +28,7 @@ abstract class AIRepository { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }); } @@ -45,12 +46,14 @@ class AppFlowyAIService implements AIRepository { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = AppFlowyCompletionStream( onStart: onStart, processMessage: processMessage, processAssistMessage: processAssistMessage, processError: onError, + onLocalAIInitializing: onLocalAIInitializing, onEnd: onEnd, ); @@ -85,6 +88,7 @@ abstract class CompletionStream { required this.processMessage, required this.processAssistMessage, required this.processError, + required this.onLocalAIInitializing, required this.onEnd, }); @@ -92,6 +96,7 @@ abstract class CompletionStream { final Future Function(String text) processMessage; final Future Function(String text) processAssistMessage; final void Function(AIError error) processError; + final void Function() onLocalAIInitializing; final Future Function() onEnd; } @@ -102,6 +107,7 @@ class AppFlowyCompletionStream extends CompletionStream { required super.processAssistMessage, required super.processError, required super.onEnd, + required super.onLocalAIInitializing, }) { _startListening(); } @@ -159,6 +165,10 @@ class AppFlowyCompletionStream extends CompletionStream { await onEnd(); } + if (event.startsWith("LOCAL_AI_NOT_READY:")) { + onLocalAIInitializing(); + } + if (event.startsWith("error:")) { processError( AIError(message: event.substring(6), code: AIErrorCode.other), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart index 769fabcd1f..1a94342f11 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -568,6 +568,24 @@ class MainContentArea extends StatelessWidget { ), ); } + if (state is LocalAIRunningAiWriterState) { + return Padding( + padding: EdgeInsets.all(8.0), + child: Row( + children: [ + const HSpace(8.0), + Opacity( + opacity: 0.5, + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), + ), + ), + const HSpace(8.0), + const CircularProgressIndicator.adaptive(), + ], + ), + ); + } return const SizedBox.shrink(); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 3275f0d75c..fae66d316c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -390,6 +390,9 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, + onLocalAIInitializing: () { + emit(LocalAIRunningAiWriterState(command)); + }, ); if (stream != null) { @@ -481,6 +484,9 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, + onLocalAIInitializing: () { + emit(LocalAIRunningAiWriterState(command)); + }, ); if (stream != null) { emit( @@ -569,6 +575,9 @@ class AiWriterCubit extends Cubit { AiWriterRecord.ai(content: _textRobot.markdownText), ); }, + onLocalAIInitializing: () { + emit(LocalAIRunningAiWriterState(command)); + }, ); if (stream != null) { emit( @@ -639,6 +648,9 @@ class AiWriterCubit extends Cubit { } emit(ErrorAiWriterState(command, error: error)); }, + onLocalAIInitializing: () { + emit(LocalAIRunningAiWriterState(command)); + }, ); if (stream != null) { emit( @@ -714,3 +726,11 @@ class DocumentContentEmptyAiWriterState extends AiWriterState final void Function() onConfirm; } + +class LocalAIRunningAiWriterState extends AiWriterState + with RegisteredAiWriter { + const LocalAIRunningAiWriterState(this.command); + + @override + final AiWriterCommand command; +} diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index bffe27e985..0cd494dd4d 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -30,6 +30,7 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = _MockCompletionStream(); unawaited( @@ -62,6 +63,7 @@ class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = _MockCompletionStream(); unawaited( @@ -90,6 +92,7 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = _MockCompletionStream(); unawaited( @@ -120,6 +123,7 @@ class _MockErrorRepository extends Mock implements AppFlowyAIService { required Future Function(String text) processAssistMessage, required Future Function() onEnd, required void Function(AIError error) onError, + required void Function() onLocalAIInitializing, }) async { final stream = _MockCompletionStream(); unawaited( diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 59877db7a6..72b46000a2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -858,7 +858,8 @@ "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", "localAIRunning": "Local AI is running", - "localAIInitializing": "Local AI is loading and may take a few minutes, depending on your device", + "localAIInitializing": "Local AI is loading and may take a few seconds, depending on your device", + "localAINotReadyRetryLater": "Local AI is initializing, please retry later", "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", "failToLoadLocalAI": "Failed to start local AI", "restartLocalAI": "Restart Local AI", diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index 38dea5f5e4..4a59cb0a8c 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -12,6 +12,7 @@ use flowy_error::{FlowyError, FlowyResult}; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; +use crate::stream_message::StreamMessage; use crate::util::ai_available_models_key; use flowy_sqlite::kv::KVStorePreferences; use std::sync::{Arc, Weak}; @@ -188,12 +189,18 @@ impl CompletionTask { } } -async fn handle_error(sink: &mut IsolateSink, error: FlowyError) { - if error.is_ai_response_limit_exceeded() { +async fn handle_error(sink: &mut IsolateSink, err: FlowyError) { + if err.is_ai_response_limit_exceeded() { let _ = sink.send("AI_RESPONSE_LIMIT".to_string()).await; - } else if error.is_ai_image_response_limit_exceeded() { + } else if err.is_ai_image_response_limit_exceeded() { let _ = sink.send("AI_IMAGE_RESPONSE_LIMIT".to_string()).await; + } else if err.is_ai_max_required() { + let _ = sink.send(format!("AI_MAX_REQUIRED:{}", err.msg)).await; + } else if err.is_local_ai_not_ready() { + let _ = sink.send(format!("LOCAL_AI_NOT_READY:{}", err.msg)).await; } else { - let _ = sink.send(format!("error:{}", error)).await; + let _ = sink + .send(StreamMessage::OnError(err.msg.clone()).to_string()) + .await; } }