wire things up for adding a provider

This commit is contained in:
vanzue
2025-10-21 18:14:16 +08:00
parent 86e013df7e
commit d89198d26e
12 changed files with 1347 additions and 511 deletions

View File

@@ -116,6 +116,7 @@ namespace AdvancedPaste.Settings
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
AdvancedAIConfiguration = properties.AdvancedAIConfiguration ?? new AdvancedAIConfiguration();
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
PasteAIConfiguration.EnsureActiveProvider();
var sourceAdditionalActions = properties.AdditionalActions;
(PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats =

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -111,21 +112,23 @@ namespace AdvancedPaste.Services.CustomActions
private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config)
{
config ??= new PasteAIConfiguration();
var serviceType = NormalizeServiceType(config.ServiceTypeKind);
var systemPrompt = string.IsNullOrWhiteSpace(config.SystemPrompt) ? DefaultSystemPrompt : config.SystemPrompt;
var provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition();
var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
var systemPrompt = string.IsNullOrWhiteSpace(provider.SystemPrompt) ? DefaultSystemPrompt : provider.SystemPrompt;
var apiKey = AcquireApiKey(serviceType);
var modelName = config.ModelName;
var modelName = provider.ModelName;
var providerConfig = new PasteAIConfig
{
ProviderType = serviceType,
ApiKey = apiKey,
Model = modelName,
Endpoint = config.EndpointUrl,
DeploymentName = config.DeploymentName,
ModelPath = config.ModelPath,
Endpoint = provider.EndpointUrl,
DeploymentName = provider.DeploymentName,
LocalModelPath = provider.ModelPath,
ModelPath = provider.ModelPath,
SystemPrompt = systemPrompt,
ModerationEnabled = config.ModerationEnabled,
ModerationEnabled = provider.ModerationEnabled,
};
return providerConfig;

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AdvancedPaste.Settings;
using Microsoft.PowerToys.Settings.UI.Library;
using Windows.Security.Credentials;
@@ -20,6 +21,8 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
{
public AIServiceType ServiceType { get; set; } = AIServiceType.Unknown;
public string ProviderId { get; set; } = string.Empty;
public (string Resource, string Username)? Entry { get; set; }
public string Key { get; set; } = string.Empty;
@@ -68,14 +71,17 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
private bool UpdateSlot(AICredentialScope scope, bool forceRefresh)
{
var slot = _slots[scope];
var desiredServiceType = NormalizeServiceType(ResolveServiceType(scope));
var (serviceType, providerId) = ResolveCredentialTarget(scope);
var desiredServiceType = NormalizeServiceType(serviceType);
providerId ??= string.Empty;
var hasChanged = false;
if (slot.ServiceType != desiredServiceType)
if (slot.ServiceType != desiredServiceType || !string.Equals(slot.ProviderId, providerId, StringComparison.Ordinal))
{
slot.ServiceType = desiredServiceType;
slot.Entry = BuildCredentialEntry(desiredServiceType, scope);
slot.ProviderId = providerId;
slot.Entry = BuildCredentialEntry(desiredServiceType, providerId, scope);
forceRefresh = true;
hasChanged = true;
}
@@ -95,13 +101,13 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
return hasChanged;
}
private AIServiceType ResolveServiceType(AICredentialScope scope)
private (AIServiceType ServiceType, string ProviderId) ResolveCredentialTarget(AICredentialScope scope)
{
return scope switch
{
AICredentialScope.AdvancedAI => _userSettings.AdvancedAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI,
AICredentialScope.PasteAI => _userSettings.PasteAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI,
_ => AIServiceType.OpenAI,
AICredentialScope.AdvancedAI => (ResolveAdvancedAiServiceType(), string.Empty),
AICredentialScope.PasteAI => ResolvePasteAiServiceTarget(),
_ => (AIServiceType.OpenAI, string.Empty),
};
}
@@ -110,6 +116,22 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
}
private AIServiceType ResolveAdvancedAiServiceType()
{
return _userSettings.AdvancedAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI;
}
private (AIServiceType ServiceType, string ProviderId) ResolvePasteAiServiceTarget()
{
var provider = _userSettings.PasteAIConfiguration?.ActiveProvider;
if (provider is null)
{
return (AIServiceType.OpenAI, string.Empty);
}
return (provider.ServiceTypeKind, provider.Id ?? string.Empty);
}
private static string LoadKey((string Resource, string Username)? entry)
{
if (entry is null)
@@ -128,12 +150,12 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
}
}
private static (string Resource, string Username)? BuildCredentialEntry(AIServiceType serviceType, AICredentialScope scope)
private static (string Resource, string Username)? BuildCredentialEntry(AIServiceType serviceType, string providerId, AICredentialScope scope)
{
return scope switch
{
AICredentialScope.AdvancedAI => GetAdvancedAiEntry(serviceType),
AICredentialScope.PasteAI => GetPasteAiEntry(serviceType),
AICredentialScope.PasteAI => GetPasteAiEntry(serviceType, providerId),
_ => null,
};
}
@@ -155,20 +177,60 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
};
}
private static (string Resource, string Username)? GetPasteAiEntry(AIServiceType serviceType)
private static (string Resource, string Username)? GetPasteAiEntry(AIServiceType serviceType, string providerId)
{
return serviceType switch
string resource;
string serviceKey;
switch (serviceType)
{
AIServiceType.OpenAI => ("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_PasteAI_OpenAI"),
AIServiceType.AzureOpenAI => ("https://azure.microsoft.com/products/ai-services/openai-service", "PowerToys_AdvancedPaste_PasteAI_AzureOpenAI"),
AIServiceType.AzureAIInference => ("https://azure.microsoft.com/products/ai-services/ai-inference", "PowerToys_AdvancedPaste_PasteAI_AzureAIInference"),
AIServiceType.Mistral => ("https://console.mistral.ai/account/api-keys", "PowerToys_AdvancedPaste_PasteAI_Mistral"),
AIServiceType.Google => ("https://ai.google.dev/", "PowerToys_AdvancedPaste_PasteAI_Google"),
AIServiceType.HuggingFace => ("https://huggingface.co/settings/tokens", "PowerToys_AdvancedPaste_PasteAI_HuggingFace"),
AIServiceType.Ollama => null,
AIServiceType.Anthropic => null,
AIServiceType.AmazonBedrock => null,
_ => null,
};
case AIServiceType.OpenAI:
resource = "https://platform.openai.com/api-keys";
serviceKey = "openai";
break;
case AIServiceType.AzureOpenAI:
resource = "https://azure.microsoft.com/products/ai-services/openai-service";
serviceKey = "azureopenai";
break;
case AIServiceType.AzureAIInference:
resource = "https://azure.microsoft.com/products/ai-services/ai-inference";
serviceKey = "azureaiinference";
break;
case AIServiceType.Mistral:
resource = "https://console.mistral.ai/account/api-keys";
serviceKey = "mistral";
break;
case AIServiceType.Google:
resource = "https://ai.google.dev/";
serviceKey = "google";
break;
case AIServiceType.HuggingFace:
resource = "https://huggingface.co/settings/tokens";
serviceKey = "huggingface";
break;
case AIServiceType.FoundryLocal:
case AIServiceType.ML:
case AIServiceType.Onnx:
case AIServiceType.Ollama:
case AIServiceType.Anthropic:
case AIServiceType.AmazonBedrock:
return null;
default:
return null;
}
string username = $"PowerToys_AdvancedPaste_PasteAI_{serviceKey}_{NormalizeProviderIdentifier(providerId)}";
return (resource, username);
}
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();
}
}

View File

@@ -26,7 +26,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
"azureopenai" or "azure" => AIServiceType.AzureOpenAI,
"onnx" => AIServiceType.Onnx,
"foundrylocal" or "foundry" or "fl" => AIServiceType.FoundryLocal,
"ml" => AIServiceType.ML,
"ml" or "windowsml" or "winml" => AIServiceType.ML,
"mistral" => AIServiceType.Mistral,
"google" or "googleai" or "googlegemini" => AIServiceType.Google,
"huggingface" => AIServiceType.HuggingFace,

View File

@@ -0,0 +1,26 @@
// 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.Linq;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Metadata information for an AI service type.
/// </summary>
public class AIServiceTypeMetadata
{
public AIServiceType ServiceType { get; init; }
public string DisplayName { get; init; }
public string IconPath { get; init; }
public bool IsOnlineService { get; init; }
public bool IsAvailableInUI { get; init; } = true;
}
}

View File

