feat(ai-scribe): Improve UI for AI Scribe suggestion modal

This commit is contained in:
dab246
2025-12-18 03:40:53 +07:00
committed by Dat H. Pham
parent 582b1ebe97
commit 1a1b8f489f
11 changed files with 125 additions and 55 deletions
@@ -278,7 +278,8 @@ extension AppColor on Color {
static const m3Primary = Color(0xFF0A84FF); static const m3Primary = Color(0xFF0A84FF);
static const m3Primary95 = Color(0xFFE3F1FF); static const m3Primary95 = Color(0xFFE3F1FF);
static const gray49454F = Color(0xFF49454F); static const gray49454F = Color(0xFF49454F);
static const blueFF00B7FF = Color(0xFF00B7FF); static const blue00B7FF = Color(0xFF00B7FF);
static const blueD2E9FF = Color(0xFFD2E9FF);
static const lightGrayF9FAFB = Color(0xFFF9FAFB); static const lightGrayF9FAFB = Color(0xFFF9FAFB);
static const black4D4D4D = Color(0xFF4D4D4D); static const black4D4D4D = Color(0xFF4D4D4D);
static const black1A1A1A = Color(0xFF1A1A1A); static const black1A1A1A = Color(0xFF1A1A1A);
@@ -3,6 +3,7 @@ import 'package:core/utils/platform_info.dart';
import 'package:core/utils/string_convert.dart'; import 'package:core/utils/string_convert.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:scribe/scribe.dart'; import 'package:scribe/scribe.dart';
import 'package:scribe/scribe/ai/presentation/model/context_menu/ai_scribe_suggestion_actions.dart';
import 'package:scribe/scribe/ai/presentation/model/modal/modal_cross_axis_alignment.dart'; import 'package:scribe/scribe/ai/presentation/model/modal/modal_cross_axis_alignment.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selection_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selection_mixin.dart';
@@ -19,7 +20,8 @@ extension HandleAiScribeInComposerExtension on ComposerController {
try { try {
final htmlContent = await getContentInEditor(); final htmlContent = await getContentInEditor();
String textContent = StringConvert.convertHtmlContentToTextContent( String textContent = StringConvert.convertHtmlContentToTextContent(
htmlContent,); htmlContent,
);
return textContent; return textContent;
} catch (e) { } catch (e) {
logError('$runtimeType::getTextOnlyContentInEditor:Exception = $e'); logError('$runtimeType::getTextOnlyContentInEditor:Exception = $e');
@@ -40,7 +42,7 @@ extension HandleAiScribeInComposerExtension on ComposerController {
Future<void> openAIAssistantModal(Offset? position, Size? size) async { Future<void> openAIAssistantModal(Offset? position, Size? size) async {
final fullText = await _getTextOnlyContentInEditor(); final fullText = await _getTextOnlyContentInEditor();
final aiResult = await AiScribeModalManager.showAIScribeMenuModal( await AiScribeModalManager.showAIScribeMenuModal(
imagePaths: imagePaths, imagePaths: imagePaths,
availableCategories: AIScribeMenuCategory.values, availableCategories: AIScribeMenuCategory.values,
buttonPosition: position, buttonPosition: position,
@@ -48,10 +50,20 @@ extension HandleAiScribeInComposerExtension on ComposerController {
content: fullText, content: fullText,
preferredPlacement: ModalPlacement.top, preferredPlacement: ModalPlacement.top,
crossAxisAlignment: ModalCrossAxisAlignment.start, crossAxisAlignment: ModalCrossAxisAlignment.start,
onSelectAiScribeSuggestionAction: handleAiScribeSuggestionAction,
); );
}
if (aiResult != null) { void handleAiScribeSuggestionAction(
insertTextInEditor(aiResult); AiScribeSuggestionActions action,
String suggestionText,
) {
switch(action) {
case AiScribeSuggestionActions.replace:
break;
case AiScribeSuggestionActions.insert:
insertTextInEditor(suggestionText);
break;
} }
} }
@@ -61,9 +73,9 @@ extension HandleAiScribeInComposerExtension on ComposerController {
selectedText: textSelectionData.selectedText, selectedText: textSelectionData.selectedText,
coordinates: textSelectionData.coordinates != null coordinates: textSelectionData.coordinates != null
? Offset( ? Offset(
textSelectionData.coordinates!.x, textSelectionData.coordinates!.x,
textSelectionData.coordinates!.y, textSelectionData.coordinates!.y,
) )
: null, : null,
); );
} else { } else {
@@ -22,7 +22,8 @@ class ComposerAiScribeSelectionOverlay extends StatelessWidget {
return AiSelectionOverlay( return AiSelectionOverlay(
selection: controller.editorTextSelection.value, selection: controller.editorTextSelection.value,
imagePaths: controller.imagePaths, imagePaths: controller.imagePaths,
onAiScribeResultCallback: controller.insertTextInEditor, onSelectAiScribeSuggestionAction:
controller.handleAiScribeSuggestionAction,
); );
}); });
} }
@@ -0,0 +1,15 @@
import 'package:scribe/scribe/ai/localizations/scribe_localizations.dart';
enum AiScribeSuggestionActions {
replace,
insert;
String getLabel(ScribeLocalizations localizations) {
switch (this) {
case AiScribeSuggestionActions.replace:
return localizations.replace;
case AiScribeSuggestionActions.insert:
return localizations.insert;
}
}
}
@@ -9,13 +9,15 @@ import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart';
import 'package:scribe/scribe/ai/presentation/utils/context_menu/popup_submenu_controller.dart'; import 'package:scribe/scribe/ai/presentation/utils/context_menu/popup_submenu_controller.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/ai_scribe_modal_widget.dart'; import 'package:scribe/scribe/ai/presentation/widgets/modal/ai_scribe_modal_widget.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart'; import 'package:scribe/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart';
class AiScribeModalManager { class AiScribeModalManager {
AiScribeModalManager._(); AiScribeModalManager._();
static Future<String?> showAIScribeMenuModal({ static Future<void> showAIScribeMenuModal({
required ImagePaths imagePaths, required ImagePaths imagePaths,
required List<AIScribeMenuCategory> availableCategories, required List<AIScribeMenuCategory> availableCategories,
required OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction,
String? content, String? content,
Offset? buttonPosition, Offset? buttonPosition,
Size? buttonSize, Size? buttonSize,
@@ -39,32 +41,39 @@ class AiScribeModalManager {
).whenComplete(submenuController.hide); ).whenComplete(submenuController.hide);
if (aiAction != null) { if (aiAction != null) {
return await showAIScribeSuggestionModal( await showAIScribeSuggestionModal(
aiAction: aiAction, aiAction: aiAction,
imagePaths: imagePaths, imagePaths: imagePaths,
content: content, content: content,
buttonPosition: buttonPosition, buttonPosition: buttonPosition,
buttonSize: buttonSize, buttonSize: buttonSize,
preferredPlacement: preferredPlacement,
crossAxisAlignment: crossAxisAlignment,
onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
); );
} }
return null;
} }
static Future<String?> showAIScribeSuggestionModal({ static Future<void> showAIScribeSuggestionModal({
required AIAction aiAction, required AIAction aiAction,
required ImagePaths imagePaths, required ImagePaths imagePaths,
required OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction,
String? content, String? content,
Offset? buttonPosition, Offset? buttonPosition,
Size? buttonSize, Size? buttonSize,
}) { ModalPlacement? preferredPlacement,
return Get.dialog<String?>( ModalCrossAxisAlignment crossAxisAlignment = ModalCrossAxisAlignment.center,
}) async {
await Get.dialog<String?>(
AiScribeSuggestionWidget( AiScribeSuggestionWidget(
aiAction: aiAction, aiAction: aiAction,
imagePaths: imagePaths, imagePaths: imagePaths,
content: content, content: content,
buttonPosition: buttonPosition, buttonPosition: buttonPosition,
buttonSize: buttonSize, buttonSize: buttonSize,
preferredPlacement: preferredPlacement,
crossAxisAlignment: crossAxisAlignment,
onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
), ),
barrierColor: AIScribeColors.dialogBarrier, barrierColor: AIScribeColors.dialogBarrier,
); );
@@ -4,17 +4,17 @@ import 'package:flutter/material.dart';
import 'package:scribe/scribe/ai/presentation/model/ai_scribe_menu_action.dart'; import 'package:scribe/scribe/ai/presentation/model/ai_scribe_menu_action.dart';
import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart'; import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart';
import 'package:scribe/scribe/ai/presentation/utils/modal/ai_scribe_modal_manager.dart'; import 'package:scribe/scribe/ai/presentation/utils/modal/ai_scribe_modal_manager.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart'; import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart';
class InlineAiAssistButton extends StatelessWidget { class InlineAiAssistButton extends StatelessWidget {
final ImagePaths imagePaths; final ImagePaths imagePaths;
final String? selectedText; final String? selectedText;
final OnAiScribeResultCallback onAiScribeResultCallback; final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
const InlineAiAssistButton({ const InlineAiAssistButton({
super.key, super.key,
required this.imagePaths, required this.imagePaths,
required this.onAiScribeResultCallback, required this.onSelectAiScribeSuggestionAction,
this.selectedText, this.selectedText,
}); });
@@ -39,20 +39,17 @@ class InlineAiAssistButton extends StatelessWidget {
Size? size; Size? size;
if (renderBox != null && renderBox is RenderBox) { if (renderBox != null && renderBox is RenderBox) {
position = renderBox.localToGlobal(Offset.zero); position = renderBox.localToGlobal(Offset.zero);
size = renderBox.size; size = renderBox.size;
} }
final aiResult = await AiScribeModalManager.showAIScribeMenuModal( await AiScribeModalManager.showAIScribeMenuModal(
imagePaths: imagePaths, imagePaths: imagePaths,
availableCategories: AIScribeMenuCategory.values, availableCategories: AIScribeMenuCategory.values,
buttonPosition: position, buttonPosition: position,
content: selectedText, content: selectedText,
buttonSize: size, buttonSize: size,
onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
); );
if (aiResult != null) {
onAiScribeResultCallback(aiResult);
}
} }
} }
@@ -18,8 +18,7 @@ import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe
import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_header.dart'; import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_header.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_loading.dart'; import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_loading.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success.dart'; import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart';
typedef OnAiScribeResultCallback = void Function(String result);
class AiScribeSuggestionWidget extends StatefulWidget { class AiScribeSuggestionWidget extends StatefulWidget {
final AIAction aiAction; final AIAction aiAction;
@@ -29,11 +28,13 @@ class AiScribeSuggestionWidget extends StatefulWidget {
final Size? buttonSize; final Size? buttonSize;
final ModalPlacement? preferredPlacement; final ModalPlacement? preferredPlacement;
final ModalCrossAxisAlignment crossAxisAlignment; final ModalCrossAxisAlignment crossAxisAlignment;
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
const AiScribeSuggestionWidget({ const AiScribeSuggestionWidget({
super.key, super.key,
required this.aiAction, required this.aiAction,
required this.imagePaths, required this.imagePaths,
required this.onSelectAiScribeSuggestionAction,
this.content, this.content,
this.buttonPosition, this.buttonPosition,
this.buttonSize, this.buttonSize,
@@ -123,7 +124,7 @@ class _AiScribeSuggestionWidgetModalState
return AiScribeSuggestionSuccess( return AiScribeSuggestionSuccess(
imagePaths: widget.imagePaths, imagePaths: widget.imagePaths,
suggestionText: value.response.result, suggestionText: value.response.result,
aiAction: widget.aiAction, onSelectAction: widget.onSelectAiScribeSuggestionAction,
); );
} else if (value is GenerateAITextFailure) { } else if (value is GenerateAITextFailure) {
return AiScribeSuggestionError(imagePaths: widget.imagePaths); return AiScribeSuggestionError(imagePaths: widget.imagePaths);
@@ -22,7 +22,7 @@ class AiScribeSuggestionLoading extends StatelessWidget {
children: [ children: [
_SparklePulseIcon( _SparklePulseIcon(
asset: imagePaths.icSparkle, asset: imagePaths.icSparkle,
color: AppColor.blueFF00B7FF, color: AppColor.blue00B7FF,
), ),
Expanded( Expanded(
child: _AnimatedEllipsisText( child: _AnimatedEllipsisText(
@@ -1,19 +1,18 @@
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:scribe/scribe.dart';
import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart'; import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart'; import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart';
class AiScribeSuggestionSuccess extends StatelessWidget { class AiScribeSuggestionSuccess extends StatelessWidget {
final ImagePaths imagePaths; final ImagePaths imagePaths;
final String suggestionText; final String suggestionText;
final AIAction aiAction; final OnSelectAiScribeSuggestionAction onSelectAction;
const AiScribeSuggestionSuccess({ const AiScribeSuggestionSuccess({
super.key, super.key,
required this.imagePaths, required this.imagePaths,
required this.suggestionText, required this.suggestionText,
required this.aiAction, required this.onSelectAction,
}); });
@override @override
@@ -23,6 +22,7 @@ class AiScribeSuggestionSuccess extends StatelessWidget {
child: Column( child: Column(
spacing: 8, spacing: 8,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
@@ -33,7 +33,7 @@ class AiScribeSuggestionSuccess extends StatelessWidget {
AiScribeSuggestionSuccessListActions( AiScribeSuggestionSuccessListActions(
imagePaths: imagePaths, imagePaths: imagePaths,
suggestionText: suggestionText, suggestionText: suggestionText,
aiAction: aiAction, onSelectAction: onSelectAction,
), ),
], ],
), ),
@@ -1,36 +1,70 @@
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/core.dart';
import 'package:core/presentation/views/dialog/confirm_dialog_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:scribe/scribe.dart'; import 'package:scribe/scribe/ai/localizations/scribe_localizations.dart';
import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart'; import 'package:scribe/scribe/ai/presentation/model/context_menu/ai_scribe_suggestion_actions.dart';
typedef OnSelectAiScribeSuggestionAction = void Function(
AiScribeSuggestionActions action,
String suggestionText,
);
class AiScribeSuggestionSuccessListActions extends StatelessWidget { class AiScribeSuggestionSuccessListActions extends StatelessWidget {
final ImagePaths imagePaths; final ImagePaths imagePaths;
final String suggestionText; final String suggestionText;
final AIAction aiAction; final OnSelectAiScribeSuggestionAction onSelectAction;
const AiScribeSuggestionSuccessListActions({ const AiScribeSuggestionSuccessListActions({
super.key, super.key,
required this.imagePaths, required this.imagePaths,
required this.suggestionText, required this.suggestionText,
required this.aiAction, required this.onSelectAction,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( final localizations = ScribeLocalizations.of(context);
padding: const EdgeInsetsDirectional.only(bottom: 8),
child: Column( return Row(
spacing: 8, mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min, spacing: 8,
children: [ children: [
Flexible( Flexible(
child: Text( child: Container(
suggestionText, constraints: const BoxConstraints(minWidth: 67),
style: AIScribeTextStyles.suggestionContent, height: 36,
child: ConfirmDialogButton(
label: AiScribeSuggestionActions.replace.getLabel(localizations),
textColor: AppColor.primaryMain,
onTapAction: () {
Navigator.of(context).pop();
onSelectAction(
AiScribeSuggestionActions.replace,
suggestionText,
);
},
), ),
), ),
], ),
), Flexible(
child: Container(
constraints: const BoxConstraints(minWidth: 72),
height: 36,
child: ConfirmDialogButton(
label: AiScribeSuggestionActions.insert.getLabel(localizations),
backgroundColor: AppColor.blueD2E9FF,
textColor: AppColor.primaryMain,
onTapAction: () {
Navigator.of(context).pop();
onSelectAction(
AiScribeSuggestionActions.insert,
suggestionText,
);
},
),
),
),
],
); );
} }
} }
@@ -3,19 +3,19 @@ import 'package:flutter/material.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:scribe/scribe/ai/presentation/model/text_selection_model.dart'; import 'package:scribe/scribe/ai/presentation/model/text_selection_model.dart';
import 'package:scribe/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart'; import 'package:scribe/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart'; import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart';
class AiSelectionOverlay extends StatelessWidget { class AiSelectionOverlay extends StatelessWidget {
const AiSelectionOverlay({ const AiSelectionOverlay({
super.key, super.key,
required this.selection, required this.selection,
required this.imagePaths, required this.imagePaths,
required this.onAiScribeResultCallback, required this.onSelectAiScribeSuggestionAction,
}); });
final TextSelectionModel? selection; final TextSelectionModel? selection;
final ImagePaths imagePaths; final ImagePaths imagePaths;
final OnAiScribeResultCallback onAiScribeResultCallback; final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -35,7 +35,7 @@ class AiSelectionOverlay extends StatelessWidget {
child: InlineAiAssistButton( child: InlineAiAssistButton(
imagePaths: imagePaths, imagePaths: imagePaths,
selectedText: selectedText, selectedText: selectedText,
onAiScribeResultCallback: onAiScribeResultCallback, onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
), ),
), ),
); );