From d89198d26ed0691c0ae75e06fdb773f43f92de41 Mon Sep 17 00:00:00 2001 From: vanzue Date: Tue, 21 Oct 2025 18:14:16 +0800 Subject: [PATCH] wire things up for adding a provider --- .../AdvancedPaste/Helpers/UserSettings.cs | 1 + .../CustomActionTransformService.cs | 17 +- .../EnhancedVaultCredentialsProvider.cs | 106 +++- .../AIServiceTypeExtensions.cs | 2 +- .../AIServiceTypeMetadata.cs | 26 + .../AIServiceTypeRegistry.cs | 172 ++++++ .../PasteAIConfiguration.cs | 271 +++++++--- .../PasteAIProviderDefinition.cs | 142 +++++ .../Converters/ServiceTypeToIconConverter.cs | 30 + .../SettingsXAML/Views/AdvancedPastePage.xaml | 272 +++------- .../Views/AdvancedPastePage.xaml.cs | 308 ++++++++--- .../ViewModels/AdvancedPasteViewModel.cs | 511 +++++++++++++----- 12 files changed, 1347 insertions(+), 511 deletions(-) create mode 100644 src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs create mode 100644 src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs create mode 100644 src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs create mode 100644 src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs index 2d331f7cd9..97f7c1b8f7 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs @@ -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 = diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs index 4d2f1ccdfd..ee2bf7d3cd 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs @@ -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; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs index 2094bb5572..5946b6de76 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs @@ -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(); } } diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs index 8982161c70..91f035d23a 100644 --- a/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs +++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs @@ -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, diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs new file mode 100644 index 0000000000..cb4cc15258 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs @@ -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 +{ + /// + /// Metadata information for an AI service type. + /// + 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; + } +} diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs new file mode 100644 index 0000000000..f87d03db7c --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs @@ -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; + +/// +/// Centralized registry for AI service type metadata. +/// +public static class AIServiceTypeRegistry +{ + private static readonly Dictionary 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, + }, + }; + + /// + /// Get metadata for a specific service type. + /// + public static AIServiceTypeMetadata GetMetadata(AIServiceType serviceType) + { + return MetadataMap.TryGetValue(serviceType, out var metadata) + ? metadata + : MetadataMap[AIServiceType.Unknown]; + } + + /// + /// Get metadata for a service type from its string representation. + /// + public static AIServiceTypeMetadata GetMetadata(string serviceType) + { + var type = serviceType.ToAIServiceType(); + return GetMetadata(type); + } + + /// + /// Get icon path for a service type. + /// + public static string GetIconPath(AIServiceType serviceType) + { + return GetMetadata(serviceType).IconPath; + } + + /// + /// Get icon path for a service type from its string representation. + /// + public static string GetIconPath(string serviceType) + { + return GetMetadata(serviceType).IconPath; + } + + /// + /// Get all service types available in the UI. + /// + public static IEnumerable GetAvailableServiceTypes() + { + return MetadataMap.Values.Where(m => m.IsAvailableInUI); + } + + /// + /// Get all online service types available in the UI. + /// + public static IEnumerable GetOnlineServiceTypes() + { + return GetAvailableServiceTypes().Where(m => m.IsOnlineService); + } + + /// + /// Get all local service types available in the UI. + /// + public static IEnumerable GetLocalServiceTypes() + { + return GetAvailableServiceTypes().Where(m => !m.IsOnlineService); + } +} diff --git a/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs b/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs index 4e7ded0d22..6b4a0d1bef 100644 --- a/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs +++ b/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs @@ -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 /// 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 _providers = new(); private bool _useSharedCredentials = true; - private string _systemPrompt = string.Empty; - private bool _moderationEnabled = true; - private Dictionary _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 _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 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()); } [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 ProviderConfigurations + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary LegacyProviderConfigurations { - get => _providerConfigurations; - set => SetProperty(ref _providerConfigurations, value ?? new Dictionary(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(); + + 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; - } } } diff --git a/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs new file mode 100644 index 0000000000..c59dd573b0 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs @@ -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 +{ + /// + /// Represents a single Paste AI provider configuration entry. + /// + 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(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + field = value; + OnPropertyChanged(propertyName); + return true; + } + + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs b/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs new file mode 100644 index 0000000000..7d632906c2 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs @@ -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(); + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml index fa82f3d5b8..73fd9f9b97 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml @@ -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"> + + + + + ms-appx:///Assets/Settings/Modules/APDialog.dark.png @@ -125,100 +132,55 @@ - + - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Spacing="16"> + + + Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelName, Mode=TwoWay}" /> + Text="{x:Bind ViewModel.PasteAIProviderDraft.EndpointUrl, Mode=TwoWay}" /> + Text="{x:Bind ViewModel.PasteAIProviderDraft.DeploymentName, Mode=TwoWay}" /> + Text="{x:Bind ViewModel.PasteAIProviderDraft.ModelPath, Mode=TwoWay}" />