feat: support switch model (#5575)

* feat: ai settings page

* chore: intergate client api

* chore: replace open ai calls

* chore: disable gen image from ai

* chore: clippy

* chore: remove learn about ai

* chore: fix wanrings

* chore: fix restart button title

* chore: remove await

* chore: remove loading indicator

---------

Co-authored-by: nathan <nathan@appflowy.io>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Mathias Mogensen 2024-06-25 01:59:38 +02:00 committed by GitHub
parent 40312f4260
commit 54c9d12171
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 1383 additions and 499 deletions

View File

@ -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);

View File

@ -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<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
}
Future<void> mockOpenAIRepository() async {
await getIt.unregister<OpenAIRepository>();
getIt.registerFactoryAsync<OpenAIRepository>(
await getIt.unregister<AIRepository>();
getIt.registerFactoryAsync<AIRepository>(
() => Future.value(
MockOpenAIRepository(),
),

View File

@ -44,7 +44,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(OpenAIError error) onError,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,

View File

@ -90,7 +90,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
UploadImageType.local,
UploadImageType.url,
UploadImageType.unsplash,
UploadImageType.openAI,
// UploadImageType.openAI,
UploadImageType.stabilityAI,
],
onSelectedLocalImage: (path) {

View File

@ -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<OpenAIImageWidget> {
Future<FlowyResult<List<String>, OpenAIError>>? future;
Future<FlowyResult<List<String>, AIError>>? future;
String query = '';
@override
@ -93,7 +93,7 @@ class _OpenAIImageWidgetState extends State<OpenAIImageWidget> {
}
void _search() async {
final openAI = await getIt.getAsync<OpenAIRepository>();
final openAI = await getIt.getAsync<AIRepository>();
setState(() {
future = openAI.generateImage(
prompt: query,

View File

@ -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<UploadImageMenu> {
),
),
);
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(

View File

@ -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<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
});
Future<void> streamCompletion({
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
});
Future<FlowyResult<List<String>, AIError>> generateImage({
required String prompt,
int n = 1,
});
}

View File

@ -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<String, Object?> json) =>
_$OpenAIErrorFromJson(json);
factory AIError.fromJson(Map<String, Object?> json) =>
_$AIErrorFromJson(json);
}

View File

@ -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<FlowyResult<TextCompletionResponse, OpenAIError>> getCompletions({
required String prompt,
String? suffix,
int maxTokens = 2048,
double temperature = .3,
});
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> 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<FlowyResult<TextEditResponse, OpenAIError>> 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<FlowyResult<List<String>, 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<FlowyResult<TextCompletionResponse, OpenAIError>> 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<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> 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<FlowyResult<TextEditResponse, OpenAIError>> 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<FlowyResult<List<String>, OpenAIError>> generateImage({
Future<FlowyResult<List<String>, 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<void> streamCompletion({
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
}) {
throw UnimplementedError();
}
}

View File

@ -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<void> _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,
);
}
}

View File

@ -28,7 +28,7 @@ class Loading {
),
);
Future<void> stop() async {
void stop() {
if (loadingContext != null) {
Navigator.of(loadingContext!).pop();
loadingContext = null;
@ -54,5 +54,5 @@ class BarrierDialog {
),
);
Future<void> dismiss() async => Navigator.of(loadingContext).pop();
void dismiss() => Navigator.of(loadingContext).pop();
}

View File

@ -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<SmartEditInputWidget> {
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<SmartEditInputWidget> {
);
}
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<SmartEditInputWidget> {
result = "";
});
}
final openAIRepository = await getIt.getAsync<OpenAIRepository>();
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<AIRepository>();
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();
},
);
}
}

View File

