Skip to content

Conversation

@zatteo
Copy link
Member

@zatteo zatteo commented Dec 10, 2025

Big Scribe PR #4187 has been split in two:

About prompts

A small PR will come soon that slighty update them to make them work as good as we can quickly. But later, we will handle differently prompts:

  • they will be fetched from a remote location
  • they will be managed by a dedicated library or SDK

Video

Screen.Recording.2025-12-10.at.09.47.59.mov

Summary by CodeRabbit

Release Notes

  • New Features

    • Added AI-powered text assistant to help generate, refine, and enhance content during composition
    • Support for predefined AI actions and custom prompts for text generation
    • AI assistance available for both full editor content and selected text
    • AI-generated suggestions can be inserted directly into the editor
  • Localization

    • AI Assistant now supported in multiple languages including French, Russian, and Vietnamese

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 10, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR integrates AI Scribe text generation functionality into the composer feature. It establishes dependency injection for GenerateAITextInteractor through AIScribeBindings, adds text selection tracking callbacks to mobile and web editor views, and introduces AI Scribe UI components including buttons, menus, suggestions, and input bars with accompanying styles. The ComposerController is enhanced to extract text, display AI menus for both full and selected text, and insert generated text into the editor. The composer views conditionally render AI Scribe UI elements and wire selection change callbacks. Localization strings for the AI assistant are added across multiple languages.

Possibly related PRs

  • linagora/tmail-flutter#4196: Introduces GenerateAITextInteractor and text selection mixin infrastructure that this PR depends on for AI Scribe integration.

Suggested labels

feature, ai-scribe, composer, localization

Suggested reviewers

  • tddang-linagora
  • hoangdat

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'AI scribe UI' is partially related to the changeset. It refers to a real and significant part of the changes (the UI components for AI scribe), but does not capture the full scope, which includes dependency injection setup, localization, and integration across multiple layers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zatteo zatteo mentioned this pull request Dec 10, 2025
3 tasks
@zatteo
Copy link
Member Author

zatteo commented Dec 10, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 10, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions
Copy link

This PR has been deployed to https://linagora.github.io/tmail-flutter/4197.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (17)
scribe/pubspec.yaml (1)

27-28: GetX dependency addition

Adding get: 4.6.6 here is reasonable for the AIScribe bindings/DI. Just confirm this version matches what the main app uses to avoid version skew across packages.

lib/main/localizations/app_localizations.dart (1)

5440-5445: aiAssistant getter wiring is correct; consider copy consistency

The new getter correctly exposes the aiAssistant key for use in the UI. If you want strict consistency with other locales or marketing copy, you might consider capitalizing it as "AI Assistant" in the default string.

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_bar.dart (1)

1-89: AIScribeBar state & lifecycle look solid; minor UX tweak optional

  • Controller setup/disposal and _isButtonEnabled logic are correct, and trimming before send avoids empty/whitespace prompts.
  • Button and onSubmitted both funnel through _onSendPressed, so behavior is consistent.

Optional: you could add textInputAction: TextInputAction.send to the TextField to better align the keyboard affordance with the send action.

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_button.dart (1)

18-26: Consider using theme-aware colors for dark mode compatibility.

The backgroundColor: Colors.white is hardcoded. If the app supports dark mode or plans to, consider extracting this to AIScribeColors or using theme-based colors for consistency.

-        backgroundColor: Colors.white,
+        backgroundColor: AIScribeColors.scribeButtonBackground,
scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart (2)

196-198: Add user feedback after copying to clipboard.

The copy operation silently copies text without any visual confirmation. Users may not know if the copy succeeded.

             onPressed: () {
               Clipboard.setData(ClipboardData(text: suggestion));
+              ScaffoldMessenger.of(context).showSnackBar(
+                SnackBar(content: Text(ScribeLocalizations.of(context)!.copiedToClipboard)),
+              );
             },

147-150: Redundant error display - showing both generic and raw error.

The error state currently shows both a generic "Failed to generate" message and the raw error string. If you keep the raw error, it duplicates information; if you sanitize it (per earlier suggestion), this second text block becomes unnecessary.

lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart (2)

53-56: Add defensive type checking for JavaScript callback data.

