chore: sync release 0.1.1 (#2075)

This commit is contained in:
Lucas.Xu
2023-03-22 14:49:15 +08:00
committed by GitHub
parent 92878d7e89
commit 98f1ac52b4
43 changed files with 720 additions and 284 deletions

View File

@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'text_completion.dart';
import 'package:dartz/dartz.dart';
@ -125,6 +124,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) async {
final parameters = {
'model': 'text-davinci-003',
@ -151,14 +151,22 @@ class HttpOpenAIRepository implements OpenAIRepository {
.transform(const Utf8Decoder())
.transform(const LineSplitter())) {
syntax += 1;
if (syntax == 3) {
await onStart();
continue;
} else if (syntax < 3) {
continue;
if (!useAction) {
if (syntax == 3) {
await onStart();
continue;
} else if (syntax < 3) {
continue;
}
} else {
if (syntax == 2) {
await onStart();
continue;
} else if (syntax < 2) {
continue;
}
}
final data = chunk.trim().split('data: ');
Log.editor.info(data.toString());
if (data.length > 1) {
if (data[1] != '[DONE]') {
final response = TextCompletionResponse.fromJson(
@ -173,7 +181,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
previousSyntax = response.choices.first.text;
}
} else {
onEnd();
await onEnd();
}
}
}
@ -183,6 +191,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
OpenAIError.fromJson(json.decode(body)['error']),
);
}
return;
}
@override

View File

@ -0,0 +1,9 @@
import 'package:url_launcher/url_launcher.dart';
Future<void> openLearnMorePage() async {
final uri = Uri.parse(
'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai');
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/loading.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -9,7 +11,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/rendering.dart';
import 'package:http/http.dart' as http;
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
@ -56,6 +58,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
final controller = TextEditingController();
final focusNode = FocusNode();
final textFieldFocusNode = FocusNode();
final interceptor = SelectionInterceptor();
@override
void initState() {
@ -63,6 +66,34 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
textFieldFocusNode.addListener(_onFocusChanged);
textFieldFocusNode.requestFocus();
widget.editorState.service.selectionService.register(interceptor
..canTap = (details) {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox != null) {
if (!isTapDownDetailsInRenderBox(details, renderBox)) {
if (text.isNotEmpty || controller.text.isNotEmpty) {
showDialog(
context: context,
builder: (context) {
return DiscardDialog(
onConfirm: () => _onDiscard(),
onCancel: () {},
);
},
);
} else if (controller.text.isEmpty) {
_onExit();
}
}
}
return false;
});
}
bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) {
var result = BoxHitTestResult();
box.hitTest(result, position: box.globalToLocal(details.globalPosition));
return result.path.any((entry) => entry.target == box);
}
@override
@ -71,6 +102,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
textFieldFocusNode.removeListener(_onFocusChanged);
widget.editorState.service.selectionService.currentSelection
.removeListener(_onCancelWhenSelectionChanged);
widget.editorState.service.selectionService.unRegister(interceptor);
super.dispose();
}
@ -119,34 +151,26 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
fontSize: 14,
),
const Spacer(),
FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
onTap: () async {
await openLearnMorePage();
},
)
],
);
}
Widget _buildInputWidget(BuildContext context) {
return RawKeyboardListener(
focusNode: focusNode,
onKey: (RawKeyEvent event) async {
if (event is! RawKeyDownEvent) return;
if (event.logicalKey == LogicalKeyboardKey.enter) {
if (controller.text.isNotEmpty) {
textFieldFocusNode.unfocus();
await _onGenerate();
}
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
await _onExit();
}
},
child: FlowyTextField(
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
controller: controller,
maxLines: 3,
focusNode: textFieldFocusNode,
autoFocus: false,
),
return FlowyTextField(
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
controller: controller,
maxLines: 3,
focusNode: textFieldFocusNode,
autoFocus: false,
);
}
@ -157,15 +181,9 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.button_generate.tr()} ',
text: LocaleKeys.button_generate.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
TextSpan(
text: '',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
onPressed: () async => await _onGenerate(),
@ -175,19 +193,23 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.button_Cancel.tr()} ',
text: LocaleKeys.button_Cancel.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
TextSpan(
text: LocaleKeys.button_esc.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
onPressed: () async => await _onExit(),
),
Expanded(
child: Container(
alignment: Alignment.centerRight,
child: FlowyText.regular(
LocaleKeys.document_plugins_warning.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
),
],
);
}

