feat(ai-scribe): Improve data & domain layer for ai scribe

This commit is contained in:
dab246
2025-12-18 04:31:18 +07:00
committed by Dat H. Pham
parent 1a1b8f489f
commit 860393fc90
16 changed files with 94 additions and 117 deletions
+5 -2
View File
@@ -40,9 +40,12 @@ class DioClient {
CancelToken? cancelToken, CancelToken? cancelToken,
ProgressCallback? onSendProgress, ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress, ProgressCallback? onReceiveProgress,
bool useJMAPHeader = true,
}) async { }) async {
final newOptions = options?.appendHeaders({HttpHeaders.acceptHeader : jmapHeader}) Map<String, dynamic> defaultHeaders =
?? Options(headers: {HttpHeaders.acceptHeader : jmapHeader}) ; useJMAPHeader ? {HttpHeaders.acceptHeader: jmapHeader} : {};
final newOptions = options?.appendHeaders(defaultHeaders) ??
Options(headers: defaultHeaders);
return await _dio.post(path, return await _dio.post(path,
data: data, data: data,
+26 -4
View File
@@ -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/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.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'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
mixin AiScribeMixin { mixin AiScribeMixin {
bool isAICapabilitySupported({Session? session, AccountId? accountId}) { AICapability? getAICapability({Session? session, AccountId? accountId}) {
if (PlatformInfo.isMobile) return null;
if (accountId == null || session == 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;
} }
} }
@@ -263,8 +263,6 @@ class ComposerController extends BaseController
TransformHtmlEmailContentInteractor get transformHtmlEmailContentInteractor => _transformHtmlEmailContentInteractor; TransformHtmlEmailContentInteractor get transformHtmlEmailContentInteractor => _transformHtmlEmailContentInteractor;
GenerateAITextInteractor get generateAITextInteractor => Get.find<GenerateAITextInteractor>();
String get ownEmailAddress => String get ownEmailAddress =>
mailboxDashBoardController.ownEmailAddress.value; mailboxDashBoardController.ownEmailAddress.value;
@@ -10,10 +10,11 @@ import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selectio
extension HandleAiScribeInComposerExtension on ComposerController { extension HandleAiScribeInComposerExtension on ComposerController {
bool get isAIScribeAvailable { bool get isAIScribeAvailable {
return isAICapabilitySupported( final aiCapability = getAICapability(
session: mailboxDashBoardController.sessionCurrent, session: mailboxDashBoardController.sessionCurrent,
accountId: mailboxDashBoardController.accountId.value, accountId: mailboxDashBoardController.accountId.value,
); );
return aiCapability?.isScribeEndpointAvailable == true;
} }
Future<String> _getTextOnlyContentInEditor() async { Future<String> _getTextOnlyContentInEditor() async {
@@ -31,6 +31,7 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:rxdart/transformers.dart'; import 'package:rxdart/transformers.dart';
import 'package:server_settings/server_settings/tmail_server_settings_extension.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/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/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/message_dialog_action_manager.dart';
import 'package:tmail_ui_user/features/base/mixin/own_email_address_mixin.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/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/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/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/state/auto_sign_in_via_deep_link_state.dart';
import 'package:tmail_ui_user/features/home/domain/usecases/store_session_interactor.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/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/identity_creator/domain/usecase/get_identity_cache_on_web_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.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 class MailboxDashBoardController extends ReloadableController
with ContactSupportMixin, with ContactSupportMixin,
OwnEmailAddressMixin, OwnEmailAddressMixin,
SaaSPremiumMixin { SaaSPremiumMixin,
AiScribeMixin {
final RemoveEmailDraftsInteractor _removeEmailDraftsInteractor = Get.find<RemoveEmailDraftsInteractor>(); final RemoveEmailDraftsInteractor _removeEmailDraftsInteractor = Get.find<RemoveEmailDraftsInteractor>();
final EmailReceiveManager _emailReceiveManager = Get.find<EmailReceiveManager>(); final EmailReceiveManager _emailReceiveManager = Get.find<EmailReceiveManager>();
@@ -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<AIDataSourceImpl>()) {
Get.delete<AIDataSourceImpl>(force: true);
}
if (Get.isRegistered<AIDataSource>()) {
Get.delete<AIDataSource>(force: true);
}
if (Get.isRegistered<AIScribeRepositoryImpl>()) {
Get.delete<AIScribeRepositoryImpl>(force: true);
}
if (Get.isRegistered<AIScribeRepository>()) {
Get.delete<AIScribeRepository>(force: true);
}
if (Get.isRegistered<GenerateAITextInteractor>()) {
Get.delete<GenerateAITextInteractor>(force: true);
}
// Reinitialize with the correct endpoint
AIScribeBindings(scribeEndpoint: scribeEndpoint).dependencies();
} catch (e) {
logError('MailboxDashBoardController::injectAIScribeBindings(): $e');
}
}
@override @override
Future<void> injectFCMBindings(Session? session, AccountId? accountId) async { Future<void> injectFCMBindings(Session? session, AccountId? accountId) async {
try { try {
@@ -14,6 +14,7 @@ import 'package:model/model.dart';
import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart';
import 'package:server_settings/server_settings/capability_server_settings.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/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/mixin/own_email_address_mixin.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.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'; 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'; import 'package:tmail_ui_user/main/utils/app_config.dart';
class ManageAccountDashBoardController extends ReloadableController class ManageAccountDashBoardController extends ReloadableController
with OwnEmailAddressMixin { with OwnEmailAddressMixin, AiScribeMixin {
GetAllVacationInteractor? _getAllVacationInteractor; GetAllVacationInteractor? _getAllVacationInteractor;
UpdateVacationInteractor? _updateVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor;
@@ -152,6 +153,7 @@ class ManageAccountDashBoardController extends ReloadableController
_setUpMinInputLengthAutocomplete(); _setUpMinInputLengthAutocomplete();
_bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _bindingInteractorForMenuItemView(sessionCurrent, accountId.value);
_getVacationResponse(); _getVacationResponse();
injectAIScribeBindings(sessionCurrent, accountId.value);
paywallController = PaywallController( paywallController = PaywallController(
ownEmailAddress: ownEmailAddress.value, ownEmailAddress: ownEmailAddress.value,
); );
@@ -1,5 +1,5 @@
import 'package:scribe/scribe/ai/domain/model/ai_response.dart'; import 'package:scribe/scribe/ai/domain/model/ai_response.dart';
abstract class AIDataSource { abstract class AIDataSource {
Future<AIResponse> request(String prompt); Future<AIResponse> generateMessage(String prompt);
} }
@@ -1,34 +1,17 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:scribe/scribe/ai/data/datasource/ai_datasource.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/data/network/ai_api.dart';
import 'package:scribe/scribe/ai/domain/model/ai_response.dart'; import 'package:scribe/scribe/ai/domain/model/ai_response.dart';
class AIDataSourceImpl implements AIDataSource { class AIDataSourceImpl implements AIDataSource {
final AIApi _aiApi; final AIApi _aiApi;
AIDataSourceImpl({ AIDataSourceImpl(this._aiApi);
required Dio dio,
String? endpoint,
}) : _aiApi = AIApi(
dio: dio,
endpoint: endpoint,
);
@override @override
Future<AIResponse> request(String prompt) async { Future<AIResponse> generateMessage(String prompt) async {
try { try {
final aiRequest = AIAPIRequest( final apiResponse = await _aiApi.generateMessage(prompt);
messages: [
AIMessage(
role: 'user',
content: prompt,
),
],
);
final apiResponse = await _aiApi.chatCompletion(aiRequest);
return AIResponse(result: apiResponse.content); return AIResponse(result: apiResponse.content);
} on DioError catch (e) { } on DioError catch (e) {
throw Exception('Failed to generate AI text: ${e.message}'); throw Exception('Failed to generate AI text: ${e.message}');
@@ -12,7 +12,13 @@ class AIMessage {
required this.content, required this.content,
}); });
factory AIMessage.fromJson(Map<String, dynamic> json) => _$AIMessageFromJson(json); factory AIMessage.fromJson(Map<String, dynamic> json) =>
_$AIMessageFromJson(json);
Map<String, dynamic> toJson() => _$AIMessageToJson(this); Map<String, dynamic> toJson() => _$AIMessageToJson(this);
factory AIMessage.ofUser(String content) => AIMessage(
role: 'user',
content: content,
);
} }
+16 -20
View File
@@ -1,30 +1,22 @@
import 'package:dio/dio.dart'; import 'package:core/data/network/dio_client.dart';
import 'package:scribe/scribe/ai/data/model/ai_api_response.dart';
import 'package:scribe/scribe/ai/data/model/ai_api_request.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'; import 'package:scribe/scribe/ai/data/network/ai_api_exception.dart';
class AIApi { class AIApi {
final Dio _dio; final DioClient _dioClient;
final String? _endpoint; final String aiEndpoint;
AIApi({ AIApi(this._dioClient, this.aiEndpoint);
required Dio dio,
String? endpoint,
}) : _dio = dio,
_endpoint = endpoint;
Future<AIApiResponse> chatCompletion( Future<AIApiResponse> generateMessage(String prompt) async {
AIAPIRequest request final aiRequest = _generateRequest(prompt);
) async {
final url = _endpoint;
if (url == null) { final response = await _dioClient.post(
throw AIApiNotAvailableException(); aiEndpoint,
} data: aiRequest.toJson(),
useJMAPHeader: false,
final response = await _dio.post(
url,
data: request.toJson(),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
@@ -39,4 +31,8 @@ class AIApi {
); );
} }
} }
AIAPIRequest _generateRequest(String prompt) {
return AIAPIRequest(messages: [AIMessage.ofUser(prompt)]);
}
} }
@@ -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'; import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.dart';
class AIScribeRepositoryImpl implements AIScribeRepository { class AIScribeRepositoryImpl implements AIScribeRepository {
final AIDataSource _dataSource; final AIDataSource _aiDataSource;
AIScribeRepositoryImpl(this._dataSource); AIScribeRepositoryImpl(this._aiDataSource);
@override @override
Future<AIResponse> generateText(String prompt) async { Future<AIResponse> generateMessage(String prompt) {
return _dataSource.request(prompt); return _aiDataSource.generateMessage(prompt);
} }
} }
@@ -1,5 +1,5 @@
import 'package:scribe/scribe/ai/domain/model/ai_response.dart'; import 'package:scribe/scribe/ai/domain/model/ai_response.dart';
abstract class AIScribeRepository { abstract class AIScribeRepository {
Future<AIResponse> generateText(String prompt); Future<AIResponse> generateMessage(String prompt);
} }
@@ -1,5 +1,6 @@
import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:scribe/scribe/ai/domain/constants/ai_prompts.dart'; import 'package:scribe/scribe/ai/domain/constants/ai_prompts.dart';
import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.dart'; import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.dart';
@@ -17,9 +18,8 @@ class GenerateAITextInteractor {
) async { ) async {
try { try {
final prompt = AIPrompts.buildPrompt(action, selectedText); final prompt = AIPrompts.buildPrompt(action, selectedText);
log('$runtimeType::execute():prompt: $prompt');
final response = await _repository.generateText(prompt); final response = await _repository.generateMessage(prompt);
return Right(GenerateAITextSuccess(response)); return Right(GenerateAITextSuccess(response));
} catch (e) { } catch (e) {
return Left(GenerateAITextFailure(e)); return Left(GenerateAITextFailure(e));
@@ -1,18 +1,21 @@
import 'package:dio/dio.dart'; import 'package:core/core.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:scribe/scribe/ai/data/datasource/ai_datasource.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/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/data/repository/ai_repository_impl.dart';
import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.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/domain/usecases/generate_ai_text_interactor.dart';
class AIScribeBindings extends Bindings { class AIScribeBindings extends Bindings {
final String? scribeEndpoint;
AIScribeBindings({this.scribeEndpoint}); final String aiEndpoint;
AIScribeBindings(this.aiEndpoint);
@override @override
void dependencies() { void dependencies() {
_bindingsAPI();
_bindingsDataSourceImpl(); _bindingsDataSourceImpl();
_bindingsDataSource(); _bindingsDataSource();
_bindingsRepositoryImpl(); _bindingsRepositoryImpl();
@@ -20,11 +23,12 @@ class AIScribeBindings extends Bindings {
_bindingsInteractor(); _bindingsInteractor();
} }
void _bindingsAPI() {
Get.lazyPut<AIApi>(() => AIApi(Get.find<DioClient>(), aiEndpoint));
}
void _bindingsDataSourceImpl() { void _bindingsDataSourceImpl() {
Get.lazyPut<AIDataSourceImpl>(() => AIDataSourceImpl( Get.lazyPut<AIDataSourceImpl>(() => AIDataSourceImpl(Get.find<AIApi>()));
dio: Get.find<Dio>(),
endpoint: scribeEndpoint,
));
} }
void _bindingsDataSource() { void _bindingsDataSource() {
@@ -32,9 +36,9 @@ class AIScribeBindings extends Bindings {
} }
void _bindingsRepositoryImpl() { void _bindingsRepositoryImpl() {
Get.lazyPut<AIScribeRepositoryImpl>(() => AIScribeRepositoryImpl( Get.lazyPut<AIScribeRepositoryImpl>(
Get.find<AIDataSource>(), () => AIScribeRepositoryImpl(Get.find<AIDataSource>()),
)); );
} }
void _bindingsRepository() { void _bindingsRepository() {
@@ -42,8 +46,8 @@ class AIScribeBindings extends Bindings {
} }
void _bindingsInteractor() { void _bindingsInteractor() {
Get.lazyPut<GenerateAITextInteractor>(() => GenerateAITextInteractor( Get.lazyPut<GenerateAITextInteractor>(
Get.find<AIScribeRepository>(), () => GenerateAITextInteractor(Get.find<AIScribeRepository>()),
)); );
} }
} }
@@ -15,6 +15,8 @@ class AICapability extends CapabilityProperties {
factory AICapability.deserialize(Map<String, dynamic> json) => _$AICapabilityFromJson(json); factory AICapability.deserialize(Map<String, dynamic> json) => _$AICapabilityFromJson(json);
bool get isScribeEndpointAvailable => scribeEndpoint?.trim().isNotEmpty == true;
@override @override
List<Object?> get props => [scribeEndpoint]; List<Object?> get props => [scribeEndpoint];
} }
@@ -66,7 +66,7 @@ class _AiScribeSuggestionWidgetModalState
} }
} }
void _getAiSuggestion( Future<void> _getAiSuggestion(
GenerateAITextInteractor interactor, GenerateAITextInteractor interactor,
AIAction action, AIAction action,
String? content, String? content,