The cast args[0] as Map<dynamic, dynamic> will throw a TypeError if the JavaScript side sends unexpected data. Consider adding type checking.

         if (args.isNotEmpty) {
-          final data = args[0] as Map<dynamic, dynamic>;
-          handleSelectionChange(data);
+          final rawData = args[0];
+          if (rawData is Map<dynamic, dynamic>) {
+            handleSelectionChange(rawData);
+          }
         }

65-69: Setting _editorApi = null doesn't remove the JavaScript handler.

The JavaScript handler registered via addJavaScriptHandler persists until the webview is destroyed. While the mounted check prevents processing after disposal, consider whether explicit handler removal is needed for cleaner lifecycle management.

lib/features/composer/presentation/composer_view.dart (2)

567-571: Hardcoded padding value should reference the actual style constant.

The editorHorizontalPadding = 12.0 should reference ComposerStyle.mobileEditorPadding.horizontal or the actual constant to stay in sync if the padding changes.

-        // Account for the horizontal padding around the editor
-        const editorHorizontalPadding = 12.0;
+        // Account for the horizontal padding around the editor
+        final editorHorizontalPadding = ComposerStyle.mobileEditorPadding.left;
         return PositionedDirectional(
           start: coordinates.dx + editorHorizontalPadding,

239-270: Duplicated Stack/AIScribe overlay structure between mobile and tablet layouts.

The Stack wrapping the editor with AIScribe selection button is duplicated. Consider extracting this into a shared builder method to reduce duplication and ensure consistency.

Widget _buildEditorWithAIScribeOverlay({
  required BuildContext context,
  required Widget editorView,
  required Widget? viewEntireMessageWidget,
}) {
  return Stack(
    children: [
      Column(
        children: [
          editorView,
          viewEntireMessageWidget ?? const SizedBox.shrink(),
          SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64),
        ],
      ),
      _buildAIScribeSelectionButton(context),
    ],
  );
}

Also applies to: 428-459

scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart (1)

19-27: Consider documenting dependency lifecycle expectations if instances may be disposed during app runtime.

GetX's lazyPut without fenix: true (the default) removes the factory after instance disposal, preventing automatic recreation on subsequent Get.find() calls. If your application's SmartManagement disposes these dependencies (AIDataSourceImpl, AIDataSource, etc.) and they need to be recreated, add fenix: true to preserve the factory for resurrection. Alternatively, use permanent: true if they should never be auto-disposed. Without visibility into the disposal patterns, clarify the intended lifecycle for these bindings.

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_menu.dart (2)

207-248: Submenu panel lacks dismiss-on-outside-tap behavior.

The _SubmenuPanel displays as a positioned overlay but doesn't handle taps outside the submenu to dismiss it. Users may expect tapping outside to close the submenu.

