Skip to content

Commit 61704f6

Browse files
authored
Fix �release v0.1.1 known issues. (#2025)
* feat: optimize the smart edit user-experience * chore: update language files * feat: show discard dialog when users tap blank are that out of ai plugins * fix: toolbar_service test fail
1 parent b85990e commit 61704f6

File tree

9 files changed

+226
-21
lines changed

9 files changed

+226
-21
lines changed

frontend/appflowy_flutter/assets/translations/en.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,13 @@
347347
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
348348
"autoGeneratorLearnMore": "Learn more",
349349
"autoGeneratorGenerate": "Generate",
350-
"autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
350+
"autoGeneratorHintText": "Ask OpenAI ...",
351351
"autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
352352
"smartEdit": "Smart Edit",
353353
"smartEditTitleName": "OpenAI: Smart Edit",
354+
"openAI": "OpenAI",
354355
"smartEditFixSpelling": "Fix spelling",
356+
"warning": "⚠️ AI responses can be inaccurate or misleading.",
355357
"smartEditSummarize": "Summarize",
356358
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
357359
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
@@ -363,7 +365,8 @@
363365
"abstract": "Abstract",
364366
"addCover": "Add Cover",
365367
"addLocalImage": "Add local image"
366-
}
368+
},
369+
"discardResponse": "Do you want to discard the AI responses?"
367370
}
368371
},
369372
"board": {

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:convert';
22

33
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
44
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
5+
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
56
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/loading.dart';
67
import 'package:appflowy/user/application/user_service.dart';
78
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -10,6 +11,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
1011
import 'package:flowy_infra_ui/style_widget/text_field.dart';
1112
import 'package:flowy_infra_ui/widget/spacing.dart';
1213
import 'package:flutter/material.dart';
14+
import 'package:flutter/rendering.dart';
1315
import 'package:http/http.dart' as http;
1416
import 'package:appflowy/generated/locale_keys.g.dart';
1517
import 'package:easy_localization/easy_localization.dart';
@@ -56,13 +58,42 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
5658
final controller = TextEditingController();
5759
final focusNode = FocusNode();
5860
final textFieldFocusNode = FocusNode();
61+
final interceptor = SelectionInterceptor();
5962

6063
@override
6164
void initState() {
6265
super.initState();
6366

6467
textFieldFocusNode.addListener(_onFocusChanged);
6568
textFieldFocusNode.requestFocus();
69+
widget.editorState.service.selectionService.register(interceptor
70+
..canTap = (details) {
71+
final renderBox = context.findRenderObject() as RenderBox?;
72+
if (renderBox != null) {
73+
if (!isTapDownDetailsInRenderBox(details, renderBox)) {
74+
if (text.isNotEmpty || controller.text.isEmpty) {
75+
showDialog(
76+
context: context,
77+
builder: (context) {
78+
return DiscardDialog(
79+
onConfirm: () => _onDiscard(),
80+
onCancel: () {},
81+
);
82+
},
83+
);
84+
} else if (controller.text.isEmpty) {
85+
_onExit();
86+
}
87+
}
88+
}
89+
return false;
90+
});
91+
}
92+
93+
bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) {
94+
var result = BoxHitTestResult();
95+
box.hitTest(result, position: box.globalToLocal(details.globalPosition));
96+
return result.path.any((entry) => entry.target == box);
6697
}
6798

6899
@override
@@ -71,6 +102,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
71102
textFieldFocusNode.removeListener(_onFocusChanged);
72103
widget.editorState.service.selectionService.currentSelection
73104
.removeListener(_onCancelWhenSelectionChanged);
105+
widget.editorState.service.selectionService.unRegister(interceptor);
74106

