feat(ai-scribe): Fix the English locale file `(intl_en.arb)` is missing all three AI Scribe keys
feat(ai-scribe): Fix async function declared without proper return type annotation. feat(ai-scribe): Fix Async operations not properly awaited. feat(ai-scribe): Fix inconsistent capitalization between button labels. feat(ai-scribe): Fix GenerateAITextFailure check will never match. feat(ai-scribe): Fix consider validating text input for predefined actions. feat(ai-scribe): Using a subshell for directory isolation. feat(ai-scribe): Fix verify the mock usage or document intent. feat(ai-scribe): Using a more conventional import for Offset. feat(ai-scribe): Adding error handling for consistency feat(ai-scribe): Remove `fromJson` and `toJson` of AIResponse feat(ai-scribe): Using constants for role values. feat(ai-scribe): Using a more specific import. feat(ai-scribe): Using Timer for cleaner lifecycle management feat(ai-scribe): Adding error handling to fromMap() for consistency. feat(ai-scribe): Disposing the ValueNotifier feat(ai-scribe): Redundant null check after assignment feat(ai-scribe): Fix calling `registerSelectionChangeListener` multiple times may be inefficient. feat(ai-scribe): Safer type handling for JavaScript callback args feat(ai-scribe): Fix async callbacks not awaited in switch statement. feat(ai-scribe): Fix binding lifecycle mismatch for GetAIScribeConfigInteractor feat(ai-scribe): Fix ai prompts feat(ai-scribe): Fix unawaited async call to `_setupSelectionListener` feat(ai-scribe): Fix switch cases use top-origin coordinates while `PositionedDirectional(bottom:)`` expects bottom-origin coordinates. feat(ai-scribe): Adding error handling for selection listener setup.
This commit is contained in:
@@ -48,7 +48,7 @@ class HtmlUtils {
|
|||||||
editor.parentNode.replaceChild(newEditor, editor);''',
|
editor.parentNode.replaceChild(newEditor, editor);''',
|
||||||
name: 'unregisterDropListener');
|
name: 'unregisterDropListener');
|
||||||
|
|
||||||
static ({String name, String script})registerSelectionChangeListener(String viewId) => (
|
static registerSelectionChangeListener(String viewId) => (
|
||||||
script: '''
|
script: '''
|
||||||
let lastSelectedText = '';
|
let lastSelectedText = '';
|
||||||
|
|
||||||
|
|||||||
@@ -199,20 +199,25 @@ class StringConvert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String convertHtmlContentToTextContent(String htmlContent) {
|
static String convertHtmlContentToTextContent(String htmlContent) {
|
||||||
final document = parse(htmlContent);
|
try {
|
||||||
|
final document = parse(htmlContent);
|
||||||
|
|
||||||
// Each paragraph is surrounded by block tags so we add a /n for each block tag
|
// Each paragraph is surrounded by block tags so we add a /n for each block tag
|
||||||
// Even <br> are surrounded by block tags so we can ignore <br> and treat them
|
// Even <br> are surrounded by block tags so we can ignore <br> and treat them
|
||||||
// as paragraph
|
// as paragraph
|
||||||
const blockTags = 'p, div, li, section, blockquote, article, header, footer, h1, h2, h3, h4, h5, h6';
|
const blockTags = 'p, div, li, section, blockquote, article, header, footer, h1, h2, h3, h4, h5, h6';
|
||||||
|
|
||||||
document.querySelectorAll(blockTags).forEach((element) {
|
document.querySelectorAll(blockTags).forEach((element) {
|
||||||
element.append(Text('\n'));
|
element.append(Text('\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
final String textContent = document.body?.text ?? '';
|
final String textContent = document.body?.text ?? '';
|
||||||
|
|
||||||
return textContent.trim();
|
return textContent.trim();
|
||||||
|
} catch (e) {
|
||||||
|
logError('StringConvert::convertHtmlContentToTextContent:Exception = $e');
|
||||||
|
return htmlContent.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String convertTextContentToHtmlContent(String textContent) {
|
static String convertTextContentToHtmlContent(String textContent) {
|
||||||
|
|||||||
+8
-9
@@ -35,7 +35,7 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void insertTextInEditor(String text) async {
|
Future<void> insertTextInEditor(String text) async {
|
||||||
try {
|
try {
|
||||||
final htmlContent = StringConvert.convertTextContentToHtmlContent(text);
|
final htmlContent = StringConvert.convertTextContentToHtmlContent(text);
|
||||||
|
|
||||||
@@ -87,18 +87,17 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
|||||||
// Ensure we only insert at cursor position by collapsing selection before inserting
|
// Ensure we only insert at cursor position by collapsing selection before inserting
|
||||||
Future<void> onInsertTextCallback(String text) async {
|
Future<void> onInsertTextCallback(String text) async {
|
||||||
await collapseSelection();
|
await collapseSelection();
|
||||||
|
await insertTextInEditor(text);
|
||||||
insertTextInEditor(text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a selection, it will replace the selection, else it will replace everything
|
// If there is a selection, it will replace the selection, else it will replace everything
|
||||||
void onReplaceTextCallback(String text) {
|
Future<void> onReplaceTextCallback(String text) async {
|
||||||
final selection = editorTextSelection.value?.selectedText;
|
final selection = editorTextSelection.value?.selectedText;
|
||||||
if (selection == null || selection.isEmpty) {
|
if (selection == null || selection.isEmpty) {
|
||||||
clearTextInEditor();
|
clearTextInEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
insertTextInEditor(text);
|
await insertTextInEditor(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openAIAssistantModal(Offset? position, Size? size) async {
|
Future<void> openAIAssistantModal(Offset? position, Size? size) async {
|
||||||
@@ -116,16 +115,16 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleAiScribeSuggestionAction(
|
Future<void> handleAiScribeSuggestionAction(
|
||||||
AiScribeSuggestionActions action,
|
AiScribeSuggestionActions action,
|
||||||
String suggestionText,
|
String suggestionText,
|
||||||
) {
|
) async {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case AiScribeSuggestionActions.replace:
|
case AiScribeSuggestionActions.replace:
|
||||||
onReplaceTextCallback(suggestionText);
|
await onReplaceTextCallback(suggestionText);
|
||||||
break;
|
break;
|
||||||
case AiScribeSuggestionActions.insert:
|
case AiScribeSuggestionActions.insert:
|
||||||
onInsertTextCallback(suggestionText);
|
await onInsertTextCallback(suggestionText);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,12 +58,21 @@ class TextSelectionCoordinates {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory TextSelectionCoordinates.fromMap(Map<dynamic, dynamic> data) {
|
factory TextSelectionCoordinates.fromMap(Map<dynamic, dynamic> data) {
|
||||||
return TextSelectionCoordinates(
|
try {
|
||||||
x: (data['x'] as num?)?.toDouble() ?? 0.0,
|
return TextSelectionCoordinates(
|
||||||
y: (data['y'] as num?)?.toDouble() ?? 0.0,
|
x: (data['x'] as num?)?.toDouble() ?? 0.0,
|
||||||
width: (data['width'] as num?)?.toDouble() ?? 0.0,
|
y: (data['y'] as num?)?.toDouble() ?? 0.0,
|
||||||
height: (data['height'] as num?)?.toDouble() ?? 0.0,
|
width: (data['width'] as num?)?.toDouble() ?? 0.0,
|
||||||
);
|
height: (data['height'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return const TextSelectionCoordinates(
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: 0.0,
|
||||||
|
height: 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Offset get position => Offset(x, y);
|
Offset get position => Offset(x, y);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import 'package:core/presentation/constants/constants_ui.dart';
|
import 'package:core/presentation/constants/constants_ui.dart';
|
||||||
|
import 'package:core/utils/app_logger.dart';
|
||||||
import 'package:core/utils/html/html_template.dart';
|
import 'package:core/utils/html/html_template.dart';
|
||||||
import 'package:core/utils/platform_info.dart';
|
import 'package:core/utils/platform_info.dart';
|
||||||
import 'package:core/utils/html/html_utils.dart';
|
import 'package:core/utils/html/html_utils.dart';
|
||||||
@@ -36,7 +37,6 @@ class MobileEditorWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _MobileEditorState extends State<MobileEditorWidget> with TextSelectionMixin {
|
class _MobileEditorState extends State<MobileEditorWidget> with TextSelectionMixin {
|
||||||
|
|
||||||
HtmlEditorApi? _editorApi;
|
|
||||||
late String _createdViewId;
|
late String _createdViewId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -48,9 +48,8 @@ class _MobileEditorState extends State<MobileEditorWidget> with TextSelectionMix
|
|||||||
@override
|
@override
|
||||||
void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged;
|
void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged;
|
||||||
|
|
||||||
void _setupSelectionListener() async {
|
Future<void> _setupSelectionListener(HtmlEditorApi editorApi) async {
|
||||||
final webViewController = _editorApi?.webViewController;
|
final webViewController = editorApi.webViewController;
|
||||||
if (webViewController == null) return;
|
|
||||||
|
|
||||||
final registerSelectionChange =
|
final registerSelectionChange =
|
||||||
HtmlUtils.registerSelectionChangeListener(_createdViewId);
|
HtmlUtils.registerSelectionChangeListener(_createdViewId);
|
||||||
@@ -61,8 +60,10 @@ class _MobileEditorState extends State<MobileEditorWidget> with TextSelectionMix
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (args.isNotEmpty) {
|
if (args.isNotEmpty) {
|
||||||
final data = args[0] as Map<dynamic, dynamic>;
|
final rawData = args[0];
|
||||||
handleSelectionChange(data);
|
if (rawData is Map<dynamic, dynamic>) {
|
||||||
|
handleSelectionChange(rawData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -72,10 +73,13 @@ class _MobileEditorState extends State<MobileEditorWidget> with TextSelectionMix
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _onWebViewCreated(HtmlEditorApi editorApi) async {
|
||||||
void dispose() {
|
widget.onCreatedEditorAction.call(context, editorApi, widget.content);
|
||||||
_editorApi = null;
|
try {
|
||||||
super.dispose();
|
await _setupSelectionListener(editorApi);
|
||||||
|
} catch (e) {
|
||||||
|
logError('Error onWebViewCreated: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -90,11 +94,7 @@ class _MobileEditorState extends State<MobileEditorWidget> with TextSelectionMix
|
|||||||
direction: widget.direction,
|
direction: widget.direction,
|
||||||
useDefaultFontStyle: true,
|
useDefaultFontStyle: true,
|
||||||
),
|
),
|
||||||
onCreated: (editorApi) {
|
onCreated: _onWebViewCreated,
|
||||||
_editorApi = editorApi;
|
|
||||||
widget.onCreatedEditorAction.call(context, editorApi, widget.content);
|
|
||||||
_setupSelectionListener();
|
|
||||||
},
|
|
||||||
onCompleted: widget.onLoadCompletedEditorAction,
|
onCompleted: widget.onLoadCompletedEditorAction,
|
||||||
onContentHeightChanged: PlatformInfo.isIOS ? widget.onEditorContentHeightChanged : null,
|
onContentHeightChanged: PlatformInfo.isIOS ? widget.onEditorContentHeightChanged : null,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class _WebEditorState extends State<WebEditorWidget> with TextSelectionMixin {
|
|||||||
bool _signatureTooltipReady = false;
|
bool _signatureTooltipReady = false;
|
||||||
|
|
||||||
late String _createdViewId;
|
late String _createdViewId;
|
||||||
|
late WebScript _selectionChangeScript;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged;
|
void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged;
|
||||||
@@ -103,7 +104,11 @@ class _WebEditorState extends State<WebEditorWidget> with TextSelectionMixin {
|
|||||||
_editorController = widget.editorController;
|
_editorController = widget.editorController;
|
||||||
|
|
||||||
final registerSelectionChange =
|
final registerSelectionChange =
|
||||||
HtmlUtils.registerSelectionChangeListener(_createdViewId);
|
HtmlUtils.registerSelectionChangeListener(_createdViewId);
|
||||||
|
_selectionChangeScript = WebScript(
|
||||||
|
name: registerSelectionChange.name,
|
||||||
|
script: registerSelectionChange.script,
|
||||||
|
);
|
||||||
|
|
||||||
_editorListener = (event) {
|
_editorListener = (event) {
|
||||||
try {
|
try {
|
||||||
@@ -112,7 +117,7 @@ class _WebEditorState extends State<WebEditorWidget> with TextSelectionMixin {
|
|||||||
|
|
||||||
if (data['name'] == HtmlUtils.registerDropListener.name) {
|
if (data['name'] == HtmlUtils.registerDropListener.name) {
|
||||||
_editorController.evaluateJavascriptWeb(HtmlUtils.removeLineHeight1px.name);
|
_editorController.evaluateJavascriptWeb(HtmlUtils.removeLineHeight1px.name);
|
||||||
} else if (data['name'] == registerSelectionChange.name
|
} else if (data['name'] == _selectionChangeScript.name
|
||||||
&& data['viewId'] == _createdViewId) {
|
&& data['viewId'] == _createdViewId) {
|
||||||
handleSelectionChange(data);
|
handleSelectionChange(data);
|
||||||
}
|
}
|
||||||
@@ -123,9 +128,8 @@ class _WebEditorState extends State<WebEditorWidget> with TextSelectionMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (_editorListener != null) {
|
|
||||||
window.addEventListener("message", _editorListener!);
|
window.addEventListener("message", _editorListener!);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -189,8 +193,8 @@ class _WebEditorState extends State<WebEditorWidget> with TextSelectionMixin {
|
|||||||
script: HtmlUtils.unregisterDropListener.script,
|
script: HtmlUtils.unregisterDropListener.script,
|
||||||
),
|
),
|
||||||
WebScript(
|
WebScript(
|
||||||
name: HtmlUtils.registerSelectionChangeListener(_createdViewId).name,
|
name: _selectionChangeScript.name,
|
||||||
script: HtmlUtils.registerSelectionChangeListener(_createdViewId).script,
|
script: _selectionChangeScript.script,
|
||||||
),
|
),
|
||||||
WebScript(
|
WebScript(
|
||||||
name: HtmlUtils.collapseSelectionToEnd.name,
|
name: HtmlUtils.collapseSelectionToEnd.name,
|
||||||
|
|||||||
+1
-1
@@ -309,7 +309,7 @@ class MailboxDashBoardController extends ReloadableController
|
|||||||
final isPopupMenuOpened = RxBool(false);
|
final isPopupMenuOpened = RxBool(false);
|
||||||
final octetsQuota = Rxn<Quota>();
|
final octetsQuota = Rxn<Quota>();
|
||||||
final isTextFormattingMenuOpened = RxBool(false);
|
final isTextFormattingMenuOpened = RxBool(false);
|
||||||
final cachedAIScribeConfig = Rx<AIScribeConfig>(AIScribeConfig.initial());
|
final cachedAIScribeConfig = AIScribeConfig.initial().obs;
|
||||||
|
|
||||||
Map<Role, MailboxId> mapDefaultMailboxIdByRole = {};
|
Map<Role, MailboxId> mapDefaultMailboxIdByRole = {};
|
||||||
Map<MailboxId, PresentationMailbox> mapMailboxById = {};
|
Map<MailboxId, PresentationMailbox> mapMailboxById = {};
|
||||||
|
|||||||
-2
@@ -6,7 +6,6 @@ import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_
|
|||||||
import 'package:tmail_ui_user/features/manage_account/data/local/preferences_setting_manager.dart';
|
import 'package:tmail_ui_user/features/manage_account/data/local/preferences_setting_manager.dart';
|
||||||
import 'package:tmail_ui_user/features/manage_account/data/repository/manage_account_repository_impl.dart';
|
import 'package:tmail_ui_user/features/manage_account/data/repository/manage_account_repository_impl.dart';
|
||||||
import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart';
|
import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart';
|
||||||
import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart';
|
|
||||||
import 'package:tmail_ui_user/features/manage_account/domain/usecases/save_language_interactor.dart';
|
import 'package:tmail_ui_user/features/manage_account/domain/usecases/save_language_interactor.dart';
|
||||||
import 'package:tmail_ui_user/features/server_settings/data/datasource/server_settings_data_source.dart';
|
import 'package:tmail_ui_user/features/server_settings/data/datasource/server_settings_data_source.dart';
|
||||||
import 'package:tmail_ui_user/features/server_settings/data/datasource_impl/remote_server_settings_data_source_impl.dart';
|
import 'package:tmail_ui_user/features/server_settings/data/datasource_impl/remote_server_settings_data_source_impl.dart';
|
||||||
@@ -102,6 +101,5 @@ class PreferencesInteractorsBindings extends InteractorsBindings {
|
|||||||
Get.delete<ManageAccountRepositoryImpl>(tag: composerId);
|
Get.delete<ManageAccountRepositoryImpl>(tag: composerId);
|
||||||
Get.delete<ManageAccountRepository>(tag: composerId);
|
Get.delete<ManageAccountRepository>(tag: composerId);
|
||||||
Get.delete<SaveLanguageInteractor>(tag: composerId);
|
Get.delete<SaveLanguageInteractor>(tag: composerId);
|
||||||
Get.delete<GetAIScribeConfigInteractor>(tag: composerId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3274,5 +3274,23 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"aiScribe": "AI Scribe",
|
||||||
|
"@aiScribe": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"aiScribeSettingExplanation": "Use AI to help write and improve your emails",
|
||||||
|
"@aiScribeSettingExplanation": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"aiScribeToggleDescription": "Enable AI Scribe",
|
||||||
|
"@aiScribeToggleDescription": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ part 'ai_message.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class AIMessage {
|
class AIMessage {
|
||||||
|
static const String aiUserRole = 'user';
|
||||||
|
|
||||||
final String role;
|
final String role;
|
||||||
final String content;
|
final String content;
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ class AIMessage {
|
|||||||
Map<String, dynamic> toJson() => _$AIMessageToJson(this);
|
Map<String, dynamic> toJson() => _$AIMessageToJson(this);
|
||||||
|
|
||||||
factory AIMessage.ofUser(String content) => AIMessage(
|
factory AIMessage.ofUser(String content) => AIMessage(
|
||||||
role: 'user',
|
role: aiUserRole,
|
||||||
content: content,
|
content: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import 'package:scribe/scribe/ai/presentation/model/ai_action.dart';
|
import 'package:scribe/scribe/ai/presentation/model/ai_action.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';
|
||||||
|
|
||||||
const PERFORM_TASK = "Perform only the following task:";
|
|
||||||
const PRESERVE_LANGUAGE_PROMPT = "Do not translate. Strictly keep the original language of the input text. For example, if it's French, keep French. If it's English, keep English.";
|
|
||||||
const DO_NOT_ADD_INFO_PROMPT = "Do not add any extra information or interpret anything beyond the explicit task.";
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// In a near future, prompts will be loaded from a remote source
|
|
||||||
class AIPrompts {
|
class AIPrompts {
|
||||||
|
static const _performTask = "Perform only the following task:";
|
||||||
|
static const _preserveLanguagePrompt =
|
||||||
|
"Do not translate. Strictly keep the original language of the input text. For example, if it's French, keep French. If it's English, keep English.";
|
||||||
|
static const _doNotAddInfoPrompt =
|
||||||
|
"Do not add any extra information or interpret anything beyond the explicit task.";
|
||||||
|
|
||||||
static String buildPrompt(AIAction action, String? text) {
|
static String buildPrompt(AIAction action, String? text) {
|
||||||
return switch (action) {
|
return switch (action) {
|
||||||
PredefinedAction(action: final menuAction) =>
|
PredefinedAction(action: final menuAction) =>
|
||||||
buildPredefinedPrompt(menuAction, text ?? ''),
|
text?.trim().isNotEmpty == true
|
||||||
|
? buildPredefinedPrompt(menuAction, text!)
|
||||||
|
: throw ArgumentError('Text cannot be empty for predefined actions'),
|
||||||
CustomPromptAction(prompt: final customPrompt) =>
|
CustomPromptAction(prompt: final customPrompt) =>
|
||||||
buildCustomPrompt(customPrompt, text),
|
buildCustomPrompt(customPrompt, text),
|
||||||
};
|
};
|
||||||
@@ -47,31 +49,31 @@ class AIPrompts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String improveMakeShorter(String text) {
|
static String improveMakeShorter(String text) {
|
||||||
return '$PERFORM_TASK make the text shorter but preserve the meaning. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
|
return '$_performTask make the text shorter but preserve the meaning. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String improveExpandContext(String text) {
|
static String improveExpandContext(String text) {
|
||||||
return '$PERFORM_TASK expand the context of the text to make it more detailed and comprehensive. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
|
return '$_performTask expand the context of the text to make it more detailed and comprehensive. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String improveEmojify(String text) {
|
static String improveEmojify(String text) {
|
||||||
return '$PERFORM_TASK add emojis to the important parts of the text. Do not try to rephrase or replace text. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
|
return '$_performTask add emojis to the important parts of the text. Do not try to rephrase or replace text. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String improveTransformToBullets(String text) {
|
static String improveTransformToBullets(String text) {
|
||||||
return '$PERFORM_TASK transform the text in a bullet list. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
|
return '$_performTask transform the text into a bullet list. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String correctGrammar(String text) {
|
static String correctGrammar(String text) {
|
||||||
return '$PERFORM_TASK correct grammar and spelling. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
|
return '$_performTask correct grammar and spelling. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String changeToneTo(String text, String tone) {
|
static String changeToneTo(String text, String tone) {
|
||||||
return '$PERFORM_TASK change the tone to be $tone. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
|
return '$_performTask change the tone to be $tone. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String translateTo(String text, String language) {
|
static String translateTo(String text, String language) {
|
||||||
return '$PERFORM_TASK translate. Translate the text to the specified language: $language. $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
|
return '$_performTask translate. Translate the text to the specified language: $language. $_doNotAddInfoPrompt Text:\n\n$text';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String buildCustomPrompt(String customPrompt, String? text) {
|
static String buildCustomPrompt(String customPrompt, String? text) {
|
||||||
|
|||||||
@@ -2,16 +2,4 @@ class AIResponse {
|
|||||||
final String result;
|
final String result;
|
||||||
|
|
||||||
const AIResponse({required this.result});
|
const AIResponse({required this.result});
|
||||||
|
|
||||||
factory AIResponse.fromJson(Map<String, dynamic> json) {
|
|
||||||
return AIResponse(
|
|
||||||
result: json['result'] as String? ?? json['text'] as String? ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'result': result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"replaceButton": "заменить",
|
"replaceButton": "Заменить",
|
||||||
"@replaceButton": {
|
"@replaceButton": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'dart:ui' show Offset;
|
||||||
|
|
||||||
class TextSelectionModel {
|
class TextSelectionModel {
|
||||||
final String? selectedText;
|
final String? selectedText;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
typedef OnHoverShowSubmenu = void Function(GlobalKey key);
|
typedef OnHoverShowSubmenu = void Function(GlobalKey key);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:flutter/animation.dart';
|
import 'dart:ui';
|
||||||
import 'package:scribe/scribe.dart';
|
import 'package:scribe/scribe.dart';
|
||||||
|
|
||||||
class AnchoredModalLayoutCalculator {
|
class AnchoredModalLayoutCalculator {
|
||||||
@@ -201,21 +201,22 @@ class AnchoredModalLayoutCalculator {
|
|||||||
switch (placement) {
|
switch (placement) {
|
||||||
case ModalPlacement.right:
|
case ModalPlacement.right:
|
||||||
left = anchorPosition.dx + anchorSize.width + gap;
|
left = anchorPosition.dx + anchorSize.width + gap;
|
||||||
bottom = anchorPosition.dy + verticalOffset;
|
bottom = screenSize.height - (anchorPosition.dy + verticalOffset);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ModalPlacement.bottom:
|
case ModalPlacement.bottom:
|
||||||
left = anchorPosition.dx;
|
left = anchorPosition.dx;
|
||||||
bottom = anchorPosition.dy + anchorSize.height + gap + verticalOffset;
|
bottom = screenSize.height - (anchorPosition.dy + anchorSize.height + gap + verticalOffset);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ModalPlacement.top:
|
case ModalPlacement.top:
|
||||||
left = anchorPosition.dx;
|
left = anchorPosition.dx;
|
||||||
bottom = anchorPosition.dy - menuSize.height - gap + verticalOffset;
|
bottom = screenSize.height - (anchorPosition.dy - menuSize.height - gap + verticalOffset);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ModalPlacement.left:
|
case ModalPlacement.left:
|
||||||
left = anchorPosition.dx - menuSize.width - gap;
|
left = anchorPosition.dx - menuSize.width - gap;
|
||||||
bottom = anchorPosition.dy + verticalOffset;
|
bottom = screenSize.height - (anchorPosition.dy + verticalOffset);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget> {
|
|||||||
widget.content,
|
widget.content,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
(failure) => _state.value = dartz.Left(failure),
|
(failure) => _state.value = dartz.Left(failure),
|
||||||
(success) => _state.value = dartz.Right(success),
|
(success) => _state.value = dartz.Right(success),
|
||||||
@@ -203,9 +205,18 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget> {
|
|||||||
widget.buttonPosition != null && widget.buttonSize != null;
|
widget.buttonPosition != null && widget.buttonSize != null;
|
||||||
|
|
||||||
void _handleClickOutside() {
|
void _handleClickOutside() {
|
||||||
final result = _state.value.getOrElse(() => UIState.idle);
|
final shouldDismiss = _state.value.fold(
|
||||||
if (result is GenerateAITextSuccess || result is GenerateAITextFailure) {
|
(failure) => failure is GenerateAITextFailure,
|
||||||
|
(success) => success is GenerateAITextSuccess,
|
||||||
|
);
|
||||||
|
if (shouldDismiss) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_state.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-6
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:core/presentation/extensions/color_extension.dart';
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
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';
|
||||||
@@ -110,21 +112,18 @@ class _AnimatedEllipsisText extends StatefulWidget {
|
|||||||
class _AnimatedEllipsisTextState extends State<_AnimatedEllipsisText> {
|
class _AnimatedEllipsisTextState extends State<_AnimatedEllipsisText> {
|
||||||
static const _dotStates = ['', '.', '..', '...'];
|
static const _dotStates = ['', '.', '..', '...'];
|
||||||
int _index = 0;
|
int _index = 0;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tick();
|
_timer = Timer.periodic(const Duration(milliseconds: 500), (_) {
|
||||||
}
|
|
||||||
|
|
||||||
void _tick() {
|
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _index = (_index + 1) % _dotStates.length);
|
setState(() => _index = (_index + 1) % _dotStates.length);
|
||||||
_tick();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Text(
|
return Text(
|
||||||
@@ -132,4 +131,10 @@ class _AnimatedEllipsisTextState extends State<_AnimatedEllipsisText> {
|
|||||||
style: widget.style,
|
style: widget.style,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -27,7 +27,8 @@ dart run build_runner build --delete-conflicting-outputs &&
|
|||||||
dart run intl_generator:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/main/localizations/app_localizations.dart lib/l10n/intl*.arb
|
dart run intl_generator:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/main/localizations/app_localizations.dart lib/l10n/intl*.arb
|
||||||
|
|
||||||
# For scribe module localizations
|
# For scribe module localizations
|
||||||
cd scribe
|
(
|
||||||
dart run intl_generator:extract_to_arb --output-dir=./lib/scribe/ai/l10n lib/scribe/ai/localizations/scribe_localizations.dart &&
|
cd scribe
|
||||||
dart run intl_generator:generate_from_arb --output-dir=lib/scribe/ai/l10n --no-use-deferred-loading lib/scribe/ai/localizations/scribe_localizations.dart lib/scribe/ai/l10n/intl*.arb
|
dart run intl_generator:extract_to_arb --output-dir=./lib/scribe/ai/l10n lib/scribe/ai/localizations/scribe_localizations.dart &&
|
||||||
cd ../
|
dart run intl_generator:generate_from_arb --output-dir=lib/scribe/ai/l10n --no-use-deferred-loading lib/scribe/ai/localizations/scribe_localizations.dart lib/scribe/ai/l10n/intl*.arb
|
||||||
|
)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import 'package:model/email/email_action_type.dart';
|
|||||||
import 'package:model/extensions/session_extension.dart';
|
import 'package:model/extensions/session_extension.dart';
|
||||||
import 'package:model/mailbox/presentation_mailbox.dart';
|
import 'package:model/mailbox/presentation_mailbox.dart';
|
||||||
import 'package:rich_text_composer/rich_text_composer.dart';
|
import 'package:rich_text_composer/rich_text_composer.dart';
|
||||||
import 'package:scribe/scribe/ai/domain/usecases/generate_ai_text_interactor.dart';
|
|
||||||
import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart';
|
import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart';
|
||||||
import 'package:tmail_ui_user/features/caching/caching_manager.dart';
|
import 'package:tmail_ui_user/features/caching/caching_manager.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart';
|
import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart';
|
||||||
@@ -147,7 +146,7 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon
|
|||||||
RxBool get isPopupMenuOpened => false.obs;
|
RxBool get isPopupMenuOpened => false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rx<AIScribeConfig> get cachedAIScribeConfig => Rx<AIScribeConfig>(AIScribeConfig.initial());
|
Rx<AIScribeConfig> get cachedAIScribeConfig => AIScribeConfig.initial().obs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateNiceMocks([
|
@GenerateNiceMocks([
|
||||||
@@ -183,7 +182,6 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon
|
|||||||
MockSpec<PrintEmailInteractor>(),
|
MockSpec<PrintEmailInteractor>(),
|
||||||
MockSpec<ComposerRepository>(),
|
MockSpec<ComposerRepository>(),
|
||||||
MockSpec<SaveTemplateEmailInteractor>(),
|
MockSpec<SaveTemplateEmailInteractor>(),
|
||||||
MockSpec<GenerateAITextInteractor>(),
|
|
||||||
|
|
||||||
// Additional Getx dependencies mock specs
|
// Additional Getx dependencies mock specs
|
||||||
MockSpec<NetworkConnectionController>(fallbackGenerators: fallbackGenerators),
|
MockSpec<NetworkConnectionController>(fallbackGenerators: fallbackGenerators),
|
||||||
|
|||||||
Reference in New Issue
Block a user