mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
1455 lines
52 KiB
C#
1455 lines
52 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.IO.Abstractions;
|
|
using System.Linq;
|
|
using System.Runtime.Versioning;
|
|
using System.Text.Json;
|
|
|
|
using global::PowerToys.GPOWrapper;
|
|
using Microsoft.PowerToys.Settings.UI.Helpers;
|
|
using Microsoft.PowerToys.Settings.UI.Library;
|
|
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
|
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
|
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
|
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
|
using Microsoft.UI.Dispatching;
|
|
using Microsoft.Win32;
|
|
using Windows.Security.Credentials;
|
|
|
|
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|
{
|
|
public partial class AdvancedPasteViewModel : PageViewModelBase
|
|
{
|
|
private static readonly HashSet<string> WarnHotkeys = ["Ctrl + V", "Ctrl + Shift + V"];
|
|
|
|
private bool _disposed;
|
|
private PasteAIProviderDefinition _pasteAIProviderDraft;
|
|
private PasteAIProviderDefinition _editingPasteAIProvider;
|
|
|
|
protected override string ModuleName => AdvancedPasteSettings.ModuleName;
|
|
|
|
private GeneralSettings GeneralSettingsConfig { get; set; }
|
|
|
|
private readonly ISettingsUtils _settingsUtils;
|
|
|
|
private readonly AdvancedPasteSettings _advancedPasteSettings;
|
|
private readonly AdvancedPasteAdditionalActions _additionalActions;
|
|
private readonly ObservableCollection<AdvancedPasteCustomAction> _customActions;
|
|
private readonly DispatcherQueue _dispatcherQueue;
|
|
private IFileSystemWatcher _settingsWatcher;
|
|
private bool _suppressSave;
|
|
|
|
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
|
private bool _enabledStateIsGPOConfigured;
|
|
private GpoRuleConfigured _onlineAIModelsGpoRuleConfiguration;
|
|
private bool _onlineAIModelsDisallowedByGPO;
|
|
private bool _isEnabled;
|
|
|
|
private Func<string, int> SendConfigMSG { get; }
|
|
|
|
private static readonly HashSet<string> CustomActionNonPersistedProperties = new(StringComparer.Ordinal)
|
|
{
|
|
nameof(AdvancedPasteCustomAction.CanMoveUp),
|
|
nameof(AdvancedPasteCustomAction.CanMoveDown),
|
|
nameof(AdvancedPasteCustomAction.IsValid),
|
|
nameof(AdvancedPasteCustomAction.HasConflict),
|
|
nameof(AdvancedPasteCustomAction.Tooltip),
|
|
nameof(AdvancedPasteCustomAction.SubActions),
|
|
};
|
|
|
|
public AdvancedPasteViewModel(
|
|
ISettingsUtils settingsUtils,
|
|
ISettingsRepository<GeneralSettings> settingsRepository,
|
|
ISettingsRepository<AdvancedPasteSettings> advancedPasteSettingsRepository,
|
|
Func<string, int> ipcMSGCallBackFunc)
|
|
{
|
|
// To obtain the general settings configurations of PowerToys Settings.
|
|
ArgumentNullException.ThrowIfNull(settingsRepository);
|
|
|
|
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
|
|
|
// To obtain the settings configurations of Fancy zones.
|
|
ArgumentNullException.ThrowIfNull(settingsRepository);
|
|
|
|
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
|
|
|
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
|
|
|
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
|
|
|
|
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
|
|
|
|
AttachConfigurationHandlers();
|
|
|
|
// set the callback functions value to handle outgoing IPC message.
|
|
SendConfigMSG = ipcMSGCallBackFunc;
|
|
|
|
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
|
|
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
|
|
|
|
SetupSettingsFileWatcher();
|
|
|
|
InitializePasteAIProviderState();
|
|
|
|
InitializeEnabledValue();
|
|
MigrateLegacyAIEnablement();
|
|
|
|
foreach (var action in _additionalActions.GetAllActions())
|
|
{
|
|
action.PropertyChanged += OnAdditionalActionPropertyChanged;
|
|
}
|
|
|
|
foreach (var customAction in _customActions)
|
|
{
|
|
customAction.PropertyChanged += OnCustomActionPropertyChanged;
|
|
}
|
|
|
|
_customActions.CollectionChanged += OnCustomActionsCollectionChanged;
|
|
UpdateCustomActionsCanMoveUpDown();
|
|
}
|
|
|
|
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
|
|
{
|
|
var hotkeySettings = new List<HotkeySettings>
|
|
{
|
|
PasteAsPlainTextShortcut,
|
|
AdvancedPasteUIShortcut,
|
|
PasteAsMarkdownShortcut,
|
|
PasteAsJsonShortcut,
|
|
};
|
|
|
|
foreach (var action in _additionalActions.GetAllActions())
|
|
{
|
|
if (action is AdvancedPasteAdditionalAction additionalAction)
|
|
{
|
|
hotkeySettings.Add(additionalAction.Shortcut);
|
|
}
|
|
}
|
|
|
|
// Custom actions do not have localization header, just use the action name.
|
|
foreach (var customAction in _customActions)
|
|
{
|
|
hotkeySettings.Add(customAction.Shortcut);
|
|
}
|
|
|
|
return new Dictionary<string, HotkeySettings[]>
|
|
{
|
|
[ModuleName] = hotkeySettings.ToArray(),
|
|
};
|
|
}
|
|
|
|
private void InitializeEnabledValue()
|
|
{
|
|
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAdvancedPasteEnabledValue();
|
|
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
|
|
{
|
|
// Get the enabled state from GPO.
|
|
_enabledStateIsGPOConfigured = true;
|
|
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
|
|
}
|
|
else
|
|
{
|
|
_isEnabled = GeneralSettingsConfig.Enabled.AdvancedPaste;
|
|
}
|
|
|
|
_onlineAIModelsGpoRuleConfiguration = GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue();
|
|
_onlineAIModelsDisallowedByGPO = _onlineAIModelsGpoRuleConfiguration == GpoRuleConfigured.Disabled;
|
|
|
|
if (_onlineAIModelsDisallowedByGPO)
|
|
{
|
|
// disable AI if it was enabled
|
|
DisableAI();
|
|
}
|
|
}
|
|
|
|
private void MigrateLegacyAIEnablement()
|
|
{
|
|
var properties = _advancedPasteSettings?.Properties;
|
|
if (properties is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
|
|
bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
|
|
|
|
if (IsOnlineAIModelsDisallowedByGPO)
|
|
{
|
|
if (legacyAdvancedAIConsumed)
|
|
{
|
|
SaveAndNotifySettings();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var configuration = properties.PasteAIConfiguration;
|
|
if (configuration is null)
|
|
{
|
|
configuration = new PasteAIConfiguration();
|
|
properties.PasteAIConfiguration = configuration;
|
|
}
|
|
|
|
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
|
|
|
|
if (legacyCredential is null && !legacyAdvancedAIConsumed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool configurationUpdated = false;
|
|
|
|
const string openAIServiceType = "OpenAI";
|
|
PasteAIProviderDefinition openAIProvider = configuration?.Providers?.FirstOrDefault(
|
|
provider => string.Equals(provider.ServiceType, openAIServiceType, StringComparison.OrdinalIgnoreCase));
|
|
|
|
bool shouldEnsureOpenAIProvider = legacyCredential is not null;
|
|
if (shouldEnsureOpenAIProvider)
|
|
{
|
|
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
|
|
openAIProvider = ensureResult.Provider;
|
|
configurationUpdated |= ensureResult.Updated;
|
|
}
|
|
|
|
if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
|
|
{
|
|
openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled;
|
|
configurationUpdated = true;
|
|
}
|
|
|
|
if (legacyCredential is not null && openAIProvider is not null)
|
|
{
|
|
SavePasteAIApiKey(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
|
|
RemoveLegacyOpenAICredential();
|
|
}
|
|
|
|
bool shouldEnableAI = legacyCredential is not null;
|
|
bool enabledChanged = false;
|
|
if (properties.IsAIEnabled != shouldEnableAI)
|
|
{
|
|
properties.IsAIEnabled = shouldEnableAI;
|
|
enabledChanged = true;
|
|
}
|
|
|
|
bool shouldPersist = configurationUpdated || enabledChanged || legacyAdvancedAIConsumed;
|
|
|
|
if (shouldPersist)
|
|
{
|
|
SaveAndNotifySettings();
|
|
|
|
if (configurationUpdated)
|
|
{
|
|
OnPropertyChanged(nameof(PasteAIConfiguration));
|
|
}
|
|
|
|
if (enabledChanged)
|
|
{
|
|
OnPropertyChanged(nameof(IsAIEnabled));
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsEnabled
|
|
{
|
|
get => _isEnabled;
|
|
set
|
|
{
|
|
if (_enabledStateIsGPOConfigured)
|
|
{
|
|
// If it's GPO configured, shouldn't be able to change this state.
|
|
return;
|
|
}
|
|
|
|
if (_isEnabled != value)
|
|
{
|
|
_isEnabled = value;
|
|
OnPropertyChanged(nameof(IsEnabled));
|
|
OnPropertyChanged(nameof(ShowOnlineAIModelsGpoConfiguredInfoBar));
|
|
OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar));
|
|
|
|
// Set the status of AdvancedPaste in the general settings
|
|
GeneralSettingsConfig.Enabled.AdvancedPaste = value;
|
|
var outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
|
|
|
SendConfigMSG(outgoing.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
public ObservableCollection<AdvancedPasteCustomAction> CustomActions => _customActions;
|
|
|
|
public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
|
|
|
|
public static IEnumerable<AIServiceTypeMetadata> AvailableProviders => AIServiceTypeRegistry.GetAvailableServiceTypes();
|
|
|
|
/// <summary>
|
|
/// Gets available AI providers filtered by GPO policies.
|
|
/// Only returns providers that are not explicitly disabled by GPO.
|
|
/// </summary>
|
|
public IEnumerable<AIServiceTypeMetadata> AvailableProvidersFilteredByGPO =>
|
|
AvailableProviders.Where(metadata => IsServiceTypeAllowedByGPO(metadata.ServiceType));
|
|
|
|
public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO;
|
|
|
|
private PasswordCredential TryGetLegacyOpenAICredential()
|
|
{
|
|
try
|
|
{
|
|
PasswordVault vault = new();
|
|
var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
|
credential?.RetrievePassword();
|
|
return credential;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void RemoveLegacyOpenAICredential()
|
|
{
|
|
try
|
|
{
|
|
PasswordVault vault = new();
|
|
TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
public bool IsEnabledGpoConfigured
|
|
{
|
|
get => _enabledStateIsGPOConfigured;
|
|
}
|
|
|
|
public bool IsOnlineAIModelsDisallowedByGPO
|
|
{
|
|
get => _onlineAIModelsDisallowedByGPO || _enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled;
|
|
}
|
|
|
|
public bool ShowOnlineAIModelsGpoConfiguredInfoBar
|
|
{
|
|
get => _onlineAIModelsDisallowedByGPO && _isEnabled;
|
|
}
|
|
|
|
private bool IsClipboardHistoryEnabled()
|
|
{
|
|
string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\";
|
|
try
|
|
{
|
|
int enableClipboardHistory = (int)Registry.GetValue(registryKey, "EnableClipboardHistory", false);
|
|
return enableClipboardHistory != 0;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool IsClipboardHistoryDisabledByGPO()
|
|
{
|
|
string registryKey = @"HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\System\";
|
|
try
|
|
{
|
|
object allowClipboardHistory = Registry.GetValue(registryKey, "AllowClipboardHistory", null);
|
|
if (allowClipboardHistory != null)
|
|
{
|
|
return (int)allowClipboardHistory == 0;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void SetClipboardHistoryEnabled(bool value)
|
|
{
|
|
string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\";
|
|
try
|
|
{
|
|
Registry.SetValue(registryKey, "EnableClipboardHistory", value ? 1 : 0);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
public bool ClipboardHistoryEnabled
|
|
{
|
|
get => IsClipboardHistoryEnabled();
|
|
set
|
|
{
|
|
if (IsClipboardHistoryEnabled() != value)
|
|
{
|
|
SetClipboardHistoryEnabled(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool ClipboardHistoryDisabledByGPO
|
|
{
|
|
get => IsClipboardHistoryDisabledByGPO();
|
|
}
|
|
|
|
public bool ShowClipboardHistoryIsGpoConfiguredInfoBar
|
|
{
|
|
get => IsClipboardHistoryDisabledByGPO() && _isEnabled;
|
|
}
|
|
|
|
public HotkeySettings AdvancedPasteUIShortcut
|
|
{
|
|
get => _advancedPasteSettings.Properties.AdvancedPasteUIShortcut;
|
|
set
|
|
{
|
|
if (_advancedPasteSettings.Properties.AdvancedPasteUIShortcut != value)
|
|
{
|
|
_advancedPasteSettings.Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut;
|
|
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
|
|
OnPropertyChanged(nameof(AdvancedPasteUIShortcut));
|
|
SaveAndNotifySettings();
|
|
}
|
|
}
|
|
}
|
|
|
|
public HotkeySettings PasteAsPlainTextShortcut
|
|
{
|
|
get => _advancedPasteSettings.Properties.PasteAsPlainTextShortcut;
|
|
set
|
|
{
|
|
if (_advancedPasteSettings.Properties.PasteAsPlainTextShortcut != value)
|
|
{
|
|
_advancedPasteSettings.Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut;
|
|
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
|
|
OnPropertyChanged(nameof(PasteAsPlainTextShortcut));
|
|
SaveAndNotifySettings();
|
|
}
|
|
}
|
|
}
|
|
|
|
public HotkeySettings PasteAsMarkdownShortcut
|
|
{
|
|
get => _advancedPasteSettings.Properties.PasteAsMarkdownShortcut;
|
|
set
|
|
{
|
|
if (_advancedPasteSettings.Properties.PasteAsMarkdownShortcut != value)
|
|
{
|
|
_advancedPasteSettings.Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings();
|
|
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
|
|
OnPropertyChanged(nameof(PasteAsMarkdownShortcut));
|
|
SaveAndNotifySettings();
|
|
}
|
|
}
|
|
}
|
|
|
|
public HotkeySettings PasteAsJsonShortcut
|
|
{
|
|
get => _advancedPasteSettings.Properties.PasteAsJsonShortcut;
|
|
set
|
|
{
|
|
if (_advancedPasteSettings.Properties.PasteAsJsonShortcut != value)
|
|
{
|
|
_advancedPasteSettings.Properties.PasteAsJsonShortcut = value ?? new HotkeySettings();
|
|
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
|
|
OnPropertyChanged(nameof(PasteAsJsonShortcut));
|
|
SaveAndNotifySettings();
|
|
}
|
|
}
|
|
}
|
|
|
|
public PasteAIConfiguration PasteAIConfiguration
|
|
{
|
|
get => _advancedPasteSettings.Properties.PasteAIConfiguration;
|
|
set
|
|
{
|
|
if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration))
|
|
{
|
|
UnsubscribeFromPasteAIConfiguration(_advancedPasteSettings.Properties.PasteAIConfiguration);
|
|
|
|
var newValue = value ?? new PasteAIConfiguration();
|
|
_advancedPasteSettings.Properties.PasteAIConfiguration = newValue;
|
|
SubscribeToPasteAIConfiguration(newValue);
|
|
|
|
OnPropertyChanged(nameof(PasteAIConfiguration));
|
|
SaveAndNotifySettings();
|
|
}
|
|
}
|
|
}
|
|
|
|
public PasteAIProviderDefinition PasteAIProviderDraft
|
|
{
|
|
get => _pasteAIProviderDraft;
|
|
private set
|
|
{
|
|
if (!ReferenceEquals(_pasteAIProviderDraft, value))
|
|
{
|
|
_pasteAIProviderDraft = value;
|
|
OnPropertyChanged(nameof(PasteAIProviderDraft));
|
|
OnPropertyChanged(nameof(ShowPasteAIProviderGpoConfiguredInfoBar));
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool ShowPasteAIProviderGpoConfiguredInfoBar
|
|
{
|
|
get
|
|
{
|
|
if (_pasteAIProviderDraft is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var serviceType = _pasteAIProviderDraft.ServiceType.ToAIServiceType();
|
|
return !IsServiceTypeAllowedByGPO(serviceType);
|
|
}
|
|
}
|
|
|
|
public bool IsEditingPasteAIProvider => _editingPasteAIProvider is not null;
|
|
|
|
public bool ShowCustomPreview
|
|
{
|
|
get => _advancedPasteSettings.Properties.ShowCustomPreview;
|
|
set
|
|
{
|
|
if (value != _advancedPasteSettings.Properties.ShowCustomPreview)
|
|
{
|
|
_advancedPasteSettings.Properties.ShowCustomPreview = value;
|
|
NotifySettingsChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool CloseAfterLosingFocus
|
|
{
|
|
get => _advancedPasteSettings.Properties.CloseAfterLosingFocus;
|
|
set
|
|
{
|
|
if (value != _advancedPasteSettings.Properties.CloseAfterLosingFocus)
|
|
{
|
|
_advancedPasteSettings.Properties.CloseAfterLosingFocus = value;
|
|
NotifySettingsChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool EnableClipboardPreview
|
|
{
|
|
get => _advancedPasteSettings.Properties.EnableClipboardPreview;
|
|
set
|
|
{
|
|
if (value != _advancedPasteSettings.Properties.EnableClipboardPreview)
|
|
{
|
|
_advancedPasteSettings.Properties.EnableClipboardPreview = value;
|
|
NotifySettingsChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsConflictingCopyShortcut =>
|
|
_customActions.Select(customAction => customAction.Shortcut)
|
|
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
|
|
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
|
|
|
public bool IsAdditionalActionConflictingCopyShortcut =>
|
|
_additionalActions.GetAllActions()
|
|
.OfType<AdvancedPasteAdditionalAction>()
|
|
.Select(additionalAction => additionalAction.Shortcut)
|
|
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
|
|
|
private void NotifySettingsChanged()
|
|
{
|
|
// Using InvariantCulture as this is an IPC message
|
|
SendConfigMSG(
|
|
string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
|
|
AdvancedPasteSettings.ModuleName,
|
|
JsonSerializer.Serialize(_advancedPasteSettings, SourceGenerationContextContext.Default.AdvancedPasteSettings)));
|
|
}
|
|
|
|
public void RefreshEnabledState()
|
|
{
|
|
InitializeEnabledValue();
|
|
MigrateLegacyAIEnablement();
|
|
OnPropertyChanged(nameof(IsEnabled));
|
|
OnPropertyChanged(nameof(ShowOnlineAIModelsGpoConfiguredInfoBar));
|
|
OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar));
|
|
OnPropertyChanged(nameof(IsAIEnabled));
|
|
}
|
|
|
|
public void BeginAddPasteAIProvider(string serviceType)
|
|
{
|
|
var normalizedServiceType = NormalizeServiceType(serviceType, out var persistedServiceType);
|
|
|
|
var metadata = AIServiceTypeRegistry.GetMetadata(normalizedServiceType);
|
|
var provider = new PasteAIProviderDefinition
|
|
{
|
|
ServiceType = persistedServiceType,
|
|
ModelName = PasteAIProviderDefaults.GetDefaultModelName(normalizedServiceType),
|
|
EndpointUrl = string.Empty,
|
|
ApiVersion = string.Empty,
|
|
DeploymentName = string.Empty,
|
|
ModelPath = string.Empty,
|
|
SystemPrompt = string.Empty,
|
|
ModerationEnabled = normalizedServiceType == AIServiceType.OpenAI,
|
|
IsLocalModel = metadata.IsLocalModel,
|
|
};
|
|
|
|
if (normalizedServiceType is AIServiceType.FoundryLocal or AIServiceType.Onnx or AIServiceType.ML)
|
|
{
|
|
provider.ModelName = string.Empty;
|
|
}
|
|
|
|
_editingPasteAIProvider = null;
|
|
PasteAIProviderDraft = provider;
|
|
}
|
|
|
|
private static AIServiceType NormalizeServiceType(string serviceType, out string persistedServiceType)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(serviceType))
|
|
{
|
|
persistedServiceType = AIServiceType.OpenAI.ToConfigurationString();
|
|
return AIServiceType.OpenAI;
|
|
}
|
|
|
|
var trimmed = serviceType.Trim();
|
|
var serviceTypeKind = trimmed.ToAIServiceType();
|
|
|
|
if (serviceTypeKind == AIServiceType.Unknown)
|
|
{
|
|
persistedServiceType = AIServiceType.OpenAI.ToConfigurationString();
|
|
return AIServiceType.OpenAI;
|
|
}
|
|
|
|
persistedServiceType = trimmed;
|
|
return serviceTypeKind;
|
|
}
|
|
|
|
public bool IsServiceTypeAllowedByGPO(AIServiceType serviceType)
|
|
{
|
|
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
|
|
|
|
// Check if this is an online service
|
|
if (metadata.IsOnlineService)
|
|
{
|
|
// For online services, first check the global online AI models GPO
|
|
if (_onlineAIModelsGpoRuleConfiguration == GpoRuleConfigured.Disabled)
|
|
{
|
|
// If global online AI is disabled, all online services are blocked
|
|
return false;
|
|
}
|
|
|
|
// If global online AI is enabled or not configured, check individual endpoint GPO
|
|
var individualGpoRule = serviceType switch
|
|
{
|
|
AIServiceType.OpenAI => GPOWrapper.GetAllowedAdvancedPasteOpenAIValue(),
|
|
AIServiceType.AzureOpenAI => GPOWrapper.GetAllowedAdvancedPasteAzureOpenAIValue(),
|
|
AIServiceType.AzureAIInference => GPOWrapper.GetAllowedAdvancedPasteAzureAIInferenceValue(),
|
|
AIServiceType.Mistral => GPOWrapper.GetAllowedAdvancedPasteMistralValue(),
|
|
AIServiceType.Google => GPOWrapper.GetAllowedAdvancedPasteGoogleValue(),
|
|
_ => GpoRuleConfigured.Unavailable,
|
|
};
|
|
|
|
// If individual GPO is explicitly disabled, block it
|
|
return individualGpoRule != GpoRuleConfigured.Disabled;
|
|
}
|
|
else
|
|
{
|
|
// For local models, only check their individual GPO (not affected by online AI GPO)
|
|
var localGpoRule = serviceType switch
|
|
{
|
|
AIServiceType.Ollama => GPOWrapper.GetAllowedAdvancedPasteOllamaValue(),
|
|
AIServiceType.FoundryLocal => GPOWrapper.GetAllowedAdvancedPasteFoundryLocalValue(),
|
|
_ => GpoRuleConfigured.Unavailable,
|
|
};
|
|
|
|
// If local model GPO is explicitly disabled, block it
|
|
return localGpoRule != GpoRuleConfigured.Disabled;
|
|
}
|
|
}
|
|
|
|
public void BeginEditPasteAIProvider(PasteAIProviderDefinition provider)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(provider);
|
|
|
|
_editingPasteAIProvider = provider;
|
|
var draft = provider.Clone();
|
|
var storedEndpoint = GetPasteAIEndpoint(draft.Id, draft.ServiceType);
|
|
if (!string.IsNullOrWhiteSpace(storedEndpoint))
|
|
{
|
|
draft.EndpointUrl = storedEndpoint;
|
|
}
|
|
|
|
PasteAIProviderDraft = draft;
|
|
}
|
|
|
|
public void CancelPasteAIProviderDraft()
|
|
{
|
|
PasteAIProviderDraft = null;
|
|
_editingPasteAIProvider = null;
|
|
}
|
|
|
|
public void CommitPasteAIProviderDraft(string apiKey, string endpoint)
|
|
{
|
|
if (PasteAIProviderDraft is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var config = PasteAIConfiguration ?? new PasteAIConfiguration();
|
|
if (_advancedPasteSettings.Properties.PasteAIConfiguration is null)
|
|
{
|
|
PasteAIConfiguration = config;
|
|
}
|
|
|
|
var draft = PasteAIProviderDraft;
|
|
draft.EndpointUrl = endpoint?.Trim() ?? string.Empty;
|
|
|
|
SavePasteAIApiKey(draft.Id, draft.ServiceType, apiKey);
|
|
|
|
if (_editingPasteAIProvider is null)
|
|
{
|
|
config.Providers.Add(draft);
|
|
config.ActiveProviderId ??= draft.Id;
|
|
}
|
|
else
|
|
{
|
|
UpdateProviderFromDraft(_editingPasteAIProvider, draft);
|
|
_editingPasteAIProvider = null;
|
|
}
|
|
|
|
PasteAIProviderDraft = null;
|
|
SaveAndNotifySettings();
|
|
OnPropertyChanged(nameof(PasteAIConfiguration));
|
|
}
|
|
|
|
public void RemovePasteAIProvider(PasteAIProviderDefinition provider)
|
|
{
|
|
if (provider is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var config = PasteAIConfiguration;
|
|
if (config?.Providers is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (config.Providers.Remove(provider))
|
|
{
|
|
RemovePasteAICredentials(provider.Id, provider.ServiceType);
|
|
SaveAndNotifySettings();
|
|
OnPropertyChanged(nameof(PasteAIConfiguration));
|
|
}
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
if (disposing)
|
|
{
|
|
UnsubscribeFromPasteAIConfiguration(_advancedPasteSettings?.Properties.PasteAIConfiguration);
|
|
|
|
foreach (var action in _additionalActions.GetAllActions())
|
|
{
|
|
action.PropertyChanged -= OnAdditionalActionPropertyChanged;
|
|
}
|
|
|
|
foreach (var customAction in _customActions)
|
|
{
|
|
customAction.PropertyChanged -= OnCustomActionPropertyChanged;
|
|
}
|
|
|
|
_customActions.CollectionChanged -= OnCustomActionsCollectionChanged;
|
|
_settingsWatcher?.Dispose();
|
|
_settingsWatcher = null;
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
internal void DisableAI()
|
|
{
|
|
try
|
|
{
|
|
bool stateChanged = false;
|
|
|
|
if (_advancedPasteSettings.Properties.IsAIEnabled)
|
|
{
|
|
_advancedPasteSettings.Properties.IsAIEnabled = false;
|
|
stateChanged = true;
|
|
}
|
|
|
|
if (stateChanged)
|
|
{
|
|
SaveAndNotifySettings();
|
|
}
|
|
else
|
|
{
|
|
NotifySettingsChanged();
|
|
}
|
|
|
|
OnPropertyChanged(nameof(IsAIEnabled));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
internal void EnableAI()
|
|
{
|
|
try
|
|
{
|
|
if (IsOnlineAIModelsDisallowedByGPO)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool stateChanged = false;
|
|
|
|
if (!_advancedPasteSettings.Properties.IsAIEnabled)
|
|
{
|
|
_advancedPasteSettings.Properties.IsAIEnabled = true;
|
|
stateChanged = true;
|
|
}
|
|
|
|
if (stateChanged)
|
|
{
|
|
SaveAndNotifySettings();
|
|
}
|
|
else
|
|
{
|
|
NotifySettingsChanged();
|
|
}
|
|
|
|
OnPropertyChanged(nameof(IsAIEnabled));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
internal void SavePasteAIApiKey(string providerId, string serviceType, string apiKey)
|
|
{
|
|
try
|
|
{
|
|
apiKey = apiKey?.Trim() ?? string.Empty;
|
|
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
|
providerId ??= string.Empty;
|
|
|
|
string credentialResource = GetAICredentialResource(serviceType);
|
|
string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType);
|
|
string endpointCredentialUserName = GetPasteAIEndpointCredentialUserName(providerId, serviceType);
|
|
PasswordVault vault = new();
|
|
TryRemoveCredential(vault, credentialResource, credentialUserName);
|
|
TryRemoveCredential(vault, credentialResource, endpointCredentialUserName);
|
|
|
|
bool storeApiKey = RequiresCredentialStorage(serviceType) && !string.IsNullOrWhiteSpace(apiKey);
|
|
if (storeApiKey)
|
|
{
|
|
PasswordCredential cred = new(credentialResource, credentialUserName, apiKey);
|
|
vault.Add(cred);
|
|
}
|
|
|
|
OnPropertyChanged(nameof(IsAIEnabled));
|
|
NotifySettingsChanged();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
internal string GetPasteAIApiKey(string providerId, string serviceType)
|
|
{
|
|
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
|
providerId ??= string.Empty;
|
|
return RetrieveCredentialValue(
|
|
GetAICredentialResource(serviceType),
|
|
GetPasteAICredentialUserName(providerId, serviceType));
|
|
}
|
|
|
|
internal string GetPasteAIEndpoint(string providerId, string serviceType)
|
|
{
|
|
providerId ??= string.Empty;
|
|
var providers = PasteAIConfiguration?.Providers;
|
|
if (providers is null)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var provider = providers.FirstOrDefault(p => string.Equals(p.Id ?? string.Empty, providerId, StringComparison.OrdinalIgnoreCase));
|
|
if (provider is null && !string.IsNullOrWhiteSpace(serviceType))
|
|
{
|
|
provider = providers.FirstOrDefault(p => string.Equals(p.ServiceType, serviceType, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
return provider?.EndpointUrl?.Trim() ?? string.Empty;
|
|
}
|
|
|
|
private string GetAICredentialResource(string serviceType)
|
|
{
|
|
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
|
return serviceType.ToLowerInvariant() switch
|
|
{
|
|
"openai" => "https://platform.openai.com/api-keys",
|
|
"azureopenai" => "https://azure.microsoft.com/products/ai-services/openai-service",
|
|
"azureaiinference" => "https://azure.microsoft.com/products/ai-services/ai-inference",
|
|
"mistral" => "https://console.mistral.ai/account/api-keys",
|
|
"google" => "https://ai.google.dev/",
|
|
"ollama" => "https://ollama.com/",
|
|
_ => "https://platform.openai.com/api-keys",
|
|
};
|
|
}
|
|
|
|
private string GetPasteAICredentialUserName(string providerId, string serviceType)
|
|
{
|
|
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
|
providerId ??= string.Empty;
|
|
|
|
string service = serviceType.ToLowerInvariant();
|
|
string normalizedId = NormalizeProviderIdentifier(providerId);
|
|
|
|
return $"PowerToys_AdvancedPaste_PasteAI_{service}_{normalizedId}";
|
|
}
|
|
|
|
private string GetPasteAIEndpointCredentialUserName(string providerId, string serviceType)
|
|
{
|
|
return GetPasteAICredentialUserName(providerId, serviceType) + "_Endpoint";
|
|
}
|
|
|
|
private static void UpdateProviderFromDraft(PasteAIProviderDefinition target, PasteAIProviderDefinition source)
|
|
{
|
|
if (target is null || source is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
target.ServiceType = source.ServiceType;
|
|
target.ModelName = source.ModelName;
|
|
target.EndpointUrl = source.EndpointUrl;
|
|
target.ApiVersion = source.ApiVersion;
|
|
target.DeploymentName = source.DeploymentName;
|
|
target.ModelPath = source.ModelPath;
|
|
target.SystemPrompt = source.SystemPrompt;
|
|
target.ModerationEnabled = source.ModerationEnabled;
|
|
target.EnableAdvancedAI = source.EnableAdvancedAI;
|
|
target.IsLocalModel = source.IsLocalModel;
|
|
}
|
|
|
|
private void RemovePasteAICredentials(string providerId, string serviceType)
|
|
{
|
|
try
|
|
{
|
|
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
|
providerId ??= string.Empty;
|
|
|
|
string credentialResource = GetAICredentialResource(serviceType);
|
|
PasswordVault vault = new();
|
|
TryRemoveCredential(vault, credentialResource, GetPasteAICredentialUserName(providerId, serviceType));
|
|
TryRemoveCredential(vault, credentialResource, GetPasteAIEndpointCredentialUserName(providerId, serviceType));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
private static string NormalizeProviderIdentifier(string providerId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(providerId))
|
|
{
|
|
return "default";
|
|
}
|
|
|
|
var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray());
|
|
return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant();
|
|
}
|
|
|
|
private static bool RequiresCredentialStorage(string serviceType)
|
|
{
|
|
var serviceTypeKind = serviceType.ToAIServiceType();
|
|
|
|
return serviceTypeKind switch
|
|
{
|
|
AIServiceType.Onnx => false,
|
|
AIServiceType.Ollama => false,
|
|
AIServiceType.FoundryLocal => false,
|
|
AIServiceType.ML => false,
|
|
_ => true,
|
|
};
|
|
}
|
|
|
|
private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
|
|
{
|
|
try
|
|
{
|
|
PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
|
|
vault.Remove(existingCred);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Credential doesn't exist, which is fine
|
|
}
|
|
}
|
|
|
|
internal AdvancedPasteCustomAction GetNewCustomAction(string namePrefix)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(namePrefix);
|
|
|
|
var maxUsedPrefix = _customActions.Select(customAction => customAction.Name)
|
|
.Where(name => name.StartsWith(namePrefix, StringComparison.InvariantCulture))
|
|
.Select(name => int.TryParse(name.AsSpan(namePrefix.Length), out int number) ? number : 0)
|
|
.DefaultIfEmpty(0)
|
|
.Max();
|
|
|
|
var maxUsedId = _customActions.Select(customAction => customAction.Id)
|
|
.DefaultIfEmpty(-1)
|
|
.Max();
|
|
return new()
|
|
{
|
|
Id = maxUsedId + 1,
|
|
Name = $"{namePrefix} {maxUsedPrefix + 1}",
|
|
IsShown = true,
|
|
};
|
|
}
|
|
|
|
internal void AddCustomAction(AdvancedPasteCustomAction customAction)
|
|
{
|
|
if (_customActions.Any(existingCustomAction => existingCustomAction.Id == customAction.Id))
|
|
{
|
|
throw new ArgumentException("Duplicate custom action", nameof(customAction));
|
|
}
|
|
|
|
_customActions.Add(customAction);
|
|
}
|
|
|
|
internal void DeleteCustomAction(AdvancedPasteCustomAction customAction) => _customActions.Remove(customAction);
|
|
|
|
private void SaveCustomActions() => SaveAndNotifySettings();
|
|
|
|
private void SaveAndNotifySettings()
|
|
{
|
|
if (_suppressSave)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_settingsUtils.SaveSettings(_advancedPasteSettings.ToJsonString(), AdvancedPasteSettings.ModuleName);
|
|
NotifySettingsChanged();
|
|
}
|
|
|
|
private void OnAdditionalActionPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
SaveAndNotifySettings();
|
|
|
|
if (e.PropertyName == nameof(AdvancedPasteAdditionalAction.Shortcut))
|
|
{
|
|
OnPropertyChanged(nameof(IsAdditionalActionConflictingCopyShortcut));
|
|
}
|
|
}
|
|
|
|
private void OnCustomActionPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (!string.IsNullOrEmpty(e.PropertyName) && !CustomActionNonPersistedProperties.Contains(e.PropertyName))
|
|
{
|
|
SaveCustomActions();
|
|
}
|
|
|
|
if (e.PropertyName == nameof(AdvancedPasteCustomAction.Shortcut))
|
|
{
|
|
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
|
|
}
|
|
}
|
|
|
|
private void OnCustomActionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
void AddRange(System.Collections.IList items)
|
|
{
|
|
foreach (AdvancedPasteCustomAction item in items)
|
|
{
|
|
item.PropertyChanged += OnCustomActionPropertyChanged;
|
|
}
|
|
}
|
|
|
|
void RemoveRange(System.Collections.IList items)
|
|
{
|
|
foreach (AdvancedPasteCustomAction item in items)
|
|
{
|
|
item.PropertyChanged -= OnCustomActionPropertyChanged;
|
|
}
|
|
}
|
|
|
|
switch (e.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
AddRange(e.NewItems);
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Remove:
|
|
RemoveRange(e.OldItems);
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Replace:
|
|
AddRange(e.NewItems);
|
|
RemoveRange(e.OldItems);
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Move:
|
|
break;
|
|
|
|
default:
|
|
throw new ArgumentException($"Unsupported {nameof(e.Action)} {e.Action}", nameof(e));
|
|
}
|
|
|
|
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
|
|
UpdateCustomActionsCanMoveUpDown();
|
|
SaveCustomActions();
|
|
}
|
|
|
|
private void AttachConfigurationHandlers()
|
|
{
|
|
SubscribeToPasteAIConfiguration(_advancedPasteSettings.Properties.PasteAIConfiguration);
|
|
}
|
|
|
|
private void SetupSettingsFileWatcher()
|
|
{
|
|
_settingsWatcher = Helper.GetFileWatcher(AdvancedPasteSettings.ModuleName, SettingsUtils.DefaultFileName, OnSettingsFileChanged);
|
|
}
|
|
|
|
private void OnSettingsFileChanged()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
void Handler()
|
|
{
|
|
ApplyExternalSettings();
|
|
}
|
|
|
|
if (_dispatcherQueue is not null && !_dispatcherQueue.HasThreadAccess)
|
|
{
|
|
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, Handler);
|
|
}
|
|
else
|
|
{
|
|
Handler();
|
|
}
|
|
}
|
|
|
|
private void ApplyExternalSettings()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AdvancedPasteSettings latestSettings;
|
|
|
|
try
|
|
{
|
|
latestSettings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteSettings.ModuleName);
|
|
}
|
|
catch
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (latestSettings?.Properties is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_suppressSave = true;
|
|
ApplyExternalProperties(latestSettings.Properties);
|
|
}
|
|
finally
|
|
{
|
|
_suppressSave = false;
|
|
}
|
|
}
|
|
|
|
private void ApplyExternalProperties(AdvancedPasteProperties source)
|
|
{
|
|
var target = _advancedPasteSettings?.Properties;
|
|
|
|
if (target is null || source is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (target.IsAIEnabled != source.IsAIEnabled)
|
|
{
|
|
target.IsAIEnabled = source.IsAIEnabled;
|
|
OnPropertyChanged(nameof(IsAIEnabled));
|
|
}
|
|
|
|
if (target.ShowCustomPreview != source.ShowCustomPreview)
|
|
{
|
|
target.ShowCustomPreview = source.ShowCustomPreview;
|
|
OnPropertyChanged(nameof(ShowCustomPreview));
|
|
}
|
|
|
|
if (target.CloseAfterLosingFocus != source.CloseAfterLosingFocus)
|
|
{
|
|
target.CloseAfterLosingFocus = source.CloseAfterLosingFocus;
|
|
OnPropertyChanged(nameof(CloseAfterLosingFocus));
|
|
}
|
|
|
|
if (target.EnableClipboardPreview != source.EnableClipboardPreview)
|
|
{
|
|
target.EnableClipboardPreview = source.EnableClipboardPreview;
|
|
OnPropertyChanged(nameof(EnableClipboardPreview));
|
|
}
|
|
|
|
var incomingConfig = source.PasteAIConfiguration ?? new PasteAIConfiguration();
|
|
if (ShouldReplacePasteAIConfiguration(target.PasteAIConfiguration, incomingConfig))
|
|
{
|
|
PasteAIConfiguration = incomingConfig;
|
|
}
|
|
}
|
|
|
|
private static bool ShouldReplacePasteAIConfiguration(PasteAIConfiguration current, PasteAIConfiguration incoming)
|
|
{
|
|
if (incoming is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (current is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!string.Equals(current.ActiveProviderId ?? string.Empty, incoming.ActiveProviderId ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var currentProviders = current.Providers ?? new ObservableCollection<PasteAIProviderDefinition>();
|
|
var incomingProviders = incoming.Providers ?? new ObservableCollection<PasteAIProviderDefinition>();
|
|
|
|
if (currentProviders.Count != incomingProviders.Count)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
for (int i = 0; i < currentProviders.Count; i++)
|
|
{
|
|
var existing = currentProviders[i];
|
|
var updated = incomingProviders[i];
|
|
|
|
if (!string.Equals(existing?.Id ?? string.Empty, updated?.Id ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!string.Equals(existing?.ServiceType ?? string.Empty, updated?.ServiceType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!string.Equals(existing?.ModelName ?? string.Empty, updated?.ModelName ?? string.Empty, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!string.Equals(existing?.EndpointUrl ?? string.Empty, updated?.EndpointUrl ?? string.Empty, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!string.Equals(existing?.DeploymentName ?? string.Empty, updated?.DeploymentName ?? string.Empty, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!string.Equals(existing?.ApiVersion ?? string.Empty, updated?.ApiVersion ?? string.Empty, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!string.Equals(existing?.SystemPrompt ?? string.Empty, updated?.SystemPrompt ?? string.Empty, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (existing?.ModerationEnabled != updated?.ModerationEnabled || existing?.EnableAdvancedAI != updated?.EnableAdvancedAI || existing?.IsActive != updated?.IsActive)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void SubscribeToPasteAIConfiguration(PasteAIConfiguration configuration)
|
|
{
|
|
if (configuration is not null)
|
|
{
|
|
configuration.PropertyChanged += OnPasteAIConfigurationPropertyChanged;
|
|
SubscribeToPasteAIProviders(configuration);
|
|
}
|
|
}
|
|
|
|
private void UnsubscribeFromPasteAIConfiguration(PasteAIConfiguration configuration)
|
|
{
|
|
if (configuration is not null)
|
|
{
|
|
configuration.PropertyChanged -= OnPasteAIConfigurationPropertyChanged;
|
|
UnsubscribeFromPasteAIProviders(configuration);
|
|
}
|
|
}
|
|
|
|
private void SubscribeToPasteAIProviders(PasteAIConfiguration configuration)
|
|
{
|
|
if (configuration?.Providers is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
configuration.Providers.CollectionChanged -= OnPasteAIProvidersCollectionChanged;
|
|
configuration.Providers.CollectionChanged += OnPasteAIProvidersCollectionChanged;
|
|
|
|
foreach (var provider in configuration.Providers)
|
|
{
|
|
provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
|
|
provider.PropertyChanged += OnPasteAIProviderPropertyChanged;
|
|
}
|
|
}
|
|
|
|
private void UnsubscribeFromPasteAIProviders(PasteAIConfiguration configuration)
|
|
{
|
|
if (configuration?.Providers is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
configuration.Providers.CollectionChanged -= OnPasteAIProvidersCollectionChanged;
|
|
|
|
foreach (var provider in configuration.Providers)
|
|
{
|
|
provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
|
|
}
|
|
}
|
|
|
|
private void OnPasteAIProvidersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e?.NewItems is not null)
|
|
{
|
|
foreach (PasteAIProviderDefinition provider in e.NewItems)
|
|
{
|
|
provider.PropertyChanged += OnPasteAIProviderPropertyChanged;
|
|
}
|
|
}
|
|
|
|
if (e?.OldItems is not null)
|
|
{
|
|
foreach (PasteAIProviderDefinition provider in e.OldItems)
|
|
{
|
|
provider.PropertyChanged -= OnPasteAIProviderPropertyChanged;
|
|
}
|
|
}
|
|
|
|
var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
|
|
|
|
OnPropertyChanged(nameof(PasteAIConfiguration));
|
|
SaveAndNotifySettings();
|
|
}
|
|
|
|
private void OnPasteAIProviderPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (sender is PasteAIProviderDefinition provider)
|
|
{
|
|
// When service type changes we may need to update credentials entry names.
|
|
if (string.Equals(e.PropertyName, nameof(PasteAIProviderDefinition.ServiceType), StringComparison.Ordinal))
|
|
{
|
|
SaveAndNotifySettings();
|
|
return;
|
|
}
|
|
|
|
SaveAndNotifySettings();
|
|
}
|
|
}
|
|
|
|
private void OnPasteAIConfigurationPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.Providers), StringComparison.Ordinal))
|
|
{
|
|
SubscribeToPasteAIProviders(PasteAIConfiguration);
|
|
SaveAndNotifySettings();
|
|
return;
|
|
}
|
|
|
|
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal))
|
|
{
|
|
SaveAndNotifySettings();
|
|
}
|
|
}
|
|
|
|
private void InitializePasteAIProviderState()
|
|
{
|
|
var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
|
|
if (pasteConfig is null)
|
|
{
|
|
_advancedPasteSettings.Properties.PasteAIConfiguration = new PasteAIConfiguration();
|
|
pasteConfig = _advancedPasteSettings.Properties.PasteAIConfiguration;
|
|
}
|
|
|
|
pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
|
|
|
|
SubscribeToPasteAIProviders(pasteConfig);
|
|
}
|
|
|
|
private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(credentialResource) || string.IsNullOrWhiteSpace(credentialUserName))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
try
|
|
{
|
|
PasswordVault vault = new();
|
|
PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
|
|
existingCred?.RetrievePassword();
|
|
return existingCred?.Password?.Trim() ?? string.Empty;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
private void UpdateCustomActionsCanMoveUpDown()
|
|
{
|
|
for (int index = 0; index < _customActions.Count; index++)
|
|
{
|
|
var customAction = _customActions[index];
|
|
customAction.CanMoveUp = index != 0;
|
|
customAction.CanMoveDown = index != _customActions.Count - 1;
|
|
}
|
|
}
|
|
}
|
|
}
|