mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support prompt and suffix
This commit is contained in:
parent
494e31993b
commit
fc1efeb70b
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user