@@ -0,0 +1,172 @@
// 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.Linq;
namespace Microsoft.PowerToys.Settings.UI.Library;
/// <summary>
/// Centralized registry for AI service type metadata.
/// </summary>
public static class AIServiceTypeRegistry
{
private static readonly Dictionary<AIServiceType, AIServiceTypeMetadata> MetadataMap = new()
{
[AIServiceType.OpenAI] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.OpenAI,
DisplayName = "OpenAI",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg",
IsOnlineService = true,
},
[AIServiceType.AzureOpenAI] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.AzureOpenAI,
DisplayName = "Azure OpenAI",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/AzureAI.svg",
IsOnlineService = true,
},
[AIServiceType.Mistral] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.Mistral,
DisplayName = "Mistral",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Mistral.svg",
IsOnlineService = true,
},
[AIServiceType.Google] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.Google,
DisplayName = "Google",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Gemini.svg",
IsOnlineService = true,
},
[AIServiceType.AzureAIInference] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.AzureAIInference,
DisplayName = "Azure AI Inference",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/AzureAI.svg",
IsOnlineService = true,
},
[AIServiceType.Ollama] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.Ollama,
DisplayName = "Ollama",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Ollama.svg",
IsOnlineService = true,
},
[AIServiceType.Anthropic] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.Anthropic,
DisplayName = "Anthropic",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Anthropic.svg",
IsOnlineService = true,
},
[AIServiceType.AmazonBedrock] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.AmazonBedrock,
DisplayName = "Amazon Bedrock",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Bedrock.svg",
IsOnlineService = true,
},
[AIServiceType.FoundryLocal] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.FoundryLocal,
DisplayName = "Foundry Local",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg",
IsOnlineService = false,
},
[AIServiceType.ML] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.ML,
DisplayName = "Windows ML",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/WindowsML.svg",
IsOnlineService = false,
},
[AIServiceType.HuggingFace] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.HuggingFace,
DisplayName = "Hugging Face",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/HuggingFace.svg",
IsOnlineService = true,
IsAvailableInUI = false, // Currently disabled in UI
},
[AIServiceType.Onnx] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.Onnx,
DisplayName = "ONNX",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Onnx.svg",
IsOnlineService = false,
IsAvailableInUI = false,
},
[AIServiceType.Unknown] = new AIServiceTypeMetadata
{
ServiceType = AIServiceType.Unknown,
DisplayName = "Unknown",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg",
IsOnlineService = false,
IsAvailableInUI = false,
},
};
/// <summary>
/// Get metadata for a specific service type.
/// </summary>
public static AIServiceTypeMetadata GetMetadata(AIServiceType serviceType)
{
return MetadataMap.TryGetValue(serviceType, out var metadata)
? metadata
: MetadataMap[AIServiceType.Unknown];
}
/// <summary>
/// Get metadata for a service type from its string representation.
/// </summary>
public static AIServiceTypeMetadata GetMetadata(string serviceType)
{
var type = serviceType.ToAIServiceType();
return GetMetadata(type);
}
/// <summary>
/// Get icon path for a service type.
/// </summary>
public static string GetIconPath(AIServiceType serviceType)
{
return GetMetadata(serviceType).IconPath;
}
/// <summary>
/// Get icon path for a service type from its string representation.
/// </summary>
public static string GetIconPath(string serviceType)
{
return GetMetadata(serviceType).IconPath;
}
/// <summary>
/// Get all service types available in the UI.
/// </summary>
public static IEnumerable<AIServiceTypeMetadata> GetAvailableServiceTypes()
{
return MetadataMap.Values.Where(m => m.IsAvailableInUI);
}
/// <summary>
/// Get all online service types available in the UI.
/// </summary>
public static IEnumerable<AIServiceTypeMetadata> GetOnlineServiceTypes()
{
return GetAvailableServiceTypes().Where(m => m.IsOnlineService);
}
/// <summary>
/// Get all local service types available in the UI.
/// </summary>
public static IEnumerable<AIServiceTypeMetadata> GetLocalServiceTypes()
{
return GetAvailableServiceTypes().Where(m => !m.IsOnlineService);
}
}

View File