@ -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<SmartEditActionList> {
bool isOpenAIEnabled = false;
bool isAIEnabled = false;
@override
void initState() {
@ -40,8 +41,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
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<SmartEditActionList> {
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<SmartEditActionList> {
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,
);
}

View File

@ -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<OpenAIRepository>(
getIt.registerFactoryAsync<AIRepository>(
() 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}');

View File

@ -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<FlowyResult<List<String>, AIError>> generateImage({
required String prompt,
int n = 1,
}) {
throw UnimplementedError();
}
@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) {
throw UnimplementedError();
}
@override
Future<CompletionStream> streamCompletion({
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> 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<void> Function() onStart,
Future<void> Function(String text) onProcess,
Future<void> 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<String> _controller = StreamController.broadcast();
late StreamSubscription<String> _subscription;
int get nativePort => _port.sendPort.nativePort;
Future<void> dispose() async {
await _controller.close();
await _subscription.cancel();
_port.close();
}
StreamSubscription<String> listen(
void Function(String event)? onData,
) {
return _controller.stream.listen(onData);
}
}

View File

@ -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<UserProfilePB, FlowyError>;
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<WorkspaceSettingPB, FlowyError>;
class UserWorkspaceListener {
UserWorkspaceListener();
class FolderListener {
FolderListener();
final PublishNotifier<WorkspaceSettingNotifyValue> _settingChangedNotifier =
PublishNotifier();

View File

@ -9,12 +9,12 @@ part 'home_bloc.freezed.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc(WorkspaceSettingPB workspaceSetting)
: _workspaceListener = UserWorkspaceListener(),
: _workspaceListener = FolderListener(),
super(HomeState.initial(workspaceSetting)) {
_dispatch(workspaceSetting);
}
final UserWorkspaceListener _workspaceListener;
final FolderListener _workspaceListener;
@override
Future<void> close() async {

View File

@ -15,7 +15,7 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
WorkspaceSettingPB workspaceSetting,
AppearanceSettingsCubit appearanceSettingsCubit,
double screenWidthPx,
) : _listener = UserWorkspaceListener(),
) : _listener = FolderListener(),
_appearanceSettingsCubit = appearanceSettingsCubit,
super(
HomeSettingState.initial(
@ -27,7 +27,7 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
_dispatch();
}
final UserWorkspaceListener _listener;
final FolderListener _listener;
final AppearanceSettingsCubit _appearanceSettingsCubit;
@override

View File

@ -13,7 +13,7 @@ part 'menu_user_bloc.freezed.dart';
class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
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<MenuUserEvent, MenuUserState> {
final UserBackendService _userService;
final UserListener _userListener;
final UserWorkspaceListener _userWorkspaceListener;
final FolderListener _userWorkspaceListener;
final UserProfilePB userProfile;
@override

View File

@ -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<SettingsAIEvent, SettingsAIState> {
SettingsAIBloc(this.userProfile)
: _userListener = UserListener(userProfile: userProfile),
super(SettingsAIState(userProfile: userProfile)) {
_dispatch();
}
final UserListener _userListener;
final UserProfilePB userProfile;
@override
Future<void> close() async {
await _userListener.stop();
return super.close();
}
void _dispatch() {
on<SettingsAIEvent>((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<UserProfilePB, FlowyError> 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;
}

View File

@ -13,12 +13,13 @@ enum SettingsPage {
account,
workspace,
manageData,
shortcuts,
ai,
plan,
billing,
// OLD
notifications,
cloud,
shortcuts,
member,
featureFlags,
}

View File

@ -63,24 +63,6 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
);
});
},
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;

View File

@ -29,9 +29,9 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
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;

View File

@ -136,39 +136,7 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
// ),
// ],
// ),
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<SettingsUserViewBloc>()
.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<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserStabilityAIKey(key)),
),
],
),
SettingsCategory(
title: LocaleKeys.settings_accountPage_login_title.tr(),
children: [

View File

@ -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<SettingsAIBloc>(
create: (context) =>
SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
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<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
return Expanded(
child: SettingsDropdown<AIModelPB>(
key: const Key('AIModelDropdown'),
expandWidth: false,
onChanged: (format) {
context.read<SettingsAIBloc>().add(
SettingsAIEvent.selectModel(format),
);
},
selectedOption: state.userProfile.aiModel,
options: _availableModels
.map(
(format) => buildDropdownMenuEntry<AIModelPB>(
context,
value: format,
label: _titleForAIModel(format),
),
)
.toList(),
),
);
},
),
],
);
}
}
List<AIModelPB> _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<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
if (state.aiSettings == null) {
return const CircularProgressIndicator.adaptive();
} else {
return Toggle(
value: state.enableSearchIndexing,
onChanged: (_) {
context.read<SettingsAIBloc>().add(
const SettingsAIEvent.toggleAISearch(),
);
},
);
}
},
),
],
);
}
}

