Skip to content

Commit 8c956af

Browse files
authored
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
1 parent f9fbf62 commit 8c956af

File tree

3 files changed

+151
-54
lines changed

3 files changed

+151
-54
lines changed

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_bloc.dart

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
4+
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
45
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
56
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
67
import 'package:appflowy/user/application/ai_service.dart';
@@ -45,11 +46,12 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
4546
isCanceled = true;
4647
await _exit();
4748
},
48-
update: (result, isLoading) async {
49+
update: (result, isLoading, aiError) async {
4950
emit(
5051
state.copyWith(
5152
result: result,
5253
loading: isLoading,
54+
requestError: aiError,
5355
),
5456
);
5557
},
@@ -73,7 +75,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
7375
await aiRepositoryCompleter.future;
7476

7577
if (rewrite) {
76-
add(const SmartEditEvent.update('', true));
78+
add(const SmartEditEvent.update('', true, null));
7779
}
7880

7981
if (enableLogging) {
@@ -91,7 +93,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
9193
if (enableLogging) {
9294
Log.info('[smart_edit] start generating');
9395
}
94-
add(const SmartEditEvent.update('', true));
96+
add(const SmartEditEvent.update('', true, null));
9597
},
9698
onProcess: (text) async {
9799
if (isCanceled) {
@@ -102,7 +104,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
102104
Log.debug('[smart_edit] onProcess: $text');
103105
}
104106
final newResult = state.result + text;
105-
add(SmartEditEvent.update(newResult, false));
107+
add(SmartEditEvent.update(newResult, false, null));
106108
},
107109
onEnd: () async {
108110
if (isCanceled) {
@@ -111,7 +113,7 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
111113
if (enableLogging) {
112114
Log.info('[smart_edit] end generating');
113115
}
114-
add(SmartEditEvent.update('${state.result}\n', false));
116+
add(SmartEditEvent.update('${state.result}\n', false, null));
115117
},
116118
onError: (error) async {
117119
if (isCanceled) {
@@ -120,7 +122,9 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
120122
if (enableLogging) {
121123
Log.info('[smart_edit] onError: $error');
122124
}
125+
add(SmartEditEvent.update('', false, error));
123126
await _exit();
127+
await _clearSelection();
124128
},
125129
);
126130
}
@@ -207,6 +211,14 @@ class SmartEditBloc extends Bloc<SmartEditEvent, SmartEditState> {
207211
),
208212
);
209213
}
214+
215+
Future<void> _clearSelection() async {
216+
final selection = editorState.selection;
217+
if (selection == null) {
218+
return;
219+
}
220+
editorState.selection = null;
221+
}
210222
}
211223

212224
@freezed
@@ -219,7 +231,11 @@ class SmartEditEvent with _$SmartEditEvent {
219231
const factory SmartEditEvent.replace() = _Replace;
220232
const factory SmartEditEvent.insertBelow() = _InsertBelow;
221233
const factory SmartEditEvent.cancel() = _Cancel;
222-
const factory SmartEditEvent.update(String result, bool isLoading) = _Update;
234+
const factory SmartEditEvent.update(
235+
String result,
236+
bool isLoading,
237+
AIError? error,
238+
) = _Update;
223239
}
224240

225241
@freezed
@@ -228,6 +244,7 @@ class SmartEditState with _$SmartEditState {
228244
required bool loading,
229245
required String result,
230246
required SmartEditAction action,
247+
@Default(null) AIError? requestError,
231248
}) = _SmartEditState;
232249

