Skip to content

Commit 310236d

Browse files
committed
feat: add sample for open AI editing
1 parent fc1efeb commit 310236d

File tree

8 files changed

+264
-4
lines changed

8 files changed

+264
-4
lines changed

frontend/app_flowy/packages/appflowy_editor/example/lib/pages/simple_editor.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
55
import 'package:example/plugin/AI/continue_to_write.dart';
66
import 'package:example/plugin/AI/auto_completion.dart';
77
import 'package:example/plugin/AI/getgpt3completions.dart';
8+
import 'package:example/plugin/AI/smart_edit.dart';
89
import 'package:flutter/material.dart';
910

1011
class SimpleEditor extends StatelessWidget {
@@ -73,6 +74,9 @@ class SimpleEditor extends StatelessWidget {
7374
continueToWriteMenuItem,
7475
]
7576
],
77+
toolbarItems: [
78+
smartEditItem,
79+
],
7680
);
7781
} else {
7882
return const Center(

frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/AI/continue_to_write.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ SelectionMenuItem continueToWriteMenuItem = SelectionMenuItem(
2929
textNode.toPlainText().length,
3030
)
3131
.toPlainText();
32-
debugPrint('AI: prompt = $prompt, suffix = $suffix');
3332
final textRobot = TextRobot(editorState: editorState);
3433
getGPT3Completion(
3534
apiKey,

frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/AI/getgpt3completions.dart

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ Future<void> getGPT3Completion(
1414
{
1515
int maxTokens = 200,
1616
double temperature = .3,
17+
bool stream = true,
1718
}) async {
1819
final data = {
1920
'prompt': prompt,
2021
'suffix': suffix,
2122
'max_tokens': maxTokens,
2223
'temperature': temperature,
23-
'stream': true, // set stream parameter to true
24+
'stream': stream, // set stream parameter to true
2425
};
2526

2627
final headers = {
@@ -70,3 +71,41 @@ Future<void> getGPT3Completion(
7071
}
7172
}
7273
}
74+
75+
Future<void> getGPT3Edit(
76+
String apiKey,
77+
String input,
78+
String instruction, {
79+
required Future<void> Function(List<String> result) onResult,
80+
required Future<void> Function() onError,
81+
int n = 1,
82+
double temperature = .3,
83+
}) async {
84+
final data = {
85+
'model': 'text-davinci-edit-001',
86+
'input': input,
87+
'instruction': instruction,
88+
'temperature': temperature,
89+
'n': n,
90+
};
91+
92+
final headers = {
93+
'Authorization': apiKey,
94+
'Content-Type': 'application/json',
95+
};
96+
97+
var response = await http.post(
98+
Uri.parse('https://api.openai.com/v1/edits'),
99+
headers: headers,
100+
body: json.encode(data),
101+
);
102+
if (response.statusCode == 200) {
103+
final result = json.decode(response.body);
104+
final choices = result['choices'];
105+
if (choices != null && choices is List) {
106+
onResult(choices.map((e) => e['text'] as String).toList());
107+
}
108+
} else {
109+
onError();
110+
}
111+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import 'package:appflowy_editor/appflowy_editor.dart';
2+
import 'package:example/plugin/AI/getgpt3completions.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter/services.dart';
5+
6+
ToolbarItem smartEditItem = ToolbarItem(
7+
id: 'appflowy.toolbar.smart_edit',
8+
type: 5,
9+
iconBuilder: (isHighlight) {
10+
return Icon(
11+
Icons.edit,
12+
color: isHighlight ? Colors.lightBlue : Colors.white,
13+
size: 14,
14+
);
15+
},
16+
validator: (editorState) {
17+
final nodes = editorState.service.selectionService.currentSelectedNodes;
18+
return nodes.whereType<TextNode>().length == nodes.length &&
19+
1 == nodes.length;
20+
},
21+
highlightCallback: (_) => false,
22+
tooltipsMessage: 'Smart Edit',
23+
handler: (editorState, context) {
24+
showDialog(
25+
context: context,
26+
builder: (context) {
27+
return AlertDialog(
28+
content: SmartEditWidget(
29+
editorState: editorState,
30+
),
31+
);
32+
},
33+
);
34+
},
35+
);
36+
37+
class SmartEditWidget extends StatefulWidget {
38+
const SmartEditWidget({
39+
super.key,
40+
required this.editorState,
41+
});
42+
43+
final EditorState editorState;
44+
45+
@override
46+
State<SmartEditWidget> createState() => _SmartEditWidgetState();
47+
}
48+
49+
class _SmartEditWidgetState extends State<SmartEditWidget> {
50+
final inputEventController = TextEditingController(text: '');
51+
final resultController = TextEditingController(text: '');
52+
53+
var result = '';
54+
55+
Iterable<TextNode> get currentSelectedTextNodes =>
56+
widget.editorState.service.selectionService.currentSelectedNodes
57+
.whereType<TextNode>();
58+
Selection? get currentSelection =>
59+
widget.editorState.service.selectionService.currentSelection.value;
60+
61+
@override
62+
Widget build(BuildContext context) {
63+
return Container(
64+
constraints: const BoxConstraints(maxWidth: 400),
65+
child: Column(
66+
crossAxisAlignment: CrossAxisAlignment.start,
67+
mainAxisSize: MainAxisSize.min,
68+
children: [
69+
RawKeyboardListener(
70+
focusNode: FocusNode(),
71+
child: TextField(
72+
autofocus: true,
73+
controller: inputEventController,
74+
maxLines: null,
75+
decoration: const InputDecoration(
76+
border: OutlineInputBorder(),
77+
hintText: 'Describe how you\'d like AppFlowy to edit this text',
78+
),
79+
),
80+
onKey: (key) {
81+
if (key is! RawKeyDownEvent) return;
82+
if (key.logicalKey == LogicalKeyboardKey.enter) {
83+
_requestGPT3EditResult();
84+
} else if (key.logicalKey == LogicalKeyboardKey.escape) {
85+
Navigator.of(context).pop();
86+
}
87+
},
88+
),
89+
if (result.isNotEmpty) ...[
90+
const SizedBox(height: 20),
91+
const Text(
92+
'Result: ',
93+
style: TextStyle(color: Colors.grey),
94+
),
95+
const SizedBox(height: 10),
96+
SizedBox(
97+
height: 300,
98+
child: TextField(
99+
controller: resultController..text = result,
100+
maxLines: null,
101+
decoration: const InputDecoration(
102+
border: OutlineInputBorder(),
103+
hintText:
104+
'Describe how you\'d like AppFlowy to edit this text',
105+
),
106+
),
107+
),
108+
const SizedBox(height: 10),
109+
Row(
110+
mainAxisAlignment: MainAxisAlignment.end,
111+
children: [
112+
TextButton(
113+
onPressed: () {
114+
Navigator.of(context).pop();
115+
},
116+
child: const Text('Cancel'),
117+
),
118+
TextButton(
119+
onPressed: () {
120+
Navigator.of(context).pop();
121+
122+
// replace the text
123+
final selection = currentSelection;
124+
if (selection != null) {
125+
assert(selection.isSingle);
126+
final transaction = widget.editorState.transaction;
127+
transaction.replaceText(
128+
currentSelectedTextNodes.first,
129+
selection.startIndex,
130+
selection.length,
131+
resultController.text,
132+
);
133+
widget.editorState.apply(transaction);
134+
}
135+
},
136+
child: const Text('Replace'),
137+
),
138+
],
139+
),
140+
]
141+
],
142+
),
143+
);
144+
}
145+
146+
void _requestGPT3EditResult() {
147+
final selection =
148+
widget.editorState.service.selectionService.currentSelection.value;
149+
if (selection == null || !selection.isSingle) {
150+
return;
151+
}
152+
final text =
153+
widget.editorState.service.selectionService.currentSelectedNodes
154+
.whereType<TextNode>()
155+
.first
156+
.delta
157+
.slice(
158+
selection.startIndex,
159+
selection.endIndex,
160+
)
161+
.toPlainText();
162+
if (text.isEmpty) {
163+
Navigator.of(context).pop();
164+
return;
165+
}
166+
167+
showDialog(
168+
context: context,
169+
builder: (context) {
170+
return AlertDialog(
171+
content: Column(
172+
mainAxisSize: MainAxisSize.min,
173+
children: const [
174+
CircularProgressIndicator(),
175+
SizedBox(height: 10),
176+
Text('Loading'),
177+
],
178+
),
179+
);
180+
},
181+
);
182+
183+
getGPT3Edit(
184+
apiKey,
185+
text,
186+
inputEventController.text,
187+
onResult: (result) async {
188+
Navigator.of(context).pop(true);
189+
setState(() {
190+
this.result = result.join('\n').trim();
191+
});
192+
},
193+
onError: () async {
194+
Navigator.of(context).pop(true);
195+
},
196+
);
197+
}
198+
}

frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
4343
export 'src/plugins/markdown/document_markdown.dart';
4444
export 'src/plugins/quill_delta/delta_document_encoder.dart';
4545
export 'src/commands/text/text_commands.dart';
46+
export 'src/render/toolbar/toolbar_item.dart';

frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:appflowy_editor/src/core/document/node.dart';
33
import 'package:appflowy_editor/src/infra/log.dart';
44
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
55
import 'package:appflowy_editor/src/render/style/editor_style.dart';
6+
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
67
import 'package:appflowy_editor/src/service/service.dart';
78
import 'package:flutter/material.dart';
89

@@ -60,6 +61,9 @@ class EditorState {
6061
/// Stores the selection menu items.
6162
List<SelectionMenuItem> selectionMenuItems = [];
6263

64+
/// Stores the toolbar items.
65+
List<ToolbarItem> toolbarItems = [];
66+
6367
/// Operation stream.
6468
Stream<Transaction> get transactionStream => _observer.stream;
6569
final StreamController<Transaction> _observer = StreamController.broadcast();

frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:appflowy_editor/appflowy_editor.dart';
22
import 'package:appflowy_editor/src/flutter/overlay.dart';
33
import 'package:appflowy_editor/src/render/image/image_node_builder.dart';
4+
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
45
import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
56
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
67

@@ -30,6 +31,7 @@ class AppFlowyEditor extends StatefulWidget {
3031
this.customBuilders = const {},
3132
this.shortcutEvents = const [],
3233
this.selectionMenuItems = const [],
34+
this.toolbarItems = const [],
3335
this.editable = true,
3436
this.autoFocus = false,
3537
ThemeData? themeData,
@@ -51,6 +53,8 @@ class AppFlowyEditor extends StatefulWidget {
5153

5254
final List<SelectionMenuItem> selectionMenuItems;
5355

56+
final List<ToolbarItem> toolbarItems;
57+
5458
late final ThemeData themeData;
5559

5660
final bool editable;
@@ -74,6 +78,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
7478
super.initState();
7579

7680
editorState.selectionMenuItems = widget.selectionMenuItems;
81+
editorState.toolbarItems = widget.toolbarItems;
7782
editorState.themeData = widget.themeData;
7883
editorState.service.renderPluginService = _createRenderPlugin();
7984
editorState.editable = widget.editable;
@@ -94,6 +99,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
9499

95100
if (editorState.service != oldWidget.editorState.service) {
96101
editorState.selectionMenuItems = widget.selectionMenuItems;
102+
editorState.toolbarItems = widget.toolbarItems;
97103
editorState.service.renderPluginService = _createRenderPlugin();
98104
}
99105

frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,20 @@ class _FlowyToolbarState extends State<FlowyToolbar>
3535
implements AppFlowyToolbarService {
3636
OverlayEntry? _toolbarOverlay;
3737
final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget');
38+
late final List<ToolbarItem> toolbarItems;
39+
40+
@override
41+
void initState() {
42+
super.initState();
43+
44+
toolbarItems = [...defaultToolbarItems, ...widget.editorState.toolbarItems]
45+
..sort((a, b) => a.type.compareTo(b.type));
46+
}
3847

3948
@override
4049
void showInOffset(Offset offset, Alignment alignment, LayerLink layerLink) {
4150
hide();
42-
final items = _filterItems(defaultToolbarItems);
51+
final items = _filterItems(toolbarItems);
4352
if (items.isEmpty) {
4453
return;
4554
}
@@ -65,7 +74,7 @@ class _FlowyToolbarState extends State<FlowyToolbar>
6574

6675
@override
6776
bool triggerHandler(String id) {
68-
final items = defaultToolbarItems.where((item) => item.id == id);
77+
final items = toolbarItems.where((item) => item.id == id);
6978
if (items.length != 1) {
7079
assert(items.length == 1, 'The toolbar item\'s id must be unique');
7180
return false;

0 commit comments

Comments
 (0)