Skip to content

Commit 653f880

Browse files
committed
feat: improve the smart edit user-experience
1 parent 0c88c7c commit 653f880

File tree

8 files changed

+153
-93
lines changed

8 files changed

+153
-93
lines changed

frontend/appflowy_flutter/assets/translations/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@
138138
"keep": "Keep",
139139
"tryAgain": "Try again",
140140
"discard": "Discard",
141-
"replace": "Replace"
141+
"replace": "Replace",
142+
"insertBelow": "Insert Below"
142143
},
143144
"label": {
144145
"welcome": "Welcome!",

frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'dart:convert';
22

33
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
4-
import 'package:appflowy_editor/appflowy_editor.dart';
54

65
import 'text_completion.dart';
76
import 'package:dartz/dartz.dart';
@@ -125,6 +124,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
125124
String? suffix,
126125
int maxTokens = 2048,
127126
double temperature = 0.3,
127+
bool useAction = false,
128128
}) async {
129129
final parameters = {
130130
'model': 'text-davinci-003',
@@ -151,14 +151,22 @@ class HttpOpenAIRepository implements OpenAIRepository {
151151
.transform(const Utf8Decoder())
152152
.transform(const LineSplitter())) {
153153
syntax += 1;
154-
if (syntax == 3) {
155-
await onStart();
156-
continue;
157-
} else if (syntax < 3) {
158-
continue;
154+
if (!useAction) {
155+
if (syntax == 3) {
156+
await onStart();
157+
continue;
158+
} else if (syntax < 3) {
159+
continue;
160+
}
161+
} else {
162+
if (syntax == 2) {
163+
await onStart();
164+
continue;
165+
} else if (syntax < 2) {
166+
continue;
167+
}
159168
}
160169
final data = chunk.trim().split('data: ');
161-
Log.editor.info(data.toString());
162170
if (data.length > 1) {
163171
if (data[1] != '[DONE]') {
164172
final response = TextCompletionResponse.fromJson(
@@ -173,7 +181,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
173181
previousSyntax = response.choices.first.text;
174182
}
175183
} else {
176-
onEnd();
184+
await onEnd();
177185
}
178186
}
179187
}
@@ -183,6 +191,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
183191
OpenAIError.fromJson(json.decode(body)['error']),
184192
);
185193
}
194+
return;
186195
}
187196

188197
@override

frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,30 @@ enum SmartEditAction {
1010
String get toInstruction {
1111
switch (this) {
1212
case SmartEditAction.summarize:
13-
return 'Make this shorter and more concise:';
13+
return 'Tl;dr';
1414
case SmartEditAction.fixSpelling:
1515
return 'Correct this to standard English:';
1616
}
1717
}
18+
19+
String prompt(String input) {
20+
switch (this) {
21+
case SmartEditAction.summarize:
22+
return '$input\n\nTl;dr';
23+
case SmartEditAction.fixSpelling:
24+
return 'Correct this to standard English:\n\n$input';
25+
}
26+
}
27+
28+
static SmartEditAction from(int index) {
29+
switch (index) {
30+
case 0:
31+
return SmartEditAction.summarize;
32+
case 1:
33+
return SmartEditAction.fixSpelling;
34+
}
35+
return SmartEditAction.fixSpelling;
36+
}
1837
}
1938

