Ver código fonte

fix: auto generator bugs (#1934)

Lucas.Xu 2 anos atrás
pai
commit
e73870e6e2

+ 81 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart

@@ -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,

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

@@ -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) =>

+ 16 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/editor_extension.dart

@@ -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;
       }
     }

+ 32 - 24
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart

@@ -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();
+    }
+  }
 }

+ 9 - 3
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart

@@ -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