Skip to content

Conversation

@zatteo
Copy link
Member

@zatteo zatteo commented Dec 2, 2025

Follow the work of #4172. Mockups are finalized and the goal of this PR is to go to production contrary to the previous one which was more a draft to play and learn.

Still some work to do, but already creating the PR to get first feedbacks especially about usage of components, controllers, mixins, code sharing, respect of convention, etc.

Mockups

Screenshot 2025-12-02 at 16 05 38 Screenshot 2025-12-02 at 16 05 23

Current status

Screen.Recording.2025-12-02.at.16.19.26.mov

Todo

To start, I will not implement every features from the mockup. I consider enough for a first version if I add:

  • ability to write custom prompt
  • copy result in clipboard
  • mobile version (full screen instead of popup menu and popup modal)

To play with it

Add in .env

AI_ENABLED=true
AI_API_KEY=<key from ai.linagora.com>
AI_API_URL=https://ai.linagora.com/api/v1

Summary by CodeRabbit

  • New Features

    • AI Scribe assistant added to the composer: generate, correct, rephrase, translate, change tone, and format text.
    • Selection-aware workflow across web, mobile, and iframe/webview: detect text selection and show an in-editor AIScribe trigger to act on selected text or full message.
    • In-editor UI: selection-triggered AIScribe button, bottom-bar AI button, dialog and suggestion modal for inserting AI results.
    • New localized label "AI Assistant" with translations (en/fr/ru/vi).
  • Chores

    • New AI package integrated and build/localization steps updated.

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

AI_ENABLED allows to test AI features only in some wanted environment.

AI_API_KEY and AI_API_URL allows to call the LLM while we wait for tmail backend endpoints.
It will be removed when tmail backend endpoints are ready.
}

