Translation caching. New ai actions panel.

This commit is contained in:
Burak Kaan Köse
2026-04-03 11:56:25 +02:00
parent 8f16f553f5
commit 27e91316d3
20 changed files with 1150 additions and 23 deletions
@@ -0,0 +1,232 @@
<UserControl
x:Class="Wino.Mail.WinUI.Controls.AiActionsPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:models="using:Wino.Core.Domain.Models.Ai"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="root"
Loaded="OnLoaded"
Unloaded="OnUnloaded"
mc:Ignorable="d">
<Border
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="10">
<Grid>
<StackPanel Spacing="14">
<ProgressBar x:Name="BusyProgressBar" IsIndeterminate="True" Visibility="Collapsed" />
<StackPanel x:Name="LoadingPanel" Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="10">
<ProgressRing Width="20" Height="20" IsActive="True" />
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.AiActions_CheckingStatus, Mode=OneWay}" />
</StackPanel>
</StackPanel>
<StackPanel x:Name="SignedOutPanel" Spacing="14" Visibility="Collapsed">
<Border Height="120" Padding="18" CornerRadius="12">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="#1A6EE7B7" />
<GradientStop Offset="0.55" Color="#2038BDF8" />
<GradientStop Offset="1" Color="#1A818CF8" />
</LinearGradientBrush>
</Border.Background>
<Grid ColumnSpacing="14">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Width="56" Height="56" VerticalAlignment="Top" Background="#22FFFFFF" CornerRadius="28">
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="26"
Foreground="White"
Glyph="&#xE945;" />
</Border>
<StackPanel Grid.Column="1" Spacing="6">
<TextBlock
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind domain:Translator.AiActions_SignedOutTitle, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.AiActions_SignedOutDescription, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Grid>
</Border>
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button
Click="SignInButton_Click"
Content="{x:Bind domain:Translator.Buttons_SignIn, Mode=OneWay}"
Style="{StaticResource AccentButtonStyle}" />
<Button
Grid.Column="1"
Click="CreateAccountButton_Click"
Content="{x:Bind domain:Translator.Buttons_CreateAccount, Mode=OneWay}" />
</Grid>
</StackPanel>
<StackPanel x:Name="PurchasePanel" Spacing="14" Visibility="Collapsed">
<Border Padding="16" Background="{ThemeResource CardBackgroundFillColorTertiaryBrush}" CornerRadius="12">
<StackPanel Spacing="8">
<TextBlock
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.AiActions_NoPackTitle, Mode=OneWay}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind domain:Translator.AiActions_NoPackDescription, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Padding="8,2" Background="{ThemeResource AccentFillColorDefaultBrush}" CornerRadius="8">
<TextBlock Foreground="White" Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoPrice, Mode=OneWay}" />
</Border>
<Border Padding="8,2" Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" CornerRadius="8">
<TextBlock Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoRequests, Mode=OneWay}" />
</Border>
</StackPanel>
</StackPanel>
</Border>
<Button
Click="PurchaseButton_Click"
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackGetButton, Mode=OneWay}"
HorizontalAlignment="Left"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
<StackPanel x:Name="ReadyPanel" Spacing="12" Visibility="Collapsed">
<Border Padding="12" Background="{ThemeResource CardBackgroundFillColorTertiaryBrush}" CornerRadius="10">
<Grid ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Width="36"
Height="36"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="18"
Opacity="0.18" />
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{StaticResource SymbolThemeFontFamily}"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE945;" />
<StackPanel Grid.Column="1" Spacing="3">
<TextBlock
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.Composer_AiActions, Mode=OneWay}" />
<TextBlock
x:Name="UsageSummaryTextBlock"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
TextWrapping="WrapWholeWords" />
<TextBlock
x:Name="UsageResetTextBlock"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Grid>
</Border>
<controls:Segmented x:Name="ActionSelector" SelectionChanged="ActionSelector_SelectionChanged">
<controls:SegmentedItem x:Name="TranslateSegment" Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureTranslate, Mode=OneWay}">
<controls:SegmentedItem.Icon>
<SymbolIcon Symbol="Switch" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
<controls:SegmentedItem x:Name="RewriteSegment" Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureRewrite, Mode=OneWay}">
<controls:SegmentedItem.Icon>
<SymbolIcon Symbol="Edit" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
<controls:SegmentedItem x:Name="SummarizeSegment" Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureSummarize, Mode=OneWay}">
<controls:SegmentedItem.Icon>
<SymbolIcon Symbol="Bullets" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
</controls:Segmented>
<StackPanel x:Name="TranslateOptionsPanel" Spacing="8" Visibility="Collapsed">
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.Composer_AiTranslateLanguage, Mode=OneWay}" />
<ComboBox
x:Name="TranslateLanguageComboBox"
SelectedItem="{x:Bind SelectedTranslateLanguageOption, Mode=TwoWay}"
SelectionChanged="TranslateLanguageComboBox_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:AiTranslateLanguageOption">
<TextBlock Text="{x:Bind Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button
x:Name="RunTranslateButton"
Click="RunTranslateButton_Click"
Content="{x:Bind domain:Translator.Composer_AiTranslateApply, Mode=OneWay}"
HorizontalAlignment="Left"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
<StackPanel x:Name="RewriteOptionsPanel" Spacing="8" Visibility="Collapsed">
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.Composer_AiRewriteMode, Mode=OneWay}" />
<ComboBox
x:Name="RewriteModeComboBox"
SelectedItem="{x:Bind SelectedRewriteModeOption, Mode=TwoWay}"
SelectionChanged="RewriteModeComboBox_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:AiRewriteModeOption">
<TextBlock Text="{x:Bind Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
x:Name="RewriteDescriptionTextBlock"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
TextWrapping="WrapWholeWords" />
<TextBox
x:Name="CustomRewriteTextBox"
PlaceholderText="{x:Bind domain:Translator.Composer_AiRewriteCustomPlaceholder, Mode=OneWay}"
Visibility="Collapsed" />
<Button
x:Name="RunRewriteButton"
Click="RunRewriteButton_Click"
Content="{x:Bind domain:Translator.Composer_AiRewriteApply, Mode=OneWay}"
HorizontalAlignment="Left"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,519 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
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
{
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>();
private bool _disposedValue;
private bool _isRefreshing;
private bool _isBusy;
private AiActionType _lastConfigurableAction = AiActionType.Translate;
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; }
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());
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
LoadOptions();
UpdateActionAvailability();
ApplySelectedAction(SelectDefaultAction());
_ = RefreshAvailabilityAsync();
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
CancelPendingOperation();
}
private void LoadOptions()
{
_translateOptions = _optionsService.GetTranslateLanguageOptions();
_rewriteOptions = _optionsService.GetRewriteModeOptions();
TranslateLanguageComboBox.ItemsSource = _translateOptions;
RewriteModeComboBox.ItemsSource = _rewriteOptions;
SelectedTranslateLanguageOption ??= _translateOptions.Count > 0 ? _translateOptions[0] : null;
SelectedRewriteModeOption ??= _rewriteOptions.Count > 0 ? _rewriteOptions[0] : null;
TranslateLanguageComboBox.SelectedItem = SelectedTranslateLanguageOption;
RewriteModeComboBox.SelectedItem = SelectedRewriteModeOption;
UpdateRewriteOptionState();
}
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;
}
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;
}
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)
{
UpdateUsageSummary(string.Empty, string.Empty);
UpdatePanelState(showSignedOut: true);
return;
}
var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(true);
if (!hasAiPack)
{
UpdateUsageSummary(string.Empty, string.Empty);
UpdatePanelState(showPurchase: true);
return;
}
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(true);
if (aiStatusResponse.IsSuccess && aiStatusResponse.Result != null)
{
UpdateUsageSummary(CreateUsageSummary(aiStatusResponse.Result), CreateUsageResetText(aiStatusResponse.Result));
}
else
{
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed, string.Empty);
}
ApplySelectedAction(SelectDefaultAction());
UpdatePanelState(showReady: true);
}
catch (OperationCanceledException)
{
}
catch (Exception)
{
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed, string.Empty);
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;
}
private static string CreateUsageResetText(AiStatusResultDto aiStatus)
{
return aiStatus.CurrentPeriodEndUtc is DateTimeOffset resetDateUtc
? string.Format(Translator.WinoAccount_Management_AiPackResets, resetDateUtc.LocalDateTime)
: string.Empty;
}
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;
}
private void UpdateUsageSummary(string usageText, string resetText)
{
UsageSummaryTextBlock.Text = usageText;
UsageResetTextBlock.Text = resetText;
UsageResetTextBlock.Visibility = string.IsNullOrWhiteSpace(resetText) ? Visibility.Collapsed : Visibility.Visible;
}
private void SetBusyUi(bool isBusy, bool showLoading)
{
_isBusy = isBusy;
BusyProgressBar.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
ActionSelector.IsEnabled = !isBusy;
TranslateLanguageComboBox.IsEnabled = !isBusy;
RewriteModeComboBox.IsEnabled = !isBusy;
CustomRewriteTextBox.IsEnabled = !isBusy;
RunTranslateButton.IsEnabled = !isBusy;
RunRewriteButton.IsEnabled = !isBusy;
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);
_ = ExecuteAiActionAsync(AiActionType.Summarize);
}
}
private void TranslateLanguageComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (TranslateLanguageComboBox.SelectedItem is AiTranslateLanguageOption option)
{
SelectedTranslateLanguageOption = option;
}
}
private void RewriteModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (RewriteModeComboBox.SelectedItem is AiRewriteModeOption option)
{
SelectedRewriteModeOption = option;
UpdateRewriteOptionState();
}
}
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);
}
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
{
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);
return;
}
if (action == AiActionType.Translate && SelectedTranslateLanguageOption == null)
{
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error);
return;
}
if (action == AiActionType.Rewrite && string.IsNullOrWhiteSpace(ResolveRewriteMode()))
{
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error);
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),
AiActionType.Summarize => await _profileService.SummarizeAsync(html, cancellationToken).ConfigureAwait(true),
_ => 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;
}
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;
}
if (action == AiActionType.Summarize)
{
var fallbackAction = _lastConfigurableAction != AiActionType.None && HasAction(_lastConfigurableAction)
? _lastConfigurableAction
: SelectDefaultAction();
ApplySelectedAction(fallbackAction);
}
}
}
private string ResolveRewriteMode()
{
if (SelectedRewriteModeOption == null)
{
return string.Empty;
}
if (!SelectedRewriteModeOption.IsCustom)
{
return SelectedRewriteModeOption.Mode;
}
return CustomRewriteTextBox.Text?.Trim() ?? string.Empty;
}
private void CancelAndDisposeActionCancellationToken()
{
if (_actionCancellationTokenSource == null)
{
return;
}
_actionCancellationTokenSource.Cancel();
_actionCancellationTokenSource.Dispose();
_actionCancellationTokenSource = null;
}
}
@@ -0,0 +1,12 @@
using System.Threading;
using System.Threading.Tasks;
namespace Wino.Mail.WinUI.Controls;
public interface IAiHtmlActionHost
{
Task<string?> GetCurrentHtmlAsync(CancellationToken cancellationToken);
Task ApplyHtmlResultAsync(string html, CancellationToken cancellationToken);
Task<string?> TryGetCachedTranslationHtmlAsync(string languageCode, CancellationToken cancellationToken);
Task SaveCachedTranslationHtmlAsync(string languageCode, string html, CancellationToken cancellationToken);
}