mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-09-25 08:19:29 +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 '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/plugins.dart';
|
||||
import 'package:appflowy/user/application/ai_service.dart';
|
||||
@ -45,11 +46,12 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
||||
isCanceled = true;
|
||||
await _exit();
|
||||
},
|
||||
update: (result, isLoading) async {
|
||||
update: (result, isLoading, aiError) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
result: result,
|
||||
loading: isLoading,
|
||||
requestError: aiError,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -73,7 +75,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
||||
await aiRepositoryCompleter.future;
|
||||
|
||||
if (rewrite) {
|
||||
add(const SmartEditEvent.update('', true));
|
||||
add(const SmartEditEvent.update('', true, null));
|
||||
}
|
||||
|
||||
if (enableLogging) {
|
||||
@ -91,7 +93,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
||||
if (enableLogging) {
|
||||
Log.info('[smart_edit] start generating');
|
||||
}
|
||||
add(const SmartEditEvent.update('', true));
|
||||
add(const SmartEditEvent.update('', true, null));
|
||||
},
|
||||
onProcess: (text) async {
|
||||
if (isCanceled) {
|
||||
@ -102,7 +104,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
||||
Log.debug('[smart_edit] onProcess: $text');
|
||||
}
|
||||
final newResult = state.result + text;
|
||||
add(SmartEditEvent.update(newResult, false));
|
||||
add(SmartEditEvent.update(newResult, false, null));
|
||||
},
|
||||
onEnd: () async {
|
||||
if (isCanceled) {
|
||||
@ -111,7 +113,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
||||
if (enableLogging) {
|
||||
Log.info('[smart_edit] end generating');
|
||||
}
|
||||
add(SmartEditEvent.update('${state.result}\n', false));
|
||||
add(SmartEditEvent.update('${state.result}\n', false, null));
|
||||
},
|
||||
onError: (error) async {
|
||||
if (isCanceled) {
|
||||
@ -120,7 +122,9 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
|
||||
if (enableLogging) {
|
||||
Log.info('[smart_edit] onError: $error');
|
||||
}
|
||||
add(SmartEditEvent.update('', false, error));
|
||||
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
|
||||
@ -219,7 +231,11 @@ class SmartEditEvent with _$SmartEditEvent {
|
||||
const factory SmartEditEvent.replace() = _Replace;
|
||||
const factory SmartEditEvent.insertBelow() = _InsertBelow;
|
||||
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
|
||||
@ -228,6 +244,7 @@ class SmartEditState with _$SmartEditState {
|
||||
required bool loading,
|
||||
required String result,
|
||||
required SmartEditAction action,
|
||||
@Default(null) AIError? requestError,
|
||||
}) = _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/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_bloc.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:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
|
||||
class SmartEditBlockKeys {
|
||||
const SmartEditBlockKeys._();
|
||||
@ -123,41 +126,44 @@ class _SmartEditBlockComponentWidgetState
|
||||
|
||||
return BlocProvider.value(
|
||||
value: smartEditBloc,
|
||||
child: AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
offset: const Offset(40, 0), // align the editor block
|
||||
windowPadding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(maxWidth: width),
|
||||
canClose: () async {
|
||||
final completer = Completer<bool>();
|
||||
final state = smartEditBloc.state;
|
||||
if (state.result.isEmpty) {
|
||||
completer.complete(true);
|
||||
} else {
|
||||
await showCancelAndConfirmDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.document_plugins_discardResponse.tr(),
|
||||
description: '',
|
||||
confirmLabel: LocaleKeys.button_discard.tr(),
|
||||
onConfirm: () => completer.complete(true),
|
||||
onCancel: () => completer.complete(false),
|
||||
child: BlocListener<SmartEditBloc, SmartEditState>(
|
||||
listener: _onListen,
|
||||
child: AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
offset: const Offset(40, 0), // align the editor block
|
||||
windowPadding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(maxWidth: width),
|
||||
canClose: () async {
|
||||
final completer = Completer<bool>();
|
||||
final state = smartEditBloc.state;
|
||||
if (state.result.isEmpty) {
|
||||
completer.complete(true);
|
||||
} else {
|
||||
await showCancelAndConfirmDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.document_plugins_discardResponse.tr(),
|
||||
description: '',
|
||||
confirmLabel: LocaleKeys.button_discard.tr(),
|
||||
onConfirm: () => completer.complete(true),
|
||||
onCancel: () => completer.complete(false),
|
||||
);
|
||||
}
|
||||
return completer.future;
|
||||
},
|
||||
onClose: _removeNode,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return BlocProvider.value(
|
||||
// request the result when opening the popover
|
||||
value: smartEditBloc..add(const SmartEditEvent.started()),
|
||||
child: const SmartEditInputContent(),
|
||||
);
|
||||
}
|
||||
return completer.future;
|
||||
},
|
||||
onClose: _removeNode,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return BlocProvider.value(
|
||||
// request the result when opening the popover
|
||||
value: smartEditBloc..add(const SmartEditEvent.started()),
|
||||
child: const SmartEditInputContent(),
|
||||
);
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -179,6 +185,21 @@ class _SmartEditBlockComponentWidgetState
|
||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||
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 {
|
||||
|
@ -9,7 +9,7 @@ import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockAIRepository extends Mock implements AIRepository {
|
||||
class _MockAIRepository extends Mock implements AIRepository {
|
||||
@override
|
||||
Future<void> streamCompletion({
|
||||
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() {
|
||||
group('SmartEditorBloc: ', () {
|
||||
blocTest<SmartEditBloc, SmartEditState>(
|
||||
@ -64,7 +84,7 @@ void main() {
|
||||
);
|
||||
},
|
||||
act: (bloc) {
|
||||
bloc.add(SmartEditEvent.initial(Future.value(MockAIRepository())));
|
||||
bloc.add(SmartEditEvent.initial(Future.value(_MockAIRepository())));
|
||||
bloc.add(const SmartEditEvent.rewrite());
|
||||
},
|
||||
expect: () => [
|
||||
@ -78,17 +98,56 @@ void main() {
|
||||
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