diff --git a/Directory.Packages.props b/Directory.Packages.props index 700a216d..3c780068 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,7 +33,7 @@ - + diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs index 9b80f8e2..53645338 100644 --- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs +++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs @@ -67,6 +67,21 @@ public interface IPreferencesService : INotifyPropertyChanged /// bool IsWinoAccountButtonHidden { get; set; } + /// + /// Setting: Default target language code used for AI translation actions. + /// + string AiDefaultTranslationLanguageCode { get; set; } + + /// + /// Setting: Preferred target language code for AI summarize actions. + /// + string AiSummarizeLanguageCode { get; set; } + + /// + /// Setting: Preferred folder path used when saving AI summaries. + /// + string AiSummarySavePath { get; set; } + /// /// Serializes the current syncable preferences snapshot. /// diff --git a/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs b/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs index 615de7d6..a76b1764 100644 --- a/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs +++ b/Wino.Core.Domain/Interfaces/IWinoAccountApiClient.cs @@ -19,7 +19,7 @@ public interface IWinoAccountApiClient Task> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default); Task> GetCurrentUserAsync(CancellationToken cancellationToken = default); Task> GetAiStatusAsync(CancellationToken cancellationToken = default); - Task> SummarizeAsync(string html, CancellationToken cancellationToken = default); + Task> SummarizeAsync(string html, string targetLanguage, CancellationToken cancellationToken = default); Task> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default); Task> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default); Task> CreateCollectionsIdTicketAsync(CancellationToken cancellationToken = default); diff --git a/Wino.Core.Domain/Interfaces/IWinoAccountProfileService.cs b/Wino.Core.Domain/Interfaces/IWinoAccountProfileService.cs index 66083d43..7857c8cf 100644 --- a/Wino.Core.Domain/Interfaces/IWinoAccountProfileService.cs +++ b/Wino.Core.Domain/Interfaces/IWinoAccountProfileService.cs @@ -24,7 +24,7 @@ public interface IWinoAccountProfileService Task HasActiveAccountAsync(); Task> GetCurrentUserAsync(CancellationToken cancellationToken = default); Task> GetAiStatusAsync(CancellationToken cancellationToken = default); - Task> SummarizeAsync(string html, CancellationToken cancellationToken = default); + Task> SummarizeAsync(string html, string targetLanguage, CancellationToken cancellationToken = default); Task> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default); Task> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default); Task> SyncStoreEntitlementsAsync(CancellationToken cancellationToken = default); diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index a8fd04fa..51c0b3b1 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -724,6 +724,16 @@ "SettingsAppPreferences_HideWinoAccountButton_Description": "Hide the title bar profile button that opens the Wino account flyout.", "SettingsAppPreferences_StoreUpdateNotifications_Title": "Store update notifications", "SettingsAppPreferences_StoreUpdateNotifications_Description": "Show notifications and footer actions when a Microsoft Store update is available.", + "SettingsAppPreferences_AiActions_Title": "AI actions", + "SettingsAppPreferences_AiActions_Description": "Choose default AI languages and where summaries should be saved.", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Title": "Default translation language", + "SettingsAppPreferences_AiDefaultTranslationLanguage_Description": "Pick the default target language used by AI translate actions.", + "SettingsAppPreferences_AiSummarizeLanguage_Title": "Summarize language", + "SettingsAppPreferences_AiSummarizeLanguage_Description": "Choose the preferred summarize language for future AI summary output.", + "SettingsAppPreferences_AiSummarySavePath_Title": "Default summary save path", + "SettingsAppPreferences_AiSummarySavePath_Description": "Choose the folder Wino should use by default when saving AI summaries.", + "SettingsAppPreferences_AiSummarySavePath_Placeholder": "Use the system default save location", + "SettingsAppPreferences_AiSummarySavePath_InvalidHint": "This folder does not exist. The default save location will be used for summaries.", "SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.", "SettingsAutoSelectNextItem_Title": "Auto select next item", "SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.", @@ -1071,7 +1081,7 @@ "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.", + "AiActions_UsageSummary": "{0} of {1} credits used this month.", "Composer_AiRewritePolite": "Make it polite", "Composer_AiRewritePoliteDescription": "Softens the wording while keeping the same intent.", "Composer_AiRewriteAngry": "Make it angry", @@ -1243,7 +1253,7 @@ "WinoAccount_Management_AiPackPromoTitle": "Unlock AI Pack", "WinoAccount_Management_AiPackPromoDescription": "Supercharge your email workflow with AI-powered tools. Translate messages into 50+ languages, rewrite for clarity and tone, and get instant summaries of long threads.", "WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo", - "WinoAccount_Management_AiPackPromoRequests": "1,000 requests", + "WinoAccount_Management_AiPackPromoRequests": "1,000 credits", "WinoAccount_Management_AiPackGetButton": "Get AI Pack", "WinoAddOn_AI_PACK_Name": "Wino AI Pack", "WinoAddOn_AI_PACK_Description": "AI-powered tools for translate, rewrite, and summarize actions in Wino Mail.", @@ -1256,7 +1266,7 @@ "WinoAccount_Management_StoreSyncFailed": "Your purchase finished, but Wino could not refresh your account benefits yet. Please try again in a moment.", "WinoAccount_Management_AiPackSubscriptionActive": "Your subscription is active", "WinoAccount_Management_AiPackRenews": "Renews {0:d}", - "WinoAccount_Management_AiPackRequestsUsed": "Requests used this month", + "WinoAccount_Management_AiPackRequestsUsed": "Credits used this month", "WinoAccount_Management_AiPackResets": "Resets {0:d}", "WinoAccount_Management_AiPackUsageLoadFailed": "We had issues loading your AI usage balance.", "WinoAccount_Management_AiPackFeatureTranslate": "Translate", diff --git a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs index 8f4c2724..4c6ab537 100644 --- a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs +++ b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Ai; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Translations; @@ -16,12 +18,14 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel IMailDialogService dialogService, IPreferencesService preferencesService, IStartupBehaviorService startupBehaviorService, - ITranslationService translationService) + ITranslationService translationService, + IAiActionOptionsService aiActionOptionsService) { _dialogService = dialogService; PreferencesService = preferencesService; _startupBehaviorService = startupBehaviorService; _translationService = translationService; + _aiActionOptionsService = aiActionOptionsService; SearchModes = [ @@ -40,6 +44,7 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel SelectedDefaultSearchMode = SearchModes[(int)PreferencesService.DefaultSearchMode]; SelectedDefaultApplicationMode = ApplicationModes[(int)PreferencesService.DefaultApplicationMode]; EmailSyncIntervalMinutes = PreferencesService.EmailSyncIntervalMinutes; + SummarySavePath = PreferencesService.AiSummarySavePath; } public IPreferencesService PreferencesService { get; } @@ -56,6 +61,21 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel [ObservableProperty] public partial AppLanguageModel SelectedLanguage { get; set; } + [ObservableProperty] + public partial List AvailableAiLanguages { get; set; } = []; + + [ObservableProperty] + public partial AiTranslateLanguageOption SelectedDefaultTranslationLanguage { get; set; } + + [ObservableProperty] + public partial AiTranslateLanguageOption SelectedSummarizeLanguage { get; set; } + + [ObservableProperty] + public partial string SummarySavePath { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool HasInvalidSummarySavePath { get; set; } + [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))] [NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))] @@ -64,7 +84,9 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel private readonly IMailDialogService _dialogService; private readonly IStartupBehaviorService _startupBehaviorService; private readonly ITranslationService _translationService; + private readonly IAiActionOptionsService _aiActionOptionsService; private bool _isLanguageInitialized; + private bool _isAiPreferencesInitialized; private int _emailSyncIntervalMinutes; private string _selectedDefaultSearchMode; private string _selectedDefaultApplicationMode; @@ -110,6 +132,31 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel _ = _translationService.InitializeLanguageAsync(value.Language); } + partial void OnSelectedDefaultTranslationLanguageChanged(AiTranslateLanguageOption value) + { + if (!_isAiPreferencesInitialized || value == null) + return; + + PreferencesService.AiDefaultTranslationLanguageCode = value.Code; + } + + partial void OnSelectedSummarizeLanguageChanged(AiTranslateLanguageOption value) + { + if (!_isAiPreferencesInitialized || value == null) + return; + + PreferencesService.AiSummarizeLanguageCode = value.Code; + } + + partial void OnSummarySavePathChanged(string value) + { + if (!_isAiPreferencesInitialized) + return; + + PreferencesService.AiSummarySavePath = value ?? string.Empty; + RefreshSummarySavePathState(); + } + [RelayCommand] private async Task ToggleStartupBehaviorAsync() { @@ -157,15 +204,56 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel } } + [RelayCommand] + private async Task BrowseSummarySavePathAsync() + { + var pickedPath = await _dialogService.PickWindowsFolderAsync(); + if (string.IsNullOrWhiteSpace(pickedPath)) + return; + + await ExecuteUIThread(() => SummarySavePath = pickedPath); + } + public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); - AvailableLanguages = _translationService.GetAvailableLanguages(); - SelectedLanguage = AvailableLanguages.Find(language => language.Language == PreferencesService.CurrentLanguage) - ?? (AvailableLanguages.Count > 0 ? AvailableLanguages[0] : null); - _isLanguageInitialized = true; + var availableLanguages = _translationService.GetAvailableLanguages(); + var availableAiLanguages = new List(_aiActionOptionsService.GetTranslateLanguageOptions()); + var startupBehaviorResult = await _startupBehaviorService.GetCurrentStartupBehaviorAsync(); - StartupBehaviorResult = await _startupBehaviorService.GetCurrentStartupBehaviorAsync(); + await ExecuteUIThread(() => + { + AvailableLanguages = availableLanguages; + SelectedLanguage = AvailableLanguages.Find(language => language.Language == PreferencesService.CurrentLanguage) + ?? (AvailableLanguages.Count > 0 ? AvailableLanguages[0] : null); + _isLanguageInitialized = true; + + AvailableAiLanguages = availableAiLanguages; + SelectedDefaultTranslationLanguage = FindAiLanguageOption(PreferencesService.AiDefaultTranslationLanguageCode) + ?? FindAiLanguageOption("en-US") + ?? (AvailableAiLanguages.Count > 0 ? AvailableAiLanguages[0] : null); + SelectedSummarizeLanguage = FindAiLanguageOption(PreferencesService.AiSummarizeLanguageCode) + ?? FindAiLanguageOption("en-US") + ?? (AvailableAiLanguages.Count > 0 ? AvailableAiLanguages[0] : null); + SummarySavePath = PreferencesService.AiSummarySavePath; + RefreshSummarySavePathState(); + _isAiPreferencesInitialized = true; + + StartupBehaviorResult = startupBehaviorResult; + }); + } + + private AiTranslateLanguageOption FindAiLanguageOption(string languageCode) + { + if (string.IsNullOrWhiteSpace(languageCode)) + return null; + + return AvailableAiLanguages.Find(option => option.Code == languageCode); + } + + private void RefreshSummarySavePathState() + { + HasInvalidSummarySavePath = !string.IsNullOrWhiteSpace(SummarySavePath) && !Directory.Exists(SummarySavePath); } } diff --git a/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml b/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml index b799c1e2..cf49e1f9 100644 --- a/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml +++ b/Wino.Mail.WinUI/Controls/AiActionsPanel.xaml @@ -143,71 +143,50 @@ Style="{StaticResource AccentButtonStyle}" /> - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -284,11 +263,24 @@ x:Name="SummarizeOptionsPanel" Spacing="8" Visibility="Collapsed"> - - + + + + + + + + + + +