Skip to content

Commit

Permalink
fix: show AI limit error toast if exceeding the AI response (#6505)
Browse files Browse the repository at this point in the history
* fix: show AI limit error toast if exceeding the AI response

* test: add ai limit test
  • Loading branch information
LucasXu0 authored Oct 8, 2024
1 parent f9fbf62 commit 8c956af
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
),
);
},
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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();
},
);
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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._();
Expand Down Expand Up @@ -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,
),
),
),
);
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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>(
Expand Down Expand Up @@ -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: () => [
Expand All @@ -78,17 +98,56 @@ void main() {
isA<SmartEditState>().having((s) => s.loading, 'loading', false),
],
);
});
}


// [
// _$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.
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),
);

// , action: SmartEditAction.makeItLonger),
// _$SmartEditStateImpl:SmartEditState(loading: false, result:
// , action: SmartEditAction.makeItLonger)
// ]
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,
),
],
);
});
}

0 comments on commit 8c956af

Please sign in to comment.