Consider wrapping with a full-screen GestureDetector to capture outside taps:

     return Stack(
       children: [
+        Positioned.fill(
+          child: GestureDetector(
+            onTap: widget.onDismiss,
+            behavior: HitTestBehavior.translucent,
+            child: const SizedBox.expand(),
+          ),
+        ),
         Positioned(
           left: adjustedLeft,
           top: adjustedTop,

118-128: Silent no-op when category has no actions.

If a category without a submenu has an empty actions list, onTap does nothing. Consider adding a guard or logging for this edge case.

scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart (2)

76-92: Consider extracting duplicated positioned dialog wrapper.

The pattern of wrapping content with PointerInterceptor, GestureDetector, and Positioned inside a Stack is repeated for both dialog phases. A helper widget could reduce duplication.

Also applies to: 137-153


201-216: Throwing generic exceptions loses error context.

Using throw Exception(...) discards typed failure information. Consider propagating the original failure or using a custom exception type for better error handling upstream.

lib/features/composer/presentation/composer_view_web.dart (1)

563-564: Minor: Repeated AIConfig.isAiEnabled checks could be consolidated.

The same ternary pattern appears twice. Consider caching the result or extracting to a local variable if the config is static.

Also applies to: 839-840

lib/features/composer/presentation/composer_controller.dart (1)

872-884: HTML-to-text conversion has limited entity support.

The manual entity replacement handles only 6 common HTML entities. Consider using a dedicated HTML parsing library for more robust conversion, especially for user-generated content that may contain numeric entities (&#123;) or other named entities.

+import 'package:html/parser.dart' as html_parser;
+
 String convertHtmlContentToTextContent(String htmlContent) {
-    String textContent = htmlContent.replaceAll(RegExp(r'<[^>]*>'), '');
-
-    textContent = textContent
-      .replaceAll('&nbsp;', ' ')
-      .replaceAll('&amp;', '&')
-      .replaceAll('&lt;', '<')
-      .replaceAll('&gt;', '>')
-      .replaceAll('&quot;', '"')
-      .replaceAll('&#39;', "'");
-
-    return textContent.trim();
+    final document = html_parser.parse(htmlContent);
+    return document.body?.text ?? '';
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 986ddb9 and a5720ac.

⛔ Files ignored due to path filters (1)
  • scribe/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • lib/features/composer/presentation/composer_bindings.dart (2 hunks)
  • lib/features/composer/presentation/composer_controller.dart (7 hunks)
  • lib/features/composer/presentation/composer_view.dart (6 hunks)
  • lib/features/composer/presentation/composer_view_web.dart (11 hunks)
  • lib/features/composer/presentation/view/mobile/mobile_editor_view.dart (3 hunks)
  • lib/features/composer/presentation/view/web/web_editor_view.dart (8 hunks)
  • lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart (2 hunks)
  • lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart (3 hunks)
  • lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart (3 hunks)
  • lib/features/composer/presentation/widgets/web/web_editor_widget.dart (8 hunks)
  • lib/l10n/intl_fr.arb (1 hunks)
  • lib/l10n/intl_messages.arb (2 hunks)
  • lib/l10n/intl_ru.arb (1 hunks)
  • lib/l10n/intl_vi.arb (1 hunks)
  • lib/main/bindings/main_bindings.dart (2 hunks)
  • lib/main/localizations/app_localizations.dart (1 hunks)
  • scribe/lib/scribe.dart (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_messages.arb (1 hunks)
  • scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_bar.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_button.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_menu.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart (1 hunks)
  • scribe/pubspec.yaml (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-09T09:36:45.349Z
Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4194
File: lib/features/manage_account/presentation/manage_account_dashboard_controller.dart:174-176
Timestamp: 2025-12-09T09:36:45.349Z
Learning: In Dart/Flutter projects using GetX, do not wrap getBinding<T>() calls in try/catch since they return null when not found. Only wrap Get.find<T>() calls in try/catch because they throw if a dependency is unavailable. When a binding is optional, consider checking Get.isRegistered<T>() or handling the null/exception path gracefully instead of blindly catching, and document the expectation for failure modes where a dependency may not be registered.

Applied to files:

  • scribe/lib/scribe.dart
  • lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart
  • lib/main/localizations/app_localizations.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_bar.dart
  • lib/features/composer/presentation/view/web/web_editor_view.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart
  • lib/features/composer/presentation/composer_bindings.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_button.dart
  • lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart
  • lib/features/composer/presentation/view/mobile/mobile_editor_view.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_menu.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart
  • lib/features/composer/presentation/widgets/web/web_editor_widget.dart
  • scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart
  • scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart
  • lib/main/bindings/main_bindings.dart
  • lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
  • lib/features/composer/presentation/composer_controller.dart
  • lib/features/composer/presentation/composer_view_web.dart
  • lib/features/composer/presentation/composer_view.dart
📚 Learning: 2025-12-09T12:47:45.861Z
Learnt from: zatteo
Repo: linagora/tmail-flutter PR: 4196
File: scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart:34-35
Timestamp: 2025-12-09T12:47:45.861Z
Learning: In the tmail-flutter repository, avoid suggesting replacing DioError with DioException unless there is a codebase-wide migration. This pattern applies to all Dart files; when reviewing, only propose a DioError-safe change (e.g., compatible error handling or conversion) unless a global migration is in place. Ensure consistency of DioError usage across modules and flag any deviations for a repo-wide decision.

Applied to files:

  • scribe/lib/scribe.dart
  • lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart
  • lib/main/localizations/app_localizations.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_bar.dart
  • lib/features/composer/presentation/view/web/web_editor_view.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart
  • lib/features/composer/presentation/composer_bindings.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_button.dart
  • lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart
  • lib/features/composer/presentation/view/mobile/mobile_editor_view.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_menu.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart
  • lib/features/composer/presentation/widgets/web/web_editor_widget.dart
  • scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart
  • scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart
  • lib/main/bindings/main_bindings.dart
  • lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
  • lib/features/composer/presentation/composer_controller.dart
  • lib/features/composer/presentation/composer_view_web.dart
  • lib/features/composer/presentation/composer_view.dart
🪛 GitHub Actions: Analyze and test
scribe/lib/scribe.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/main/localizations/app_localizations.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_bar.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/view/web/web_editor_view.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/composer_bindings.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_button.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/view/mobile/mobile_editor_view.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_menu.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart

[warning] 208-208: UNNECESSARY_NULL_COMPARISON: The operand can't be 'null', so the condition is always 'false'.

lib/features/composer/presentation/widgets/web/web_editor_widget.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/main/bindings/main_bindings.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/composer_controller.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/composer_view_web.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

lib/features/composer/presentation/composer_view.dart

[error] 1-1: The process '/opt/hostedtoolcache/flutter/bin/cache/dart-sdk/bin/dart' failed with exit code 3

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build web version and deploy
🔇 Additional comments (31)
scribe/lib/scribe/ai/l10n/intl_messages.arb (1)

2-2: Timestamp update is fine

@@last_modified looks consistent with normal ARB metadata updates; no functional impact.

lib/l10n/intl_fr.arb (1)

5476-5482: French aiAssistant translation looks good

Key/value "aiAssistant": "Assistant IA" and its metadata follow the existing ARB pattern and should integrate cleanly with the new localization getter.

scribe/lib/scribe.dart (1)

11-11: Export path is correct and file is properly integrated

The file scribe/ai/data/config/ai_config.dart exists and is already used internally by ai_datasource_impl.dart. The export follows the established pattern for data layer exports in this file and poses no buildability concerns.

lib/l10n/intl_messages.arb (1)

2-2: New aiAssistant localization correctly integrated

The @@last_modified timestamp update and the new "aiAssistant" entry with @aiAssistant metadata follow the existing ARB structure and naming conventions. The corresponding getter is properly wired in AppLocalizations (lib/main/localizations/app_localizations.dart, line 5440) and is already being used in the UI components at lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart and lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart. Translations are present in multiple language files (Vietnamese, Russian, French).

lib/l10n/intl_ru.arb (1)

5494-5500: LGTM!

The new aiAssistant localization entry follows the established ARB format and the Russian translation "Помощник ИИ" is appropriate for "AI Assistant".

lib/features/composer/presentation/widgets/web/web_editor_widget.dart (5)

81-81: Mixin integration looks correct.

The state class properly mixes in TextSelectionMixin and overrides the onSelectionChanged getter to forward to the widget's callback. This is a clean pattern for sharing selection handling logic.

Also applies to: 96-97


115-126: Selection change listener follows the established drop listener pattern.

The implementation mirrors the existing _dropListener setup, which is good for consistency. The listener properly:

  1. Checks for MessageEvent type
  2. Decodes JSON data
  3. Validates the message name before calling handleSelectionChange

146-149: Proper cleanup prevents memory leaks.

The dispose method correctly removes the selection change listener and nullifies the reference, mirroring the cleanup pattern used for the drop listener.


218-222: Registration guard prevents duplicate listener registration.

The _selectionChangeListenerRegistered flag ensures the JavaScript listener is only registered once, consistent with the drop listener pattern.


9-9: Remove this review comment.

The TextSelectionMixin import is correctly implemented. The mixin exists at the referenced path and properly exports the handleSelectionChange method and onSelectionChanged getter. The pipeline failure is unrelated to this import.

Likely an incorrect or invalid review comment.

lib/features/composer/presentation/view/mobile/mobile_editor_view.dart (2)

22-22: LGTM - Clean callback pass-through.

The onTextSelectionChanged callback is properly declared and accepted via constructor, following the existing pattern for other callbacks like onEditorContentHeightChanged.

Also applies to: 31-31


142-153: Callback correctly wired to MobileEditorWidget.

The buildEditor method properly passes the onTextSelectionChanged callback to MobileEditorWidget, enabling text selection events to propagate up to the composer.

lib/features/composer/presentation/composer_bindings.dart (2)

77-77: No action needed — the scribe package is properly configured.

The scribe package is declared as a local path dependency in pubspec.yaml (path: scribe), and the import path correctly references the existing file at scribe/lib/scribe/ai/domain/usecases/generate_ai_text_interactor.dart. This is not an external dependency issue.


342-342: GenerateAITextInteractor is registered globally — this is intentional and safe.

The global registration (without tag: composerId) is correct. GenerateAITextInteractor is registered in AIScribeBindings and initialized via main_bindings.dart before ComposerBindings loads. No defensive checks are needed; the pattern is consistent with the codebase and guarantees availability.

Likely an incorrect or invalid review comment.

lib/l10n/intl_vi.arb (1)

5477-5488: New aiAssistant VI localization looks correct

Key and metadata are consistent with the existing ARB structure, and "Trợ lý AI" is an appropriate translation for the assistant label.

lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart (1)

34-36: AIScribe button integration into bottom bar is consistent and safe

  • Adding onOpenAIScribe as nullable and gating the button with if (onOpenAIScribe != null) keeps the API backward‑compatible and avoids null issues when invoking the callback.
  • Using AbsorbPointer and icon color changes tied to isCodeViewEnabled matches the existing behavior of other formatting actions, so the UX remains coherent.
  • Passing aiScribeButtonKey through as the widget key is a reasonable way to expose this control to parents that need positioning/measurement.

No issues from a correctness standpoint.

Also applies to: 59-61, 134-152

lib/features/composer/presentation/view/web/web_editor_view.dart (1)

39-39: LGTM!

The onTextSelectionChanged callback is correctly added as an optional field and consistently forwarded to all WebEditorWidget instantiations across all code paths (compose, edit draft, reply/forward, and default cases).

Also applies to: 63-63, 96-96

lib/main/bindings/main_bindings.dart (1)

25-25: LGTM!

AIScribeBindings is correctly integrated into the main dependency graph. The placement after network/session bindings and before platform-specific bindings is appropriate.

lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart (1)

78-89: LGTM!

The AI Scribe button is conditionally rendered with proper null checking and consistent styling. The spread operator pattern (if ... [...,]) cleanly integrates the optional button into the row.

scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart (1)

25-27: Potential issue: Get.find inside lazyPut builder may throw if dependency isn't resolved yet.

The _bindingsDataSource uses Get.find<AIDataSourceImpl>() inside the lazyPut builder. Since lazyPut defers creation until first access, if AIDataSource is requested before AIDataSourceImpl is ever accessed, this could fail. The current call order in dependencies() registers them correctly, but the actual instantiation happens lazily.

This should work correctly as long as consumers only access GenerateAITextInteractor (which cascades the resolution), but verify the access patterns are correct.

Based on learnings, Get.find<T>() throws if a dependency is unavailable.

lib/features/composer/presentation/composer_view.dart (1)

473-474: LGTM!

The AI Scribe props are correctly passed conditionally based on AIConfig.isAiEnabled, keeping the feature properly gated.

scribe/lib/scribe/ai/presentation/widgets/ai_scribe_menu.dart (1)

28-46: LGTM! Proper overlay lifecycle management.

The overlay cleanup in dispose() prevents memory leaks, and the _removeSubmenu() helper correctly nullifies the reference after removal.

scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart (1)

161-192: LGTM! Edge-fitting logic is well-implemented.

The _calculateModalPosition function correctly handles horizontal and vertical screen boundary constraints with appropriate padding.

lib/features/composer/presentation/composer_view_web.dart (2)

271-272: LGTM! Consistent text selection callback wiring.

The onTextSelectionChanged callback is correctly wired across all three responsive layouts (mobile, desktop, tablet), ensuring consistent AI Scribe behavior.

Also applies to: 515-516, 792-793


960-998: LGTM! AIScribe selection button implementation is sound.

The button correctly:

  • Guards on AIConfig.isAiEnabled
  • Uses Obx for reactive updates
  • Handles null coordinates gracefully
  • Uses Builder to obtain button context for position calculation
scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart (2)

1-85: LGTM! Well-organized design token system.

The centralized styling approach with clear categorization (colors, shadows, text styles, button styles, sizes) promotes consistency and maintainability across AIScribe UI components.


2-3: Remove the verification request—both imports are actively used.

The imports color_extension.dart and theme_utils.dart are necessary and actively used throughout the file:

  • AppColor is referenced 5 times (lines 28, 29, 31, 35, 39)
  • ThemeUtils is referenced 5 times (lines 28, 29, 30, 31, 35)

No action is needed on these imports.

lib/features/composer/presentation/composer_controller.dart (4)

162-164: LGTM! Reactive state for text selection tracking.

Using Rxn<String> and Rxn<Offset> with GetX observables enables reactive UI updates when selection changes.


909-928: LGTM! Proper context.mounted check after async gap.

The context.mounted check at line 918 correctly guards against using a stale context after the async getTextOnlyContentInEditor() call.


946-964: LGTM! Selection state management is well-structured.

The handleTextSelection method cleanly handles both the selection present and absent cases, updating all three reactive fields consistently.


899-907: Simple newline-to-br conversion may not preserve all formatting.

Replacing \n with <br> works for plain text but may produce unexpected results if the AI generates content with existing HTML. Consider whether the AI response should be sanitized or escaped first.

- Button : the button with sparkle icon to display AI scribe
- Menu : the menu where you can select the action (eg: translate)
- Bar : the input where you can write a custom action
- Suggestion : the wrapper of the result of the LLM
Editor exposes html content (so with html tags) and we do not want to send them to AI backend
@dab246
Copy link
Member

dab246 commented Dec 11, 2025

Screenshot 2025-12-11 at 13 13 41
  • IMO, We should show submenus when hovering over an item, instead of clicking, so users know which item is selected and which submenu it belongs to.

@dab246
Copy link
Member

dab246 commented Dec 11, 2025

  • What's about mobile?

Comment on lines +80 to +88
TMailButtonWidget.fromIcon(
key: aiScribeButtonKey,
icon: imagePaths.icSparkle,
borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
padding: TabletBottomBarComposerWidgetStyle.iconPadding,
iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
tooltipMessage: AppLocalizations.of(context).aiAssistant,
onTapActionCallback: onOpenAIScribe!,
),
Copy link
Member

Choose a reason for hiding this comment

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

We should create a separate widget for it so we can reuse it in multiple places.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure? I don't know, except mutualizing the icon, it does not mutualize a lot because it must have the style of bottom bar buttons.

final VoidCallback saveAsTemplateAction;
final VoidCallback onOpenInsertLink;
final VoidCallback? onOpenAIScribe;
final GlobalKey? aiScribeButtonKey;
Copy link
Member

Choose a reason for hiding this comment

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

IMO, the aiScribe widget should hold the GlobalKey so that it can manage its own key, preventing widgets holding keys from causing memory leaks.

Copy link
Member Author

Choose a reason for hiding this comment

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

@dab246 Who is "the aiScribe widget"? In my case I need it at the top in the controller to position the scribe menu according the position of the bottom bar button.

Comment on lines 116 to 122
if (event is MessageEvent) {
final data = jsonDecode(event.data);

if (data['name'] == HtmlUtils.registerSelectionChangeListener.name) {
handleSelectionChange(data);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Should use try/catch for this

}
};
if (_selectionChangeListener != null) {
window.addEventListener("message", _selectionChangeListener!);
Copy link
Member

Choose a reason for hiding this comment

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

We should listen for the message event in one place to avoid calling it multiple times. We could reuse window.addEventListener("message", _dropListener!);, but it should be modified appropriately for general use, as we might have multiple features that need to be handled in this function.

Comment on lines 162 to 164
final selectedText = Rxn<String>();
final hasTextSelection = false.obs;
final textSelectionCoordinates = Rxn<Offset>();
Copy link
Member

Choose a reason for hiding this comment

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

IMO, these values ​​are always set together, so we don't need too many values ​​to observe here. If you absolutely need these three values, you should create an object to contain them and just observe this object to update the UI.

Comment on lines 872 to 884
String convertHtmlContentToTextContent(String htmlContent) {
String textContent = htmlContent.replaceAll(RegExp(r'<[^>]*>'), '');

textContent = textContent
.replaceAll('&nbsp;', ' ')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'");

return textContent.trim();
}
Copy link
Member

Choose a reason for hiding this comment

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

It should be placed in the util class within the core module for reuse.

Comment on lines 101 to 103
children: categories.map((category) {
return _buildCategoryItem(category);
}).toList(),
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
children: categories.map((category) {
return _buildCategoryItem(category);
}).toList(),
children: categories.map(_buildCategoryItem).toList(),

Comment on lines 109 to 111
return Container(
key: _categoryKeys[category],
child: _buildMenuItem(
Copy link
Member

Choose a reason for hiding this comment

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

Why not just pass the key directly to _buildMenuItem?

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually it has no purpose anymore and I forgot to remove it during refactoring and commit merging. I removed it.

@dab246
Copy link
Member

dab246 commented Dec 11, 2025

  • Fix failed unit tests
Screenshot 2025-12-11 at 13 44 43

Instead of listening multiple times for message from editor JS side,
let's add only one listener and route inside the callback.
hasTextSelection had no interest.

The other two values have been put inside an object to observe only one object.
@zatteo
Copy link
Member Author

zatteo commented Dec 11, 2025

Screenshot 2025-12-11 at 13 13 41
* [ ]  IMO, We should show submenus when hovering over an item, instead of clicking, so users know which item is selected and which submenu it belongs to.

Done in a241bdb. Idea confirmed by the designer 🧑‍🎨

@zatteo
Copy link
Member Author

zatteo commented Dec 11, 2025

  • What's about mobile?

If you don't mind, I propose to do another PR for it. See mockups below. Very different.

image

Avoid adding too much code in the controller
If it was opened at the top of the composer, it could go off screen to the top.

modalHeight was missing so the calculateModalPosition method could not protect the AI scribe menu to go off screen from the top.
@chibenwa
Copy link
Member

I see in last comment we rely on .env to selectively enable AI support.

I understand this is simple but:

  • It adds configuration that can easily be overlooked. I'd prefer have only conf on the back and have the front self adapt.
  • It won't work on mobile

The way we generally move forward is by leveraging a JMAP capability to advertize back support.

CF https://github.com/linagora/tmail-backend/blob/master/tmail-backend/tmail-third-party/ai-bot/AIChatCompletionRoutes.adoc#additions-to-the-capability-object

Given that we already have such mechanism we can very likely just drop the .env stuff.

"type": "text",
"placeholders_order": [],
"placeholders": {}
},
Copy link
Member

@chibenwa chibenwa Dec 12, 2025

Choose a reason for hiding this comment

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

I do really love the work done on modularization and only seeing onXXX events in main files, and a simple binding for the feature.

We did not manage however to get a modular system for the I10n ?

(notpick I know.... feel free to overlook it for now...)

Edit: I did spot some I10n for scribe below. I bet it's for the main menu entry?

@chibenwa
Copy link
Member

<3 <3 <3 <3 <3 <3 <3

@Crash--
Copy link

Crash-- commented Dec 12, 2025

The way we generally move forward is by leveraging a JMAP capability to advertize back support.

@chibenwa next PR #4203

@chibenwa
Copy link
Member

@chibenwa next PR #4203

Too much PR exception, the recovery from OSXP is a bit harsh :-p

(and I did do homeworks in the train!)

Comment on lines +200 to +210
static String convertHtmlContentToTextContent(String htmlContent) {
String textContent = htmlContent.replaceAll(RegExp(r'<[^>]*>'), '');

textContent = textContent
.replaceAll('&nbsp;', ' ')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'");

Copy link
Member

Choose a reason for hiding this comment

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

What do you think of this piece of Flutter code? Should I roast it?

🔍 What’s good

  • The function is simple and readable.
  • For very basic HTML, it might work.

But…


⚠️ The issues (yes, you can roast this code 😄)

1. Using a regex to strip HTML tags…

This is always fragile.

Problems you will eventually hit:

  • Tags with attributes containing >
  • Comments, scripts, styles
  • Broken or unusual HTML
  • Nested content where regex fails

👉 There’s a reason HTML parsers exist.


2. Only a few HTML entities are handled

There are thousands of HTML entities.
Your code handles maybe 5.

Anything like &eacute;, &copy;, &hellip;, etc. will remain unchanged.


3. No handling of <br>, <p>, etc.

All line breaks will disappear.
Your output becomes a single messy block:

"Line1Line2Line3"


4. Whitespace is not normalized

HTML often contains multiple spaces, newlines, indentation.
Your output might be weirdly spaced.


✔️ A proper solution in Flutter

Use the html package:

import 'package:html/parser.dart' as html;

String convertHtmlContentToTextContent(String htmlContent) {
  final document = html.parse(htmlContent);
  final String parsedString = document.body?.text ?? '';
  return parsedString.trim();
}

Why this is better:

  • Correctly strips tags
  • Correctly decodes all HTML entities (&eacute;é)
  • More robust
  • Easier to maintain

🧠 Conclusion

Your current code works only for very simple HTML and is not reliable for real-world HTML content.

👉 Strong recommendation: use an HTML parser instead.

Comment on lines +200 to +210
static String convertHtmlContentToTextContent(String htmlContent) {
String textContent = htmlContent.replaceAll(RegExp(r'<[^>]*>'), '');

textContent = textContent
.replaceAll('&nbsp;', ' ')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'");

Copy link
Member

Choose a reason for hiding this comment

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

Redos vulnerable?

Short answer: Yes — this regex can be ReDoS-vulnerable depending on the input.

Let’s break it down clearly.


Your regex

RegExp(r'<[^>]*>')

⚠️ Is it vulnerable to ReDoS?

Potentially, yes — in specific edge cases.

Reason:

  • <[^>]*> uses [^>] with *, which is a greedy, unbounded quantifier.
  • On malformed or adversarial input designed to confuse the engine, it can trigger catastrophic backtracking.

Example of problematic input

A long string like:

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<!

Or nested angled brackets like:

< < < < < < < < < < < < < < x

The regex tries to match:

  • The opening <
  • Then tries to find the next >, but finds none
  • It backtracks repeatedly through a huge number of combinations

This can cause:

  • CPU spike
  • App freeze
  • DoS in worst cases

🔍 Why this happens

The [^>] is fine by itself, but when used like this:

< [^>]* >

…it forces the engine to try every possible way to match the sequence when no matching > exists.

This is a textbook case of a super-linear regex pattern.


✔️ How to avoid ReDoS

1. Use a non-greedy version

RegExp(r'<[^>]*?>');

Still fragile, but less prone to catastrophic backtracking.


2. Use a possessive quantifier (not available in Dart RegExp 😞)

In languages like Java:

<[^>]*+>

Dart does not support this — so you can’t fully fix it with regex alone.


3. Best solution: don’t use regex for HTML parsing

Dart has an HTML parser that avoids regex pitfalls entirely:

import 'package:html/parser.dart' as html;

String convertHtmlContentToTextContent(String htmlContent) {
  final document = html.parse(htmlContent);
  return document.body?.text.trim() ?? '';
}

This eliminates:

  • ReDoS risk
  • Invalid HTML issues
  • Missed entities
  • Missing line breaks

🧠 Conclusion

Yes, your regex can be ReDoS-vulnerable with adversarial input.

If this code runs on:

  • user-generated content
  • emails
  • scraped HTML
  • chat messages

👉 Do NOT use regex. Use an HTML parser.

If you want, I can also analyze your specific use-case and tell you the actual risk level.

Copy link
Member

Choose a reason for hiding this comment

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

(Or use a re2 engine without backtracking if really forced with a regexp)

Comment on lines +200 to +210
static String convertHtmlContentToTextContent(String htmlContent) {
String textContent = htmlContent.replaceAll(RegExp(r'<[^>]*>'), '');

textContent = textContent
.replaceAll('&nbsp;', ' ')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'");

Copy link
Member

Choose a reason for hiding this comment

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

And yes, I fucking hate REGEXPs...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants