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),
+ ),
+ ],
+ ),
],
),
);