Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: insert below and replace in smart-edit highlights text #2107

Merged
merged 26 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3731a25
feat: insert below and replace in smart-edit highlights text
squidrye Mar 25, 2023
46729cf
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Mar 27, 2023
e4afc64
test: added integration tests to validate insert below and replace in…
squidrye Mar 27, 2023
994b2ca
refactor: using get_it to inject OpenAiRepository to inject mock repo…
squidrye Mar 27, 2023
7c9ed9e
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Mar 28, 2023
0fdfac4
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Mar 29, 2023
eeacd87
fix: delete node does not propagate non null selection
squidrye Mar 29, 2023
69c6b2b
refactor: suggested changes and fixed bugs causing warning in github-ci
squidrye Mar 29, 2023
1fa5562
fix: integration tests causing error in github-ci
squidrye Mar 29, 2023
790ac32
chore: merge conflicts
squidrye Apr 4, 2023
b1139c5
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Apr 6, 2023
419deb4
refactor: reverting redundant changes due to recent changes in repo
squidrye Apr 6, 2023
8048d08
refactor: reverting redundant changes due to recent changes in repo
squidrye Apr 6, 2023
8c34645
refactor: refactoring to workspace based integration testing.
squidrye Apr 6, 2023
0d990d3
refactor: reverting redundant changes due to recent changes in repo
squidrye Apr 6, 2023
e5787c5
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Apr 6, 2023
9fb5580
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Apr 11, 2023
a3a367b
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Apr 11, 2023
d11d227
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Apr 14, 2023
d4d5607
chore: fix analysis issues
squidrye Apr 11, 2023
a452e01
chore: fix analysis issues
squidrye Apr 14, 2023
6b44bca
Merge branch 'main' into highlight-summary-2059
LucasXu0 Apr 24, 2023
916f720
Merge branch 'AppFlowy-IO:main' into highlight-summary-2059
squidrye Apr 29, 2023
c7d38db
chore: remove the unnecessary conversion
LucasXu0 May 3, 2023
44678a0
Merge branch 'main' into highlight-summary-2059
LucasXu0 May 3, 2023
8e28f2d
chore: merge main branch
LucasXu0 May 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/mock/mock_openai_repository.dart';
import 'util/util.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:appflowy/startup/startup.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const service = TestWorkspaceService(TestWorkspace.aiWorkSpace);

group('integration tests for open-ai smart menu', () {
setUpAll(() async => await service.setUpAll());
setUp(() async => await service.setUp());

testWidgets('testing selection on open-ai smart menu replace', (tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;

editorState.service.selectionService.updateSelection(
Selection(
start: Position(path: [1], offset: 4),
end: Position(path: [1], offset: 10),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.pumpAndSettle();

expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));

await tester.tap(find.byTooltip('AI Assistants'));
await tester.pumpAndSettle(const Duration(milliseconds: 500));

await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();

await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
await tester.pumpAndSettle();

expect(
editorState.service.selectionService.currentSelection.value,
Selection(
start: Position(path: [1], offset: 4),
end: Position(path: [1], offset: 84),
),
);
});
testWidgets('testing selection on open-ai smart menu insert', (tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;

editorState.service.selectionService.updateSelection(
Selection(
start: Position(path: [1], offset: 0),
end: Position(path: [1], offset: 5),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));

await tester.tap(find.byTooltip('AI Assistants'));
await tester.pumpAndSettle(const Duration(milliseconds: 500));

await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();

await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
await tester.pumpAndSettle();

expect(
editorState.service.selectionService.currentSelection.value,
Selection(
start: Position(path: [2], offset: 0),
end: Position(path: [3], offset: 0),
),
);
});
});
}

Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
await tester.initializeAppFlowy();
await mockOpenAIRepository();

await simulateKeyDownEvent(LogicalKeyboardKey.controlLeft);
await simulateKeyDownEvent(LogicalKeyboardKey.backslash);
await tester.pumpAndSettle();

final Finder editor = find.byType(AppFlowyEditor);
await tester.tap(editor);
await tester.pumpAndSettle();
return (tester.state(editor).widget as AppFlowyEditor);
}

Future<void> mockOpenAIRepository() async {
await getIt.unregister<OpenAIRepository>();
getIt.registerFactoryAsync<OpenAIRepository>(
() => Future.value(
MockOpenAIRepository(),
),
);
return;
}
2 changes: 2 additions & 0 deletions frontend/appflowy_flutter/integration_test/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:integration_test/integration_test.dart';
import 'board_test.dart' as board_test;
import 'switch_folder_test.dart' as switch_folder_test;
import 'empty_document_test.dart' as empty_document_test;
import 'open_ai_smart_menu_test.dart' as smart_menu_test;

/// The main task runner for all integration tests in AppFlowy.
///
Expand All @@ -16,4 +17,5 @@ void main() {
switch_folder_test.main();
board_test.main();
empty_document_test.main();
smart_menu_test.main();
}
3 changes: 2 additions & 1 deletion frontend/appflowy_flutter/integration_test/util/data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import 'package:shared_preferences/shared_preferences.dart';

