From 27e91316d3e812014c68067d0a039cdeee8086a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Fri, 3 Apr 2026 11:56:25 +0200 Subject: [PATCH] Translation caching. New ai actions panel. --- Wino.Core.Domain/Enums/AiActionType.cs | 12 + .../Interfaces/IAiActionOptionsService.cs | 10 + .../Interfaces/IMimeFileService.cs | 10 + .../Models/Ai/AiRewriteModeOption.cs | 3 + .../Models/Ai/AiTranslateLanguageOption.cs | 3 + .../Translations/en_US/resources.json | 16 +- .../MailRenderingPageViewModel.cs | 2 + Wino.Mail.WinUI/App.xaml.cs | 1 + .../Behaviors/BindableCommandBarBehavior.cs | 33 +- Wino.Mail.WinUI/Controls/AiActionsPanel.xaml | 232 ++++++++ .../Controls/AiActionsPanel.xaml.cs | 519 ++++++++++++++++++ Wino.Mail.WinUI/Controls/IAiHtmlActionHost.cs | 12 + .../Services/AiActionOptionsService.cs | 46 ++ .../Services/WinoAccountAiErrorTranslator.cs | 57 ++ Wino.Mail.WinUI/Views/Mail/ComposePage.xaml | 23 +- .../Views/Mail/ComposePage.xaml.cs | 36 ++ .../Views/Mail/MailRenderingPage.xaml | 34 +- .../Views/Mail/MailRenderingPage.xaml.cs | 61 ++ Wino.Services/MimeFileService.cs | 59 ++ Wino.Services/WinoAccountApiClient.cs | 4 +- 20 files changed, 1150 insertions(+), 23 deletions(-) create mode 100644 Wino.Core.Domain/Enums/AiActionType.cs create mode 100644 Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs create mode 100644 Wino.Core.Domain/Models/Ai/AiRewriteModeOption.cs create mode 100644 Wino.Core.Domain/Models/Ai/AiTranslateLanguageOption.cs create mode 100644 Wino.Mail.WinUI/Controls/AiActionsPanel.xaml create mode 100644 Wino.Mail.WinUI/Controls/AiActionsPanel.xaml.cs create mode 100644 Wino.Mail.WinUI/Controls/IAiHtmlActionHost.cs create mode 100644 Wino.Mail.WinUI/Services/AiActionOptionsService.cs create mode 100644 Wino.Mail.WinUI/Services/WinoAccountAiErrorTranslator.cs diff --git a/Wino.Core.Domain/Enums/AiActionType.cs b/Wino.Core.Domain/Enums/AiActionType.cs new file mode 100644 index 00000000..26501bb0 --- /dev/null +++ b/Wino.Core.Domain/Enums/AiActionType.cs @@ -0,0 +1,12 @@ +using System; + +namespace Wino.Core.Domain.Enums; + +[Flags] +public enum AiActionType +{ + None = 0, + Translate = 1, + Rewrite = 2, + Summarize = 4, +} diff --git a/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs b/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs new file mode 100644 index 00000000..5efaf1e7 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IAiActionOptionsService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Models.Ai; + +namespace Wino.Core.Domain.Interfaces; + +public interface IAiActionOptionsService +{ + IReadOnlyList GetTranslateLanguageOptions(); + IReadOnlyList GetRewriteModeOptions(); +} diff --git a/Wino.Core.Domain/Interfaces/IMimeFileService.cs b/Wino.Core.Domain/Interfaces/IMimeFileService.cs index a7b5b8db..bf4a79f7 100644 --- a/Wino.Core.Domain/Interfaces/IMimeFileService.cs +++ b/Wino.Core.Domain/Interfaces/IMimeFileService.cs @@ -59,6 +59,16 @@ public interface IMimeFileService /// Task DeleteMimeMessageAsync(Guid accountId, Guid fileId); + /// + /// Returns cached translated html for the given mime resource if it exists. + /// + Task GetTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, CancellationToken cancellationToken = default); + + /// + /// Saves translated html for the given mime resource. + /// + Task SaveTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, string html, CancellationToken cancellationToken = default); + /// /// Prepares the final model containing rendering details. /// diff --git a/Wino.Core.Domain/Models/Ai/AiRewriteModeOption.cs b/Wino.Core.Domain/Models/Ai/AiRewriteModeOption.cs new file mode 100644 index 00000000..3b3013bb --- /dev/null +++ b/Wino.Core.Domain/Models/Ai/AiRewriteModeOption.cs @@ -0,0 +1,3 @@ +namespace Wino.Core.Domain.Models.Ai; + +public sealed record AiRewriteModeOption(string Mode, string Label, string Description, bool IsCustom = false); diff --git a/Wino.Core.Domain/Models/Ai/AiTranslateLanguageOption.cs b/Wino.Core.Domain/Models/Ai/AiTranslateLanguageOption.cs new file mode 100644 index 00000000..ed0b97be --- /dev/null +++ b/Wino.Core.Domain/Models/Ai/AiTranslateLanguageOption.cs @@ -0,0 +1,3 @@ +namespace Wino.Core.Domain.Models.Ai; + +public sealed record AiTranslateLanguageOption(string Code, string Label); diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 4e592398..33a1310b 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -1065,10 +1065,18 @@ "Composer_AiTranslate": "Translate with AI", "Composer_AiActions": "AI Actions", "Composer_AiRewrite": "Rewrite with AI", + "AiActions_CheckingStatus": "Checking AI access...", + "AiActions_SignedOutTitle": "Unlock Wino AI Pack", + "AiActions_SignedOutDescription": "Translate, rewrite, and summarize emails with AI after signing in to your Wino Account and activating the AI Pack add-on.", + "AiActions_NoPackTitle": "AI Pack required", + "AiActions_NoPackDescription": "You're signed in, but AI Pack is not active yet. Purchase it to use Wino's AI translation, rewrite, and summarize tools.", + "AiActions_UsageSummary": "{0} of {1} requests used this month.", "Composer_AiRewritePolite": "Make it polite", "Composer_AiRewritePoliteDescription": "Softens the wording while keeping the same intent.", - "Composer_AiRewriteAngry": "Make it direct", - "Composer_AiRewriteAngryDescription": "Uses a firmer, more forceful tone.", + "Composer_AiRewriteAngry": "Make it angry", + "Composer_AiRewriteAngryDescription": "Uses a sharper and more confrontational tone.", + "Composer_AiRewriteHappy": "Make it happy", + "Composer_AiRewriteHappyDescription": "Adds a more upbeat and enthusiastic tone.", "Composer_AiRewriteFormal": "Make it formal", "Composer_AiRewriteFormalDescription": "Makes the message sound more professional and structured.", "Composer_AiRewriteFriendly": "Make it friendly", @@ -1077,6 +1085,9 @@ "Composer_AiRewriteShorterDescription": "Tightens the text and removes unnecessary detail.", "Composer_AiRewriteClearer": "Make it clearer", "Composer_AiRewriteClearerDescription": "Improves readability and makes the message easier to follow.", + "Composer_AiRewriteCustom": "Custom", + "Composer_AiRewriteCustomDescription": "Describe your own rewrite intention.", + "Composer_AiRewriteCustomPlaceholder": "Describe how you want the message rewritten", "Composer_AiRewriteMode": "Rewrite tone", "Composer_AiRewriteApply": "Apply rewrite", "Composer_AiTranslateDialogTitle": "Translate with AI", @@ -1326,6 +1337,7 @@ "WinoAccount_Error_ExternalLoginInvalid": "The external sign-in request is invalid.", "WinoAccount_Error_ExternalAuthStateInvalid": "The external sign-in state is invalid or expired.", "WinoAccount_Error_ExternalAuthCodeInvalid": "The external sign-in code is invalid or expired.", + "WinoAccount_Error_AiPackRequired": "An active Wino AI Pack subscription is required for this action.", "WinoAccount_Error_AiQuotaExceeded": "Your AI Pack usage limit has been reached for the current billing period.", "WinoAccount_Error_AiHtmlEmpty": "There is no email content to process.", "WinoAccount_Error_AiHtmlTooLarge": "This email is too large to process with Wino AI.", diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 11140273..24611d8b 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -141,6 +141,8 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, public IStatePersistanceService StatePersistenceService { get; } public IPreferencesService PreferencesService { get; } public IPrintService PrintService { get; } + public Guid? CurrentMailAccountId => initializedMailItemViewModel?.MailCopy.AssignedAccount?.Id; + public Guid? CurrentMailFileId => initializedMailItemViewModel?.MailCopy.FileId; public MailRenderingPageViewModel(IMailDialogService dialogService, INativeAppService nativeAppService, diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index a185a8d2..d59a30b0 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -298,6 +298,7 @@ public partial class App : WinoApplication, { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services.AddSingleton(); services.AddSingleton(); diff --git a/Wino.Mail.WinUI/Behaviors/BindableCommandBarBehavior.cs b/Wino.Mail.WinUI/Behaviors/BindableCommandBarBehavior.cs index fdb2d9ae..a8228679 100644 --- a/Wino.Mail.WinUI/Behaviors/BindableCommandBarBehavior.cs +++ b/Wino.Mail.WinUI/Behaviors/BindableCommandBarBehavior.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Specialized; using System.Windows.Input; @@ -9,9 +10,9 @@ using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.Xaml.Interactivity; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Menus; -using Wino.Mail.WinUI.Controls; using Wino.Helpers; using Wino.Mail.WinUI; +using Wino.Mail.WinUI.Controls; namespace Wino.Behaviors; @@ -41,14 +42,25 @@ public partial class BindableCommandBarBehavior : Behavior { foreach (var item in enumerable) { - if (item is ButtonBase button) - { - button.Click -= Button_Click; - } + DetachCommandElement(item); } } } + private void DetachCommandElement(object item) + { + if (item is ButtonBase button) + { + button.Click -= Button_Click; + return; + } + + if (item is AppBarElementContainer container && container.Content is IDisposable disposable) + { + disposable.Dispose(); + } + } + private void UpdatePrimaryCommands() { if (AssociatedObject == null) @@ -61,10 +73,7 @@ public partial class BindableCommandBarBehavior : Behavior { foreach (var item in enumerableObjects) { - if (item is ButtonBase button) - { - button.Click -= Button_Click; - } + DetachCommandElement(item); } } @@ -72,10 +81,7 @@ public partial class BindableCommandBarBehavior : Behavior { foreach (var item in secondaryObject) { - if (item is ButtonBase button) - { - button.Click -= Button_Click; - } + DetachCommandElement(item); } } @@ -135,7 +141,6 @@ public partial class BindableCommandBarBehavior : Behavior AssociatedObject.PrimaryCommands.Add(menuItem); } } - //if (dependencyObject is ICommandBarElement icommandBarElement) //{ // if (dependencyObject is ButtonBase button) diff --git a/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml b/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml new file mode 100644 index 00000000..d308f517 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +