Compare commits

...
Author SHA1 Message Date
Nathan 71cb0f023b chore: bump client api 2025-02-06 17:33:28 +08:00
Nathan 669c17064b chore: bump client api 2025-02-06 17:06:57 +08:00
Nathan b3cb6090f2 Merge branch 'main' into chore/optional-response-format 2025-02-06 17:03:07 +08:00
FakhriAzzouz 0d89e22ed2 chore(i18n): update ar-SA translations (#7312)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-06 13:54:16 +08:00
ArtemisOne ff2aae213c chore(i18n): update es-VE and ga-IE translations (#7320)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-06 13:53:24 +08:00
Richard Shiue 43e64d8219 test: fix image integration test (#7323) 2025-02-05 19:45:21 +08:00
Richard Shiue 6823fe5d24 fix: row document images (#7322)
* fix: row document images

* fix: calendar

* chore: fallback to documentbloc
2025-02-05 16:40:03 +08:00
Lucas f683085618 fix: unable to upload file on Android devices (#7314) 2025-02-04 20:57:06 +08:00
Nathan.fooo 71a22dc466 chore: fix ai page user profile refresh (#7317) 2025-02-04 20:29:56 +08:00
Lucas eb508a3ec9 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 14:05:57 +08:00
Nathan.fooo aacd09d8e2 chore: Support new error code (#7311)
* chore: fetch model list

* chore: suppor new error code
2025-02-03 20:52:08 +08:00
Peter Jose 25a27dfa81 chore(i18n): update id-ID translations (#7290)
- Translate 'themeMode' label
- Adjust text 'fontFamily'
- Update 'layoutDirection' translation
- Update 'textDirection' translation
2025-02-03 10:41:05 +08:00
Mohammad Mahdi Momeni 36349778e3 chore(i18n): update fa translations (#7292) 2025-02-03 10:05:34 +08:00
Nathan.fooo 9271d42db5 chore: fetch model list (#7306)
* chore: fetch model list
2025-02-01 23:48:32 +08:00
Richard Shiue 90d6e98b51 fix(flutter_mobile): drop focus on tap outside (#7274) 2025-01-24 09:23:52 +08:00
Richard Shiue c39a91a014 chore: optional response format 2025-01-16 11:10:55 +08:00
56 changed files with 697 additions and 233 deletions

No files matched your search

@@ -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();
}
@@ -458,7 +458,7 @@ void main() {
});
testWidgets('paste the image url', (tester) async {
const plainText = 'https://appflowy.io/1.jpg';
const plainText = 'http://example.com/1.jpg';
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
+22 -22
View File
@@ -175,36 +175,36 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
@@ -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();
}
@@ -138,16 +138,18 @@ class _MobileChatInputState extends State<MobileChatInput> {
else
const VSpace(8.0),
inputTextField(context),
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
const HSpace(8.0),
leadingButtons(context),
const Spacer(),
sendButton(),
const HSpace(12.0),
],
TextFieldTapRegion(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
const HSpace(8.0),
leadingButtons(context),
const Spacer(),
sendButton(),
const HSpace(12.0),
],
),
),
),
],
@@ -281,6 +283,7 @@ class _MobileChatInputState extends State<MobileChatInput> {
fontWeight: FontWeight.w600,
),
),
onTapOutside: (_) => focusNode.unfocus(),
);
},
);
@@ -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,
+3 -3
View File
@@ -603,13 +603,13 @@ packages:
source: hosted
version: "7.0.0"
file_picker:
dependency: transitive
dependency: "direct overridden"
description:
name: file_picker
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c"
url: "https://pub.dev"
source: hosted
version: "8.1.7"
version: "8.1.4"
file_selector_linux:
dependency: transitive
description:
+4
View File
@@ -197,6 +197,10 @@ dependency_overrides:
commit: fbab857b1b1d209240a146d32f496379b9f62276
path: flutter_cache_manager
# Don't upgrade file_picker until the issue is fixed
# https://github.com/miguelpruivo/flutter_file_picker/issues/1652
file_picker: 8.1.4
flutter:
generate: true
uses-material-design: true
@@ -384,6 +384,8 @@
"askOwnerToUpgradeToProIOS": "مساحة العمل الخاصة بك على وشك النفاد من مساحة التخزين المجانية.",
"askOwnerToUpgradeToAIMax": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة العمل الخاصة بك. يرجى مطالبة مالك مساحة العمل الخاصة بك بترقية الخطة أو شراء إضافات الذكاء الاصطناعي",
"askOwnerToUpgradeToAIMaxIOS": "مساحة العمل الخاصة بك تفتقر إلى الاستجابات المجانية للذكاء الاصطناعي.",
"purchaseAIMax": "لقد نفدت استجابات الصور بالذكاء الاصطناعي من مساحة العمل الخاصة بك. يرجى مطالبة مالك مساحة العمل الخاصة بك بشراء AI Max",
"aiImageResponseLimit": "لقد نفدت استجابات الصور الخاصة بالذكاء الاصطناعي.<inlang-LineFeed>\nانتقل إلى الإعدادات -&gt; الخطة -&gt; انقر فوق AI Max للحصول على المزيد من استجابات صور AI",
"purchaseStorageSpace": "شراء مساحة تخزين",
"singleFileProPlanLimitationDescription": "لقد تجاوزت الحد الأقصى لحجم تحميل الملف المسموح به في الخطة المجانية. يرجى الترقية إلى الخطة الاحترافية لتحميل ملفات أكبر حجمًا",
"purchaseAIResponse": "شراء",
@@ -1194,6 +1196,7 @@
"system": "التكيف مع النظام"
},
"fontScaleFactor": "عامل مقياس الخط",
"displaySize": "حجم العرض",
"documentSettings": {
"cursorColor": "لون مؤشر المستند",
"selectionColor": "لون اختيار المستند",
+2 -2
View File
@@ -54,8 +54,8 @@
"LogInWithGoogle": "چوونە ژوورەوە لە ڕێگەی گووگڵەوە",
"LogInWithGithub": "چوونە ژوورەوە لە ڕێگەی گیتهاب",
"LogInWithDiscord": "چوونە ژوورەوە لە ڕێگەی دیسکۆرد",
"loginAsGuestButtonText": "دەست پێ بکە",
"logInWithMagicLink": "بە مەجیک لینک بچۆرە ژوورەوە"
"logInWithMagicLink": "بە مەجیک لینک بچۆرە ژوورەوە",
"loginAsGuestButtonText": "دەست پێ بکە"
},
"workspace": {
"chooseWorkspace": "هەڵبژاردنی شوێنی کارەکەت",
-1
View File
@@ -901,7 +901,6 @@
"description": "Unlimited AI responses powered by advanced AI models, and 50 AI images per month",
"price": "{}",
"priceInfo": "Per user per month billed annually"
},
"aiOnDevice": {
"title": "AI On-device for Mac",
+48 -7
View File
@@ -36,6 +36,7 @@
"loginButtonText": "Ingresar",
"loginStartWithAnonymous": "Comience una sesión anónima",
"continueAnonymousUser": "Continuar con una sesión anónima",
"anonymous": "Anónimo",
"buttonText": "Ingresar",
"signingInText": "Iniciando sesión...",
"forgotPassword": "¿Olvidó su contraseña?",
@@ -50,6 +51,8 @@
"signInWithGoogle": "Iniciar sesión con Google",
"signInWithGithub": "Iniciar sesión con Github",
"signInWithDiscord": "Iniciar sesión con Discord",
"signInWithApple": "Continuar con Apple",
"continueAnotherWay": "Continuar por otro camino",
"signUpWithGoogle": "Registrarse con Google",
"signUpWithGithub": "Registrarse con Github",
"signUpWithDiscord": "Registrarse con Discord",
@@ -68,8 +71,12 @@
},
"workspace": {
"chooseWorkspace": "Elige tu espacio de trabajo",
"defaultName": "Mi espacio de trabajo",
"create": "Crear espacio de trabajo",
"new": "Nuevo espacio de trabajo",
"learnMore": "Más información",
"reset": "Restablecer espacio de trabajo",
"renameWorkspace": "Cambiar el nombre del espacio de trabajo",
"resetWorkspacePrompt": "Al restablecer el espacio de trabajo se eliminarán todas las páginas y datos que contiene. ¿Está seguro de que desea restablecer el espacio de trabajo? Alternativamente, puede comunicarse con el equipo de soporte para restaurar el espacio de trabajo.",
"hint": "Espacio de trabajo",
"notFoundError": "Espacio de trabajo no encontrado",
@@ -105,7 +112,9 @@
"html": "HTML",
"clipboard": "Copiar al portapapeles",
"csv": "CSV",
"copyLink": "Copiar enlace"
"copyLink": "Copiar enlace",
"publish": "Publicar",
"publishTab": "Publicar"
},
"moreAction": {
"small": "pequeño",
@@ -118,7 +127,9 @@
"charCount": "Número de caracteres : {}",
"createdAt": "Creado: {}",
"deleteView": "Borrar",
"duplicateView": "Duplicar"
"duplicateView": "Duplicar",
"createdAtLabel": "Creado: ",
"syncedAtLabel": "Sincronizado: "
},
"importPanel": {
"textAndMarkdown": "Texto y Markdown",
@@ -136,7 +147,8 @@
"openNewTab": "Abrir en una pestaña nueva",
"moveTo": "Mover a",
"addToFavorites": "Añadira los favoritos",
"copyLink": "Copiar Enlace"
"copyLink": "Copiar Enlace",
"move": "Mover"
},
"blankPageTitle": "Página en blanco",
"newPageText": "Nueva página",
@@ -149,16 +161,31 @@
"relatedQuestion": "Relacionado",
"serverUnavailable": "Servicio temporalmente no disponible. Por favor, inténtelo de nuevo más tarde.",
"aiServerUnavailable": "🌈 ¡Uh-oh! 🌈. Un unicornio se comió nuestra respuesta. ¡Por favor, intenta de nuevo!",
"retry": "Rever",
"clickToRetry": "Haga clic para volver a intentarlo",
"regenerateAnswer": "Regenerar",
"question1": "Cómo utilizar Kanban para gestionar tareas",
"question2": "Explica el método GTD",
"question3": "¿Por qué usar Rust?",
"aiMistakePrompt": "La IA puede cometer errores. Consulta información importante."
"aiMistakePrompt": "La IA puede cometer errores. Consulta información importante.",
"referenceSource": {
"one": "Se encontró {count} fuente",
"other": "Se encontraron {count} fuentes"
},
"regenerate": "Intentar otra vez",
"addToNewPage": "Crear nueva página",
"changeFormat": {
"textOnly": "Texto",
"text": "Párrafo"
},
"selectBanner": {
"saveButton": "Añadir …"
}
},
"trash": {
"text": "Papelera",
"restoreAll": "Recuperar todo",
"restore": "Restaurar",
"deleteAll": "Eliminar todo",
"pageHeader": {
"fileName": "Nombre de archivo",
@@ -204,7 +231,8 @@
"moreButtonToolTip": "Eliminar, renombrar y más...",
"addPageTooltip": "Inserta una página",
"defaultNewPageName": "Sin Título",
"renameDialog": "Renombrar"
"renameDialog": "Renombrar",
"pageNameSuffix": "Copiar"
},
"noPagesInside": "No hay páginas dentro",
"toolbar": {
@@ -260,7 +288,10 @@
"emptyRecent": "Sin documentos recientes",
"favoriteSpace": "Favoritos",
"RecentSpace": "Reciente",
"Spaces": "Espacios"
"Spaces": "Espacios",
"aiImageResponseLimit": "Se ha quedado sin respuestas de imágenes de IA.\n\nVaya a Configuración -> Plan -> Haga clic en AI Max para obtener más respuestas de imágenes de IA",
"purchaseStorageSpace": "Comprar espacio de almacenamiento",
"purchaseAIResponse": "Compra "
},
"notifications": {
"export": {
@@ -294,6 +325,7 @@
"upload": "Subir",
"edit": "Editar",
"delete": "Borrar",
"copy": "Copiar",
"duplicate": "Duplicar",
"putback": "Volver",
"update": "Actualizar",
@@ -305,6 +337,7 @@
"helpCenter": "Centro de ayuda",
"add": "Añadir",
"yes": "Si",
"no": "No",
"clear": "Limpiar",
"remove": "Eliminar",
"dontRemove": "no quitar",
@@ -319,7 +352,12 @@
"signInDiscord": "Iniciar sesión con discordia",
"more": "Más",
"create": "Crear",
"close": "Cerca"
"close": "Cerca",
"next": "Próximo",
"previous": "Anterior",
"submit": "Entregar",
"download": "Descargar",
"backToHome": "Volver a Inicio"
},
"label": {
"welcome": "¡Bienvenido!",
@@ -343,6 +381,9 @@
},
"settings": {
"title": "Ajustes",
"popupMenuItem": {
"settings": "Ajustes"
},
"accountPage": {
"menuLabel": "Mi cuenta",
"title": "Mi cuenta",
+30
View File
@@ -2,12 +2,14 @@
"appName": "AppFlowy",
"defaultUsername": "من",
"welcomeText": "به @:appName خوش آمدید",
"welcomeTo": "خوش آمدید به",
"githubStarText": "به گیت‌هاب ما ستاره دهید",
"subscribeNewsletterText": "اشتراک در خبرنامه",
"letsGoButtonText": "شروع کنید",
"title": "عنوان",
"youCanAlso": "همچنین می‌توانید",
"and": "و",
"failedToOpenUrl": "خطا در بازکردن نشانی وب: {}",
"blockActions": {
"addBelowTooltip": "برای افزودن در زیر کلیک کنید",
"addAboveCmd": "Alt+click",
@@ -32,19 +34,47 @@
"signIn": {
"loginTitle": "ورود به @:appName",
"loginButtonText": "ورود",
"loginStartWithAnonymous": "ادامه دادن با یک جلسه ناشناس",
"continueAnonymousUser": "ادامه دادن به صورت کاربر مهمان",
"anonymous": "ناشناس",
"buttonText": "ورود",
"signingInText": "در حال ورود...",
"forgotPassword": "رمز عبور را فراموش کرده اید؟",
"emailHint": "ایمیل",
"passwordHint": "رمز عبور",
"dontHaveAnAccount": "آیا حساب کاربری ندارید؟",
"createAccount": "ساخت حساب کاربری",
"repeatPasswordEmptyError": "تکرار رمز عبور نمی‌تواند خالی باشد",
"unmatchedPasswordError": "تکرار رمز عبور مشابه رمز عبور نیست",
"syncPromptMessage": "همگام سازی داده ها ممکن است کمی طول بکشد. لطفا این صفحه را نبندید",
"or": "یا",
"signInWithGoogle": "ادامه دادن با گوگل",
"signInWithGithub": "ادامه دادن با گیتهاب",
"signInWithDiscord": "ادامه دادن با دیسکورد",
"signInWithApple": "ادامه دادن با اپل",
"continueAnotherWay": "ادامه دادن از طریق دیگر",
"signUpWithGoogle": "ثبت نام با گوگل",
"signUpWithGithub": "ثبت نام با گیتهاب",
"signUpWithDiscord": "ثبت نام با دیسکورد",
"signInWith": "ثبت نام با:",
"signInWithEmail": "ادامه دادن با ایمیل",
"signInWithMagicLink": "ادامه",
"pleaseInputYourEmail": "لطفا آدرس ایمیل خود را وارد کنید",
"settings": "تنظیمات",
"invalidEmail": "لطفا یک آدرس ایمیل معتبر وارد کنید",
"alreadyHaveAnAccount": "حساب کاربری دارید؟",
"logIn": "ورود",
"generalError": "مشکلی پیش آمد. لطفاً بعداً دوباره امتحان کنید",
"loginAsGuestButtonText": "شروع کنید"
},
"workspace": {
"chooseWorkspace": "فضای کار خود را انتخاب کنید",
"defaultName": "فضای کار من",
"create": "ایجاد فضای کار",
"new": "فضای کار جدید",
"learnMore": "بیشتر بدانید",
"renameWorkspace": "حذف فضای کار",
"workspaceNameCannotBeEmpty": "اسم فضای کار نمی‌تواند خالی باشد",
"hint": "فضای کار",
"notFoundError": "فضای کاری پیدا نشد"
},
+2 -2
View File
@@ -72,8 +72,8 @@
"LogInWithGoogle": "Se connecter avec Google",
"LogInWithGithub": "Se connecter avec Github",
"LogInWithDiscord": "Se connecter avec Discord",
"loginAsGuestButtonText": "Commencer",
"logInWithMagicLink": "Connectez-vous avec Magic Link"
"logInWithMagicLink": "Connectez-vous avec Magic Link",
"loginAsGuestButtonText": "Commencer"
},
"workspace": {
"chooseWorkspace": "Choisissez votre espace de travail",
+119
View File
@@ -0,0 +1,119 @@
{
"appName": "Appflowy",
"defaultUsername": "Liom",
"welcomeText": "Fáilte go @:appName",
"welcomeTo": "Fáilte chuig",
"githubStarText": "Réalta ar GitHub",
"subscribeNewsletterText": "Liostáil le Nuachtlitir",
"letsGoButtonText": "Tús Tapa",
"title": "Teideal",
"youCanAlso": "Is féidir leat freisin",
"and": "agus",
"failedToOpenUrl": "Theip ar oscailt an url: {}",
"blockActions": {
"addBelowTooltip": "Cliceáil chun cur leis thíos",
"addAboveCmd": "Alt+cliceáil",
"addAboveMacCmd": "Option+cliceáil",
"addAboveTooltip": "a chur thuas",
"dragTooltip": "Tarraing chun bogadh",
"openMenuTooltip": "Cliceáil chun an roghchlár a oscailt"
},
"signUp": {
"buttonText": "Cláraigh",
"title": "Cláraigh le @:appName",
"getStartedText": "Faigh Tosaigh",
"emptyPasswordError": "Ní féidir le pasfhocal a bheith folamh",
"repeatPasswordEmptyError": "Ní féidir an pasfhocal athdhéanta a bheith folamh",
"unmatchedPasswordError": "Ní ionann pasfhocal athdhéanta agus pasfhocal",
"alreadyHaveAnAccount": "An bhfuil cuntas agat cheana féin?",
"emailHint": "Ríomhphost",
"passwordHint": "Pasfhocal",
"repeatPasswordHint": "Déan pasfhocal arís",
"signUpWith": "Cláraigh le:"
},
"signIn": {
"loginTitle": "Logáil isteach ar @:appName",
"loginButtonText": "Logáil isteach",
"loginStartWithAnonymous": "Lean ar aghaidh le seisiún gan ainm",
"continueAnonymousUser": "Lean ar aghaidh le seisiún gan ainm",
"anonymous": "Gan ainm",
"buttonText": "Sínigh Isteach",
"signingInText": "Ag síniú isteach...",
"forgotPassword": "Pasfhocal Dearmadta?",
"emailHint": "Ríomhphost",
"passwordHint": "Pasfhocal",
"dontHaveAnAccount": "Nach bhfuil cuntas agat?",
"createAccount": "Cruthaigh cuntas",
"repeatPasswordEmptyError": "Ní féidir an pasfhocal athdhéanta a bheith folamh",
"unmatchedPasswordError": "Ní hionann pasfhocal athdhéanta agus pasfhocal",
"syncPromptMessage": "Seans go dtógfaidh sé tamall na sonraí a shioncronú. Ná dún an leathanach seo, le do thoil",
"or": "NÓ",
"signInWithGoogle": "Lean ar aghaidh le Google",
"signInWithGithub": "Lean ar aghaidh le GitHub",
"signInWithDiscord": "Lean ar aghaidh le Discord",
"signInWithApple": "Lean ar aghaidh le Apple",
"continueAnotherWay": "Lean ar aghaidh ar bhealach eile",
"signUpWithGoogle": "Cláraigh le Google",
"signUpWithGithub": "Cláraigh le Github",
"signUpWithDiscord": "Cláraigh le Discord",
"signInWith": "Lean ar aghaidh le:",
"signInWithEmail": "Lean ar aghaidh le Ríomhphost",
"signInWithMagicLink": "Lean ort",
"signUpWithMagicLink": "Cláraigh le Magic Link",
"pleaseInputYourEmail": "Cuir isteach do sheoladh ríomhphoist",
"settings": "Socruithe",
"magicLinkSent": "Magic Link seolta!",
"invalidEmail": "Cuir isteach seoladh ríomhphoist bailí",
"alreadyHaveAnAccount": "An bhfuil cuntas agat cheana féin?",
"logIn": "Logáil isteach",
"generalError": "Chuaigh rud éigin mícheart. Bain triail eile as ar ball",
"limitRateError": "Ar chúiseanna slándála, ní féidir leat nasc draíochta a iarraidh ach gach 60 soicind"
},
"workspace": {
"chooseWorkspace": "Roghnaigh do spás oibre",
"defaultName": "Mo Spás Oibre",
"create": "Cruthaigh spás oibre",
"new": "Spás oibre nua",
"importFromNotion": "Iompórtáil ó Notion",
"learnMore": "Foghlaim níos mó",
"reset": "Athshocraigh spás oibre",
"renameWorkspace": "Athainmnigh spás oibre",
"workspaceNameCannotBeEmpty": "Ní féidir leis an ainm spás oibre a bheith folamh",
"hint": "spás oibre",
"notFoundError": "Spás oibre gan aimsiú",
"errorActions": {
"reportIssue": "Tuairiscigh saincheist",
"reportIssueOnGithub": "Tuairiscigh ceist faoi Github",
"exportLogFiles": "Easpórtáil comhaid logáil",
"reachOut": "Bhaint amach le Discord"
},
"menuTitle": "Spásanna oibre",
"createSuccess": "Cruthaíodh spás oibre go rathúil",
"leaveCurrentWorkspace": "Fág spás óibre"
},
"shareAction": {
"buttonText": "Comhroinn",
"workInProgress": "Ag teacht go luath",
"markdown": "Markdown",
"html": "HTML",
"clipboard": "Cóipeáil chuig an ngearrthaisce",
"csv": "CSV",
"copyLink": "Cóipeáil nasc",
"publishToTheWeb": "Foilsigh don Ghréasán",
"publishToTheWebHint": "Cruthaigh suíomh Gréasáin le AppFlowy",
"publish": "Foilsigh",
"unPublish": "Dífhoilsiú",
"visitSite": "Tabhair cuairt ar an suíomh",
"publishTab": "Foilsigh",
"shareTab": "Comhroinn"
},
"moreAction": {
"small": "beag",
"medium": "meánach",
"large": "mór",
"fontSize": "Clómhéid",
"import": "Iompórtáil",
"createdAt": "Cruthaithe: {}",
"deleteView": "Scrios"
}
}
+21 -21
View File
@@ -206,13 +206,13 @@
"addBlockBelow": "Tambahkan blok di bawah ini"
},
"sideBar": {
"closeSidebar": "Close sidebar",
"openSidebar": "Open sidebar",
"closeSidebar": "Tutup sidebar",
"openSidebar": "Buka sidebar",
"personal": "Pribadi",
"favorites": "Favorit",
"clickToHidePersonal": "Klik untuk menutup seksi pribadi",
"clickToHideFavorites": "Klik untuk menutup seksi favorit",
"addAPage": "Tambah sebuah page"
"clickToHidePersonal": "Klik untuk menutup Pribadi",
"clickToHideFavorites": "Klik untuk menutup Favorit",
"addAPage": "Tambah halaman baru"
},
"notifications": {
"export": {
@@ -305,28 +305,28 @@
"appearance": {
"resetSetting": "Mengatur ulang pengaturan ini",
"fontFamily": {
"label": "Keluarga Fon",
"search": "Mencari"
"label": "Jenis Font",
"search": "Cari"
},
"themeMode": {
"label": "Theme Mode",
"light": "Mode Terang",
"dark": "Mode Gelap",
"system": "Adapt to System"
"label": "Tema",
"light": "Terang",
"dark": "Gelap",
"system": "Sesuai Sistem"
},
"layoutDirection": {
"label": "Arah Layout",
"hint": "Mengontrol aliran konten pada layar Anda, dari kiri ke kanan atau kanan ke kiri.",
"ltr": "LTR",
"rtl": "RTL"
"label": "Arah Tampilan",
"hint": "Mengatur arah tampilan konten, apakah dari kiri ke kanan atau kanan ke kiri.",
"ltr": "Kiri ke Kanan",
"rtl": "Kanan ke Kiri"
},
"textDirection": {
"label": "Arah teks default",
"hint": "Menentukan apakah teks harus dimulai dari kiri atau kanan sebagai default.",
"ltr": "LTR",
"rtl": "RTL",
"auto": "AUTO",
"fallback": "Sama seperti arah layout"
"label": "Arah Teks Bawaan",
"hint": "Atur arah teks bawaan, apakah dari kiri ke kanan atau kanan ke kiri.",
"ltr": "Kiri ke Kanan",
"rtl": "Kanan ke Kiri",
"auto": "Otomatis",
"fallback": "Sesuai Arah Tampilan"
},
"themeUpload": {
"button": "Mengunggah",
+2 -2
View File
@@ -72,8 +72,8 @@
"LogInWithGoogle": "使用 Google 登录",
"LogInWithGithub": "使用 Github 登录",
"LogInWithDiscord": "使用 Discord 登录",
"loginAsGuestButtonText": "开始使用",
"logInWithMagicLink": "使用魔法链接登录"
"logInWithMagicLink": "使用魔法链接登录",
"loginAsGuestButtonText": "开始使用"
},
"workspace": {
"chooseWorkspace": "选择您的工作区",
+28 -14
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
dependencies = [
"anyhow",
"async-trait",
@@ -1400,7 +1400,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@@ -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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
dependencies = [
"anyhow",
"bytes",
@@ -4624,7 +4624,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_macros 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@@ -4644,6 +4644,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.3",
"phf_shared 0.11.2",
]
@@ -4711,6 +4712,19 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.94",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@@ -6140,7 +6154,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=c80157a687397a38c7390ec3d7fee034b93db963#c80157a687397a38c7390ec3d7fee034b93db963"
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 = "c80157a687397a38c7390ec3d7fee034b93db963" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c80157a687397a38c7390ec3d7fee034b93db963" }
[profile.dev]
opt-level = 0
+4 -2
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;
@@ -54,7 +54,7 @@ pub trait ChatCloudService: Send + Sync + 'static {
workspace_id: &str,
chat_id: &str,
message_id: i64,
format: ResponseFormat,
format: Option<ResponseFormat>,
) -> Result<StreamAnswer, FlowyError>;
async fn get_answer(
@@ -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 {
+7 -3
View File
@@ -138,7 +138,7 @@ impl Chat {
// Save message to disk
save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?;
let format = params.format.clone().unwrap_or_default().into();
let format = params.format.clone().map(Into::into);
self.stream_response(
params.answer_stream_port,
@@ -171,7 +171,7 @@ impl Chat {
.store(false, std::sync::atomic::Ordering::SeqCst);
self.stream_buffer.lock().await.clear();
let format = format.unwrap_or_default().into();
let format = format.map(Into::into);
let answer_stream_buffer = self.stream_buffer.clone();
let uid = self.user_service.user_id()?;
@@ -196,7 +196,7 @@ impl Chat {
uid: i64,
workspace_id: String,
question_id: i64,
format: ResponseFormat,
format: Option<ResponseFormat>,
) {
let stop_stream = self.stop_stream.clone();
let chat_id = self.chat_id.clone();
@@ -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;
}
@@ -104,8 +104,10 @@ impl CompletionTask {
custom_prompt: None,
metadata: Some(CompletionMetadata {
object_id: self.context.object_id,
workspace_id: None,
rag_ids: Some(self.context.rag_ids),
}),
format: Default::default(),
};
info!("start completion: {:?}", params);
@@ -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};
@@ -156,7 +156,7 @@ impl ChatCloudService for AICloudServiceMiddleware {
workspace_id: &str,
chat_id: &str,
question_id: i64,
format: ResponseFormat,
format: Option<ResponseFormat>,
) -> Result<StreamAnswer, FlowyError> {
if self.local_llm_controller.is_running() {
let row = self.get_message_record(question_id)?;
@@ -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::{
@@ -704,7 +704,7 @@ impl ChatCloudService for ServerProvider {
workspace_id: &str,
chat_id: &str,
message_id: i64,
format: ResponseFormat,
format: Option<ResponseFormat>,
) -> Result<StreamAnswer, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
@@ -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};
@@ -100,8 +100,9 @@ where
workspace_id: &str,
chat_id: &str,
message_id: i64,
format: ResponseFormat,
format: Option<ResponseFormat>,
) -> Result<StreamAnswer, FlowyError> {
let format = format.unwrap_or_default();
trace!(
"stream_answer: workspace_id={}, chat_id={}, format={:?}",
workspace_id,
@@ -270,4 +271,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;
@@ -51,7 +51,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
_workspace_id: &str,
_chat_id: &str,
_message_id: i64,
_format: ResponseFormat,
_format: Option<ResponseFormat>,
) -> Result<StreamAnswer, FlowyError> {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
}
@@ -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(())
+1
View File
@@ -13,6 +13,7 @@
"fa",
"fr-CA",
"fr-FR",
"ga-IE",
"he",
"hu-HU",
"id-ID",