View File

@ -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:

View File

@ -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),

View File

@ -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 &&

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -33,6 +33,9 @@ pub struct UserProfilePB {
#[pb(index = 10)]
pub stability_ai_key: String,
#[pb(index = 11)]
pub ai_model: String,
}
impl From<UserProfile> for UserProfilePB {
@ -52,6 +55,7 @@ impl From<UserProfile> 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,
}
}
}

View File

@ -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>(
"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;

View File

@ -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<WatchStream<UserTokenState>> {
self.get_server().subscribe_token_state()
}

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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::<ChatMessageListPB>()
}
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::<CompleteTextTaskPB>()
}
}

View File

@ -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;
}

View File

@ -1 +1,2 @@
mod ai_tool_test;
mod chat_message_test;

View File

@ -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<ChatMessage, AppResponseError>>;
pub type StreamAnswer = BoxStream<'static, Result<Bytes, AppResponseError>>;
pub type StreamComplete = BoxStream<'static, Result<Bytes, AppResponseError>>;
#[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<ChatMessage, FlowyError>;
async fn stream_complete(
&self,
workspace_id: &str,
text: &str,
complete_type: CompletionType,
) -> Result<StreamComplete, FlowyError>;
}

View File

@ -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;

View File

@ -17,8 +17,8 @@ pub trait ChatUserService: Send + Sync + 'static {
}
pub struct ChatManager {
cloud_service: Arc<dyn ChatCloudService>,
user_service: Arc<dyn ChatUserService>,
pub(crate) cloud_service: Arc<dyn ChatCloudService>,
pub(crate) user_service: Arc<dyn ChatUserService>,
chats: Arc<DashMap<String, Arc<Chat>>>,
}

View File

@ -205,3 +205,32 @@ impl From<RepeatedRelatedQuestion> 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,
}

View File

@ -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<Weak<ChatManager>>,
@ -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<CompleteTextPB>,
tools: AFPluginState<Arc<AITools>>,
) -> DataResult<CompleteTextTaskPB, FlowyError> {
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<CompleteTextTaskPB>,
tools: AFPluginState<Arc<AITools>>,
) -> Result<(), FlowyError> {
let data = data.into_inner();
tools.cancel_complete_task(&data.task_id).await;
Ok(())
}

View File

@ -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<ChatManager>) -> 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,
}

View File

@ -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;

View File

@ -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<DashMap<String, tokio::sync::mpsc::Sender<()>>>,
cloud_service: Weak<dyn ChatCloudService>,
user_service: Weak<dyn ChatUserService>,
}
impl AITools {
pub fn new(
cloud_service: Weak<dyn ChatCloudService>,
user_service: Weak<dyn ChatUserService>,
) -> Self {
Self {
tasks: Arc::new(DashMap::new()),
cloud_service,
user_service,
}
}
pub async fn create_complete_task(
&self,
complete: CompleteTextPB,
) -> FlowyResult<CompleteTextTaskPB> {
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<dyn ChatCloudService>,
}
impl ToolTask {
pub fn new(
workspace_id: String,
context: CompleteTextPB,
cloud_service: Weak<dyn ChatCloudService>,
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;
},
}
},
}
});
}
}

View File

@ -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;

View File

@ -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};

View File

@ -54,6 +54,7 @@ pub fn create_log_filter(level: String, with_crates: Vec<String>, 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));

View File

@ -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<WatchStream<UserTokenState>> {
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<StreamComplete, FlowyError> {
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]

View File

@ -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};

