Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee404369ff | ||
|
|
58ea654ddf | ||
|
|
cc14793535 | ||
|
|
122fb89c87 | ||
|
|
e9201cce72 | ||
|
|
5e5693e249 | ||
|
|
5ac0eded3c |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
+80
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
+9
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+5
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
+8
-4
@@ -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();
|
||||
|
||||
+8
-4
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+13
-9
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+3
-1
@@ -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(
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+4
-6
@@ -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,
|
||||
|
||||
+68
-3
@@ -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);
|
||||
}
|
||||
+6
-34
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+12
-12
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in new issue
Block a user