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 m3Primary95 = Color(0xFFE3F1FF);
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 black4D4D4D = Color(0xFF4D4D4D);
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:flutter/material.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:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selection_mixin.dart';
@@ -19,7 +20,8 @@ extension HandleAiScribeInComposerExtension on ComposerController {
try {
final htmlContent = await getContentInEditor();
String textContent = StringConvert.convertHtmlContentToTextContent(
htmlContent,);
htmlContent,
);
return textContent;
} catch (e) {
logError('$runtimeType::getTextOnlyContentInEditor:Exception = $e');
@@ -40,7 +42,7 @@ extension HandleAiScribeInComposerExtension on ComposerController {
Future<void> openAIAssistantModal(Offset? position, Size? size) async {
final fullText = await _getTextOnlyContentInEditor();
final aiResult = await AiScribeModalManager.showAIScribeMenuModal(
await AiScribeModalManager.showAIScribeMenuModal(
imagePaths: imagePaths,
availableCategories: AIScribeMenuCategory.values,
buttonPosition: position,
@@ -48,10 +50,20 @@ extension HandleAiScribeInComposerExtension on ComposerController {
content: fullText,
preferredPlacement: ModalPlacement.top,
crossAxisAlignment: ModalCrossAxisAlignment.start,
onSelectAiScribeSuggestionAction: handleAiScribeSuggestionAction,
);
}
if (aiResult != null) {
insertTextInEditor(aiResult);
void handleAiScribeSuggestionAction(
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,
coordinates: textSelectionData.coordinates != null
? Offset(
textSelectionData.coordinates!.x,
textSelectionData.coordinates!.y,
)
textSelectionData.coordinates!.x,
textSelectionData.coordinates!.y,
)
: null,
);
} else {
@@ -22,7 +22,8 @@ class ComposerAiScribeSelectionOverlay extends StatelessWidget {
return AiSelectionOverlay(
selection: controller.editorTextSelection.value,
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/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/suggestion/ai_scribe_suggestion_success_list_actions.dart';
class AiScribeModalManager {
AiScribeModalManager._();
static Future<String?> showAIScribeMenuModal({
static Future<void> showAIScribeMenuModal({
required ImagePaths imagePaths,
required List<AIScribeMenuCategory> availableCategories,
required OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction,
String? content,
Offset? buttonPosition,
Size? buttonSize,
@@ -39,32 +41,39 @@ class AiScribeModalManager {
).whenComplete(submenuController.hide);
if (aiAction != null) {
return await showAIScribeSuggestionModal(
await showAIScribeSuggestionModal(
aiAction: aiAction,
imagePaths: imagePaths,
content: content,
buttonPosition: buttonPosition,
buttonSize: buttonSize,
preferredPlacement: preferredPlacement,
crossAxisAlignment: crossAxisAlignment,
onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
);
}
return null;
}
static Future<String?> showAIScribeSuggestionModal({
static Future<void> showAIScribeSuggestionModal({
required AIAction aiAction,
required ImagePaths imagePaths,
required OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction,
String? content,
Offset? buttonPosition,
Size? buttonSize,
}) {
return Get.dialog<String?>(
ModalPlacement? preferredPlacement,
ModalCrossAxisAlignment crossAxisAlignment = ModalCrossAxisAlignment.center,
}) async {
await Get.dialog<String?>(
AiScribeSuggestionWidget(
aiAction: aiAction,
imagePaths: imagePaths,
content: content,
buttonPosition: buttonPosition,
buttonSize: buttonSize,
preferredPlacement: preferredPlacement,
crossAxisAlignment: crossAxisAlignment,
onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
),
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/styles/ai_scribe_styles.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 {
final ImagePaths imagePaths;
final String? selectedText;
final OnAiScribeResultCallback onAiScribeResultCallback;
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
const InlineAiAssistButton({
super.key,
required this.imagePaths,
required this.onAiScribeResultCallback,
required this.onSelectAiScribeSuggestionAction,
this.selectedText,
});
@@ -39,20 +39,17 @@ class InlineAiAssistButton extends StatelessWidget {
Size? size;
if (renderBox != null && renderBox is RenderBox) {
position = renderBox.localToGlobal(Offset.zero);
size = renderBox.size;
position = renderBox.localToGlobal(Offset.zero);
size = renderBox.size;
}
final aiResult = await AiScribeModalManager.showAIScribeMenuModal(
await AiScribeModalManager.showAIScribeMenuModal(
imagePaths: imagePaths,
availableCategories: AIScribeMenuCategory.values,
buttonPosition: position,
content: selectedText,
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_loading.dart';
import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success.dart';
typedef OnAiScribeResultCallback = void Function(String result);
import 'package:scribe/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart';
class AiScribeSuggestionWidget extends StatefulWidget {
final AIAction aiAction;
@@ -29,11 +28,13 @@ class AiScribeSuggestionWidget extends StatefulWidget {
final Size? buttonSize;
final ModalPlacement? preferredPlacement;
final ModalCrossAxisAlignment crossAxisAlignment;
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
const AiScribeSuggestionWidget({
super.key,
required this.aiAction,
required this.imagePaths,
required this.onSelectAiScribeSuggestionAction,
this.content,
this.buttonPosition,
this.buttonSize,
@@ -123,7 +124,7 @@ class _AiScribeSuggestionWidgetModalState
return AiScribeSuggestionSuccess(
imagePaths: widget.imagePaths,
suggestionText: value.response.result,
aiAction: widget.aiAction,
onSelectAction: widget.onSelectAiScribeSuggestionAction,
);
} else if (value is GenerateAITextFailure) {
return AiScribeSuggestionError(imagePaths: widget.imagePaths);
@@ -22,7 +22,7 @@ class AiScribeSuggestionLoading extends StatelessWidget {
children: [
_SparklePulseIcon(
asset: imagePaths.icSparkle,
color: AppColor.blueFF00B7FF,
color: AppColor.blue00B7FF,
),
Expanded(
child: _AnimatedEllipsisText(
@@ -1,19 +1,18 @@
import 'package:core/presentation/resources/image_paths.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/widgets/modal/suggestion/ai_scribe_suggestion_success_list_actions.dart';
class AiScribeSuggestionSuccess extends StatelessWidget {
final ImagePaths imagePaths;
final String suggestionText;
final AIAction aiAction;
final OnSelectAiScribeSuggestionAction onSelectAction;
const AiScribeSuggestionSuccess({
super.key,
required this.imagePaths,
required this.suggestionText,
required this.aiAction,
required this.onSelectAction,
});
@override
@@ -23,6 +22,7 @@ class AiScribeSuggestionSuccess extends StatelessWidget {
child: Column(
spacing: 8,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
@@ -33,7 +33,7 @@ class AiScribeSuggestionSuccess extends StatelessWidget {
AiScribeSuggestionSuccessListActions(
imagePaths: imagePaths,
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:scribe/scribe.dart';
import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart';
import 'package:scribe/scribe/ai/localizations/scribe_localizations.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 {
final ImagePaths imagePaths;
final String suggestionText;
final AIAction aiAction;
final OnSelectAiScribeSuggestionAction onSelectAction;
const AiScribeSuggestionSuccessListActions({
super.key,
required this.imagePaths,
required this.suggestionText,
required this.aiAction,
required this.onSelectAction,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsetsDirectional.only(bottom: 8),
child: Column(
spacing: 8,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
suggestionText,
style: AIScribeTextStyles.suggestionContent,
final localizations = ScribeLocalizations.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
spacing: 8,
children: [
Flexible(
child: Container(
constraints: const BoxConstraints(minWidth: 67),
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: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/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 {
const AiSelectionOverlay({
super.key,
required this.selection,
required this.imagePaths,
required this.onAiScribeResultCallback,
required this.onSelectAiScribeSuggestionAction,
});
final TextSelectionModel? selection;
final ImagePaths imagePaths;
final OnAiScribeResultCallback onAiScribeResultCallback;
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
@override
Widget build(BuildContext context) {
@@ -35,7 +35,7 @@ class AiSelectionOverlay extends StatelessWidget {
child: InlineAiAssistButton(
imagePaths: imagePaths,
selectedText: selectedText,
onAiScribeResultCallback: onAiScribeResultCallback,
onSelectAiScribeSuggestionAction: onSelectAiScribeSuggestionAction,
),
),
);