fix: auto generator bugs (#1934)

This commit is contained in:
Lucas.Xu 2023-03-08 08:58:28 +08:00 committed by GitHub
parent 9e235c578e
commit e73870e6e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 32 deletions

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'text_completion.dart';
import 'package:dartz/dartz.dart';
@ -41,6 +43,17 @@ abstract class OpenAIRepository {
double temperature = .3,
});
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required VoidCallback onEnd,
required void Function(OpenAIError error) onError,
String? suffix,
int maxTokens = 500,
double temperature = 0.3,
});
/// Get edits from GPT-3
///
/// [input] is the input text
@ -103,6 +116,74 @@ class HttpOpenAIRepository implements OpenAIRepository {
}
}
@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required VoidCallback onEnd,
required void Function(OpenAIError error) onError,
String? suffix,
int maxTokens = 500,
double temperature = 0.3,
}) async {
final parameters = {
'model': 'text-davinci-003',
'prompt': prompt,
'suffix': suffix,
'max_tokens': maxTokens,
'temperature': temperature,
'stream': true,
};
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
request.headers.addAll(headers);
request.body = jsonEncode(parameters);
final response = await client.send(request);
// NEED TO REFACTOR.
// WHY OPENAI USE TWO LINES TO INDICATE THE START OF THE STREAMING RESPONSE?
// AND WHY OPENAI USE [DONE] TO INDICATE THE END OF THE STREAMING RESPONSE?
int syntax = 0;
var previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream
.transform(const Utf8Decoder())
.transform(const LineSplitter())) {
syntax += 1;
if (syntax == 3) {
await onStart();
continue;
} else if (syntax < 3) {
continue;
}
final data = chunk.trim().split('data: ');
if (data.length > 1 && data[1] != '[DONE]') {
final response = TextCompletionResponse.fromJson(
json.decode(data[1]),
);
if (response.choices.isNotEmpty) {
final text = response.choices.first.text;
if (text == previousSyntax && text == '\n') {
continue;
}
await onProcess(response);
previousSyntax = response.choices.first.text;
Log.editor.info(response.choices.first.text);
}
} else {
onEnd();
}
}
} else {
final body = await response.stream.bytesToString();
onError(
OpenAIError.fromJson(json.decode(body)['error']),
);
}
}
@override
Future<Either<OpenAIError, TextEditResponse>> getEdits({
required String input,

View File

@ -8,7 +8,7 @@ class TextCompletionChoice with _$TextCompletionChoice {
required String text,
required int index,
// ignore: invalid_annotation_target
@JsonKey(name: 'finish_reason') required String finishReason,
@JsonKey(name: 'finish_reason') String? finishReason,
}) = _TextCompletionChoice;
factory TextCompletionChoice.fromJson(Map<String, Object?> json) =>

View File

@ -11,6 +11,10 @@ extension TextRobot on EditorState {
TextRobotInputType inputType = TextRobotInputType.word,
Duration delay = const Duration(milliseconds: 10),
}) async {
if (text == '\n') {
await insertNewLineAtCurrentSelection();
return;
}
final lines = text.split('\n');
for (final line in lines) {
if (line.isEmpty) {
@ -28,13 +32,21 @@ extension TextRobot on EditorState {
}
break;
case TextRobotInputType.word:
final words = line.split(' ').map((e) => '$e ');
for (final word in words) {
final words = line.split(' ');
if (words.length == 1 ||
(words.length == 2 &&
(words.first.isEmpty || words.last.isEmpty))) {
await insertTextAtCurrentSelection(
word,
line,
);
await Future.delayed(delay, () {});
} else {
for (final word in words.map((e) => '$e ')) {
await insertTextAtCurrentSelection(
word,
);
}
}
await Future.delayed(delay, () {});
break;
}
}

View File

@ -61,19 +61,14 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
void initState() {
super.initState();
focusNode.addListener(() {
if (focusNode.hasFocus) {
widget.editorState.service.selectionService.clearSelection();
} else {
widget.editorState.service.keyboardService?.enable();
}
});
textFieldFocusNode.addListener(_onFocusChanged);
textFieldFocusNode.requestFocus();
}
@override
void dispose() {
controller.dispose();
textFieldFocusNode.removeListener(_onFocusChanged);
super.dispose();
}
@ -242,30 +237,33 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
loading.start();
await _updateEditingText();
final result = await UserBackendService.getCurrentUserProfile();
result.fold((userProfile) async {
final openAIRepository = HttpOpenAIRepository(
client: http.Client(),
apiKey: userProfile.openaiKey,
);
final completions = await openAIRepository.getCompletions(
await openAIRepository.getStreamedCompletions(
prompt: controller.text,
onStart: () async {
loading.stop();
await _makeSurePreviousNodeIsEmptyTextNode();
},
onProcess: (response) async {
if (response.choices.isNotEmpty) {
final text = response.choices.first.text;
await widget.editorState.autoInsertText(
text,
inputType: TextRobotInputType.word,
);
}
},
onEnd: () {},
onError: (error) async {
loading.stop();
await _showError(error.message);
},
);
completions.fold((error) async {
loading.stop();
await _showError(error.message);
}, (textCompletion) async {
loading.stop();
await _makeSurePreviousNodeIsEmptyTextNode();
// Open AI result uses two '\n' as the begin syntax.
var texts = textCompletion.choices.first.text.split('\n');
if (texts.length > 2) {
texts.removeRange(0, 2);
await widget.editorState.autoInsertText(
texts.join('\n'),
);
}
focusNode.requestFocus();
});
}, (error) async {
loading.stop();
await _showError(
@ -345,4 +343,14 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
),
);
}
void _onFocusChanged() {
if (textFieldFocusNode.hasFocus) {
widget.editorState.service.keyboardService?.disable(
disposition: UnfocusDisposition.previouslyFocusedChild,
);
} else {
widget.editorState.service.keyboardService?.enable();
}
}
}

View File

@ -35,7 +35,10 @@ abstract class AppFlowyKeyboardService {
/// you can disable the keyboard service of flowy_editor.
/// But you need to call the `enable` function to restore after exiting
/// your custom component, otherwise the keyboard service will fails.
void disable({bool showCursor = false});
void disable({
bool showCursor = false,
UnfocusDisposition disposition = UnfocusDisposition.scope,
});
}
/// Process keyboard events
@ -102,10 +105,13 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
}
@override
void disable({bool showCursor = false}) {
void disable({
bool showCursor = false,
UnfocusDisposition disposition = UnfocusDisposition.scope,
}) {
isFocus = false;
this.showCursor = showCursor;
_focusNode.unfocus();
_focusNode.unfocus(disposition: disposition);
}
@override