Compare commits

...
7 Commits
Author SHA1 Message Date
Richard Shiue ee404369ff fix: row document images (#7322)
* fix: row document images

* fix: calendar

* chore: fallback to documentbloc
2025-02-05 16:42:18 +08:00
Lucas.Xu 58ea654ddf chore: update changelog.md 2025-02-05 16:40:49 +08:00
Lucas.Xu cc14793535 chore: bump version 0.8.3 2025-02-05 14:20:18 +08:00
Nathan.fooo 122fb89c87 chore: fix ai page user profile refresh (#7317) 2025-02-04 20:40:06 +08:00
Lucas e9201cce72 fix: editor stuck on image loading loop when uploading image in row document (#7313)
* fix: editor stuck on image loading loop when uploading image in row document

* test: editor stuck on image loading loop when uploading image in row document
2025-02-04 18:03:28 +08:00
Nathan.fooo 5e5693e249 chore: Support new error code (#7311)
* chore: fetch model list

* chore: suppor new error code
2025-02-03 20:52:31 +08:00
Nathan 5ac0eded3c chore: fetch model list 2025-02-01 23:50:58 +08:00
43 changed files with 408 additions and 154 deletions

No files matched your search

+7
View File
@@ -1,4 +1,11 @@
# Release Notes
## Version 0.8.3 - 05/02/2025
### New Features
- Support OpenAI o3-mini model
### Bug Fixes
- Fixed an issue where users were unable to upload images in row pages
- Fixed an issue where users were unable to upload files on Android devices
## Version 0.8.2 - 23/01/2025
### New Features
- Customized database view icons
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.8.2"
APPFLOWY_VERSION = "0.8.3"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -1,5 +1,6 @@
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'database/database_test_runner.dart' as database_test_runner;
import 'document/document_test_runner.dart' as document_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
@@ -28,4 +29,7 @@ Future<void> main() async {
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
// database
database_test_runner.main();
}
@@ -0,0 +1,80 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide UploadImageMenu, ResizableImage;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../../shared/constants.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// copy link to block
group('database image:', () {
testWidgets('insert image', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open the first row detail page and upload an image
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Grid,
pageName: 'database image',
);
await tester.openFirstRowDetailPage();
// insert an image block
{
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
}
// upload an image
{
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(
paths: [imagePath],
);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
// remove the temp file
file.deleteSync();
}
});
});
}
@@ -0,0 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'database_image_test.dart' as database_image_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
database_image_test.main();
}
@@ -148,6 +148,15 @@ class CompletionStream {
);
}
if (event.startsWith("AI_MAX_REQUIRED:")) {
final msg = event.substring(16);
onError(
AIError(
message: msg,
),
);
}
if (event.startsWith("start:")) {
await onStart();
}
+1
View File
@@ -247,6 +247,7 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
// In development mode, the app is configured to access the AppFlowy cloud server directly through specific ports.
// This setup bypasses the need for Nginx, meaning that the AppFlowy cloud should be running without an Nginx server
// in the development environment.
// If you modify following code, please update the corresponding documentation in the appflowy billing.
if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) {
return AppFlowyCloudConfiguration(
base_url: "$baseUrl:8000",
@@ -105,6 +105,13 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
),
);
},
onAIMaxRequired: (message) {
emit(
state.copyWith(
messageState: MessageState.onAIMaxRequired(message),
),
);
},
receiveMetadata: (metadata) {
Log.debug("AI Steps: ${metadata.progress?.step}");
emit(
@@ -146,6 +153,12 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
add(ChatAIMessageEvent.receiveMetadata(metadata));
}
},
onAIMaxRequired: (message) {
if (!isClosed) {
Log.info(message);
add(ChatAIMessageEvent.onAIMaxRequired(message));
}
},
);
}
}
@@ -159,6 +172,8 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent {
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
const factory ChatAIMessageEvent.onAIImageResponseLimit() =
_OnAIImageResponseLimit;
const factory ChatAIMessageEvent.onAIMaxRequired(String message) =
_OnAIMaxRquired;
const factory ChatAIMessageEvent.receiveMetadata(
MetadataCollection metadata,
) = _ReceiveMetadata;
@@ -193,6 +208,7 @@ class MessageState with _$MessageState {
const factory MessageState.onError(String error) = _Error;
const factory MessageState.onAIResponseLimit() = _AIResponseLimit;
const factory MessageState.onAIImageResponseLimit() = _AIImageResponseLimit;
const factory MessageState.onAIMaxRequired(String message) = _AIMaxRequired;
const factory MessageState.ready() = _Ready;
const factory MessageState.loading() = _Loading;
}
@@ -28,6 +28,14 @@ class AnswerStream {
} else if (event == "AI_IMAGE_RESPONSE_LIMIT") {
_aiImageLimitReached = true;
_onAIImageResponseLimit?.call();
} else if (event.startsWith("AI_MAX_REQUIRED:")) {
final msg = event.substring(16);
// If the callback is not registered yet, add the event to the buffer.
if (_onAIMaxRequired != null) {
_onAIMaxRequired!(msg);
} else {
_pendingAIMaxRequiredEvents.add(msg);
}
}
},
onDone: () {
@@ -56,8 +64,12 @@ class AnswerStream {
void Function(String error)? _onError;
void Function()? _onAIResponseLimit;
void Function()? _onAIImageResponseLimit;
void Function(String message)? _onAIMaxRequired;
void Function(MetadataCollection metadataCollection)? _onMetadata;
// Buffer for events that occur before listen() is called.
final List<String> _pendingAIMaxRequiredEvents = [];
int get nativePort => _port.sendPort.nativePort;
bool get hasStarted => _hasStarted;
bool get aiLimitReached => _aiLimitReached;
@@ -78,6 +90,7 @@ class AnswerStream {
void Function(String error)? onError,
void Function()? onAIResponseLimit,
void Function()? onAIImageResponseLimit,
void Function(String message)? onAIMaxRequired,
void Function(MetadataCollection metadata)? onMetadata,
}) {
_onData = onData;
@@ -87,6 +100,15 @@ class AnswerStream {
_onAIResponseLimit = onAIResponseLimit;
_onAIImageResponseLimit = onAIImageResponseLimit;
_onMetadata = onMetadata;
_onAIMaxRequired = onAIMaxRequired;
// Flush any buffered AI_MAX_REQUIRED events.
if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) {
for (final msg in _pendingAIMaxRequiredEvents) {
_onAIMaxRequired!(msg);
}
_pendingAIMaxRequiredEvents.clear();
}
_onStart?.call();
}
@@ -137,6 +137,11 @@ class ChatAIMessageWidget extends StatelessWidget {
errorMessage: LocaleKeys.sideBar_purchaseAIMax.tr(),
);
},
onAIMaxRequired: (message) {
return ChatErrorMessageWidget(
errorMessage: message,
);
},
),
),
);
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
@@ -856,10 +857,13 @@ void _openCard({
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
builder: (_) => BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
),
),
);
}
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -426,10 +427,13 @@ class HiddenGroupPopupItemList extends StatelessWidget {
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
builder: (_) => BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
),
),
);
PopoverContainer.of(context).close();
@@ -1,5 +1,6 @@
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
@@ -178,10 +179,13 @@ class _EventCardState extends State<EventCard> {
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: widget.databaseController,
rowController: rowController,
userProfile: context.read<CalendarBloc>().userProfile,
builder: (_) => BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: widget.databaseController,
rowController: rowController,
userProfile: context.read<CalendarBloc>().userProfile,
),
),
);
},
@@ -1,5 +1,6 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
@@ -13,7 +14,6 @@ import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dar
import 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -390,7 +390,7 @@ void showEventDetails({
context: context,
builder: (BuildContext overlayContext) {
return BlocProvider.value(
value: context.read<ViewBloc>(),
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
rowController: rowController,
databaseController: databaseController,
@@ -457,14 +457,18 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
),
),
),
popupBuilder: (_) => BlocProvider.value(
value: context.read<CalendarBloc>(),
child: BlocProvider.value(
value: context.read<ViewBloc>(),
child: UnscheduleEventsList(
databaseController: widget.databaseController,
unscheduleEvents: state.unscheduleEvents,
popupBuilder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<CalendarBloc>(),
),
BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
),
],
child: UnscheduleEventsList(
databaseController: widget.databaseController,
unscheduleEvents: state.unscheduleEvents,
),
),
);
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -13,7 +14,6 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dar
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@@ -196,7 +196,7 @@ class _GridPageState extends State<GridPage> {
FlowyOverlay.show(
context: context,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: context.read<GridBloc>().databaseController,
rowController: rowController,
@@ -233,7 +233,7 @@ class _GridPageState extends State<GridPage> {
FlowyOverlay.show(
context: context,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController:
context.read<GridBloc>().databaseController,
@@ -559,7 +559,7 @@ class _GridRowsState extends State<_GridRows> {
}
return BlocProvider.value(
value: context.read<ViewBloc>(),
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
rowController: RowController(
viewId: viewId,
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -26,10 +27,13 @@ class RelatedRowDetailPage extends StatelessWidget {
return state.when(
loading: () => const SizedBox.shrink(),
ready: (databaseController, rowController) {
return RowDetailPage(
databaseController: databaseController,
rowController: rowController,
allowOpenAsFullPage: false,
return BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
allowOpenAsFullPage: false,
),
);
},
);
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
@@ -145,7 +146,8 @@ class _ImageMenuState extends State<ImageMenu> {
showDialog(
context: context,
builder: (_) => InteractiveImageViewer(
userProfile: context.read<UserWorkspaceBloc>().userProfile,
userProfile: context.read<UserWorkspaceBloc?>()?.userProfile ??
context.read<DocumentBloc>().state.userProfilePB,
imageProvider: AFBlockImageProvider(
images: [
ImageBlockData(
@@ -54,7 +54,8 @@ class _ImageBrowserLayoutState extends State<ImageBrowserLayout> {
@override
void initState() {
super.initState();
_userProfile = context.read<UserWorkspaceBloc?>()?.userProfile;
_userProfile = context.read<UserWorkspaceBloc?>()?.userProfile ??
context.read<DocumentBloc>().state.userProfilePB;
}
@override
@@ -70,8 +70,11 @@ class _ResizableImageState extends State<ResizableImage> {
@override
void initState() {
super.initState();
imageWidth = widget.width;
_userProfilePB = context.read<UserWorkspaceBloc?>()?.userProfile;
_userProfilePB = context.read<UserWorkspaceBloc?>()?.userProfile ??
context.read<DocumentBloc>().state.userProfilePB;
}
@override
@@ -97,11 +100,6 @@ class _ResizableImageState extends State<ResizableImage> {
Widget child;
final src = widget.src;
if (isURL(src)) {
// load network image
if (widget.type == CustomImageType.internal && _userProfilePB == null) {
return _buildLoading(context);
}
_cacheImage = FlowyNetworkImage(
url: widget.src,
width: imageWidth - moveDistance,
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -9,6 +11,7 @@ import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'settings_ai_bloc.freezed.dart';
part 'settings_ai_bloc.g.dart';
class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
SettingsAIBloc(
@@ -19,6 +22,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
_userService = UserBackendService(userId: userProfile.id),
super(
SettingsAIState(
selectedAIModel: userProfile.aiModel,
userProfile: userProfile,
currentWorkspaceMemberRole: currentWorkspaceMemberRole,
),
@@ -65,6 +69,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
},
);
_loadUserWorkspaceSetting();
_loadModelList();
},
didReceiveUserProfile: (userProfile) {
emit(state.copyWith(userProfile: userProfile));
@@ -78,7 +83,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
!(state.aiSettings?.disableSearchIndexing ?? false),
);
},
selectModel: (AIModelPB model) {
selectModel: (String model) {
_updateUserWorkspaceSetting(model: model);
},
didLoadAISetting: (UseAISettingPB settings) {
@@ -89,6 +94,34 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
),
);
},
didLoadAvailableModels: (String models) {
final dynamic decodedJson = jsonDecode(models);
Log.info("Available models: $decodedJson");
if (decodedJson is Map<String, dynamic>) {
final models = ModelList.fromJson(decodedJson).models;
if (models.isEmpty) {
// If available models is empty, then we just show the
// Default
emit(state.copyWith(availableModels: ["Default"]));
return;
}
if (!models.contains(state.selectedAIModel)) {
// Use first model as default model if current selected model
// is not available
final selectedModel = models[0];
_updateUserWorkspaceSetting(model: selectedModel);
emit(
state.copyWith(
availableModels: models,
selectedAIModel: selectedModel,
),
);
} else {
emit(state.copyWith(availableModels: models));
}
}
},
refreshMember: (member) {
emit(state.copyWith(currentWorkspaceMemberRole: member.role));
},
@@ -98,7 +131,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
void _updateUserWorkspaceSetting({
bool? disableSearchIndexing,
AIModelPB? model,
String? model,
}) {
final payload = UpdateUserWorkspaceSettingPB(
workspaceId: workspaceId,
@@ -132,6 +165,18 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
});
});
}
void _loadModelList() {
AIEventGetAvailableModels().send().then((result) {
result.fold((config) {
if (!isClosed) {
add(SettingsAIEvent.didLoadAvailableModels(config.models));
}
}, (err) {
Log.error(err);
});
});
}
}
@freezed
@@ -145,11 +190,15 @@ class SettingsAIEvent with _$SettingsAIEvent {
const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) =
_RefreshMember;
const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel;
const factory SettingsAIEvent.selectModel(String model) = _SelectAIModel;
const factory SettingsAIEvent.didReceiveUserProfile(
UserProfilePB newUserProfile,
) = _DidReceiveUserProfile;
const factory SettingsAIEvent.didLoadAvailableModels(
String models,
) = _DidLoadAvailableModels;
}
@freezed
@@ -157,7 +206,23 @@ class SettingsAIState with _$SettingsAIState {
const factory SettingsAIState({
required UserProfilePB userProfile,
UseAISettingPB? aiSettings,
@Default("Default") String selectedAIModel,
AFRolePB? currentWorkspaceMemberRole,
@Default(["Default"]) List<String> availableModels,
@Default(true) bool enableSearchIndexing,
}) = _SettingsAIState;
}
@JsonSerializable()
class ModelList {
ModelList({
required this.models,
});
factory ModelList.fromJson(Map<String, dynamic> json) =>
_$ModelListFromJson(json);
final List<String> models;
Map<String, dynamic> toJson() => _$ModelListToJson(this);
}
@@ -4,8 +4,6 @@ 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/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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -30,18 +28,18 @@ class AIModelSelection extends StatelessWidget {
),
const Spacer(),
Flexible(
child: SettingsDropdown<AIModelPB>(
child: SettingsDropdown<String>(
key: const Key('_AIModelSelection'),
onChanged: (model) => context
.read<SettingsAIBloc>()
.add(SettingsAIEvent.selectModel(model)),
selectedOption: state.userProfile.aiModel,
options: _availableModels
selectedOption: state.selectedAIModel,
options: state.availableModels
.map(
(format) => buildDropdownMenuEntry<AIModelPB>(
(model) => buildDropdownMenuEntry<String>(
context,
value: format,
label: _titleForAIModel(format),
value: model,
label: model,
),
)
.toList(),
@@ -54,29 +52,3 @@ class AIModelSelection extends StatelessWidget {
);
}
}
List<AIModelPB> _availableModels = [
AIModelPB.DefaultModel,
AIModelPB.Claude3Opus,
AIModelPB.Claude3Sonnet,
AIModelPB.GPT4oMini,
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.GPT4oMini:
return "GPT-4o-mini";
case AIModelPB.GPT4o:
return "GPT-4o";
default:
Log.error("Unknown AI model: $model, fallback to default");
return "Default";
}
}
@@ -141,6 +141,7 @@ class SettingsDialog extends StatelessWidget {
case SettingsPage.ai:
if (user.authenticator == AuthenticatorPB.AppFlowyCloud) {
return SettingsAIView(
key: ValueKey(user.hashCode),
userProfile: user,
currentWorkspaceMemberRole: currentWorkspaceMemberRole,
workspaceId: workspaceId,
+1 -1
View File
@@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an
your data. The best open source alternative to Notion.
publish_to: "none"
version: 0.8.2
version: 0.8.3
environment:
flutter: ">=3.22.0"
+12 -12
View File
@@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
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=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"anyhow",
"bytes",
@@ -786,7 +786,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"again",
"anyhow",
@@ -843,7 +843,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"collab-entity",
"collab-rt-entity",
@@ -856,7 +856,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"futures-channel",
"futures-util",
@@ -1128,7 +1128,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"anyhow",
"bincode",
@@ -1153,7 +1153,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"anyhow",
"async-trait",
@@ -1548,7 +1548,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"anyhow",
"app-error",
@@ -2970,7 +2970,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"anyhow",
"futures-util",
@@ -2987,7 +2987,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"anyhow",
"app-error",
@@ -3598,7 +3598,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"anyhow",
"bytes",
@@ -6140,7 +6140,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4a26572a4e43714def9b362d444c640fdf1bc0d9#4a26572a4e43714def9b362d444c640fdf1bc0d9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a#4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a"
dependencies = [
"anyhow",
"app-error",
+2 -2
View File
@@ -103,8 +103,8 @@ dashmap = "6.0.1"
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4a26572a4e43714def9b362d444c640fdf1bc0d9" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4a26572a4e43714def9b362d444c640fdf1bc0d9" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4ed5b367eac5ae9ffd603812e2fea26b3ed3da7a" }
[profile.dev]
opt-level = 0
+3 -1
View File
@@ -1,7 +1,7 @@
use bytes::Bytes;
pub use client_api::entity::ai_dto::{
AppFlowyOfflineAI, CompleteTextParams, CompletionMetadata, CompletionType, CreateChatContext,
LLMModel, LocalAIConfig, ModelInfo, OutputContent, OutputLayout, RelatedQuestion,
LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, OutputLayout, RelatedQuestion,
RepeatedRelatedQuestion, ResponseFormat, StringOrMessage,
};
pub use client_api::entity::billing_dto::SubscriptionPlan;
@@ -119,4 +119,6 @@ pub trait ChatCloudService: Send + Sync + 'static {
chat_id: &str,
params: UpdateChatParams,
) -> Result<(), FlowyError>;
async fn get_available_models(&self, workspace_id: &str) -> Result<ModelList, FlowyError>;
}
+10 -1
View File
@@ -10,7 +10,7 @@ use std::collections::HashMap;
use appflowy_plugin::manager::PluginManager;
use dashmap::DashMap;
use flowy_ai_pub::cloud::{ChatCloudService, ChatSettings, UpdateChatParams};
use flowy_ai_pub::cloud::{ChatCloudService, ChatSettings, ModelList, UpdateChatParams};
use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::kv::KVStorePreferences;
use flowy_sqlite::DBConnection;
@@ -241,6 +241,15 @@ impl AIManager {
Ok(())
}
pub async fn get_available_models(&self) -> FlowyResult<ModelList> {
let workspace_id = self.user_service.workspace_id()?;
let list = self
.cloud_service_wm
.get_available_models(&workspace_id)
.await?;
Ok(list)
}
pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result<Arc<Chat>, FlowyError> {
let chat = self.chats.get(chat_id).as_deref().cloned();
match chat {
+4
View File
@@ -259,6 +259,10 @@ impl Chat {
let _ = answer_sink
.send("AI_IMAGE_RESPONSE_LIMIT".to_string())
.await;
} else if err.is_ai_max_required() {
let _ = answer_sink
.send(format!("AI_MAX_REQUIRED:{}", err.msg))
.await;
} else {
let _ = answer_sink.send(format!("error:{}", err)).await;
}
@@ -182,6 +182,12 @@ pub struct ChatMessageListPB {
pub total: i64,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct ModelConfigPB {
#[pb(index = 1)]
pub models: String,
}
impl From<RepeatedChatMessage> for ChatMessageListPB {
fn from(repeated_chat_message: RepeatedChatMessage) -> Self {
let messages = repeated_chat_message
@@ -13,6 +13,7 @@ use flowy_ai_pub::cloud::{ChatMessageMetadata, ChatMessageType, ChatRAGData, Con
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use lib_infra::isolate_stream::IsolateSink;
use serde_json::json;
use std::sync::{Arc, Weak};
use tracing::trace;
use validator::Validate;
@@ -107,6 +108,22 @@ pub(crate) async fn regenerate_response_handler(
Ok(())
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn get_available_model_list_handler(
ai_manager: AFPluginState<Weak<AIManager>>,
) -> DataResult<ModelConfigPB, FlowyError> {
let ai_manager = upgrade_ai_manager(ai_manager)?;
let available_models = ai_manager.get_available_models().await?;
let models = available_models
.models
.into_iter()
.map(|m| m.name)
.collect::<Vec<String>>();
let models = serde_json::to_string(&json!({"models": models}))?;
data_result_ok(ModelConfigPB { models })
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn load_prev_message_handler(
data: AFPluginData<LoadPrevChatMessagePB>,
@@ -60,6 +60,10 @@ pub fn init(ai_manager: Weak<AIManager>) -> AFPlugin {
.event(AIEvent::GetChatSettings, get_chat_settings_handler)
.event(AIEvent::UpdateChatSettings, update_chat_settings_handler)
.event(AIEvent::RegenerateResponse, regenerate_response_handler)
.event(
AIEvent::GetAvailableModels,
get_available_model_list_handler,
)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@@ -154,4 +158,7 @@ pub enum AIEvent {
#[event(input = "RegenerateResponsePB")]
RegenerateResponse = 27,
#[event(output = "ModelConfigPB")]
GetAvailableModels = 28,
}
@@ -10,9 +10,9 @@ use std::collections::HashMap;
use flowy_ai_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings,
CompleteTextParams, LocalAIConfig, MessageCursor, RelatedQuestion, RepeatedChatMessage,
RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, SubscriptionPlan,
UpdateChatParams,
CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RelatedQuestion,
RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete,
SubscriptionPlan, UpdateChatParams,
};
use flowy_error::{FlowyError, FlowyResult};
use futures::{stream, Sink, StreamExt, TryStreamExt};
@@ -353,4 +353,8 @@ impl ChatCloudService for AICloudServiceMiddleware {
.update_chat_settings(workspace_id, chat_id, params)
.await
}
async fn get_available_models(&self, workspace_id: &str) -> Result<ModelList, FlowyError> {
self.cloud_service.get_available_models(workspace_id).await
}
}
@@ -22,7 +22,7 @@ use collab_integrate::collab_builder::{
};
use flowy_ai_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings,
CompleteTextParams, LocalAIConfig, MessageCursor, RepeatedChatMessage, ResponseFormat,
CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat,
StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams,
};
use flowy_database_pub::cloud::{
@@ -838,6 +838,14 @@ impl ChatCloudService for ServerProvider {
.update_chat_settings(workspace_id, chat_id, params)
.await
}
async fn get_available_models(&self, workspace_id: &str) -> Result<ModelList, FlowyError> {
self
.get_server()?
.chat_service()
.get_available_models(workspace_id)
.await
}
}
#[async_trait]
@@ -362,6 +362,9 @@ pub enum ErrorCode {
#[error("AI Image Response limit exceeded")]
AIImageResponseLimitExceeded = 124,
#[error("AI Max Required")]
AIMaxRequired = 125,
}
impl ErrorCode {
+5 -1
View File
@@ -13,7 +13,7 @@ use crate::code::ErrorCode;
pub type FlowyResult<T> = anyhow::Result<T, FlowyError>;
#[derive(Debug, Default, Clone, ProtoBuf, Error)]
#[error("{msg}")]
#[error("code:{code}, message:{msg}")]
pub struct FlowyError {
#[pb(index = 1)]
pub code: ErrorCode,
@@ -95,6 +95,10 @@ impl FlowyError {
self.code == ErrorCode::AIImageResponseLimitExceeded
}
pub fn is_ai_max_required(&self) -> bool {
self.code == ErrorCode::AIMaxRequired
}
static_flowy_error!(internal, ErrorCode::Internal);
static_flowy_error!(record_not_found, ErrorCode::RecordNotFound);
static_flowy_error!(workspace_initialize, ErrorCode::WorkspaceInitializeError);
@@ -25,6 +25,7 @@ impl From<AppResponseError> for FlowyError {
AppErrorCode::WorkspaceMemberLimitExceeded => ErrorCode::WorkspaceMemberLimitExceeded,
AppErrorCode::AIResponseLimitExceeded => ErrorCode::AIResponseLimitExceeded,
AppErrorCode::AIImageResponseLimitExceeded => ErrorCode::AIImageResponseLimitExceeded,
AppErrorCode::AIMaxRequired => ErrorCode::AIMaxRequired,
AppErrorCode::FileStorageLimitExceeded => ErrorCode::FileStorageLimitExceeded,
AppErrorCode::SingleUploadLimitExceeded => ErrorCode::SingleUploadLimitExceeded,
AppErrorCode::CustomNamespaceDisabled => ErrorCode::CustomNamespaceRequirePlanUpgrade,
@@ -8,7 +8,7 @@ use client_api::entity::chat_dto::{
};
use flowy_ai_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, LocalAIConfig,
StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams,
ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams,
};
use flowy_error::FlowyError;
use futures_util::{StreamExt, TryStreamExt};
@@ -270,4 +270,13 @@ where
.await?;
Ok(())
}
async fn get_available_models(&self, workspace_id: &str) -> Result<ModelList, FlowyError> {
let list = self
.inner
.try_get_client()?
.get_model_list(workspace_id)
.await?;
Ok(list)
}
}
@@ -18,7 +18,7 @@ use client_api::entity::{
use client_api::entity::{QueryCollab, QueryCollabParams};
use client_api::{Client, ClientConfiguration};
use collab_entity::{CollabObject, CollabType};
use tracing::instrument;
use tracing::{instrument, trace};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver};
@@ -586,6 +586,7 @@ where
workspace_id: &str,
workspace_settings: AFWorkspaceSettingsChange,
) -> Result<AFWorkspaceSettings, FlowyError> {
trace!("Sync workspace settings: {:?}", workspace_settings);
let workspace_id = workspace_id.to_string();
let try_get_client = self.server.try_get_client();
let client = try_get_client?;
@@ -1,4 +1,3 @@
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
@@ -7,7 +6,6 @@ use crate::af_cloud::define::ServerUser;
use anyhow::Error;
use arc_swap::ArcSwap;
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::{
@@ -124,7 +122,7 @@ impl AppFlowyServer for AppFlowyCloudServer {
}
fn set_ai_model(&self, ai_model: &str) -> Result<(), Error> {
self.client.set_ai_model(AIModel::from_str(ai_model)?);
self.client.set_ai_model(ai_model.to_string());
Ok(())
}
@@ -1,7 +1,7 @@
use client_api::entity::ai_dto::{LocalAIConfig, RepeatedRelatedQuestion};
use flowy_ai_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings,
CompleteTextParams, MessageCursor, RepeatedChatMessage, ResponseFormat, StreamAnswer,
CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer,
StreamComplete, SubscriptionPlan, UpdateChatParams,
};
use flowy_error::FlowyError;
@@ -144,4 +144,8 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
) -> Result<(), FlowyError> {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
}
async fn get_available_models(&self, _workspace_id: &str) -> Result<ModelList, FlowyError> {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
}
}
@@ -1,13 +1,11 @@
use std::convert::TryInto;
use std::str::FromStr;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_user_pub::entities::*;
use lib_infra::validator_fn::required_not_empty_str;
use std::convert::TryInto;
use validator::Validate;
use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword};
use crate::entities::{AIModelPB, AuthenticatorPB};
use crate::entities::AuthenticatorPB;
use crate::errors::ErrorCode;
use super::parser::UserStabilityAIKey;
@@ -58,7 +56,7 @@ pub struct UserProfilePB {
pub stability_ai_key: String,
#[pb(index = 11)]
pub ai_model: AIModelPB,
pub ai_model: String,
}
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
@@ -79,6 +77,10 @@ impl From<UserProfile> for UserProfilePB {
EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption),
EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric),
};
let mut ai_model = user_profile.ai_model;
if ai_model.is_empty() {
ai_model = "Default".to_string();
}
Self {
id: user_profile.uid,
email: user_profile.email,
@@ -90,7 +92,7 @@ impl From<UserProfile> for UserProfilePB {
encryption_sign,
encryption_type: encryption_ty,
stability_ai_key: user_profile.stability_ai_key,
ai_model: AIModelPB::from_str(&user_profile.ai_model).unwrap_or_default(),
ai_model,
}
}
}
@@ -3,7 +3,6 @@ use client_api::entity::billing_dto::{
WorkspaceSubscriptionStatus, WorkspaceUsageAndLimit,
};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use validator::Validate;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
@@ -382,19 +381,19 @@ pub struct UseAISettingPB {
pub disable_search_indexing: bool,
#[pb(index = 2)]
pub ai_model: AIModelPB,
pub ai_model: String,
}
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(),
ai_model: value.ai_model,
}
}
}
#[derive(ProtoBuf, Default, Clone, Validate)]
#[derive(ProtoBuf, Default, Clone, Validate, Debug)]
pub struct UpdateUserWorkspaceSettingPB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
@@ -404,58 +403,22 @@ pub struct UpdateUserWorkspaceSettingPB {
pub disable_search_indexing: Option<bool>,
#[pb(index = 3, one_of)]
pub ai_model: Option<AIModelPB>,
pub ai_model: Option<String>,
}
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);
change.disable_search_indexing = Some(disable_search_indexing);
}
if let Some(ai_model) = value.ai_model {
change = change.ai_model(ai_model.to_str().to_string());
change.ai_model = Some(ai_model);
}
change
}
}
#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default)]
pub enum AIModelPB {
#[default]
DefaultModel = 0,
GPT4oMini = 1,
GPT4o = 2,
Claude3Sonnet = 3,
Claude3Opus = 4,
}
impl AIModelPB {
pub fn to_str(&self) -> &str {
match self {
AIModelPB::DefaultModel => "default-model",
AIModelPB::GPT4oMini => "gpt-4o-mini",
AIModelPB::GPT4o => "gpt-4o",
AIModelPB::Claude3Sonnet => "claude-3-sonnet",
AIModelPB::Claude3Opus => "claude-3-opus",
}
}
}
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::GPT4oMini),
"gpt-4o" => Ok(AIModelPB::GPT4o),
"claude-3-sonnet" => Ok(AIModelPB::Claude3Sonnet),
"claude-3-opus" => Ok(AIModelPB::Claude3Opus),
_ => Ok(AIModelPB::DefaultModel),
}
}
}
#[derive(Debug, ProtoBuf, Default, Clone)]
pub struct WorkspaceSubscriptionInfoPB {
#[pb(index = 1)]
@@ -559,12 +559,12 @@ impl UserManager {
.send();
if let Some(ai_model) = &ai_model {
if let Err(err) = self.cloud_services.set_ai_model(ai_model.to_str()) {
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.to_str());
let params = UpdateUserProfileParams::new(uid).with_ai_model(ai_model);
upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?;
}
Ok(())