2039
class SmartEditActionWrapper extends ActionCell {

frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart

Lines changed: 102 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
21
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
3-
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
42
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
53
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
64
import 'package:appflowy/user/application/user_service.dart';
@@ -12,8 +10,6 @@ import 'package:flutter/material.dart';
1210
import 'package:appflowy/generated/locale_keys.g.dart';
1311
import 'package:easy_localization/easy_localization.dart';
1412
import 'package:http/http.dart' as http;
15-
import 'package:dartz/dartz.dart' as dartz;
16-
import 'package:appflowy/util/either_extension.dart';
1713

1814
const String kSmartEditType = 'smart_edit_input';
1915
const String kSmartEditInstructionType = 'smart_edit_instruction';
@@ -22,9 +18,9 @@ const String kSmartEditInputType = 'smart_edit_input';
2218
class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
2319
@override
2420
NodeValidator<Node> get nodeValidator => (node) {
25-
return SmartEditAction.values.map((e) => e.toInstruction).contains(
26-
node.attributes[kSmartEditInstructionType],
27-
) &&
21+
return SmartEditAction.values
22+
.map((e) => e.index)
23+
.contains(node.attributes[kSmartEditInstructionType]) &&
2824
node.attributes[kSmartEditInputType] is String;
2925
};
3026

@@ -53,13 +49,14 @@ class _SmartEditInput extends StatefulWidget {
5349
}
5450

5551
class _SmartEditInputState extends State<_SmartEditInput> {
56-
String get instruction => widget.node.attributes[kSmartEditInstructionType];
52+
SmartEditAction get action =>
53+
SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
5754
String get input => widget.node.attributes[kSmartEditInputType];
5855

5956
final focusNode = FocusNode();
6057
final client = http.Client();
61-
dartz.Either<OpenAIError, TextEditResponse>? result;
6258
bool loading = true;
59+
String result = '';
6360

6461
@override
6562
void initState() {
@@ -72,12 +69,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
7269
widget.editorState.service.keyboardService?.enable();
7370
}
7471
});
75-
_requestEdits().then(
76-
(value) => setState(() {
77-
result = value;
78-
loading = false;
79-
}),
80-
);
72+
_requestCompletions();
8173
}
8274

8375
@override
@@ -141,25 +133,14 @@ class _SmartEditInputState extends State<_SmartEditInput> {
141133
child: const CircularProgressIndicator(),
142134
),
143135
);
144-
if (result == null) {
136+
if (result.isEmpty) {
145137
return loading;
146138
}
147-
return result!.fold((error) {
148-
return Flexible(
149-
child: Text(
150-
error.message,
151-
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
152-
color: Colors.red,
153-
),
154-
),
155-
);
156-
}, (response) {
157-
return Flexible(
158-
child: Text(
159-
response.choices.map((e) => e.text).join('\n'),
160-
),
161-
);
162-
});
139+
return Flexible(
140+
child: Text(
141+
result,
142+
),
143+
);
163144
}
164145

