Translation caching. New ai actions panel.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
[Flags]
|
||||
public enum AiActionType
|
||||
{
|
||||
None = 0,
|
||||
Translate = 1,
|
||||
Rewrite = 2,
|
||||
Summarize = 4,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Models.Ai;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface IAiActionOptionsService
|
||||
{
|
||||
IReadOnlyList<AiTranslateLanguageOption> GetTranslateLanguageOptions();
|
||||
IReadOnlyList<AiRewriteModeOption> GetRewriteModeOptions();
|
||||
}
|
||||
@@ -59,6 +59,16 @@ public interface IMimeFileService
|
||||
/// </summary>
|
||||
Task<bool> DeleteMimeMessageAsync(Guid accountId, Guid fileId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns cached translated html for the given mime resource if it exists.
|
||||
/// </summary>
|
||||
Task<string> GetTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves translated html for the given mime resource.
|
||||
/// </summary>
|
||||
Task SaveTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, string html, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the final model containing rendering details.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Wino.Core.Domain.Models.Ai;
|
||||
|
||||
public sealed record AiRewriteModeOption(string Mode, string Label, string Description, bool IsCustom = false);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Wino.Core.Domain.Models.Ai;
|
||||
|
||||
public sealed record AiTranslateLanguageOption(string Code, string Label);
|
||||
@@ -1065,10 +1065,18 @@
|
||||
"Composer_AiTranslate": "Translate with AI",
|
||||
"Composer_AiActions": "AI Actions",
|
||||
"Composer_AiRewrite": "Rewrite with AI",
|
||||
"AiActions_CheckingStatus": "Checking AI access...",
|
||||
"AiActions_SignedOutTitle": "Unlock Wino AI Pack",
|
||||
"AiActions_SignedOutDescription": "Translate, rewrite, and summarize emails with AI after signing in to your Wino Account and activating the AI Pack add-on.",
|
||||
"AiActions_NoPackTitle": "AI Pack required",
|
||||
"AiActions_NoPackDescription": "You're signed in, but AI Pack is not active yet. Purchase it to use Wino's AI translation, rewrite, and summarize tools.",
|
||||
"AiActions_UsageSummary": "{0} of {1} requests used this month.",
|
||||
"Composer_AiRewritePolite": "Make it polite",
|
||||
"Composer_AiRewritePoliteDescription": "Softens the wording while keeping the same intent.",
|
||||
"Composer_AiRewriteAngry": "Make it direct",
|
||||
"Composer_AiRewriteAngryDescription": "Uses a firmer, more forceful tone.",
|
||||
"Composer_AiRewriteAngry": "Make it angry",
|
||||
"Composer_AiRewriteAngryDescription": "Uses a sharper and more confrontational tone.",
|
||||
"Composer_AiRewriteHappy": "Make it happy",
|
||||
"Composer_AiRewriteHappyDescription": "Adds a more upbeat and enthusiastic tone.",
|
||||
"Composer_AiRewriteFormal": "Make it formal",
|
||||
"Composer_AiRewriteFormalDescription": "Makes the message sound more professional and structured.",
|
||||
"Composer_AiRewriteFriendly": "Make it friendly",
|
||||
@@ -1077,6 +1085,9 @@
|
||||
"Composer_AiRewriteShorterDescription": "Tightens the text and removes unnecessary detail.",
|
||||
"Composer_AiRewriteClearer": "Make it clearer",
|
||||
"Composer_AiRewriteClearerDescription": "Improves readability and makes the message easier to follow.",
|
||||
"Composer_AiRewriteCustom": "Custom",
|
||||
"Composer_AiRewriteCustomDescription": "Describe your own rewrite intention.",
|
||||
"Composer_AiRewriteCustomPlaceholder": "Describe how you want the message rewritten",
|
||||
"Composer_AiRewriteMode": "Rewrite tone",
|
||||
"Composer_AiRewriteApply": "Apply rewrite",
|
||||
"Composer_AiTranslateDialogTitle": "Translate with AI",
|
||||
@@ -1326,6 +1337,7 @@
|
||||
"WinoAccount_Error_ExternalLoginInvalid": "The external sign-in request is invalid.",
|
||||
"WinoAccount_Error_ExternalAuthStateInvalid": "The external sign-in state is invalid or expired.",
|
||||
"WinoAccount_Error_ExternalAuthCodeInvalid": "The external sign-in code is invalid or expired.",
|
||||
"WinoAccount_Error_AiPackRequired": "An active Wino AI Pack subscription is required for this action.",
|
||||
"WinoAccount_Error_AiQuotaExceeded": "Your AI Pack usage limit has been reached for the current billing period.",
|
||||
"WinoAccount_Error_AiHtmlEmpty": "There is no email content to process.",
|
||||
"WinoAccount_Error_AiHtmlTooLarge": "This email is too large to process with Wino AI.",
|
||||
|
||||
@@ -141,6 +141,8 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
public IStatePersistanceService StatePersistenceService { get; }
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
public IPrintService PrintService { get; }
|
||||
public Guid? CurrentMailAccountId => initializedMailItemViewModel?.MailCopy.AssignedAccount?.Id;
|
||||
public Guid? CurrentMailFileId => initializedMailItemViewModel?.MailCopy.FileId;
|
||||
|
||||
public MailRenderingPageViewModel(IMailDialogService dialogService,
|
||||
INativeAppService nativeAppService,
|
||||
|
||||
@@ -298,6 +298,7 @@ public partial class App : WinoApplication,
|
||||
{
|
||||
services.AddSingleton<INavigationService, NavigationService>();
|
||||
services.AddSingleton<IMailDialogService, DialogService>();
|
||||
services.AddSingleton<IAiActionOptionsService, AiActionOptionsService>();
|
||||
services.AddTransient<IProviderService, ProviderService>();
|
||||
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
|
||||
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows.Input;
|
||||
@@ -9,9 +10,9 @@ using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.Xaml.Interactivity;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
using Wino.Mail.WinUI.Controls;
|
||||
using Wino.Helpers;
|
||||
using Wino.Mail.WinUI;
|
||||
using Wino.Mail.WinUI.Controls;
|
||||
|
||||
namespace Wino.Behaviors;
|
||||
|
||||
@@ -40,12 +41,23 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
||||
if (PrimaryCommands is IEnumerable enumerable)
|
||||
{
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
DetachCommandElement(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DetachCommandElement(object item)
|
||||
{
|
||||
if (item is ButtonBase button)
|
||||
{
|
||||
button.Click -= Button_Click;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (item is AppBarElementContainer container && container.Content is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,10 +73,7 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
||||
{
|
||||
foreach (var item in enumerableObjects)
|
||||
{
|
||||
if (item is ButtonBase button)
|
||||
{
|
||||
button.Click -= Button_Click;
|
||||
}
|
||||
DetachCommandElement(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +81,7 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
||||
{
|
||||
foreach (var item in secondaryObject)
|
||||
{
|
||||
if (item is ButtonBase button)
|
||||
{
|
||||
button.Click -= Button_Click;
|
||||
}
|
||||
DetachCommandElement(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +141,6 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
||||
AssociatedObject.PrimaryCommands.Add(menuItem);
|
||||
}
|
||||
}
|
||||
|
||||
//if (dependencyObject is ICommandBarElement icommandBarElement)
|
||||
//{
|
||||
// if (dependencyObject is ButtonBase button)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Ai;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public sealed class AiActionOptionsService : IAiActionOptionsService
|
||||
{
|
||||
public IReadOnlyList<AiTranslateLanguageOption> GetTranslateLanguageOptions()
|
||||
{
|
||||
return
|
||||
[
|
||||
new("en-US", Translator.Composer_AiTranslateLanguageEnglish),
|
||||
new("tr-TR", Translator.Composer_AiTranslateLanguageTurkish),
|
||||
new("de-DE", Translator.Composer_AiTranslateLanguageGerman),
|
||||
new("fr-FR", Translator.Composer_AiTranslateLanguageFrench),
|
||||
new("es-ES", Translator.Composer_AiTranslateLanguageSpanish),
|
||||
new("it-IT", Translator.Composer_AiTranslateLanguageItalian),
|
||||
new("pt-BR", Translator.Composer_AiTranslateLanguagePortugueseBrazil),
|
||||
new("nl-NL", Translator.Composer_AiTranslateLanguageDutch),
|
||||
new("pl-PL", Translator.Composer_AiTranslateLanguagePolish),
|
||||
new("ru-RU", Translator.Composer_AiTranslateLanguageRussian),
|
||||
new("ja-JP", Translator.Composer_AiTranslateLanguageJapanese),
|
||||
new("ko-KR", Translator.Composer_AiTranslateLanguageKorean),
|
||||
new("zh-CN", Translator.Composer_AiTranslateLanguageChineseSimplified),
|
||||
new("ar-SA", Translator.Composer_AiTranslateLanguageArabic),
|
||||
new("hi-IN", Translator.Composer_AiTranslateLanguageHindi),
|
||||
];
|
||||
}
|
||||
|
||||
public IReadOnlyList<AiRewriteModeOption> GetRewriteModeOptions()
|
||||
{
|
||||
return
|
||||
[
|
||||
new("polite", Translator.Composer_AiRewritePolite, Translator.Composer_AiRewritePoliteDescription),
|
||||
new("angry", Translator.Composer_AiRewriteAngry, Translator.Composer_AiRewriteAngryDescription),
|
||||
new("happy", Translator.Composer_AiRewriteHappy, Translator.Composer_AiRewriteHappyDescription),
|
||||
new("formal", Translator.Composer_AiRewriteFormal, Translator.Composer_AiRewriteFormalDescription),
|
||||
new("friendly", Translator.Composer_AiRewriteFriendly, Translator.Composer_AiRewriteFriendlyDescription),
|
||||
new("shorter", Translator.Composer_AiRewriteShorter, Translator.Composer_AiRewriteShorterDescription),
|
||||
new("clearer", Translator.Composer_AiRewriteClearer, Translator.Composer_AiRewriteClearerDescription),
|
||||
new(string.Empty, Translator.Composer_AiRewriteCustom, Translator.Composer_AiRewriteCustomDescription, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Mail.Api.Contracts.Common;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public static class WinoAccountAiErrorTranslator
|
||||
{
|
||||
public static string Translate(string? errorCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(errorCode))
|
||||
{
|
||||
return Translator.GeneralTitle_Error;
|
||||
}
|
||||
|
||||
return errorCode switch
|
||||
{
|
||||
ApiErrorCodes.AiPackRequired => Translator.WinoAccount_Error_AiPackRequired,
|
||||
ApiErrorCodes.AiQuotaExceeded => Translator.WinoAccount_Error_AiQuotaExceeded,
|
||||
ApiErrorCodes.AiHtmlEmpty => Translator.WinoAccount_Error_AiHtmlEmpty,
|
||||
ApiErrorCodes.AiHtmlTooLarge => Translator.WinoAccount_Error_AiHtmlTooLarge,
|
||||
ApiErrorCodes.AiUnsupportedLanguage => Translator.WinoAccount_Error_AiUnsupportedLanguage,
|
||||
ApiErrorCodes.Forbidden => Translator.WinoAccount_Error_Forbidden,
|
||||
ApiErrorCodes.ValidationFailed => Translator.WinoAccount_Error_ValidationFailed,
|
||||
_ => errorCode
|
||||
};
|
||||
}
|
||||
|
||||
public static string Format(string? errorCode, string? errorMessage)
|
||||
{
|
||||
var translatedCode = Translate(errorCode);
|
||||
var hasCode = !string.IsNullOrWhiteSpace(errorCode);
|
||||
var hasMessage = !string.IsNullOrWhiteSpace(errorMessage);
|
||||
|
||||
if (!hasCode && !hasMessage)
|
||||
{
|
||||
return Translator.GeneralTitle_Error;
|
||||
}
|
||||
|
||||
var formattedCode = translatedCode;
|
||||
if (hasCode && !string.Equals(translatedCode, errorCode, System.StringComparison.Ordinal))
|
||||
{
|
||||
formattedCode = $"{translatedCode} ({errorCode})";
|
||||
}
|
||||
|
||||
if (!hasMessage || string.Equals(errorMessage, translatedCode, System.StringComparison.OrdinalIgnoreCase) || string.Equals(errorMessage, errorCode, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return formattedCode;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(formattedCode))
|
||||
{
|
||||
return errorMessage!;
|
||||
}
|
||||
|
||||
return $"{formattedCode}{System.Environment.NewLine}{errorMessage}";
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,17 @@
|
||||
Visibility="{x:Bind ViewModel.IsDraftBusy, Mode=OneWay}">
|
||||
<ProgressRing IsActive="True" />
|
||||
</AppBarButton>
|
||||
<AppBarToggleButton
|
||||
x:Name="ComposeAiActionsToggleButton"
|
||||
Checked="ComposeAiActionsToggleButton_Checked"
|
||||
MinWidth="40"
|
||||
HorizontalContentAlignment="Center"
|
||||
LabelPosition="Collapsed"
|
||||
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_AiActions}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
<AppBarButton Click="ToggleEditorThemeClicked" ToolTipService.ToolTip="{x:Bind GetEditorThemeToolTip(WebViewEditor.IsEditorDarkMode), Mode=OneWay}">
|
||||
<AppBarButton.Icon>
|
||||
<coreControls:WinoFontIcon Icon="{x:Bind GetEditorThemeIcon(WebViewEditor.IsEditorDarkMode), Mode=OneWay}" />
|
||||
@@ -307,6 +318,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -445,10 +457,19 @@
|
||||
BorderThickness="0"
|
||||
Text="{x:Bind ViewModel.Subject, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<coreControls:AiActionsPanel
|
||||
x:Name="ComposeAiActionsPanel"
|
||||
Grid.Row="5"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,8,0,0"
|
||||
AvailableActions="Rewrite"
|
||||
HtmlHost="{x:Bind}"
|
||||
Visibility="{x:Bind GetAiActionsPanelVisibility(ComposeAiActionsToggleButton.IsChecked), Mode=OneWay}" />
|
||||
|
||||
<!-- Attachments -->
|
||||
<ListView
|
||||
x:Name="AttachmentsListView"
|
||||
Grid.Row="5"
|
||||
Grid.Row="6"
|
||||
Grid.ColumnSpan="2"
|
||||
ui:ListViewExtensions.Command="{x:Bind ViewModel.OpenAttachmentCommand}"
|
||||
x:Load="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.IncludedAttachments.Count), Mode=OneWay}"
|
||||
|
||||
@@ -32,12 +32,15 @@ using Wino.Views.Abstract;
|
||||
namespace Wino.Views.Mail;
|
||||
|
||||
public sealed partial class ComposePage : ComposePageAbstract,
|
||||
IAiHtmlActionHost,
|
||||
IRecipient<CreateNewComposeMailRequested>,
|
||||
IRecipient<ApplicationThemeChanged>,
|
||||
IRecipient<ReaderItemRefreshRequestedEvent>
|
||||
{
|
||||
public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView();
|
||||
|
||||
public Visibility GetAiActionsPanelVisibility(bool? isChecked) => isChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
private readonly List<IDisposable> _disposables = [];
|
||||
|
||||
public ComposePage()
|
||||
@@ -283,6 +286,11 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
||||
ViewModel.IsCCBCCVisible = true;
|
||||
}
|
||||
|
||||
private async void ComposeAiActionsToggleButton_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await ComposeAiActionsPanel.RefreshAvailabilityAsync();
|
||||
}
|
||||
|
||||
private async void TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
|
||||
{
|
||||
// Check is valid email.
|
||||
@@ -410,11 +418,39 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
||||
base.OnNavigatingFrom(e);
|
||||
|
||||
FocusManager.GotFocus -= GlobalFocusManagerGotFocus;
|
||||
ComposeAiActionsPanel.CancelPendingOperation();
|
||||
await ViewModel.UpdateMimeChangesAsync();
|
||||
|
||||
DisposeDisposables();
|
||||
}
|
||||
|
||||
public async Task<string?> GetCurrentHtmlAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var html = await WebViewEditor.GetHtmlBodyAsync();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return html;
|
||||
}
|
||||
|
||||
public async Task ApplyHtmlResultAsync(string html, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await WebViewEditor.RenderHtmlAsync(html);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
public Task<string?> TryGetCachedTranslationHtmlAsync(string languageCode, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult<string?>(null);
|
||||
}
|
||||
|
||||
public Task SaveCachedTranslationHtmlAsync(string languageCode, string html, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OpenAttachment_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuFlyoutItem item && item.CommandParameter is MailAttachmentViewModel attachment)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
xmlns:local="using:Wino.Behaviors"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
|
||||
x:Name="root"
|
||||
@@ -181,6 +182,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0" Margin="5,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -273,13 +275,16 @@
|
||||
IsDynamicOverflowEnabled="True"
|
||||
OverflowButtonVisibility="Auto">
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<local:BindableCommandBarBehavior ItemClickedCommand="{x:Bind ViewModel.OperationClickedCommand}" PrimaryCommands="{x:Bind ViewModel.MenuItems, Mode=OneWay}" />
|
||||
<local:BindableCommandBarBehavior
|
||||
ItemClickedCommand="{x:Bind ViewModel.OperationClickedCommand}"
|
||||
PrimaryCommands="{x:Bind ViewModel.MenuItems, Mode=OneWay}" />
|
||||
</interactivity:Interaction.Behaviors>
|
||||
<CommandBar.Content>
|
||||
<Grid Padding="0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -317,6 +322,19 @@
|
||||
<TextBlock FontSize="12" Text="{x:Bind helpers:XamlHelpers.GetCreationDateString(ViewModel.CreationDate, ViewModel.PreferencesService.Prefer24HourTimeFormat), Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<AppBarToggleButton
|
||||
x:Name="ReaderAiActionsToggleButton"
|
||||
Grid.Column="2"
|
||||
Checked="ReaderAiActionsToggleButton_Checked"
|
||||
MinWidth="40"
|
||||
HorizontalContentAlignment="Center"
|
||||
LabelPosition="Collapsed"
|
||||
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_AiActions}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
</Grid>
|
||||
</CommandBar.Content>
|
||||
</CommandBar>
|
||||
@@ -409,8 +427,16 @@
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
<coreControls:AiActionsPanel
|
||||
x:Name="ReaderAiActionsPanel"
|
||||
Grid.Row="3"
|
||||
Margin="0,8,0,0"
|
||||
AvailableActions="Translate, Summarize"
|
||||
HtmlHost="{x:Bind}"
|
||||
Visibility="{x:Bind GetAiActionsPanelVisibility(ReaderAiActionsToggleButton.IsChecked), Mode=OneWay}" />
|
||||
|
||||
<!-- Attachments -->
|
||||
<Grid Grid.Row="3">
|
||||
<Grid Grid.Row="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -442,7 +468,7 @@
|
||||
|
||||
<InfoBar
|
||||
x:Name="ImageLoadingDisabledMessage"
|
||||
Grid.Row="4"
|
||||
Grid.Row="5"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
x:Load="{x:Bind ViewModel.IsImageRenderingDisabled, Mode=OneWay}"
|
||||
IsOpen="True"
|
||||
@@ -458,7 +484,7 @@
|
||||
|
||||
<ProgressBar
|
||||
x:Name="DownloadingProgressBar"
|
||||
Grid.Row="3"
|
||||
Grid.Row="4"
|
||||
Margin="12,1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -16,6 +17,7 @@ using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Printing;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.WinUI;
|
||||
using Wino.Mail.WinUI.Controls;
|
||||
using Wino.Mail.WinUI.Extensions;
|
||||
using Wino.Messaging.Client.Mails;
|
||||
using Wino.Messaging.Client.Shell;
|
||||
@@ -24,19 +26,24 @@ using Wino.Views.Abstract;
|
||||
namespace Wino.Views.Mail;
|
||||
|
||||
public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
||||
IAiHtmlActionHost,
|
||||
IRecipient<HtmlRenderingRequested>,
|
||||
IRecipient<CancelRenderingContentRequested>,
|
||||
IRecipient<ApplicationThemeChanged>
|
||||
{
|
||||
private readonly IPreferencesService _preferencesService = App.Current.Services.GetService<IPreferencesService>()!;
|
||||
private readonly IMailDialogService _dialogService = App.Current.Services.GetService<IMailDialogService>()!;
|
||||
private readonly IMimeFileService _mimeFileService = App.Current.Services.GetRequiredService<IMimeFileService>();
|
||||
|
||||
private bool isRenderingInProgress = false;
|
||||
private bool? _lastAppliedDarkTheme;
|
||||
private TaskCompletionSource<bool> DOMLoadedTask = new TaskCompletionSource<bool>();
|
||||
private string _currentRenderedHtml = string.Empty;
|
||||
|
||||
public WebView2 GetWebView() => Chromium;
|
||||
|
||||
public Visibility GetAiActionsPanelVisibility(bool? isChecked) => isChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public MailRenderingPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -82,6 +89,7 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
||||
private async Task RenderInternalAsync(string htmlBody)
|
||||
{
|
||||
isRenderingInProgress = true;
|
||||
_currentRenderedHtml = htmlBody ?? string.Empty;
|
||||
|
||||
await DOMLoadedTask.Task;
|
||||
|
||||
@@ -141,10 +149,63 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
||||
|
||||
ViewModel.SaveHTMLasPDFFunc = null;
|
||||
ViewModel.DirectPrintFuncAsync = null;
|
||||
_currentRenderedHtml = string.Empty;
|
||||
ReaderAiActionsPanel.CancelPendingOperation();
|
||||
|
||||
DisposeWebView2();
|
||||
}
|
||||
|
||||
public Task<string?> GetCurrentHtmlAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult<string?>(_currentRenderedHtml);
|
||||
}
|
||||
|
||||
public async Task ApplyHtmlResultAsync(string html, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await RenderInternalAsync(html);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
private async void ReaderAiActionsToggleButton_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await ReaderAiActionsPanel.RefreshAvailabilityAsync();
|
||||
}
|
||||
|
||||
public async Task<string?> TryGetCachedTranslationHtmlAsync(string languageCode, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!ViewModel.CurrentMailAccountId.HasValue || !ViewModel.CurrentMailFileId.HasValue || string.IsNullOrWhiteSpace(languageCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await _mimeFileService.GetTranslatedHtmlAsync(
|
||||
ViewModel.CurrentMailAccountId.Value,
|
||||
ViewModel.CurrentMailFileId.Value,
|
||||
languageCode,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SaveCachedTranslationHtmlAsync(string languageCode, string html, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!ViewModel.CurrentMailAccountId.HasValue || !ViewModel.CurrentMailFileId.HasValue || string.IsNullOrWhiteSpace(languageCode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _mimeFileService.SaveTranslatedHtmlAsync(
|
||||
ViewModel.CurrentMailAccountId.Value,
|
||||
ViewModel.CurrentMailFileId.Value,
|
||||
languageCode,
|
||||
html,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void DisposeWebView2()
|
||||
{
|
||||
if (Chromium == null) return;
|
||||
|
||||
@@ -120,6 +120,48 @@ public class MimeFileService : IMimeFileService
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string> GetTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(targetLanguage))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var translatedHtmlPath = await GetTranslatedHtmlPathAsync(accountId, fileId, targetLanguage).ConfigureAwait(false);
|
||||
if (!File.Exists(translatedHtmlPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await File.ReadAllTextAsync(translatedHtmlPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Could not read translated html cache for FileId: {FileId}, Language: {Language}", fileId, targetLanguage);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, string html, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(targetLanguage) || string.IsNullOrWhiteSpace(html))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var translatedHtmlPath = await GetTranslatedHtmlPathAsync(accountId, fileId, targetLanguage).ConfigureAwait(false);
|
||||
await File.WriteAllTextAsync(translatedHtmlPath, html, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Could not save translated html cache for FileId: {FileId}, Language: {Language}", fileId, targetLanguage);
|
||||
}
|
||||
}
|
||||
|
||||
public MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null)
|
||||
{
|
||||
var visitor = CreateHTMLPreviewVisitor(message, mimeLocalPath);
|
||||
@@ -209,4 +251,21 @@ public class MimeFileService : IMimeFileService
|
||||
Log.Error(ex, "Failed to remove user's mime cache folder.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetTranslatedHtmlPathAsync(Guid accountId, Guid fileId, string targetLanguage)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
|
||||
return Path.Combine(resourcePath, $"translated-{SanitizeFileNamePart(targetLanguage)}.html");
|
||||
}
|
||||
|
||||
private static string SanitizeFileNamePart(string value)
|
||||
{
|
||||
var invalidCharacters = Path.GetInvalidFileNameChars();
|
||||
var sanitizedChars = value
|
||||
.Trim()
|
||||
.Select(ch => invalidCharacters.Contains(ch) ? '_' : char.ToLowerInvariant(ch))
|
||||
.ToArray();
|
||||
|
||||
return sanitizedChars.Length == 0 ? "default" : new string(sanitizedChars);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
||||
private readonly SemaphoreSlim _tokenRefreshLock = new(1, 1);
|
||||
private readonly bool _ownsHttpClient;
|
||||
|
||||
// private const string ApiUrl = "https://localhost:7204/";
|
||||
private const string ApiUrl = "https://api.winomail.app/";
|
||||
private const string ApiUrl = "https://localhost:7204/";
|
||||
// private const string ApiUrl = "https://api.winomail.app/";
|
||||
|
||||
public WinoAccountApiClient(IDatabaseService databaseService, HttpClient? httpClient = null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user