Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/lib/utils/html/html_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,6 +30,42 @@ mixin AIScribeInComposerMixin {
}
}

Future<void> 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();

Expand All @@ -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,
);
Expand All @@ -60,7 +98,8 @@ mixin AIScribeInComposerMixin {
context: context,
imagePaths: imagePaths,
content: selection,
onInsertText: insertTextInEditor,
onInsertText: onInsertTextCallback,
onReplaceText: onReplaceTextCallback,
interactor: generateAITextInteractor,
buttonPosition: buttonPosition,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ class _WebEditorState extends State<WebEditorWidget> 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,
Expand Down
6 changes: 6 additions & 0 deletions scribe/lib/scribe/ai/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,11 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"replaceButton": "Replace",
"@replaceButton": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
6 changes: 6 additions & 0 deletions scribe/lib/scribe/ai/l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,11 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"replaceButton": "Remplacer",
"@replaceButton": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
6 changes: 6 additions & 0 deletions scribe/lib/scribe/ai/l10n/intl_messages.arb
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,11 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"replaceButton": "Replace",
"@replaceButton": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
6 changes: 6 additions & 0 deletions scribe/lib/scribe/ai/l10n/intl_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,11 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"replaceButton": "заменить",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd scribe && cat -n lib/scribe/ai/l10n/intl_ru.arb | head -150

Repository: linagora/tmail-flutter

Length of output: 3891


Fix capitalization for consistency with other button labels.

The Russian translation "заменить" is lowercase, while all other button and action labels in this file use capitalized first letters (e.g., "Вставить" on line 111). Change "заменить" to "Заменить" to match the established capitalization pattern.

🤖 Prompt for AI Agents
In scribe/lib/scribe/ai/l10n/intl_ru.arb around line 117, the value for
"replaceButton" is "заменить" (lowercase) which is inconsistent with other
button labels that use an initial capital letter; update the translation to
"Заменить" so the first letter is capitalized to match the file's established
capitalization pattern.

"@replaceButton": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
6 changes: 6 additions & 0 deletions scribe/lib/scribe/ai/l10n/intl_vi.arb
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,11 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"replaceButton": "Thay thế",
"@replaceButton": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
5 changes: 5 additions & 0 deletions scribe/lib/scribe/ai/localizations/scribe_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScribeLocalizations> {
Expand Down
5 changes: 5 additions & 0 deletions scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Future<void> showAIScribeDialog({
required ImagePaths imagePaths,
required String content,
required AIScribeResultCallback onInsertText,
AIScribeResultCallback? onReplaceText,
required GenerateAITextInteractor interactor,
List<AIScribeMenuCategory>? availableCategories,
Offset? buttonPosition,
Expand Down Expand Up @@ -124,6 +125,10 @@ Future<void> showAIScribeDialog({
onInsertText(result);
Navigator.of(context).pop();
},
onReplace: onReplaceText != null ? (result) {
onReplaceText(result);
Navigator.of(context).pop();
} : null,
imagePaths: imagePaths,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class AIScribeSuggestion extends StatefulWidget {
final Future<String> suggestionFuture;
final VoidCallback onClose;
final OnInsertTextCallback onInsert;
final OnInsertTextCallback? onReplace;
final ImagePaths imagePaths;

const AIScribeSuggestion({
Expand All @@ -21,6 +22,7 @@ class AIScribeSuggestion extends StatefulWidget {
required this.suggestionFuture,
required this.onClose,
required this.onInsert,
this.onReplace,
required this.imagePaths,
});

Expand Down Expand Up @@ -184,13 +186,27 @@ class _AIScribeSuggestionModalState extends State<AIScribeSuggestion> {
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),
),
],
),
],
),
);
Expand Down
Loading