From 96c95866c64bd3c4f4ea4af5b9e5799dc7d60f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Poizat?= Date: Thu, 4 Dec 2025 17:12:24 +0100 Subject: [PATCH] Open AI Scribe from bottom bar button --- .../presentation/composer_controller.dart | 54 +++++++++++++++++++ .../composer/presentation/composer_view.dart | 3 ++ .../presentation/composer_view_web.dart | 5 ++ .../tablet_bottom_bar_composer_widget.dart | 16 ++++++ .../web/bottom_bar_composer_widget.dart | 23 ++++++++ lib/l10n/intl_fr.arb | 6 +++ lib/l10n/intl_messages.arb | 8 ++- lib/l10n/intl_ru.arb | 6 +++ lib/l10n/intl_vi.arb | 6 +++ lib/main/localizations/app_localizations.dart | 7 +++ scribe/lib/scribe/ai/l10n/intl_messages.arb | 2 +- 11 files changed, 134 insertions(+), 2 deletions(-) diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 3592ba1c5..0f43341bc 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -27,6 +27,7 @@ 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/ai/presentation/widgets/ai_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'; @@ -89,6 +90,7 @@ 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'; @@ -198,6 +200,7 @@ class ComposerController extends BaseController final GlobalKey keyReplyToEmailTagEditor = GlobalKey(); final GlobalKey headerEditorMobileWidgetKey = GlobalKey(); final GlobalKey identityDropdownKey = GlobalKey(); + final GlobalKey aiScribeButtonKey = GlobalKey(); final double defaultPaddingCoordinateYCursorEditor = 8; FocusNode? subjectEmailInputFocusNode; @@ -859,6 +862,57 @@ class ComposerController extends BaseController } } + Future getTextOnlyContentInEditor() async { + try { + final htmlContent = await getContentInEditor(); + + String textContent = htmlContent.replaceAll(RegExp(r'<[^>]*>'), ''); + + textContent = textContent + .replaceAll(' ', ' ') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'"); + + return textContent.trim(); + } catch (e) { + logError('ComposerController::getTextOnlyContentInEditor:Exception = $e'); + return ''; + } + } + + void insertTextInEditor(String text) { + final htmlContent = text.replaceAll('\n', '
'); + + 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, + buttonPosition: buttonPosition, + ); + } + Future _prepareToSendMessages(BuildContext context) async { final arguments = composerArguments.value; final session = mailboxDashBoardController.sessionCurrent; diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 159722820..9a701e400 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -33,6 +33,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_c import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/from_composer_drop_down_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/view_entire_message_with_message_clipped_widget.dart'; +import 'package:scribe/scribe.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class ComposerView extends GetWidget { @@ -448,6 +449,8 @@ class ComposerView extends GetWidget { sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), + onOpenAIScribe: () => controller.showAIScribeMenuForFullText(context), + aiScribeButtonKey: controller.aiScribeButtonKey, )), ] ) diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 7277ccda1..67013c5a1 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -29,6 +29,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/list_recipi import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_mobile_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; +import 'package:scribe/scribe.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart'; @@ -555,6 +556,8 @@ class ComposerView extends GetWidget { toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), onOpenInsertLink: controller.openInsertLink, + onOpenAIScribe: () => controller.showAIScribeMenuForFullText(context), + aiScribeButtonKey: controller.aiScribeButtonKey, )), ], ), @@ -827,6 +830,8 @@ class ComposerView extends GetWidget { toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), onOpenInsertLink: controller.openInsertLink, + onOpenAIScribe: () => controller.showAIScribeMenuForFullText(context), + aiScribeButtonKey: controller.aiScribeButtonKey, )), ], ), diff --git a/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart index 7ee7b4e7e..d9d1f3dda 100644 --- a/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart @@ -14,6 +14,8 @@ class TabletBottomBarComposerWidget extends StatelessWidget { final VoidCallback sendMessageAction; final VoidCallback requestReadReceiptAction; final VoidCallback toggleMarkAsImportantAction; + final VoidCallback? onOpenAIScribe; + final GlobalKey? aiScribeButtonKey; const TabletBottomBarComposerWidget({ super.key, @@ -25,6 +27,8 @@ class TabletBottomBarComposerWidget extends StatelessWidget { required this.sendMessageAction, required this.requestReadReceiptAction, required this.toggleMarkAsImportantAction, + this.onOpenAIScribe, + this.aiScribeButtonKey, }); @override @@ -71,6 +75,18 @@ 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!, + ), + ], const SizedBox(width: TabletBottomBarComposerWidgetStyle.space), TMailButtonWidget.fromIcon( icon: imagePaths.icSaveToDraft, diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart index 31dcc635e..808ee035b 100644 --- a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -31,6 +31,8 @@ class BottomBarComposerWidget extends StatelessWidget { final VoidCallback toggleMarkAsImportantAction; final VoidCallback saveAsTemplateAction; final VoidCallback onOpenInsertLink; + final VoidCallback? onOpenAIScribe; + final GlobalKey? aiScribeButtonKey; final OnMenuChanged? onPopupMenuChanged; const BottomBarComposerWidget({ @@ -54,6 +56,8 @@ class BottomBarComposerWidget extends StatelessWidget { required this.toggleMarkAsImportantAction, required this.saveAsTemplateAction, required this.onOpenInsertLink, + this.onOpenAIScribe, + this.aiScribeButtonKey, this.onPopupMenuChanged, }); @@ -127,6 +131,25 @@ class BottomBarComposerWidget extends StatelessWidget { onTapActionCallback: onOpenInsertLink, ), ), + if (onOpenAIScribe != null) ...[ + const SizedBox(width: BottomBarComposerWidgetStyle.space), + AbsorbPointer( + absorbing: isCodeViewEnabled, + child: TMailButtonWidget.fromIcon( + key: aiScribeButtonKey, + icon: imagePaths.icSparkle, + 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!, + ), + ), + ], const SizedBox(width: BottomBarComposerWidgetStyle.space), PopupMenuOverlayWidget( controller: menuMoreOptionController, diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 8ba85e55b..9a6a256ff 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -5473,5 +5473,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "aiAssistant": "Assistant IA", + "@aiAssistant": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index e70ce5321..164bb43fb 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-12-10T16:32:00.611716", + "@@last_modified": "2025-12-10T10:22:35.345340", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -5129,5 +5129,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "aiAssistant": "AI assistant", + "@aiAssistant": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 38ee0a1a5..2edcbb702 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -5491,5 +5491,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "aiAssistant": "Помощник ИИ", + "@aiAssistant": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 829a15bc5..c273baead 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -5479,5 +5479,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "aiAssistant": "Trợ lý AI", + "@aiAssistant": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index ade2495b2..bb0127f1f 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -5436,4 +5436,11 @@ class AppLocalizations { name: 'startToAddFavoritesEmails', ); } + + String get aiAssistant { + return Intl.message( + 'AI assistant', + name: 'aiAssistant', + ); + } } diff --git a/scribe/lib/scribe/ai/l10n/intl_messages.arb b/scribe/lib/scribe/ai/l10n/intl_messages.arb index 1ca452e3f..89da188c0 100644 --- a/scribe/lib/scribe/ai/l10n/intl_messages.arb +++ b/scribe/lib/scribe/ai/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-12-10T16:32:03.004307", + "@@last_modified": "2025-12-10T10:22:36.990738", "categoryCorrectGrammar": "Correct grammar", "@categoryCorrectGrammar": { "type": "text",