diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart index 00dab4b696..3701375328 100644 --- a/core/lib/utils/html/html_utils.dart +++ b/core/lib/utils/html/html_utils.dart @@ -111,6 +111,16 @@ class HtmlUtils { });''', name: 'onSelectionChange'); + static const collapseSelectionToEnd = ( + script: ''' + (() => { + const selection = window.getSelection(); + if (selection) { + selection.collapseToEnd() + } + })();''', + name: 'collapseSelectionToEnd'); + static recalculateEditorHeight({double? maxHeight}) => ( script: ''' const editable = document.querySelector('.note-editable'); diff --git a/lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart b/lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart index ea345299d7..5f8bd1b5db 100644 --- a/lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart +++ b/lib/features/composer/presentation/mixin/ai_scribe_in_composer_mixin.dart @@ -1,4 +1,5 @@ import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/html/html_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -29,6 +30,42 @@ mixin AIScribeInComposerMixin { } } + Future collapseSelection() async { + if (PlatformInfo.isWeb) { + await richTextWebController?.editorController.evaluateJavascriptWeb( + HtmlUtils.collapseSelectionToEnd.name, + hasReturnValue: false, + ); + } else { + await richTextMobileTabletController?.htmlEditorApi?.webViewController.evaluateJavascript(source: HtmlUtils.collapseSelectionToEnd.script); + } + } + + void clearTextInEditor() { + if (PlatformInfo.isWeb) { + richTextWebController?.editorController.setText(''); + } else { + richTextMobileTabletController?.htmlEditorApi?.setText(''); + } + } + + // Ensure we only insert at cursor position by collapsing selection before inserting + void onInsertTextCallback(String text) async { + await collapseSelection(); + + insertTextInEditor(text); + } + + // If there is a selection, it will replace the selection, else it will replace everything + void onReplaceTextCallback(String text) async { + final selection = editorTextSelection.value?.selectedText; + if (selection == null || selection.isEmpty) { + clearTextInEditor(); + } + + insertTextInEditor(text); + } + void showAIScribeMenuForFullText(BuildContext context) async { final fullText = await getTextOnlyContentInEditor(); @@ -44,7 +81,8 @@ mixin AIScribeInComposerMixin { context: context, imagePaths: imagePaths, content: fullText, - onInsertText: insertTextInEditor, + onInsertText: onInsertTextCallback, + onReplaceText: fullText.isEmpty ? null : onReplaceTextCallback, interactor: generateAITextInteractor, buttonPosition: buttonPosition, ); @@ -60,7 +98,8 @@ mixin AIScribeInComposerMixin { context: context, imagePaths: imagePaths, content: selection, - onInsertText: insertTextInEditor, + onInsertText: onInsertTextCallback, + onReplaceText: onReplaceTextCallback, interactor: generateAITextInteractor, buttonPosition: buttonPosition, ); diff --git a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart index d2ecc749fd..7a6e1ff550 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -185,6 +185,10 @@ class _WebEditorState extends State with TextSelectionMixin { name: HtmlUtils.registerSelectionChangeListener.name, script: HtmlUtils.registerSelectionChangeListener.script, ), + WebScript( + name: HtmlUtils.collapseSelectionToEnd.name, + script: HtmlUtils.collapseSelectionToEnd.script, + ), WebScript( name: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).name, script: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).script, diff --git a/scribe/lib/scribe/ai/l10n/intl_en.arb b/scribe/lib/scribe/ai/l10n/intl_en.arb index 72657c82ad..45384b6447 100644 --- a/scribe/lib/scribe/ai/l10n/intl_en.arb +++ b/scribe/lib/scribe/ai/l10n/intl_en.arb @@ -113,5 +113,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "replaceButton": "Replace", + "@replaceButton": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/scribe/lib/scribe/ai/l10n/intl_fr.arb b/scribe/lib/scribe/ai/l10n/intl_fr.arb index 6d910f946f..42626526e4 100644 --- a/scribe/lib/scribe/ai/l10n/intl_fr.arb +++ b/scribe/lib/scribe/ai/l10n/intl_fr.arb @@ -113,5 +113,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "replaceButton": "Remplacer", + "@replaceButton": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/scribe/lib/scribe/ai/l10n/intl_messages.arb b/scribe/lib/scribe/ai/l10n/intl_messages.arb index 89da188c00..3fb6e1cb67 100644 --- a/scribe/lib/scribe/ai/l10n/intl_messages.arb +++ b/scribe/lib/scribe/ai/l10n/intl_messages.arb @@ -113,5 +113,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "replaceButton": "Replace", + "@replaceButton": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/scribe/lib/scribe/ai/l10n/intl_ru.arb b/scribe/lib/scribe/ai/l10n/intl_ru.arb index bb406d97b4..8e1e9fc16f 100644 --- a/scribe/lib/scribe/ai/l10n/intl_ru.arb +++ b/scribe/lib/scribe/ai/l10n/intl_ru.arb @@ -113,5 +113,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "replaceButton": "заменить", + "@replaceButton": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/scribe/lib/scribe/ai/l10n/intl_vi.arb b/scribe/lib/scribe/ai/l10n/intl_vi.arb index 3fa174a214..04b7e9da13 100644 --- a/scribe/lib/scribe/ai/l10n/intl_vi.arb +++ b/scribe/lib/scribe/ai/l10n/intl_vi.arb @@ -113,5 +113,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "replaceButton": "Thay thế", + "@replaceButton": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/scribe/lib/scribe/ai/localizations/scribe_localizations.dart b/scribe/lib/scribe/ai/localizations/scribe_localizations.dart index 5c6e4f90a6..a680d6187a 100644 --- a/scribe/lib/scribe/ai/localizations/scribe_localizations.dart +++ b/scribe/lib/scribe/ai/localizations/scribe_localizations.dart @@ -121,6 +121,11 @@ class ScribeLocalizations { return Intl.message('Insert', name: 'insertButton'); } + + String get replaceButton { + return Intl.message('Replace', + name: 'replaceButton'); + } } class _ScribeLocalizationsDelegate extends LocalizationsDelegate { diff --git a/scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart b/scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart index 48effa9471..247205f979 100644 --- a/scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart +++ b/scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart @@ -18,6 +18,7 @@ Future showAIScribeDialog({ required ImagePaths imagePaths, required String content, required AIScribeResultCallback onInsertText, + AIScribeResultCallback? onReplaceText, required GenerateAITextInteractor interactor, List? availableCategories, Offset? buttonPosition, @@ -124,6 +125,10 @@ Future showAIScribeDialog({ onInsertText(result); Navigator.of(context).pop(); }, + onReplace: onReplaceText != null ? (result) { + onReplaceText(result); + Navigator.of(context).pop(); + } : null, imagePaths: imagePaths, ); diff --git a/scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart b/scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart index 520a9c8578..e71e8532e9 100644 --- a/scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart +++ b/scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart @@ -13,6 +13,7 @@ class AIScribeSuggestion extends StatefulWidget { final Future suggestionFuture; final VoidCallback onClose; final OnInsertTextCallback onInsert; + final OnInsertTextCallback? onReplace; final ImagePaths imagePaths; const AIScribeSuggestion({ @@ -21,6 +22,7 @@ class AIScribeSuggestion extends StatefulWidget { required this.suggestionFuture, required this.onClose, required this.onInsert, + this.onReplace, required this.imagePaths, }); @@ -184,13 +186,27 @@ class _AIScribeSuggestionModalState extends State { Clipboard.setData(ClipboardData(text: suggestion)); }, ), - TMailButtonWidget( - text: ScribeLocalizations.of(context)!.insertButton, - textStyle: AIScribeButtonStyles.mainActionButtonText, - padding: AIScribeButtonStyles.mainActionButtonPadding, - backgroundColor: AIScribeButtonStyles.mainActionButtonBackgroundColor, - onTapActionCallback: () => widget.onInsert(suggestion), - ) + Row( + children: [ + if (widget.onReplace != null) ...[ + TMailButtonWidget( + text: ScribeLocalizations.of(context)!.replaceButton, + textStyle: AIScribeButtonStyles.mainActionButtonText, + padding: AIScribeButtonStyles.mainActionButtonPadding, + backgroundColor: Colors.transparent, + onTapActionCallback: () => widget.onReplace!(suggestion), + ), + const SizedBox(width: AIScribeSizes.fieldSpacing), + ], + TMailButtonWidget( + text: ScribeLocalizations.of(context)!.insertButton, + textStyle: AIScribeButtonStyles.mainActionButtonText, + padding: AIScribeButtonStyles.mainActionButtonPadding, + backgroundColor: AIScribeButtonStyles.mainActionButtonBackgroundColor, + onTapActionCallback: () => widget.onInsert(suggestion), + ), + ], + ), ], ), );