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/auto_completion.dart';
|
||||
import 'package:example/plugin/AI/getgpt3completions.dart';
|
||||
import 'package:example/plugin/AI/smart_edit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SimpleEditor extends StatelessWidget {
|
||||
@ -73,6 +74,9 @@ class SimpleEditor extends StatelessWidget {
|
||||
continueToWriteMenuItem,
|
||||
]
|
||||
],
|
||||
toolbarItems: [
|
||||
smartEditItem,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
|
@ -29,7 +29,6 @@ SelectionMenuItem continueToWriteMenuItem = SelectionMenuItem(
|
||||
textNode.toPlainText().length,
|
||||
)
|
||||
.toPlainText();
|
||||
debugPrint('AI: prompt = $prompt, suffix = $suffix');
|
||||
final textRobot = TextRobot(editorState: editorState);
|
||||
getGPT3Completion(
|
||||
apiKey,
|
||||
|
@ -14,13 +14,14 @@ Future<void> getGPT3Completion(
|
||||
{
|
||||
int maxTokens = 200,
|
||||
double temperature = .3,
|
||||
bool stream = true,
|
||||
}) async {
|
||||
final data = {
|
||||
'prompt': prompt,
|
||||
'suffix': suffix,
|
||||
'max_tokens': maxTokens,
|
||||
'temperature': temperature,
|
||||
'stream': true, // set stream parameter to true
|
||||
'stream': stream, // set stream parameter to true
|
||||
};
|
||||
|
||||
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/quill_delta/delta_document_encoder.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/render/selection_menu/selection_menu_widget.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:flutter/material.dart';
|
||||
|
||||
@ -60,6 +61,9 @@ class EditorState {
|
||||
/// Stores the selection menu items.
|
||||
List<SelectionMenuItem> selectionMenuItems = [];
|
||||
|
||||
/// Stores the toolbar items.
|
||||
List<ToolbarItem> toolbarItems = [];
|
||||
|
||||
/// Operation stream.
|
||||
Stream<Transaction> get transactionStream => _observer.stream;
|
||||
final StreamController<Transaction> _observer = StreamController.broadcast();
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.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/toolbar/toolbar_item.dart';
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
|
||||
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||
|
||||
@ -30,6 +31,7 @@ class AppFlowyEditor extends StatefulWidget {
|
||||
this.customBuilders = const {},
|
||||
this.shortcutEvents = const [],
|
||||
this.selectionMenuItems = const [],
|
||||
this.toolbarItems = const [],
|
||||
this.editable = true,
|
||||
this.autoFocus = false,
|
||||
ThemeData? themeData,
|
||||
@ -51,6 +53,8 @@ class AppFlowyEditor extends StatefulWidget {
|
||||
|
||||
final List<SelectionMenuItem> selectionMenuItems;
|
||||
|
||||
final List<ToolbarItem> toolbarItems;
|
||||
|
||||
late final ThemeData themeData;
|
||||
|
||||
final bool editable;
|
||||
@ -74,6 +78,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
||||
super.initState();
|
||||
|
||||
editorState.selectionMenuItems = widget.selectionMenuItems;
|
||||
editorState.toolbarItems = widget.toolbarItems;
|
||||
editorState.themeData = widget.themeData;
|
||||
editorState.service.renderPluginService = _createRenderPlugin();
|
||||
editorState.editable = widget.editable;
|
||||
@ -94,6 +99,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
||||
|
||||
if (editorState.service != oldWidget.editorState.service) {
|
||||
editorState.selectionMenuItems = widget.selectionMenuItems;
|
||||
editorState.toolbarItems = widget.toolbarItems;
|
||||
editorState.service.renderPluginService = _createRenderPlugin();
|
||||
}
|
||||
|
||||
|
@ -35,11 +35,20 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
||||
implements AppFlowyToolbarService {
|
||||
OverlayEntry? _toolbarOverlay;
|
||||
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
|
||||
void showInOffset(Offset offset, Alignment alignment, LayerLink layerLink) {
|
||||
hide();
|
||||
final items = _filterItems(defaultToolbarItems);
|
||||
final items = _filterItems(toolbarItems);
|
||||
if (items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@ -65,7 +74,7 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
||||
|
||||
@override
|
||||
bool triggerHandler(String id) {
|
||||
final items = defaultToolbarItems.where((item) => item.id == id);
|
||||
final items = toolbarItems.where((item) => item.id == id);
|
||||
if (items.length != 1) {
|
||||
assert(items.length == 1, 'The toolbar item\'s id must be unique');
|
||||
return false;
|
||||
|
Loading…
Reference in New Issue
Block a user