enum TestWorkspace {
board("board"),
emptyDocument("empty_document");
emptyDocument("empty_document"),
aiWorkSpace("ai_workspace");

const TestWorkspace(this._name);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:mocktail/mocktail.dart';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_completion.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
import 'package:http/http.dart' as http;
import 'dart:async';

class MyMockClient extends Mock implements http.Client {
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final requestType = request.method;
final requestUri = request.url;

if (requestType == 'POST' && requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = <String, String>{'content-type': 'text/event-stream'};
final responseBody = Stream.fromIterable([
utf8.encode(
'{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
),
utf8.encode('\n'),
utf8.encode('[DONE]'),
]);

// Return a mocked response with the expected data
return http.StreamedResponse(responseBody, 200, headers: responseHeaders);
}

// Return an error response for any other request
return http.StreamedResponse(const Stream.empty(), 404);
}
}

class MockOpenAIRepository extends HttpOpenAIRepository {
a-wallen marked this conversation as resolved.
Show resolved Hide resolved
MockOpenAIRepository() : super(apiKey: 'dummyKey', client: MyMockClient());

@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(OpenAIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) async {
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
final response = await client.send(request);

var previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream.transform(const Utf8Decoder()).transform(const LineSplitter())) {
await onStart();
final data = chunk.trim().split('data: ');
if (data[0] != '[DONE]') {
final response = TextCompletionResponse.fromJson(
json.decode(data[0]),
);
if (response.choices.isNotEmpty) {
final text = response.choices.first.text;
if (text == previousSyntax && text == '\n') {
continue;
}
await onProcess(response);
previousSyntax = response.choices.first.text;
}
} else {
await onEnd();
}
}
}
return;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/service/op
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
Expand Down Expand Up @@ -242,7 +242,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
),
onPressed: () async {
await _onReplace();
_onExit();
await _onExit();
},
),
const Space(10, 0),
Expand All @@ -257,7 +257,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
),
onPressed: () async {
await _onInsertBelow();
_onExit();
await _onExit();
},
),
const Space(10, 0),
Expand All @@ -272,10 +272,13 @@ class _SmartEditInputState extends State<_SmartEditInput> {
),
onPressed: () async => await _onExit(),
),
const Spacer(),
FlowyText.regular(
LocaleKeys.document_plugins_warning.tr(),
color: Theme.of(context).hintColor,
const Spacer(flex:2),
Expanded(
child: FlowyText.regular(
overflow: TextOverflow.ellipsis,
LocaleKeys.document_plugins_warning.tr(),
color: Theme.of(context).hintColor,
),
),
],
);
Expand All @@ -298,7 +301,22 @@ class _SmartEditInputState extends State<_SmartEditInput> {
selection,
texts,
);
return widget.editorState.apply(transaction);
await widget.editorState.apply(transaction);

int endOffset = texts.last.length;
if (texts.length == 1) {
endOffset += selection.start.offset;
}

await widget.editorState.updateCursorSelection(
Selection(
start: selection.start,
end: Position(
path: [selection.start.path.first + texts.length - 1],
offset: endOffset,
),
),
);
}

Future<void> _onInsertBelow() async {
Expand All @@ -317,7 +335,16 @@ class _SmartEditInputState extends State<_SmartEditInput> {
),
),
);
return widget.editorState.apply(transaction);
await widget.editorState.apply(transaction);

await widget.editorState.updateCursorSelection(
Selection(
start: Position(path: selection.end.path.next, offset: 0),
end: Position(
path: [selection.end.path.next.first + texts.length],
),
),
);
}

Future<void> _onExit() async {
Expand All @@ -333,49 +360,40 @@ class _SmartEditInputState extends State<_SmartEditInput> {
}

Future<void> _requestCompletions() async {
final result = await UserBackendService.getCurrentUserProfile();
return result.fold((l) async {
final openAIRepository = HttpOpenAIRepository(
client: client,
apiKey: l.openaiKey,
);
final openAIRepository = await getIt.getAsync<OpenAIRepository>() as HttpOpenAIRepository;
squidrye marked this conversation as resolved.
Show resolved Hide resolved

var lines = input.split('\n\n');
if (action == SmartEditAction.summarize) {
lines = [lines.join('\n')];
}
for (var i = 0; i < lines.length; i++) {
final element = lines[i];
await openAIRepository.getStreamedCompletions(
useAction: true,
prompt: action.prompt(element),
onStart: () async {
setState(() {
loading = false;
});
},
onProcess: (response) async {
setState(() {
this.result += response.choices.first.text;
});
},
onEnd: () async {
setState(() {
if (i != lines.length - 1) {
this.result += '\n';
}
});
},
onError: (error) async {
await _showError(error.message);
await _onExit();
},
);
}
}, (r) async {
await _showError(r.msg);
await _onExit();
});
var lines = input.split('\n\n');
if (action == SmartEditAction.summarize) {
lines = [lines.join('\n')];
}
for (var i = 0; i < lines.length; i++) {
final element = lines[i];
await openAIRepository.getStreamedCompletions(
useAction: true,
prompt: action.prompt(element),
onStart: () async {
setState(() {
loading = false;
});
},
onProcess: (response) async {
setState(() {
result += response.choices.first.text;
});
},
onEnd: () async {
setState(() {
if (i != lines.length - 1) {
result += '\n';
}
});
},
onError: (error) async {
await _showError(error.message);
await _onExit();
},
);
}
}

Future<void> _showError(String message) async {
Expand All @@ -391,4 +409,4 @@ class _SmartEditInputState extends State<_SmartEditInput> {
),
);
}
}
}
Loading