static String correctGrammar(String text) {
return 'Correct grammar of the following text. Output only the result. No formatting. Text:\n\n$text';
Copy link
Member

Choose a reason for hiding this comment

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

How about

Suggested change
return 'Correct grammar of the following text. Output only the result. No formatting. Text:\n\n$text';
return 'Correct grammar of the following text. Output only the result. Preserve input formatting. Text:\n\n$text';

Copy link
Member Author

Choose a reason for hiding this comment

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

I updated it.

When we will move this prompt somewhere else, we will need to find a proper way of evaluating these prompts.

Comment on lines 10 to 13
case AIScribeMenuAction.changeToneFriendly:
return translateTo(text, 'friendly');
case AIScribeMenuAction.changeToneProfessionnal:
return translateTo(text, 'professionnal');
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
case AIScribeMenuAction.changeToneFriendly:
return translateTo(text, 'friendly');
case AIScribeMenuAction.changeToneProfessionnal:
return translateTo(text, 'professionnal');
case AIScribeMenuAction.changeToneFriendly:
return changeToneTo(text, 'friendly');
case AIScribeMenuAction.changeToneProfessionnal:
return changeToneTo(text, 'professionnal');

?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, copy/paste mistake.

),
child: Text(
action.label,
style: AIScribeTextStyles.menuItem,
Copy link
Member

@chibenwa chibenwa Dec 2, 2025

Choose a reason for hiding this comment

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

o.O

Too much ident level.

Is thi idiomatic from this code base?

this.arguments,
this.contentViewState,
this.onEditorContentHeightChanged,
this.onTextSelectionChanged,
Copy link
Member

Choose a reason for hiding this comment

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

I would LOVE to have a separated commit for adding onTextSelectionChanged to the composer.

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you think now?

onTapActionCallback: onOpenInsertLink,
),
),
if (onOpenAIScribe != null) ...[
Copy link
Member

@chibenwa chibenwa Dec 2, 2025

Choose a reason for hiding this comment

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

Two things:

  • The button should be show if and only if the AI capability is advertized by the bac IMO

CF com:linagora:params:jmap:aibot is present in the JMAP capabilities

  • I'd love to have one more indirection level so that regular tmail code has no direct dependencies onto AI components.

Acceptence criteria:

  • All AI related changes are in the ai subfolder
  • We actually only use declarative registration: plug the AI module.

(Idealy I would love to have a central slim code base and a code base with TWP extensions like AI, Drive integration, top bar integratins etc)

Copy link
Member Author

Choose a reason for hiding this comment

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

I will try to go to this way 👍

String get label {
switch (this) {
case AIScribeMenuAction.correctGrammar:
return 'Correct grammar';
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 intl instead


GenerateAITextInteractor(this._repository);

Future<Either<Failure, Success>> execute(
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 Stream, it makes UI more flexible to react

@hoangdat
Copy link
Member

hoangdat commented Dec 4, 2025

Hi @zatteo ,

To make it easier to support you, you should clean the history a bit:

  • create a separate module (which similar with rule_filter, server_settings, ...): scribe
  • dedicated commits for abstract layer for AI: repository, data source,
  • dedicated commits for implementation: interactors, repository, data source, API (maybe test for interactor)
  • commits for Controller in each use-case
  • one commit for UI components
  • one commit for integrate things, also binding component with Getx

For this, we can easily understand the logic behind scribe, and maybe can support you directly in UI components to make it a perfect fit with prototype. Moreover, it will help us easily to integrate with BE, when BE ready a capability for it.

@zatteo
Copy link
Member Author

zatteo commented Dec 4, 2025

* create a separate module (which similar with `rule_filter`, `server_settings`, ...): `scribe`

So basically move lib/features/ai in ai/lib?

* dedicated commits for abstract layer for AI: repository, data source,

OK

* dedicated commits for implementation: interactors, repository, data source, API (maybe test for interactor)

OK

* commits for Controller in each use-case

What do you mean by use case here?

* one commit for UI components

OK

* one commit for integrate things, also binding component with Getx

OK will try to learn more about Getx

@zatteo
Copy link
Member Author

zatteo commented Dec 9, 2025

Every comments have been answered.

AI scribe UI is displayed only when env var AI_ENABLED is true.

Because scribe will be potentially separated in another library later, I created an independant i18n for scribe.

In another PR because this one is already too big, I will manage request to backend because it has been merged (see PR) and mobile version.

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: 3

♻️ Duplicate comments (5)
lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart (1)

78-89: Tooltip localization has been addressed.

The tooltip now correctly uses AppLocalizations.of(context).aiAssistant for i18n support, consistent with other buttons in this widget.

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

15-23: Regarding the past review on DI: This design is acceptable.

The GenerateAITextInteractor is passed as a parameter, which is a valid form of dependency injection (method injection). This allows the caller (e.g., a GetX controller) to obtain the interactor via GetX and pass it here, keeping this utility function decoupled from the DI framework. This addresses the previous review concern about presentation calling data layer directly.

scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.dart (2)

18-46: LGTM - Localization properly implemented via ScribeLocalizations.

This addresses the past review comments about using intl. Labels are now fetched from ScribeLocalizations, enabling proper localization support.


4-77: LGTM - Methods defined directly in enum.

This addresses the past review comment about not needing extensions for enums. The getLabel, category, and getFullLabel methods are correctly defined within the enum body.

scribe/lib/scribe/ai/presentation/model/ai_action.dart (1)

20-28: Localization support addresses past review comment.

CustomPromptAction.getLabel now uses ScribeLocalizations.of(context)!.customPromptAction, providing multi-language support as requested in the previous review.

🧹 Nitpick comments (15)
scribe/pubspec.yaml (2)

9-31: Verify all dependencies are actively used.

Given the previous feedback to "import dependencies you need," please confirm each public dependency is actively used in the implementation:

  • dartz: functional Either/Option types?
  • dio: HTTP requests to AI API?
  • flutter_dotenv: environment configuration?
  • get: state management and GetX bindings?
  • pointer_interceptor: UI event handling?
  • intl: localization strings?

If any of these are unused or only conditionally imported, remove them or clearly document their purpose.


43-46: Pin intl_generator to a specific commit hash instead of master branch.

Pinning to the master branch introduces a maintenance risk: the branch can change unexpectedly and potentially break reproducible builds. Although the comment explains the fork rationale (original abandoned), it's better to pin to a specific commit hash or release tag.

Update the dependency to pin to a specific commit:

   intl_generator:
     git:
       url: https://github.com/linagora/intl_generator.git
-      ref: master
+      ref: <specific-commit-hash-or-tag>

Check the linagora/intl_generator repository for the latest stable commit or tag to use.

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

5439-5445: aiAssistant getter matches existing localization patterns; consider capitalization alignment.

The new aiAssistant getter is correctly defined and consistent with other Intl.message usages. To avoid translator confusion, consider ensuring the English default text here uses the same capitalization as the corresponding aiAssistant entries in your ARB files (e.g. either all "AI Assistant" or all "AI assistant").

scribe/lib/scribe/ai/l10n/intl_en.arb (1)

1-129: intl_en ARB structure and key set look solid; a few copy nits are optional

The file is well-structured (keys + @metadata pairs, no placeholders needed) and the naming scheme is consistent and clear for the AI Scribe feature. Only optional UX wording tweaks you might consider:

  • actionTransformToBullets: something like “Transform into bullet list” or “Convert to bullet points” may read a bit more naturally.
  • noDataReceived: “No response received from AI” or “No response received” can be clearer than the more technical-sounding “No data received”.

These are purely copy choices; structurally the file is good to go.

scribe/lib/scribe.dart (1)

1-16: Consider separating public API from implementation exports.

The library exports both abstractions (ai_scribe_repository.dart) and implementations (ai_repository_impl.dart, ai_datasource.dart). For cleaner API boundaries, consider:

  1. A main export for consumers (domain + presentation only)
  2. A separate export file for DI/bindings (e.g., scribe_bindings.dart) containing implementations

This is optional and depends on how tightly coupled the consumer is expected to be.

 library scribe;
 
+// Domain layer (public API)
 export 'scribe/ai/domain/usecases/generate_ai_text_interactor.dart';
 export 'scribe/ai/domain/repository/ai_scribe_repository.dart';
 export 'scribe/ai/domain/model/ai_response.dart';
 export 'scribe/ai/domain/state/generate_ai_text_state.dart';
+
+// Presentation layer (public API)
 export 'scribe/ai/presentation/widgets/ai_scribe.dart';
 export 'scribe/ai/presentation/widgets/ai_scribe_bar.dart';
 export 'scribe/ai/presentation/widgets/ai_scribe_button.dart';
 export 'scribe/ai/presentation/widgets/ai_scribe_menu.dart';
 export 'scribe/ai/presentation/widgets/ai_scribe_suggestion.dart';
 export 'scribe/ai/presentation/model/ai_action.dart';
 export 'scribe/ai/presentation/model/ai_scribe_menu_action.dart';
+
+// Data layer (for DI wiring)
 export 'scribe/ai/data/repository/ai_repository_impl.dart';
 export 'scribe/ai/data/datasource/ai_datasource.dart';
 export 'scribe/ai/data/config/ai_config.dart';
lib/features/composer/presentation/composer_view.dart (1)

557-595: RTL support properly implemented with PositionedDirectional.

Good use of PositionedDirectional with start instead of Positioned with left for RTL language support.

Consider extracting the magic number to a style constant:

-        // Account for the horizontal padding around the editor
-        const editorHorizontalPadding = 12.0;
+        // Account for the horizontal padding around the editor
+        final editorHorizontalPadding = ComposerStyle.mobileEditorPadding.horizontal / 2;

Alternatively, define this value in ComposerStyle if it's not already derivable from existing constants.

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

174-189: Add user feedback after clipboard copy action.

The copy button silently copies to clipboard without informing the user. Consider showing a snackbar or tooltip confirmation.

   Widget _buildToolbar(String suggestion) {
     return Container(
       padding: const EdgeInsets.all(8),
       child: Row(
         children: [
-          IconButton(
-            icon: const Icon(Icons.content_copy, size: 18),
-            color: Colors.grey[600],
-            onPressed: () {
-              Clipboard.setData(ClipboardData(text: suggestion));
-            },
+          Tooltip(
+            message: ScribeLocalizations.of(context)!.copyToClipboard,
+            child: IconButton(
+              icon: const Icon(Icons.content_copy, size: 18),
+              color: Colors.grey[600],
+              onPressed: () {
+                Clipboard.setData(ClipboardData(text: suggestion));
+                ScaffoldMessenger.of(context).showSnackBar(
+                  SnackBar(
+                    content: Text(ScribeLocalizations.of(context)!.copiedToClipboard),
+                    duration: const Duration(seconds: 2),
+                  ),
+                );
+              },
+            ),
           ),
         ],
       ),
     );
   }

Note: You'll need to add the localization keys copyToClipboard and copiedToClipboard to your localization files.


27-29: State class naming inconsistency.

The State class is named _AIScribeSuggestionModalState but the widget is AIScribeSuggestion. Convention is to name it _AIScribeSuggestionState.

   @override
-  State<AIScribeSuggestion> createState() => _AIScribeSuggestionModalState();
+  State<AIScribeSuggestion> createState() => _AIScribeSuggestionState();
 }

-class _AIScribeSuggestionModalState extends State<AIScribeSuggestion> {
+class _AIScribeSuggestionState extends State<AIScribeSuggestion> {
scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart (3)

39-41: Consider using theme colors instead of hardcoded Colors.white.

Hardcoded colors reduce flexibility for theming and dark mode support. Consider extracting these to AIScribeColors or using Theme.of(context).

-              Material(
-                color: Colors.white,
+              Material(
+                color: Theme.of(context).colorScheme.surface,

Also applies to: 55-58


30-31: Extract magic numbers to named constants.

Values like 240.0, 500.0, 400.0, 16, and 10 are scattered throughout. Consider defining them in AIScribeSizes for consistency and maintainability.

Also applies to: 135-135


180-195: Error handling loses original failure details.

The fold callback wraps failures in a generic Exception, losing any structured error information. Consider defining a custom exception type or preserving the failure details for better debugging and error handling upstream.

   return result.fold(
     (failure) {
-      throw Exception('Failed to generate AI response: $failure');
+      throw AIScribeException('Failed to generate AI response', failure);
     },
     (success) {
       if (success is GenerateAITextSuccess) {
         final result = success.response.result;
         if (result == null) {
-          throw Exception('AI response result is null');
+          throw AIScribeException('AI response result is null');
         }
         return result;
       } else {
-        throw Exception('Unexpected success state: $success');
+        throw AIScribeException('Unexpected success state: $success');
       }
     },
   );
scribe/lib/scribe/ai/localizations/scribe_localizations.dart (1)

103-112: inputPlaceholder and customPromptAction have identical default text.

Both return 'Help me write'. If this is intentional for UX consistency, consider adding a comment. Otherwise, customPromptAction may need a distinct message.

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

220-223: Hardcoded Colors.white - same concern as ai_scribe.dart.

Consider using theme colors or AIScribeColors for consistency and theming support.


115-125: Edge case: category without actions.

If category.actions is empty, onActionSelected is never called and the tap is silently ignored. Consider logging or disabling the item when no actions exist.

       return _buildMenuItem(
         label: category.getLabel(context),
         onTap: () {
           if (category.actions.isNotEmpty) {
             widget.onActionSelected(category.actions.first);
+          } else {
+            // Consider logging or handling empty category case
           }
         },
       );
scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.dart (1)

99-124: actions getter creates a new list on every call.

Each invocation allocates a new list. If called frequently (e.g., in build methods), consider caching or using const lists.

   List<AIScribeMenuAction> get actions {
     switch (this) {
       case AIScribeMenuCategory.correctGrammar:
-        return [AIScribeMenuAction.correctGrammar];
+        return const [AIScribeMenuAction.correctGrammar];
       case AIScribeMenuCategory.improve:
-        return [
+        return const [
           AIScribeMenuAction.improveMakeShorter,
           ...
         ];
       // ... same for other cases
     }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a32402 and 8e26e85.

⛔ Files ignored due to path filters (1)
  • scribe/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • lib/features/composer/presentation/composer_view.dart (5 hunks)
  • lib/features/composer/presentation/composer_view_web.dart (10 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/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.dart (2 hunks)
  • lib/main/localizations/app_localizations.dart (1 hunks)
  • scribe/lib/scribe.dart (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_en.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_fr.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_messages.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_ru.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_vi.arb (1 hunks)
  • scribe/lib/scribe/ai/localizations/scribe_localizations.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/model/ai_action.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.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_menu.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart (1 hunks)
  • scribe/pubspec.yaml (1 hunks)
  • scripts/prebuild.sh (2 hunks)
✅ Files skipped from review due to trivial changes (2)
  • scribe/lib/scribe/ai/l10n/intl_fr.arb
  • scribe/lib/scribe/ai/l10n/intl_vi.arb
🚧 Files skipped from review as they are similar to previous changes (4)
  • scripts/prebuild.sh
  • lib/l10n/intl_messages.arb
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_bar.dart
  • lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart
⏰ 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). (10)
  • GitHub Check: analyze-test (contact)
  • GitHub Check: Build web version and deploy
  • GitHub Check: analyze-test (server_settings)
  • GitHub Check: analyze-test (rule_filter)
  • GitHub Check: analyze-test (forward)
  • GitHub Check: analyze-test (email_recovery)
  • GitHub Check: analyze-test (default)
  • GitHub Check: analyze-test (fcm)
  • GitHub Check: analyze-test (core)
  • GitHub Check: analyze-test (model)
🔇 Additional comments (20)
scribe/lib/scribe/ai/l10n/intl_ru.arb (1)

1-129: AI Scribe RU ARB looks structurally correct and consistent.

JSON structure, key naming, and @ metadata blocks are valid and aligned with the English AI-scribe keys; Russian labels are clear and use no placeholders, which matches the metadata.

scribe/lib/scribe/ai/l10n/intl_messages.arb (1)

1-129: English AI Scribe ARB is well-formed and mirrors the RU Scribe keys.

Key set and metadata are consistent with the RU Scribe ARB, making it a solid base for additional locales.

lib/l10n/intl_ru.arb (1)

5495-5500: RU translation for aiAssistant is correctly added.

Key name and metadata follow the existing ARB conventions, and "Помощник ИИ" is an appropriate Russian label for “AI assistant”.

lib/main.dart (1)

12-12: Scribe localization delegate is correctly wired into the app.

Importing ScribeLocalizations and adding ScribeLocalizations.delegate alongside existing delegates is consistent with the current localization setup. Ensure that the scribe package is properly declared in pubspec.yaml (as a path or local package) so this import resolves in all environments.

lib/l10n/intl_vi.arb (1)

5482-5488: aiAssistant VI localization looks correct and consistent

Key and metadata follow existing ARB conventions, and the translation “Trợ lý AI” is appropriate; no further changes needed.

lib/l10n/intl_fr.arb (1)

5476-5482: aiAssistant FR localization is well-formed and idiomatic

String “Assistant IA” and its @Aiassistant metadata align with the existing FR ARB style; nothing to adjust.

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

17-18: LGTM! Clean addition of optional AI Scribe parameters.

The nullable fields and optional constructor parameters follow the existing widget pattern, allowing backward compatibility while enabling AI Scribe integration.

Also applies to: 30-31

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

238-269: Good restructuring for AIScribe overlay integration.

The Stack-based approach correctly layers the AIScribe selection button over the editor content while maintaining the existing scroll behavior.


472-473: LGTM! Proper feature flag guards for AI Scribe.

The conditional assignment using AIConfig.isAiEnabled ensures the AI features are only wired when enabled.

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

270-271: LGTM! Text selection callback properly wired.

The onTextSelectionChanged callback is correctly connected to the controller's handler across all responsive views.


562-563: LGTM! Consistent feature flag guards for AI Scribe.

The pattern matches the mobile implementation with proper AIConfig.isAiEnabled checks.

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

11-29: LGTM! Well-structured widget API.

The widget has a clean, focused API with all required dependencies explicitly declared. The use of Future<String> for the suggestion allows flexible async data fetching patterns.

scribe/lib/scribe/ai/localizations/scribe_localizations.dart (3)

136-147: LGTM - Standard LocalizationsDelegate implementation.

The delegate correctly implements isSupported, load, and shouldReload. The supported locales match the available ARB files.


11-20: Global Intl.defaultLocale mutation may cause side effects.

Setting Intl.defaultLocale in the load() method affects the entire application. If multiple localization modules use this pattern, the last one to load wins. Verify whether this is intentional or if the scribe module should rely on the app's already-set locale instead of mutating global state.


7-9: Nullable return type requires careful handling by callers.

of() returns ScribeLocalizations?, but callers use ! (e.g., in ai_scribe_menu_action.dart line 19). If ScribeLocalizations.delegate is not registered in MaterialApp.localizationsDelegates, this will throw at runtime. Ensure proper registration and consider adding a helpful assertion or fallback.

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

27-34: LGTM - Proper initialization and key management.

GlobalKeys are correctly initialized for each category in initState, enabling accurate positioning for submenu overlays.


36-45: LGTM - Proper overlay cleanup in dispose.

Calling _removeSubmenu() in dispose() ensures the overlay is removed when the widget is unmounted, preventing memory leaks and orphaned overlays.

scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.dart (1)

19-19: Potential null pointer if ScribeLocalizations is not registered.

Both getLabel methods use ScribeLocalizations.of(context)!. If the delegate is not registered in the app's localizationsDelegates, this will throw. Consider a defensive fallback or ensure registration is verified at startup.

Also applies to: 86-86

scribe/lib/scribe/ai/presentation/model/ai_action.dart (2)

5-9: LGTM - Well-designed sealed class hierarchy.

Using sealed class enables exhaustive pattern matching in Dart 3, ensuring all action types are handled. The abstract getLabel method enforces consistent labeling across implementations.


27-27: Same force-unwrap concern as other files.

ScribeLocalizations.of(context)! will throw if the delegate is not registered. This is a recurring pattern across files - ensure ScribeLocalizations.delegate is properly registered in the app.

@zatteo zatteo requested a review from dab246 December 9, 2025 07:36
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: 0

♻️ Duplicate comments (1)
scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart (1)

12-18: Clarify ownership of API key/base URL between AIDataSourceImpl, AIApi, and AIConfig.

You’re wiring AIConfig.aiApiKey / AIConfig.aiApiUrl into AIApi here. If AIApi is always used with those same values, consider either:

  • letting AIApi own reading from AIConfig in its default constructor, or
  • injecting apiKey / baseUrl (or an already‑configured AIApi) from the DI layer,

to reduce coupling to static config and centralize this responsibility. This is essentially the same concern already raised in the previous review about where apiKey / baseUrl should live.

🧹 Nitpick comments (6)
scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart (1)

21-38: Preserve original error details instead of throwing a new generic Exception.

Catching DioError / generic e and throwing a new Exception stringifies the error and drops both the concrete type and original stack trace, which makes debugging and upstream handling harder.

Consider either:

  • rethrowing the original error after any logging, or
  • wrapping it in a domain‑specific failure type while preserving the stack (e.g. via Error.throwWithStackTrace) and, if useful, including status code / response body.

This keeps your AIDataSource contract clean while retaining enough information to diagnose issues.

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

181-187: Consider adding user feedback after copying to clipboard.

The clipboard operation succeeds silently. Users typically expect visual confirmation (e.g., a snackbar or tooltip) when content is copied.

           IconButton(
             icon: const Icon(Icons.content_copy, size: 18),
             color: Colors.grey[600],
             onPressed: () {
               Clipboard.setData(ClipboardData(text: suggestion));
+              ScaffoldMessenger.of(context).showSnackBar(
+                const SnackBar(
+                  content: Text('Copied to clipboard'),
+                  duration: Duration(seconds: 2),
+                ),
+              );
             },
           ),
scribe/lib/scribe/ai/localizations/scribe_localizations.dart (1)

104-112: inputPlaceholder and customPromptAction have identical values.

Both getters return 'Help me write' with different message names. If these are intentionally the same, consider whether they need separate localization keys, or if one can reference the other to reduce duplication in translation files.

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

871-883: HTML entity decoding is incomplete.

The manual replacement handles only 6 common entities. Consider using a library or more comprehensive approach for proper HTML decoding, as content may contain other entities (e.g., &mdash;, &copy;, numeric entities like &#8212;).

+import 'package:html_unescape/html_unescape.dart';
+
+final _htmlUnescape = HtmlUnescape();
+
 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();
+    return _htmlUnescape.convert(textContent).trim();
 }

Alternatively, if adding a dependency is not desired, at minimum consider adding a comment noting the limitation.

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

238-269: Consider extracting the duplicated Stack pattern into a helper method.

The mobile (lines 238-269) and tablet (lines 427-458) views have nearly identical Stack structures wrapping the editor with the AI scribe selection button. This could be refactored to reduce duplication.

Widget _buildEditorWithAIScribeOverlay({
  required BuildContext context,
  required Widget editorView,
  required Widget? additionalContent,
}) {
  return Stack(
    children: [
      Column(
        children: [
          editorView,
          if (additionalContent != null) additionalContent,
          SizedBox(height: MediaQuery.viewInsetsOf(context).bottom + 64),
        ],
      ),
      _buildAIScribeSelectionButton(context),
    ],
  );
}

Also applies to: 427-458

scribe/lib/scribe.dart (1)

1-16: Consider tightening the public API surface of the scribe package

The barrel currently exports domain, presentation, and all data-layer internals (e.g., ai_repository_impl.dart, ai_datasource.dart, ai_config.dart). That’s convenient but makes it harder to refactor internals later without breaking callers, and encourages outside code to couple directly to data implementations instead of the domain abstractions.

If you want more flexibility long term, consider:

  • Keeping only domain contracts, interactor(s), and presentation widgets public.
  • Either not exporting data implementations at the top level, or re‑exporting them from a more “advanced/internal” sub‑barrel (e.g., scribe/data.dart) so typical consumers use only domain + UI.

This is not a blocker, but worth deciding now since it defines the stable API of the module.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26e85 and e763978.

⛔ Files ignored due to path filters (1)
  • scribe/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (44)
  • lib/features/composer/presentation/composer_bindings.dart (2 hunks)
  • lib/features/composer/presentation/composer_controller.dart (6 hunks)
  • lib/features/composer/presentation/composer_view.dart (5 hunks)
  • lib/features/composer/presentation/composer_view_web.dart (10 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/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.dart (2 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/data/config/ai_config.dart (1 hunks)
  • scribe/lib/scribe/ai/data/datasource/ai_datasource.dart (1 hunks)
  • scribe/lib/scribe/ai/data/datasource_impl/ai_datasource_impl.dart (1 hunks)
  • scribe/lib/scribe/ai/data/model/ai_api_request.dart (1 hunks)
  • scribe/lib/scribe/ai/data/model/ai_api_response.dart (1 hunks)
  • scribe/lib/scribe/ai/data/model/ai_message.dart (1 hunks)
  • scribe/lib/scribe/ai/data/network/ai_api.dart (1 hunks)
  • scribe/lib/scribe/ai/data/repository/ai_repository_impl.dart (1 hunks)
  • scribe/lib/scribe/ai/domain/constants/ai_prompts.dart (1 hunks)
  • scribe/lib/scribe/ai/domain/model/ai_response.dart (1 hunks)
  • scribe/lib/scribe/ai/domain/repository/ai_scribe_repository.dart (1 hunks)
  • scribe/lib/scribe/ai/domain/state/generate_ai_text_state.dart (1 hunks)
  • scribe/lib/scribe/ai/domain/usecases/generate_ai_text_interactor.dart (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_en.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_fr.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_messages.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_ru.arb (1 hunks)
  • scribe/lib/scribe/ai/l10n/intl_vi.arb (1 hunks)
  • scribe/lib/scribe/ai/localizations/scribe_localizations.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/model/ai_action.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.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)
  • scripts/prebuild.sh (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (29)
  • scribe/lib/scribe/ai/data/datasource/ai_datasource.dart
  • scripts/prebuild.sh
  • scribe/lib/scribe/ai/data/network/ai_api.dart
  • lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart
  • scribe/lib/scribe/ai/data/config/ai_config.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_button.dart
  • lib/features/composer/presentation/composer_bindings.dart
  • scribe/lib/scribe/ai/l10n/intl_vi.arb
  • scribe/lib/scribe/ai/domain/repository/ai_scribe_repository.dart
  • lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart
  • scribe/lib/scribe/ai/l10n/intl_ru.arb
  • scribe/lib/scribe/ai/data/model/ai_message.dart
  • lib/main/localizations/app_localizations.dart
  • scribe/lib/scribe/ai/domain/model/ai_response.dart
  • lib/l10n/intl_ru.arb
  • scribe/lib/scribe/ai/data/model/ai_api_response.dart
  • scribe/lib/scribe/ai/data/model/ai_api_request.dart
  • scribe/lib/scribe/ai/domain/usecases/generate_ai_text_interactor.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_menu.dart
  • scribe/lib/scribe/ai/data/repository/ai_repository_impl.dart
  • lib/l10n/intl_messages.arb
  • scribe/lib/scribe/ai/l10n/intl_en.arb
  • lib/main.dart
  • scribe/pubspec.yaml
  • scribe/lib/scribe/ai/l10n/intl_fr.arb
  • scribe/lib/scribe/ai/domain/constants/ai_prompts.dart
  • scribe/lib/scribe/ai/presentation/bindings/ai_scribe_bindings.dart
  • scribe/lib/scribe/ai/domain/state/generate_ai_text_state.dart
  • scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart
⏰ 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). (10)
  • GitHub Check: analyze-test (server_settings)
  • GitHub Check: analyze-test (email_recovery)
  • GitHub Check: Build web version and deploy
  • GitHub Check: analyze-test (fcm)
  • GitHub Check: analyze-test (contact)
  • GitHub Check: analyze-test (forward)
  • GitHub Check: analyze-test (core)
  • GitHub Check: analyze-test (model)
  • GitHub Check: analyze-test (rule_filter)
  • GitHub Check: analyze-test (default)
🔇 Additional comments (22)
lib/main/bindings/main_bindings.dart (1)

3-3: AIScribe DI wiring looks correct; just confirm behavior when AI is disabled

Importing AIScribeBindings and invoking AIScribeBindings().dependencies() in MainBindings.dependencies() is consistent with the existing binding pattern and ordering.

Since AI is feature‑toggled via AI_ENABLED, please confirm that AIScribeBindings().dependencies() is safe when AI is disabled or env vars are missing (no crashes, minimal overhead).

Also applies to: 25-25

lib/l10n/intl_vi.arb (1)

5483-5487: New aiAssistant Vietnamese localization is consistent

Key name, value "Trợ lý AI", and @aiAssistant metadata match existing ARB conventions and other locales. No issues.

lib/l10n/intl_fr.arb (1)

5477-5481: New aiAssistant French localization is well‑formed

"Assistant IA" plus the @aiAssistant metadata follow the same pattern as other entries and align with terminology already used in the app.

scribe/lib/scribe/ai/l10n/intl_messages.arb (1)

1-129: Scribe ARB definitions are coherent and ready for use

The new intl_messages.arb file is structurally valid, keys are clearly namespaced (category*, action*, language*, etc.), and all metadata blocks are consistent. This should integrate cleanly with the Scribe localization layer.

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

1-103: LGTM!

The widget implementation is clean and well-structured:

  • Proper lifecycle management with listener removal and controller disposal
  • Input validation with trim before callback invocation
  • Dependency injection pattern followed for imagePaths
  • Localization properly used for placeholder text
scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart (1)

121-153: Error state layout has been corrected.

The error state now correctly uses Flexible(fit: FlexFit.loose, ...) matching the loading and success states, which addresses the previous review concern about layout issues with Expanded.

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

908-927: Potential null context access after async gap.

The context.mounted check at line 917 is good, but showAIScribeDialog is called with context which could theoretically become invalid between the check and usage. This is generally fine in practice but worth noting.

The async flow and null safety handling are appropriate. The context.mounted guard before calling showAIScribeDialog prevents crashes if the widget is disposed during the async operation.


945-963: LGTM!

The handleTextSelection method properly updates all related reactive state variables and handles the null case correctly by resetting all values.

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

557-595: LGTM!

The _buildAIScribeSelectionButton implementation is well-structured:

  • Correctly uses PositionedDirectional for RTL support (addresses past review comment)
  • Properly guards with AIConfig.isAiEnabled check
  • Uses Builder pattern appropriately to get the button's render context for position calculation
  • Handles the case where renderBox might be null

472-473: AI feature gating is correctly applied.

The onOpenAIScribe and aiScribeButtonKey are conditionally passed only when AIConfig.isAiEnabled is true, ensuring the AI features are properly feature-flagged.

scribe/lib/scribe/ai/localizations/scribe_localizations.dart (1)

11-20: Setting Intl.defaultLocale in LocalizationsDelegate.load() follows Flutter best practices.

This is the standard recommended pattern for Flutter localization delegates. The code correctly initializes messages before setting the locale. However, verify that Intl.defaultLocale is set from only one centralized place in the application to avoid conflicts. If the main app also manages locale changes, ensure they coordinate through a single source of truth rather than multiple scattered assignments.

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

32-32: LGTM! Text selection tracking properly integrated.

The scribe package import and onTextSelectionChanged callbacks are correctly wired across all responsive variants (mobile, desktop, tablet), enabling text selection detection for the AI Scribe feature.

Also applies to: 270-270, 514-514, 791-791


336-336: LGTM! AI Scribe selection button consistently positioned.

The AI Scribe selection button is correctly added to the Stack overlay in all three responsive variants, enabling contextual AI actions when text is selected.

Also applies to: 610-610, 885-885


562-563: LGTM! AI features properly gated in bottom bar.

The onOpenAIScribe callback and aiScribeButtonKey are correctly conditionally passed to BottomBarComposerWidget when AI is enabled, maintaining proper feature gating.

Also applies to: 838-839


959-997: LGTM! RTL support correctly implemented.

The method properly addresses the previous review concern by using PositionedDirectional with start instead of Positioned with left, ensuring correct behavior in RTL languages. The implementation includes:

  • Proper feature gating with early return
  • Reactive selection state tracking via Obx
  • Button position calculation with fallback handling
  • PointerInterceptor for web iframe interaction
scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.dart (2)

4-77: LGTM! Past review feedback addressed.

The implementation correctly:

  • Uses ScribeLocalizations (intl-based) for localization, addressing previous comments about intl support
  • Defines methods inside the enum rather than as extensions, as suggested in prior feedback
  • Provides exhaustive switch coverage for all enum values
  • Implements clean category grouping and submenu labeling logic

79-129: LGTM! Clean category implementation.

The AIScribeMenuCategory enum follows the same localization pattern as AIScribeMenuAction, correctly groups actions by category, and provides clear submenu detection logic.

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

15-23: LGTM! Dependency injection properly implemented.

The function correctly receives GenerateAITextInteractor as a parameter rather than instantiating data layer classes directly, addressing the previous review feedback about using dependency injection.


73-98: LGTM! Robust dialog positioning with bounds checking.

The positioning logic correctly:

  • Calculates dialog position relative to button
  • Ensures dialogs stay within screen bounds with appropriate padding
  • Uses responsive sizing (90% width on mobile, max 500px on larger screens)
  • Provides fallback to centered positioning when buttonPosition is null

Also applies to: 130-155


173-196: Verify error handling in AIScribeSuggestion widget.

The _executeAIRequest helper throws exceptions for failures and unexpected states, which will cause the suggestionFuture (line 116) to complete with an error. Ensure that AIScribeSuggestion properly handles Future errors to display appropriate error messages to the user. Check that the widget uses FutureBuilder with an error state handler or implements catchError/onError callbacks.

scribe/lib/scribe/ai/presentation/model/ai_action.dart (2)

5-9: LGTM! Clean sealed class abstraction.

The sealed class pattern enables exhaustive checking of action types and ensures all implementations provide localized labels via the getLabel method.


11-29: LGTM! Multi-language support implemented.

Both PredefinedAction and CustomPromptAction correctly implement localized labels via ScribeLocalizations, addressing the previous feedback about supporting multiple languages. The implementation cleanly separates predefined menu actions from custom prompt actions.

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: 0

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

26-103: Consider extracting magic numbers and positioning logic.

The dialog construction logic is solid, but could benefit from:

  1. Extracting magic numbers (240.0, 12, 8, 16, 10) as named constants for maintainability.
  2. The positioning logic (lines 73-98) is duplicated in the second dialog (lines 130-165) and could be extracted into a helper function.

Example refactor for constants:

// At the top of the file or in a constants file
const double _kMenuWidth = 240.0;
const double _kDialogElevation = 8.0;
const double _kDialogBorderRadius = 12.0;
const double _kDialogSpacing = 8.0;
const double _kDialogMargin = 16.0;
const double _kButtonOffset = 10.0;

Example refactor for positioning:

Widget _positionDialog({
  required Widget child,
  required Offset buttonPosition,
  required BuildContext context,
  required double dialogWidth,
  double? dialogHeight,
}) {
  final screenSize = MediaQuery.of(context).size;
  double left = buttonPosition.dx;
  double bottom = screenSize.height - buttonPosition.dy + _kButtonOffset;
  
  // Horizontal bounds checking
  if (left + dialogWidth > screenSize.width) {
    left = screenSize.width - dialogWidth - _kDialogMargin;
  }
  if (left < _kDialogMargin) {
    left = _kDialogMargin;
  }
  
  // Vertical bounds checking if height provided
  if (dialogHeight != null) {
    if (bottom + dialogHeight > screenSize.height) {
      bottom = screenSize.height - dialogHeight - _kDialogMargin;
    }
    if (bottom < _kDialogMargin) {
      bottom = _kDialogMargin;
    }
  }
  
  return Stack(
    children: [
      Positioned(
        left: left,
        bottom: bottom,
        child: child,
      ),
    ],
  );
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e763978 and 4c03537.

📒 Files selected for processing (4)
  • 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_menu.dart (1 hunks)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_menu.dart
  • scribe/lib/scribe/ai/presentation/widgets/ai_scribe_bar.dart
⏰ 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). (10)
  • GitHub Check: analyze-test (server_settings)
  • GitHub Check: analyze-test (core)
  • GitHub Check: analyze-test (email_recovery)
  • GitHub Check: analyze-test (rule_filter)
  • GitHub Check: analyze-test (fcm)
  • GitHub Check: analyze-test (forward)
  • GitHub Check: analyze-test (model)
  • GitHub Check: analyze-test (contact)
  • GitHub Check: analyze-test (default)
  • GitHub Check: Build web version and deploy
🔇 Additional comments (5)
scribe/lib/scribe/ai/presentation/widgets/ai_scribe.dart (5)

1-14: LGTM!

The imports are appropriate for the AI Scribe dialog functionality, and the typedef provides clear semantics for the callback.


15-24: Good use of dependency injection.

The interactor is now passed as a parameter rather than being instantiated directly, which properly addresses the previous review feedback about presentation/data layer separation.


105-110: LGTM!

Good defensive programming with null checks and context.mounted verification before proceeding to the second dialog.


173-196: Error handling relies on consumer widget.

The function correctly uses the Either pattern with fold and throws descriptive exceptions. However, the error handling responsibility is delegated to the caller/consumer (AIScribeSuggestion widget). This is acceptable if that widget properly handles these exceptions.

The null check on line 187 and the unexpected state check on line 191 are good defensive measures.

Note: This comment is related to the verification requested in the previous comment about AIScribeSuggestion error handling.


112-170: Verify error handling in AIScribeSuggestion widget.

The _executeAIRequest call (line 116) can throw exceptions, but there's no error handling at this level. Ensure that the AIScribeSuggestion widget properly handles future errors (e.g., using FutureBuilder with error state) to prevent the app from crashing when the AI request fails.

@dab246
Copy link
Member

dab246 commented Dec 9, 2025

The current PR is quite large, quite difficult to review after each fixup. IMO, we should split this issue into multiple PRs for easier review (maybe 2). 1. To handle AI Scibe's API logic at the data and domain layers. 2. To handle UI @hoangdat @zatteo

}

static String improveMakeShorter(String text) {
return 'Make the following text shorter and more concise. Output only the result. Preserve input formatting. Text:\n\n$text';
Copy link
Member

Choose a reason for hiding this comment

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

how about to make it as an env variable, IMO, user can easily to custom it then.

// Get screen size to ensure modal stays within bounds
final screenSize = MediaQuery.of(context).size;
// Use responsive width: 90% on mobile, max 500px on larger screens
final modalWidth = screenSize.width < 600 ? screenSize.width * 0.9 : 500.0;
Copy link
Member

Choose a reason for hiding this comment

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

separate it in method

final screenSize = MediaQuery.of(context).size;
// Use responsive width: 90% on mobile, max 500px on larger screens
final modalWidth = screenSize.width < 600 ? screenSize.width * 0.9 : 500.0;
const modalHeight = 400.0;
Copy link
Member

Choose a reason for hiding this comment

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

const it

controller: _controller,
decoration: InputDecoration(
hintText: 'Help me write',
hintStyle: TextStyle(
Copy link
Member

Choose a reason for hiding this comment

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

use system text theme

const SizedBox(width: 8),
TMailButtonWidget.fromIcon(
icon: ImagePaths().icSend,
iconSize: 16,
Copy link
Member

Choose a reason for hiding this comment

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

move to style file all magic number

Copy link
Member Author

Choose a reason for hiding this comment

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

I managed all your last comments in last commits of the PR #4197

@zatteo
Copy link
Member Author

zatteo commented Dec 9, 2025

The current PR is quite large, quite difficult to review after each fixup. IMO, we should split this issue into multiple PRs for easier review (maybe 2). 1. To handle AI Scibe's API logic at the data and domain layers. 2. To handle UI @hoangdat @zatteo

Follow up for the logical part: #4196

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

zatteo commented Dec 10, 2025

Follow up for the UI part: #4197

@zatteo zatteo closed this Dec 10, 2025
@zatteo
Copy link
Member Author

zatteo commented Dec 10, 2025

Closing because split in two PR

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.

4 participants