@@ -4,7 +4,9 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -16,66 +18,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// </summary>
public class PasteAIConfiguration : INotifyPropertyChanged
{
private string _serviceType = "OpenAI";
private string _modelName = "gpt-3.5-turbo";
private string _endpointUrl = string.Empty;
private string _apiVersion = string.Empty;
private string _deploymentName = string.Empty;
private string _modelPath = string.Empty;
private string _activeProviderId = string.Empty;
private ObservableCollection<PasteAIProviderDefinition> _providers = new();
private bool _useSharedCredentials = true;
private string _systemPrompt = string.Empty;
private bool _moderationEnabled = true;
private Dictionary<string, AIProviderConfigurationSnapshot> _providerConfigurations = new(StringComparer.OrdinalIgnoreCase);
private string _legacyServiceType = "OpenAI";
private string _legacyModelName = "gpt-3.5-turbo";
private string _legacyEndpointUrl = string.Empty;
private string _legacyApiVersion = string.Empty;
private string _legacyDeploymentName = string.Empty;
private string _legacyModelPath = string.Empty;
private string _legacySystemPrompt = string.Empty;
private bool _legacyModerationEnabled = true;
private Dictionary<string, AIProviderConfigurationSnapshot> _legacyProviderConfigurations;
public event PropertyChangedEventHandler PropertyChanged;
[JsonPropertyName("service-type")]
public string ServiceType
[JsonPropertyName("active-provider-id")]
public string ActiveProviderId
{
get => _serviceType;
set => SetProperty(ref _serviceType, value);
get => _activeProviderId;
set => SetProperty(ref _activeProviderId, value ?? string.Empty);
}
[JsonIgnore]
public AIServiceType ServiceTypeKind
[JsonPropertyName("providers")]
public ObservableCollection<PasteAIProviderDefinition> Providers
{
get => _serviceType.ToAIServiceType();
set => ServiceType = value.ToConfigurationString();
}
[JsonPropertyName("model-name")]
public string ModelName
{
get => _modelName;
set => SetProperty(ref _modelName, value);
}
[JsonPropertyName("endpoint-url")]
public string EndpointUrl
{
get => _endpointUrl;
set => SetProperty(ref _endpointUrl, value);
}
[JsonPropertyName("api-version")]
public string ApiVersion
{
get => _apiVersion;
set => SetProperty(ref _apiVersion, value);
}
[JsonPropertyName("deployment-name")]
public string DeploymentName
{
get => _deploymentName;
set => SetProperty(ref _deploymentName, value);
}
[JsonPropertyName("model-path")]
public string ModelPath
{
get => _modelPath;
set => SetProperty(ref _modelPath, value);
get => _providers;
set => SetProperty(ref _providers, value ?? new ObservableCollection<PasteAIProviderDefinition>());
}
[JsonPropertyName("use-shared-credentials")]
@@ -85,49 +54,192 @@ namespace Microsoft.PowerToys.Settings.UI.Library
set => SetProperty(ref _useSharedCredentials, value);
}
[JsonPropertyName("system-prompt")]
public string SystemPrompt
// Legacy properties retained for migration. They will be cleared once converted to the new format.
[JsonPropertyName("service-type")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LegacyServiceType
{
get => _systemPrompt;
set => SetProperty(ref _systemPrompt, value?.Trim() ?? string.Empty);
get => _legacyServiceType;
set => _legacyServiceType = value;
}
[JsonPropertyName("model-name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LegacyModelName
{
get => _legacyModelName;
set => _legacyModelName = value;
}
[JsonPropertyName("endpoint-url")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LegacyEndpointUrl
{
get => _legacyEndpointUrl;
set => _legacyEndpointUrl = value;
}
[JsonPropertyName("api-version")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LegacyApiVersion
{
get => _legacyApiVersion;
set => _legacyApiVersion = value;
}
[JsonPropertyName("deployment-name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LegacyDeploymentName
{
get => _legacyDeploymentName;
set => _legacyDeploymentName = value;
}
[JsonPropertyName("model-path")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LegacyModelPath
{
get => _legacyModelPath;
set => _legacyModelPath = value;
}
[JsonPropertyName("system-prompt")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LegacySystemPrompt
{
get => _legacySystemPrompt;
set => _legacySystemPrompt = value;
}
[JsonPropertyName("moderation-enabled")]
public bool ModerationEnabled
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool LegacyModerationEnabled
{
get => _moderationEnabled;
set => SetProperty(ref _moderationEnabled, value);
get => _legacyModerationEnabled;
set => _legacyModerationEnabled = value;
}
[JsonPropertyName("provider-configurations")]
public Dictionary<string, AIProviderConfigurationSnapshot> ProviderConfigurations
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, AIProviderConfigurationSnapshot> LegacyProviderConfigurations
{
get => _providerConfigurations;
set => SetProperty(ref _providerConfigurations, value ?? new Dictionary<string, AIProviderConfigurationSnapshot>(StringComparer.OrdinalIgnoreCase));
get => _legacyProviderConfigurations;
set => _legacyProviderConfigurations = value;
}
public bool HasProviderConfiguration(string serviceType)
[JsonIgnore]
public PasteAIProviderDefinition ActiveProvider
{
return _providerConfigurations.ContainsKey(NormalizeServiceType(serviceType));
}
public AIProviderConfigurationSnapshot GetOrCreateProviderConfiguration(string serviceType)
{
var key = NormalizeServiceType(serviceType);
if (!_providerConfigurations.TryGetValue(key, out var snapshot))
get
{
snapshot = new AIProviderConfigurationSnapshot();
_providerConfigurations[key] = snapshot;
OnPropertyChanged(nameof(ProviderConfigurations));
if (_providers is null || _providers.Count == 0)
{
return null;
}
if (!string.IsNullOrWhiteSpace(_activeProviderId))
{
var match = _providers.FirstOrDefault(provider => string.Equals(provider.Id, _activeProviderId, StringComparison.OrdinalIgnoreCase));
if (match is not null)
{
return match;
}
}
return _providers[0];
}
}
[JsonIgnore]
public AIServiceType ActiveServiceTypeKind => ActiveProvider?.ServiceTypeKind ?? AIServiceType.OpenAI;
public void EnsureActiveProvider()
{
EnsureProvidersFromLegacyData();
if (_providers is null || _providers.Count == 0)
{
_activeProviderId = string.Empty;
return;
}
return snapshot;
if (string.IsNullOrWhiteSpace(_activeProviderId) || !_providers.Any(provider => string.Equals(provider.Id, _activeProviderId, StringComparison.OrdinalIgnoreCase)))
{
_activeProviderId = _providers[0].Id;
}
}
public void SetProviderConfiguration(string serviceType, AIProviderConfigurationSnapshot snapshot)
private void EnsureProvidersFromLegacyData()
{
_providerConfigurations[NormalizeServiceType(serviceType)] = snapshot ?? new AIProviderConfigurationSnapshot();
OnPropertyChanged(nameof(ProviderConfigurations));
_providers ??= new ObservableCollection<PasteAIProviderDefinition>();
if (_providers.Count > 0)
{
return;
}
bool migrated = false;
if (_legacyProviderConfigurations is not null && _legacyProviderConfigurations.Count > 0)
{
foreach (var kvp in _legacyProviderConfigurations)
{
var snapshot = kvp.Value ?? new AIProviderConfigurationSnapshot();
string serviceType = string.IsNullOrWhiteSpace(kvp.Key) ? _legacyServiceType ?? "OpenAI" : kvp.Key;
var provider = new PasteAIProviderDefinition
{
ServiceType = serviceType ?? "OpenAI",
ModelName = snapshot.ModelName ?? string.Empty,
EndpointUrl = snapshot.EndpointUrl ?? string.Empty,
ApiVersion = snapshot.ApiVersion ?? string.Empty,
DeploymentName = snapshot.DeploymentName ?? string.Empty,
ModelPath = snapshot.ModelPath ?? string.Empty,
SystemPrompt = snapshot.SystemPrompt ?? string.Empty,
ModerationEnabled = snapshot.ModerationEnabled,
};
_providers.Add(provider);
}
migrated = true;
}
else if (!string.IsNullOrWhiteSpace(_legacyServiceType)
|| !string.IsNullOrWhiteSpace(_legacyModelName)
|| !string.IsNullOrWhiteSpace(_legacyEndpointUrl)
|| !string.IsNullOrWhiteSpace(_legacyApiVersion)
|| !string.IsNullOrWhiteSpace(_legacyDeploymentName)
|| !string.IsNullOrWhiteSpace(_legacyModelPath)
|| !string.IsNullOrWhiteSpace(_legacySystemPrompt))
{
var provider = new PasteAIProviderDefinition
{
ServiceType = _legacyServiceType ?? "OpenAI",
ModelName = _legacyModelName ?? "gpt-3.5-turbo",
EndpointUrl = _legacyEndpointUrl ?? string.Empty,
ApiVersion = _legacyApiVersion ?? string.Empty,
DeploymentName = _legacyDeploymentName ?? string.Empty,
ModelPath = _legacyModelPath ?? string.Empty,
SystemPrompt = _legacySystemPrompt ?? string.Empty,
ModerationEnabled = _legacyModerationEnabled,
};
_providers.Add(provider);
migrated = true;
}
if (migrated)
{
_legacyServiceType = null;
_legacyModelName = null;
_legacyEndpointUrl = null;
_legacyApiVersion = null;
_legacyDeploymentName = null;
_legacyModelPath = null;
_legacySystemPrompt = null;
_legacyModerationEnabled = false;
_legacyProviderConfigurations = null;
}
}
public override string ToString()
@@ -149,10 +261,5 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private static string NormalizeServiceType(string serviceType)
{
return string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
}
}
}

View File

@@ -0,0 +1,142 @@
// 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.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Represents a single Paste AI provider configuration entry.
/// </summary>
public class PasteAIProviderDefinition : INotifyPropertyChanged
{
private string _id = Guid.NewGuid().ToString("N");
private string _serviceType = "OpenAI";
private string _modelName = string.Empty;
private string _endpointUrl = string.Empty;
private string _apiVersion = string.Empty;
private string _deploymentName = string.Empty;
private string _modelPath = string.Empty;
private string _systemPrompt = string.Empty;
private bool _moderationEnabled = true;
private bool _isActive;
public event PropertyChangedEventHandler PropertyChanged;
[JsonPropertyName("id")]
public string Id
{
get => _id;
set => SetProperty(ref _id, value);
}
[JsonPropertyName("service-type")]
public string ServiceType
{
get => _serviceType;
set => SetProperty(ref _serviceType, string.IsNullOrWhiteSpace(value) ? "OpenAI" : value);
}
[JsonIgnore]
public AIServiceType ServiceTypeKind
{
get => ServiceType.ToAIServiceType();
set => ServiceType = value.ToConfigurationString();
}
[JsonPropertyName("model-name")]
public string ModelName
{
get => _modelName;
set => SetProperty(ref _modelName, value ?? string.Empty);
}
[JsonPropertyName("endpoint-url")]
public string EndpointUrl
{
get => _endpointUrl;
set => SetProperty(ref _endpointUrl, value ?? string.Empty);
}
[JsonPropertyName("api-version")]
public string ApiVersion
{
get => _apiVersion;
set => SetProperty(ref _apiVersion, value ?? string.Empty);
}
[JsonPropertyName("deployment-name")]
public string DeploymentName
{
get => _deploymentName;
set => SetProperty(ref _deploymentName, value ?? string.Empty);
}
[JsonPropertyName("model-path")]
public string ModelPath
{
get => _modelPath;
set => SetProperty(ref _modelPath, value ?? string.Empty);
}
[JsonPropertyName("system-prompt")]
public string SystemPrompt
{
get => _systemPrompt;
set => SetProperty(ref _systemPrompt, value?.Trim() ?? string.Empty);
}
[JsonPropertyName("moderation-enabled")]
public bool ModerationEnabled
{
get => _moderationEnabled;
set => SetProperty(ref _moderationEnabled, value);
}
[JsonIgnore]
public bool IsActive
{
get => _isActive;
set => SetProperty(ref _isActive, value);
}
public PasteAIProviderDefinition Clone()
{
return new PasteAIProviderDefinition
{
Id = Id,
ServiceType = ServiceType,
ModelName = ModelName,
EndpointUrl = EndpointUrl,
ApiVersion = ApiVersion,
DeploymentName = DeploymentName,
ModelPath = ModelPath,
SystemPrompt = SystemPrompt,
ModerationEnabled = ModerationEnabled,
IsActive = IsActive,
};
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,30 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;
namespace Microsoft.PowerToys.Settings.UI.Converters;
public partial class ServiceTypeToIconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is not string serviceType || string.IsNullOrWhiteSpace(serviceType))
{
return new ImageIcon { Source = new SvgImageSource(new Uri(AIServiceTypeRegistry.GetIconPath(AIServiceType.OpenAI))) };
}
var iconPath = AIServiceTypeRegistry.GetIconPath(serviceType);
return new ImageIcon { Source = new SvgImageSource(new Uri(iconPath)) };
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -3,17 +3,24 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:Microsoft.PowerToys.Settings.UI.Library"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewmodels="using:Microsoft.PowerToys.Settings.UI.ViewModels"
x:Name="RootPage"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<local:NavigablePage.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<converters:ServiceTypeToIconConverter x:Key="ServiceTypeToIconConverter" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<ImageSource x:Key="DialogHeaderImage">ms-appx:///Assets/Settings/Modules/APDialog.dark.png</ImageSource>
@@ -125,100 +132,55 @@
<HyperlinkButton x:Uid="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore" NavigateUri="https://learn.microsoft.com/windows/powertoys/advanced-paste" />
</StackPanel>
</tkcontrols:SettingsExpander.Description>
<tkcontrols:SettingsExpander.ItemsHeader>
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard Description="Add online or local models (you can toggle these in the Advanced Paste UI)" Header="Model providers">
<StackPanel Orientation="Horizontal" Spacing="8">
<!-- Click="PasteAIProviderConfigureButton_Click" -->
<Button Content="Add model" Style="{StaticResource AccentButtonStyle}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsEnabled="False"
IsHitTestVisible="False"
Text="Online models" />
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/OpenAI.light.png}"
Tag="OpenAI"
Text="OpenAI" />
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/AzureAI.png}"
Tag="AzureOpenAI"
Text="Azure OpenAI" />
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/Mistral.png}"
Tag="Mistral"
Text="Mistral" />
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/Gemini.png}"
Tag="Google"
Text="Google" />
<!--<MenuFlyoutItem Text="Hugging Face"
Tag="HuggingFace"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/HuggingFace.png}"
Click="ProviderMenuFlyoutItem_Click" />-->
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/AzureAI.png}"
Tag="AzureAIInference"
Text="Azure AI Inference" />
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/Ollama.png}"
Tag="Ollama"
Text="Ollama" />
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/Anthropic.png}"
Tag="Anthropic"
Text="Anthropic" />
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/Bedrock.png}"
Tag="AmazonBedrock"
Text="Amazon Bedrock" />
<!-- Local models header -->
<MenuFlyoutItem
Margin="0,16,0,0"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsEnabled="False"
IsHitTestVisible="False"
Text="Local models" />
<!-- Local providers -->
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/FoundryLocal.png}"
Tag="FoundryLocal"
Text="Foundry Local" />
<MenuFlyoutItem
Click="ProviderMenuFlyoutItem_Click"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Models/WindowsML.png}"
Tag="WindowsML"
Text="Windows ML" />
</MenuFlyout>
<MenuFlyout x:Name="AddProviderMenuFlyout" Opening="AddProviderMenuFlyout_Opening" />
</Button.Flyout>
</Button>
</StackPanel>
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.ItemsHeader>
<tkcontrols:SettingsExpander.Items />
<ItemsControl
x:Name="PasteAIProvidersList"
Margin="0,2,0,0"
ItemsSource="{x:Bind ViewModel.PasteAIConfiguration.Providers, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:PasteAIProviderDefinition">
<tkcontrols:SettingsCard
Description="{x:Bind ServiceType, Mode=OneWay}"
Header="{x:Bind ModelName, Mode=OneWay}"
HeaderIcon="{x:Bind ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}">
<Button
Padding="8"
Background="Transparent"
BorderThickness="0"
Tag="{x:Bind}">
<FontIcon FontSize="16" Glyph="&#xE712;" />
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
Click="EditPasteAIProviderButton_Click"
Icon="{ui:FontIcon Glyph=&#xE70F;}"
Tag="{x:Bind}"
Text="Edit" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
Click="RemovePasteAIProviderButton_Click"
Icon="{ui:FontIcon Glyph=&#xE74D;}"
Tag="{x:Bind}"
Text="Remove" />
</MenuFlyout>
</Button.Flyout>
</Button>
</tkcontrols:SettingsCard>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</tkcontrols:SettingsExpander.Items>
<!--<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="...">
<controls:SettingsCard Description="{x:Bind ModelProvider}" Header="{x:Bind ModelName}">
@@ -682,6 +644,7 @@
<ContentDialog
x:Name="PasteAIProviderConfigurationDialog"
Title="Paste with AI provider configuration"
Closed="PasteAIProviderConfigurationDialog_Closed"
PrimaryButtonClick="PasteAIProviderConfigurationDialog_PrimaryButtonClick"
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
PrimaryButtonText="Save"
@@ -690,110 +653,13 @@
<x:Double x:Key="ContentDialogMaxWidth">900</x:Double>
<StaticResource x:Key="ContentDialogTopOverlay" ResourceKey="NavigationViewContentBackground" />
</ContentDialog.Resources>
<Grid
<StackPanel
MinWidth="720"
Padding="0,0,0,0"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="186" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle
Grid.ColumnSpan="2"
Height="1"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<ListView
x:Name="PasteAIServiceTypeListView"
Grid.Row="1"
Margin="0,4,0,0"
SelectedValue="{x:Bind ViewModel.PasteAIConfiguration.ServiceType, Mode=TwoWay}"
SelectedValuePath="Tag"
SelectionChanged="PasteAIServiceTypeListView_SelectionChanged">
<ListViewHeaderItem
Content="Cloud models"
IsEnabled="False"
IsHitTestVisible="False"
Style="{StaticResource ModelHeaderStyle}" />
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="OpenAI">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="{ThemeResource OpenAIIconImage}" />
<TextBlock Text="OpenAI" />
</StackPanel>
</ListViewItem>
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="AzureOpenAI">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/Azure.svg" />
<TextBlock Text="Azure OpenAI" />
</StackPanel>
</ListViewItem>
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="Mistral">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/Mistral.svg" />
<TextBlock Text="Mistral" />
</StackPanel>
</ListViewItem>
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="Google">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/Gemini.svg" />
<TextBlock Text="Google" />
</StackPanel>
</ListViewItem>
<!--<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="HuggingFace">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/HuggingFace.svg" />
<TextBlock Text="Hugging Face" />
</StackPanel>
</ListViewItem>-->
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="AzureAIInference">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/AzureAI.svg" />
<TextBlock Text="Azure AI Inference" />
</StackPanel>
</ListViewItem>
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="Ollama">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/Ollama.svg" />
<TextBlock Text="Ollama" />
</StackPanel>
</ListViewItem>
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="Anthropic">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/Anthropic.svg" />
<TextBlock Text="Anthropic" />
</StackPanel>
</ListViewItem>
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="AmazonBedrock">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/Bedrock.svg" />
<TextBlock Text="Amazon Bedrock" />
</StackPanel>
</ListViewItem>
<ListViewHeaderItem
Margin="0,12,0,0"
Content="Local models"
IsEnabled="False"
IsHitTestVisible="False"
Style="{StaticResource ModelHeaderStyle}" />
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="FoundryLocal">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/FoundryLocal.svg" />
<TextBlock Text="Foundry Local" />
</StackPanel>
</ListViewItem>
<ListViewItem Style="{StaticResource DefaultListViewItemStyle}" Tag="WindowsML">
<StackPanel Orientation="Horizontal" Spacing="12">
<Image Width="16" Source="/Assets/Settings/Icons/Models/WindowsML.svg" />
<TextBlock Text="Windows ML" />
</StackPanel>
</ListViewItem>
</ListView>
<ScrollViewer Grid.Row="1" Grid.Column="1">
Spacing="16">
<Rectangle Height="1" Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<ScrollViewer>
<StackPanel
Margin="0,16,0,0"
Margin="0,8,0,0"
Orientation="Vertical"
Spacing="24">
<TextBox
@@ -802,7 +668,7 @@
HorizontalAlignment="Stretch"
Header="Model name"
PlaceholderText="gpt-4"
Text="{x:Bind ViewModel.PasteAIConfiguration.ModelName, Mode=TwoWay}" />
Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelName, Mode=TwoWay}" />
<TextBox
x:Name="PasteAISystemPromptTextBox"
MinWidth="200"
@@ -811,7 +677,7 @@
AcceptsReturn="True"
Header="System prompt"
PlaceholderText="You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it. Do not output anything else besides the reformatted clipboard content."
Text="{x:Bind ViewModel.PasteAIConfiguration.SystemPrompt, Mode=TwoWay}"
Text="{x:Bind ViewModel.PasteAIProviderDraft.SystemPrompt, Mode=TwoWay}"
TextWrapping="Wrap" />
<Grid
x:Name="FoundryLocalPanel"
@@ -825,21 +691,21 @@
HorizontalAlignment="Stretch"
Header="Endpoint URL"
PlaceholderText="https://your-resource.openai.azure.com/"
Text="{x:Bind ViewModel.PasteAIConfiguration.EndpointUrl, Mode=TwoWay}" />
Text="{x:Bind ViewModel.PasteAIProviderDraft.EndpointUrl, Mode=TwoWay}" />
<TextBox
x:Name="PasteAIApiVersionTextBox"
MinWidth="200"
HorizontalAlignment="Stretch"
Header="API version"
PlaceholderText="2024-10-01"
Text="{x:Bind ViewModel.PasteAIConfiguration.ApiVersion, Mode=TwoWay}"
Text="{x:Bind ViewModel.PasteAIProviderDraft.ApiVersion, Mode=TwoWay}"
Visibility="Collapsed" />
<TextBox
x:Name="PasteAIDeploymentNameTextBox"
MinWidth="200"
Header="Deployment name"
PlaceholderText="gpt-4"
Text="{x:Bind ViewModel.PasteAIConfiguration.DeploymentName, Mode=TwoWay}" />
Text="{x:Bind ViewModel.PasteAIProviderDraft.DeploymentName, Mode=TwoWay}" />
<StackPanel
x:Name="PasteAIModelPanel"
Orientation="Horizontal"
@@ -850,7 +716,7 @@
HorizontalAlignment="Stretch"
Header="Model path"
PlaceholderText="C:\Models\phi-3.onnx"
Text="{x:Bind ViewModel.PasteAIConfiguration.ModelPath, Mode=TwoWay}" />
Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelPath, Mode=TwoWay}" />
<Button
VerticalAlignment="Bottom"
Click="BrowsePasteAIModelPath_Click"
@@ -867,35 +733,35 @@
<CheckBox
x:Name="PasteAIModerationToggle"
x:Uid="AdvancedPaste_EnablePasteAIModerationToggle"
IsChecked="{x:Bind ViewModel.PasteAIConfiguration.ModerationEnabled, Mode=TwoWay}"
IsChecked="{x:Bind ViewModel.PasteAIProviderDraft.ModerationEnabled, Mode=TwoWay}"
Visibility="Collapsed" />
<StackPanel
Margin="0,12,0,0"
Orientation="Vertical"
Spacing="8"
Visibility="{x:Bind GetServiceLegalVisibility(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}">
Visibility="{x:Bind GetServiceLegalVisibility(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind GetServiceLegalDescription(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}"
Text="{x:Bind GetServiceLegalDescription(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
TextWrapping="Wrap" />
<StackPanel
Margin="-12,0,0,0"
Orientation="Horizontal"
Spacing="8">
<HyperlinkButton
Content="{x:Bind GetServiceTermsLabel(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}"
NavigateUri="{x:Bind GetServiceTermsUri(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}"
Visibility="{x:Bind GetServiceTermsVisibility(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}" />
Content="{x:Bind GetServiceTermsLabel(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
NavigateUri="{x:Bind GetServiceTermsUri(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
Visibility="{x:Bind GetServiceTermsVisibility(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}" />
<HyperlinkButton
Content="{x:Bind GetServicePrivacyLabel(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}"
NavigateUri="{x:Bind GetServicePrivacyUri(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}"
Visibility="{x:Bind GetServicePrivacyVisibility(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}" />
Content="{x:Bind GetServicePrivacyLabel(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
NavigateUri="{x:Bind GetServicePrivacyUri(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}"
Visibility="{x:Bind GetServicePrivacyVisibility(ViewModel.PasteAIProviderDraft?.ServiceType), Mode=OneWay}" />
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</StackPanel>
</ContentDialog>
</Grid>
</local:NavigablePage>

