mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: auto generator bugs (#1934)
This commit is contained in:
parent
9e235c578e
commit
e73870e6e2
@ -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,
|
||||
|
@ -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) =>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user