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 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
|
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 'text_completion.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
@ -41,6 +43,17 @@ abstract class OpenAIRepository {
|
|||||||
double temperature = .3,
|
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
|
/// Get edits from GPT-3
|
||||||
///
|
///
|
||||||
/// [input] is the input text
|
/// [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
|
@override
|
||||||
Future<Either<OpenAIError, TextEditResponse>> getEdits({
|
Future<Either<OpenAIError, TextEditResponse>> getEdits({
|
||||||
required String input,
|
required String input,
|
||||||
|
@ -8,7 +8,7 @@ class TextCompletionChoice with _$TextCompletionChoice {
|
|||||||
required String text,
|
required String text,
|
||||||
required int index,
|
required int index,
|
||||||
// ignore: invalid_annotation_target
|
// ignore: invalid_annotation_target
|
||||||
@JsonKey(name: 'finish_reason') required String finishReason,
|
@JsonKey(name: 'finish_reason') String? finishReason,
|
||||||
}) = _TextCompletionChoice;
|
}) = _TextCompletionChoice;
|
||||||
|
|
||||||
factory TextCompletionChoice.fromJson(Map<String, Object?> json) =>
|
factory TextCompletionChoice.fromJson(Map<String, Object?> json) =>
|
||||||
|
@ -11,6 +11,10 @@ extension TextRobot on EditorState {
|
|||||||
TextRobotInputType inputType = TextRobotInputType.word,
|
TextRobotInputType inputType = TextRobotInputType.word,
|
||||||
Duration delay = const Duration(milliseconds: 10),
|
Duration delay = const Duration(milliseconds: 10),
|
||||||
}) async {
|
}) async {
|
||||||
|
if (text == '\n') {
|
||||||
|
await insertNewLineAtCurrentSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
final lines = text.split('\n');
|
final lines = text.split('\n');
|
||||||
for (final line in lines) {
|
for (final line in lines) {
|
||||||
if (line.isEmpty) {
|
if (line.isEmpty) {
|
||||||
@ -28,13 +32,21 @@ extension TextRobot on EditorState {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TextRobotInputType.word:
|
case TextRobotInputType.word:
|
||||||
final words = line.split(' ').map((e) => '$e ');
|
final words = line.split(' ');
|
||||||
for (final word in words) {
|
if (words.length == 1 ||
|
||||||
|
(words.length == 2 &&
|
||||||
|
(words.first.isEmpty || words.last.isEmpty))) {
|
||||||
await insertTextAtCurrentSelection(
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,19 +61,14 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
focusNode.addListener(() {
|
textFieldFocusNode.addListener(_onFocusChanged);
|
||||||
if (focusNode.hasFocus) {
|
|
||||||
widget.editorState.service.selectionService.clearSelection();
|
|
||||||
} else {
|
|
||||||
widget.editorState.service.keyboardService?.enable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
textFieldFocusNode.requestFocus();
|
textFieldFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
|
textFieldFocusNode.removeListener(_onFocusChanged);
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -242,30 +237,33 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
|||||||
loading.start();
|
loading.start();
|
||||||
await _updateEditingText();
|
await _updateEditingText();
|
||||||
final result = await UserBackendService.getCurrentUserProfile();
|
final result = await UserBackendService.getCurrentUserProfile();
|
||||||
|
|
||||||
result.fold((userProfile) async {
|
result.fold((userProfile) async {
|
||||||
final openAIRepository = HttpOpenAIRepository(
|
final openAIRepository = HttpOpenAIRepository(
|
||||||
client: http.Client(),
|
client: http.Client(),
|
||||||
apiKey: userProfile.openaiKey,
|
apiKey: userProfile.openaiKey,
|
||||||
);
|
);
|
||||||
final completions = await openAIRepository.getCompletions(
|
await openAIRepository.getStreamedCompletions(
|
||||||
prompt: controller.text,
|
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 {
|
}, (error) async {
|
||||||
loading.stop();
|
loading.stop();
|
||||||
await _showError(
|
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.
|
/// you can disable the keyboard service of flowy_editor.
|
||||||
/// But you need to call the `enable` function to restore after exiting
|
/// But you need to call the `enable` function to restore after exiting
|
||||||
/// your custom component, otherwise the keyboard service will fails.
|
/// 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
|
/// Process keyboard events
|
||||||
@ -102,10 +105,13 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void disable({bool showCursor = false}) {
|
void disable({
|
||||||
|
bool showCursor = false,
|
||||||
|
UnfocusDisposition disposition = UnfocusDisposition.scope,
|
||||||
|
}) {
|
||||||
isFocus = false;
|
isFocus = false;
|
||||||
this.showCursor = showCursor;
|
this.showCursor = showCursor;
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus(disposition: disposition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Loading…
Reference in New Issue
Block a user