75107
super.dispose();
76108
}
@@ -168,6 +200,11 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
168200
),
169201
onPressed: () async => await _onExit(),
170202
),
203+
const Spacer(),
204+
FlowyText.regular(
205+
LocaleKeys.document_plugins_warning.tr(),
206+
color: Theme.of(context).hintColor,
207+
),
171208
],
172209
);
173210
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:appflowy/generated/locale_keys.g.dart';
5+
6+
import 'package:easy_localization/easy_localization.dart';
7+
8+
class DiscardDialog extends StatelessWidget {
9+
const DiscardDialog({
10+
super.key,
11+
required this.onConfirm,
12+
required this.onCancel,
13+
});
14+
15+
final VoidCallback onConfirm;
16+
final VoidCallback onCancel;
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return NavigatorOkCancelDialog(
21+
message: LocaleKeys.document_plugins_discardResponse.tr(),
22+
okTitle: LocaleKeys.button_discard.tr(),
23+
cancelTitle: LocaleKeys.button_Cancel.tr(),
24+
onOkPressed: onConfirm,
25+
onCancelPressed: onCancel,
26+
);
27+
}
28+
}

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ enum SmartEditAction {
3434
}
3535
return SmartEditAction.fixSpelling;
3636
}
37+
38+
String get name {
39+
switch (this) {
40+
case SmartEditAction.summarize:
41+
return LocaleKeys.document_plugins_smartEditSummarize.tr();
42+
case SmartEditAction.fixSpelling:
43+
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
44+
}
45+
}
3746
}
3847

3948
class SmartEditActionWrapper extends ActionCell {
@@ -45,11 +54,6 @@ class SmartEditActionWrapper extends ActionCell {
4554

4655
@override
4756
String get name {
48-
switch (inner) {
49-
case SmartEditAction.summarize:
50-
return LocaleKeys.document_plugins_smartEditSummarize.tr();
51-
case SmartEditAction.fixSpelling:
52-
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
53-
}
57+
return inner.name;
5458
}
5559
}

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

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import 'dart:async';
2+
13
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
24
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
5+
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
36
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
47
import 'package:appflowy/user/application/user_service.dart';
58
import 'package:appflowy_editor/appflowy_editor.dart';
6-
import 'package:flowy_infra_ui/style_widget/button.dart';
7-
import 'package:flowy_infra_ui/style_widget/text.dart';
8-
import 'package:flowy_infra_ui/widget/spacing.dart';
9+
import 'package:appflowy_popover/appflowy_popover.dart';
10+
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
11+
import 'package:flowy_infra_ui/style_widget/decoration.dart';
912
import 'package:flutter/material.dart';
1013
import 'package:appflowy/generated/locale_keys.g.dart';
1114
import 'package:easy_localization/easy_localization.dart';
@@ -26,24 +29,106 @@ class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
2629

2730
@override
2831
Widget build(NodeWidgetContext<Node> context) {
29-
return _SmartEditInput(
32+
return _HoverSmartInput(
3033
key: context.node.key,
3134
node: context.node,
3235
editorState: context.editorState,
3336
);
3437
}
3538
}
3639

37-
class _SmartEditInput extends StatefulWidget {
38-
final Node node;
40+
class _HoverSmartInput extends StatefulWidget {
41+
const _HoverSmartInput({
42+
required super.key,
43+
required this.node,
44+
required this.editorState,
45+
});
3946

47+
final Node node;
4048
final EditorState editorState;
49+
50+
@override
51+
State<_HoverSmartInput> createState() => _HoverSmartInputState();
52+
}
53+
54+
class _HoverSmartInputState extends State<_HoverSmartInput> {
55+
final popoverController = PopoverController();
56+
final key = GlobalKey(debugLabel: 'smart_edit_input');
57+
58+
@override
59+
void initState() {
60+
super.initState();
61+
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
62+
popoverController.show();
63+
});
64+
}
65+
66+
@override
67+
Widget build(BuildContext context) {
68+
final width = _maxWidth();
69+
70+
return AppFlowyPopover(
71+
controller: popoverController,
72+
direction: PopoverDirection.bottomWithLeftAligned,
73+
triggerActions: PopoverTriggerFlags.none,
74+
margin: EdgeInsets.zero,
75+
constraints: BoxConstraints(maxWidth: width),
76+
decoration: FlowyDecoration.decoration(
77+
Colors.transparent,
78+
Colors.transparent,
79+
),
80+
child: const SizedBox(
81+
width: double.infinity,
82+
),
83+
canClose: () async {
84+
final completer = Completer<bool>();
85+
final state = key.currentState as _SmartEditInputState;
86+
if (state.result.isEmpty) {
87+
completer.complete(true);
88+
} else {
89+
showDialog(
90+
context: context,
91+
builder: (context) {
92+
return DiscardDialog(
93+
onConfirm: () => completer.complete(true),
94+
onCancel: () => completer.complete(false),
95+
);
96+
},
97+
);
98+
}
99+
return completer.future;
100+
},
101+
popupBuilder: (BuildContext popoverContext) {
102+
return _SmartEditInput(
103+
key: key,
104+
node: widget.node,
105+
editorState: widget.editorState,
106+
);
107+
},
108+
);
109+
}
110+
111+
double _maxWidth() {
112+
var width = double.infinity;
113+
final editorSize = widget.editorState.renderBox?.size;
114+
final padding = widget.editorState.editorStyle.padding;
115+
if (editorSize != null && padding != null) {
116+
width = editorSize.width - padding.left - padding.right;
117+
}
118+
return width;
119+
}
120+
}
121+
122+
class _SmartEditInput extends StatefulWidget {
41123
const _SmartEditInput({
42-
Key? key,
124+
required super.key,
43125
required this.node,
44126
required this.editorState,
45127
});
46128

129+
final Node node;
130+
final EditorState editorState;
131+
47132
@override
48133
State<_SmartEditInput> createState() => _SmartEditInputState();
49134
}
@@ -108,7 +193,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
108193
return Row(
109194
children: [
110195
FlowyText.medium(
111-
LocaleKeys.document_plugins_smartEditTitleName.tr(),
196+
'${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}',
112197
fontSize: 14,
113198
),
114199
const Spacer(),
@@ -187,6 +272,11 @@ class _SmartEditInputState extends State<_SmartEditInput> {
187272
),
188273
onPressed: () async => await _onExit(),
189274
),
275+
const Spacer(),
276+
FlowyText.regular(
277+
LocaleKeys.document_plugins_warning.tr(),
278+
color: Theme.of(context).hintColor,
279+
),
190280
],
191281
);
192282
}

frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ abstract class AppFlowySelectionService {
8282

8383
/// The current selection areas's rect in editor.
8484
List<Rect> get selectionRects;
85+
86+
void register(SelectionInterceptor interceptor);
87+
void unRegister(SelectionInterceptor interceptor);
88+
}
89+
90+
class SelectionInterceptor {
91+
bool Function(TapDownDetails details)? canTap;
8592
}
8693

8794
class AppFlowySelection extends StatefulWidget {
@@ -212,6 +219,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
212219

213220
selectionRects.clear();
214221
clearSelection();
222+
_clearToolbar();
215223

216224
if (selection != null) {
217225
if (selection.isCollapsed) {
@@ -286,6 +294,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
286294
}
287295

288296
void _onTapDown(TapDownDetails details) {
297+
final canTap =
298+
_interceptors.every((element) => element.canTap?.call(details) ?? true);
299+
if (!canTap) return;
300+
289301
// clear old state.
290302
_panStartOffset = null;
291303

@@ -701,4 +713,15 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
701713
// }
702714
// }
703715
}
716+
717+
final List<SelectionInterceptor> _interceptors = [];
718+
@override
719+
void register(SelectionInterceptor interceptor) {
720+
_interceptors.add(interceptor);
721+
}
722+
723+
@override
724+
void unRegister(SelectionInterceptor interceptor) {
725+
_interceptors.removeWhere((element) => element == interceptor);
726+
}
704727
}

frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ void main() async {
9494
await editor.updateSelection(
9595
Selection.single(path: [1], startOffset: 0, endOffset: text.length * 2),
9696
);
97+
await tester.pumpAndSettle(const Duration(milliseconds: 500));
9798
testHighlight(false);
9899

99100
await editor.updateSelection(
@@ -103,6 +104,7 @@ void main() async {
103104
endOffset: text.length * 2,
104105
),
105106
);
107+
await tester.pumpAndSettle(const Duration(milliseconds: 500));
106108
testHighlight(true);
107109

108110
await editor.updateSelection(
@@ -112,6 +114,7 @@ void main() async {
112114
endOffset: text.length * 2 - 2,
113115
),
114116
);
117+
await tester.pumpAndSettle(const Duration(milliseconds: 500));
115118
testHighlight(true);
116119
});
117120

0 commit comments

Comments
 (0)