View File

@@ -19,6 +19,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
namespace Microsoft.PowerToys.Settings.UI.Views
{
@@ -30,6 +31,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private bool _suppressFoundrySelectionChanged;
private bool _isFoundryLocalAvailable;
private bool _disposed;
private const string PasteAiDialogDefaultTitle = "Paste with AI provider configuration";
private static readonly Dictionary<string, ServiceLegalInfo> ServiceLegalInformation = new(StringComparer.OrdinalIgnoreCase)
{
@@ -253,7 +255,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
if (!string.IsNullOrEmpty(selectedFile))
{
PasteAIModelPathTextBox.Text = selectedFile;
ViewModel.PasteAIConfiguration.ModelPath = selectedFile;
if (ViewModel?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIProviderDraft.ModelPath = selectedFile;
}
}
}
@@ -296,47 +301,54 @@ namespace Microsoft.PowerToys.Settings.UI.Views
System.Diagnostics.Debug.WriteLine($"{configType} API key saved successfully");
}
private async void PasteAIServiceTypeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync();
}
private void UpdatePasteAIUIVisibility()
{
if (PasteAIServiceTypeListView?.SelectedValue == null)
var draft = ViewModel?.PasteAIProviderDraft;
if (draft is null)
{
return;
}
string selectedType = PasteAIServiceTypeListView.SelectedValue.ToString();
string selectedType = draft.ServiceType ?? string.Empty;
AIServiceType serviceKind = draft.ServiceTypeKind;
bool isOnnx = string.Equals(selectedType, "Onnx", StringComparison.OrdinalIgnoreCase);
bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
bool showEndpoint = string.Equals(selectedType, "AzureOpenAI", StringComparison.OrdinalIgnoreCase)
|| string.Equals(selectedType, "AzureAIInference", StringComparison.OrdinalIgnoreCase)
|| string.Equals(selectedType, "Mistral", StringComparison.OrdinalIgnoreCase)
|| string.Equals(selectedType, "HuggingFace", StringComparison.OrdinalIgnoreCase)
|| string.Equals(selectedType, "Ollama", StringComparison.OrdinalIgnoreCase);
bool showDeployment = string.Equals(selectedType, "AzureOpenAI", StringComparison.OrdinalIgnoreCase);
bool requiresEndpoint = serviceKind is AIServiceType.AzureOpenAI
or AIServiceType.AzureAIInference
or AIServiceType.Mistral
or AIServiceType.HuggingFace
or AIServiceType.Ollama;
bool requiresDeployment = serviceKind == AIServiceType.AzureOpenAI;
bool requiresApiVersion = serviceKind == AIServiceType.AzureOpenAI;
bool requiresModelPath = serviceKind == AIServiceType.Onnx;
bool isFoundryLocal = serviceKind == AIServiceType.FoundryLocal;
bool requiresApiKey = RequiresApiKeyForService(selectedType);
bool showModerationToggle = string.Equals(selectedType, "OpenAI", StringComparison.OrdinalIgnoreCase);
bool showModerationToggle = serviceKind == AIServiceType.OpenAI;
if (ViewModel.PasteAIConfiguration is not null)
if (string.IsNullOrWhiteSpace(draft.EndpointUrl))
{
ViewModel.PasteAIConfiguration.EndpointUrl = ViewModel.GetPasteAIEndpoint(selectedType);
string storedEndpoint = ViewModel.GetPasteAIEndpoint(draft.Id, selectedType);
if (!string.IsNullOrWhiteSpace(storedEndpoint))
{
draft.EndpointUrl = storedEndpoint;
}
}
PasteAIEndpointUrlTextBox.Visibility = showEndpoint ? Visibility.Visible : Visibility.Collapsed;
PasteAIDeploymentNameTextBox.Visibility = showDeployment ? Visibility.Visible : Visibility.Collapsed;
PasteAIModelPanel.Visibility = isOnnx ? Visibility.Visible : Visibility.Collapsed;
PasteAIEndpointUrlTextBox.Visibility = requiresEndpoint ? Visibility.Visible : Visibility.Collapsed;
if (requiresEndpoint)
{
PasteAIEndpointUrlTextBox.PlaceholderText = GetEndpointPlaceholder(serviceKind);
}
PasteAIDeploymentNameTextBox.Visibility = requiresDeployment ? Visibility.Visible : Visibility.Collapsed;
PasteAIApiVersionTextBox.Visibility = requiresApiVersion ? Visibility.Visible : Visibility.Collapsed;
PasteAIModelPanel.Visibility = requiresModelPath ? Visibility.Visible : Visibility.Collapsed;
PasteAIModerationToggle.Visibility = showModerationToggle ? Visibility.Visible : Visibility.Collapsed;
PasteAIApiKeyPasswordBox.Visibility = requiresApiKey ? Visibility.Visible : Visibility.Collapsed;
PasteAIModelNameTextBox.Visibility = isFoundryLocal ? Visibility.Collapsed : Visibility.Visible;
if (requiresApiKey)
{
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(selectedType);
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(draft.Id, selectedType);
}
else
{
@@ -384,13 +396,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private Task UpdateFoundryLocalUIAsync(bool refreshFoundry = false)
{
if (PasteAIServiceTypeListView?.SelectedValue == null)
{
return Task.CompletedTask;
}
string selectedType = PasteAIServiceTypeListView.SelectedValue.ToString();
bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.Ordinal);
string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
if (FoundryLocalPanel is not null)
{
@@ -409,11 +416,18 @@ namespace Microsoft.PowerToys.Settings.UI.Views
FoundryLocalPicker.SelectedModel = null;
}
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
if (PasteAIProviderConfigurationDialog is not null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
}
return Task.CompletedTask;
}
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
if (PasteAIProviderConfigurationDialog is not null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
}
FoundryLocalPicker?.RequestLoad(refreshFoundry);
@@ -565,7 +579,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return;
}
var currentModelReference = ViewModel?.PasteAIConfiguration?.ModelName;
var currentModelReference = ViewModel?.PasteAIProviderDraft?.ModelName;
ModelDetails matchingModel = null;
@@ -587,9 +601,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
if (matchingModel is null)
{
if (ViewModel?.PasteAIConfiguration is not null)
if (ViewModel?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIConfiguration.ModelName = string.Empty;
ViewModel.PasteAIProviderDraft.ModelName = string.Empty;
}
if (FoundryLocalPicker is not null)
@@ -601,9 +615,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
else
{
if (ViewModel?.PasteAIConfiguration is not null)
if (ViewModel?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIConfiguration.ModelName = NormalizeFoundryModelReference(matchingModel.Url ?? matchingModel.Name);
ViewModel.PasteAIProviderDraft.ModelName = NormalizeFoundryModelReference(matchingModel.Url ?? matchingModel.Name);
}
if (FoundryLocalPicker is not null)
@@ -635,9 +649,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return;
}
bool isFoundrySelected = string.Equals(PasteAIServiceTypeListView?.SelectedValue?.ToString(), "FoundryLocal", StringComparison.Ordinal);
bool isFoundrySelected = string.Equals(ViewModel?.PasteAIProviderDraft?.ServiceType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
if (!isFoundrySelected)
if (!isFoundrySelected || ViewModel?.PasteAIProviderDraft is null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
return;
@@ -662,9 +676,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
if (selectedModel is not null)
{
if (ViewModel?.PasteAIConfiguration is not null)
if (ViewModel?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIConfiguration.ModelName = NormalizeFoundryModelReference(selectedModel.Url ?? selectedModel.Name);
ViewModel.PasteAIProviderDraft.ModelName = NormalizeFoundryModelReference(selectedModel.Url ?? selectedModel.Name);
}
if (FoundryLocalPicker is not null)
@@ -674,9 +688,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
else
{
if (ViewModel?.PasteAIConfiguration is not null)
if (ViewModel?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIConfiguration.ModelName = string.Empty;
ViewModel.PasteAIProviderDraft.ModelName = string.Empty;
}
if (FoundryLocalPicker is not null)
@@ -814,10 +828,17 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void PasteAIProviderConfigurationDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
string serviceType = PasteAIServiceTypeListView.SelectedValue?.ToString() ?? "OpenAI";
var draft = ViewModel?.PasteAIProviderDraft;
if (draft is null)
{
args.Cancel = true;
return;
}
string serviceType = draft.ServiceType ?? "OpenAI";
string apiKey = PasteAIApiKeyPasswordBox.Password;
string trimmedApiKey = apiKey?.Trim() ?? string.Empty;
string endpoint = (ViewModel.PasteAIConfiguration.EndpointUrl ?? string.Empty).Trim();
string endpoint = (draft.EndpointUrl ?? string.Empty).Trim();
if (RequiresApiKeyForService(serviceType) && string.IsNullOrWhiteSpace(trimmedApiKey))
{
@@ -825,10 +846,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return;
}
ViewModel.PasteAIConfiguration.EndpointUrl = endpoint;
ViewModel.SavePasteAICredential(serviceType, endpoint, trimmedApiKey);
ViewModel.PasteAIConfiguration.EndpointUrl = ViewModel.GetPasteAIEndpoint(serviceType);
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(serviceType);
ViewModel.CommitPasteAIProviderDraft(trimmedApiKey, endpoint);
PasteAIApiKeyPasswordBox.Password = string.Empty;
// Show success message
ShowApiKeySavedMessage("Paste AI");
@@ -847,27 +866,31 @@ namespace Microsoft.PowerToys.Settings.UI.Views
RefreshDialogBindings();
}
private async void PasteAIServiceTypeListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync(refreshFoundry: true);
RefreshDialogBindings();
}
private static bool RequiresApiKeyForService(string serviceType)
{
if (string.IsNullOrWhiteSpace(serviceType))
{
return true;
}
var serviceKind = serviceType.ToAIServiceType();
return serviceType.Equals("Onnx", StringComparison.OrdinalIgnoreCase)
? false
: !serviceType.Equals("Ollama", StringComparison.OrdinalIgnoreCase)
&& !serviceType.Equals("FoundryLocal", StringComparison.OrdinalIgnoreCase)
&& !serviceType.Equals("WindowsML", StringComparison.OrdinalIgnoreCase)
&& !serviceType.Equals("Anthropic", StringComparison.OrdinalIgnoreCase)
&& !serviceType.Equals("AmazonBedrock", StringComparison.OrdinalIgnoreCase);
return serviceKind switch
{
AIServiceType.Onnx => false,
AIServiceType.Ollama => false,
AIServiceType.FoundryLocal => false,
AIServiceType.ML => false,
_ => true,
};
}
private static string GetEndpointPlaceholder(AIServiceType serviceKind)
{
return serviceKind switch
{
AIServiceType.AzureOpenAI => "https://your-resource.openai.azure.com/",
AIServiceType.AzureAIInference => "https://{resource-name}.cognitiveservices.azure.com/",
AIServiceType.Mistral => "https://api.mistral.ai/v1/",
AIServiceType.HuggingFace => "https://api-inference.huggingface.co/models/",
AIServiceType.Ollama => "http://localhost:11434/",
_ => "https://your-resource.openai.azure.com/",
};
}
private bool HasServiceLegalInfo(string serviceType) => TryGetServiceLegalInfo(serviceType, out _);
@@ -977,12 +1000,155 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
private void ProviderMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
private void AddProviderMenuFlyout_Opening(object sender, object e)
{
if (sender is MenuFlyoutItem menuItem && menuItem.Tag is string tag)
if (sender is not MenuFlyout menuFlyout)
{
// TODO: Open dialog and set the right fields
return;
}
// Clear existing items
menuFlyout.Items.Clear();
// Add online models header
var onlineHeader = new MenuFlyoutItem
{
Text = "Online models",
FontSize = 12,
Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["TextFillColorSecondaryBrush"],
IsEnabled = false,
IsHitTestVisible = false,
};
menuFlyout.Items.Add(onlineHeader);
// Add online providers
foreach (var metadata in AIServiceTypeRegistry.GetOnlineServiceTypes())
{
var menuItem = new MenuFlyoutItem
{
Text = metadata.DisplayName,
Tag = metadata.ServiceType.ToConfigurationString(),
Icon = new ImageIcon { Source = new SvgImageSource(new Uri(metadata.IconPath)) },
};
menuItem.Click += ProviderMenuFlyoutItem_Click;
menuFlyout.Items.Add(menuItem);
}
// Add local models header
var localHeader = new MenuFlyoutItem
{
Text = "Local models",
FontSize = 12,
Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["TextFillColorSecondaryBrush"],
IsEnabled = false,
IsHitTestVisible = false,
Margin = new Thickness(0, 16, 0, 0),
};
menuFlyout.Items.Add(localHeader);
// Add local providers
foreach (var metadata in AIServiceTypeRegistry.GetLocalServiceTypes())
{
var menuItem = new MenuFlyoutItem
{
Text = metadata.DisplayName,
Tag = metadata.ServiceType.ToConfigurationString(),
Icon = new ImageIcon { Source = new SvgImageSource(new Uri(metadata.IconPath)) },
};
menuItem.Click += ProviderMenuFlyoutItem_Click;
menuFlyout.Items.Add(menuItem);
}
}
private async void ProviderMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not string tag || string.IsNullOrWhiteSpace(tag))
{
return;
}
if (ViewModel is null || PasteAIProviderConfigurationDialog is null)
{
return;
}
string serviceType = tag.Trim();
string displayName = string.IsNullOrWhiteSpace(menuItem.Text) ? serviceType : menuItem.Text.Trim();
ViewModel.BeginAddPasteAIProvider(serviceType);
if (ViewModel.PasteAIProviderDraft is null)
{
return;
}
PasteAIProviderConfigurationDialog.Title = PasteAiDialogDefaultTitle;
if (!string.IsNullOrWhiteSpace(displayName))
{
PasteAIProviderConfigurationDialog.Title = $"{displayName} provider configuration";
}
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync(refreshFoundry: true);
RefreshDialogBindings();
PasteAIApiKeyPasswordBox.Password = string.Empty;
await PasteAIProviderConfigurationDialog.ShowAsync();
}
private async void EditPasteAIProviderButton_Click(object sender, RoutedEventArgs e)
{
// sender is MenuFlyoutItem with PasteAIProviderDefinition Tag
if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
{
return;
}
if (ViewModel is null || PasteAIProviderConfigurationDialog is null)
{
return;
}
ViewModel.BeginEditPasteAIProvider(provider);
string titlePrefix = string.IsNullOrWhiteSpace(provider.ModelName) ? provider.ServiceType : provider.ModelName;
PasteAIProviderConfigurationDialog.Title = string.IsNullOrWhiteSpace(titlePrefix)
? PasteAiDialogDefaultTitle
: $"{titlePrefix} provider configuration";
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync(refreshFoundry: false);
RefreshDialogBindings();
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
await PasteAIProviderConfigurationDialog.ShowAsync();
}
private void RemovePasteAIProviderButton_Click(object sender, RoutedEventArgs e)
{
// sender is MenuFlyoutItem with PasteAIProviderDefinition Tag
if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
{
return;
}
ViewModel?.RemovePasteAIProvider(provider);
}
private void SetActivePasteAIProvider_Click(object sender, RoutedEventArgs e)
{
// sender is MenuFlyoutItem with PasteAIProviderDefinition Tag
if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
{
return;
}
ViewModel?.SetActivePasteAIProvider(provider);
}
private void PasteAIProviderConfigurationDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
ViewModel?.CancelPasteAIProviderDraft();
PasteAIProviderConfigurationDialog.Title = PasteAiDialogDefaultTitle;
PasteAIApiKeyPasswordBox.Password = string.Empty;
}
}
}

