Translation caching. New ai actions panel.
This commit is contained in:
@@ -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="" />
|
||||
</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="" />
|
||||
|
||||
<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);
|
||||
}
|
||||
Reference in New Issue
Block a user