feat: support prompt and suffix

This commit is contained in:
Lucas.Xu 2023-01-08 13:17:18 +08:00
parent 494e31993b
commit fc1efeb70b
9 changed files with 221 additions and 120 deletions

View File

@ -4,7 +4,7 @@ import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:example/pages/simple_editor.dart';
import 'package:example/plugin/text_robot.dart';
import 'package:example/plugin/AI/text_robot.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

View File

@ -2,7 +2,9 @@ import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:example/plugin/text_robot.dart';
import 'package:example/plugin/AI/continue_to_write.dart';
import 'package:example/plugin/AI/auto_completion.dart';
import 'package:example/plugin/AI/getgpt3completions.dart';
import 'package:flutter/material.dart';
class SimpleEditor extends StatelessWidget {
@ -65,8 +67,11 @@ class SimpleEditor extends StatelessWidget {
codeBlockMenuItem,
// Emoji
emojiMenuItem,
// Text Robot
textRobotMenuItem,
// Open AI
if (apiKey.isNotEmpty) ...[
autoCompletionMenuItem,
continueToWriteMenuItem,
]
],
);
} else {

View File

@ -1,10 +1,11 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:example/plugin/AI/getgpt3completions.dart';
import 'package:example/plugin/AI/text_robot.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
SelectionMenuItem textRobotMenuItem = SelectionMenuItem(
name: () => 'Open AI',
SelectionMenuItem autoCompletionMenuItem = SelectionMenuItem(
name: () => 'Auto generate content',
icon: (editorState, onSelected) => Icon(
Icons.rocket,
size: 18.0,
@ -12,7 +13,7 @@ SelectionMenuItem textRobotMenuItem = SelectionMenuItem(
? editorState.editorStyle.selectionMenuItemSelectedIconColor
: editorState.editorStyle.selectionMenuItemIconColor,
),
keywords: ['open ai', 'gpt3', 'ai'],
keywords: ['auto generate content', 'open ai', 'gpt3', 'ai'],
handler: ((editorState, menuService, context) async {
showDialog(
context: context,
@ -35,11 +36,11 @@ SelectionMenuItem textRobotMenuItem = SelectionMenuItem(
if (key.logicalKey == LogicalKeyboardKey.enter) {
Navigator.of(context).pop();
// fetch the result and insert it
// Please fill in your own API key
getGPT3Completion('', controller.text, '', 200, .3,
(result) async {
await editorState.insertTextAtCurrentSelection(
final textRobot = TextRobot(editorState: editorState);
getGPT3Completion(apiKey, controller.text, '', (result) async {
await textRobot.insertText(
result,
inputType: TextRobotInputType.character,
);
});
} else if (key.logicalKey == LogicalKeyboardKey.escape) {
@ -52,44 +53,3 @@ SelectionMenuItem textRobotMenuItem = SelectionMenuItem(
);
}),
);
enum TextRobotInputType {
character,
word,
}
class TextRobot {
const TextRobot({
required this.editorState,
this.delay = const Duration(milliseconds: 30),
});
final EditorState editorState;
final Duration delay;
Future<void> insertText(
String text, {
TextRobotInputType inputType = TextRobotInputType.character,
}) async {
final lines = text.split('\n');
for (final line in lines) {
switch (inputType) {
case TextRobotInputType.character:
final iterator = line.runes.iterator;
while (iterator.moveNext()) {
await editorState.insertTextAtCurrentSelection(
iterator.currentAsString,
);
await Future.delayed(delay);
}
break;
default:
}
// insert new line
if (lines.length > 1) {
await editorState.insertNewLine(editorState);
}
}
}
}

View File

@ -0,0 +1,50 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:example/plugin/AI/getgpt3completions.dart';
import 'package:example/plugin/AI/text_robot.dart';
import 'package:flutter/material.dart';
SelectionMenuItem continueToWriteMenuItem = SelectionMenuItem(
name: () => 'Continue To Write',
icon: (editorState, onSelected) => Icon(
Icons.print,
size: 18.0,
color: onSelected
? editorState.editorStyle.selectionMenuItemSelectedIconColor
: editorState.editorStyle.selectionMenuItemIconColor,
),
keywords: ['continue to write'],
handler: ((editorState, menuService, context) async {
// get the current text
final selection =
editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes;
if (selection == null || !selection.isCollapsed || textNodes.length != 1) {
return;
}
final textNode = textNodes.first as TextNode;
final prompt = textNode.delta.slice(0, selection.startIndex).toPlainText();
final suffix = textNode.delta
.slice(
selection.endIndex,
textNode.toPlainText().length,
)
.toPlainText();
debugPrint('AI: prompt = $prompt, suffix = $suffix');
final textRobot = TextRobot(editorState: editorState);
getGPT3Completion(
apiKey,
prompt,
suffix,
(result) async {
if (result == '\\n') {
await editorState.insertNewLineAtCurrentSelection();
} else {
await textRobot.insertText(
result,
inputType: TextRobotInputType.word,
);
}
},
);
}),
);

View File

@ -2,14 +2,19 @@ import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';
// Please fill in your own API key
const apiKey = '';
Future<void> getGPT3Completion(
String apiKey,
String prompt,
String suffix,
int maxTokens,
double temperature,
Function(String) onData, // callback function to handle streaming data
) async {
Future<void> Function(String)
onData, // callback function to handle streaming data
{
int maxTokens = 200,
double temperature = .3,
}) async {
final data = {
'prompt': prompt,
'suffix': suffix,
@ -43,7 +48,6 @@ Future<void> getGPT3Completion(
}
final processedText = text
.replaceAll('\\n', '\n')
.replaceAll('\\r', '\r')
.replaceAll('\\t', '\t')
.replaceAll('\\b', '\b')
@ -62,7 +66,7 @@ Future<void> getGPT3Completion(
.replaceAll('\\8', '8')
.replaceAll('\\9', '9');
onData(processedText);
await onData(processedText);
}
}
}

View File

@ -0,0 +1,48 @@
import 'package:appflowy_editor/appflowy_editor.dart';
enum TextRobotInputType {
character,
word,
}
class TextRobot {
const TextRobot({
required this.editorState,
this.delay = const Duration(milliseconds: 30),
});
final EditorState editorState;
final Duration delay;
Future<void> insertText(
String text, {
TextRobotInputType inputType = TextRobotInputType.character,
}) async {
final lines = text.split('\\n');
for (final line in lines) {
if (line.isEmpty) continue;
switch (inputType) {
case TextRobotInputType.character:
final iterator = line.runes.iterator;
while (iterator.moveNext()) {
await editorState.insertTextAtCurrentSelection(
iterator.currentAsString,
);
await Future.delayed(delay, () {});
}
break;
case TextRobotInputType.word:
await editorState.insertTextAtCurrentSelection(
line,
);
await Future.delayed(delay, () {});
break;
}
// insert new line
if (lines.length > 1) {
await editorState.insertNewLineAtCurrentSelection();
}
}
}
}

View File

@ -12,25 +12,21 @@ extension TextCommands on EditorState {
Path? path,
TextNode? textNode,
}) async {
return futureCommand(() {
final n = getTextNode(path: path, textNode: textNode);
apply(
transaction..insertText(n, index, text),
);
});
final n = getTextNode(path: path, textNode: textNode);
return apply(
transaction..insertText(n, index, text),
);
}
Future<void> insertTextAtCurrentSelection(String text) async {
return futureCommand(() async {
final selection = getSelection(null);
assert(selection.isCollapsed);
final textNode = getTextNode(path: selection.start.path);
await insertText(
textNode.toPlainText().length,
text,
textNode: textNode,
);
});
final selection = getSelection(null);
assert(selection.isCollapsed);
final textNode = getTextNode(path: selection.start.path);
return insertText(
selection.startIndex,
text,
textNode: textNode,
);
}
Future<void> formatText(
@ -40,13 +36,11 @@ extension TextCommands on EditorState {
Path? path,
TextNode? textNode,
}) async {
return futureCommand(() {
final n = getTextNode(path: path, textNode: textNode);
final s = getSelection(selection);
apply(
transaction..formatText(n, s.startIndex, s.length, attributes),
);
});
final n = getTextNode(path: path, textNode: textNode);
final s = getSelection(selection);
return apply(
transaction..formatText(n, s.startIndex, s.length, attributes),
);
}
Future<void> formatTextWithBuiltInAttribute(
@ -57,26 +51,24 @@ extension TextCommands on EditorState {
Path? path,
TextNode? textNode,
}) async {
return futureCommand(() {
final n = getTextNode(path: path, textNode: textNode);
if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
final attr = n.attributes
..removeWhere(
(key, _) => BuiltInAttributeKey.globalStyleKeys.contains(key))
..addAll(attributes)
..addAll({
BuiltInAttributeKey.subtype: key,
});
apply(
transaction..updateNode(n, attr),
);
} else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) {
final s = getSelection(selection);
apply(
transaction..formatText(n, s.startIndex, s.length, attributes),
);
}
});
final n = getTextNode(path: path, textNode: textNode);
if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
final attr = n.attributes
..removeWhere(
(key, _) => BuiltInAttributeKey.globalStyleKeys.contains(key))
..addAll(attributes)
..addAll({
BuiltInAttributeKey.subtype: key,
});
return apply(
transaction..updateNode(n, attr),
);
} else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) {
final s = getSelection(selection);
return apply(
transaction..formatText(n, s.startIndex, s.length, attributes),
);
}
}
Future<void> formatTextToCheckbox(
@ -109,19 +101,28 @@ extension TextCommands on EditorState {
);
}
Future<void> insertNewLine(
EditorState editorState, {
Future<void> insertNewLine({
Path? path,
}) async {
return futureCommand(() async {
final p = path ?? getSelection(null).start.path.next;
final transaction = editorState.transaction;
transaction.insertNode(p, TextNode.empty());
transaction.afterSelection = Selection.single(
path: p,
startOffset: 0,
);
apply(transaction);
});
final p = path ?? getSelection(null).start.path.next;
final transaction = this.transaction;
transaction.insertNode(p, TextNode.empty());
transaction.afterSelection = Selection.single(
path: p,
startOffset: 0,
);
return apply(transaction);
}
Future<void> insertNewLineAtCurrentSelection() async {
final selection = getSelection(null);
assert(selection.isCollapsed);
final textNode = getTextNode(path: selection.start.path);
final transaction = this.transaction;
transaction.splitText(
textNode,
selection.startIndex,
);
return apply(transaction);
}
}

View File

@ -169,6 +169,25 @@ extension TextTransaction on Transaction {
));
}
void splitText(TextNode textNode, int offset) {
final delta = textNode.delta;
final first = delta.slice(0, offset);
final second = delta.slice(offset, delta.length);
final path = textNode.path.next;
updateText(textNode, first);
insertNode(
path,
TextNode(
attributes: textNode.attributes,
delta: second,
),
);
afterSelection = Selection.collapsed(Position(
path: path,
offset: 0,
));
}
/// Inserts the text content at a specified index.
///
/// Optionally, you may specify formatting attributes that are applied to the inserted string.

View File

@ -96,13 +96,21 @@ class EditorState {
return null;
}
updateCursorSelection(Selection? cursorSelection,
[CursorUpdateReason reason = CursorUpdateReason.others]) {
Future<void> updateCursorSelection(
Selection? cursorSelection, [
CursorUpdateReason reason = CursorUpdateReason.others,
]) {
final completer = Completer<void>();
// broadcast to other users here
if (reason != CursorUpdateReason.uiEvent) {
service.selectionService.updateSelection(cursorSelection);
}
_cursorSelection = cursorSelection;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}
Timer? _debouncedSealHistoryItemTimer;
@ -121,14 +129,17 @@ class EditorState {
///
/// The options can be used to determine whether the editor
/// should record the transaction in undo/redo stack.
void apply(
Future<void> apply(
Transaction transaction, {
ApplyOptions options = const ApplyOptions(recordUndo: true),
ruleCount = 0,
withUpdateCursor = true,
}) {
}) async {
final completer = Completer<void>();
if (!editable) {
return;
completer.complete();
return completer.future;
}
// TODO: validate the transation.
for (final op in transaction.operations) {
@ -137,10 +148,11 @@ class EditorState {
_observer.add(transaction);
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_applyRules(ruleCount);
if (withUpdateCursor) {
updateCursorSelection(transaction.afterSelection);
await updateCursorSelection(transaction.afterSelection);
completer.complete();
}
});
@ -160,6 +172,8 @@ class EditorState {
redoItem.afterSelection = transaction.afterSelection;
undoManager.redoStack.push(redoItem);
}
return completer.future;
}
void _debouncedSealHistoryItem() {