View File

@ -1,4 +1,4 @@
use flowy_chat::manager::ChatManager;
use flowy_chat::chat_manager::ChatManager;
use std::sync::Weak;
use flowy_database2::DatabaseManager;

View File

@ -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<StreamComplete, FlowyError> {
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())
}
}

View File

@ -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<AFWorkspaceSettings, FlowyError> {
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<AFWorkspaceSettings, FlowyError> {
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<AFCloudClient>) -> FlowyResult<Client> {

View File

@ -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(),
})
}

View File

@ -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<WatchStream<UserTokenState>> {
let mut token_state_rx = self.client.subscribe_token_state();
let (watch_tx, watch_rx) = watch::channel(UserTokenState::Init);

View File

@ -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<StreamComplete, FlowyError> {
Err(FlowyError::not_support().with_context("complete text is not supported in local server."))
}
}

View File

@ -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<WatchStream<UserTokenState>> {
None
}

View File

@ -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(),
}),
}
})

View File

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE user_table ADD COLUMN ai_model TEXT NOT NULL DEFAULT '';

View File

@ -75,6 +75,7 @@ diesel::table! {
encryption_type -> Text,
stability_ai_key -> Text,
updated_at -> BigInt,
ai_model -> Text,
}
}

View File

@ -21,3 +21,4 @@ tokio-stream = "0.1.14"
flowy-folder-pub.workspace = true
tracing.workspace = true
base64 = "0.21"
client-api = { workspace = true }

View File

@ -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<String, FlowyError> {
FutureResult::new(async { Err(FlowyError::not_support()) })
}
fn get_workspace_setting(
&self,
workspace_id: &str,
) -> FutureResult<AFWorkspaceSettings, FlowyError> {
FutureResult::new(async { Err(FlowyError::not_support()) })
}
fn update_workspace_setting(
&self,
workspace_id: &str,
workspace_settings: AFWorkspaceSettingsChange,
) -> FutureResult<AFWorkspaceSettings, FlowyError> {
FutureResult::new(async { Err(FlowyError::not_support()) })
}
}
pub type UserUpdateReceiver = tokio::sync::mpsc::Receiver<UserUpdate>;

View File

@ -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<String>,
pub encryption_sign: Option<String>,
pub token: Option<String>,
pub ai_model: Option<String>,
}
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()

View File

@ -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<UserProfile> 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<UpdateUserProfileParams> for UpdateUserProfilePayloadPB {
encryption_sign: None,
token: None,
stability_ai_key,
ai_model: None,
})
}
}

View File

@ -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<AFWorkspaceSettings> 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<bool>,
#[pb(index = 3, one_of)]
pub ai_model: Option<AIModelPB>,
}
impl From<UpdateUserWorkspaceSettingPB> 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<Self, Self::Err> {
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),
}
}
}

View File

@ -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<UpdateUserWorkspaceSettingPB>,
manager: AFPluginState<Weak<UserManager>>,
) -> 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<UserWorkspaceIdPB>,
manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<UseAISettingPB, FlowyError> {
let params = params.try_into_inner()?;
let manager = upgrade_manager(manager)?;
let pb = manager.get_workspace_settings(&params.workspace_id).await?;
data_result_ok(pb)
}

View File

@ -74,6 +74,9 @@ pub fn init(user_manager: Weak<UserManager>) -> 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 {

View File

@ -14,6 +14,7 @@ pub(crate) enum UserNotification {
DidUpdateUserWorkspaces = 3,
DidUpdateCloudConfig = 4,
DidUpdateUserWorkspace = 5,
DidUpdateAISetting = 6,
}
impl std::convert::From<UserNotification> for i32 {

View File

@ -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<UserTable> 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<String>,
pub token: Option<String>,
pub stability_ai_key: Option<String>,
pub ai_model: Option<String>,
}
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),
}
}
}

View File

@ -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,

View File

@ -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<UseAISettingPB> {
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<WorkspaceMember> {
let workspace_id = self.get_session()?.user_workspace.id.clone();