mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add sample for open AI editing
This commit is contained in:
parent
fc1efeb70b
commit
310236dca0
@ -5,6 +5,7 @@ import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
|||||||
import 'package:example/plugin/AI/continue_to_write.dart';
|
import 'package:example/plugin/AI/continue_to_write.dart';
|
||||||
import 'package:example/plugin/AI/auto_completion.dart';
|
import 'package:example/plugin/AI/auto_completion.dart';
|
||||||
import 'package:example/plugin/AI/getgpt3completions.dart';
|
import 'package:example/plugin/AI/getgpt3completions.dart';
|
||||||
|
import 'package:example/plugin/AI/smart_edit.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SimpleEditor extends StatelessWidget {
|
class SimpleEditor extends StatelessWidget {
|
||||||
@ -73,6 +74,9 @@ class SimpleEditor extends StatelessWidget {
|
|||||||
continueToWriteMenuItem,
|
continueToWriteMenuItem,
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
toolbarItems: [
|
||||||
|
smartEditItem,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return const Center(
|
return const Center(
|
||||||
|
@ -29,7 +29,6 @@ SelectionMenuItem continueToWriteMenuItem = SelectionMenuItem(
|
|||||||
textNode.toPlainText().length,
|
textNode.toPlainText().length,
|
||||||
)
|
)
|
||||||
.toPlainText();
|
.toPlainText();
|
||||||
debugPrint('AI: prompt = $prompt, suffix = $suffix');
|
|
||||||
final textRobot = TextRobot(editorState: editorState);
|
final textRobot = TextRobot(editorState: editorState);
|
||||||
getGPT3Completion(
|
getGPT3Completion(
|
||||||
apiKey,
|
apiKey,
|
||||||
|
@ -14,13 +14,14 @@ Future<void> getGPT3Completion(
|
|||||||
{
|
{
|
||||||
int maxTokens = 200,
|
int maxTokens = 200,
|
||||||
double temperature = .3,
|
double temperature = .3,
|
||||||
|
bool stream = true,
|
||||||
}) async {
|
}) async {
|
||||||
final data = {
|
final data = {
|
||||||
'prompt': prompt,
|
'prompt': prompt,
|
||||||
'suffix': suffix,
|
'suffix': suffix,
|
||||||
'max_tokens': maxTokens,
|
'max_tokens': maxTokens,
|
||||||
'temperature': temperature,
|
'temperature': temperature,
|
||||||
'stream': true, // set stream parameter to true
|
'stream': stream, // set stream parameter to true
|
||||||
};
|
};
|
||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
@ -70,3 +71,41 @@ Future<void> getGPT3Completion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> getGPT3Edit(
|
||||||
|
String apiKey,
|
||||||
|
String input,
|
||||||
|
String instruction, {
|
||||||
|
required Future<void> Function(List<String> result) onResult,
|
||||||
|
required Future<void> Function() onError,
|
||||||
|
int n = 1,
|
||||||
|
double temperature = .3,
|
||||||
|
}) async {
|
||||||
|
final data = {
|
||||||
|
'model': 'text-davinci-edit-001',
|
||||||
|
'input': input,
|
||||||
|
'instruction': instruction,
|
||||||
|
'temperature': temperature,
|
||||||
|
'n': n,
|
||||||
|
};
|
||||||
|
|
||||||
|
final headers = {
|
||||||
|
'Authorization': apiKey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await http.post(
|
||||||
|
Uri.parse('https://api.openai.com/v1/edits'),
|
||||||
|
headers: headers,
|
||||||
|
body: json.encode(data),
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final result = json.decode(response.body);
|
||||||
|
final choices = result['choices'];
|
||||||
|
if (choices != null && choices is List) {
|
||||||
|
onResult(choices.map((e) => e['text'] as String).toList());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,198 @@
|
|||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:example/plugin/AI/getgpt3completions.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
ToolbarItem smartEditItem = ToolbarItem(
|
||||||
|
id: 'appflowy.toolbar.smart_edit',
|
||||||
|
type: 5,
|
||||||
|
iconBuilder: (isHighlight) {
|
||||||
|
return Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: isHighlight ? Colors.lightBlue : Colors.white,
|
||||||
|
size: 14,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
validator: (editorState) {
|
||||||
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
|
return nodes.whereType<TextNode>().length == nodes.length &&
|
||||||
|
1 == nodes.length;
|
||||||
|
},
|
||||||
|
highlightCallback: (_) => false,
|
||||||
|
tooltipsMessage: 'Smart Edit',
|
||||||
|
handler: (editorState, context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: SmartEditWidget(
|
||||||
|
editorState: editorState,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
class SmartEditWidget extends StatefulWidget {
|
||||||
|
const SmartEditWidget({
|
||||||
|
super.key,
|
||||||
|
required this.editorState,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SmartEditWidget> createState() => _SmartEditWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SmartEditWidgetState extends State<SmartEditWidget> {
|
||||||
|
final inputEventController = TextEditingController(text: '');
|
||||||
|
final resultController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
var result = '';
|
||||||
|
|
||||||
|
Iterable<TextNode> get currentSelectedTextNodes =>
|
||||||
|
widget.editorState.service.selectionService.currentSelectedNodes
|
||||||
|
.whereType<TextNode>();
|
||||||
|
Selection? get currentSelection =>
|
||||||
|
widget.editorState.service.selectionService.currentSelection.value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
RawKeyboardListener(
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
child: TextField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: inputEventController,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
hintText: 'Describe how you\'d like AppFlowy to edit this text',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onKey: (key) {
|
||||||
|
if (key is! RawKeyDownEvent) return;
|
||||||
|
if (key.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
|
_requestGPT3EditResult();
|
||||||
|
} else if (key.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (result.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Result: ',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: TextField(
|
||||||
|
controller: resultController..text = result,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
hintText:
|
||||||
|
'Describe how you\'d like AppFlowy to edit this text',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// replace the text
|
||||||
|
final selection = currentSelection;
|
||||||
|
if (selection != null) {
|
||||||
|
assert(selection.isSingle);
|
||||||
|
final transaction = widget.editorState.transaction;
|
||||||
|
transaction.replaceText(
|
||||||
|
currentSelectedTextNodes.first,
|
||||||
|
selection.startIndex,
|
||||||
|
selection.length,
|
||||||
|
resultController.text,
|
||||||
|
);
|
||||||
|
widget.editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Replace'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requestGPT3EditResult() {
|
||||||
|
final selection =
|
||||||
|
widget.editorState.service.selectionService.currentSelection.value;
|
||||||
|
if (selection == null || !selection.isSingle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final text =
|
||||||
|
widget.editorState.service.selectionService.currentSelectedNodes
|
||||||
|
.whereType<TextNode>()
|
||||||
|
.first
|
||||||
|
.delta
|
||||||
|
.slice(
|
||||||
|
selection.startIndex,
|
||||||
|
selection.endIndex,
|
||||||
|
)
|
||||||
|
.toPlainText();
|
||||||
|
if (text.isEmpty) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
Text('Loading'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
getGPT3Edit(
|
||||||
|
apiKey,
|
||||||
|
text,
|
||||||
|
inputEventController.text,
|
||||||
|
onResult: (result) async {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
setState(() {
|
||||||
|
this.result = result.join('\n').trim();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () async {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -43,3 +43,4 @@ export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
|
|||||||
export 'src/plugins/markdown/document_markdown.dart';
|
export 'src/plugins/markdown/document_markdown.dart';
|
||||||
export 'src/plugins/quill_delta/delta_document_encoder.dart';
|
export 'src/plugins/quill_delta/delta_document_encoder.dart';
|
||||||
export 'src/commands/text/text_commands.dart';
|
export 'src/commands/text/text_commands.dart';
|
||||||
|
export 'src/render/toolbar/toolbar_item.dart';
|
||||||
|
@ -3,6 +3,7 @@ import 'package:appflowy_editor/src/core/document/node.dart';
|
|||||||
import 'package:appflowy_editor/src/infra/log.dart';
|
import 'package:appflowy_editor/src/infra/log.dart';
|
||||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
||||||
import 'package:appflowy_editor/src/render/style/editor_style.dart';
|
import 'package:appflowy_editor/src/render/style/editor_style.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
|
||||||
import 'package:appflowy_editor/src/service/service.dart';
|
import 'package:appflowy_editor/src/service/service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -60,6 +61,9 @@ class EditorState {
|
|||||||
/// Stores the selection menu items.
|
/// Stores the selection menu items.
|
||||||
List<SelectionMenuItem> selectionMenuItems = [];
|
List<SelectionMenuItem> selectionMenuItems = [];
|
||||||
|
|
||||||
|
/// Stores the toolbar items.
|
||||||
|
List<ToolbarItem> toolbarItems = [];
|
||||||
|
|
||||||
/// Operation stream.
|
/// Operation stream.
|
||||||
Stream<Transaction> get transactionStream => _observer.stream;
|
Stream<Transaction> get transactionStream => _observer.stream;
|
||||||
final StreamController<Transaction> _observer = StreamController.broadcast();
|
final StreamController<Transaction> _observer = StreamController.broadcast();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
|
import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
|
||||||
|
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
|
||||||
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
|
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
|
||||||
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ class AppFlowyEditor extends StatefulWidget {
|
|||||||
this.customBuilders = const {},
|
this.customBuilders = const {},
|
||||||
this.shortcutEvents = const [],
|
this.shortcutEvents = const [],
|
||||||
this.selectionMenuItems = const [],
|
this.selectionMenuItems = const [],
|
||||||
|
this.toolbarItems = const [],
|
||||||
this.editable = true,
|
this.editable = true,
|
||||||
this.autoFocus = false,
|
this.autoFocus = false,
|
||||||
ThemeData? themeData,
|
ThemeData? themeData,
|
||||||
@ -51,6 +53,8 @@ class AppFlowyEditor extends StatefulWidget {
|
|||||||
|
|
||||||
final List<SelectionMenuItem> selectionMenuItems;
|
final List<SelectionMenuItem> selectionMenuItems;
|
||||||
|
|
||||||
|
final List<ToolbarItem> toolbarItems;
|
||||||
|
|
||||||
late final ThemeData themeData;
|
late final ThemeData themeData;
|
||||||
|
|
||||||
final bool editable;
|
final bool editable;
|
||||||
@ -74,6 +78,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
editorState.selectionMenuItems = widget.selectionMenuItems;
|
editorState.selectionMenuItems = widget.selectionMenuItems;
|
||||||
|
editorState.toolbarItems = widget.toolbarItems;
|
||||||
editorState.themeData = widget.themeData;
|
editorState.themeData = widget.themeData;
|
||||||
editorState.service.renderPluginService = _createRenderPlugin();
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
editorState.editable = widget.editable;
|
editorState.editable = widget.editable;
|
||||||
@ -94,6 +99,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
|
|
||||||
if (editorState.service != oldWidget.editorState.service) {
|
if (editorState.service != oldWidget.editorState.service) {
|
||||||
editorState.selectionMenuItems = widget.selectionMenuItems;
|
editorState.selectionMenuItems = widget.selectionMenuItems;
|
||||||
|
editorState.toolbarItems = widget.toolbarItems;
|
||||||
editorState.service.renderPluginService = _createRenderPlugin();
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,11 +35,20 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
|||||||
implements AppFlowyToolbarService {
|
implements AppFlowyToolbarService {
|
||||||
OverlayEntry? _toolbarOverlay;
|
OverlayEntry? _toolbarOverlay;
|
||||||
final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget');
|
final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget');
|
||||||
|
late final List<ToolbarItem> toolbarItems;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
toolbarItems = [...defaultToolbarItems, ...widget.editorState.toolbarItems]
|
||||||
|
..sort((a, b) => a.type.compareTo(b.type));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void showInOffset(Offset offset, Alignment alignment, LayerLink layerLink) {
|
void showInOffset(Offset offset, Alignment alignment, LayerLink layerLink) {
|
||||||
hide();
|
hide();
|
||||||
final items = _filterItems(defaultToolbarItems);
|
final items = _filterItems(toolbarItems);
|
||||||
if (items.isEmpty) {
|
if (items.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -65,7 +74,7 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool triggerHandler(String id) {
|
bool triggerHandler(String id) {
|
||||||
final items = defaultToolbarItems.where((item) => item.id == id);
|
final items = toolbarItems.where((item) => item.id == id);
|
||||||
if (items.length != 1) {
|
if (items.length != 1) {
|
||||||
assert(items.length == 1, 'The toolbar item\'s id must be unique');
|
assert(items.length == 1, 'The toolbar item\'s id must be unique');
|
||||||
return false;
|
return false;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user