diff --git a/core/lib/data/network/dio_client.dart b/core/lib/data/network/dio_client.dart index 022f690a6..20c7928be 100644 --- a/core/lib/data/network/dio_client.dart +++ b/core/lib/data/network/dio_client.dart @@ -40,9 +40,12 @@ class DioClient { CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, + bool useJMAPHeader = true, }) async { - final newOptions = options?.appendHeaders({HttpHeaders.acceptHeader : jmapHeader}) - ?? Options(headers: {HttpHeaders.acceptHeader : jmapHeader}) ; + Map defaultHeaders = + useJMAPHeader ? {HttpHeaders.acceptHeader: jmapHeader} : {}; + final newOptions = options?.appendHeaders(defaultHeaders) ?? + Options(headers: defaultHeaders); return await _dio.post(path, data: data, diff --git a/lib/features/base/mixin/ai_scribe_mixin.dart b/lib/features/base/mixin/ai_scribe_mixin.dart index edb8133cd..2af4cfa65 100644 --- a/lib/features/base/mixin/ai_scribe_mixin.dart +++ b/lib/features/base/mixin/ai_scribe_mixin.dart @@ -1,13 +1,35 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:scribe/scribe.dart'; +import 'package:scribe/scribe/ai/presentation/bindings/ai_scribe_bindings.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; mixin AiScribeMixin { - bool isAICapabilitySupported({Session? session, AccountId? accountId}) { + AICapability? getAICapability({Session? session, AccountId? accountId}) { + if (PlatformInfo.isMobile) return null; + if (accountId == null || session == null) { - return false; + return null; + } + + return session.getAICapability(accountId); + } + + void injectAIScribeBindings(Session? session, AccountId? accountId) { + try { + final aiCapability = getAICapability( + session: session, + accountId: accountId, + ); + final scribeEndpoint = aiCapability?.scribeEndpoint; + + if (scribeEndpoint == null || scribeEndpoint.isEmpty) return; + + AIScribeBindings(scribeEndpoint).dependencies(); + } catch (e) { + logError('AiScribeMixin::injectAIScribeBindings(): $e'); } - final aiCapability = session.getAICapability(accountId); - return aiCapability != null; } } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index d37c1f522..e30528d12 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -263,8 +263,6 @@ class ComposerController extends BaseController TransformHtmlEmailContentInteractor get transformHtmlEmailContentInteractor => _transformHtmlEmailContentInteractor; - GenerateAITextInteractor get generateAITextInteractor => Get.find(); - String get ownEmailAddress => mailboxDashBoardController.ownEmailAddress.value; 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 1390ab995..5a4d8545f 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 @@ -10,10 +10,11 @@ import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selectio extension HandleAiScribeInComposerExtension on ComposerController { bool get isAIScribeAvailable { - return isAICapabilitySupported( + final aiCapability = getAICapability( session: mailboxDashBoardController.sessionCurrent, accountId: mailboxDashBoardController.accountId.value, ); + return aiCapability?.isScribeEndpointAvailable == true; } Future _getTextOnlyContentInEditor() async { 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 6afb676b7..5fead10f9 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -31,6 +31,7 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rxdart/transformers.dart'; import 'package:server_settings/server_settings/tmail_server_settings_extension.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; +import 'package:tmail_ui_user/features/base/mixin/ai_scribe_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart'; import 'package:tmail_ui_user/features/base/mixin/own_email_address_mixin.dart'; @@ -78,15 +79,8 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/email_recovery/presentation/model/email_recovery_arguments.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; -import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart'; import 'package:tmail_ui_user/features/home/domain/usecases/store_session_interactor.dart'; -import 'package:scribe/scribe/ai/data/datasource/ai_datasource.dart'; -import 'package:scribe/scribe/ai/data/datasource_impl/ai_datasource_impl.dart'; -import 'package:scribe/scribe/ai/data/repository/ai_repository_impl.dart'; -import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.dart'; -import 'package:scribe/scribe/ai/domain/usecases/generate_ai_text_interactor.dart'; -import 'package:scribe/scribe/ai/presentation/bindings/ai_scribe_bindings.dart'; import 'package:tmail_ui_user/features/identity_creator/domain/state/get_identity_cache_on_web_state.dart'; import 'package:tmail_ui_user/features/identity_creator/domain/usecase/get_identity_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.dart'; @@ -223,7 +217,8 @@ import 'package:uuid/uuid.dart'; class MailboxDashBoardController extends ReloadableController with ContactSupportMixin, OwnEmailAddressMixin, - SaaSPremiumMixin { + SaaSPremiumMixin, + AiScribeMixin { final RemoveEmailDraftsInteractor _removeEmailDraftsInteractor = Get.find(); final EmailReceiveManager _emailReceiveManager = Get.find(); @@ -825,41 +820,6 @@ class MailboxDashBoardController extends ReloadableController } } - void injectAIScribeBindings(Session? session, AccountId? accountId) { - try { - if (!PlatformInfo.isWeb) return; - - if (session == null || accountId == null) return; - - final aiCapability = session.getAICapability(accountId); - final scribeEndpoint = aiCapability?.scribeEndpoint; - - if (scribeEndpoint == null || scribeEndpoint.isEmpty) return; - - // Delete existing AI bindings if they exist - if (Get.isRegistered()) { - Get.delete(force: true); - } - if (Get.isRegistered()) { - Get.delete(force: true); - } - if (Get.isRegistered()) { - Get.delete(force: true); - } - if (Get.isRegistered()) { - Get.delete(force: true); - } - if (Get.isRegistered()) { - Get.delete(force: true); - } - - // Reinitialize with the correct endpoint - AIScribeBindings(scribeEndpoint: scribeEndpoint).dependencies(); - } catch (e) { - logError('MailboxDashBoardController::injectAIScribeBindings(): $e'); - } - } - @override Future injectFCMBindings(Session? session, AccountId? accountId) async { try { 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 6b9db045a..d74c43917 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -14,6 +14,7 @@ import 'package:model/model.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.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'; import 'package:tmail_ui_user/features/base/mixin/own_email_address_mixin.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/base/widget/dialog_picker/color_dialog_picker.dart'; @@ -55,7 +56,7 @@ import 'package:tmail_ui_user/main/routes/route_utils.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; class ManageAccountDashBoardController extends ReloadableController - with OwnEmailAddressMixin { + with OwnEmailAddressMixin, AiScribeMixin { GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -152,6 +153,7 @@ class ManageAccountDashBoardController extends ReloadableController _setUpMinInputLengthAutocomplete(); _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); + injectAIScribeBindings(sessionCurrent, accountId.value); paywallController = PaywallController( ownEmailAddress: ownEmailAddress.value, ); diff --git a/scribe/lib/scribe/ai/data/datasource/ai_datasource.dart b/scribe/lib/scribe/ai/data/datasource/ai_datasource.dart index 70f7409c9..73af09b4d 100644 --- a/scribe/lib/scribe/ai/data/datasource/ai_datasource.dart +++ b/scribe/lib/scribe/ai/data/datasource/ai_datasource.dart @@ -1,5 +1,5 @@ import 'package:scribe/scribe/ai/domain/model/ai_response.dart'; abstract class AIDataSource { - Future request(String prompt); + Future generateMessage(String prompt); } diff --git a/scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart b/scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart index c65951a26..00130be62 100644 --- a/scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart +++ b/scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart @@ -1,34 +1,17 @@ import 'package:dio/dio.dart'; import 'package:scribe/scribe/ai/data/datasource/ai_datasource.dart'; -import 'package:scribe/scribe/ai/data/model/ai_message.dart'; -import 'package:scribe/scribe/ai/data/model/ai_api_request.dart'; import 'package:scribe/scribe/ai/data/network/ai_api.dart'; import 'package:scribe/scribe/ai/domain/model/ai_response.dart'; class AIDataSourceImpl implements AIDataSource { final AIApi _aiApi; - AIDataSourceImpl({ - required Dio dio, - String? endpoint, - }) : _aiApi = AIApi( - dio: dio, - endpoint: endpoint, - ); + AIDataSourceImpl(this._aiApi); @override - Future request(String prompt) async { + Future generateMessage(String prompt) async { try { - final aiRequest = AIAPIRequest( - messages: [ - AIMessage( - role: 'user', - content: prompt, - ), - ], - ); - - final apiResponse = await _aiApi.chatCompletion(aiRequest); + final apiResponse = await _aiApi.generateMessage(prompt); return AIResponse(result: apiResponse.content); } on DioError catch (e) { throw Exception('Failed to generate AI text: ${e.message}'); diff --git a/scribe/lib/scribe/ai/data/model/ai_message.dart b/scribe/lib/scribe/ai/data/model/ai_message.dart index 9f318800c..67d2482a8 100644 --- a/scribe/lib/scribe/ai/data/model/ai_message.dart +++ b/scribe/lib/scribe/ai/data/model/ai_message.dart @@ -12,7 +12,13 @@ class AIMessage { required this.content, }); - factory AIMessage.fromJson(Map json) => _$AIMessageFromJson(json); + factory AIMessage.fromJson(Map json) => + _$AIMessageFromJson(json); Map toJson() => _$AIMessageToJson(this); + + factory AIMessage.ofUser(String content) => AIMessage( + role: 'user', + content: content, + ); } diff --git a/scribe/lib/scribe/ai/data/network/ai_api.dart b/scribe/lib/scribe/ai/data/network/ai_api.dart index ecae0061c..567de494c 100644 --- a/scribe/lib/scribe/ai/data/network/ai_api.dart +++ b/scribe/lib/scribe/ai/data/network/ai_api.dart @@ -1,30 +1,22 @@ -import 'package:dio/dio.dart'; -import 'package:scribe/scribe/ai/data/model/ai_api_response.dart'; +import 'package:core/data/network/dio_client.dart'; import 'package:scribe/scribe/ai/data/model/ai_api_request.dart'; +import 'package:scribe/scribe/ai/data/model/ai_api_response.dart'; +import 'package:scribe/scribe/ai/data/model/ai_message.dart'; import 'package:scribe/scribe/ai/data/network/ai_api_exception.dart'; class AIApi { - final Dio _dio; - final String? _endpoint; + final DioClient _dioClient; + final String aiEndpoint; - AIApi({ - required Dio dio, - String? endpoint, - }) : _dio = dio, - _endpoint = endpoint; + AIApi(this._dioClient, this.aiEndpoint); - Future chatCompletion( - AIAPIRequest request - ) async { - final url = _endpoint; + Future generateMessage(String prompt) async { + final aiRequest = _generateRequest(prompt); - if (url == null) { - throw AIApiNotAvailableException(); - } - - final response = await _dio.post( - url, - data: request.toJson(), + final response = await _dioClient.post( + aiEndpoint, + data: aiRequest.toJson(), + useJMAPHeader: false, ); if (response.statusCode == 200) { @@ -39,4 +31,8 @@ class AIApi { ); } } + + AIAPIRequest _generateRequest(String prompt) { + return AIAPIRequest(messages: [AIMessage.ofUser(prompt)]); + } } diff --git a/scribe/lib/scribe/ai/data/repository/ai_repository_impl.dart b/scribe/lib/scribe/ai/data/repository/ai_repository_impl.dart index a88c5ec6b..f3d6115f1 100644 --- a/scribe/lib/scribe/ai/data/repository/ai_repository_impl.dart +++ b/scribe/lib/scribe/ai/data/repository/ai_repository_impl.dart @@ -3,12 +3,12 @@ import 'package:scribe/scribe/ai/domain/model/ai_response.dart'; import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.dart'; class AIScribeRepositoryImpl implements AIScribeRepository { - final AIDataSource _dataSource; + final AIDataSource _aiDataSource; - AIScribeRepositoryImpl(this._dataSource); + AIScribeRepositoryImpl(this._aiDataSource); @override - Future generateText(String prompt) async { - return _dataSource.request(prompt); + Future generateMessage(String prompt) { + return _aiDataSource.generateMessage(prompt); } } diff --git a/scribe/lib/scribe/ai/domain/repository/ai_scribe_repository.dart b/scribe/lib/scribe/ai/domain/repository/ai_scribe_repository.dart index 8942eaa7d..2412bf758 100644 --- a/scribe/lib/scribe/ai/domain/repository/ai_scribe_repository.dart +++ b/scribe/lib/scribe/ai/domain/repository/ai_scribe_repository.dart @@ -1,5 +1,5 @@ import 'package:scribe/scribe/ai/domain/model/ai_response.dart'; abstract class AIScribeRepository { - Future generateText(String prompt); + Future generateMessage(String prompt); } diff --git a/scribe/lib/scribe/ai/domain/usecases/generate_ai_text_interactor.dart b/scribe/lib/scribe/ai/domain/usecases/generate_ai_text_interactor.dart index 81766711c..8bd3fff26 100644 --- a/scribe/lib/scribe/ai/domain/usecases/generate_ai_text_interactor.dart +++ b/scribe/lib/scribe/ai/domain/usecases/generate_ai_text_interactor.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:scribe/scribe/ai/domain/constants/ai_prompts.dart'; import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.dart'; @@ -17,9 +18,8 @@ class GenerateAITextInteractor { ) async { try { final prompt = AIPrompts.buildPrompt(action, selectedText); - - final response = await _repository.generateText(prompt); - + log('$runtimeType::execute():prompt: $prompt'); + final response = await _repository.generateMessage(prompt); return Right(GenerateAITextSuccess(response)); } catch (e) { return Left(GenerateAITextFailure(e)); diff --git a/scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart b/scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart index 4f938bb92..93c919fb3 100644 --- a/scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart +++ b/scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart @@ -1,18 +1,21 @@ -import 'package:dio/dio.dart'; +import 'package:core/core.dart'; import 'package:get/get.dart'; import 'package:scribe/scribe/ai/data/datasource/ai_datasource.dart'; import 'package:scribe/scribe/ai/data/datasource_impl/ai_datasource_impl.dart'; +import 'package:scribe/scribe/ai/data/network/ai_api.dart'; import 'package:scribe/scribe/ai/data/repository/ai_repository_impl.dart'; import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.dart'; import 'package:scribe/scribe/ai/domain/usecases/generate_ai_text_interactor.dart'; class AIScribeBindings extends Bindings { - final String? scribeEndpoint; - AIScribeBindings({this.scribeEndpoint}); + final String aiEndpoint; + + AIScribeBindings(this.aiEndpoint); @override void dependencies() { + _bindingsAPI(); _bindingsDataSourceImpl(); _bindingsDataSource(); _bindingsRepositoryImpl(); @@ -20,11 +23,12 @@ class AIScribeBindings extends Bindings { _bindingsInteractor(); } + void _bindingsAPI() { + Get.lazyPut(() => AIApi(Get.find(), aiEndpoint)); + } + void _bindingsDataSourceImpl() { - Get.lazyPut(() => AIDataSourceImpl( - dio: Get.find(), - endpoint: scribeEndpoint, - )); + Get.lazyPut(() => AIDataSourceImpl(Get.find())); } void _bindingsDataSource() { @@ -32,9 +36,9 @@ class AIScribeBindings extends Bindings { } void _bindingsRepositoryImpl() { - Get.lazyPut(() => AIScribeRepositoryImpl( - Get.find(), - )); + Get.lazyPut( + () => AIScribeRepositoryImpl(Get.find()), + ); } void _bindingsRepository() { @@ -42,8 +46,8 @@ class AIScribeBindings extends Bindings { } void _bindingsInteractor() { - Get.lazyPut(() => GenerateAITextInteractor( - Get.find(), - )); + Get.lazyPut( + () => GenerateAITextInteractor(Get.find()), + ); } } diff --git a/scribe/lib/scribe/ai/presentation/model/ai_capability.dart b/scribe/lib/scribe/ai/presentation/model/ai_capability.dart index 657316cd7..652047264 100644 --- a/scribe/lib/scribe/ai/presentation/model/ai_capability.dart +++ b/scribe/lib/scribe/ai/presentation/model/ai_capability.dart @@ -15,6 +15,8 @@ class AICapability extends CapabilityProperties { factory AICapability.deserialize(Map json) => _$AICapabilityFromJson(json); + bool get isScribeEndpointAvailable => scribeEndpoint?.trim().isNotEmpty == true; + @override List get props => [scribeEndpoint]; } diff --git a/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart b/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart index 071e44a5b..0cd8be6fd 100644 --- a/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart +++ b/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart @@ -66,7 +66,7 @@ class _AiScribeSuggestionWidgetModalState } } - void _getAiSuggestion( + Future _getAiSuggestion( GenerateAITextInteractor interactor, AIAction action, String? content,