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>
|
/// </summary>
|
||||||
Task<bool> DeleteMimeMessageAsync(Guid accountId, Guid fileId);
|
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>
|
/// <summary>
|
||||||
/// Prepares the final model containing rendering details.
|
/// Prepares the final model containing rendering details.
|
||||||
/// </summary>
|
/// </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_AiTranslate": "Translate with AI",
|
||||||
"Composer_AiActions": "AI Actions",
|
"Composer_AiActions": "AI Actions",
|
||||||
"Composer_AiRewrite": "Rewrite with AI",
|
"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_AiRewritePolite": "Make it polite",
|
||||||
"Composer_AiRewritePoliteDescription": "Softens the wording while keeping the same intent.",
|
"Composer_AiRewritePoliteDescription": "Softens the wording while keeping the same intent.",
|
||||||
"Composer_AiRewriteAngry": "Make it direct",
|
"Composer_AiRewriteAngry": "Make it angry",
|
||||||
"Composer_AiRewriteAngryDescription": "Uses a firmer, more forceful tone.",
|
"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_AiRewriteFormal": "Make it formal",
|
||||||
"Composer_AiRewriteFormalDescription": "Makes the message sound more professional and structured.",
|
"Composer_AiRewriteFormalDescription": "Makes the message sound more professional and structured.",
|
||||||
"Composer_AiRewriteFriendly": "Make it friendly",
|
"Composer_AiRewriteFriendly": "Make it friendly",
|
||||||
@@ -1077,6 +1085,9 @@
|
|||||||
"Composer_AiRewriteShorterDescription": "Tightens the text and removes unnecessary detail.",
|
"Composer_AiRewriteShorterDescription": "Tightens the text and removes unnecessary detail.",
|
||||||
"Composer_AiRewriteClearer": "Make it clearer",
|
"Composer_AiRewriteClearer": "Make it clearer",
|
||||||
"Composer_AiRewriteClearerDescription": "Improves readability and makes the message easier to follow.",
|
"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_AiRewriteMode": "Rewrite tone",
|
||||||
"Composer_AiRewriteApply": "Apply rewrite",
|
"Composer_AiRewriteApply": "Apply rewrite",
|
||||||
"Composer_AiTranslateDialogTitle": "Translate with AI",
|
"Composer_AiTranslateDialogTitle": "Translate with AI",
|
||||||
@@ -1326,6 +1337,7 @@
|
|||||||
"WinoAccount_Error_ExternalLoginInvalid": "The external sign-in request is invalid.",
|
"WinoAccount_Error_ExternalLoginInvalid": "The external sign-in request is invalid.",
|
||||||
"WinoAccount_Error_ExternalAuthStateInvalid": "The external sign-in state is invalid or expired.",
|
"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_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_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_AiHtmlEmpty": "There is no email content to process.",
|
||||||
"WinoAccount_Error_AiHtmlTooLarge": "This email is too large to process with Wino AI.",
|
"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 IStatePersistanceService StatePersistenceService { get; }
|
||||||
public IPreferencesService PreferencesService { get; }
|
public IPreferencesService PreferencesService { get; }
|
||||||
public IPrintService PrintService { get; }
|
public IPrintService PrintService { get; }
|
||||||
|
public Guid? CurrentMailAccountId => initializedMailItemViewModel?.MailCopy.AssignedAccount?.Id;
|
||||||
|
public Guid? CurrentMailFileId => initializedMailItemViewModel?.MailCopy.FileId;
|
||||||
|
|
||||||
public MailRenderingPageViewModel(IMailDialogService dialogService,
|
public MailRenderingPageViewModel(IMailDialogService dialogService,
|
||||||
INativeAppService nativeAppService,
|
INativeAppService nativeAppService,
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ public partial class App : WinoApplication,
|
|||||||
{
|
{
|
||||||
services.AddSingleton<INavigationService, NavigationService>();
|
services.AddSingleton<INavigationService, NavigationService>();
|
||||||
services.AddSingleton<IMailDialogService, DialogService>();
|
services.AddSingleton<IMailDialogService, DialogService>();
|
||||||
|
services.AddSingleton<IAiActionOptionsService, AiActionOptionsService>();
|
||||||
services.AddTransient<IProviderService, ProviderService>();
|
services.AddTransient<IProviderService, ProviderService>();
|
||||||
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
|
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
|
||||||
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
|
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
@@ -9,9 +10,9 @@ using Microsoft.UI.Xaml.Controls.Primitives;
|
|||||||
using Microsoft.Xaml.Interactivity;
|
using Microsoft.Xaml.Interactivity;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Menus;
|
using Wino.Core.Domain.Models.Menus;
|
||||||
using Wino.Mail.WinUI.Controls;
|
|
||||||
using Wino.Helpers;
|
using Wino.Helpers;
|
||||||
using Wino.Mail.WinUI;
|
using Wino.Mail.WinUI;
|
||||||
|
using Wino.Mail.WinUI.Controls;
|
||||||
|
|
||||||
namespace Wino.Behaviors;
|
namespace Wino.Behaviors;
|
||||||
|
|
||||||
@@ -41,14 +42,25 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
|||||||
{
|
{
|
||||||
foreach (var item in enumerable)
|
foreach (var item in enumerable)
|
||||||
{
|
{
|
||||||
if (item is ButtonBase button)
|
DetachCommandElement(item);
|
||||||
{
|
|
||||||
button.Click -= Button_Click;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdatePrimaryCommands()
|
private void UpdatePrimaryCommands()
|
||||||
{
|
{
|
||||||
if (AssociatedObject == null)
|
if (AssociatedObject == null)
|
||||||
@@ -61,10 +73,7 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
|||||||
{
|
{
|
||||||
foreach (var item in enumerableObjects)
|
foreach (var item in enumerableObjects)
|
||||||
{
|
{
|
||||||
if (item is ButtonBase button)
|
DetachCommandElement(item);
|
||||||
{
|
|
||||||
button.Click -= Button_Click;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,10 +81,7 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
|||||||
{
|
{
|
||||||
foreach (var item in secondaryObject)
|
foreach (var item in secondaryObject)
|
||||||
{
|
{
|
||||||
if (item is ButtonBase button)
|
DetachCommandElement(item);
|
||||||
{
|
|
||||||
button.Click -= Button_Click;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +141,6 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
|||||||
AssociatedObject.PrimaryCommands.Add(menuItem);
|
AssociatedObject.PrimaryCommands.Add(menuItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (dependencyObject is ICommandBarElement icommandBarElement)
|
//if (dependencyObject is ICommandBarElement icommandBarElement)
|
||||||
//{
|
//{
|
||||||
// if (dependencyObject is ButtonBase button)
|
// 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}">
|
Visibility="{x:Bind ViewModel.IsDraftBusy, Mode=OneWay}">
|
||||||
<ProgressRing IsActive="True" />
|
<ProgressRing IsActive="True" />
|
||||||
</AppBarButton>
|
</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 Click="ToggleEditorThemeClicked" ToolTipService.ToolTip="{x:Bind GetEditorThemeToolTip(WebViewEditor.IsEditorDarkMode), Mode=OneWay}">
|
||||||
<AppBarButton.Icon>
|
<AppBarButton.Icon>
|
||||||
<coreControls:WinoFontIcon Icon="{x:Bind GetEditorThemeIcon(WebViewEditor.IsEditorDarkMode), Mode=OneWay}" />
|
<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" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -445,10 +457,19 @@
|
|||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
Text="{x:Bind ViewModel.Subject, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
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 -->
|
<!-- Attachments -->
|
||||||
<ListView
|
<ListView
|
||||||
x:Name="AttachmentsListView"
|
x:Name="AttachmentsListView"
|
||||||
Grid.Row="5"
|
Grid.Row="6"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
ui:ListViewExtensions.Command="{x:Bind ViewModel.OpenAttachmentCommand}"
|
ui:ListViewExtensions.Command="{x:Bind ViewModel.OpenAttachmentCommand}"
|
||||||
x:Load="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.IncludedAttachments.Count), Mode=OneWay}"
|
x:Load="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.IncludedAttachments.Count), Mode=OneWay}"
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ using Wino.Views.Abstract;
|
|||||||
namespace Wino.Views.Mail;
|
namespace Wino.Views.Mail;
|
||||||
|
|
||||||
public sealed partial class ComposePage : ComposePageAbstract,
|
public sealed partial class ComposePage : ComposePageAbstract,
|
||||||
|
IAiHtmlActionHost,
|
||||||
IRecipient<CreateNewComposeMailRequested>,
|
IRecipient<CreateNewComposeMailRequested>,
|
||||||
IRecipient<ApplicationThemeChanged>,
|
IRecipient<ApplicationThemeChanged>,
|
||||||
IRecipient<ReaderItemRefreshRequestedEvent>
|
IRecipient<ReaderItemRefreshRequestedEvent>
|
||||||
{
|
{
|
||||||
public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView();
|
public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView();
|
||||||
|
|
||||||
|
public Visibility GetAiActionsPanelVisibility(bool? isChecked) => isChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
private readonly List<IDisposable> _disposables = [];
|
private readonly List<IDisposable> _disposables = [];
|
||||||
|
|
||||||
public ComposePage()
|
public ComposePage()
|
||||||
@@ -283,6 +286,11 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
ViewModel.IsCCBCCVisible = true;
|
ViewModel.IsCCBCCVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void ComposeAiActionsToggleButton_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await ComposeAiActionsPanel.RefreshAvailabilityAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private async void TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
|
private async void TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
|
||||||
{
|
{
|
||||||
// Check is valid email.
|
// Check is valid email.
|
||||||
@@ -410,11 +418,39 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
base.OnNavigatingFrom(e);
|
base.OnNavigatingFrom(e);
|
||||||
|
|
||||||
FocusManager.GotFocus -= GlobalFocusManagerGotFocus;
|
FocusManager.GotFocus -= GlobalFocusManagerGotFocus;
|
||||||
|
ComposeAiActionsPanel.CancelPendingOperation();
|
||||||
await ViewModel.UpdateMimeChangesAsync();
|
await ViewModel.UpdateMimeChangesAsync();
|
||||||
|
|
||||||
DisposeDisposables();
|
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)
|
private void OpenAttachment_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is MenuFlyoutItem item && item.CommandParameter is MailAttachmentViewModel attachment)
|
if (sender is MenuFlyoutItem item && item.CommandParameter is MailAttachmentViewModel attachment)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
xmlns:local="using:Wino.Behaviors"
|
xmlns:local="using:Wino.Behaviors"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||||
|
xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
|
||||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||||
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
|
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
|
||||||
x:Name="root"
|
x:Name="root"
|
||||||
@@ -181,6 +182,7 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid Grid.Row="0" Margin="5,0">
|
<Grid Grid.Row="0" Margin="5,0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -273,13 +275,16 @@
|
|||||||
IsDynamicOverflowEnabled="True"
|
IsDynamicOverflowEnabled="True"
|
||||||
OverflowButtonVisibility="Auto">
|
OverflowButtonVisibility="Auto">
|
||||||
<interactivity:Interaction.Behaviors>
|
<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>
|
</interactivity:Interaction.Behaviors>
|
||||||
<CommandBar.Content>
|
<CommandBar.Content>
|
||||||
<Grid Padding="0,5">
|
<Grid Padding="0,5">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -317,6 +322,19 @@
|
|||||||
<TextBlock FontSize="12" Text="{x:Bind helpers:XamlHelpers.GetCreationDateString(ViewModel.CreationDate, ViewModel.PreferencesService.Prefer24HourTimeFormat), Mode=OneWay}" />
|
<TextBlock FontSize="12" Text="{x:Bind helpers:XamlHelpers.GetCreationDateString(ViewModel.CreationDate, ViewModel.PreferencesService.Prefer24HourTimeFormat), Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
</CommandBar.Content>
|
</CommandBar.Content>
|
||||||
</CommandBar>
|
</CommandBar>
|
||||||
@@ -409,8 +427,16 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</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 -->
|
<!-- Attachments -->
|
||||||
<Grid Grid.Row="3">
|
<Grid Grid.Row="4">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
@@ -442,7 +468,7 @@
|
|||||||
|
|
||||||
<InfoBar
|
<InfoBar
|
||||||
x:Name="ImageLoadingDisabledMessage"
|
x:Name="ImageLoadingDisabledMessage"
|
||||||
Grid.Row="4"
|
Grid.Row="5"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
x:Load="{x:Bind ViewModel.IsImageRenderingDisabled, Mode=OneWay}"
|
x:Load="{x:Bind ViewModel.IsImageRenderingDisabled, Mode=OneWay}"
|
||||||
IsOpen="True"
|
IsOpen="True"
|
||||||
@@ -458,7 +484,7 @@
|
|||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
x:Name="DownloadingProgressBar"
|
x:Name="DownloadingProgressBar"
|
||||||
Grid.Row="3"
|
Grid.Row="4"
|
||||||
Margin="12,1"
|
Margin="12,1"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -16,6 +17,7 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Printing;
|
using Wino.Core.Domain.Models.Printing;
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
using Wino.Mail.WinUI;
|
using Wino.Mail.WinUI;
|
||||||
|
using Wino.Mail.WinUI.Controls;
|
||||||
using Wino.Mail.WinUI.Extensions;
|
using Wino.Mail.WinUI.Extensions;
|
||||||
using Wino.Messaging.Client.Mails;
|
using Wino.Messaging.Client.Mails;
|
||||||
using Wino.Messaging.Client.Shell;
|
using Wino.Messaging.Client.Shell;
|
||||||
@@ -24,19 +26,24 @@ using Wino.Views.Abstract;
|
|||||||
namespace Wino.Views.Mail;
|
namespace Wino.Views.Mail;
|
||||||
|
|
||||||
public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
||||||
|
IAiHtmlActionHost,
|
||||||
IRecipient<HtmlRenderingRequested>,
|
IRecipient<HtmlRenderingRequested>,
|
||||||
IRecipient<CancelRenderingContentRequested>,
|
IRecipient<CancelRenderingContentRequested>,
|
||||||
IRecipient<ApplicationThemeChanged>
|
IRecipient<ApplicationThemeChanged>
|
||||||
{
|
{
|
||||||
private readonly IPreferencesService _preferencesService = App.Current.Services.GetService<IPreferencesService>()!;
|
private readonly IPreferencesService _preferencesService = App.Current.Services.GetService<IPreferencesService>()!;
|
||||||
private readonly IMailDialogService _dialogService = App.Current.Services.GetService<IMailDialogService>()!;
|
private readonly IMailDialogService _dialogService = App.Current.Services.GetService<IMailDialogService>()!;
|
||||||
|
private readonly IMimeFileService _mimeFileService = App.Current.Services.GetRequiredService<IMimeFileService>();
|
||||||
|
|
||||||
private bool isRenderingInProgress = false;
|
private bool isRenderingInProgress = false;
|
||||||
private bool? _lastAppliedDarkTheme;
|
private bool? _lastAppliedDarkTheme;
|
||||||
private TaskCompletionSource<bool> DOMLoadedTask = new TaskCompletionSource<bool>();
|
private TaskCompletionSource<bool> DOMLoadedTask = new TaskCompletionSource<bool>();
|
||||||
|
private string _currentRenderedHtml = string.Empty;
|
||||||
|
|
||||||
public WebView2 GetWebView() => Chromium;
|
public WebView2 GetWebView() => Chromium;
|
||||||
|
|
||||||
|
public Visibility GetAiActionsPanelVisibility(bool? isChecked) => isChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
public MailRenderingPage()
|
public MailRenderingPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -82,6 +89,7 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
private async Task RenderInternalAsync(string htmlBody)
|
private async Task RenderInternalAsync(string htmlBody)
|
||||||
{
|
{
|
||||||
isRenderingInProgress = true;
|
isRenderingInProgress = true;
|
||||||
|
_currentRenderedHtml = htmlBody ?? string.Empty;
|
||||||
|
|
||||||
await DOMLoadedTask.Task;
|
await DOMLoadedTask.Task;
|
||||||
|
|
||||||
@@ -141,10 +149,63 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
|
|
||||||
ViewModel.SaveHTMLasPDFFunc = null;
|
ViewModel.SaveHTMLasPDFFunc = null;
|
||||||
ViewModel.DirectPrintFuncAsync = null;
|
ViewModel.DirectPrintFuncAsync = null;
|
||||||
|
_currentRenderedHtml = string.Empty;
|
||||||
|
ReaderAiActionsPanel.CancelPendingOperation();
|
||||||
|
|
||||||
DisposeWebView2();
|
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()
|
private void DisposeWebView2()
|
||||||
{
|
{
|
||||||
if (Chromium == null) return;
|
if (Chromium == null) return;
|
||||||
|
|||||||
@@ -120,6 +120,48 @@ public class MimeFileService : IMimeFileService
|
|||||||
return true;
|
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)
|
public MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null)
|
||||||
{
|
{
|
||||||
var visitor = CreateHTMLPreviewVisitor(message, mimeLocalPath);
|
var visitor = CreateHTMLPreviewVisitor(message, mimeLocalPath);
|
||||||
@@ -209,4 +251,21 @@ public class MimeFileService : IMimeFileService
|
|||||||
Log.Error(ex, "Failed to remove user's mime cache folder.");
|
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 SemaphoreSlim _tokenRefreshLock = new(1, 1);
|
||||||
private readonly bool _ownsHttpClient;
|
private readonly bool _ownsHttpClient;
|
||||||
|
|
||||||
// private const string ApiUrl = "https://localhost:7204/";
|
private const string ApiUrl = "https://localhost:7204/";
|
||||||
private const string ApiUrl = "https://api.winomail.app/";
|
// private const string ApiUrl = "https://api.winomail.app/";
|
||||||
|
|
||||||
public WinoAccountApiClient(IDatabaseService databaseService, HttpClient? httpClient = null)
|
public WinoAccountApiClient(IDatabaseService databaseService, HttpClient? httpClient = null)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user