feat(ai-scribe): reorganize abstractions and update dialog modal UI
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||
import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
||||
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
|
||||
|
||||
mixin AiScribeMixin {
|
||||
bool isAICapabilitySupported({Session? session, AccountId? accountId}) {
|
||||
if (accountId == null || session == null) {
|
||||
return false;
|
||||
}
|
||||
final aiCapability = session.getAICapability(accountId);
|
||||
return aiCapability != null;
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,12 @@ import 'package:model/model.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
import 'package:rich_text_composer/rich_text_composer.dart';
|
||||
import 'package:scribe/scribe.dart';
|
||||
import 'package:super_tag_editor/tag_editor.dart';
|
||||
import 'package:tmail_ui_user/features/base/base_controller.dart';
|
||||
import 'package:tmail_ui_user/features/base/before_reconnect_handler.dart';
|
||||
import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart';
|
||||
import 'package:tmail_ui_user/features/base/mixin/ai_scribe_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/base/mixin/auto_complete_result_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart';
|
||||
import 'package:tmail_ui_user/features/base/state/base_ui_state.dart';
|
||||
@@ -77,7 +79,6 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_em
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_list_identities_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_selected_identity_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/update_screen_display_mode_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart';
|
||||
@@ -90,7 +91,6 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_template_dialog_view.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart';
|
||||
import 'package:scribe/scribe.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/save_template_email_state.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/state/update_template_email_state.dart';
|
||||
@@ -103,9 +103,9 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen
|
||||
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.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/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.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/extensions/open_and_close_composer_extension.dart';
|
||||
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/state/get_all_identities_state.dart';
|
||||
import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart';
|
||||
@@ -131,9 +131,12 @@ import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html;
|
||||
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
||||
|
||||
class ComposerController extends BaseController
|
||||
with DragDropFileMixin, AutoCompleteResultMixin, EditorViewMixin, AIScribeInComposerMixin
|
||||
with
|
||||
DragDropFileMixin,
|
||||
AutoCompleteResultMixin,
|
||||
EditorViewMixin,
|
||||
AiScribeMixin
|
||||
implements BeforeReconnectHandler {
|
||||
|
||||
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
|
||||
final networkConnectionController = Get.find<NetworkConnectionController>();
|
||||
final _beforeReconnectManager = Get.find<BeforeReconnectManager>();
|
||||
@@ -157,6 +160,7 @@ class ComposerController extends BaseController
|
||||
final isEmailChanged = Rx<bool>(false);
|
||||
final isMarkAsImportant = Rx<bool>(false);
|
||||
final isContentHeightExceeded = Rx<bool>(false);
|
||||
final editorTextSelection = Rxn<TextSelectionModel>();
|
||||
|
||||
final LocalFilePickerInteractor _localFilePickerInteractor;
|
||||
final LocalImagePickerInteractor _localImagePickerInteractor;
|
||||
@@ -221,11 +225,8 @@ class ComposerController extends BaseController
|
||||
StreamSubscription<html.Event>? _subscriptionOnBlur;
|
||||
StreamSubscription<String>? _composerCacheListener;
|
||||
|
||||
@override
|
||||
RichTextMobileTabletController? richTextMobileTabletController;
|
||||
@override
|
||||
RichTextWebController? richTextWebController;
|
||||
|
||||
CustomPopupMenuController? menuMoreOptionController;
|
||||
|
||||
final ScrollController scrollController = ScrollController();
|
||||
@@ -262,18 +263,11 @@ class ComposerController extends BaseController
|
||||
|
||||
TransformHtmlEmailContentInteractor get transformHtmlEmailContentInteractor => _transformHtmlEmailContentInteractor;
|
||||
|
||||
@override
|
||||
GenerateAITextInteractor get generateAITextInteractor => Get.find<GenerateAITextInteractor>();
|
||||
|
||||
String get ownEmailAddress =>
|
||||
mailboxDashBoardController.ownEmailAddress.value;
|
||||
|
||||
@override
|
||||
Session? get session => mailboxDashBoardController.sessionCurrent;
|
||||
|
||||
@override
|
||||
AccountId? get accountId => mailboxDashBoardController.accountId.value;
|
||||
|
||||
late Worker uploadInlineImageWorker;
|
||||
late bool _isEmailBodyLoaded;
|
||||
|
||||
@@ -873,20 +867,6 @@ class ComposerController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getTextOnlyContentInEditor() async {
|
||||
try {
|
||||
final htmlContent = await getContentInEditor();
|
||||
|
||||
String textContent = StringConvert.convertHtmlContentToTextContent(htmlContent);
|
||||
|
||||
return textContent;
|
||||
} catch (e) {
|
||||
logError('ComposerController::getTextOnlyContentInEditor:Exception = $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _prepareToSendMessages(BuildContext context) async {
|
||||
final arguments = composerArguments.value;
|
||||
final session = mailboxDashBoardController.sessionCurrent;
|
||||
|
||||
@@ -8,8 +8,8 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||
import 'package:model/email/prefix_email_address.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/widgets/ai_scribe_button.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_content_height_exceeded_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_open_context_menu_extension.dart';
|
||||
@@ -22,6 +22,7 @@ import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_b
|
||||
import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_container_view.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_editor_view.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/view/mobile/tablet_container_view.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/list_recipients_collapsed_widget.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart';
|
||||
@@ -264,7 +265,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64),
|
||||
],
|
||||
),
|
||||
_buildAIScribeSelectionButton(context),
|
||||
ComposerAiScribeSelectionOverlay(controller: controller),
|
||||
],
|
||||
)
|
||||
],
|
||||
@@ -453,7 +454,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64),
|
||||
],
|
||||
),
|
||||
_buildAIScribeSelectionButton(context),
|
||||
ComposerAiScribeSelectionOverlay(controller: controller),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -469,8 +470,9 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
sendMessageAction: () => controller.handleClickSendButton(context),
|
||||
requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context),
|
||||
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
|
||||
onOpenAIScribe: controller.isAIScribeAvailable ? () => controller.showAIScribeMenuForFullText(context) : null,
|
||||
aiScribeButtonKey: controller.isAIScribeAvailable ? controller.aiScribeButtonKey : null,
|
||||
onOpenAiAssistantModal: controller.isAIScribeAvailable
|
||||
? controller.openAIAssistantModal
|
||||
: null,
|
||||
)),
|
||||
]
|
||||
)
|
||||
@@ -553,46 +555,4 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputOnMobileAction,
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildAIScribeSelectionButton(BuildContext context) {
|
||||
if (!controller.isAIScribeAvailable) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final textSelection = controller.editorTextSelection.value;
|
||||
if (textSelection != null &&
|
||||
textSelection.hasSelection &&
|
||||
textSelection.coordinates != null) {
|
||||
final coordinates = textSelection.coordinates!;
|
||||
// Account for the horizontal padding around the editor
|
||||
const editorHorizontalPadding = 12.0;
|
||||
return PositionedDirectional(
|
||||
start: coordinates.dx + editorHorizontalPadding,
|
||||
top: coordinates.dy,
|
||||
child: Builder(
|
||||
builder: (buttonContext) {
|
||||
return AIScribeButton(
|
||||
imagePaths: controller.imagePaths,
|
||||
onTap: () {
|
||||
final RenderBox? renderBox = buttonContext.findRenderObject() as RenderBox?;
|
||||
if (renderBox != null) {
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
controller.showAIScribeMenuForSelectedText(
|
||||
context,
|
||||
buttonPosition: position,
|
||||
);
|
||||
} else {
|
||||
controller.showAIScribeMenuForSelectedText(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,16 @@ import 'package:get/get.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||
import 'package:model/email/prefix_email_address.dart';
|
||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/widgets/ai_scribe_button.dart';
|
||||
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/dialog_picker/color_dialog_picker.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/composer_print_draft_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_insert_link_composer_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_keyboard_shortcut_actions_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/mark_as_important_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/preview_upload_file_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/remove_draggable_email_address_between_recipient_fields_extension.dart';
|
||||
@@ -25,6 +25,7 @@ import 'package:tmail_ui_user/features/composer/presentation/view/web/desktop_re
|
||||
import 'package:tmail_ui_user/features/composer/presentation/view/web/mobile_responsive_container_view.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/view/web/tablet_responsive_container_view.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/view/web/web_editor_view.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/list_recipients_collapsed_widget.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_mobile_widget.dart';
|
||||
@@ -333,7 +334,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
_buildAIScribeSelectionButton(context),
|
||||
ComposerAiScribeSelectionOverlay(controller: controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -559,8 +560,9 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
|
||||
saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
|
||||
onOpenInsertLink: controller.openInsertLink,
|
||||
onOpenAIScribe: controller.isAIScribeAvailable ? () => controller.showAIScribeMenuForFullText(context) : null,
|
||||
aiScribeButtonKey: controller.isAIScribeAvailable ? controller.aiScribeButtonKey : null,
|
||||
onOpenAiAssistantModal: controller.isAIScribeAvailable
|
||||
? controller.openAIAssistantModal
|
||||
: null,
|
||||
)),
|
||||
],
|
||||
),
|
||||
@@ -607,7 +609,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
_buildAIScribeSelectionButton(context),
|
||||
ComposerAiScribeSelectionOverlay(controller: controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -835,8 +837,9 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
|
||||
saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
|
||||
onOpenInsertLink: controller.openInsertLink,
|
||||
onOpenAIScribe: controller.isAIScribeAvailable ? () => controller.showAIScribeMenuForFullText(context) : null,
|
||||
aiScribeButtonKey: controller.isAIScribeAvailable ? controller.aiScribeButtonKey : null,
|
||||
onOpenAiAssistantModal: controller.isAIScribeAvailable
|
||||
? controller.openAIAssistantModal
|
||||
: null,
|
||||
)),
|
||||
],
|
||||
),
|
||||
@@ -882,7 +885,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
_buildAIScribeSelectionButton(context),
|
||||
ComposerAiScribeSelectionOverlay(controller: controller),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -955,46 +958,4 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputOnMobileAction,
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildAIScribeSelectionButton(BuildContext context) {
|
||||
if (!controller.isAIScribeAvailable) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final textSelection = controller.editorTextSelection.value;
|
||||
if (textSelection != null &&
|
||||
textSelection.hasSelection &&
|
||||
textSelection.coordinates != null) {
|
||||
final coordinates = textSelection.coordinates!;
|
||||
return PositionedDirectional(
|
||||
start: coordinates.dx,
|
||||
top: coordinates.dy,
|
||||
child: PointerInterceptor(
|
||||
child: Builder(
|
||||
builder: (buttonContext) {
|
||||
return AIScribeButton(
|
||||
imagePaths: controller.imagePaths,
|
||||
onTap: () {
|
||||
final RenderBox? renderBox = buttonContext.findRenderObject() as RenderBox?;
|
||||
if (renderBox != null) {
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
controller.showAIScribeMenuForSelectedText(
|
||||
context,
|
||||
buttonPosition: position,
|
||||
);
|
||||
} else {
|
||||
controller.showAIScribeMenuForSelectedText(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:core/utils/string_convert.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scribe/scribe.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/model/modal/modal_cross_axis_alignment.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selection_mixin.dart';
|
||||
|
||||
extension HandleAiScribeInComposerExtension on ComposerController {
|
||||
bool get isAIScribeAvailable {
|
||||
return isAICapabilitySupported(
|
||||
session: mailboxDashBoardController.sessionCurrent,
|
||||
accountId: mailboxDashBoardController.accountId.value,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getTextOnlyContentInEditor() async {
|
||||
try {
|
||||
final htmlContent = await getContentInEditor();
|
||||
String textContent = StringConvert.convertHtmlContentToTextContent(
|
||||
htmlContent,);
|
||||
return textContent;
|
||||
} catch (e) {
|
||||
logError('$runtimeType::getTextOnlyContentInEditor:Exception = $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
void insertTextInEditor(String text) {
|
||||
final htmlContent = text.replaceAll('\n', '<br>');
|
||||
|
||||
if (PlatformInfo.isWeb) {
|
||||
richTextWebController?.editorController.insertHtml(htmlContent);
|
||||
} else {
|
||||
richTextMobileTabletController?.htmlEditorApi?.insertHtml(htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openAIAssistantModal(Offset? position, Size? size) async {
|
||||
final fullText = await _getTextOnlyContentInEditor();
|
||||
|
||||
final aiResult = await AiScribeModalManager.showAIScribeMenuModal(
|
||||
imagePaths: imagePaths,
|
||||
availableCategories: AIScribeMenuCategory.values,
|
||||
buttonPosition: position,
|
||||
buttonSize: size,
|
||||
content: fullText,
|
||||
preferredPlacement: ModalPlacement.top,
|
||||
crossAxisAlignment: ModalCrossAxisAlignment.start,
|
||||
);
|
||||
|
||||
if (aiResult != null) {
|
||||
insertTextInEditor(aiResult);
|
||||
}
|
||||
}
|
||||
|
||||
void handleTextSelection(TextSelectionData? textSelectionData) {
|
||||
if (textSelectionData != null && textSelectionData.hasSelection) {
|
||||
editorTextSelection.value = TextSelectionModel(
|
||||
selectedText: textSelectionData.selectedText,
|
||||
coordinates: textSelectionData.coordinates != null
|
||||
? Offset(
|
||||
textSelectionData.coordinates!.x,
|
||||
textSelectionData.coordinates!.y,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
editorTextSelection.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.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/widgets/ai_scribe.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/mixins/text_selection_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
|
||||
|
||||
mixin AIScribeInComposerMixin {
|
||||
final editorTextSelection = Rxn<EditorTextSelection>();
|
||||
final GlobalKey aiScribeButtonKey = GlobalKey();
|
||||
|
||||
RichTextWebController? get richTextWebController;
|
||||
RichTextMobileTabletController? get richTextMobileTabletController;
|
||||
ImagePaths get imagePaths;
|
||||
GenerateAITextInteractor get generateAITextInteractor;
|
||||
Session? get session;
|
||||
AccountId? get accountId;
|
||||
|
||||
bool get isAIScribeAvailable {
|
||||
if (!PlatformInfo.isWeb) return false;
|
||||
|
||||
final currentSession = session;
|
||||
final currentAccountId = accountId;
|
||||
|
||||
if (currentSession == null || currentAccountId == null) return false;
|
||||
|
||||
final aiCapability = currentSession.getAICapability(currentAccountId);
|
||||
return aiCapability != null;
|
||||
}
|
||||
|
||||
Future<String> getTextOnlyContentInEditor();
|
||||
|
||||
void insertTextInEditor(String text) {
|
||||
final htmlContent = text.replaceAll('\n', '<br>');
|
||||
|
||||
if (PlatformInfo.isWeb) {
|
||||
richTextWebController?.editorController.insertHtml(htmlContent);
|
||||
} else {
|
||||
richTextMobileTabletController?.htmlEditorApi?.insertHtml(htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
void showAIScribeMenuForFullText(BuildContext context) async {
|
||||
final fullText = await getTextOnlyContentInEditor();
|
||||
|
||||
final RenderBox? renderBox = aiScribeButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
Offset? buttonPosition;
|
||||
if (renderBox != null) {
|
||||
buttonPosition = renderBox.localToGlobal(Offset.zero);
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
showAIScribeDialog(
|
||||
context: context,
|
||||
imagePaths: imagePaths,
|
||||
content: fullText,
|
||||
onInsertText: insertTextInEditor,
|
||||
interactor: generateAITextInteractor,
|
||||
buttonPosition: buttonPosition,
|
||||
);
|
||||
}
|
||||
|
||||
void showAIScribeMenuForSelectedText(BuildContext context, {Offset? buttonPosition}) {
|
||||
final selection = editorTextSelection.value?.selectedText;
|
||||
if (selection == null || selection.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
showAIScribeDialog(
|
||||
context: context,
|
||||
imagePaths: imagePaths,
|
||||
content: selection,
|
||||
onInsertText: insertTextInEditor,
|
||||
interactor: generateAITextInteractor,
|
||||
buttonPosition: buttonPosition,
|
||||
);
|
||||
}
|
||||
|
||||
void handleTextSelection(TextSelectionData? textSelectionData) {
|
||||
if (textSelectionData != null && textSelectionData.hasSelection) {
|
||||
editorTextSelection.value = EditorTextSelection(
|
||||
selectedText: textSelectionData.selectedText,
|
||||
coordinates: textSelectionData.coordinates != null
|
||||
? Offset(
|
||||
textSelectionData.coordinates!.x,
|
||||
textSelectionData.coordinates!.y,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
editorTextSelection.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
-12
@@ -69,18 +69,6 @@ class TextSelectionCoordinates {
|
||||
Offset get position => Offset(x, y);
|
||||
}
|
||||
|
||||
class EditorTextSelection {
|
||||
final String? selectedText;
|
||||
final Offset? coordinates;
|
||||
|
||||
const EditorTextSelection({
|
||||
this.selectedText,
|
||||
this.coordinates,
|
||||
});
|
||||
|
||||
bool get hasSelection => selectedText != null && selectedText!.isNotEmpty;
|
||||
}
|
||||
|
||||
mixin TextSelectionMixin<T extends StatefulWidget> on State<T> {
|
||||
OnTextSelectionChanged? get onSelectionChanged => null;
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart';
|
||||
|
||||
class ComposerAiScribeSelectionOverlay extends StatelessWidget {
|
||||
const ComposerAiScribeSelectionOverlay({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final ComposerController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!controller.isAIScribeAvailable) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
return AiSelectionOverlay(
|
||||
selection: controller.editorTextSelection.value,
|
||||
imagePaths: controller.imagePaths,
|
||||
onAiScribeResultCallback: controller.insertTextInEditor,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import 'package:core/utils/platform_info.dart';
|
||||
import 'package:core/utils/html/html_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rich_text_composer/rich_text_composer.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/mixins/text_selection_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selection_mixin.dart';
|
||||
|
||||
typedef OnCreatedEditorAction = Function(BuildContext context, HtmlEditorApi editorApi, String content);
|
||||
typedef OnLoadCompletedEditorAction = Function(HtmlEditorApi editorApi, WebUri? url);
|
||||
|
||||
+10
-15
@@ -1,6 +1,7 @@
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/widgets/button/ai_assistant_button.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/styles/mobile/tablet_bottom_bar_composer_widget_style.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
|
||||
@@ -14,8 +15,7 @@ class TabletBottomBarComposerWidget extends StatelessWidget {
|
||||
final VoidCallback sendMessageAction;
|
||||
final VoidCallback requestReadReceiptAction;
|
||||
final VoidCallback toggleMarkAsImportantAction;
|
||||
final VoidCallback? onOpenAIScribe;
|
||||
final GlobalKey? aiScribeButtonKey;
|
||||
final OnOpenAiAssistantModal? onOpenAiAssistantModal;
|
||||
|
||||
const TabletBottomBarComposerWidget({
|
||||
super.key,
|
||||
@@ -27,8 +27,7 @@ class TabletBottomBarComposerWidget extends StatelessWidget {
|
||||
required this.sendMessageAction,
|
||||
required this.requestReadReceiptAction,
|
||||
required this.toggleMarkAsImportantAction,
|
||||
this.onOpenAIScribe,
|
||||
this.aiScribeButtonKey,
|
||||
this.onOpenAiAssistantModal,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -75,18 +74,14 @@ class TabletBottomBarComposerWidget extends StatelessWidget {
|
||||
: AppLocalizations.of(context).turnOnRequestReadReceipt,
|
||||
onTapActionCallback: requestReadReceiptAction,
|
||||
),
|
||||
if (onOpenAIScribe != null) ...[
|
||||
const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
|
||||
TMailButtonWidget.fromIcon(
|
||||
key: aiScribeButtonKey,
|
||||
icon: imagePaths.icSparkle,
|
||||
borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
|
||||
padding: TabletBottomBarComposerWidgetStyle.iconPadding,
|
||||
iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
|
||||
tooltipMessage: AppLocalizations.of(context).aiAssistant,
|
||||
onTapActionCallback: onOpenAIScribe!,
|
||||
if (onOpenAiAssistantModal != null)
|
||||
AiAssistantButton(
|
||||
imagePaths: imagePaths,
|
||||
margin: const EdgeInsetsDirectional.only(
|
||||
start: TabletBottomBarComposerWidgetStyle.space,
|
||||
),
|
||||
onOpenAiAssistantModal: onOpenAiAssistantModal!,
|
||||
),
|
||||
],
|
||||
const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
|
||||
TMailButtonWidget.fromIcon(
|
||||
icon: imagePaths.icSaveToDraft,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:custom_pop_up_menu/custom_pop_up_menu.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/widgets/button/ai_assistant_button.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/highlight_svg_icon_on_hover.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/popup_menu_overlay_widget.dart';
|
||||
@@ -31,9 +32,8 @@ class BottomBarComposerWidget extends StatelessWidget {
|
||||
final VoidCallback toggleMarkAsImportantAction;
|
||||
final VoidCallback saveAsTemplateAction;
|
||||
final VoidCallback onOpenInsertLink;
|
||||
final VoidCallback? onOpenAIScribe;
|
||||
final GlobalKey? aiScribeButtonKey;
|
||||
final OnMenuChanged? onPopupMenuChanged;
|
||||
final OnOpenAiAssistantModal? onOpenAiAssistantModal;
|
||||
|
||||
const BottomBarComposerWidget({
|
||||
super.key,
|
||||
@@ -56,9 +56,8 @@ class BottomBarComposerWidget extends StatelessWidget {
|
||||
required this.toggleMarkAsImportantAction,
|
||||
required this.saveAsTemplateAction,
|
||||
required this.onOpenInsertLink,
|
||||
this.onOpenAIScribe,
|
||||
this.aiScribeButtonKey,
|
||||
this.onPopupMenuChanged,
|
||||
this.onOpenAiAssistantModal,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -131,25 +130,20 @@ class BottomBarComposerWidget extends StatelessWidget {
|
||||
onTapActionCallback: onOpenInsertLink,
|
||||
),
|
||||
),
|
||||
if (onOpenAIScribe != null) ...[
|
||||
const SizedBox(width: BottomBarComposerWidgetStyle.space),
|
||||
if (onOpenAiAssistantModal != null)
|
||||
AbsorbPointer(
|
||||
absorbing: isCodeViewEnabled,
|
||||
child: TMailButtonWidget.fromIcon(
|
||||
key: aiScribeButtonKey,
|
||||
icon: imagePaths.icSparkle,
|
||||
child: AiAssistantButton(
|
||||
imagePaths: imagePaths,
|
||||
iconColor: isCodeViewEnabled
|
||||
? BottomBarComposerWidgetStyle.disabledIconColor
|
||||
: BottomBarComposerWidgetStyle.iconColor,
|
||||
borderRadius: BottomBarComposerWidgetStyle.iconRadius,
|
||||
backgroundColor: Colors.transparent,
|
||||
padding: BottomBarComposerWidgetStyle.iconPadding,
|
||||
iconSize: BottomBarComposerWidgetStyle.iconSize,
|
||||
tooltipMessage: AppLocalizations.of(context).aiAssistant,
|
||||
onTapActionCallback: onOpenAIScribe!,
|
||||
: null,
|
||||
margin: const EdgeInsetsDirectional.only(
|
||||
start: BottomBarComposerWidgetStyle.space,
|
||||
),
|
||||
onOpenAiAssistantModal: onOpenAiAssistantModal!,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: BottomBarComposerWidgetStyle.space),
|
||||
PopupMenuOverlayWidget(
|
||||
controller: menuMoreOptionController,
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:core/utils/html/html_template.dart';
|
||||
import 'package:core/utils/html/html_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html_editor_enhanced/html_editor.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/mixins/text_selection_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selection_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/signature_tooltip_widget.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:universal_html/html.dart' hide VoidCallback;
|
||||
|
||||
@@ -5436,11 +5436,4 @@ class AppLocalizations {
|
||||
name: 'startToAddFavoritesEmails',
|
||||
);
|
||||
}
|
||||
|
||||
String get aiAssistant {
|
||||
return Intl.message(
|
||||
'AI assistant',
|
||||
name: 'aiAssistant',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user