Skip to content

Commit e73870e

Browse files
authored
fix: auto generator bugs (#1934)
1 parent 9e235c5 commit e73870e

File tree

5 files changed

+139
-32
lines changed

5 files changed

+139
-32
lines changed

frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:convert';
22

33
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
4+
import 'package:appflowy_editor/appflowy_editor.dart';
5+
import 'package:flutter/material.dart';
46

57
import 'text_completion.dart';
68
import 'package:dartz/dartz.dart';
@@ -41,6 +43,17 @@ abstract class OpenAIRepository {
4143
double temperature = .3,
4244
});
4345

46+
Future<void> getStreamedCompletions({
47+
required String prompt,
48+
required Future<void> Function() onStart,
49+
required Future<void> Function(TextCompletionResponse response) onProcess,
50+
required VoidCallback onEnd,
51+
required void Function(OpenAIError error) onError,
52+
String? suffix,
53+
int maxTokens = 500,
54+
double temperature = 0.3,
55+
});
56+
4457
/// Get edits from GPT-3
4558
///
4659
/// [input] is the input text
@@ -103,6 +116,74 @@ class HttpOpenAIRepository implements OpenAIRepository {
103116
}
104117
}
105118

119+
@override
120+
Future<void> getStreamedCompletions({
121+
required String prompt,
122+
required Future<void> Function() onStart,
123+
required Future<void> Function(TextCompletionResponse response) onProcess,
124+
required VoidCallback onEnd,
125+
required void Function(OpenAIError error) onError,
126+
String? suffix,
127+
int maxTokens = 500,
128+
double temperature = 0.3,
129+
}) async {
130+
final parameters = {
131+
'model': 'text-davinci-003',
132+
'prompt': prompt,
133+
'suffix': suffix,
134+
'max_tokens': maxTokens,
135+
'temperature': temperature,
136+
'stream': true,
137+
};
138+
139+
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
140+
request.headers.addAll(headers);
141+
request.body = jsonEncode(parameters);
142+
143+
final response = await client.send(request);
144+
145+
// NEED TO REFACTOR.
146+
// WHY OPENAI USE TWO LINES TO INDICATE THE START OF THE STREAMING RESPONSE?
147+
// AND WHY OPENAI USE [DONE] TO INDICATE THE END OF THE STREAMING RESPONSE?
148+
int syntax = 0;
149+
var previousSyntax = '';
150+
if (response.statusCode == 200) {
151+
await for (final chunk in response.stream
152+
.transform(const Utf8Decoder())
153+
.transform(const LineSplitter())) {
154+
syntax += 1;
155+
if (syntax == 3) {
156+
await onStart();
157+
continue;
158+
} else if (syntax < 3) {
159+
continue;
160+
}
161+
final data = chunk.trim().split('data: ');
162+
if (data.length > 1 && data[1] != '[DONE]') {
163+
final response = TextCompletionResponse.fromJson(
164+
json.decode(data[1]),
165+
);
166+
if (response.choices.isNotEmpty) {
167+
final text = response.choices.first.text;
168+
if (text == previousSyntax && text == '\n') {
169+
continue;
170+
}
171+
await onProcess(response);
172+
previousSyntax = response.choices.first.text;
173+
Log.editor.info(response.choices.first.text);
174+
}
175+
} else {
176+
onEnd();
177+
}
178+
}
179+
} else {
180+
final body = await response.stream.bytesToString();
181+
onError(
182+
OpenAIError.fromJson(json.decode(body)['error']),
183+
);
184+
}
185+
}
186+
106187
@override
107188
Future<Either<OpenAIError, TextEditResponse>> getEdits({
108189
required String input,

frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/text_completion.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class TextCompletionChoice with _$TextCompletionChoice {
88
required String text,
99
required int index,
1010
// ignore: invalid_annotation_target
11-
@JsonKey(name: 'finish_reason') required String finishReason,
11+
@JsonKey(name: 'finish_reason') String? finishReason,
1212
}) = _TextCompletionChoice;
1313

1414
factory TextCompletionChoice.fromJson(Map<String, Object?> json) =>

frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/editor_extension.dart

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ extension TextRobot on EditorState {
1111
TextRobotInputType inputType = TextRobotInputType.word,
1212
Duration delay = const Duration(milliseconds: 10),
1313
}) async {
14+
if (text == '\n') {
15+
await insertNewLineAtCurrentSelection();
16+
return;
17+
}
1418
final lines = text.split('\n');
1519
for (final line in lines) {
1620
if (line.isEmpty) {
@@ -28,13 +32,21 @@ extension TextRobot on EditorState {
2832
}
2933
break;
3034
case TextRobotInputType.word:
31-
final words = line.split(' ').map((e) => '$e ');
32-
for (final word in words) {
35+
final words = line.split(' ');
36+
if (words.length == 1 ||
37+
(words.length == 2 &&
38+
(words.first.isEmpty || words.last.isEmpty))) {
3339
await insertTextAtCurrentSelection(
34-
word,
40+
line,
3541
);
36-
await Future.delayed(delay, () {});
42+
} else {
43+
for (final word in words.map((e) => '$e ')) {
44+
await insertTextAtCurrentSelection(
45+
word,
46+
);
47+
}
3748
}
49+
await Future.delayed(delay, () {});
3850
break;
3951
}
4052
}

frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,14 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
6161
void initState() {
6262
super.initState();
6363

64-
focusNode.addListener(() {
65-
if (focusNode.hasFocus) {
66-
widget.editorState.service.selectionService.clearSelection();
67-
} else {
68-
widget.editorState.service.keyboardService?.enable();
69-
}
70-
});
64+
textFieldFocusNode.addListener(_onFocusChanged);
7165
textFieldFocusNode.requestFocus();
7266
}
7367

7468
@override
7569
void dispose() {
7670
controller.dispose();
71+
textFieldFocusNode.removeListener(_onFocusChanged);
7772

7873
super.dispose();
7974
}
@@ -242,30 +237,33 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
242237
loading.start();
243238
await _updateEditingText();
244239
final result = await UserBackendService.getCurrentUserProfile();
240+
245241
result.fold((userProfile) async {
246242
final openAIRepository = HttpOpenAIRepository(
247243
client: http.Client(),
248244
apiKey: userProfile.openaiKey,
249245
);
250-
final completions = await openAIRepository.getCompletions(
246+
await openAIRepository.getStreamedCompletions(
251247
prompt: controller.text,
248+
onStart: () async {
249+
loading.stop();
250+
await _makeSurePreviousNodeIsEmptyTextNode();
251+
},
252+
onProcess: (response) async {
253+
if (response.choices.isNotEmpty) {
254+
final text = response.choices.first.text;
255+
await widget.editorState.autoInsertText(
256+
text,
257+
inputType: TextRobotInputType.word,
258+
);
259+
}
260+
},
261+
onEnd: () {},
262+
onError: (error) async {
263+
loading.stop();
264+
await _showError(error.message);
265+
},
252266
);
253-
completions.fold((error) async {
254-
loading.stop();
255-
await _showError(error.message);
256-
}, (textCompletion) async {
257-
loading.stop();
258-
await _makeSurePreviousNodeIsEmptyTextNode();
259-
// Open AI result uses two '\n' as the begin syntax.
260-
var texts = textCompletion.choices.first.text.split('\n');
261-
if (texts.length > 2) {
262-
texts.removeRange(0, 2);
263-
await widget.editorState.autoInsertText(
264-
texts.join('\n'),
265-
);
266-
}
267-
focusNode.requestFocus();
268-
});
269267
}, (error) async {
270268
loading.stop();
271269
await _showError(
@@ -345,4 +343,14 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
345343
),
346344
);
347345
}
346+
347+
void _onFocusChanged() {
348+
if (textFieldFocusNode.hasFocus) {
349+
widget.editorState.service.keyboardService?.disable(
350+
disposition: UnfocusDisposition.previouslyFocusedChild,
351+
);
352+
} else {
353+
widget.editorState.service.keyboardService?.enable();
354+
}
355+
}
348356
}

frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ abstract class AppFlowyKeyboardService {
3535
/// you can disable the keyboard service of flowy_editor.
3636
/// But you need to call the `enable` function to restore after exiting
3737
/// your custom component, otherwise the keyboard service will fails.
38-
void disable({bool showCursor = false});
38+
void disable({
39+
bool showCursor = false,
40+
UnfocusDisposition disposition = UnfocusDisposition.scope,
41+
});
3942
}
4043

4144
/// Process keyboard events
@@ -102,10 +105,13 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
102105
}
103106

104107
@override
105-
void disable({bool showCursor = false}) {
108+
void disable({
109+
bool showCursor = false,
110+
UnfocusDisposition disposition = UnfocusDisposition.scope,
111+
}) {
106112
isFocus = false;
107113
this.showCursor = showCursor;
108-
_focusNode.unfocus();
114+
_focusNode.unfocus(disposition: disposition);
109115
}
110116

111117
@override

0 commit comments

Comments
 (0)