233250
factory SmartEditState.initial(SmartEditAction action) => SmartEditState(

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'dart:async';
22

33
import 'package:appflowy/generated/locale_keys.g.dart';
44
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
5+
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
6+
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/ai_limit_dialog.dart';
57
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
68
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_bloc.dart';
79
import 'package:appflowy/startup/startup.dart';
@@ -12,6 +14,7 @@ import 'package:easy_localization/easy_localization.dart';
1214
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
1315
import 'package:flutter/material.dart';
1416
import 'package:flutter_bloc/flutter_bloc.dart';
17+
import 'package:toastification/toastification.dart';
1518

1619
class SmartEditBlockKeys {
1720
const SmartEditBlockKeys._();
@@ -123,41 +126,44 @@ class _SmartEditBlockComponentWidgetState
123126

124127
return BlocProvider.value(
125128
value: smartEditBloc,
126-
child: AppFlowyPopover(
127-
controller: popoverController,
128-
direction: PopoverDirection.bottomWithLeftAligned,
129-
triggerActions: PopoverTriggerFlags.none,
130-
margin: EdgeInsets.zero,
131-
offset: const Offset(40, 0), // align the editor block
132-
windowPadding: EdgeInsets.zero,
133-
constraints: BoxConstraints(maxWidth: width),
134-
canClose: () async {
135-
final completer = Completer<bool>();
136-
final state = smartEditBloc.state;
137-
if (state.result.isEmpty) {
138-
completer.complete(true);
139-
} else {
140-
await showCancelAndConfirmDialog(
141-
context: context,
142-
title: LocaleKeys.document_plugins_discardResponse.tr(),
143-
description: '',
144-
confirmLabel: LocaleKeys.button_discard.tr(),
145-
onConfirm: () => completer.complete(true),
146-
onCancel: () => completer.complete(false),
129+
child: BlocListener<SmartEditBloc, SmartEditState>(
130+
listener: _onListen,
131+
child: AppFlowyPopover(
132+
controller: popoverController,
133+
direction: PopoverDirection.bottomWithLeftAligned,
134+
triggerActions: PopoverTriggerFlags.none,
135+
margin: EdgeInsets.zero,
136+
offset: const Offset(40, 0), // align the editor block
137+
windowPadding: EdgeInsets.zero,
138+
constraints: BoxConstraints(maxWidth: width),
139+
canClose: () async {
140+
final completer = Completer<bool>();
141+
final state = smartEditBloc.state;
142+
if (state.result.isEmpty) {
143+
completer.complete(true);
144+
} else {
145+
await showCancelAndConfirmDialog(
146+
context: context,
147+
title: LocaleKeys.document_plugins_discardResponse.tr(),
148+
description: '',
149+
confirmLabel: LocaleKeys.button_discard.tr(),
150+
onConfirm: () => completer.complete(true),
151+
onCancel: () => completer.complete(false),
152+
);
153+
}
154+
return completer.future;
155+
},
156+
onClose: _removeNode,
157+
popupBuilder: (BuildContext popoverContext) {
158+
return BlocProvider.value(
159+
// request the result when opening the popover
160+
value: smartEditBloc..add(const SmartEditEvent.started()),
161+
child: const SmartEditInputContent(),
147162
);
148-
}
149-
return completer.future;
150-
},
151-
onClose: _removeNode,
152-
popupBuilder: (BuildContext popoverContext) {
153-
return BlocProvider.value(
154-
// request the result when opening the popover
155-
value: smartEditBloc..add(const SmartEditEvent.started()),
156-
child: const SmartEditInputContent(),
157-
);
158-
},
159-
child: const SizedBox(
160-
width: double.infinity,
163+
},
164+
child: const SizedBox(
165+
width: double.infinity,
166+
),
161167
),
162168
),
163169
);
@@ -179,6 +185,21 @@ class _SmartEditBlockComponentWidgetState
179185
final transaction = editorState.transaction..deleteNode(widget.node);
180186
editorState.apply(transaction);
181187
}
188+
189+
void _onListen(BuildContext context, SmartEditState state) {
190+
final error = state.requestError;
191+
if (error != null) {
192+
if (error.isLimitExceeded) {
193+
showAILimitDialog(context, error.message);
194+
} else {
195+
showToastNotification(
196+
context,
197+
message: error.message,
198+
type: ToastificationType.error,
199+
);
200+
}
201+
}
202+
}
182203
}
183204

184205
class SmartEditInputContent extends StatelessWidget {

frontend/appflowy_flutter/test/bloc_test/smart_edit_test/smart_editor_bloc_test.dart

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'package:bloc_test/bloc_test.dart';
99
import 'package:flutter_test/flutter_test.dart';
1010
import 'package:mocktail/mocktail.dart';
1111

12-
class MockAIRepository extends Mock implements AIRepository {
12+
class _MockAIRepository extends Mock implements AIRepository {
1313
@override
1414
Future<void> streamCompletion({
1515
required String text,
@@ -28,6 +28,26 @@ class MockAIRepository extends Mock implements AIRepository {
2828
}
2929
}
3030

31+
class _MockErrorRepository extends Mock implements AIRepository {
32+
@override
33+
Future<void> streamCompletion({
34+
required String text,
35+
required CompletionTypePB completionType,
36+
required Future<void> Function() onStart,
37+
required Future<void> Function(String text) onProcess,
38+
required Future<void> Function() onEnd,
39+
required void Function(AIError error) onError,
40+
}) async {
41+
await onStart();
42+
onError(
43+
const AIError(
44+
message: 'Error',
45+
code: AIErrorCode.aiResponseLimitExceeded,
46+
),
47+
);
48+
}
49+
}
50+
3151
void main() {
3252
group('SmartEditorBloc: ', () {
3353
blocTest<SmartEditBloc, SmartEditState>(
@@ -64,7 +84,7 @@ void main() {
6484
);
6585
},
6686
act: (bloc) {
67-
bloc.add(SmartEditEvent.initial(Future.value(MockAIRepository())));
87+
bloc.add(SmartEditEvent.initial(Future.value(_MockAIRepository())));
6888
bloc.add(const SmartEditEvent.rewrite());
6989
},
7090
expect: () => [
@@ -78,17 +98,56 @@ void main() {
7898
isA<SmartEditState>().having((s) => s.loading, 'loading', false),
7999
],
80100
);
81-
});
82-
}
83-
84101

85-
// [
86-
// _$SmartEditStateImpl:SmartEditState(loading: true, result: , action: SmartEditAction.makeItLonger),
87-
// _$SmartEditStateImpl:SmartEditState(loading: false, result: UPDATED: 1. Select text to style using the toolbar menu.
88-
// 2. Discover more styling options in Aa.
89-
// 3. AppFlowy empowers you to beautifully and effortlessly style your content.
102+
blocTest<SmartEditBloc, SmartEditState>(
103+
'exceed the ai response limit',
104+
build: () {
105+
const text1 = '1. Select text to style using the toolbar menu.';
106+
const text2 = '2. Discover more styling options in Aa.';
107+
const text3 =
108+
'3. AppFlowy empowers you to beautifully and effortlessly style your content.';
109+
final document = Document(
110+
root: pageNode(
111+
children: [
112+
paragraphNode(text: text1),
113+
paragraphNode(text: text2),
114+
paragraphNode(text: text3),
115+
],
116+
),
117+
);
118+
final editorState = EditorState(document: document);
119+
editorState.selection = Selection(
120+
start: Position(path: [0]),
121+
end: Position(path: [2], offset: text3.length),
122+
);
90123

91-
// , action: SmartEditAction.makeItLonger),
92-
// _$SmartEditStateImpl:SmartEditState(loading: false, result:
93-
// , action: SmartEditAction.makeItLonger)
94-
// ]
124+
final node = smartEditNode(
125+
action: SmartEditAction.makeItLonger,
126+
content: [text1, text2, text3].join('\n'),
127+
);
128+
return SmartEditBloc(
129+
node: node,
130+
editorState: editorState,
131+
action: SmartEditAction.makeItLonger,
132+
enableLogging: false,
133+
);
134+
},
135+
act: (bloc) {
136+
bloc.add(SmartEditEvent.initial(Future.value(_MockErrorRepository())));
137+
bloc.add(const SmartEditEvent.rewrite());
138+
},
139+
expect: () => [
140+
isA<SmartEditState>()
141+
.having((s) => s.loading, 'loading', true)
142+
.having((s) => s.result, 'result', isEmpty),
143+
isA<SmartEditState>()
144+
.having((s) => s.requestError, 'requestError', isNotNull)
145+
.having(
146+
(s) => s.requestError?.code,
147+
'requestError.code',
148+
AIErrorCode.aiResponseLimitExceeded,
149+
),
150+
],
151+
);
152+
});
153+
}

0 commit comments

Comments
 (0)