diff --git a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart index c5f2c0d1aa374..f8fe5a8c9ac3d 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart @@ -46,7 +46,6 @@ void main() { await tester.ime.insertText(inputContent); expect(find.text(inputContent, findRichText: true), findsOneWidget); - // TODO(nathan): remove the await // 6 seconds for data sync await tester.waitForSeconds(6); diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart index f739820d04d7b..1d42cd8d280b4 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; @@ -103,8 +103,8 @@ Future setUpOpenAITesting(WidgetTester tester) async { } Future mockOpenAIRepository() async { - await getIt.unregister(); - getIt.registerFactoryAsync( + await getIt.unregister(); + getIt.registerFactoryAsync( () => Future.value( MockOpenAIRepository(), ), diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart index 78445a2f4e437..7201bd89caa5d 100644 --- a/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart +++ b/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart @@ -44,7 +44,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository { required Future Function() onStart, required Future Function(TextCompletionResponse response) onProcess, required Future Function() onEnd, - required void Function(OpenAIError error) onError, + required void Function(AIError error) onError, String? suffix, int maxTokens = 2048, double temperature = 0.3, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index 5d51c1b390e7e..d8370fe823cca 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -90,7 +90,7 @@ class ImagePlaceholderState extends State { UploadImageType.local, UploadImageType.url, UploadImageType.unsplash, - UploadImageType.openAI, + // UploadImageType.openAI, UploadImageType.stabilityAI, ], onSelectedLocalImage: (path) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart index 9cd6320518071..20ef4593a9820 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -22,7 +22,7 @@ class OpenAIImageWidget extends StatefulWidget { } class _OpenAIImageWidgetState extends State { - Future, OpenAIError>>? future; + Future, AIError>>? future; String query = ''; @override @@ -93,7 +93,7 @@ class _OpenAIImageWidgetState extends State { } void _search() async { - final openAI = await getIt.getAsync(); + final openAI = await getIt.getAsync(); setState(() { future = openAI.generateImage( prompt: query, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart index 0679f879962a8..ee8f681ff5b4f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart'; @@ -19,7 +18,7 @@ enum UploadImageType { url, unsplash, stabilityAI, - openAI, + // openAI, color; String get description { @@ -30,8 +29,8 @@ enum UploadImageType { return LocaleKeys.document_imageBlock_embedLink_label.tr(); case UploadImageType.unsplash: return LocaleKeys.document_imageBlock_unsplash_label.tr(); - case UploadImageType.openAI: - return LocaleKeys.document_imageBlock_ai_label.tr(); + // case UploadImageType.openAI: + // return LocaleKeys.document_imageBlock_ai_label.tr(); case UploadImageType.stabilityAI: return LocaleKeys.document_imageBlock_stability_ai_label.tr(); case UploadImageType.color: @@ -186,23 +185,23 @@ class _UploadImageMenuState extends State { ), ), ); - case UploadImageType.openAI: - return supportOpenAI - ? Expanded( - child: Container( - padding: const EdgeInsets.all(8.0), - constraints: constraints, - child: OpenAIImageWidget( - onSelectNetworkImage: widget.onSelectedAIImage, - ), - ), - ) - : Padding( - padding: const EdgeInsets.all(8.0), - child: FlowyText( - LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(), - ), - ); + // case UploadImageType.openAI: + // return supportOpenAI + // ? Expanded( + // child: Container( + // padding: const EdgeInsets.all(8.0), + // constraints: constraints, + // child: OpenAIImageWidget( + // onSelectNetworkImage: widget.onSelectedAIImage, + // ), + // ), + // ) + // : Padding( + // padding: const EdgeInsets.all(8.0), + // child: FlowyText( + // LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(), + // ), + // ); case UploadImageType.stabilityAI: return supportStabilityAI ? Expanded( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart new file mode 100644 index 0000000000000..19d68f58ba936 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart @@ -0,0 +1,32 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; + +abstract class AIRepository { + Future getStreamedCompletions({ + required String prompt, + required Future Function() onStart, + required Future Function(TextCompletionResponse response) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + String? suffix, + int maxTokens = 2048, + double temperature = 0.3, + bool useAction = false, + }); + + Future streamCompletion({ + required String text, + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + }); + + Future, AIError>> generateImage({ + required String prompt, + int n = 1, + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart index d682a82f0883f..684b3e8264ac5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart @@ -3,12 +3,12 @@ part 'error.freezed.dart'; part 'error.g.dart'; @freezed -class OpenAIError with _$OpenAIError { - const factory OpenAIError({ +class AIError with _$AIError { + const factory AIError({ String? code, required String message, - }) = _OpenAIError; + }) = _AIError; - factory OpenAIError.fromJson(Map json) => - _$OpenAIErrorFromJson(json); + factory AIError.fromJson(Map json) => + _$AIErrorFromJson(json); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart index eb688b2c56427..b2115ff5d73b3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pbenum.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:http/http.dart' as http; @@ -25,58 +26,7 @@ enum OpenAIRequestType { } } -abstract class OpenAIRepository { - /// Get completions from GPT-3 - /// - /// [prompt] is the prompt text - /// [suffix] is the suffix text - /// [maxTokens] is the maximum number of tokens to generate - /// [temperature] is the temperature of the model - /// - Future> getCompletions({ - required String prompt, - String? suffix, - int maxTokens = 2048, - double temperature = .3, - }); - - Future getStreamedCompletions({ - required String prompt, - required Future Function() onStart, - required Future Function(TextCompletionResponse response) onProcess, - required Future Function() onEnd, - required void Function(OpenAIError error) onError, - String? suffix, - int maxTokens = 2048, - double temperature = 0.3, - bool useAction = false, - }); - - /// Get edits from GPT-3 - /// - /// [input] is the input text - /// [instruction] is the instruction text - /// [temperature] is the temperature of the model - /// - Future> getEdits({ - required String input, - required String instruction, - double temperature = 0.3, - }); - - /// Generate image from GPT-3 - /// - /// [prompt] is the prompt text - /// [n] is the number of images to generate - /// - /// the result is a list of urls - Future, OpenAIError>> generateImage({ - required String prompt, - int n = 1, - }); -} - -class HttpOpenAIRepository implements OpenAIRepository { +class HttpOpenAIRepository implements AIRepository { const HttpOpenAIRepository({ required this.client, required this.apiKey, @@ -90,50 +40,13 @@ class HttpOpenAIRepository implements OpenAIRepository { 'Content-Type': 'application/json', }; - @override - Future> getCompletions({ - required String prompt, - String? suffix, - int maxTokens = 2048, - double temperature = 0.3, - }) async { - final parameters = { - 'model': 'gpt-3.5-turbo-instruct', - 'prompt': prompt, - 'suffix': suffix, - 'max_tokens': maxTokens, - 'temperature': temperature, - 'stream': false, - }; - - final response = await client.post( - OpenAIRequestType.textCompletion.uri, - headers: headers, - body: json.encode(parameters), - ); - - if (response.statusCode == 200) { - return FlowyResult.success( - TextCompletionResponse.fromJson( - json.decode( - utf8.decode(response.bodyBytes), - ), - ), - ); - } else { - return FlowyResult.failure( - OpenAIError.fromJson(json.decode(response.body)['error']), - ); - } - } - @override Future getStreamedCompletions({ required String prompt, required Future Function() onStart, required Future Function(TextCompletionResponse response) onProcess, required Future Function() onEnd, - required void Function(OpenAIError error) onError, + required void Function(AIError error) onError, String? suffix, int maxTokens = 2048, double temperature = 0.3, @@ -201,50 +114,14 @@ class HttpOpenAIRepository implements OpenAIRepository { } else { final body = await response.stream.bytesToString(); onError( - OpenAIError.fromJson(json.decode(body)['error']), + AIError.fromJson(json.decode(body)['error']), ); } return; } @override - Future> getEdits({ - required String input, - required String instruction, - double temperature = 0.3, - int n = 1, - }) async { - final parameters = { - 'model': 'gpt-4', - 'input': input, - 'instruction': instruction, - 'temperature': temperature, - 'n': n, - }; - - final response = await client.post( - OpenAIRequestType.textEdit.uri, - headers: headers, - body: json.encode(parameters), - ); - - if (response.statusCode == 200) { - return FlowyResult.success( - TextEditResponse.fromJson( - json.decode( - utf8.decode(response.bodyBytes), - ), - ), - ); - } else { - return FlowyResult.failure( - OpenAIError.fromJson(json.decode(response.body)['error']), - ); - } - } - - @override - Future, OpenAIError>> generateImage({ + Future, AIError>> generateImage({ required String prompt, int n = 1, }) async { @@ -273,11 +150,23 @@ class HttpOpenAIRepository implements OpenAIRepository { return FlowyResult.success(urls); } else { return FlowyResult.failure( - OpenAIError.fromJson(json.decode(response.body)['error']), + AIError.fromJson(json.decode(response.body)['error']), ); } } catch (error) { - return FlowyResult.failure(OpenAIError(message: error.toString())); + return FlowyResult.failure(AIError(message: error.toString())); } } + + @override + Future streamCompletion({ + required String text, + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + }) { + throw UnimplementedError(); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart index 0675fb3bafa27..6f07521858118 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart @@ -1,22 +1,20 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; +import 'package:appflowy/user/application/ai_service.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; 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/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:provider/provider.dart'; class AutoCompletionBlockKeys { @@ -187,15 +185,11 @@ class _AutoCompletionBlockComponentState } Future _onGenerate() async { - final loading = Loading(context); - loading.start(); - await _updateEditingText(); final userProfile = await UserBackendService.getCurrentUserProfile() .then((value) => value.toNullable()); if (userProfile == null) { - await loading.stop(); if (mounted) { showSnackBarMessage( context, @@ -208,34 +202,28 @@ class _AutoCompletionBlockComponentState final textRobot = TextRobot(editorState: editorState); BarrierDialog? barrierDialog; - final openAIRepository = HttpOpenAIRepository( - client: http.Client(), - apiKey: userProfile.openaiKey, - ); - await openAIRepository.getStreamedCompletions( - prompt: controller.text, + final aiRepository = AppFlowyAIService(); + await aiRepository.streamCompletion( + text: controller.text, + completionType: CompletionTypePB.ContinueWriting, onStart: () async { - await loading.stop(); if (mounted) { barrierDialog = BarrierDialog(context); barrierDialog?.show(); await _makeSurePreviousNodeIsEmptyParagraphNode(); } }, - onProcess: (response) async { - if (response.choices.isNotEmpty) { - final text = response.choices.first.text; - await textRobot.autoInsertText( - text, - delay: Duration.zero, - ); - } + onProcess: (text) async { + await textRobot.autoInsertText( + text, + delay: Duration.zero, + ); }, onEnd: () async { - await barrierDialog?.dismiss(); + barrierDialog?.dismiss(); }, onError: (error) async { - await loading.stop(); + barrierDialog?.dismiss(); if (mounted) { showSnackBarMessage( context, @@ -272,8 +260,6 @@ class _AutoCompletionBlockComponentState return; } - final loading = Loading(context); - loading.start(); // clear previous response final selection = startSelection; if (selection != null) { @@ -292,7 +278,6 @@ class _AutoCompletionBlockComponentState final userProfile = await UserBackendService.getCurrentUserProfile() .then((value) => value.toNullable()); if (userProfile == null) { - await loading.stop(); if (mounted) { showSnackBarMessage( context, @@ -303,28 +288,21 @@ class _AutoCompletionBlockComponentState return; } final textRobot = TextRobot(editorState: editorState); - final openAIRepository = HttpOpenAIRepository( - client: http.Client(), - apiKey: userProfile.openaiKey, - ); - await openAIRepository.getStreamedCompletions( - prompt: _rewritePrompt(previousOutput), + final aiResposity = AppFlowyAIService(); + await aiResposity.streamCompletion( + text: _rewritePrompt(previousOutput), + completionType: CompletionTypePB.ContinueWriting, onStart: () async { - await loading.stop(); await _makeSurePreviousNodeIsEmptyParagraphNode(); }, - onProcess: (response) async { - if (response.choices.isNotEmpty) { - final text = response.choices.first.text; - await textRobot.autoInsertText( - text, - delay: Duration.zero, - ); - } + onProcess: (text) async { + await textRobot.autoInsertText( + text, + delay: Duration.zero, + ); }, onEnd: () async {}, onError: (error) async { - await loading.stop(); if (mounted) { showSnackBarMessage( context, @@ -462,23 +440,9 @@ class AutoCompletionHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - children: [ - FlowyText.medium( - LocaleKeys.document_plugins_autoGeneratorTitleName.tr(), - fontSize: 14, - ), - const Spacer(), - FlowyButton( - useIntrinsicWidth: true, - text: FlowyText.regular( - LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), - ), - onTap: () async { - await openLearnMorePage(); - }, - ), - ], + return FlowyText.medium( + LocaleKeys.document_plugins_autoGeneratorTitleName.tr(), + fontSize: 14, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart index abaec7c9e9a47..66ce0bef5f174 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart @@ -28,7 +28,7 @@ class Loading { ), ); - Future stop() async { + void stop() { if (loadingContext != null) { Navigator.of(loadingContext!).pop(); loadingContext = null; @@ -54,5 +54,5 @@ class BarrierDialog { ), ); - Future dismiss() async => Navigator.of(loadingContext).pop(); + void dismiss() => Navigator.of(loadingContext).pop(); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart index f023d56abee8f..ef1148608e3f7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart @@ -1,18 +1,18 @@ import 'dart:async'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/ai_service.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.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:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:http/http.dart' as http; import 'package:provider/provider.dart'; @@ -229,7 +229,11 @@ class _SmartEditInputWidgetState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeaderWidget(context), + FlowyText.medium( + action.name, + fontSize: 14, + ), + // _buildHeaderWidget(context), const Space(0, 10), _buildResultWidget(context), const Space(0, 10), @@ -238,27 +242,6 @@ class _SmartEditInputWidgetState extends State { ); } - Widget _buildHeaderWidget(BuildContext context) { - return Row( - children: [ - FlowyText.medium( - '${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}', - fontSize: 14, - ), - const Spacer(), - FlowyButton( - useIntrinsicWidth: true, - text: FlowyText.regular( - LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), - ), - onTap: () async { - await openLearnMorePage(); - }, - ), - ], - ); - } - Widget _buildResultWidget(BuildContext context) { final loadingWidget = Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), @@ -423,45 +406,33 @@ class _SmartEditInputWidgetState extends State { result = ""; }); } - final openAIRepository = await getIt.getAsync(); - - var lines = content.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(() { - if (response.choices.first.text != '\n') { - result += response.choices.first.text; - } - }); - }, - onEnd: () async { - setState(() { - if (i != lines.length - 1) { - result += '\n'; - } - }); - }, - onError: (error) async { - showSnackBarMessage( - context, - error.message, - showCancel: true, - ); - await _onExit(); - }, - ); - } + final aiResitory = await getIt.getAsync(); + await aiResitory.streamCompletion( + text: content, + completionType: completionTypeFromInt(action), + onStart: () async { + setState(() { + loading = false; + }); + }, + onProcess: (text) async { + setState(() { + result += text; + }); + }, + onEnd: () async { + setState(() { + result += '\n'; + }); + }, + onError: (error) async { + showSnackBarMessage( + context, + error.message, + showCancel: true, + ); + await _onExit(); + }, + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart index a0f7002889c23..84d122cf1864a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart @@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -32,7 +33,7 @@ class SmartEditActionList extends StatefulWidget { } class _SmartEditActionListState extends State { - bool isOpenAIEnabled = false; + bool isAIEnabled = false; @override void initState() { @@ -40,8 +41,9 @@ class _SmartEditActionListState extends State { UserBackendService.getCurrentUserProfile().then((value) { setState(() { - isOpenAIEnabled = value.fold( - (s) => s.openaiKey.isNotEmpty, + isAIEnabled = value.fold( + (userProfile) => + userProfile.authenticator == AuthenticatorPB.AppFlowyCloud, (_) => false, ); }); @@ -60,9 +62,9 @@ class _SmartEditActionListState extends State { keepEditorFocusNotifier.increase(); return FlowyIconButton( hoverColor: Colors.transparent, - tooltipText: isOpenAIEnabled + tooltipText: isAIEnabled ? LocaleKeys.document_plugins_smartEdit.tr() - : LocaleKeys.document_plugins_smartEditDisabled.tr(), + : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), preferBelow: false, icon: const Icon( Icons.lightbulb_outline, @@ -70,12 +72,12 @@ class _SmartEditActionListState extends State { color: Colors.white, ), onPressed: () { - if (isOpenAIEnabled) { + if (isAIEnabled) { controller.show(); } else { showSnackBarMessage( context, - LocaleKeys.document_plugins_smartEditDisabled.tr(), + LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), showCancel: true, ); } diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index cf95d8f8a0015..89049fb08a3b9 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -3,13 +3,14 @@ import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart'; import 'package:appflowy/plugins/trash/application/prelude.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; +import 'package:appflowy/user/application/ai_service.dart'; import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/supabase_auth_service.dart'; @@ -85,15 +86,12 @@ void _resolveCommonService( () => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(), ); - getIt.registerFactoryAsync( + getIt.registerFactoryAsync( () async { final result = await UserBackendService.getCurrentUserProfile(); return result.fold( (s) { - return HttpOpenAIRepository( - client: http.Client(), - apiKey: s.openaiKey, - ); + return AppFlowyAIService(); }, (e) { throw Exception('Failed to get user profile: ${e.msg}'); diff --git a/frontend/appflowy_flutter/lib/user/application/ai_service.dart b/frontend/appflowy_flutter/lib/user/application/ai_service.dart new file mode 100644 index 0000000000000..41a46b9156ff5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/ai_service.dart @@ -0,0 +1,122 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:isolate'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:fixnum/fixnum.dart' as fixnum; + +class AppFlowyAIService implements AIRepository { + @override + Future, AIError>> generateImage({ + required String prompt, + int n = 1, + }) { + throw UnimplementedError(); + } + + @override + Future getStreamedCompletions({ + required String prompt, + required Future Function() onStart, + required Future Function(TextCompletionResponse response) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + String? suffix, + int maxTokens = 2048, + double temperature = 0.3, + bool useAction = false, + }) { + throw UnimplementedError(); + } + + @override + Future streamCompletion({ + required String text, + required CompletionTypePB completionType, + required Future Function() onStart, + required Future Function(String text) onProcess, + required Future Function() onEnd, + required void Function(AIError error) onError, + }) async { + final stream = CompletionStream( + onStart, + onProcess, + onEnd, + onError, + ); + final payload = CompleteTextPB( + text: text, + completionType: completionType, + streamPort: fixnum.Int64(stream.nativePort), + ); + + // ignore: unawaited_futures + ChatEventCompleteText(payload).send(); + return stream; + } +} + +CompletionTypePB completionTypeFromInt(SmartEditAction action) { + switch (action) { + case SmartEditAction.summarize: + return CompletionTypePB.MakeShorter; + case SmartEditAction.fixSpelling: + return CompletionTypePB.SpellingAndGrammar; + case SmartEditAction.improveWriting: + return CompletionTypePB.ImproveWriting; + case SmartEditAction.makeItLonger: + return CompletionTypePB.MakeLonger; + } +} + +class CompletionStream { + CompletionStream( + Future Function() onStart, + Future Function(String text) onProcess, + Future Function() onEnd, + void Function(AIError error) onError, + ) { + _port.handler = _controller.add; + _subscription = _controller.stream.listen( + (event) async { + if (event.startsWith("start:")) { + await onStart(); + } + + if (event.startsWith("data:")) { + await onProcess(event.substring(5)); + } + + if (event.startsWith("finish:")) { + await onEnd(); + } + if (event.startsWith("error:")) { + onError(AIError(message: event.substring(6))); + } + }, + ); + } + + final RawReceivePort _port = RawReceivePort(); + final StreamController _controller = StreamController.broadcast(); + late StreamSubscription _subscription; + int get nativePort => _port.sendPort.nativePort; + + Future dispose() async { + await _controller.close(); + await _subscription.cancel(); + _port.close(); + } + + StreamSubscription listen( + void Function(String event)? onData, + ) { + return _controller.stream.listen(onData); + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index 81a081b4e3349..36d6039d405f9 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/foundation.dart'; import 'package:appflowy/core/notification/folder_notification.dart'; @@ -11,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart' as user; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; @@ -23,6 +23,9 @@ typedef DidUpdateUserWorkspacesCallback = void Function( RepeatedUserWorkspacePB workspaces, ); typedef UserProfileNotifyValue = FlowyResult; +typedef DidUpdateUserWorkspaceSetting = void Function( + UseAISettingPB settings, +); class UserListener { UserListener({ @@ -37,28 +40,26 @@ class UserListener { /// Update notification about _all_ of the users workspaces /// - DidUpdateUserWorkspacesCallback? didUpdateUserWorkspaces; + DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated; /// Update notification about _one_ workspace /// - DidUpdateUserWorkspaceCallback? didUpdateUserWorkspace; + DidUpdateUserWorkspaceCallback? onUserWorkspaceUpdated; + DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated; void start({ void Function(UserProfileNotifyValue)? onProfileUpdated, - void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces, - void Function(UserWorkspacePB)? didUpdateUserWorkspace, + DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated, + void Function(UserWorkspacePB)? onUserWorkspaceUpdated, + DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated, }) { if (onProfileUpdated != null) { _profileNotifier?.addPublishListener(onProfileUpdated); } - if (didUpdateUserWorkspaces != null) { - this.didUpdateUserWorkspaces = didUpdateUserWorkspaces; - } - - if (didUpdateUserWorkspace != null) { - this.didUpdateUserWorkspace = didUpdateUserWorkspace; - } + this.onUserWorkspaceListUpdated = onUserWorkspaceListUpdated; + this.onUserWorkspaceUpdated = onUserWorkspaceUpdated; + this.onUserWorkspaceSettingUpdated = onUserWorkspaceSettingUpdated; _userParser = UserNotificationParser( id: _userProfile.id.toString(), @@ -92,13 +93,18 @@ class UserListener { result.map( (r) { final value = RepeatedUserWorkspacePB.fromBuffer(r); - didUpdateUserWorkspaces?.call(value); + onUserWorkspaceListUpdated?.call(value); }, ); break; case user.UserNotification.DidUpdateUserWorkspace: result.map( - (r) => didUpdateUserWorkspace?.call(UserWorkspacePB.fromBuffer(r)), + (r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)), + ); + case user.UserNotification.DidUpdateAISetting: + result.map( + (r) => + onUserWorkspaceSettingUpdated?.call(UseAISettingPB.fromBuffer(r)), ); break; default: @@ -110,8 +116,8 @@ class UserListener { typedef WorkspaceSettingNotifyValue = FlowyResult; -class UserWorkspaceListener { - UserWorkspaceListener(); +class FolderListener { + FolderListener(); final PublishNotifier _settingChangedNotifier = PublishNotifier(); diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 2df1c95c1f42a..9d635fd6922b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -9,12 +9,12 @@ part 'home_bloc.freezed.dart'; class HomeBloc extends Bloc { HomeBloc(WorkspaceSettingPB workspaceSetting) - : _workspaceListener = UserWorkspaceListener(), + : _workspaceListener = FolderListener(), super(HomeState.initial(workspaceSetting)) { _dispatch(workspaceSetting); } - final UserWorkspaceListener _workspaceListener; + final FolderListener _workspaceListener; @override Future close() async { diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart index 171bf634a70b7..05418f3315e4c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart @@ -15,7 +15,7 @@ class HomeSettingBloc extends Bloc { WorkspaceSettingPB workspaceSetting, AppearanceSettingsCubit appearanceSettingsCubit, double screenWidthPx, - ) : _listener = UserWorkspaceListener(), + ) : _listener = FolderListener(), _appearanceSettingsCubit = appearanceSettingsCubit, super( HomeSettingState.initial( @@ -27,7 +27,7 @@ class HomeSettingBloc extends Bloc { _dispatch(); } - final UserWorkspaceListener _listener; + final FolderListener _listener; final AppearanceSettingsCubit _appearanceSettingsCubit; @override diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart index 249144096ced6..a9e4d28a3aab9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart @@ -13,7 +13,7 @@ part 'menu_user_bloc.freezed.dart'; class MenuUserBloc extends Bloc { MenuUserBloc(this.userProfile) : _userListener = UserListener(userProfile: userProfile), - _userWorkspaceListener = UserWorkspaceListener(), + _userWorkspaceListener = FolderListener(), _userService = UserBackendService(userId: userProfile.id), super(MenuUserState.initial(userProfile)) { _dispatch(); @@ -21,7 +21,7 @@ class MenuUserBloc extends Bloc { final UserBackendService _userService; final UserListener _userListener; - final UserWorkspaceListener _userWorkspaceListener; + final FolderListener _userWorkspaceListener; final UserProfilePB userProfile; @override diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart new file mode 100644 index 0000000000000..212c3c313917c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -0,0 +1,127 @@ +import 'package:appflowy/user/application/user_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'settings_ai_bloc.freezed.dart'; + +class SettingsAIBloc extends Bloc { + SettingsAIBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + super(SettingsAIState(userProfile: userProfile)) { + _dispatch(); + } + + final UserListener _userListener; + final UserProfilePB userProfile; + + @override + Future close() async { + await _userListener.stop(); + return super.close(); + } + + void _dispatch() { + on((event, emit) { + event.when( + started: () { + _userListener.start( + onProfileUpdated: _onProfileUpdated, + onUserWorkspaceSettingUpdated: (settings) { + if (!isClosed) { + add(SettingsAIEvent.didLoadAISetting(settings)); + } + }, + ); + _loadUserWorkspaceSetting(); + }, + didReceiveUserProfile: (userProfile) { + emit(state.copyWith(userProfile: userProfile)); + }, + toggleAISearch: () { + _updateUserWorkspaceSetting( + disableSearchIndexing: + !(state.aiSettings?.disableSearchIndexing ?? false), + ); + }, + selectModel: (AIModelPB model) { + _updateUserWorkspaceSetting(model: model); + }, + didLoadAISetting: (UseAISettingPB settings) { + emit( + state.copyWith( + aiSettings: settings, + enableSearchIndexing: !settings.disableSearchIndexing, + ), + ); + }, + ); + }); + } + + void _updateUserWorkspaceSetting({ + bool? disableSearchIndexing, + AIModelPB? model, + }) { + final payload = + UpdateUserWorkspaceSettingPB(workspaceId: userProfile.workspaceId); + if (disableSearchIndexing != null) { + payload.disableSearchIndexing = disableSearchIndexing; + } + if (model != null) { + payload.aiModel = model; + } + UserEventUpdateWorkspaceSetting(payload).send(); + } + + void _onProfileUpdated( + FlowyResult userProfileOrFailed, + ) => + userProfileOrFailed.fold( + (newUserProfile) => + add(SettingsAIEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); + + void _loadUserWorkspaceSetting() { + final payload = UserWorkspaceIdPB(workspaceId: userProfile.workspaceId); + UserEventGetWorkspaceSetting(payload).send().then((result) { + result.fold((settins) { + if (!isClosed) { + add(SettingsAIEvent.didLoadAISetting(settins)); + } + }, (err) { + Log.error(err); + }); + }); + } +} + +@freezed +class SettingsAIEvent with _$SettingsAIEvent { + const factory SettingsAIEvent.started() = _Started; + const factory SettingsAIEvent.didLoadAISetting( + UseAISettingPB settings, + ) = _DidLoadWorkspaceSetting; + + const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch; + + const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel; + + const factory SettingsAIEvent.didReceiveUserProfile( + UserProfilePB newUserProfile, + ) = _DidReceiveUserProfile; +} + +@freezed +class SettingsAIState with _$SettingsAIState { + const factory SettingsAIState({ + required UserProfilePB userProfile, + UseAISettingPB? aiSettings, + @Default(true) bool enableSearchIndexing, + }) = _SettingsAIState; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 7395dce731dc0..17fbe7346509c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -13,12 +13,13 @@ enum SettingsPage { account, workspace, manageData, + shortcuts, + ai, plan, billing, // OLD notifications, cloud, - shortcuts, member, featureFlags, } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart index 59127bdab09ed..56faa9f8d8b21 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart @@ -63,24 +63,6 @@ class SettingsUserViewBloc extends Bloc { ); }); }, - updateUserOpenAIKey: (openAIKey) { - _userService.updateUserProfile(openAIKey: openAIKey).then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, - updateUserStabilityAIKey: (stabilityAIKey) { - _userService - .updateUserProfile(stabilityAiKey: stabilityAIKey) - .then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, updateUserEmail: (String email) { _userService.updateUserProfile(email: email).then((result) { result.fold( @@ -127,11 +109,6 @@ class SettingsUserEvent with _$SettingsUserEvent { const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) = _UpdateUserIcon; const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon; - const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) = - _UpdateUserOpenaiKey; - const factory SettingsUserEvent.updateUserStabilityAIKey( - String stabilityAIKey, - ) = _UpdateUserStabilityAIKey; const factory SettingsUserEvent.didReceiveUserProfile( UserProfilePB newUserProfile, ) = _DidReceiveUserProfile; diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 2f79701707f44..50d11d7882020 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -29,9 +29,9 @@ class UserWorkspaceBloc extends Bloc { await event.when( initial: () async { _listener.start( - didUpdateUserWorkspaces: (workspaces) => + onUserWorkspaceListUpdated: (workspaces) => add(UserWorkspaceEvent.updateWorkspaces(workspaces)), - didUpdateUserWorkspace: (workspace) { + onUserWorkspaceUpdated: (workspace) { // If currentWorkspace is updated, eg. Icon or Name, we should notify // the UI to render the updated information. final currentWorkspace = state.currentWorkspace; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index e4df04647eee5..dd76251539e3b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -136,39 +136,7 @@ class _SettingsAccountViewState extends State { // ), // ], // ), - SettingsCategory( - title: LocaleKeys.settings_accountPage_keys_title.tr(), - children: [ - SettingsInputField( - label: - LocaleKeys.settings_accountPage_keys_openAILabel.tr(), - tooltip: - LocaleKeys.settings_accountPage_keys_openAITooltip.tr(), - placeholder: - LocaleKeys.settings_accountPage_keys_openAIHint.tr(), - value: state.userProfile.openaiKey, - obscureText: true, - onSave: (key) => context - .read() - .add(SettingsUserEvent.updateUserOpenAIKey(key)), - ), - SettingsInputField( - label: LocaleKeys.settings_accountPage_keys_stabilityAILabel - .tr(), - tooltip: LocaleKeys - .settings_accountPage_keys_stabilityAITooltip - .tr(), - placeholder: LocaleKeys - .settings_accountPage_keys_stabilityAIHint - .tr(), - value: state.userProfile.stabilityAiKey, - obscureText: true, - onSave: (key) => context - .read() - .add(SettingsUserEvent.updateUserStabilityAIKey(key)), - ), - ], - ), + SettingsCategory( title: LocaleKeys.settings_accountPage_login_title.tr(), children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_ai_view.dart new file mode 100644 index 0000000000000..96fe256bd030e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_ai_view.dart @@ -0,0 +1,168 @@ +import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget { + const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30), + child: FlowyText( + LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(), + maxLines: null, + fontSize: 16, + lineHeight: 1.6, + ), + ); + } +} + +class SettingsAIView extends StatelessWidget { + const SettingsAIView({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()), + child: BlocBuilder( + builder: (context, state) { + return SettingsBody( + title: LocaleKeys.settings_aiPage_title.tr(), + description: + LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), + children: const [ + AIModelSeclection(), + _AISearchToggle(value: false), + ], + ); + }, + ), + ); + } +} + +class AIModelSeclection extends StatelessWidget { + const AIModelSeclection({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + FlowyText( + LocaleKeys.settings_aiPage_keys_llmModel.tr(), + fontSize: 14, + ), + const Spacer(), + BlocBuilder( + builder: (context, state) { + return Expanded( + child: SettingsDropdown( + key: const Key('AIModelDropdown'), + expandWidth: false, + onChanged: (format) { + context.read().add( + SettingsAIEvent.selectModel(format), + ); + }, + selectedOption: state.userProfile.aiModel, + options: _availableModels + .map( + (format) => buildDropdownMenuEntry( + context, + value: format, + label: _titleForAIModel(format), + ), + ) + .toList(), + ), + ); + }, + ), + ], + ); + } +} + +List _availableModels = [ + AIModelPB.DefaultModel, + AIModelPB.Claude3Opus, + AIModelPB.Claude3Sonnet, + AIModelPB.GPT35, + AIModelPB.GPT4o, +]; + +String _titleForAIModel(AIModelPB model) { + switch (model) { + case AIModelPB.DefaultModel: + return "Default"; + case AIModelPB.Claude3Opus: + return "Claude 3 Opus"; + case AIModelPB.Claude3Sonnet: + return "Claude 3 Sonnet"; + case AIModelPB.GPT35: + return "GPT-3.5"; + case AIModelPB.GPT4o: + return "GPT-4o"; + case AIModelPB.LocalAIModel: + return "Local"; + default: + Log.error("Unknown AI model: $model, fallback to default"); + return "Default"; + } +} + +class _AISearchToggle extends StatelessWidget { + const _AISearchToggle({required this.value}); + + final bool value; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: FlowyText.regular( + LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(), + fontSize: 16, + ), + ), + const HSpace(16), + BlocBuilder( + builder: (context, state) { + if (state.aiSettings == null) { + return const CircularProgressIndicator.adaptive(); + } else { + return Toggle( + value: state.enableSearchIndexing, + onChanged: (_) { + context.read().add( + const SettingsAIEvent.toggleAISearch(), + ); + }, + ); + } + }, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index cbe92f1b164b9..e464963ef830b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,9 +1,11 @@ +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_ai_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart'; @@ -13,8 +15,6 @@ import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/f import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -113,6 +113,12 @@ class SettingsDialog extends StatelessWidget { return SettingCloud(restartAppFlowy: () => restartApp()); case SettingsPage.shortcuts: return const SettingsShortcutsView(); + case SettingsPage.ai: + if (user.authenticator == AuthenticatorPB.AppFlowyCloud) { + return SettingsAIView(userProfile: user); + } else { + return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud(); + } case SettingsPage.member: return WorkspaceMembersPage(userProfile: user); case SettingsPage.plan: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart index 04bc501c6b17b..13c9e80bd90d6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart @@ -41,8 +41,7 @@ class RestartButton extends StatelessWidget { SizedBox( height: 42, child: FlowyTextButton( - LocaleKeys.settings_manageDataPage_dataStorage_actions_change - .tr(), + LocaleKeys.settings_menu_restartApp.tr(), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), fontWeight: FontWeight.w600, radius: BorderRadius.circular(12), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 0e89ba8cf7d20..08fb46d58deac 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -102,6 +102,16 @@ class SettingsMenu extends StatelessWidget { icon: const FlowySvg(FlowySvgs.settings_shortcuts_m), changeSelectedPage: changeSelectedPage, ), + SettingsMenuElement( + page: SettingsPage.ai, + selectedPage: currentPage, + label: LocaleKeys.settings_aiPage_menuLabel.tr(), + icon: const FlowySvg( + FlowySvgs.ai_summary_generate_s, + size: Size.square(24), + ), + changeSelectedPage: changeSelectedPage, + ), if (FeatureFlag.planBilling.isOn && userProfile.authenticator == AuthenticatorPB.AppFlowyCloud && diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index aa07be5eebcd8..fbeb556d742c1 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bincode", @@ -192,7 +192,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bytes", @@ -772,7 +772,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "again", "anyhow", @@ -787,6 +787,7 @@ dependencies = [ "collab", "collab-rt-entity", "collab-rt-protocol", + "futures", "futures-core", "futures-util", "getrandom 0.2.10", @@ -794,6 +795,8 @@ dependencies = [ "infra", "mime", "parking_lot 0.12.1", + "percent-encoding", + "pin-project", "prost", "reqwest", "scraper 0.17.1", @@ -818,7 +821,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "collab-entity", "collab-rt-entity", @@ -830,7 +833,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "futures-channel", "futures-util", @@ -1070,7 +1073,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bincode", @@ -1095,7 +1098,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "async-trait", @@ -1341,7 +1344,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1452,10 +1455,11 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", + "appflowy-ai-client", "bincode", "chrono", "collab-entity", @@ -2426,6 +2430,7 @@ dependencies = [ "anyhow", "base64 0.21.5", "chrono", + "client-api", "collab", "collab-entity", "flowy-error", @@ -2894,7 +2899,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "futures-util", @@ -2911,7 +2916,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", @@ -3343,7 +3348,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bytes", @@ -4850,7 +4855,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4871,7 +4876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -5835,7 +5840,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index bae84180da558..4f8315b12b168 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" } [dependencies] serde_json.workspace = true diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index 42fefea9ac8e9..7b3fe44248ec4 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -215,7 +215,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bincode", @@ -235,7 +235,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bytes", @@ -561,7 +561,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "again", "anyhow", @@ -576,6 +576,7 @@ dependencies = [ "collab", "collab-rt-entity", "collab-rt-protocol", + "futures", "futures-core", "futures-util", "getrandom 0.2.12", @@ -583,6 +584,8 @@ dependencies = [ "infra", "mime", "parking_lot 0.12.1", + "percent-encoding", + "pin-project", "prost", "reqwest", "scraper 0.17.1", @@ -607,7 +610,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "collab-entity", "collab-rt-entity", @@ -619,7 +622,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "futures-channel", "futures-util", @@ -797,7 +800,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bincode", @@ -822,7 +825,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "async-trait", @@ -1036,10 +1039,11 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", + "appflowy-ai-client", "bincode", "chrono", "collab-entity", @@ -1662,6 +1666,7 @@ dependencies = [ "anyhow", "base64 0.21.7", "chrono", + "client-api", "collab", "collab-entity", "flowy-error", @@ -1919,7 +1924,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "futures-util", @@ -1936,7 +1941,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", @@ -2237,7 +2242,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bytes", @@ -3951,7 +3956,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 9816e6ef94844..7e0387d86234a 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -20,6 +20,7 @@ flowy-derive = { path = "../../rust-lib/build-tool/flowy-derive" } flowy-codegen = { path = "../../rust-lib/build-tool/flowy-codegen" } flowy-document = { path = "../../rust-lib/flowy-document" } flowy-folder = { path = "../../rust-lib/flowy-folder" } +flowy-storage = { path = "../../rust-lib/flowy-storage" } lib-infra = { path = "../../rust-lib/lib-infra" } bytes = { version = "1.5" } protobuf = { version = "2.28.0" } @@ -54,7 +55,7 @@ yrs = "0.18.8" # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" } [profile.dev] opt-level = 0 diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/entities/user.rs b/frontend/appflowy_web/wasm-libs/af-user/src/entities/user.rs index 0aa22d6e7abb9..113ec6fec4c2c 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/src/entities/user.rs +++ b/frontend/appflowy_web/wasm-libs/af-user/src/entities/user.rs @@ -33,6 +33,9 @@ pub struct UserProfilePB { #[pb(index = 10)] pub stability_ai_key: String, + + #[pb(index = 11)] + pub ai_model: String, } impl From for UserProfilePB { @@ -52,6 +55,7 @@ impl From for UserProfilePB { authenticator: user_profile.authenticator.into(), workspace_id: user_profile.workspace_id, stability_ai_key: user_profile.stability_ai_key, + ai_model: user_profile.ai_model, } } } diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/user.rs b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/user.rs index 52f066e2d4c0c..23d352aa0bc10 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/user.rs +++ b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/user.rs @@ -36,6 +36,7 @@ pub struct UserProfilePB { pub encryption_sign: ::std::string::String, pub workspace_id: ::std::string::String, pub stability_ai_key: ::std::string::String, + pub ai_model: ::std::string::String, // special fields pub unknown_fields: ::protobuf::UnknownFields, pub cached_size: ::protobuf::CachedSize, @@ -289,6 +290,32 @@ impl UserProfilePB { pub fn take_stability_ai_key(&mut self) -> ::std::string::String { ::std::mem::replace(&mut self.stability_ai_key, ::std::string::String::new()) } + + // string ai_model = 11; + + + pub fn get_ai_model(&self) -> &str { + &self.ai_model + } + pub fn clear_ai_model(&mut self) { + self.ai_model.clear(); + } + + // Param is passed by value, moved + pub fn set_ai_model(&mut self, v: ::std::string::String) { + self.ai_model = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_ai_model(&mut self) -> &mut ::std::string::String { + &mut self.ai_model + } + + // Take field + pub fn take_ai_model(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.ai_model, ::std::string::String::new()) + } } impl ::protobuf::Message for UserProfilePB { @@ -334,6 +361,9 @@ impl ::protobuf::Message for UserProfilePB { 10 => { ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.stability_ai_key)?; }, + 11 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.ai_model)?; + }, _ => { ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; }, @@ -376,6 +406,9 @@ impl ::protobuf::Message for UserProfilePB { if !self.stability_ai_key.is_empty() { my_size += ::protobuf::rt::string_size(10, &self.stability_ai_key); } + if !self.ai_model.is_empty() { + my_size += ::protobuf::rt::string_size(11, &self.ai_model); + } my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); self.cached_size.set(my_size); my_size @@ -412,6 +445,9 @@ impl ::protobuf::Message for UserProfilePB { if !self.stability_ai_key.is_empty() { os.write_string(10, &self.stability_ai_key)?; } + if !self.ai_model.is_empty() { + os.write_string(11, &self.ai_model)?; + } os.write_unknown_fields(self.get_unknown_fields())?; ::std::result::Result::Ok(()) } @@ -500,6 +536,11 @@ impl ::protobuf::Message for UserProfilePB { |m: &UserProfilePB| { &m.stability_ai_key }, |m: &mut UserProfilePB| { &mut m.stability_ai_key }, )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "ai_model", + |m: &UserProfilePB| { &m.ai_model }, + |m: &mut UserProfilePB| { &mut m.ai_model }, + )); ::protobuf::reflect::MessageDescriptor::new_pb_name::( "UserProfilePB", fields, @@ -526,6 +567,7 @@ impl ::protobuf::Clear for UserProfilePB { self.encryption_sign.clear(); self.workspace_id.clear(); self.stability_ai_key.clear(); + self.ai_model.clear(); self.unknown_fields.clear(); } } @@ -593,7 +635,7 @@ impl ::protobuf::reflect::ProtobufValue for EncryptionTypePB { } static file_descriptor_proto_data: &'static [u8] = b"\ - \n\nuser.proto\x1a\nauth.proto\"\xc7\x02\n\rUserProfilePB\x12\x0e\n\x02i\ + \n\nuser.proto\x1a\nauth.proto\"\xe2\x02\n\rUserProfilePB\x12\x0e\n\x02i\ d\x18\x01\x20\x01(\x03R\x02id\x12\x14\n\x05email\x18\x02\x20\x01(\tR\x05\ email\x12\x12\n\x04name\x18\x03\x20\x01(\tR\x04name\x12\x14\n\x05token\ \x18\x04\x20\x01(\tR\x05token\x12\x19\n\x08icon_url\x18\x05\x20\x01(\tR\ @@ -601,8 +643,9 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \rauthenticator\x18\x07\x20\x01(\x0e2\x10.AuthenticatorPBR\rauthenticato\ r\x12'\n\x0fencryption_sign\x18\x08\x20\x01(\tR\x0eencryptionSign\x12!\n\ \x0cworkspace_id\x18\t\x20\x01(\tR\x0bworkspaceId\x12(\n\x10stability_ai\ - _key\x18\n\x20\x01(\tR\x0estabilityAiKey*3\n\x10EncryptionTypePB\x12\x10\ - \n\x0cNoEncryption\x10\0\x12\r\n\tSymmetric\x10\x01b\x06proto3\ + _key\x18\n\x20\x01(\tR\x0estabilityAiKey\x12\x19\n\x08ai_model\x18\x0b\ + \x20\x01(\tR\x07aiModel*3\n\x10EncryptionTypePB\x12\x10\n\x0cNoEncryptio\ + n\x10\0\x12\r\n\tSymmetric\x10\x01b\x06proto3\ "; static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs index 6f3c71025ac08..03ab3f536456e 100644 --- a/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs @@ -41,7 +41,7 @@ impl ServerProviderWASM { self.config.clone(), true, self.device_id.clone(), - "0.0.1" + "0.0.1", )); *self.server.write() = Some(server.clone()); server @@ -70,6 +70,10 @@ impl UserCloudServiceProvider for ServerProviderWASM { Ok(()) } + fn set_ai_model(&self, ai_model: &str) -> Result<(), FlowyError> { + Ok(()) + } + fn subscribe_token_state(&self) -> Option> { self.get_server().subscribe_token_state() } diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index a4a230c51476d..640a6b0f938ff 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bytes", @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "again", "anyhow", @@ -761,6 +761,7 @@ dependencies = [ "collab", "collab-rt-entity", "collab-rt-protocol", + "futures", "futures-core", "futures-util", "getrandom 0.2.12", @@ -768,6 +769,8 @@ dependencies = [ "infra", "mime", "parking_lot 0.12.1", + "percent-encoding", + "pin-project", "prost", "reqwest", "scraper 0.17.1", @@ -792,7 +795,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "collab-entity", "collab-rt-entity", @@ -804,7 +807,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "futures-channel", "futures-util", @@ -1053,7 +1056,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bincode", @@ -1078,7 +1081,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "async-trait", @@ -1328,7 +1331,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.10", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1439,10 +1442,11 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", + "appflowy-ai-client", "bincode", "chrono", "collab-entity", @@ -2463,6 +2467,7 @@ dependencies = [ "anyhow", "base64 0.21.7", "chrono", + "client-api", "collab", "collab-entity", "flowy-error", @@ -2968,7 +2973,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "futures-util", @@ -2985,7 +2990,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", @@ -3422,7 +3427,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bytes", @@ -4931,7 +4936,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4952,7 +4957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.55", @@ -5930,7 +5935,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index ddf01dabc564d..0856fd2fb8158 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -52,7 +52,7 @@ collab-user = { version = "0.2" } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" } [dependencies] serde_json.workspace = true diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 985e17eee8d0f..8cdd618af7845 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -369,15 +369,6 @@ "change": "Change email" } }, - "keys": { - "title": "AI API Keys", - "openAILabel": "OpenAI API key", - "openAITooltip": "You can find your Secret API key on the API key page", - "openAIHint": "Input your OpenAI API Key", - "stabilityAILabel": "Stability API key", - "stabilityAITooltip": "Your Stability API key, used to authenticate your requests", - "stabilityAIHint": "Input your Stability API Key" - }, "login": { "title": "Account login", "loginLabel": "Log in", @@ -611,6 +602,23 @@ "couldNotLoadErrorMsg": "Could not load shortcuts, Try again", "couldNotSaveErrorMsg": "Could not save shortcuts, Try again" }, + "aiPage": { + "title": "AI Settings", + "menuLabel": "AI Settings", + "keys": { + "enableAISearchTitle": "AI Search", + "aiSettingsDescription": "Select or configure Ai models used on AppFlowy. For best performance we recommend using the default model options", + "loginToEnableAIFeature": "AI features are only enabled after logging in with AppFlowy Cloud. If you don't have an AppFlowy account, go to 'My Account' to sign up", + "llmModel": "Language Model", + "title": "AI API Keys", + "openAILabel": "OpenAI API key", + "openAITooltip": "You can find your Secret API key on the API key page", + "openAIHint": "Input your OpenAI API Key", + "stabilityAILabel": "Stability API key", + "stabilityAITooltip": "Your Stability API key, used to authenticate your requests", + "stabilityAIHint": "Input your Stability API Key" + } + }, "planPage": { "menuLabel": "Plan", "title": "Pricing plan", @@ -1295,6 +1303,7 @@ "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI", "smartEditCouldNotFetchKey": "Could not fetch OpenAI key", "smartEditDisabled": "Connect OpenAI in Settings", + "appflowyAIEditDisabled": "Sign in to enable AI features", "discardResponse": "Do you want to discard the AI responses?", "createInlineMathEquation": "Create equation", "fonts": "Fonts", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index ce749cbe0ca79..9ec14034d65c9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bincode", @@ -183,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bytes", @@ -664,7 +664,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "again", "anyhow", @@ -679,6 +679,7 @@ dependencies = [ "collab", "collab-rt-entity", "collab-rt-protocol", + "futures", "futures-core", "futures-util", "getrandom 0.2.10", @@ -686,6 +687,8 @@ dependencies = [ "infra", "mime", "parking_lot 0.12.1", + "percent-encoding", + "pin-project", "prost", "reqwest", "scraper 0.17.1", @@ -710,7 +713,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "collab-entity", "collab-rt-entity", @@ -722,7 +725,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "futures-channel", "futures-util", @@ -931,7 +934,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bincode", @@ -956,7 +959,7 @@ dependencies = [ [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "async-trait", @@ -1176,7 +1179,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1276,10 +1279,11 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", + "appflowy-ai-client", "bincode", "chrono", "collab-entity", @@ -2265,6 +2269,7 @@ dependencies = [ "anyhow", "base64 0.21.5", "chrono", + "client-api", "collab", "collab-entity", "flowy-error", @@ -2567,7 +2572,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "futures-util", @@ -2584,7 +2589,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", @@ -2949,7 +2954,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "bytes", @@ -3827,7 +3832,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3847,6 +3852,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -3914,6 +3920,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -4117,7 +4136,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4138,7 +4157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -5035,7 +5054,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 590bf9fb736c5..af475b1813c5b 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -31,7 +31,6 @@ members = [ "flowy-search-pub", "flowy-chat", "flowy-chat-pub", - "flowy-storage-pub", ] resolver = "2" @@ -97,8 +96,8 @@ validator = { version = "0.16.1", features = ["derive"] } # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" } [profile.dev] opt-level = 1 diff --git a/frontend/rust-lib/event-integration-test/src/chat_event.rs b/frontend/rust-lib/event-integration-test/src/chat_event.rs index 60e8ba65b1a38..498df9511dd44 100644 --- a/frontend/rust-lib/event-integration-test/src/chat_event.rs +++ b/frontend/rust-lib/event-integration-test/src/chat_event.rs @@ -1,8 +1,8 @@ use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; use flowy_chat::entities::{ - ChatMessageListPB, ChatMessageTypePB, LoadNextChatMessagePB, LoadPrevChatMessagePB, - SendChatPayloadPB, + ChatMessageListPB, ChatMessageTypePB, CompleteTextPB, CompleteTextTaskPB, CompletionTypePB, + LoadNextChatMessagePB, LoadPrevChatMessagePB, SendChatPayloadPB, }; use flowy_chat::event_map::ChatEvent; use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; @@ -86,4 +86,22 @@ impl EventIntegrationTest { .await .parse::() } + + pub async fn complete_text( + &self, + text: &str, + completion_type: CompletionTypePB, + ) -> CompleteTextTaskPB { + let payload = CompleteTextPB { + text: text.to_string(), + completion_type, + stream_port: 0, + }; + EventBuilder::new(self.clone()) + .event(ChatEvent::CompleteText) + .payload(payload) + .async_send() + .await + .parse::() + } } diff --git a/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs new file mode 100644 index 0000000000000..52e2d5a11a332 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs @@ -0,0 +1,21 @@ + +use event_integration_test::user_event::user_localhost_af_cloud; +use event_integration_test::EventIntegrationTest; +use flowy_chat::entities::{CompletionTypePB}; + + +use std::time::Duration; + +#[tokio::test] +async fn af_cloud_complete_text_test() { + user_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.af_cloud_sign_up().await; + + let _workspace_id = test.get_current_workspace().await.id; + let _task = test + .complete_text("hello world", CompletionTypePB::MakeLonger) + .await; + + tokio::time::sleep(Duration::from_secs(6)).await; +} diff --git a/frontend/rust-lib/event-integration-test/tests/chat/mod.rs b/frontend/rust-lib/event-integration-test/tests/chat/mod.rs index 773bdab81fef0..21c16131e93ab 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/mod.rs @@ -1 +1,2 @@ +mod ai_tool_test; mod chat_message_test; diff --git a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs index 8ab60069a516e..7ea853f1fad3a 100644 --- a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs @@ -1,5 +1,7 @@ use bytes::Bytes; -pub use client_api::entity::ai_dto::{RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage}; +pub use client_api::entity::ai_dto::{ + CompletionType, RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage, +}; pub use client_api::entity::{ ChatAuthorType, ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage, }; @@ -11,6 +13,7 @@ use lib_infra::future::FutureResult; pub type ChatMessageStream = BoxStream<'static, Result>; pub type StreamAnswer = BoxStream<'static, Result>; +pub type StreamComplete = BoxStream<'static, Result>; #[async_trait] pub trait ChatCloudService: Send + Sync + 'static { fn create_chat( @@ -72,4 +75,11 @@ pub trait ChatCloudService: Send + Sync + 'static { chat_id: &str, question_message_id: i64, ) -> FutureResult; + + async fn stream_complete( + &self, + workspace_id: &str, + text: &str, + complete_type: CompletionType, + ) -> Result; } diff --git a/frontend/rust-lib/flowy-chat/src/chat.rs b/frontend/rust-lib/flowy-chat/src/chat.rs index cb32c342c8a0b..3581c8ef4942e 100644 --- a/frontend/rust-lib/flowy-chat/src/chat.rs +++ b/frontend/rust-lib/flowy-chat/src/chat.rs @@ -1,7 +1,7 @@ +use crate::chat_manager::ChatUserService; use crate::entities::{ ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB, }; -use crate::manager::ChatUserService; use crate::notification::{send_notification, ChatNotification}; use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable}; use allo_isolate::Isolate; diff --git a/frontend/rust-lib/flowy-chat/src/manager.rs b/frontend/rust-lib/flowy-chat/src/chat_manager.rs similarity index 98% rename from frontend/rust-lib/flowy-chat/src/manager.rs rename to frontend/rust-lib/flowy-chat/src/chat_manager.rs index b72cdfb87d145..4e22f26aa7463 100644 --- a/frontend/rust-lib/flowy-chat/src/manager.rs +++ b/frontend/rust-lib/flowy-chat/src/chat_manager.rs @@ -17,8 +17,8 @@ pub trait ChatUserService: Send + Sync + 'static { } pub struct ChatManager { - cloud_service: Arc, - user_service: Arc, + pub(crate) cloud_service: Arc, + pub(crate) user_service: Arc, chats: Arc>>, } diff --git a/frontend/rust-lib/flowy-chat/src/entities.rs b/frontend/rust-lib/flowy-chat/src/entities.rs index 4ef687c3c46f0..12bf2c3753b04 100644 --- a/frontend/rust-lib/flowy-chat/src/entities.rs +++ b/frontend/rust-lib/flowy-chat/src/entities.rs @@ -205,3 +205,32 @@ impl From for RepeatedRelatedQuestionPB { } } } + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct CompleteTextPB { + #[pb(index = 1)] + pub text: String, + + #[pb(index = 2)] + pub completion_type: CompletionTypePB, + + #[pb(index = 3)] + pub stream_port: i64, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct CompleteTextTaskPB { + #[pb(index = 1)] + pub task_id: String, +} + +#[derive(Clone, Debug, ProtoBuf_Enum, Default)] +pub enum CompletionTypePB { + UnknownCompletionType = 0, + #[default] + ImproveWriting = 1, + SpellingAndGrammar = 2, + MakeShorter = 3, + MakeLonger = 4, + ContinueWriting = 5, +} diff --git a/frontend/rust-lib/flowy-chat/src/event_handler.rs b/frontend/rust-lib/flowy-chat/src/event_handler.rs index 1d4499c6b23dc..93734aa7d0d83 100644 --- a/frontend/rust-lib/flowy-chat/src/event_handler.rs +++ b/frontend/rust-lib/flowy-chat/src/event_handler.rs @@ -2,11 +2,12 @@ use flowy_chat_pub::cloud::ChatMessageType; use std::sync::{Arc, Weak}; use validator::Validate; +use crate::tools::AITools; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use crate::chat_manager::ChatManager; use crate::entities::*; -use crate::manager::ChatManager; fn upgrade_chat_manager( chat_manager: AFPluginState>, @@ -110,3 +111,22 @@ pub(crate) async fn stop_stream_handler( chat_manager.stop_stream(&data.chat_id).await?; Ok(()) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn start_complete_text_handler( + data: AFPluginData, + tools: AFPluginState>, +) -> DataResult { + let task = tools.create_complete_task(data.into_inner()).await?; + data_result_ok(task) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn stop_complete_text_handler( + data: AFPluginData, + tools: AFPluginState>, +) -> Result<(), FlowyError> { + let data = data.into_inner(); + tools.cancel_complete_task(&data.task_id).await; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-chat/src/event_map.rs b/frontend/rust-lib/flowy-chat/src/event_map.rs index e3b7828936261..5b3fa7464402d 100644 --- a/frontend/rust-lib/flowy-chat/src/event_map.rs +++ b/frontend/rust-lib/flowy-chat/src/event_map.rs @@ -1,23 +1,30 @@ -use std::sync::Weak; +use std::sync::{Arc, Weak}; use strum_macros::Display; +use crate::tools::AITools; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::*; +use crate::chat_manager::ChatManager; use crate::event_handler::*; -use crate::manager::ChatManager; pub fn init(chat_manager: Weak) -> AFPlugin { + let user_service = Arc::downgrade(&chat_manager.upgrade().unwrap().user_service); + let cloud_service = Arc::downgrade(&chat_manager.upgrade().unwrap().cloud_service); + let ai_tools = Arc::new(AITools::new(cloud_service, user_service)); AFPlugin::new() .name("Flowy-Chat") .state(chat_manager) + .state(ai_tools) .event(ChatEvent::StreamMessage, stream_chat_message_handler) .event(ChatEvent::LoadPrevMessage, load_prev_message_handler) .event(ChatEvent::LoadNextMessage, load_next_message_handler) .event(ChatEvent::GetRelatedQuestion, get_related_question_handler) .event(ChatEvent::GetAnswerForQuestion, get_answer_handler) .event(ChatEvent::StopStream, stop_stream_handler) + .event(ChatEvent::CompleteText, start_complete_text_handler) + .event(ChatEvent::StopCompleteText, stop_complete_text_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -41,4 +48,10 @@ pub enum ChatEvent { #[event(input = "ChatMessageIdPB", output = "ChatMessagePB")] GetAnswerForQuestion = 5, + + #[event(input = "CompleteTextPB", output = "CompleteTextTaskPB")] + CompleteText = 6, + + #[event(input = "CompleteTextTaskPB")] + StopCompleteText = 7, } diff --git a/frontend/rust-lib/flowy-chat/src/lib.rs b/frontend/rust-lib/flowy-chat/src/lib.rs index 2244af580239f..12cd54845e433 100644 --- a/frontend/rust-lib/flowy-chat/src/lib.rs +++ b/frontend/rust-lib/flowy-chat/src/lib.rs @@ -2,8 +2,9 @@ mod event_handler; pub mod event_map; mod chat; +pub mod chat_manager; pub mod entities; -pub mod manager; pub mod notification; mod persistence; mod protobuf; +mod tools; diff --git a/frontend/rust-lib/flowy-chat/src/tools.rs b/frontend/rust-lib/flowy-chat/src/tools.rs new file mode 100644 index 0000000000000..b910d098e9e08 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/tools.rs @@ -0,0 +1,136 @@ +use crate::chat_manager::ChatUserService; +use crate::entities::{CompleteTextPB, CompleteTextTaskPB, CompletionTypePB}; +use allo_isolate::Isolate; + +use dashmap::DashMap; +use flowy_chat_pub::cloud::{ChatCloudService, CompletionType}; +use flowy_error::{FlowyError, FlowyResult}; + +use futures::{SinkExt, StreamExt}; +use lib_infra::isolate_stream::IsolateSink; + +use std::sync::{Arc, Weak}; +use tokio::select; +use tracing::{error, trace}; + +pub struct AITools { + tasks: Arc>>, + cloud_service: Weak, + user_service: Weak, +} + +impl AITools { + pub fn new( + cloud_service: Weak, + user_service: Weak, + ) -> Self { + Self { + tasks: Arc::new(DashMap::new()), + cloud_service, + user_service, + } + } + + pub async fn create_complete_task( + &self, + complete: CompleteTextPB, + ) -> FlowyResult { + let workspace_id = self + .user_service + .upgrade() + .ok_or_else(FlowyError::internal)? + .workspace_id()?; + let (tx, rx) = tokio::sync::mpsc::channel(1); + let task = ToolTask::new(workspace_id, complete, self.cloud_service.clone(), rx); + let task_id = task.task_id.clone(); + self.tasks.insert(task_id.clone(), tx); + + task.start().await; + Ok(CompleteTextTaskPB { task_id }) + } + + pub async fn cancel_complete_task(&self, task_id: &str) { + if let Some(entry) = self.tasks.remove(task_id) { + let _ = entry.1.send(()).await; + } + } +} + +pub struct ToolTask { + workspace_id: String, + task_id: String, + stop_rx: tokio::sync::mpsc::Receiver<()>, + context: CompleteTextPB, + cloud_service: Weak, +} + +impl ToolTask { + pub fn new( + workspace_id: String, + context: CompleteTextPB, + cloud_service: Weak, + stop_rx: tokio::sync::mpsc::Receiver<()>, + ) -> Self { + Self { + workspace_id, + task_id: uuid::Uuid::new_v4().to_string(), + context, + cloud_service, + stop_rx, + } + } + + pub async fn start(mut self) { + tokio::spawn(async move { + let mut sink = IsolateSink::new(Isolate::new(self.context.stream_port)); + match self.cloud_service.upgrade() { + None => {}, + Some(cloud_service) => { + let complete_type = match self.context.completion_type { + CompletionTypePB::UnknownCompletionType => CompletionType::ImproveWriting, + CompletionTypePB::ImproveWriting => CompletionType::ImproveWriting, + CompletionTypePB::SpellingAndGrammar => CompletionType::SpellingAndGrammar, + CompletionTypePB::MakeShorter => CompletionType::MakeShorter, + CompletionTypePB::MakeLonger => CompletionType::MakeLonger, + CompletionTypePB::ContinueWriting => CompletionType::ContinueWriting, + }; + let _ = sink.send("start:".to_string()).await; + match cloud_service + .stream_complete(&self.workspace_id, &self.context.text, complete_type) + .await + { + Ok(mut stream) => loop { + select! { + _ = self.stop_rx.recv() => { + return; + }, + result = stream.next() => { + match result { + Some(Ok(data)) => { + let s = String::from_utf8(data.to_vec()).unwrap_or_default(); + trace!("stream completion data: {}", s); + let _ = sink.send(format!("data:{}", s)).await; + }, + Some(Err(error)) => { + error!("stream error: {}", error); + let _ = sink.send(format!("error:{}", error)).await; + return; + }, + None => { + let _ = sink.send(format!("finish:{}", self.task_id)).await; + return; + }, + } + } + } + }, + Err(error) => { + error!("stream complete error: {}", error); + let _ = sink.send(format!("error:{}", error)).await; + }, + } + }, + } + }); + } +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index 9ba1604182549..e195be7a6f2e2 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -1,4 +1,4 @@ -use flowy_chat::manager::{ChatManager, ChatUserService}; +use flowy_chat::chat_manager::{ChatManager, ChatUserService}; use flowy_chat_pub::cloud::ChatCloudService; use flowy_error::FlowyError; use flowy_sqlite::DBConnection; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index e3a86d4fc7f7d..9eee9d7875a2a 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -1,7 +1,7 @@ use bytes::Bytes; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use flowy_chat::manager::ChatManager; +use flowy_chat::chat_manager::ChatManager; use flowy_database2::entities::DatabaseLayoutPB; use flowy_database2::services::share::csv::CSVFormat; use flowy_database2::template::{make_default_board, make_default_calendar, make_default_grid}; diff --git a/frontend/rust-lib/flowy-core/src/integrate/log.rs b/frontend/rust-lib/flowy-core/src/integrate/log.rs index ddf8d32277f2c..87f1634e919bd 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/log.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/log.rs @@ -54,6 +54,7 @@ pub fn create_log_filter(level: String, with_crates: Vec, platform: Plat filters.push(format!("flowy_search={}", level)); filters.push(format!("flowy_chat={}", level)); filters.push(format!("flowy_storage={}", level)); + filters.push(format!("flowy_ai={}", level)); // Enable the frontend logs. DO NOT DISABLE. // These logs are essential for debugging and verifying frontend behavior. filters.push(format!("dart_ffi={}", level)); diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 8228531d38ccb..71c5c0891dfe7 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::Error; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; -use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::ai_dto::{CompletionType, RepeatedRelatedQuestion}; use client_api::entity::ChatMessageType; use collab::core::origin::{CollabClient, CollabOrigin}; @@ -12,14 +12,14 @@ use collab::preclude::CollabPlugin; use collab_entity::CollabType; use collab_plugins::cloud_storage::postgres::SupabaseDBPlugin; use tokio_stream::wrappers::WatchStream; -use tracing::debug; +use tracing::{debug, info}; use collab_integrate::collab_builder::{ CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, }; use flowy_chat_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageStream, MessageCursor, RepeatedChatMessage, - StreamAnswer, + StreamAnswer, StreamComplete, }; use flowy_database_pub::cloud::{ CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, @@ -148,6 +148,13 @@ impl UserCloudServiceProvider for ServerProvider { Ok(()) } + fn set_ai_model(&self, ai_model: &str) -> Result<(), FlowyError> { + info!("Set AI model: {}", ai_model); + let server = self.get_server()?; + server.set_ai_model(ai_model)?; + Ok(()) + } + fn subscribe_token_state(&self) -> Option> { let server = self.get_server().ok()?; server.subscribe_token_state() @@ -667,6 +674,21 @@ impl ChatCloudService for ServerProvider { .await }) } + + async fn stream_complete( + &self, + workspace_id: &str, + text: &str, + complete_type: CompletionType, + ) -> Result { + let workspace_id = workspace_id.to_string(); + let text = text.to_string(); + let server = self.get_server()?; + server + .chat_service() + .stream_complete(&workspace_id, &text, complete_type) + .await + } } #[async_trait] diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 11bba9de8c574..4cf5f4275155b 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -9,7 +9,7 @@ use tokio::sync::RwLock; use tracing::{debug, error, event, info, instrument}; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType}; -use flowy_chat::manager::ChatManager; +use flowy_chat::chat_manager::ChatManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; use flowy_error::{FlowyError, FlowyResult}; diff --git a/frontend/rust-lib/flowy-core/src/module.rs b/frontend/rust-lib/flowy-core/src/module.rs index 7077007915f9c..70e55bc559b9b 100644 --- a/frontend/rust-lib/flowy-core/src/module.rs +++ b/frontend/rust-lib/flowy-core/src/module.rs @@ -1,4 +1,4 @@ -use flowy_chat::manager::ChatManager; +use flowy_chat::chat_manager::ChatManager; use std::sync::Weak; use flowy_database2::DatabaseManager; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 09469f5b3513b..1fff915ad50c1 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -1,11 +1,11 @@ use crate::af_cloud::AFServer; -use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::ai_dto::{CompleteTextParams, CompletionType, RepeatedRelatedQuestion}; use client_api::entity::{ CreateAnswerMessageParams, CreateChatMessageParams, CreateChatParams, MessageCursor, RepeatedChatMessage, }; use flowy_chat_pub::cloud::{ - ChatCloudService, ChatMessage, ChatMessageStream, ChatMessageType, StreamAnswer, + ChatCloudService, ChatMessage, ChatMessageStream, ChatMessageType, StreamAnswer, StreamComplete, }; use flowy_error::FlowyError; use futures_util::StreamExt; @@ -187,4 +187,23 @@ where Ok(resp) }) } + + async fn stream_complete( + &self, + workspace_id: &str, + text: &str, + completion_type: CompletionType, + ) -> Result { + let params = CompleteTextParams { + text: text.to_string(), + completion_type, + }; + let stream = self + .inner + .try_get_client()? + .stream_completion_text(workspace_id, params) + .await + .map_err(FlowyError::from)?; + Ok(stream.boxed()) + } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index cbb006ce8c85b..75922232ec670 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -9,8 +9,8 @@ use client_api::entity::workspace_dto::{ CreateWorkspaceParam, PatchWorkspaceParam, WorkspaceMemberChangeset, WorkspaceMemberInvitation, }; use client_api::entity::{ - AFRole, AFWorkspace, AFWorkspaceInvitation, AuthProvider, CollabParams, CreateCollabParams, - QueryWorkspaceMember, + AFRole, AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, + AuthProvider, CollabParams, CreateCollabParams, QueryWorkspaceMember, }; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; @@ -570,6 +570,35 @@ where Ok(url) }) } + + fn get_workspace_setting( + &self, + workspace_id: &str, + ) -> FutureResult { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.server.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let settings = client.get_workspace_settings(&workspace_id).await?; + Ok(settings) + }) + } + + fn update_workspace_setting( + &self, + workspace_id: &str, + workspace_settings: AFWorkspaceSettingsChange, + ) -> FutureResult { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.server.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let settings = client + .update_workspace_settings(&workspace_id, &workspace_settings) + .await?; + Ok(settings) + }) + } } async fn get_admin_client(client: &Arc) -> FlowyResult { diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index a33852dd53c1e..e2fc6944fc17f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -65,6 +65,7 @@ pub fn user_profile_from_af_profile( encryption_type, uid: profile.uid, updated_at: profile.updated_at, + ai_model: "".to_string(), }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index c679883b69bbb..d200f752a71d9 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -1,9 +1,11 @@ +use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use anyhow::Error; use client_api::collab_sync::ServerCollabMessage; +use client_api::entity::ai_dto::AIModel; use client_api::entity::UserMessage; use client_api::notify::{TokenState, TokenStateReceiver}; use client_api::ws::{ @@ -126,6 +128,11 @@ impl AppFlowyServer for AppFlowyCloudServer { .map_err(|err| Error::new(FlowyError::unauthorized().with_context(err))) } + fn set_ai_model(&self, ai_model: &str) -> Result<(), Error> { + self.client.set_ai_model(AIModel::from_str(ai_model)?); + Ok(()) + } + fn subscribe_token_state(&self) -> Option> { let mut token_state_rx = self.client.subscribe_token_state(); let (watch_tx, watch_rx) = watch::channel(UserTokenState::Init); diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs index 90d9bf15a7a5e..8a015aeae7b16 100644 --- a/frontend/rust-lib/flowy-server/src/default_impl.rs +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -1,6 +1,8 @@ -use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::ai_dto::{CompletionType, RepeatedRelatedQuestion}; use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage}; -use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageStream, StreamAnswer}; +use flowy_chat_pub::cloud::{ + ChatCloudService, ChatMessage, ChatMessageStream, StreamAnswer, StreamComplete, +}; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; use lib_infra::future::FutureResult; @@ -96,4 +98,13 @@ impl ChatCloudService for DefaultChatCloudServiceImpl { Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) }) } + + async fn stream_complete( + &self, + _workspace_id: &str, + _text: &str, + _complete_type: CompletionType, + ) -> Result { + Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) + } } diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index d166e0632f80d..8ad002910d927 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -46,6 +46,10 @@ pub trait AppFlowyServer: Send + Sync + 'static { Ok(()) } + fn set_ai_model(&self, _ai_model: &str) -> Result<(), Error> { + Ok(()) + } + fn subscribe_token_state(&self) -> Option> { None } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index a691ab36a90b8..b537a5689a79b 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -242,6 +242,7 @@ where authenticator: Authenticator::Supabase, encryption_type: EncryptionType::from_sign(&response.encryption_sign), updated_at: response.updated_at.timestamp(), + ai_model: "".to_string(), }), } }) diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/down.sql new file mode 100644 index 0000000000000..d9a93fe9a1a10 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/up.sql new file mode 100644 index 0000000000000..7143a9035567f --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE user_table ADD COLUMN ai_model TEXT NOT NULL DEFAULT ''; diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index f23eb029ea544..c45c329c1d402 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -75,6 +75,7 @@ diesel::table! { encryption_type -> Text, stability_ai_key -> Text, updated_at -> BigInt, + ai_model -> Text, } } diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index f70e53c248a2f..9fd1d6b8add3b 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -21,3 +21,4 @@ tokio-stream = "0.1.14" flowy-folder-pub.workspace = true tracing.workspace = true base64 = "0.21" +client-api = { workspace = true } diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 0508e5c40eec2..44f215906b6d6 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -1,3 +1,4 @@ +pub use client_api::entity::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use collab_entity::{CollabObject, CollabType}; use flowy_error::{internal_error, ErrorCode, FlowyError}; use lib_infra::box_any::BoxAny; @@ -61,6 +62,7 @@ pub trait UserCloudServiceProvider: Send + Sync { /// # Returns /// A `Result` which is `Ok` if the token is successfully set, or a `FlowyError` otherwise. fn set_token(&self, token: &str) -> Result<(), FlowyError>; + fn set_ai_model(&self, ai_model: &str) -> Result<(), FlowyError>; /// Subscribes to the state of the authentication token. /// @@ -294,6 +296,21 @@ pub trait UserCloudService: Send + Sync + 'static { fn get_billing_portal_url(&self) -> FutureResult { FutureResult::new(async { Err(FlowyError::not_support()) }) } + + fn get_workspace_setting( + &self, + workspace_id: &str, + ) -> FutureResult { + FutureResult::new(async { Err(FlowyError::not_support()) }) + } + + fn update_workspace_setting( + &self, + workspace_id: &str, + workspace_settings: AFWorkspaceSettingsChange, + ) -> FutureResult { + FutureResult::new(async { Err(FlowyError::not_support()) }) + } } pub type UserUpdateReceiver = tokio::sync::mpsc::Receiver; diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index 566aabe1c091f..642004a8f9769 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -171,6 +171,7 @@ pub struct UserProfile { // If the encryption_sign is not empty, which means the user has enabled the encryption. pub encryption_type: EncryptionType, pub updated_at: i64, + pub ai_model: String, } #[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] @@ -249,6 +250,7 @@ where encryption_type: value.encryption_type(), stability_ai_key, updated_at: value.updated_at(), + ai_model: "".to_string(), } } } @@ -264,6 +266,7 @@ pub struct UpdateUserProfileParams { pub stability_ai_key: Option, pub encryption_sign: Option, pub token: Option, + pub ai_model: Option, } impl UpdateUserProfileParams { @@ -318,6 +321,11 @@ impl UpdateUserProfileParams { self } + pub fn with_ai_model(mut self, ai_model: &str) -> Self { + self.ai_model = Some(ai_model.to_owned()); + self + } + pub fn is_empty(&self) -> bool { self.name.is_none() && self.email.is_none() diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 80dcfd1b7f08c..a637cef6d27d6 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,11 +1,12 @@ use std::convert::TryInto; +use std::str::FromStr; use validator::Validate; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; -use crate::entities::AuthenticatorPB; +use crate::entities::{AIModelPB, AuthenticatorPB}; use crate::errors::ErrorCode; use super::parser::UserStabilityAIKey; @@ -56,6 +57,9 @@ pub struct UserProfilePB { #[pb(index = 11)] pub stability_ai_key: String, + + #[pb(index = 12)] + pub ai_model: AIModelPB, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] @@ -88,6 +92,7 @@ impl From for UserProfilePB { encryption_type: encryption_ty, workspace_id: user_profile.workspace_id, stability_ai_key: user_profile.stability_ai_key, + ai_model: AIModelPB::from_str(&user_profile.ai_model).unwrap_or_default(), } } } @@ -199,6 +204,7 @@ impl TryInto for UpdateUserProfilePayloadPB { encryption_sign: None, token: None, stability_ai_key, + ai_model: None, }) } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 96660bdea26d2..392fced085c2b 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -1,6 +1,8 @@ +use std::str::FromStr; use validator::Validate; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_user_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use flowy_user_pub::entities::{ RecurringInterval, Role, SubscriptionPlan, WorkspaceInvitation, WorkspaceMember, WorkspaceSubscription, @@ -344,3 +346,86 @@ pub struct BillingPortalPB { #[pb(index = 1)] pub url: String, } + +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct UseAISettingPB { + #[pb(index = 1)] + pub disable_search_indexing: bool, + + #[pb(index = 2)] + pub ai_model: AIModelPB, +} + +impl From for UseAISettingPB { + fn from(value: AFWorkspaceSettings) -> Self { + Self { + disable_search_indexing: value.disable_search_indexing, + ai_model: AIModelPB::from_str(&value.ai_model).unwrap_or_default(), + } + } +} + +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct UpdateUserWorkspaceSettingPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub workspace_id: String, + + #[pb(index = 2, one_of)] + pub disable_search_indexing: Option, + + #[pb(index = 3, one_of)] + pub ai_model: Option, +} + +impl From for AFWorkspaceSettingsChange { + fn from(value: UpdateUserWorkspaceSettingPB) -> Self { + let mut change = AFWorkspaceSettingsChange::new(); + if let Some(disable_search_indexing) = value.disable_search_indexing { + change = change.disable_search_indexing(disable_search_indexing); + } + if let Some(ai_model) = value.ai_model { + change = change.ai_model(ai_model.to_str().to_string()); + } + change + } +} + +#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default)] +pub enum AIModelPB { + #[default] + DefaultModel = 0, + GPT35 = 1, + GPT4o = 2, + Claude3Sonnet = 3, + Claude3Opus = 4, + LocalAIModel = 5, +} + +impl AIModelPB { + pub fn to_str(&self) -> &str { + match self { + AIModelPB::DefaultModel => "default-model", + AIModelPB::GPT35 => "gpt-3.5-turbo", + AIModelPB::GPT4o => "gpt-4o", + AIModelPB::Claude3Sonnet => "claude-3-sonnet", + AIModelPB::Claude3Opus => "claude-3-opus", + AIModelPB::LocalAIModel => "local", + } + } +} + +impl FromStr for AIModelPB { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "gpt-3.5-turbo" => Ok(AIModelPB::GPT35), + "gpt-4o" => Ok(AIModelPB::GPT4o), + "claude-3-sonnet" => Ok(AIModelPB::Claude3Sonnet), + "claude-3-opus" => Ok(AIModelPB::Claude3Opus), + "local" => Ok(AIModelPB::LocalAIModel), + _ => Ok(AIModelPB::DefaultModel), + } + } +} diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 831289d1b9228..337e1aa565b03 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -832,3 +832,25 @@ pub async fn get_workspace_member_info( let member = manager.get_workspace_member_info(param.uid).await?; data_result_ok(member.into()) } + +#[tracing::instrument(level = "info", skip_all, err)] +pub async fn update_workspace_setting( + params: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params = params.try_into_inner()?; + let manager = upgrade_manager(manager)?; + manager.update_workspace_setting(params).await?; + Ok(()) +} + +#[tracing::instrument(level = "info", skip_all, err)] +pub async fn get_workspace_setting( + params: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params = params.try_into_inner()?; + let manager = upgrade_manager(manager)?; + let pb = manager.get_workspace_settings(¶ms.workspace_id).await?; + data_result_ok(pb) +} diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 387f1deaa26ba..bb9a08c823f18 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -74,6 +74,9 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::CancelWorkspaceSubscription, cancel_workspace_subscription_handler) .event(UserEvent::GetWorkspaceUsage, get_workspace_usage_handler) .event(UserEvent::GetBillingPortal, get_billing_portal_handler) + // Workspace Setting + .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting) + .event(UserEvent::GetWorkspaceSetting, get_workspace_setting) } @@ -253,6 +256,12 @@ pub enum UserEvent { #[event(input = "WorkspaceMemberIdPB", output = "WorkspaceMemberPB")] GetMemberInfo = 56, + + #[event(input = "UpdateUserWorkspaceSettingPB")] + UpdateWorkspaceSetting = 57, + + #[event(input = "UserWorkspaceIdPB", output = "UseAISettingPB")] + GetWorkspaceSetting = 58, } pub trait UserStatusCallback: Send + Sync + 'static { diff --git a/frontend/rust-lib/flowy-user/src/notification.rs b/frontend/rust-lib/flowy-user/src/notification.rs index a77e0fc8aa361..a8bd91b55b210 100644 --- a/frontend/rust-lib/flowy-user/src/notification.rs +++ b/frontend/rust-lib/flowy-user/src/notification.rs @@ -14,6 +14,7 @@ pub(crate) enum UserNotification { DidUpdateUserWorkspaces = 3, DidUpdateCloudConfig = 4, DidUpdateUserWorkspace = 5, + DidUpdateAISetting = 6, } impl std::convert::From for i32 { diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs index 71d3fd50b16e8..6f13e0f40f007 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -24,6 +24,7 @@ pub struct UserTable { pub(crate) encryption_type: String, pub(crate) stability_ai_key: String, pub(crate) updated_at: i64, + pub(crate) ai_model: String, } impl UserTable { @@ -49,6 +50,7 @@ impl From<(UserProfile, Authenticator)> for UserTable { encryption_type, stability_ai_key: user_profile.stability_ai_key, updated_at: user_profile.updated_at, + ai_model: user_profile.ai_model, } } } @@ -67,6 +69,7 @@ impl From for UserProfile { encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(), stability_ai_key: table.stability_ai_key, updated_at: table.updated_at, + ai_model: table.ai_model, } } } @@ -83,6 +86,7 @@ pub struct UserTableChangeset { pub encryption_type: Option, pub token: Option, pub stability_ai_key: Option, + pub ai_model: Option, } impl UserTableChangeset { @@ -101,6 +105,7 @@ impl UserTableChangeset { encryption_type, token: params.token, stability_ai_key: params.stability_ai_key, + ai_model: params.ai_model, } } @@ -116,6 +121,7 @@ impl UserTableChangeset { encryption_type: Some(encryption_type), token: Some(user_profile.token), stability_ai_key: Some(user_profile.stability_ai_key), + ai_model: Some(user_profile.ai_model), } } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index 9a8a95181cf0d..0f5fd66cde982 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -169,6 +169,10 @@ impl UserManager { error!("Set token failed: {}", err); } + if let Err(err) = self.cloud_services.set_ai_model(&user.ai_model) { + error!("Set ai model failed: {}", err); + } + // Subscribe the token state let weak_cloud_services = Arc::downgrade(&self.cloud_services); let weak_authenticate_user = Arc::downgrade(&self.authenticate_user); @@ -804,7 +808,7 @@ fn current_authenticator() -> Authenticator { } } -fn upsert_user_profile_change( +pub fn upsert_user_profile_change( uid: i64, mut conn: DBConnection, changeset: UserTableChangeset, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 0c22d28d434f5..0448d326d1257 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -11,13 +11,14 @@ use flowy_folder_pub::entities::{AppFlowyData, ImportData}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; use flowy_user_pub::entities::{ - Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, - WorkspaceSubscription, WorkspaceUsage, + Role, UpdateUserProfileParams, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, + WorkspaceMember, WorkspaceSubscription, WorkspaceUsage, }; use lib_dispatch::prelude::af_spawn; use crate::entities::{ - RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, UserWorkspacePB, + RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, UpdateUserWorkspaceSettingPB, + UseAISettingPB, UserWorkspacePB, }; use crate::migrations::AnonUser; use crate::notification::{send_notification, UserNotification}; @@ -27,10 +28,11 @@ use crate::services::data_import::{ use crate::services::sqlite_sql::member_sql::{ select_workspace_member, upsert_workspace_member, WorkspaceMemberTable, }; +use crate::services::sqlite_sql::user_sql::UserTableChangeset; use crate::services::sqlite_sql::workspace_sql::{ get_all_user_workspace_op, get_user_workspace_op, insert_new_workspaces_op, UserWorkspaceTable, }; -use crate::user_manager::UserManager; +use crate::user_manager::{upsert_user_profile_change, UserManager}; use flowy_user_pub::session::Session; impl UserManager { @@ -483,6 +485,49 @@ impl UserManager { Ok(url) } + pub async fn update_workspace_setting( + &self, + updated_settings: UpdateUserWorkspaceSettingPB, + ) -> FlowyResult<()> { + let ai_model = updated_settings + .ai_model + .as_ref() + .map(|model| model.to_str().to_string()); + let workspace_id = updated_settings.workspace_id.clone(); + let cloud_service = self.cloud_services.get_user_service()?; + let settings = cloud_service + .update_workspace_setting(&workspace_id, updated_settings.into()) + .await?; + + let pb = UseAISettingPB::from(settings); + let uid = self.user_id()?; + send_notification(&uid.to_string(), UserNotification::DidUpdateAISetting) + .payload(pb) + .send(); + + if let Some(ai_model) = ai_model { + if let Err(err) = self.cloud_services.set_ai_model(&ai_model) { + error!("Set ai model failed: {}", err); + } + + let conn = self.db_connection(uid)?; + let params = UpdateUserProfileParams::new(uid).with_ai_model(&ai_model); + upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; + } + Ok(()) + } + + pub async fn get_workspace_settings(&self, workspace_id: &str) -> FlowyResult { + let cloud_service = self.cloud_services.get_user_service()?; + let settings = cloud_service.get_workspace_setting(workspace_id).await?; + + let uid = self.user_id()?; + let conn = self.db_connection(uid)?; + let params = UpdateUserProfileParams::new(uid).with_ai_model(&settings.ai_model); + upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; + Ok(UseAISettingPB::from(settings)) + } + #[instrument(level = "debug", skip(self), err)] pub async fn get_workspace_member_info(&self, uid: i64) -> FlowyResult { let workspace_id = self.get_session()?.user_workspace.id.clone();