mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-09-27 09:26:47 +00:00
fix: show AI limit error toast if exceeding the AI response (#6505)
* fix: show AI limit error toast if exceeding the AI response * test: add ai limit test
This commit is contained in:
parent
f9fbf62283
commit
8c956afabd
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/user/application/ai_service.dart';
|
import 'package:appflowy/user/application/ai_service.dart';
|
||||||
@ -45,11 +46,12 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
|||||||
isCanceled = true;
|
isCanceled = true;
|
||||||
await _exit();
|
await _exit();
|
||||||
},
|
},
|
||||||
update: (result, isLoading) async {
|
update: (result, isLoading, aiError) async {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
result: result,
|
result: result,
|
||||||
loading: isLoading,
|
loading: isLoading,
|
||||||
|
requestError: aiError,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -73,7 +75,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
|||||||
await aiRepositoryCompleter.future;
|
await aiRepositoryCompleter.future;
|
||||||
|
|
||||||
if (rewrite) {
|
if (rewrite) {
|
||||||
add(const SmartEditEvent.update('', true));
|
add(const SmartEditEvent.update('', true, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableLogging) {
|
if (enableLogging) {
|
||||||
@ -91,7 +93,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
|||||||
if (enableLogging) {
|
if (enableLogging) {
|
||||||
Log.info('[smart_edit] start generating');
|
Log.info('[smart_edit] start generating');
|
||||||
}
|
}
|
||||||
add(const SmartEditEvent.update('', true));
|
add(const SmartEditEvent.update('', true, null));
|
||||||
},
|
},
|
||||||
onProcess: (text) async {
|
onProcess: (text) async {
|
||||||
if (isCanceled) {
|
if (isCanceled) {
|
||||||
@ -102,7 +104,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
|||||||
Log.debug('[smart_edit] onProcess: $text');
|
Log.debug('[smart_edit] onProcess: $text');
|
||||||
}
|
}
|
||||||
final newResult = state.result + text;
|
final newResult = state.result + text;
|
||||||
add(SmartEditEvent.update(newResult, false));
|
add(SmartEditEvent.update(newResult, false, null));
|
||||||
},
|
},
|
||||||
onEnd: () async {
|
onEnd: () async {
|
||||||
if (isCanceled) {
|
if (isCanceled) {
|
||||||
@ -111,7 +113,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
|||||||
if (enableLogging) {
|
if (enableLogging) {
|
||||||
Log.info('[smart_edit] end generating');
|
Log.info('[smart_edit] end generating');
|
||||||
}
|
}
|
||||||
add(SmartEditEvent.update('${state.result}\n', false));
|
add(SmartEditEvent.update('${state.result}\n', false, null));
|
||||||
},
|
},
|
||||||
onError: (error) async {
|
onError: (error) async {
|
||||||
if (isCanceled) {
|
if (isCanceled) {
|
||||||
@ -120,7 +122,9 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
|||||||
if (enableLogging) {
|
if (enableLogging) {
|
||||||
Log.info('[smart_edit] onError: $error');
|
Log.info('[smart_edit] onError: $error');
|
||||||
}
|
}
|
||||||
|
add(SmartEditEvent.update('', false, error));
|
||||||
await _exit();
|
await _exit();
|
||||||
|
await _clearSelection();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -207,6 +211,14 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _clearSelection() async {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editorState.selection = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -219,7 +231,11 @@ class SmartEditEvent with _$SmartEditEvent {
|
|||||||
const factory SmartEditEvent.replace() = _Replace;
|
const factory SmartEditEvent.replace() = _Replace;
|
||||||
const factory SmartEditEvent.insertBelow() = _InsertBelow;
|
const factory SmartEditEvent.insertBelow() = _InsertBelow;
|
||||||
const factory SmartEditEvent.cancel() = _Cancel;
|
const factory SmartEditEvent.cancel() = _Cancel;
|
||||||
const factory SmartEditEvent.update(String result, bool isLoading) = _Update;
|
const factory SmartEditEvent.update(
|
||||||
|
String result,
|
||||||
|
bool isLoading,
|
||||||
|
AIError? error,
|
||||||
|
) = _Update;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -228,6 +244,7 @@ class SmartEditState with _$SmartEditState {
|
|||||||
required bool loading,
|
required bool loading,
|
||||||
required String result,
|
required String result,
|
||||||
required SmartEditAction action,
|
required SmartEditAction action,
|
||||||
|
@Default(null) AIError? requestError,
|
||||||
}) = _SmartEditState;
|
}) = _SmartEditState;
|
||||||
|
|
||||||
factory SmartEditState.initial(SmartEditAction action) => SmartEditState(
|
factory SmartEditState.initial(SmartEditAction action) => SmartEditState(
|
||||||
|
@ -2,6 +2,8 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/ai_limit_dialog.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_bloc.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_bloc.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
@ -12,6 +14,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:toastification/toastification.dart';
|
||||||
|
|
||||||
class SmartEditBlockKeys {
|
class SmartEditBlockKeys {
|
||||||
const SmartEditBlockKeys._();
|
const SmartEditBlockKeys._();
|
||||||
@ -123,6 +126,8 @@ class _SmartEditBlockComponentWidgetState
|
|||||||
|
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: smartEditBloc,
|
value: smartEditBloc,
|
||||||
|
child: BlocListener<SmartEditBloc, SmartEditState>(
|
||||||
|
listener: _onListen,
|
||||||
child: AppFlowyPopover(
|
child: AppFlowyPopover(
|
||||||
controller: popoverController,
|
controller: popoverController,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
@ -160,6 +165,7 @@ class _SmartEditBlockComponentWidgetState
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,6 +185,21 @@ class _SmartEditBlockComponentWidgetState
|
|||||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onListen(BuildContext context, SmartEditState state) {
|
||||||
|
final error = state.requestError;
|
||||||
|
if (error != null) {
|
||||||
|
if (error.isLimitExceeded) {
|
||||||
|
showAILimitDialog(context, error.message);
|
||||||
|
} else {
|
||||||
|
showToastNotification(
|
||||||
|
context,
|
||||||
|
message: error.message,
|
||||||
|
type: ToastificationType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SmartEditInputContent extends StatelessWidget {
|
class SmartEditInputContent extends StatelessWidget {
|
||||||
|
@ -9,7 +9,7 @@ import 'package:bloc_test/bloc_test.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class MockAIRepository extends Mock implements AIRepository {
|
class _MockAIRepository extends Mock implements AIRepository {
|
||||||
@override
|
@override
|
||||||
Future<void> streamCompletion({
|
Future<void> streamCompletion({
|
||||||
required String text,
|
required String text,
|
||||||
@ -28,6 +28,26 @@ class MockAIRepository extends Mock implements AIRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MockErrorRepository extends Mock implements AIRepository {
|
||||||
|
@override
|
||||||
|
Future<void> streamCompletion({
|
||||||
|
required String text,
|
||||||
|
required CompletionTypePB completionType,
|
||||||
|
required Future<void> Function() onStart,
|
||||||
|
required Future<void> Function(String text) onProcess,
|
||||||
|
required Future<void> Function() onEnd,
|
||||||
|
required void Function(AIError error) onError,
|
||||||
|
}) async {
|
||||||
|
await onStart();
|
||||||
|
onError(
|
||||||
|
const AIError(
|
||||||
|
message: 'Error',
|
||||||
|
code: AIErrorCode.aiResponseLimitExceeded,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('SmartEditorBloc: ', () {
|
group('SmartEditorBloc: ', () {
|
||||||
blocTest<SmartEditBloc, SmartEditState>(
|
blocTest<SmartEditBloc, SmartEditState>(
|
||||||
@ -64,7 +84,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
act: (bloc) {
|
act: (bloc) {
|
||||||
bloc.add(SmartEditEvent.initial(Future.value(MockAIRepository())));
|
bloc.add(SmartEditEvent.initial(Future.value(_MockAIRepository())));
|
||||||
bloc.add(const SmartEditEvent.rewrite());
|
bloc.add(const SmartEditEvent.rewrite());
|
||||||
},
|
},
|
||||||
expect: () => [
|
expect: () => [
|
||||||
@ -78,17 +98,56 @@ void main() {
|
|||||||
isA<SmartEditState>().having((s) => s.loading, 'loading', false),
|
isA<SmartEditState>().having((s) => s.loading, 'loading', false),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
blocTest<SmartEditBloc, SmartEditState>(
|
||||||
|
'exceed the ai response limit',
|
||||||
|
build: () {
|
||||||
|
const text1 = '1. Select text to style using the toolbar menu.';
|
||||||
|
const text2 = '2. Discover more styling options in Aa.';
|
||||||
|
const text3 =
|
||||||
|
'3. AppFlowy empowers you to beautifully and effortlessly style your content.';
|
||||||
|
final document = Document(
|
||||||
|
root: pageNode(
|
||||||
|
children: [
|
||||||
|
paragraphNode(text: text1),
|
||||||
|
paragraphNode(text: text2),
|
||||||
|
paragraphNode(text: text3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
editorState.selection = Selection(
|
||||||
|
start: Position(path: [0]),
|
||||||
|
end: Position(path: [2], offset: text3.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
final node = smartEditNode(
|
||||||
|
action: SmartEditAction.makeItLonger,
|
||||||
|
content: [text1, text2, text3].join('\n'),
|
||||||
|
);
|
||||||
|
return SmartEditBloc(
|
||||||
|
node: node,
|
||||||
|
editorState: editorState,
|
||||||
|
action: SmartEditAction.makeItLonger,
|
||||||
|
enableLogging: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
act: (bloc) {
|
||||||
|
bloc.add(SmartEditEvent.initial(Future.value(_MockErrorRepository())));
|
||||||
|
bloc.add(const SmartEditEvent.rewrite());
|
||||||
|
},
|
||||||
|
expect: () => [
|
||||||
|
isA<SmartEditState>()
|
||||||
|
.having((s) => s.loading, 'loading', true)
|
||||||
|
.having((s) => s.result, 'result', isEmpty),
|
||||||
|
isA<SmartEditState>()
|
||||||
|
.having((s) => s.requestError, 'requestError', isNotNull)
|
||||||
|
.having(
|
||||||
|
(s) => s.requestError?.code,
|
||||||
|
'requestError.code',
|
||||||
|
AIErrorCode.aiResponseLimitExceeded,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// [
|
|
||||||
// _$SmartEditStateImpl:SmartEditState(loading: true, result: , action: SmartEditAction.makeItLonger),
|
|
||||||
// _$SmartEditStateImpl:SmartEditState(loading: false, result: UPDATED: 1. Select text to style using the toolbar menu.
|
|
||||||
// 2. Discover more styling options in Aa.
|
|
||||||
// 3. AppFlowy empowers you to beautifully and effortlessly style your content.
|
|
||||||
|
|
||||||
// , action: SmartEditAction.makeItLonger),
|
|
||||||
// _$SmartEditStateImpl:SmartEditState(loading: false, result:
|
|
||||||
// , action: SmartEditAction.makeItLonger)
|
|
||||||
// ]
|
|
Loading…
x
Reference in New Issue
Block a user