View File

@@ -37,20 +37,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
nameof(AdvancedAIConfiguration.ModerationEnabled),
};
private static readonly HashSet<string> PasteAITrackedProperties = new(StringComparer.Ordinal)
{
nameof(PasteAIConfiguration.ModelName),
nameof(PasteAIConfiguration.EndpointUrl),
nameof(PasteAIConfiguration.ApiVersion),
nameof(PasteAIConfiguration.DeploymentName),
nameof(PasteAIConfiguration.ModelPath),
nameof(PasteAIConfiguration.SystemPrompt),
nameof(PasteAIConfiguration.ModerationEnabled),
};
private bool _disposed;
private bool _isLoadingAdvancedAIProviderConfiguration;
private bool _isLoadingPasteAIProviderConfiguration;
private PasteAIProviderDefinition _pasteAIProviderDraft;
private PasteAIProviderDefinition _editingPasteAIProvider;
protected override string ModuleName => AdvancedPasteSettings.ModuleName;
@@ -90,6 +80,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
SeedProviderConfigurationSnapshots();
_advancedPasteSettings?.Properties?.PasteAIConfiguration?.EnsureActiveProvider();
AttachConfigurationHandlers();
@@ -100,7 +91,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
LoadAdvancedAIProviderConfiguration();
LoadPasteAIProviderConfiguration();
InitializePasteAIProviderState();
InitializeEnabledValue();
MigrateLegacyAIEnablement();
@@ -221,6 +212,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
public static IEnumerable<AIServiceTypeMetadata> AvailableProviders => AIServiceTypeRegistry.GetAvailableServiceTypes();
public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO;
private bool LegacyOpenAIKeyExists()
@@ -234,7 +227,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (legacyOpenAIKey != null)
{
string credentialResource = GetAICredentialResource("OpenAI");
string credentialUserName = GetPasteAICredentialUserName("OpenAI");
var targetProvider = PasteAIConfiguration?.ActiveProvider ?? PasteAIConfiguration?.Providers?.FirstOrDefault();
string providerId = targetProvider?.Id ?? string.Empty;
string serviceType = targetProvider?.ServiceType ?? "OpenAI";
string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType);
PasswordCredential cred = new(credentialResource, credentialUserName, legacyOpenAIKey.Password);
vault.Add(cred);
@@ -440,6 +436,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var newValue = value ?? new PasteAIConfiguration();
_advancedPasteSettings.Properties.PasteAIConfiguration = newValue;
SubscribeToPasteAIConfiguration(newValue);
newValue?.EnsureActiveProvider();
UpdateActivePasteAIProviderFlags();
OnPropertyChanged(nameof(PasteAIConfiguration));
SaveAndNotifySettings();
@@ -447,6 +445,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public PasteAIProviderDefinition PasteAIProviderDraft
{
get => _pasteAIProviderDraft;
private set
{
if (!ReferenceEquals(_pasteAIProviderDraft, value))
{
_pasteAIProviderDraft = value;
OnPropertyChanged(nameof(PasteAIProviderDraft));
}
}
}
public bool IsEditingPasteAIProvider => _editingPasteAIProvider is not null;
public bool ShowCustomPreview
{
get => _advancedPasteSettings.Properties.ShowCustomPreview;
@@ -505,6 +518,179 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(IsAIEnabled));
}
public void BeginAddPasteAIProvider(string serviceType)
{
var normalizedServiceType = NormalizeServiceType(serviceType, out var persistedServiceType);
var provider = new PasteAIProviderDefinition
{
ServiceType = persistedServiceType,
ModelName = GetDefaultModelName(normalizedServiceType),
EndpointUrl = string.Empty,
ApiVersion = string.Empty,
DeploymentName = string.Empty,
ModelPath = string.Empty,
SystemPrompt = string.Empty,
ModerationEnabled = normalizedServiceType == AIServiceType.OpenAI,
};
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;
}
private static string GetDefaultModelName(AIServiceType serviceType)
{
return serviceType switch
{
AIServiceType.OpenAI => "gpt-4",
AIServiceType.AzureOpenAI => "gpt-4",
AIServiceType.Mistral => "mistral-large-latest",
AIServiceType.Google => "gemini-1.5-pro",
AIServiceType.AzureAIInference => "gpt-4o-mini",
AIServiceType.Ollama => "llama3",
AIServiceType.Anthropic => "claude-3-5-sonnet",
AIServiceType.AmazonBedrock => "anthropic.claude-3-haiku",
_ => string.Empty,
};
}
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;
SavePasteAICredential(draft.Id, draft.ServiceType, draft.EndpointUrl, apiKey);
if (_editingPasteAIProvider is null)
{
config.Providers.Add(draft);
config.ActiveProviderId ??= draft.Id;
}
else
{
UpdateProviderFromDraft(_editingPasteAIProvider, draft);
_editingPasteAIProvider = null;
}
config.EnsureActiveProvider();
UpdateActivePasteAIProviderFlags();
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);
config.EnsureActiveProvider();
UpdateActivePasteAIProviderFlags();
SaveAndNotifySettings();
OnPropertyChanged(nameof(PasteAIConfiguration));
}
}
public void SetActivePasteAIProvider(PasteAIProviderDefinition provider)
{
if (provider is null)
{
return;
}
var config = PasteAIConfiguration;
if (config is null)
{
return;
}
if (!string.Equals(config.ActiveProviderId, provider.Id, StringComparison.OrdinalIgnoreCase))
{
config.ActiveProviderId = provider.Id;
UpdateActivePasteAIProviderFlags();
SaveAndNotifySettings();
OnPropertyChanged(nameof(PasteAIConfiguration));
}
}
public bool IsActivePasteAIProvider(string providerId)
{
var activeId = PasteAIConfiguration?.ActiveProviderId ?? string.Empty;
providerId ??= string.Empty;
return string.Equals(activeId, providerId, StringComparison.OrdinalIgnoreCase);
}
public bool CanSetActivePasteAIProvider(string providerId) => !IsActivePasteAIProvider(providerId);
protected override void Dispose(bool disposing)
{
if (!_disposed)
@@ -630,15 +816,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
internal void SavePasteAICredential(string serviceType, string endpoint, string apiKey)
internal void SavePasteAICredential(string providerId, string serviceType, string endpoint, string apiKey)
{
try
{
endpoint = endpoint?.Trim() ?? string.Empty;
apiKey = apiKey?.Trim() ?? string.Empty;
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
providerId ??= string.Empty;
string credentialResource = GetAICredentialResource(serviceType);
string credentialUserName = GetPasteAICredentialUserName(serviceType);
string endpointCredentialUserName = GetPasteAIEndpointCredentialUserName(serviceType);
string credentialUserName = GetPasteAICredentialUserName(providerId, serviceType);
string endpointCredentialUserName = GetPasteAIEndpointCredentialUserName(providerId, serviceType);
PasswordVault vault = new();
TryRemoveCredential(vault, credentialResource, credentialUserName);
TryRemoveCredential(vault, credentialResource, endpointCredentialUserName);
@@ -672,12 +861,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GetAdvancedAICredentialUserName(serviceType));
}
internal string GetPasteAIApiKey(string serviceType)
internal string GetPasteAIApiKey(string providerId, string serviceType)
{
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
providerId ??= string.Empty;
return RetrieveCredentialValue(
GetAICredentialResource(serviceType),
GetPasteAICredentialUserName(serviceType));
GetPasteAICredentialUserName(providerId, serviceType));
}
internal string GetAdvancedAIEndpoint(string serviceType)
@@ -688,12 +878,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GetAdvancedAIEndpointCredentialUserName(serviceType));
}
internal string GetPasteAIEndpoint(string serviceType)
internal string GetPasteAIEndpoint(string providerId, string serviceType)
{
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
providerId ??= string.Empty;
return RetrieveCredentialValue(
GetAICredentialResource(serviceType),
GetPasteAIEndpointCredentialUserName(serviceType));
GetPasteAIEndpointCredentialUserName(providerId, serviceType));
}
private static string GetAdvancedAICredentialUserName(string serviceType)
@@ -737,49 +928,96 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
};
}
private string GetPasteAICredentialUserName(string serviceType)
private string GetPasteAICredentialUserName(string providerId, string serviceType)
{
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
return serviceType.ToLowerInvariant() switch
{
"openai" => "PowerToys_AdvancedPaste_PasteAI_OpenAI",
"azureopenai" => "PowerToys_AdvancedPaste_PasteAI_AzureOpenAI",
"azureaiinference" => "PowerToys_AdvancedPaste_PasteAI_AzureAIInference",
"onnx" => "PowerToys_AdvancedPaste_PasteAI_Onnx", // Onnx doesn't need credentials but keeping consistency
"mistral" => "PowerToys_AdvancedPaste_PasteAI_Mistral",
"google" => "PowerToys_AdvancedPaste_PasteAI_Google",
"huggingface" => "PowerToys_AdvancedPaste_PasteAI_HuggingFace",
"anthropic" => "PowerToys_AdvancedPaste_PasteAI_Anthropic",
"amazonbedrock" => "PowerToys_AdvancedPaste_PasteAI_AmazonBedrock",
"ollama" => "PowerToys_AdvancedPaste_PasteAI_Ollama",
_ => "PowerToys_AdvancedPaste_PasteAI_OpenAI",
};
providerId ??= string.Empty;
string service = serviceType.ToLowerInvariant();
string normalizedId = NormalizeProviderIdentifier(providerId);
return $"PowerToys_AdvancedPaste_PasteAI_{service}_{normalizedId}";
}
private string GetPasteAIEndpointCredentialUserName(string serviceType)
private string GetPasteAIEndpointCredentialUserName(string providerId, string serviceType)
{
return GetPasteAICredentialUserName(serviceType) + "_Endpoint";
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;
}
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)
{
if (string.IsNullOrWhiteSpace(serviceType))
{
return true;
}
var serviceTypeKind = serviceType.ToAIServiceType();
return serviceType.ToLowerInvariant() switch
return serviceTypeKind switch
{
"onnx" => false,
"ollama" => false,
"foundrylocal" => false,
"windowsml" => false,
"anthropic" => false,
"amazonbedrock" => false,
AIServiceType.Onnx => false,
AIServiceType.Ollama => false,
AIServiceType.FoundryLocal => false,
AIServiceType.ML => false,
_ => true,
};
}
private void UpdateActivePasteAIProviderFlags()
{
var providers = PasteAIConfiguration?.Providers;
if (providers is null)
{
return;
}
string activeId = PasteAIConfiguration.ActiveProviderId ?? string.Empty;
foreach (var provider in providers)
{
provider.IsActive = string.Equals(provider.Id, activeId, StringComparison.OrdinalIgnoreCase);
}
}
private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
{
try
@@ -929,6 +1167,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (configuration is not null)
{
configuration.PropertyChanged += OnPasteAIConfigurationPropertyChanged;
SubscribeToPasteAIProviders(configuration);
}
}
@@ -937,6 +1176,80 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
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;
pasteConfig?.EnsureActiveProvider();
UpdateActivePasteAIProviderFlags();
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();
}
}
@@ -964,24 +1277,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private void OnPasteAIConfigurationPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_isLoadingPasteAIProviderConfiguration)
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.Providers), StringComparison.Ordinal))
{
return;
}
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ServiceType), StringComparison.Ordinal))
{
LoadPasteAIProviderConfiguration();
SubscribeToPasteAIProviders(PasteAIConfiguration);
UpdateActivePasteAIProviderFlags();
SaveAndNotifySettings();
return;
}
if (e.PropertyName is not null && PasteAITrackedProperties.Contains(e.PropertyName))
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal)
|| string.Equals(e.PropertyName, nameof(PasteAIConfiguration.UseSharedCredentials), StringComparison.Ordinal))
{
PersistPasteAIProviderConfiguration();
}
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal))
{
UpdateActivePasteAIProviderFlags();
}
SaveAndNotifySettings();
SaveAndNotifySettings();
}
}
private void SeedProviderConfigurationSnapshots()
@@ -1003,22 +1316,22 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
});
}
// Paste AI provider configurations are handled through the new provider list.
}
private void InitializePasteAIProviderState()
{
var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
if (pasteConfig is not null && !pasteConfig.HasProviderConfiguration(pasteConfig.ServiceType))
if (pasteConfig is null)
{
pasteConfig.SetProviderConfiguration(
pasteConfig.ServiceType,
new AIProviderConfigurationSnapshot
{
ModelName = pasteConfig.ModelName,
EndpointUrl = pasteConfig.EndpointUrl,
ApiVersion = pasteConfig.ApiVersion,
DeploymentName = pasteConfig.DeploymentName,
ModelPath = pasteConfig.ModelPath,
SystemPrompt = pasteConfig.SystemPrompt,
ModerationEnabled = pasteConfig.ModerationEnabled,
});
_advancedPasteSettings.Properties.PasteAIConfiguration = new PasteAIConfiguration();
pasteConfig = _advancedPasteSettings.Properties.PasteAIConfiguration;
}
pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
pasteConfig.EnsureActiveProvider();
UpdateActivePasteAIProviderFlags();
SubscribeToPasteAIProviders(pasteConfig);
}
private void LoadAdvancedAIProviderConfiguration()
@@ -1050,35 +1363,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
private void LoadPasteAIProviderConfiguration()
{
var config = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
if (config is null)
{
return;
}
var snapshot = config.GetOrCreateProviderConfiguration(config.ServiceType);
_isLoadingPasteAIProviderConfiguration = true;
try
{
config.ModelName = snapshot.ModelName ?? string.Empty;
config.EndpointUrl = snapshot.EndpointUrl ?? string.Empty;
config.ApiVersion = snapshot.ApiVersion ?? string.Empty;
config.DeploymentName = snapshot.DeploymentName ?? string.Empty;
config.ModelPath = snapshot.ModelPath ?? string.Empty;
config.SystemPrompt = snapshot.SystemPrompt ?? string.Empty;
config.ModerationEnabled = snapshot.ModerationEnabled;
string storedEndpoint = GetPasteAIEndpoint(config.ServiceType);
config.EndpointUrl = storedEndpoint;
snapshot.EndpointUrl = storedEndpoint;
}
finally
{
_isLoadingPasteAIProviderConfiguration = false;
}
}
private void PersistAdvancedAIProviderConfiguration()
{
if (_isLoadingAdvancedAIProviderConfiguration)
@@ -1102,29 +1386,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
snapshot.ModerationEnabled = config.ModerationEnabled;
}
private void PersistPasteAIProviderConfiguration()
{
if (_isLoadingPasteAIProviderConfiguration)
{
return;
}
var config = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
if (config is null)
{
return;
}
var snapshot = config.GetOrCreateProviderConfiguration(config.ServiceType);
snapshot.ModelName = config.ModelName ?? string.Empty;
snapshot.EndpointUrl = config.EndpointUrl ?? string.Empty;
snapshot.ApiVersion = config.ApiVersion ?? string.Empty;
snapshot.DeploymentName = config.DeploymentName ?? string.Empty;
snapshot.ModelPath = config.ModelPath ?? string.Empty;
snapshot.SystemPrompt = config.SystemPrompt ?? string.Empty;
snapshot.ModerationEnabled = config.ModerationEnabled;
}
private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)
{
if (string.IsNullOrWhiteSpace(credentialResource) || string.IsNullOrWhiteSpace(credentialUserName))