From 8039ccbb68fcf10b0afa5f8322d18d8eef9d2e34 Mon Sep 17 00:00:00 2001 From: "Dat H. Pham" Date: Fri, 19 Dec 2025 10:17:49 +0700 Subject: [PATCH 1/8] AIConfig model --- .../local/preferences_setting_manager.dart | 30 ++++++++++++++++ .../model/preferences/ai_scribe_config.dart | 34 +++++++++++++++++++ .../preferences/preferences_setting.dart | 12 +++++++ 3 files changed, 76 insertions(+) create mode 100644 lib/features/manage_account/domain/model/preferences/ai_scribe_config.dart 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..42024bf563 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,24 @@ 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/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]; } From 3f392241cbd80eb7297d2c4a1b9e1a038e4ecf9d Mon Sep 17 00:00:00 2001 From: "Dat H. Pham" Date: Fri, 19 Dec 2025 10:21:18 +0700 Subject: [PATCH 2/8] Interactor getAIScribeConfig --- .../state/get_ai_scribe_config_state.dart | 18 ++++++++++++++++ .../get_ai_scribe_config_interactor.dart | 21 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 lib/features/manage_account/domain/state/get_ai_scribe_config_state.dart create mode 100644 lib/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart 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..82d7d36598 --- /dev/null +++ b/lib/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart @@ -0,0 +1,21 @@ +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 preferencesSetting = await _manageAccountRepository.getLocalSettings(); + yield Right(GetAIScribeConfigSuccess(preferencesSetting.aiScribeConfig)); + } catch (e) { + yield Left(GetAIScribeConfigFailure(exception: e)); + } + } +} From 95ab7372492f5d90681558114814a554dbd71305 Mon Sep 17 00:00:00 2001 From: "Dat H. Pham" Date: Fri, 19 Dec 2025 10:41:06 +0700 Subject: [PATCH 3/8] Display AI Scribe togge in Preference --- .../manage_account_datasource_impl.dart | 5 +++++ .../manage_account_dashboard_controller.dart | 9 +++++++++ .../presentation/model/preferences_option_type.dart | 11 ++++++++++- .../bindings/preferences_interactors_bindings.dart | 6 ++++++ .../preferences/preferences_controller.dart | 8 ++++++++ .../presentation/preferences/preferences_view.dart | 7 ++++++- 6 files changed, 44 insertions(+), 2 deletions(-) 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..45db1a9169 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, 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..bd6c5ff945 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'; @@ -62,6 +63,10 @@ class PreferencesInteractorsBindings extends InteractorsBindings { () => SaveLanguageInteractor(Get.find(tag: composerId)), tag: composerId, ); + Get.lazyPut( + () => GetAIScribeConfigInteractor(Get.find(tag: composerId)), + tag: composerId, + ); } @override @@ -101,5 +106,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( From 66565e3637229c80fcaaa2de526fe50aff3f48c3 Mon Sep 17 00:00:00 2001 From: "Dat H. Pham" Date: Fri, 19 Dec 2025 10:55:21 +0700 Subject: [PATCH 4/8] Display AI toggle in Composer --- .../presentation/composer_bindings.dart | 2 ++ .../presentation/composer_controller.dart | 15 +++++++++++++ lib/l10n/intl_fr.arb | 18 ++++++++++++++++ lib/l10n/intl_ru.arb | 18 ++++++++++++++++ lib/l10n/intl_vi.arb | 18 ++++++++++++++++ lib/main/localizations/app_localizations.dart | 21 +++++++++++++++++++ 6 files changed, 92 insertions(+) diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 27cc6b6ad9..54cb30ca70 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -54,6 +54,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/se import 'package:tmail_ui_user/features/mailbox_dashboard/data/repository/composer_cache_repository_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_by_id_on_web_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/presentation/identities/identity_interactors_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart'; @@ -338,6 +339,7 @@ class ComposerBindings extends BaseBindings { Get.find(tag: composerId), Get.find(tag: composerId), Get.find(tag: composerId), + Get.find(tag: composerId), composerId: composerId, composerArgs: composerArguments, ), tag: composerId); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index e30528d124..8f0f3f120f 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -107,7 +107,10 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_text_formatting_menu_state_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/validate_premium_storage_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.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/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/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/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' @@ -161,6 +164,7 @@ class ComposerController extends BaseController final isMarkAsImportant = Rx(false); final isContentHeightExceeded = Rx(false); final editorTextSelection = Rxn(); + final _cachedAIScribeConfig = Rx(AIScribeConfig.initial()); final LocalFilePickerInteractor _localFilePickerInteractor; final LocalImagePickerInteractor _localImagePickerInteractor; @@ -179,6 +183,7 @@ class ComposerController extends BaseController final String? composerId; final ComposerArguments? composerArgs; final SaveTemplateEmailInteractor _saveTemplateEmailInteractor; + final GetAIScribeConfigInteractor _getAIScribeConfigInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -266,6 +271,8 @@ class ComposerController extends BaseController String get ownEmailAddress => mailboxDashBoardController.ownEmailAddress.value; + AIScribeConfig get aiScribeConfig => _cachedAIScribeConfig.value; + late Worker uploadInlineImageWorker; late bool _isEmailBodyLoaded; @@ -285,6 +292,7 @@ class ComposerController extends BaseController this.printEmailInteractor, this._composerRepository, this._saveTemplateEmailInteractor, + this._getAIScribeConfigInteractor, { this.composerId, this.composerArgs, @@ -308,6 +316,11 @@ class ComposerController extends BaseController _beforeReconnectManager.addListener(onBeforeReconnect); _injectBinding(); onKeyboardShortcutInit(); + _loadAIScribeConfig(); + } + + void _loadAIScribeConfig() { + consumeState(_getAIScribeConfigInteractor.execute()); } @override @@ -408,6 +421,8 @@ class ComposerController extends BaseController richTextMobileTabletController?.insertImage(inlineImage); } maxWithEditor = null; + } else if (success is GetAIScribeConfigSuccess) { + _cachedAIScribeConfig.value = success.aiScribeConfig; } else { super.handleSuccessViewState(success); } 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', From 37ed12e3d143c9f487ebee773d79ed6c90216074 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 19 Dec 2025 11:48:56 +0700 Subject: [PATCH 5/8] Enable AI Scribe modal based on configuration and availability --- lib/features/composer/presentation/composer_view.dart | 2 +- lib/features/composer/presentation/composer_view_web.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 69f262cdde..556a512b34 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -470,7 +470,7 @@ class ComposerView extends GetWidget { sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), - onOpenAiAssistantModal: controller.isAIScribeAvailable + onOpenAiAssistantModal: controller.aiScribeConfig.isEnabled && controller.isAIScribeAvailable ? controller.openAIAssistantModal : null, )), diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index a2db486c3f..65653bc222 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -560,7 +560,7 @@ class ComposerView extends GetWidget { toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), onOpenInsertLink: controller.openInsertLink, - onOpenAiAssistantModal: controller.isAIScribeAvailable + onOpenAiAssistantModal: controller.isAIScribeAvailable && controller.aiScribeConfig.isEnabled ? controller.openAIAssistantModal : null, )), From 1da33950f18475e4398bef9614c74f6fa48dd766 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 19 Dec 2025 12:38:11 +0700 Subject: [PATCH 6/8] Update environment configuration for composer_controller_test.dart --- .../composer/presentation/composer_controller_test.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index ea8b2c1898..2a258e76c7 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -54,6 +54,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/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/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart'; @@ -180,6 +181,7 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon MockSpec(), MockSpec(), MockSpec(), + MockSpec(), // Additional Getx dependencies mock specs MockSpec(fallbackGenerators: fallbackGenerators), @@ -225,6 +227,7 @@ void main() { late MockPrintEmailInteractor mockPrintEmailInteractor; late MockComposerRepository mockComposerRepository; late MockSaveTemplateEmailInteractor mockSaveTemplateEmailInteractor; + late MockGetAIScribeConfigInteractor mockGetAIScribeConfigInteractor; // Declaration Getx dependencies final mockMailboxDashBoardController = MockMailboxDashBoardController(); @@ -296,6 +299,7 @@ void main() { mockPrintEmailInteractor = MockPrintEmailInteractor(); mockComposerRepository = MockComposerRepository(); mockSaveTemplateEmailInteractor = MockSaveTemplateEmailInteractor(); + mockGetAIScribeConfigInteractor = MockGetAIScribeConfigInteractor(); composerController = ComposerController( mockLocalFilePickerInteractor, @@ -313,6 +317,7 @@ void main() { mockPrintEmailInteractor, mockComposerRepository, mockSaveTemplateEmailInteractor, + mockGetAIScribeConfigInteractor, ); mockHtmlEditorApi = MockHtmlEditorApi(); From 9603bd156e8665f8f678f502ebc6f8c53dc4db0e Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 19 Dec 2025 15:05:41 +0700 Subject: [PATCH 7/8] feat(ai-scribe): Load AI scribe config in dashboard --- lib/features/base/mixin/ai_scribe_mixin.dart | 13 ++------ .../presentation/composer_bindings.dart | 2 -- .../presentation/composer_controller.dart | 15 ---------- .../composer/presentation/composer_view.dart | 2 +- .../presentation/composer_view_web.dart | 2 +- ...andle_ai_scribe_in_composer_extension.dart | 8 ++++- .../bindings/mailbox_dashboard_bindings.dart | 7 +++++ .../mailbox_dashboard_controller.dart | 19 ++++++++++-- .../setup_cached_ai_scribe_extension.dart | 30 +++++++++++++++++++ .../datasource/manage_account_datasource.dart | 3 ++ .../manage_account_datasource_impl.dart | 7 +++++ .../local/preferences_setting_manager.dart | 2 -- .../manage_account_repository_impl.dart | 6 ++++ .../repository/manage_account_repository.dart | 3 ++ .../get_ai_scribe_config_interactor.dart | 5 ++-- .../preferences_interactors_bindings.dart | 4 --- .../ai/presentation/model/ai_capability.dart | 25 ++++++++++++++-- .../composer_controller_test.dart | 11 ++++--- 18 files changed, 114 insertions(+), 50 deletions(-) create mode 100644 lib/features/mailbox_dashboard/presentation/extensions/ai_scribe/setup_cached_ai_scribe_extension.dart 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/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 54cb30ca70..27cc6b6ad9 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -54,7 +54,6 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/se import 'package:tmail_ui_user/features/mailbox_dashboard/data/repository/composer_cache_repository_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_by_id_on_web_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/presentation/identities/identity_interactors_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart'; @@ -339,7 +338,6 @@ class ComposerBindings extends BaseBindings { Get.find(tag: composerId), Get.find(tag: composerId), Get.find(tag: composerId), - Get.find(tag: composerId), composerId: composerId, composerArgs: composerArguments, ), tag: composerId); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 8f0f3f120f..e30528d124 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -107,10 +107,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_text_formatting_menu_state_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/validate_premium_storage_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.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/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/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/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' @@ -164,7 +161,6 @@ class ComposerController extends BaseController final isMarkAsImportant = Rx(false); final isContentHeightExceeded = Rx(false); final editorTextSelection = Rxn(); - final _cachedAIScribeConfig = Rx(AIScribeConfig.initial()); final LocalFilePickerInteractor _localFilePickerInteractor; final LocalImagePickerInteractor _localImagePickerInteractor; @@ -183,7 +179,6 @@ class ComposerController extends BaseController final String? composerId; final ComposerArguments? composerArgs; final SaveTemplateEmailInteractor _saveTemplateEmailInteractor; - final GetAIScribeConfigInteractor _getAIScribeConfigInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -271,8 +266,6 @@ class ComposerController extends BaseController String get ownEmailAddress => mailboxDashBoardController.ownEmailAddress.value; - AIScribeConfig get aiScribeConfig => _cachedAIScribeConfig.value; - late Worker uploadInlineImageWorker; late bool _isEmailBodyLoaded; @@ -292,7 +285,6 @@ class ComposerController extends BaseController this.printEmailInteractor, this._composerRepository, this._saveTemplateEmailInteractor, - this._getAIScribeConfigInteractor, { this.composerId, this.composerArgs, @@ -316,11 +308,6 @@ class ComposerController extends BaseController _beforeReconnectManager.addListener(onBeforeReconnect); _injectBinding(); onKeyboardShortcutInit(); - _loadAIScribeConfig(); - } - - void _loadAIScribeConfig() { - consumeState(_getAIScribeConfigInteractor.execute()); } @override @@ -421,8 +408,6 @@ class ComposerController extends BaseController richTextMobileTabletController?.insertImage(inlineImage); } maxWithEditor = null; - } else if (success is GetAIScribeConfigSuccess) { - _cachedAIScribeConfig.value = success.aiScribeConfig; } else { super.handleSuccessViewState(success); } diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 556a512b34..69f262cdde 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -470,7 +470,7 @@ class ComposerView extends GetWidget { sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), - onOpenAiAssistantModal: controller.aiScribeConfig.isEnabled && controller.isAIScribeAvailable + onOpenAiAssistantModal: controller.isAIScribeAvailable ? controller.openAIAssistantModal : null, )), diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 65653bc222..a2db486c3f 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -560,7 +560,7 @@ class ComposerView extends GetWidget { toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), onOpenInsertLink: controller.openInsertLink, - onOpenAiAssistantModal: controller.isAIScribeAvailable && controller.aiScribeConfig.isEnabled + onOpenAiAssistantModal: controller.isAIScribeAvailable ? controller.openAIAssistantModal : null, )), 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 45db1a9169..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 @@ -64,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 42024bf563..5a7269411c 100644 --- a/lib/features/manage_account/data/local/preferences_setting_manager.dart +++ b/lib/features/manage_account/data/local/preferences_setting_manager.dart @@ -166,6 +166,4 @@ class PreferencesSettingManager { 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/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/usecases/get_ai_scribe_config_interactor.dart b/lib/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart index 82d7d36598..37854a9428 100644 --- 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 @@ -12,8 +12,9 @@ class GetAIScribeConfigInteractor { Stream> execute() async* { try { yield Right(GettingAIScribeConfigState()); - final preferencesSetting = await _manageAccountRepository.getLocalSettings(); - yield Right(GetAIScribeConfigSuccess(preferencesSetting.aiScribeConfig)); + final aiScribeConfig = + await _manageAccountRepository.getAiScribeConfigLocalSettings(); + yield Right(GetAIScribeConfigSuccess(aiScribeConfig)); } catch (e) { yield Left(GetAIScribeConfigFailure(exception: e)); } 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 bd6c5ff945..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 @@ -63,10 +63,6 @@ class PreferencesInteractorsBindings extends InteractorsBindings { () => SaveLanguageInteractor(Get.find(tag: composerId)), tag: composerId, ); - Get.lazyPut( - () => GetAIScribeConfigInteractor(Get.find(tag: composerId)), - tag: composerId, - ); } @override 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 2a258e76c7..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,7 +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/usecases/get_ai_scribe_config_interactor.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'; @@ -69,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'; @@ -145,6 +145,9 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon @override RxBool get isPopupMenuOpened => false.obs; + + @override + Rx get cachedAIScribeConfig => Rx(AIScribeConfig.initial()); } @GenerateNiceMocks([ @@ -181,7 +184,6 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon MockSpec(), MockSpec(), MockSpec(), - MockSpec(), // Additional Getx dependencies mock specs MockSpec(fallbackGenerators: fallbackGenerators), @@ -227,7 +229,6 @@ void main() { late MockPrintEmailInteractor mockPrintEmailInteractor; late MockComposerRepository mockComposerRepository; late MockSaveTemplateEmailInteractor mockSaveTemplateEmailInteractor; - late MockGetAIScribeConfigInteractor mockGetAIScribeConfigInteractor; // Declaration Getx dependencies final mockMailboxDashBoardController = MockMailboxDashBoardController(); @@ -299,7 +300,6 @@ void main() { mockPrintEmailInteractor = MockPrintEmailInteractor(); mockComposerRepository = MockComposerRepository(); mockSaveTemplateEmailInteractor = MockSaveTemplateEmailInteractor(); - mockGetAIScribeConfigInteractor = MockGetAIScribeConfigInteractor(); composerController = ComposerController( mockLocalFilePickerInteractor, @@ -317,7 +317,6 @@ void main() { mockPrintEmailInteractor, mockComposerRepository, mockSaveTemplateEmailInteractor, - mockGetAIScribeConfigInteractor, ); mockHtmlEditorApi = MockHtmlEditorApi(); From 6ddefbe80f1a056071acc22f64ae7ecadc4194ec Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 19 Dec 2025 15:09:56 +0700 Subject: [PATCH 8/8] feat(ai-scribe): Fix the English locale file ``(intl_en.arb)`` is missing all three AI Scribe keys --- lib/l10n/intl_en.arb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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": {} } }