View File

@ -2,10 +2,13 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
name: 'Auto Generator',
name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
iconData: Icons.generating_tokens,
keywords: ['autogenerator', 'auto generator'],
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
nodeBuilder: (editorState) {
final node = Node(
type: kAutoCompletionInputType,

View File

@ -0,0 +1,28 @@
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
class DiscardDialog extends StatelessWidget {
const DiscardDialog({
super.key,
required this.onConfirm,
required this.onCancel,
});
final VoidCallback onConfirm;
final VoidCallback onCancel;
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
message: LocaleKeys.document_plugins_discardResponse.tr(),
okTitle: LocaleKeys.button_discard.tr(),
cancelTitle: LocaleKeys.button_Cancel.tr(),
onOkPressed: onConfirm,
onCancelPressed: onCancel,
);
}
}

View File

@ -10,11 +10,39 @@ enum SmartEditAction {
String get toInstruction {
switch (this) {
case SmartEditAction.summarize:
return 'Make this shorter and more concise:';
return 'Tl;dr';
case SmartEditAction.fixSpelling:
return 'Correct this to standard English:';
}
}
String prompt(String input) {
switch (this) {
case SmartEditAction.summarize:
return '$input\n\nTl;dr';
case SmartEditAction.fixSpelling:
return 'Correct this to standard English:\n\n$input';
}
}
static SmartEditAction from(int index) {
switch (index) {
case 0:
return SmartEditAction.summarize;
case 1:
return SmartEditAction.fixSpelling;
}
return SmartEditAction.fixSpelling;
}
String get name {
switch (this) {
case SmartEditAction.summarize:
return LocaleKeys.document_plugins_smartEditSummarize.tr();
case SmartEditAction.fixSpelling:
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
}
}
}
class SmartEditActionWrapper extends ActionCell {
@ -26,11 +54,6 @@ class SmartEditActionWrapper extends ActionCell {
@override
String get name {
switch (inner) {
case SmartEditAction.summarize:
return LocaleKeys.document_plugins_smartEditSummarize.tr();
case SmartEditAction.fixSpelling:
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
}
return inner.name;
}
}

View File

@ -1,19 +1,18 @@
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
import 'dart:async';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart' as http;
import 'package:dartz/dartz.dart' as dartz;
import 'package:appflowy/util/either_extension.dart';
const String kSmartEditType = 'smart_edit_input';
const String kSmartEditInstructionType = 'smart_edit_instruction';
@ -22,15 +21,15 @@ const String kSmartEditInputType = 'smart_edit_input';
class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
@override
NodeValidator<Node> get nodeValidator => (node) {
return SmartEditAction.values.map((e) => e.toInstruction).contains(
node.attributes[kSmartEditInstructionType],
) &&
return SmartEditAction.values
.map((e) => e.index)
.contains(node.attributes[kSmartEditInstructionType]) &&
node.attributes[kSmartEditInputType] is String;
};
@override
Widget build(NodeWidgetContext<Node> context) {
return _SmartEditInput(
return _HoverSmartInput(
key: context.node.key,
node: context.node,
editorState: context.editorState,
@ -38,28 +37,111 @@ class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
}
}
class _SmartEditInput extends StatefulWidget {
final Node node;
final EditorState editorState;
const _SmartEditInput({
Key? key,
class _HoverSmartInput extends StatefulWidget {
const _HoverSmartInput({
required super.key,
required this.node,
required this.editorState,
});
final Node node;
final EditorState editorState;
@override
State<_HoverSmartInput> createState() => _HoverSmartInputState();
}
class _HoverSmartInputState extends State<_HoverSmartInput> {
final popoverController = PopoverController();
final key = GlobalKey(debugLabel: 'smart_edit_input');
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
popoverController.show();
});
}
@override
Widget build(BuildContext context) {
final width = _maxWidth();
return AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
margin: EdgeInsets.zero,
constraints: BoxConstraints(maxWidth: width),
decoration: FlowyDecoration.decoration(
Colors.transparent,
Colors.transparent,
),
child: const SizedBox(
width: double.infinity,
),
canClose: () async {
final completer = Completer<bool>();
final state = key.currentState as _SmartEditInputState;
if (state.result.isEmpty) {
completer.complete(true);
} else {
showDialog(
context: context,
builder: (context) {
return DiscardDialog(
onConfirm: () => completer.complete(true),
onCancel: () => completer.complete(false),
);
},
);
}
return completer.future;
},
popupBuilder: (BuildContext popoverContext) {
return _SmartEditInput(
key: key,
node: widget.node,
editorState: widget.editorState,
);
},
);
}
double _maxWidth() {
var width = double.infinity;
final editorSize = widget.editorState.renderBox?.size;
final padding = widget.editorState.editorStyle.padding;
if (editorSize != null && padding != null) {
width = editorSize.width - padding.left - padding.right;
}
return width;
}
}
class _SmartEditInput extends StatefulWidget {
const _SmartEditInput({
required super.key,
required this.node,
required this.editorState,
});
final Node node;
final EditorState editorState;
@override
State<_SmartEditInput> createState() => _SmartEditInputState();
}
class _SmartEditInputState extends State<_SmartEditInput> {
String get instruction => widget.node.attributes[kSmartEditInstructionType];
SmartEditAction get action =>
SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
String get input => widget.node.attributes[kSmartEditInputType];
final focusNode = FocusNode();
final client = http.Client();
dartz.Either<OpenAIError, TextEditResponse>? result;
bool loading = true;
String result = '';
@override
void initState() {
@ -72,12 +154,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
widget.editorState.service.keyboardService?.enable();
}
});
_requestEdits().then(
(value) => setState(() {
result = value;
loading = false;
}),
);
_requestCompletions();
}
@override
@ -99,28 +176,16 @@ class _SmartEditInputState extends State<_SmartEditInput> {
}
Widget _buildSmartEditPanel(BuildContext context) {
return RawKeyboardListener(
focusNode: focusNode,
onKey: (RawKeyEvent event) async {
if (event is! RawKeyDownEvent) return;
if (event.logicalKey == LogicalKeyboardKey.enter) {
await _onReplace();
await _onExit();
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
await _onExit();
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderWidget(context),
const Space(0, 10),
_buildResultWidget(context),
const Space(0, 10),
_buildInputFooterWidget(context),
],
),
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderWidget(context),
const Space(0, 10),
_buildResultWidget(context),
const Space(0, 10),
_buildInputFooterWidget(context),
],
);
}
@ -128,13 +193,19 @@ class _SmartEditInputState extends State<_SmartEditInput> {
return Row(
children: [
FlowyText.medium(
LocaleKeys.document_plugins_smartEditTitleName.tr(),
'${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}',
fontSize: 14,
),
const Spacer(),
FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
onTap: () async {
await openLearnMorePage();
},
)
],
);
}
@ -147,25 +218,14 @@ class _SmartEditInputState extends State<_SmartEditInput> {
child: const CircularProgressIndicator(),
),
);
if (result == null) {
if (result.isEmpty) {
return loading;
}
return result!.fold((error) {
return Flexible(
child: Text(
error.message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.red,
),
),
);
}, (response) {
return Flexible(
child: Text(
response.choices.map((e) => e.text).join('\n'),
),
);
});
return Flexible(
child: Text(
result,
),
);
}
Widget _buildInputFooterWidget(BuildContext context) {
@ -175,19 +235,13 @@ class _SmartEditInputState extends State<_SmartEditInput> {
TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.button_replace.tr()} ',
text: LocaleKeys.button_replace.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
TextSpan(
text: '',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
onPressed: () {
_onReplace();
onPressed: () async {
await _onReplace();
_onExit();
},
),
@ -196,19 +250,33 @@ class _SmartEditInputState extends State<_SmartEditInput> {
TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.button_Cancel.tr()} ',
text: LocaleKeys.button_insertBelow.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
onPressed: () async {
await _onInsertBelow();
_onExit();
},
),
const Space(10, 0),
FlowyRichTextButton(
TextSpan(
children: [
TextSpan(
text: LocaleKeys.button_esc.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
text: LocaleKeys.button_Cancel.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
onPressed: () async => await _onExit(),
),
const Spacer(),
FlowyText.regular(
LocaleKeys.document_plugins_warning.tr(),
color: Theme.of(context).hintColor,
),
],
);
}
@ -219,12 +287,11 @@ class _SmartEditInputState extends State<_SmartEditInput> {
final selectedNodes = widget
.editorState.service.selectionService.currentSelectedNodes.normalized
.whereType<TextNode>();
if (selection == null || result == null || result!.isLeft()) {
if (selection == null || result.isEmpty) {
return;
}
final texts = result!.asRight().choices.first.text.split('\n')
..removeWhere((element) => element.isEmpty);
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
final transaction = widget.editorState.transaction;
transaction.replaceTexts(
selectedNodes.toList(growable: false),
@ -234,6 +301,25 @@ class _SmartEditInputState extends State<_SmartEditInput> {
return widget.editorState.apply(transaction);
}
Future<void> _onInsertBelow() async {
final selection = widget.editorState.service.selectionService
.currentSelection.value?.normalized;
if (selection == null || result.isEmpty) {
return;
}
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
final transaction = widget.editorState.transaction;
transaction.insertNodes(
selection.normalized.end.path.next,
texts.map(
(e) => TextNode(
delta: Delta()..insert(e),
),
),
);
return widget.editorState.apply(transaction);
}
Future<void> _onExit() async {
final transaction = widget.editorState.transaction;
transaction.deleteNode(widget.node);
@ -246,35 +332,63 @@ class _SmartEditInputState extends State<_SmartEditInput> {
);
}
Future<dartz.Either<OpenAIError, TextEditResponse>> _requestEdits() async {
Future<void> _requestCompletions() async {
final result = await UserBackendService.getCurrentUserProfile();
return result.fold((userProfile) async {
return result.fold((l) async {
final openAIRepository = HttpOpenAIRepository(
client: client,
apiKey: userProfile.openaiKey,
apiKey: l.openaiKey,
);
final edits = await openAIRepository.getEdits(
input: input,
instruction: instruction,
n: 1,
);
return edits.fold((error) async {
return dartz.Left(
OpenAIError(
message:
LocaleKeys.document_plugins_smartEditCouldNotFetchResult.tr(),
),
var lines = input.split('\n\n');
if (action == SmartEditAction.summarize) {
lines = [lines.join('\n')];
}
for (var i = 0; i < lines.length; i++) {
final element = lines[i];
await openAIRepository.getStreamedCompletions(
useAction: true,
prompt: action.prompt(element),
onStart: () async {
setState(() {
loading = false;
});
},
onProcess: (response) async {
setState(() {
this.result += response.choices.first.text;
});
},
onEnd: () async {
setState(() {
if (i != lines.length - 1) {
this.result += '\n';
}
});
},
onError: (error) async {
await _showError(error.message);
await _onExit();
},
);
}, (textEdit) async {
return dartz.Right(textEdit);
});
}, (error) async {
// error
return dartz.Left(
OpenAIError(
message: LocaleKeys.document_plugins_smartEditCouldNotFetchKey.tr(),
),
);
}
}, (r) async {
await _showError(r.msg);
await _onExit();
});
}
Future<void> _showError(String message) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
action: SnackBarAction(
label: LocaleKeys.button_Cancel.tr(),
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
content: FlowyText(message),
),
);
}
}

View File

@ -101,14 +101,17 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
textNodes.normalized,
selection.normalized,
);
while (input.last.isEmpty) {
input.removeLast();
}
final transaction = widget.editorState.transaction;
transaction.insertNode(
selection.normalized.end.path.next,
Node(
type: kSmartEditType,
attributes: {
kSmartEditInstructionType: actionWrapper.inner.toInstruction,
kSmartEditInputType: input,
kSmartEditInstructionType: actionWrapper.inner.index,
kSmartEditInputType: input.join('\n\n'),
},
),
);