2026-04-03 11:56:25 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2026-04-03 19:50:52 +02:00
|
|
|
using System.IO;
|
2026-04-03 11:56:25 +02:00
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using CommunityToolkit.WinUI;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using Microsoft.UI.Xaml;
|
|
|
|
|
using Microsoft.UI.Xaml.Controls;
|
2026-04-03 19:50:52 +02:00
|
|
|
using Microsoft.UI.Xaml.Media;
|
2026-04-03 11:56:25 +02:00
|
|
|
using Wino.Core.Domain;
|
|
|
|
|
using Wino.Core.Domain.Enums;
|
|
|
|
|
using Wino.Core.Domain.Interfaces;
|
|
|
|
|
using Wino.Core.Domain.Models.Ai;
|
|
|
|
|
using Wino.Mail.Api.Contracts.Ai;
|
|
|
|
|
using Wino.Mail.Api.Contracts.Common;
|
|
|
|
|
using Wino.Mail.WinUI.Services;
|
|
|
|
|
|
|
|
|
|
namespace Wino.Mail.WinUI.Controls;
|
|
|
|
|
|
|
|
|
|
public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
public event EventHandler? CloseRequested;
|
2026-04-03 11:56:25 +02:00
|
|
|
private readonly IWinoAccountProfileService _profileService = App.Current.Services.GetRequiredService<IWinoAccountProfileService>();
|
|
|
|
|
private readonly IStoreManagementService _storeManagementService = App.Current.Services.GetRequiredService<IStoreManagementService>();
|
|
|
|
|
private readonly IMailDialogService _dialogService = App.Current.Services.GetRequiredService<IMailDialogService>();
|
|
|
|
|
private readonly IAiActionOptionsService _optionsService = App.Current.Services.GetRequiredService<IAiActionOptionsService>();
|
2026-04-04 01:34:57 +02:00
|
|
|
private readonly IPreferencesService _preferencesService = App.Current.Services.GetRequiredService<IPreferencesService>();
|
2026-04-03 11:56:25 +02:00
|
|
|
|
|
|
|
|
private bool _disposedValue;
|
|
|
|
|
private bool _isRefreshing;
|
|
|
|
|
private bool _isBusy;
|
|
|
|
|
private AiActionType _lastConfigurableAction = AiActionType.Translate;
|
2026-04-03 19:50:52 +02:00
|
|
|
private bool _hasCachedSummary;
|
2026-04-03 11:56:25 +02:00
|
|
|
private CancellationTokenSource? _actionCancellationTokenSource;
|
|
|
|
|
private IReadOnlyList<AiTranslateLanguageOption> _translateOptions = Array.Empty<AiTranslateLanguageOption>();
|
|
|
|
|
private IReadOnlyList<AiRewriteModeOption> _rewriteOptions = Array.Empty<AiRewriteModeOption>();
|
|
|
|
|
|
|
|
|
|
[GeneratedDependencyProperty(DefaultValue = AiActionType.None)]
|
|
|
|
|
public partial AiActionType AvailableActions { get; set; }
|
|
|
|
|
|
|
|
|
|
[GeneratedDependencyProperty]
|
|
|
|
|
public partial IAiHtmlActionHost? HtmlHost { get; set; }
|
|
|
|
|
|
|
|
|
|
public AiTranslateLanguageOption? SelectedTranslateLanguageOption { get; set; }
|
|
|
|
|
|
2026-04-04 01:34:57 +02:00
|
|
|
public AiTranslateLanguageOption? SelectedSummarizeLanguageOption { get; set; }
|
|
|
|
|
|
2026-04-03 11:56:25 +02:00
|
|
|
public AiRewriteModeOption? SelectedRewriteModeOption { get; set; }
|
|
|
|
|
|
|
|
|
|
public AiActionsPanel()
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void CancelPendingOperation()
|
|
|
|
|
{
|
|
|
|
|
_actionCancellationTokenSource?.Cancel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
if (_disposedValue)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_disposedValue = true;
|
|
|
|
|
CancelAndDisposeActionCancellationToken();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
partial void OnAvailableActionsChanged(AiActionType newValue)
|
|
|
|
|
{
|
|
|
|
|
UpdateActionAvailability();
|
|
|
|
|
ApplySelectedAction(SelectDefaultAction());
|
2026-04-03 19:50:52 +02:00
|
|
|
_ = RefreshCachedSummaryStateAsync();
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
LoadOptions();
|
|
|
|
|
UpdateActionAvailability();
|
|
|
|
|
ApplySelectedAction(SelectDefaultAction());
|
|
|
|
|
_ = RefreshAvailabilityAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
CancelPendingOperation();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void LoadOptions()
|
|
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
// Save current selections before replacing ItemsSource (which clears SelectedItem).
|
|
|
|
|
var previousTranslateCode = SelectedTranslateLanguageOption?.Code;
|
2026-04-04 01:34:57 +02:00
|
|
|
var previousSummarizeCode = SelectedSummarizeLanguageOption?.Code;
|
2026-04-03 19:50:52 +02:00
|
|
|
var previousRewriteMode = SelectedRewriteModeOption?.Mode;
|
2026-04-04 01:34:57 +02:00
|
|
|
var preferredTranslateCode = string.IsNullOrWhiteSpace(previousTranslateCode)
|
|
|
|
|
? _preferencesService.AiDefaultTranslationLanguageCode
|
|
|
|
|
: previousTranslateCode;
|
|
|
|
|
var preferredSummarizeCode = string.IsNullOrWhiteSpace(previousSummarizeCode)
|
|
|
|
|
? _preferencesService.AiSummarizeLanguageCode
|
|
|
|
|
: previousSummarizeCode;
|
2026-04-03 19:50:52 +02:00
|
|
|
|
2026-04-03 11:56:25 +02:00
|
|
|
_translateOptions = _optionsService.GetTranslateLanguageOptions();
|
|
|
|
|
_rewriteOptions = _optionsService.GetRewriteModeOptions();
|
|
|
|
|
|
|
|
|
|
TranslateLanguageComboBox.ItemsSource = _translateOptions;
|
2026-04-04 01:34:57 +02:00
|
|
|
SummarizeLanguageComboBox.ItemsSource = _translateOptions;
|
2026-04-03 11:56:25 +02:00
|
|
|
RewriteModeComboBox.ItemsSource = _rewriteOptions;
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
// Restore selection by matching on value, falling back to first item.
|
2026-04-04 01:34:57 +02:00
|
|
|
SelectedTranslateLanguageOption = FindOption(_translateOptions, o => o.Code == preferredTranslateCode)
|
|
|
|
|
?? FindOption(_translateOptions, o => o.Code == "en-US")
|
|
|
|
|
?? (_translateOptions.Count > 0 ? _translateOptions[0] : null);
|
|
|
|
|
SelectedSummarizeLanguageOption = FindOption(_translateOptions, o => o.Code == preferredSummarizeCode)
|
|
|
|
|
?? FindOption(_translateOptions, o => o.Code == "en-US")
|
|
|
|
|
?? (_translateOptions.Count > 0 ? _translateOptions[0] : null);
|
2026-04-03 19:50:52 +02:00
|
|
|
SelectedRewriteModeOption = FindOption(_rewriteOptions, o => o.Mode == previousRewriteMode) ?? (_rewriteOptions.Count > 0 ? _rewriteOptions[0] : null);
|
2026-04-03 11:56:25 +02:00
|
|
|
|
|
|
|
|
TranslateLanguageComboBox.SelectedItem = SelectedTranslateLanguageOption;
|
2026-04-04 01:34:57 +02:00
|
|
|
SummarizeLanguageComboBox.SelectedItem = SelectedSummarizeLanguageOption;
|
2026-04-03 11:56:25 +02:00
|
|
|
RewriteModeComboBox.SelectedItem = SelectedRewriteModeOption;
|
|
|
|
|
UpdateRewriteOptionState();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
private static T? FindOption<T>(IReadOnlyList<T> options, Func<T, bool> predicate) where T : class
|
|
|
|
|
{
|
|
|
|
|
foreach (var option in options)
|
|
|
|
|
{
|
|
|
|
|
if (predicate(option))
|
|
|
|
|
{
|
|
|
|
|
return option;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 11:56:25 +02:00
|
|
|
private void UpdateActionAvailability()
|
|
|
|
|
{
|
|
|
|
|
TranslateSegment.Visibility = HasAction(AiActionType.Translate) ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
RewriteSegment.Visibility = HasAction(AiActionType.Rewrite) ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
SummarizeSegment.Visibility = HasAction(AiActionType.Summarize) ? Visibility.Visible : Visibility.Collapsed;
|
2026-04-03 19:50:52 +02:00
|
|
|
SummarizeCachedIndicator.Visibility = HasAction(AiActionType.Summarize) && _hasCachedSummary ? Visibility.Visible : Visibility.Collapsed;
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool HasAction(AiActionType action) => (AvailableActions & action) == action;
|
|
|
|
|
|
|
|
|
|
private AiActionType SelectDefaultAction()
|
|
|
|
|
{
|
|
|
|
|
if (HasAction(AiActionType.Translate))
|
|
|
|
|
{
|
|
|
|
|
return AiActionType.Translate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (HasAction(AiActionType.Rewrite))
|
|
|
|
|
{
|
|
|
|
|
return AiActionType.Rewrite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (HasAction(AiActionType.Summarize))
|
|
|
|
|
{
|
|
|
|
|
return AiActionType.Summarize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return AiActionType.None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplySelectedAction(AiActionType action)
|
|
|
|
|
{
|
|
|
|
|
if (action is AiActionType.Translate or AiActionType.Rewrite)
|
|
|
|
|
{
|
|
|
|
|
_lastConfigurableAction = action;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ActionSelector.SelectedItem = action switch
|
|
|
|
|
{
|
|
|
|
|
AiActionType.Translate => TranslateSegment,
|
|
|
|
|
AiActionType.Rewrite => RewriteSegment,
|
|
|
|
|
AiActionType.Summarize => SummarizeSegment,
|
|
|
|
|
_ => null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
TranslateOptionsPanel.Visibility = action == AiActionType.Translate ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
RewriteOptionsPanel.Visibility = action == AiActionType.Rewrite ? Visibility.Visible : Visibility.Collapsed;
|
2026-04-03 19:50:52 +02:00
|
|
|
SummarizeOptionsPanel.Visibility = action == AiActionType.Summarize ? Visibility.Visible : Visibility.Collapsed;
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task RefreshAvailabilityAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_isRefreshing || _disposedValue)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_isRefreshing = true;
|
|
|
|
|
SetBusyUi(isBusy: false, showLoading: true);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(true);
|
|
|
|
|
if (account == null)
|
|
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
UpdateUsageSummary(string.Empty);
|
2026-04-03 11:56:25 +02:00
|
|
|
UpdatePanelState(showSignedOut: true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(true);
|
|
|
|
|
if (!hasAiPack)
|
|
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
UpdateUsageSummary(string.Empty);
|
2026-04-03 11:56:25 +02:00
|
|
|
UpdatePanelState(showPurchase: true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(true);
|
|
|
|
|
if (aiStatusResponse.IsSuccess && aiStatusResponse.Result != null)
|
|
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
UpdateUsageSummary(
|
|
|
|
|
CreateUsageSummary(aiStatusResponse.Result),
|
|
|
|
|
GetUsedCount(aiStatusResponse.Result),
|
|
|
|
|
GetUsageLimit(aiStatusResponse.Result));
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed);
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
await RefreshCachedSummaryStateAsync().ConfigureAwait(true);
|
2026-04-03 11:56:25 +02:00
|
|
|
ApplySelectedAction(SelectDefaultAction());
|
|
|
|
|
UpdatePanelState(showReady: true);
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed);
|
2026-04-03 11:56:25 +02:00
|
|
|
UpdatePanelState(showReady: true);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_isRefreshing = false;
|
|
|
|
|
SetBusyUi(_isBusy, showLoading: false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string CreateUsageSummary(AiStatusResultDto aiStatus)
|
|
|
|
|
{
|
|
|
|
|
if (aiStatus.Used is int used && aiStatus.MonthlyLimit is int limit && limit > 0)
|
|
|
|
|
{
|
|
|
|
|
return string.Format(Translator.AiActions_UsageSummary, used, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
private static int GetUsedCount(AiStatusResultDto aiStatus)
|
|
|
|
|
=> aiStatus.Used is int used ? used : 0;
|
|
|
|
|
|
|
|
|
|
private static string CreateUsageSummary(QuotaInfoDto quotaInfo)
|
2026-04-03 11:56:25 +02:00
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
if (quotaInfo.Used is int used && quotaInfo.MonthlyLimit is int limit && limit > 0)
|
|
|
|
|
{
|
|
|
|
|
return string.Format(Translator.AiActions_UsageSummary, used, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
private static int GetUsedCount(QuotaInfoDto quotaInfo)
|
|
|
|
|
=> quotaInfo.Used is int used ? used : 0;
|
|
|
|
|
|
|
|
|
|
private static int GetUsageLimit(QuotaInfoDto quotaInfo)
|
|
|
|
|
=> quotaInfo.MonthlyLimit is int limit && limit > 0 ? limit : 1000;
|
|
|
|
|
|
|
|
|
|
private static int GetUsageLimit(AiStatusResultDto aiStatus)
|
|
|
|
|
=> aiStatus.MonthlyLimit is int limit && limit > 0 ? limit : 1000;
|
|
|
|
|
|
|
|
|
|
|
2026-04-03 11:56:25 +02:00
|
|
|
private void UpdatePanelState(bool showLoading = false, bool showSignedOut = false, bool showPurchase = false, bool showReady = false)
|
|
|
|
|
{
|
|
|
|
|
LoadingPanel.Visibility = showLoading ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
SignedOutPanel.Visibility = showSignedOut ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
PurchasePanel.Visibility = showPurchase ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
ReadyPanel.Visibility = showReady ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
private void UpdateUsageSummary(string usageText, int usedCount = 0)
|
|
|
|
|
{
|
|
|
|
|
UsageSummaryTextBlock.Text = usageText;
|
|
|
|
|
UsageProgressBar.Maximum = 1000;
|
|
|
|
|
UsageProgressBar.Value = Math.Min(usedCount, 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateUsageSummary(string usageText, int usedCount, int usageLimit)
|
2026-04-03 11:56:25 +02:00
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
var normalizedLimit = usageLimit > 0 ? usageLimit : 1000;
|
2026-04-03 11:56:25 +02:00
|
|
|
UsageSummaryTextBlock.Text = usageText;
|
2026-04-03 19:50:52 +02:00
|
|
|
UsageProgressBar.Maximum = normalizedLimit;
|
|
|
|
|
UsageProgressBar.Value = Math.Min(usedCount, normalizedLimit);
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetBusyUi(bool isBusy, bool showLoading)
|
|
|
|
|
{
|
|
|
|
|
_isBusy = isBusy;
|
|
|
|
|
|
|
|
|
|
BusyProgressBar.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
ActionSelector.IsEnabled = !isBusy;
|
|
|
|
|
TranslateLanguageComboBox.IsEnabled = !isBusy;
|
2026-04-04 01:34:57 +02:00
|
|
|
SummarizeLanguageComboBox.IsEnabled = !isBusy;
|
2026-04-03 11:56:25 +02:00
|
|
|
RewriteModeComboBox.IsEnabled = !isBusy;
|
|
|
|
|
CustomRewriteTextBox.IsEnabled = !isBusy;
|
|
|
|
|
RunTranslateButton.IsEnabled = !isBusy;
|
|
|
|
|
RunRewriteButton.IsEnabled = !isBusy;
|
2026-04-03 19:50:52 +02:00
|
|
|
RunSummarizeButton.IsEnabled = !isBusy;
|
2026-04-03 11:56:25 +02:00
|
|
|
SignedOutPanel.IsHitTestVisible = !isBusy;
|
|
|
|
|
PurchasePanel.IsHitTestVisible = !isBusy;
|
|
|
|
|
|
|
|
|
|
if (showLoading)
|
|
|
|
|
{
|
|
|
|
|
UpdatePanelState(showLoading: true);
|
|
|
|
|
}
|
|
|
|
|
else if (ReadyPanel.Visibility == Visibility.Visible)
|
|
|
|
|
{
|
|
|
|
|
UpdatePanelState(showReady: true);
|
|
|
|
|
}
|
|
|
|
|
else if (SignedOutPanel.Visibility == Visibility.Visible)
|
|
|
|
|
{
|
|
|
|
|
UpdatePanelState(showSignedOut: true);
|
|
|
|
|
}
|
|
|
|
|
else if (PurchasePanel.Visibility == Visibility.Visible)
|
|
|
|
|
{
|
|
|
|
|
UpdatePanelState(showPurchase: true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void SignInButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (_isBusy)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var account = await _dialogService.ShowWinoAccountLoginDialogAsync();
|
|
|
|
|
if (account != null)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info, string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email), InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await RefreshAvailabilityAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void CreateAccountButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (_isBusy)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var account = await _dialogService.ShowWinoAccountRegistrationDialogAsync();
|
|
|
|
|
if (account != null)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info, string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email), InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await RefreshAvailabilityAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void PurchaseButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (_isBusy)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SetBusyUi(isBusy: true, showLoading: false);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var purchaseResult = await _storeManagementService.PurchaseAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(true);
|
|
|
|
|
|
|
|
|
|
if (purchaseResult == StorePurchaseResult.NotPurchased)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.WinoAccount_Management_PurchaseStartFailed, InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var syncResult = await _profileService.SyncStoreEntitlementsAsync().ConfigureAwait(true);
|
|
|
|
|
if (!syncResult.IsSuccess && !string.Equals(syncResult.ErrorCode, "MissingAccessToken", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.WinoAccount_Management_StoreSyncFailed, InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (purchaseResult == StorePurchaseResult.AlreadyPurchased)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_PurchaseExistsTitle, Translator.Info_PurchaseExistsMessage, InfoBarMessageType.Warning);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_PurchaseThankYouTitle, Translator.Info_PurchaseThankYouMessage, InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.WinoAccount_Management_PurchaseStartFailed, InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
SetBusyUi(isBusy: false, showLoading: false);
|
|
|
|
|
await RefreshAvailabilityAsync();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ActionSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (ReferenceEquals(ActionSelector.SelectedItem, TranslateSegment))
|
|
|
|
|
{
|
|
|
|
|
ApplySelectedAction(AiActionType.Translate);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ReferenceEquals(ActionSelector.SelectedItem, RewriteSegment))
|
|
|
|
|
{
|
|
|
|
|
ApplySelectedAction(AiActionType.Rewrite);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ReferenceEquals(ActionSelector.SelectedItem, SummarizeSegment))
|
|
|
|
|
{
|
|
|
|
|
ApplySelectedAction(AiActionType.Summarize);
|
2026-04-03 19:50:52 +02:00
|
|
|
_ = RefreshCachedSummaryStateAsync();
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TranslateLanguageComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (TranslateLanguageComboBox.SelectedItem is AiTranslateLanguageOption option)
|
|
|
|
|
{
|
|
|
|
|
SelectedTranslateLanguageOption = option;
|
2026-04-04 01:34:57 +02:00
|
|
|
_preferencesService.AiDefaultTranslationLanguageCode = option.Code;
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RewriteModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (RewriteModeComboBox.SelectedItem is AiRewriteModeOption option)
|
|
|
|
|
{
|
|
|
|
|
SelectedRewriteModeOption = option;
|
|
|
|
|
UpdateRewriteOptionState();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 01:34:57 +02:00
|
|
|
private void SummarizeLanguageComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (SummarizeLanguageComboBox.SelectedItem is AiTranslateLanguageOption option)
|
|
|
|
|
{
|
|
|
|
|
SelectedSummarizeLanguageOption = option;
|
|
|
|
|
_preferencesService.AiSummarizeLanguageCode = option.Code;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 11:56:25 +02:00
|
|
|
private void UpdateRewriteOptionState()
|
|
|
|
|
{
|
|
|
|
|
var isCustom = SelectedRewriteModeOption?.IsCustom ?? false;
|
|
|
|
|
RewriteDescriptionTextBlock.Text = SelectedRewriteModeOption?.Description ?? string.Empty;
|
|
|
|
|
RewriteDescriptionTextBlock.Visibility = string.IsNullOrWhiteSpace(RewriteDescriptionTextBlock.Text) ? Visibility.Collapsed : Visibility.Visible;
|
|
|
|
|
CustomRewriteTextBox.Visibility = isCustom ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void RunTranslateButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteAiActionAsync(AiActionType.Translate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void RunRewriteButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteAiActionAsync(AiActionType.Rewrite);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
private async void RunSummarizeButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteAiActionAsync(AiActionType.Summarize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
CancelPendingOperation();
|
|
|
|
|
CloseRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 11:56:25 +02:00
|
|
|
private async Task ExecuteAiActionAsync(AiActionType action)
|
|
|
|
|
{
|
|
|
|
|
if (_isBusy)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Composer_AiBusyTitle, Translator.Composer_AiBusyMessage, InfoBarMessageType.Warning);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (HtmlHost == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CancelAndDisposeActionCancellationToken();
|
|
|
|
|
_actionCancellationTokenSource = new CancellationTokenSource();
|
|
|
|
|
var cancellationToken = _actionCancellationTokenSource.Token;
|
|
|
|
|
|
|
|
|
|
SetBusyUi(isBusy: true, showLoading: false);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
if (action == AiActionType.Translate && SelectedTranslateLanguageOption == null)
|
2026-04-03 11:56:25 +02:00
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error);
|
2026-04-03 11:56:25 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
if (action == AiActionType.Rewrite && string.IsNullOrWhiteSpace(ResolveRewriteMode()))
|
2026-04-03 11:56:25 +02:00
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 01:34:57 +02:00
|
|
|
if (action == AiActionType.Summarize && SelectedSummarizeLanguageOption == null)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
if (action == AiActionType.Translate)
|
2026-04-03 11:56:25 +02:00
|
|
|
{
|
2026-04-03 19:50:52 +02:00
|
|
|
var cachedTranslation = await HtmlHost.TryGetCachedTranslationHtmlAsync(SelectedTranslateLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true);
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(cachedTranslation))
|
|
|
|
|
{
|
|
|
|
|
await HtmlHost.ApplyHtmlResultAsync(cachedTranslation, cancellationToken).ConfigureAwait(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (action == AiActionType.Summarize)
|
|
|
|
|
{
|
|
|
|
|
var cachedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(cancellationToken).ConfigureAwait(true);
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(cachedSummary))
|
|
|
|
|
{
|
|
|
|
|
_hasCachedSummary = true;
|
|
|
|
|
UpdateActionAvailability();
|
|
|
|
|
await ShowSummaryDialogAsync(cachedSummary).ConfigureAwait(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var html = await HtmlHost.GetCurrentHtmlAsync(cancellationToken).ConfigureAwait(true);
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(html))
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_AiHtmlEmpty, InfoBarMessageType.Error);
|
2026-04-03 11:56:25 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var response = action switch
|
|
|
|
|
{
|
|
|
|
|
AiActionType.Translate => await _profileService.TranslateAsync(html, SelectedTranslateLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true),
|
|
|
|
|
AiActionType.Rewrite => await _profileService.RewriteAsync(html, ResolveRewriteMode(), cancellationToken).ConfigureAwait(true),
|
2026-04-04 01:34:57 +02:00
|
|
|
AiActionType.Summarize => await _profileService.SummarizeAsync(html, SelectedSummarizeLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true),
|
2026-04-03 11:56:25 +02:00
|
|
|
_ => ApiEnvelope<AiTextResultDto>.Failure(ApiErrorCodes.ValidationFailed)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
if (!response.IsSuccess || response.Result == null || string.IsNullOrWhiteSpace(response.Result.Html))
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, WinoAccountAiErrorTranslator.Format(response.ErrorCode, null), InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
if (response.Quota != null)
|
|
|
|
|
{
|
|
|
|
|
UpdateUsageSummary(
|
|
|
|
|
CreateUsageSummary(response.Quota),
|
|
|
|
|
GetUsedCount(response.Quota),
|
|
|
|
|
GetUsageLimit(response.Quota));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (action == AiActionType.Translate)
|
|
|
|
|
{
|
|
|
|
|
await HtmlHost.SaveCachedTranslationHtmlAsync(SelectedTranslateLanguageOption?.Code ?? string.Empty, response.Result.Html, cancellationToken).ConfigureAwait(true);
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
await HtmlHost.ApplyHtmlResultAsync(response.Result.Html, cancellationToken).ConfigureAwait(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (action == AiActionType.Summarize)
|
|
|
|
|
{
|
|
|
|
|
await HtmlHost.SaveCachedSummaryTextAsync(response.Result.Html, cancellationToken).ConfigureAwait(true);
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
_hasCachedSummary = true;
|
|
|
|
|
UpdateActionAvailability();
|
|
|
|
|
|
|
|
|
|
var savedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(cancellationToken).ConfigureAwait(true);
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
await ShowSummaryDialogAsync(string.IsNullOrWhiteSpace(savedSummary) ? response.Result.Html : savedSummary).ConfigureAwait(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 11:56:25 +02:00
|
|
|
await HtmlHost.ApplyHtmlResultAsync(response.Result.Html, cancellationToken).ConfigureAwait(true);
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, WinoAccountAiErrorTranslator.Format(null, ex.Message), InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
SetBusyUi(isBusy: false, showLoading: false);
|
|
|
|
|
|
|
|
|
|
if (_actionCancellationTokenSource != null)
|
|
|
|
|
{
|
|
|
|
|
_actionCancellationTokenSource.Dispose();
|
|
|
|
|
_actionCancellationTokenSource = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
// Summarize no longer auto-switches back; the user explicitly selected the tab.
|
2026-04-03 11:56:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string ResolveRewriteMode()
|
|
|
|
|
{
|
|
|
|
|
if (SelectedRewriteModeOption == null)
|
|
|
|
|
{
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!SelectedRewriteModeOption.IsCustom)
|
|
|
|
|
{
|
|
|
|
|
return SelectedRewriteModeOption.Mode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return CustomRewriteTextBox.Text?.Trim() ?? string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
private async Task RefreshCachedSummaryStateAsync()
|
|
|
|
|
{
|
|
|
|
|
if (HtmlHost == null || !HasAction(AiActionType.Summarize))
|
|
|
|
|
{
|
|
|
|
|
_hasCachedSummary = false;
|
|
|
|
|
UpdateActionAvailability();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var cachedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(CancellationToken.None).ConfigureAwait(true);
|
|
|
|
|
_hasCachedSummary = !string.IsNullOrWhiteSpace(cachedSummary);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
_hasCachedSummary = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateActionAvailability();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ShowSummaryDialogAsync(string summary)
|
|
|
|
|
{
|
|
|
|
|
if (HtmlHost == null)
|
|
|
|
|
{
|
|
|
|
|
await _dialogService.ShowMessageAsync(summary, Translator.Composer_AiSummarize, WinoCustomMessageDialogIcon.Information);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var summaryTextBox = new TextBox
|
|
|
|
|
{
|
|
|
|
|
Text = summary,
|
|
|
|
|
IsReadOnly = true,
|
|
|
|
|
AcceptsReturn = true,
|
|
|
|
|
TextWrapping = TextWrapping.Wrap,
|
|
|
|
|
MinHeight = 240,
|
|
|
|
|
MaxHeight = 420,
|
|
|
|
|
BorderThickness = new Thickness(0),
|
|
|
|
|
Background = new SolidColorBrush(Windows.UI.Color.FromArgb(0, 0, 0, 0))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var dialog = new ContentDialog
|
|
|
|
|
{
|
|
|
|
|
XamlRoot = XamlRoot,
|
|
|
|
|
RequestedTheme = ActualTheme,
|
|
|
|
|
Title = Translator.Composer_AiSummarize,
|
|
|
|
|
PrimaryButtonText = Translator.Buttons_Save,
|
|
|
|
|
SecondaryButtonText = Translator.Buttons_Close,
|
|
|
|
|
DefaultButton = ContentDialogButton.Secondary,
|
|
|
|
|
Content = new ScrollViewer
|
|
|
|
|
{
|
|
|
|
|
Content = summaryTextBox,
|
|
|
|
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
|
|
|
|
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
dialog.PrimaryButtonClick += async (sender, args) =>
|
|
|
|
|
{
|
|
|
|
|
var deferral = args.GetDeferral();
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-04-04 01:34:57 +02:00
|
|
|
var configuredSummarySavePath = _preferencesService.AiSummarySavePath;
|
|
|
|
|
var path = !string.IsNullOrWhiteSpace(configuredSummarySavePath) && Directory.Exists(configuredSummarySavePath)
|
|
|
|
|
? Path.Combine(configuredSummarySavePath, HtmlHost.GetSuggestedSummaryFileName())
|
|
|
|
|
: await _dialogService.PickFilePathAsync(HtmlHost.GetSuggestedSummaryFileName());
|
|
|
|
|
|
2026-04-03 19:50:52 +02:00
|
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
|
|
|
{
|
|
|
|
|
args.Cancel = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await File.WriteAllTextAsync(path, summary);
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info, string.Format(Translator.ClipboardTextCopied_Message, Path.GetFileName(path)), InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
args.Cancel = true;
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
deferral.Complete();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await dialog.ShowAsync();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 11:56:25 +02:00
|
|
|
private void CancelAndDisposeActionCancellationToken()
|
|
|
|
|
{
|
|
|
|
|
if (_actionCancellationTokenSource == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_actionCancellationTokenSource.Cancel();
|
|
|
|
|
_actionCancellationTokenSource.Dispose();
|
|
|
|
|
_actionCancellationTokenSource = null;
|
|
|
|
|
}
|
|
|
|
|
}
|