165146
Widget _buildInputFooterWidget(BuildContext context) {
@@ -174,8 +155,23 @@ class _SmartEditInputState extends State<_SmartEditInput> {
174155
),
175156
],
176157
),
177-
onPressed: () {
178-
_onReplace();
158+
onPressed: () async {
159+
await _onReplace();
160+
_onExit();
161+
},
162+
),
163+
const Space(10, 0),
164+
FlowyRichTextButton(
165+
TextSpan(
166+
children: [
167+
TextSpan(
168+
text: LocaleKeys.button_insertBelow.tr(),
169+
style: Theme.of(context).textTheme.bodyMedium,
170+
),
171+
],
172+
),
173+
onPressed: () async {
174+
await _onInsertBelow();
179175
_onExit();
180176
},
181177
),
@@ -201,12 +197,11 @@ class _SmartEditInputState extends State<_SmartEditInput> {
201197
final selectedNodes = widget
202198
.editorState.service.selectionService.currentSelectedNodes.normalized
203199
.whereType<TextNode>();
204-
if (selection == null || result == null || result!.isLeft()) {
200+
if (selection == null || result.isEmpty) {
205201
return;
206202
}
207203

208-
final texts = result!.asRight().choices.first.text.split('\n')
209-
..removeWhere((element) => element.isEmpty);
204+
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
210205
final transaction = widget.editorState.transaction;
211206
transaction.replaceTexts(
212207
selectedNodes.toList(growable: false),
@@ -216,6 +211,25 @@ class _SmartEditInputState extends State<_SmartEditInput> {
216211
return widget.editorState.apply(transaction);
217212
}
218213

214+
Future<void> _onInsertBelow() async {
215+
final selection = widget.editorState.service.selectionService
216+
.currentSelection.value?.normalized;
217+
if (selection == null || result.isEmpty) {
218+
return;
219+
}
220+
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
221+
final transaction = widget.editorState.transaction;
222+
transaction.insertNodes(
223+
selection.normalized.end.path.next,
224+
texts.map(
225+
(e) => TextNode(
226+
delta: Delta()..insert(e),
227+
),
228+
),
229+
);
230+
return widget.editorState.apply(transaction);
231+
}
232+
219233
Future<void> _onExit() async {
220234
final transaction = widget.editorState.transaction;
221235
transaction.deleteNode(widget.node);
@@ -228,35 +242,62 @@ class _SmartEditInputState extends State<_SmartEditInput> {
228242
);
229243
}
230244

231-
Future<dartz.Either<OpenAIError, TextEditResponse>> _requestEdits() async {
245+
Future<void> _requestCompletions() async {
232246
final result = await UserBackendService.getCurrentUserProfile();
233-
return result.fold((userProfile) async {
247+
return result.fold((l) async {
234248
final openAIRepository = HttpOpenAIRepository(
235249
client: client,
236-
apiKey: userProfile.openaiKey,
237-
);
238-
final edits = await openAIRepository.getEdits(
239-
input: input,
240-
instruction: instruction,
241-
n: 1,
250+
apiKey: l.openaiKey,
242251
);
243-
return edits.fold((error) async {
244-
return dartz.Left(
245-
OpenAIError(
246-
message:
247-
LocaleKeys.document_plugins_smartEditCouldNotFetchResult.tr(),
248-
),
252+
var lines = input.split('\n\n');
253+
if (action == SmartEditAction.summarize) {
254+
lines = [lines.join('\n')];
255+
}
256+
for (var i = 0; i < lines.length; i++) {
257+
final element = lines[i];
258+
await openAIRepository.getStreamedCompletions(
259+
useAction: true,
260+
prompt: action.prompt(element),
261+
onStart: () async {
262+
setState(() {
263+
loading = false;
264+
});
265+
},
266+
onProcess: (response) async {
267+
setState(() {
268+
this.result += response.choices.first.text;
269+
});
270+
},
271+
onEnd: () async {
272+
setState(() {
273+
if (i != lines.length - 1) {
274+
this.result += '\n';
275+
}
276+
});
277+
},
278+
onError: (error) async {
279+
await _showError(error.message);
280+
await _onExit();
281+
},
249282
);
250-
}, (textEdit) async {
251-
return dartz.Right(textEdit);
252-
});
253-
}, (error) async {
254-
// error
255-
return dartz.Left(
256-
OpenAIError(
257-
message: LocaleKeys.document_plugins_smartEditCouldNotFetchKey.tr(),
258-
),
259-
);
283+
}
284+
}, (r) async {
285+
await _showError(r.msg);
286+
await _onExit();
260287
});
261288
}
289+
290+
Future<void> _showError(String message) async {
291+
ScaffoldMessenger.of(context).showSnackBar(
292+
SnackBar(
293+
action: SnackBarAction(
294+
label: LocaleKeys.button_Cancel.tr(),
295+
onPressed: () {
296+
ScaffoldMessenger.of(context).hideCurrentSnackBar();
297+
},
298+
),
299+
content: FlowyText(message),
300+
),
301+
);
302+
}
262303
}

frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ ToolbarItem smartEditItem = ToolbarItem(
1616
validator: (editorState) {
1717
// All selected nodes must be text.
1818
final nodes = editorState.service.selectionService.currentSelectedNodes;
19-
return nodes.whereType<TextNode>().length == nodes.length &&
20-
nodes.length == 1;
19+
return nodes.whereType<TextNode>().length == nodes.length;
2120
},
2221
itemBuilder: (context, editorState) {
2322
return _SmartEditWidget(
@@ -102,14 +101,17 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
102101
textNodes.normalized,
103102
selection.normalized,
104103
);
104+
while (input.last.isEmpty) {
105+
input.removeLast();
106+
}
105107
final transaction = widget.editorState.transaction;
106108
transaction.insertNode(
107109
selection.normalized.end.path.next,
108110
Node(
109111
type: kSmartEditType,
110112
attributes: {
111-
kSmartEditInstructionType: actionWrapper.inner.toInstruction,
112-
kSmartEditInputType: input,
113+
kSmartEditInstructionType: actionWrapper.inner.index,
114+
kSmartEditInputType: input.join('\n\n'),
113115
},
114116
),
115117
);

frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'package:appflowy/startup/startup.dart';
22
import 'package:appflowy/util/debounce.dart';
3-
import 'package:appflowy_backend/log.dart';
43
import 'package:flowy_infra/size.dart';
54
import 'package:flowy_infra_ui/style_widget/text.dart';
65
import 'package:flutter/material.dart';
@@ -133,7 +132,6 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
133132
),
134133
onChanged: (value) {
135134
debounce.call(() {
136-
Log.debug('SettingsUserViewBloc');
137135
context
138136
.read<SettingsUserViewBloc>()
139137
.add(SettingsUserEvent.updateUserOpenAIKey(value));

0 commit comments

Comments
 (0)