diff --git a/lib/features/base/mixin/ai_scribe_mixin.dart b/lib/features/base/mixin/ai_scribe_mixin.dart index c02e35dc9a..293267b5ac 100644 --- a/lib/features/base/mixin/ai_scribe_mixin.dart +++ b/lib/features/base/mixin/ai_scribe_mixin.dart @@ -23,18 +23,9 @@ mixin AiScribeMixin { session: session, accountId: accountId, ); - final scribeEndpoint = aiCapability?.scribeEndpoint; - - if (scribeEndpoint == null || scribeEndpoint.isEmpty) return; - - // Validate endpoint format - if (Uri.tryParse(scribeEndpoint)?.hasAbsolutePath != true) { - logError( - 'AiScribeMixin::injectAIScribeBindings(): Invalid endpoint format: $scribeEndpoint'); - return; + if (aiCapability?.isScribeEndpointAvailable == true) { + AIScribeBindings(aiCapability!.scribeEndpoint!).dependencies(); } - - AIScribeBindings(scribeEndpoint).dependencies(); } catch (e) { logError('AiScribeMixin::injectAIScribeBindings(): $e'); } diff --git a/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart b/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart index 8ed485b32c..a9a8722155 100644 --- a/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart +++ b/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart @@ -13,7 +13,13 @@ extension HandleAiScribeInComposerExtension on ComposerController { session: mailboxDashBoardController.sessionCurrent, accountId: mailboxDashBoardController.accountId.value, ); - return aiCapability?.isScribeEndpointAvailable == true; + final isScribeEndpointAvailable = + aiCapability?.isScribeEndpointAvailable == true; + + final isAIScribeConfigEnabled = + mailboxDashBoardController.cachedAIScribeConfig.value.isEnabled; + + return isAIScribeConfigEnabled && isScribeEndpointAvailable; } Future _getTextOnlyContentInEditor() async { diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 8d3604099c..c4886dd993 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -4,6 +4,7 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:core/utils/config/app_config_loader.dart'; import 'package:core/utils/file_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:core/utils/preview_eml_file_utils.dart'; import 'package:core/utils/print_utils.dart'; import 'package:get/get.dart'; @@ -114,6 +115,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/manage_account/data/local/preferences_setting_manager.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/identity_repository.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/bindings/setting_interactor_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/identities/identity_interactors_bindings.dart'; @@ -417,6 +419,11 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), ), ); + if (!PlatformInfo.isMobile) { + Get.lazyPut( + () => GetAIScribeConfigInteractor(Get.find()), + ); + } } @override diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 5fead10f9d..d9b18172f4 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -120,6 +120,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dow import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/ai_scribe/setup_cached_ai_scribe_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/cleanup_recent_search_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_action_type_for_email_selection.dart'; @@ -148,11 +149,14 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; import 'package:tmail_ui_user/features/mailto/presentation/model/mailto_arguments.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_rule_filter_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/get_ai_scribe_config_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/save_language_interactor.dart'; @@ -273,6 +277,7 @@ class MailboxDashBoardController extends ReloadableController SaveLanguageInteractor? saveLanguageInteractor; GetTextFormattingMenuStateInteractor? getTextFormattingMenuStateInteractor; SaveTextFormattingMenuStateInteractor? saveTextFormattingMenuStateInteractor; + GetAIScribeConfigInteractor? getAIScribeConfigInteractor; final scaffoldKey = GlobalKey(); final selectedMailbox = Rxn(); @@ -304,6 +309,7 @@ class MailboxDashBoardController extends ReloadableController final isPopupMenuOpened = RxBool(false); final octetsQuota = Rxn(); final isTextFormattingMenuOpened = RxBool(false); + final cachedAIScribeConfig = Rx(AIScribeConfig.initial()); Map mapDefaultMailboxIdByRole = {}; Map mapMailboxById = {}; @@ -399,6 +405,7 @@ class MailboxDashBoardController extends ReloadableController } _handleArguments(); _loadAppGrid(); + loadAIScribeConfig(); super.onReady(); } @@ -524,6 +531,8 @@ class MailboxDashBoardController extends ReloadableController setUpDefaultEmailSortOrder(success.emailSortOrderType); } else if (success is GetTextFormattingMenuStateSuccess) { updateTextFormattingMenuState(success.isDisplayed); + } else if (success is GetAIScribeConfigSuccess) { + handleLoadAIScribeConfigSuccess(success.aiScribeConfig); } else { super.handleSuccessViewState(success); } @@ -571,6 +580,8 @@ class MailboxDashBoardController extends ReloadableController backToHomeScreen(); } else if (failure is GetTextFormattingMenuStateFailure) { updateTextFormattingMenuState(false); + } else if (failure is GetAIScribeConfigFailure) { + handleLoadAIScribeConfigFailure(); } else { super.handleFailureViewState(failure); } @@ -2047,6 +2058,7 @@ class MailboxDashBoardController extends ReloadableController notifyThreadDetailSettingUpdated(); getServerSetting(); spamReportController.getSpamReportStateAction(); + loadAIScribeConfig(); } Future> quickSearchEmails(String query) async { @@ -2123,6 +2135,7 @@ class MailboxDashBoardController extends ReloadableController notifyThreadDetailSettingUpdated(); getServerSetting(); spamReportController.getSpamReportStateAction(); + loadAIScribeConfig(); } void _handleUpdateVacationSuccess(UpdateVacationSuccess success) { @@ -2451,7 +2464,7 @@ class MailboxDashBoardController extends ReloadableController void updateEmailList(List newEmailList) { emailsInCurrentMailbox.value = newEmailList; } - + void openMailboxAction(PresentationMailbox presentationMailbox) { dispatchMailboxUIAction(OpenMailboxAction(presentationMailbox)); } @@ -3217,7 +3230,7 @@ class MailboxDashBoardController extends ReloadableController leadingSVGIcon: imagePaths.icRecoverDeletedMessages, leadingSVGIconColor: Colors.white, backgroundColor: AppColor.primaryColor, - textColor: Colors.white, + textColor: Colors.white, ); } } @@ -3229,7 +3242,7 @@ class MailboxDashBoardController extends ReloadableController if (currentAccountId != null && currentSession != null) { final arguments = EmailRecoveryArguments(currentAccountId, currentSession); - final result = PlatformInfo.isWeb + final result = PlatformInfo.isWeb ? await DialogRouter.pushGeneralDialog( routeName: AppRoutes.emailRecovery, arguments: arguments, diff --git a/lib/features/mailbox_dashboard/presentation/extensions/ai_scribe/setup_cached_ai_scribe_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/ai_scribe/setup_cached_ai_scribe_extension.dart new file mode 100644 index 0000000000..592d17d047 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/ai_scribe/setup_cached_ai_scribe_extension.dart @@ -0,0 +1,30 @@ +import 'package:core/utils/platform_info.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension SetupCachedAiScribeExtension on MailboxDashBoardController { + void loadAIScribeConfig() { + if (PlatformInfo.isMobile) { + cachedAIScribeConfig.value = AIScribeConfig(isEnabled: false); + return; + } + + getAIScribeConfigInteractor = getBinding(); + + if (getAIScribeConfigInteractor != null) { + consumeState(getAIScribeConfigInteractor!.execute()); + } else { + handleLoadAIScribeConfigFailure(); + } + } + + void handleLoadAIScribeConfigSuccess(AIScribeConfig aiScribeConfig) { + cachedAIScribeConfig.value = aiScribeConfig; + } + + void handleLoadAIScribeConfigFailure() { + cachedAIScribeConfig.value = AIScribeConfig.initial(); + } +} diff --git a/lib/features/manage_account/data/datasource/manage_account_datasource.dart b/lib/features/manage_account/data/datasource/manage_account_datasource.dart index 7591877a94..b0888e3f48 100644 --- a/lib/features/manage_account/data/datasource/manage_account_datasource.dart +++ b/lib/features/manage_account/data/datasource/manage_account_datasource.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_setting.dart'; @@ -9,4 +10,6 @@ abstract class ManageAccountDataSource { Future toggleLocalSettingsState(PreferencesConfig preferencesConfig); Future getLocalSettings(); + + Future getAiScribeConfigLocalSettings(); } diff --git a/lib/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart b/lib/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart index 27151f7c96..c0f2b088bd 100644 --- a/lib/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart +++ b/lib/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:tmail_ui_user/features/manage_account/data/datasource/manage_account_datasource.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/preferences_setting_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_setting.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/spam_report_config.dart'; @@ -44,6 +45,10 @@ class ManageAccountDataSourceImpl extends ManageAccountDataSource { await _preferencesSettingManager.updateTextFormattingMenu( isDisplayed: preferencesConfig.isDisplayed, ); + } else if (preferencesConfig is AIScribeConfig) { + await _preferencesSettingManager.updateAIScribe( + preferencesConfig.isEnabled, + ); } else { await _preferencesSettingManager.savePreferences( preferencesConfig, @@ -59,4 +64,11 @@ class ManageAccountDataSourceImpl extends ManageAccountDataSource { return await _preferencesSettingManager.loadPreferences(); }).catchError(_exceptionThrower.throwException); } + + @override + Future getAiScribeConfigLocalSettings() { + return Future.sync(() async { + return await _preferencesSettingManager.getAIScribeConfig(); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file diff --git a/lib/features/manage_account/data/local/preferences_setting_manager.dart b/lib/features/manage_account/data/local/preferences_setting_manager.dart index fb6dc12302..5a7269411c 100644 --- a/lib/features/manage_account/data/local/preferences_setting_manager.dart +++ b/lib/features/manage_account/data/local/preferences_setting_manager.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/default_preferences_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/empty_preferences_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_config.dart'; @@ -17,6 +18,8 @@ class PreferencesSettingManager { '${_preferencesSettingKey}_SPAM_REPORT'; static const String _preferencesSettingTextFormattingMenuKey = '${_preferencesSettingKey}_TEXT_FORMATTING_MENU'; + static const String _preferencesSettingAIScribeKey = + '${_preferencesSettingKey}_AI_SCRIBE'; const PreferencesSettingManager(this._sharedPreferences); @@ -41,6 +44,8 @@ class PreferencesSettingManager { return SpamReportConfig.fromJson(jsonDecoded); case _preferencesSettingTextFormattingMenuKey: return TextFormattingMenuConfig.fromJson(jsonDecoded); + case _preferencesSettingAIScribeKey: + return AIScribeConfig.fromJson(jsonDecoded); default: return DefaultPreferencesConfig.fromJson(jsonDecoded); } @@ -71,6 +76,11 @@ class PreferencesSettingManager { _preferencesSettingTextFormattingMenuKey, jsonEncode(config.toJson()), ); + } else if (config is AIScribeConfig) { + await _sharedPreferences.setString( + _preferencesSettingAIScribeKey, + jsonEncode(config.toJson()), + ); } else { await _sharedPreferences.setString( _preferencesSettingKey, @@ -138,4 +148,22 @@ class PreferencesSettingManager { final updatedConfig = currentConfig.copyWith(isDisplayed: isDisplayed); await savePreferences(updatedConfig); } + + Future getAIScribeConfig() async { + await _sharedPreferences.reload(); + + final jsonString = _sharedPreferences.getString( + _preferencesSettingAIScribeKey, + ); + + return jsonString == null + ? AIScribeConfig.initial() + : AIScribeConfig.fromJson(jsonDecode(jsonString)); + } + + Future updateAIScribe(bool isEnabled) async { + final currentConfig = await getAIScribeConfig(); + final updatedConfig = currentConfig.copyWith(isEnabled: isEnabled); + await savePreferences(updatedConfig); + } } diff --git a/lib/features/manage_account/data/repository/manage_account_repository_impl.dart b/lib/features/manage_account/data/repository/manage_account_repository_impl.dart index bb5fbd0b33..9f8d75d9e9 100644 --- a/lib/features/manage_account/data/repository/manage_account_repository_impl.dart +++ b/lib/features/manage_account/data/repository/manage_account_repository_impl.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:tmail_ui_user/features/manage_account/data/datasource/manage_account_datasource.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_setting.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart'; @@ -25,4 +26,9 @@ class ManageAccountRepositoryImpl extends ManageAccountRepository { Future getLocalSettings() { return dataSource.getLocalSettings(); } + + @override + Future getAiScribeConfigLocalSettings() { + return dataSource.getAiScribeConfigLocalSettings(); + } } \ No newline at end of file diff --git a/lib/features/manage_account/domain/model/preferences/ai_scribe_config.dart b/lib/features/manage_account/domain/model/preferences/ai_scribe_config.dart new file mode 100644 index 0000000000..5d25c92bbe --- /dev/null +++ b/lib/features/manage_account/domain/model/preferences/ai_scribe_config.dart @@ -0,0 +1,34 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_config.dart'; + +part 'ai_scribe_config.g.dart'; + +@JsonSerializable() +class AIScribeConfig extends PreferencesConfig { + final bool isEnabled; + + AIScribeConfig({ + this.isEnabled = true, + }); + + factory AIScribeConfig.initial() => AIScribeConfig(); + + factory AIScribeConfig.fromJson(Map json) => + _$AIScribeConfigFromJson(json); + + @override + Map toJson() => _$AIScribeConfigToJson(this); + + @override + List get props => [isEnabled]; +} + +extension AIScribeConfigExtension on AIScribeConfig { + AIScribeConfig copyWith({ + bool? isEnabled, + }) { + return AIScribeConfig( + isEnabled: isEnabled ?? this.isEnabled, + ); + } +} diff --git a/lib/features/manage_account/domain/model/preferences/preferences_setting.dart b/lib/features/manage_account/domain/model/preferences/preferences_setting.dart index f80c63397c..3bb44930bd 100644 --- a/lib/features/manage_account/domain/model/preferences/preferences_setting.dart +++ b/lib/features/manage_account/domain/model/preferences/preferences_setting.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/spam_report_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/text_formatting_menu_config.dart'; @@ -15,6 +16,7 @@ class PreferencesSetting with EquatableMixin { ThreadDetailConfig.initial(), SpamReportConfig.initial(), TextFormattingMenuConfig.initial(), + AIScribeConfig.initial(), ]); } @@ -48,6 +50,16 @@ class PreferencesSetting with EquatableMixin { } } + AIScribeConfig get aiScribeConfig { + final aiConfig = + configs.firstWhereOrNull((config) => config is AIScribeConfig); + if (aiConfig != null) { + return aiConfig as AIScribeConfig; + } else { + return AIScribeConfig.initial(); + } + } + @override List get props => [configs]; } diff --git a/lib/features/manage_account/domain/repository/manage_account_repository.dart b/lib/features/manage_account/domain/repository/manage_account_repository.dart index f96ed179f1..ac5a758392 100644 --- a/lib/features/manage_account/domain/repository/manage_account_repository.dart +++ b/lib/features/manage_account/domain/repository/manage_account_repository.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_setting.dart'; @@ -9,4 +10,6 @@ abstract class ManageAccountRepository { Future toggleLocalSettingsState(PreferencesConfig preferencesConfig); Future getLocalSettings(); + + Future getAiScribeConfigLocalSettings(); } diff --git a/lib/features/manage_account/domain/state/get_ai_scribe_config_state.dart b/lib/features/manage_account/domain/state/get_ai_scribe_config_state.dart new file mode 100644 index 0000000000..e4ba3854ea --- /dev/null +++ b/lib/features/manage_account/domain/state/get_ai_scribe_config_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; + +class GettingAIScribeConfigState extends LoadingState {} + +class GetAIScribeConfigSuccess extends UIState { + GetAIScribeConfigSuccess(this.aiScribeConfig); + + final AIScribeConfig aiScribeConfig; + + @override + List get props => [aiScribeConfig]; +} + +class GetAIScribeConfigFailure extends FeatureFailure { + GetAIScribeConfigFailure({super.exception}); +} diff --git a/lib/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart b/lib/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart new file mode 100644 index 0000000000..37854a9428 --- /dev/null +++ b/lib/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/get_ai_scribe_config_state.dart'; + +class GetAIScribeConfigInteractor { + const GetAIScribeConfigInteractor(this._manageAccountRepository); + + final ManageAccountRepository _manageAccountRepository; + + Stream> execute() async* { + try { + yield Right(GettingAIScribeConfigState()); + final aiScribeConfig = + await _manageAccountRepository.getAiScribeConfigLocalSettings(); + yield Right(GetAIScribeConfigSuccess(aiScribeConfig)); + } catch (e) { + yield Left(GetAIScribeConfigFailure(exception: e)); + } + } +} diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index d74c43917c..11b8791842 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -12,6 +12,7 @@ import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; import 'package:jmap_dart_client/jmap/quotas/quota.dart'; import 'package:model/model.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; +import 'package:scribe/scribe/ai/presentation/utils/ai_scribe_constants.dart'; import 'package:server_settings/server_settings/capability_server_settings.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/base/mixin/ai_scribe_mixin.dart'; @@ -332,6 +333,14 @@ class ManageAccountDashBoardController extends ReloadableController } } + bool get isAICapabilitySupported { + if (accountId.value != null && sessionCurrent != null) { + return AiScribeConstants.aiCapability.isSupported(sessionCurrent!, accountId.value!); + } else { + return false; + } + } + void disableVacationResponder( VacationResponse vacation, { bool isAuto = false, diff --git a/lib/features/manage_account/presentation/model/preferences_option_type.dart b/lib/features/manage_account/presentation/model/preferences_option_type.dart index 836952fffd..adb29ff12a 100644 --- a/lib/features/manage_account/presentation/model/preferences_option_type.dart +++ b/lib/features/manage_account/presentation/model/preferences_option_type.dart @@ -8,7 +8,8 @@ enum PreferencesOptionType { readReceipt(isLocal: false), senderPriority(isLocal: false), thread(isLocal: true), - spamReport(isLocal: true); + spamReport(isLocal: true), + aiScribe(isLocal: true); final bool isLocal; @@ -24,6 +25,8 @@ enum PreferencesOptionType { return appLocalizations.thread; case PreferencesOptionType.spamReport: return appLocalizations.spamReports; + case PreferencesOptionType.aiScribe: + return appLocalizations.aiScribe; } } @@ -37,6 +40,8 @@ enum PreferencesOptionType { return appLocalizations.threadSettingExplanation; case PreferencesOptionType.spamReport: return appLocalizations.spamReportsSettingExplanation; + case PreferencesOptionType.aiScribe: + return appLocalizations.aiScribeSettingExplanation; } } @@ -50,6 +55,8 @@ enum PreferencesOptionType { return appLocalizations.threadToggleDescription; case PreferencesOptionType.spamReport: return appLocalizations.spamReportToggleDescription; + case PreferencesOptionType.aiScribe: + return appLocalizations.aiScribeToggleDescription; } } @@ -66,6 +73,8 @@ enum PreferencesOptionType { return preferencesSetting.threadConfig.isEnabled; case PreferencesOptionType.spamReport: return preferencesSetting.spamReportConfig.isEnabled; + case PreferencesOptionType.aiScribe: + return preferencesSetting.aiScribeConfig.isEnabled; } } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart b/lib/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart index 09dd63546a..8994a9cfd0 100644 --- a/lib/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart +++ b/lib/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart @@ -6,6 +6,7 @@ import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_ import 'package:tmail_ui_user/features/manage_account/data/local/preferences_setting_manager.dart'; import 'package:tmail_ui_user/features/manage_account/data/repository/manage_account_repository_impl.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/save_language_interactor.dart'; import 'package:tmail_ui_user/features/server_settings/data/datasource/server_settings_data_source.dart'; import 'package:tmail_ui_user/features/server_settings/data/datasource_impl/remote_server_settings_data_source_impl.dart'; @@ -101,5 +102,6 @@ class PreferencesInteractorsBindings extends InteractorsBindings { Get.delete(tag: composerId); Get.delete(tag: composerId); Get.delete(tag: composerId); + Get.delete(tag: composerId); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/preferences/preferences_controller.dart b/lib/features/manage_account/presentation/preferences/preferences_controller.dart index 1be4acd085..c12cb03ae4 100644 --- a/lib/features/manage_account/presentation/preferences/preferences_controller.dart +++ b/lib/features/manage_account/presentation/preferences/preferences_controller.dart @@ -7,6 +7,7 @@ import 'package:server_settings/server_settings/tmail_server_settings.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/loader_status.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/preferences_setting.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/spam_report_config.dart'; @@ -49,6 +50,10 @@ class PreferencesController extends BaseController { final _manageAccountDashBoardController = Get.find(); + bool get isAIScribeAvailable { + return _manageAccountDashBoardController.isAICapabilitySupported; + } + @override void onInit() { super.onInit(); @@ -150,6 +155,9 @@ class PreferencesController extends BaseController { case PreferencesOptionType.spamReport: config = SpamReportConfig(isEnabled: !isEnabled); break; + case PreferencesOptionType.aiScribe: + config = AIScribeConfig(isEnabled: !isEnabled); + break; default: break; } diff --git a/lib/features/manage_account/presentation/preferences/preferences_view.dart b/lib/features/manage_account/presentation/preferences/preferences_view.dart index dc51ab5b58..3266e2570f 100644 --- a/lib/features/manage_account/presentation/preferences/preferences_view.dart +++ b/lib/features/manage_account/presentation/preferences/preferences_view.dart @@ -72,7 +72,12 @@ class PreferencesView extends GetWidget with AppLoaderMix if (localSettingOption.configs.isNotEmpty) ...PreferencesOptionType.values.where( (optionType) => optionType.isLocal, - ), + ).where((optionType) { + if (optionType == PreferencesOptionType.aiScribe) { + return controller.isAIScribeAvailable; + } + return true; + }), ]; return Expanded( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1a9197881c..c1fcb50b68 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3274,5 +3274,23 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "aiScribe": "AI Scribe", + "@aiScribe": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "aiScribeSettingExplanation": "Use AI to help write and improve your emails", + "@aiScribeSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "aiScribeToggleDescription": "Enable AI Scribe", + "@aiScribeToggleDescription": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 9a6a256ffd..39d45eb311 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -5206,6 +5206,24 @@ "placeholders_order": [], "placeholders": {} }, + "aiScribe": "AI Scribe", + "@aiScribe": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "aiScribeSettingExplanation": "Utiliser l'IA pour vous aider à rédiger et améliorer vos e-mails", + "@aiScribeSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "aiScribeToggleDescription": "Activer AI Scribe", + "@aiScribeToggleDescription": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "showMoreAttachmentButton": "Afficher les {count} autres pièces jointes", "@showMoreAttachmentButton": { "type": "text", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 2edcbb7025..46e67eade3 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -5124,6 +5124,24 @@ "placeholders_order": [], "placeholders": {} }, + "aiScribe": "AI Scribe", + "@aiScribe": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "aiScribeSettingExplanation": "Используйте ИИ для помощи в написании и улучшении ваших электронных писем", + "@aiScribeSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "aiScribeToggleDescription": "Включить AI Scribe", + "@aiScribeToggleDescription": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "createANewRule": "Создать новое правило", "@createANewRule": { "type": "text", diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index c273baeadf..9491bdc315 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -5212,6 +5212,24 @@ "placeholders_order": [], "placeholders": {} }, + "aiScribe": "AI Scribe", + "@aiScribe": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "aiScribeSettingExplanation": "Sử dụng AI để giúp soạn thảo và cải thiện email của bạn", + "@aiScribeSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "aiScribeToggleDescription": "Bật AI Scribe", + "@aiScribeToggleDescription": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "showMoreAttachmentButton": "Hiển thị +{count}", "@showMoreAttachmentButton": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index ade2495b22..6dcd137263 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -5139,6 +5139,27 @@ class AppLocalizations { ); } + String get aiScribe { + return Intl.message( + 'AI Scribe', + name: 'aiScribe', + ); + } + + String get aiScribeSettingExplanation { + return Intl.message( + 'Use AI to help write and improve your emails', + name: 'aiScribeSettingExplanation', + ); + } + + String get aiScribeToggleDescription { + return Intl.message( + 'Enable AI Scribe', + name: 'aiScribeToggleDescription', + ); + } + String showMoreAttachmentButton(int count) { return Intl.message( 'Show +$count more', diff --git a/scribe/lib/scribe/ai/presentation/model/ai_capability.dart b/scribe/lib/scribe/ai/presentation/model/ai_capability.dart index 334c8395bb..748589ebf8 100644 --- a/scribe/lib/scribe/ai/presentation/model/ai_capability.dart +++ b/scribe/lib/scribe/ai/presentation/model/ai_capability.dart @@ -1,3 +1,4 @@ +import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -14,8 +15,28 @@ class AICapability extends CapabilityProperties { Map toJson() => _$AICapabilityToJson(this); - bool get isScribeEndpointAvailable => - scribeEndpoint?.trim().isNotEmpty == true; + bool get isScribeEndpointAvailable { + try { + final urlEndpoint = scribeEndpoint?.trim() ?? ''; + + if (urlEndpoint.isEmpty) return false; + + // Validate endpoint format - must be an absolute URI + if (Uri.tryParse(urlEndpoint)?.isAbsolute != true) { + logError( + 'AICapability::isScribeEndpointAvailable(): Invalid endpoint format: $urlEndpoint', + ); + return false; + } + + return true; + } catch (e) { + logError( + 'AICapability::isScribeEndpointAvailable(): Exception: $e', + ); + return false; + } + } @override List get props => [scribeEndpoint]; diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index ea8b2c1898..d47551ca9d 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -25,6 +25,7 @@ import 'package:model/email/email_action_type.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; +import 'package:scribe/scribe/ai/domain/usecases/generate_ai_text_interactor.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; @@ -54,6 +55,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/preferences/ai_scribe_config.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart'; @@ -68,7 +70,6 @@ import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; import 'package:tmail_ui_user/main/utils/twake_app_manager.dart'; import 'package:uuid/uuid.dart'; -import 'package:scribe/scribe/ai/domain/usecases/generate_ai_text_interactor.dart'; import '../../../fixtures/account_fixtures.dart'; import '../../../fixtures/session_fixtures.dart'; @@ -144,6 +145,9 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon @override RxBool get isPopupMenuOpened => false.obs; + + @override + Rx get cachedAIScribeConfig => Rx(AIScribeConfig.initial()); } @GenerateNiceMocks([