Init and added simple ui change.

Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
This commit is contained in:
Shuai Yuan
2025-09-03 16:18:47 +08:00
committed by Shawn Yuan (from Dev Box)
parent 8a7c944ec9
commit 4e5a2db985
6 changed files with 558 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library;
public class AdvancedPasteAIServiceOption
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,29 @@
// 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.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library;
public class AdvancedPasteAIServiceParameter : INotifyPropertyChanged
{
public string Name { get; set; }
public string DisplayName { get; set; }
public string Type { get; set; }
public string Description { get; set; }
public object Value { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

View File

@@ -2,6 +2,7 @@
// 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.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -26,6 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
IsAdvancedAIEnabled = false;
ShowCustomPreview = true;
CloseAfterLosingFocus = false;
AIServiceConfiguration = new Dictionary<string, object>();
}
[JsonConverter(typeof(BoolPropertyJsonConverter))]
@@ -57,6 +59,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnoreAttribute]
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
[JsonPropertyName("ai-service-configuration")]
[CmdConfigureIgnoreAttribute]
public Dictionary<string, object> AIServiceConfiguration { get; set; }
public override string ToString()
=> JsonSerializer.Serialize(this);
}

View File

@@ -106,6 +106,51 @@
IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsAdvancedAIEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsExpander
Name="AdvancedPasteAIConfigurationCard"
IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}"
IsExpanded="False" >
<tkcontrols:SettingsExpander.HeaderIcon>
<PathIcon Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z" />
</tkcontrols:SettingsExpander.HeaderIcon>
<ComboBox
x:Name="AIServiceComboBox"
ItemsSource="{x:Bind ViewModel.AIServiceOptions, Mode=OneWay}"
DisplayMemberPath="DisplayName"
SelectedItem="{x:Bind ViewModel.SelectedAIService, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsOnlineAIModelsDisallowedByGPO, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
ScrollViewer.VerticalScrollBarVisibility="Auto"
MinWidth="220" />
<tkcontrols:SettingsExpander.Description>
<StackPanel Orientation="Vertical">
<TextBlock x:Uid="AdvancedPaste_EnableAISettingsCardDescription" />
<HyperlinkButton x:Uid="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore" NavigateUri="https://learn.microsoft.com/windows/powertoys/advanced-paste" />
</StackPanel>
</tkcontrols:SettingsExpander.Description>
<tkcontrols:SettingsExpander.Items>
<ItemsControl ItemsSource="{x:Bind ViewModel.CurrentAIServiceParameters, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="{Binding DisplayName}" Width="120"/>
<TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<tkcontrols:SettingsCard
Margin="0,8,0,0"
Visibility="{x:Bind ViewModel.IsAIServiceEnabled, Mode=OneWay}">
<Button
x:Name="SaveAIConfigButton"
x:Uid="AdvancedPaste_SaveAIConfigButton"
Content="Save Configuration"
Click="SaveAIConfigButton_Click"
Style="{StaticResource AccentButtonStyle}"
HorizontalAlignment="Left" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="AdvancedPaste_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard

View File

@@ -60,6 +60,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
await ShowEnableDialogAsync();
}
private void SaveAIConfigButton_Click(object sender, RoutedEventArgs e)
{
// Force save the current AI service configuration
ViewModel.SaveAIConfiguration();
}
private async Task ShowEnableDialogAsync()
{
await EnableAIDialog.ShowAsync();

View File

@@ -49,6 +49,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private GpoRuleConfigured _onlineAIModelsGpoRuleConfiguration;
private bool _onlineAIModelsDisallowedByGPO;
private bool _isEnabled;
private ObservableCollection<AdvancedPasteAIServiceOption> _aiServiceOptions;
private AdvancedPasteAIServiceOption _selectedAIService;
private ObservableCollection<AdvancedPasteAIServiceParameter> _currentAIServiceParameters;
private Func<string, int> SendConfigMSG { get; }
@@ -77,6 +80,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
InitializeEnabledValue();
LoadAIServiceOptionsAndDefaults();
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
@@ -184,6 +189,457 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
public bool IsAIServiceEnabled => SelectedAIService?.Id != "Disabled";
public ObservableCollection<AdvancedPasteAIServiceOption> AIServiceOptions
{
get => _aiServiceOptions;
set
{
if (_aiServiceOptions != value)
{
_aiServiceOptions = value;
OnPropertyChanged(nameof(AIServiceOptions));
}
}
}
public AdvancedPasteAIServiceOption SelectedAIService
{
get => _selectedAIService;
set
{
if (_selectedAIService != value)
{
_selectedAIService = value;
OnPropertyChanged(nameof(SelectedAIService));
OnPropertyChanged(nameof(IsAIServiceEnabled));
UpdateCurrentAIServiceParameters();
}
}
}
public ObservableCollection<AdvancedPasteAIServiceParameter> CurrentAIServiceParameters
{
get => _currentAIServiceParameters;
set
{
if (_currentAIServiceParameters != value)
{
_currentAIServiceParameters = value;
OnPropertyChanged(nameof(CurrentAIServiceParameters));
}
}
}
private void LoadAIServiceOptionsAndDefaults()
{
AIServiceOptions = new ObservableCollection<AdvancedPasteAIServiceOption>
{
new AdvancedPasteAIServiceOption { Id = "AzureOpenAI", DisplayName = "Azure OpenAI" },
new AdvancedPasteAIServiceOption { Id = "OpenAI", DisplayName = "OpenAI" },
new AdvancedPasteAIServiceOption { Id = "Mistral", DisplayName = "Mistral" },
new AdvancedPasteAIServiceOption { Id = "GoogleGemini", DisplayName = "Google Gemini" },
new AdvancedPasteAIServiceOption { Id = "HuggingFace", DisplayName = "Hugging Face" },
new AdvancedPasteAIServiceOption { Id = "AzureAIInference", DisplayName = "Azure AI Inference" },
new AdvancedPasteAIServiceOption { Id = "Ollama", DisplayName = "Ollama" },
new AdvancedPasteAIServiceOption { Id = "Anthropic", DisplayName = "Anthropic Claude" },
new AdvancedPasteAIServiceOption { Id = "AmazonBedrock", DisplayName = "Amazon Bedrock" },
new AdvancedPasteAIServiceOption { Id = "ONNX", DisplayName = "Hugging Face" },
new AdvancedPasteAIServiceOption { Id = "Other", DisplayName = "Other" },
};
SelectedAIService = AIServiceOptions.First();
UpdateCurrentAIServiceParameters();
}
private void UpdateCurrentAIServiceParameters()
{
var parameters = new ObservableCollection<AdvancedPasteAIServiceParameter>();
switch (SelectedAIService?.Id)
{
case "Disabled":
// No parameters needed for disabled state
break;
case "OpenAI":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "apiKey",
DisplayName = "API Key",
Type = "password",
Description = "Your OpenAI API key from https://platform.openai.com/api-keys",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model",
Type = "text",
Description = "OpenAI model (e.g., gpt-4o, gpt-4-turbo, gpt-3.5-turbo)",
});
break;
case "AzureOpenAI":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "endpoint",
DisplayName = "Endpoint",
Type = "text",
Description = "Azure OpenAI endpoint URL (e.g., https://your-resource.openai.azure.com/)",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "apiKey",
DisplayName = "API Key",
Type = "password",
Description = "Azure OpenAI API key",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "deploymentName",
DisplayName = "Deployment Name",
Type = "text",
Description = "Azure OpenAI deployment name",
});
break;
case "AzureAIInference":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "apiKey",
DisplayName = "API Key",
Type = "password",
Description = "Azure AI Inference API key",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model ID",
Type = "text",
Description = "Model identifier",
});
break;
case "Anthropic":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model",
Type = "text",
Description = "Claude model (e.g., claude-3-5-sonnet-20241022, claude-3-haiku-20240307)",
});
break;
case "GoogleGemini":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "apiKey",
DisplayName = "API Key",
Type = "password",
Description = "Google AI Studio API key",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model",
Type = "text",
Description = "Gemini model (e.g., gemini-1.5-pro, gemini-1.5-flash)",
});
break;
case "Mistral":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "apiKey",
DisplayName = "API Key",
Type = "password",
Description = "Mistral AI API key from https://console.mistral.ai/",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model",
Type = "text",
Description = "Mistral model (e.g., mistral-large-latest, mistral-small-latest)",
});
break;
case "HuggingFace":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "apiKey",
DisplayName = "API Key",
Type = "password",
Description = "Hugging Face API token",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model ID",
Type = "text",
Description = "Hugging Face model identifier",
});
break;
case "Ollama":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "endpoint",
DisplayName = "Endpoint",
Type = "text",
Description = "Ollama server endpoint",
Value = "http://localhost:11434",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model",
Type = "text",
Description = "Ollama model name (e.g., llama3.2, mistral, codellama)",
});
break;
case "AmazonBedrock":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model ID",
Type = "text",
Description = "Bedrock model identifier",
});
break;
case "ONNX":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelPath",
DisplayName = "Model Path",
Type = "text",
Description = "Path to ONNX model file",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model ID",
Type = "text",
Description = "Bedrock model identifier",
});
break;
case "Other":
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "endpoint",
DisplayName = "Endpoint",
Type = "text",
Description = "Custom API endpoint URL",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "apiKey",
DisplayName = "API Key",
Type = "password",
Description = "API key for authentication",
});
parameters.Add(new AdvancedPasteAIServiceParameter
{
Name = "modelId",
DisplayName = "Model ID",
Type = "text",
Description = "Model identifier",
});
break;
default:
break;
}
CurrentAIServiceParameters = parameters;
}
public void SaveAIConfiguration()
{
if (SelectedAIService == null || CurrentAIServiceParameters == null)
{
return;
}
var sensitiveParameters = new HashSet<string> { "endpoint", "apiKey" };
var serviceId = SelectedAIService.Id;
var credentialParameters = new Dictionary<string, object>();
var settingsParameters = new Dictionary<string, object>();
foreach (var param in CurrentAIServiceParameters)
{
var value = param.Value ?? string.Empty;
if (sensitiveParameters.Contains(param.Name))
{
credentialParameters[param.Name] = value;
}
else
{
settingsParameters[param.Name] = value;
}
}
SaveCredentialParameters(serviceId, credentialParameters);
var aiConfiguration = new Dictionary<string, object>
{
["ServiceId"] = serviceId,
["ServiceDisplayName"] = SelectedAIService.DisplayName,
["Parameters"] = settingsParameters,
};
_advancedPasteSettings.Properties.AIServiceConfiguration = aiConfiguration;
SaveAndNotifySettings();
}
private void SaveCredentialParameters(string serviceId, Dictionary<string, object> credentialParameters)
{
try
{
PasswordVault vault = new PasswordVault();
foreach (var parameter in credentialParameters)
{
if (string.IsNullOrEmpty(parameter.Value?.ToString()))
{
RemoveCredential(vault, serviceId, parameter.Key);
continue;
}
var resourceName = $"PowerToys.AdvancedPaste.{serviceId}";
var userName = $"{serviceId}_{parameter.Key}";
var password = parameter.Value.ToString();
RemoveCredential(vault, resourceName, userName);
var credential = new PasswordCredential(resourceName, userName, password);
vault.Add(credential);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to save credentials: {ex.Message}");
}
}
private void RemoveCredential(PasswordVault vault, string resourceName, string userName)
{
try
{
var existingCredential = vault.Retrieve(resourceName, userName);
if (existingCredential != null)
{
vault.Remove(existingCredential);
}
}
catch (Exception)
{
}
}
private Dictionary<string, string> LoadCredentialParameters(string serviceId)
{
var credentials = new Dictionary<string, string>();
var sensitiveParameters = new HashSet<string> { "endpoint", "apiKey" };
try
{
PasswordVault vault = new PasswordVault();
var resourceName = $"PowerToys.AdvancedPaste.{serviceId}";
foreach (var parameterName in sensitiveParameters)
{
try
{
var userName = $"{serviceId}_{parameterName}";
var credential = vault.Retrieve(resourceName, userName);
if (credential != null)
{
credential.RetrievePassword();
credentials[parameterName] = credential.Password;
}
}
catch (Exception)
{
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to load credentials: {ex.Message}");
}
return credentials;
}
private void LoadAIConfigurationFromSettings()
{
if (_advancedPasteSettings.Properties.AIServiceConfiguration != null)
{
var config = _advancedPasteSettings.Properties.AIServiceConfiguration;
if (config.TryGetValue("ServiceId", out var serviceId))
{
var savedService = AIServiceOptions.FirstOrDefault(s => s.Id == serviceId?.ToString());
if (savedService != null)
{
SelectedAIService = savedService;
LoadParameterValues(config, serviceId.ToString());
}
}
}
}
private void LoadParameterValues(Dictionary<string, object> config, string serviceId)
{
var settingsParameters = new Dictionary<string, object>();
if (config.TryGetValue("Parameters", out var parameters) &&
parameters is Dictionary<string, object> paramDict)
{
settingsParameters = paramDict;
}
var credentialParameters = LoadCredentialParameters(serviceId);
foreach (var parameter in CurrentAIServiceParameters)
{
if (credentialParameters.TryGetValue(parameter.Name, out var credentialValue))
{
parameter.Value = credentialValue;
}
else if (settingsParameters.TryGetValue(parameter.Name, out var settingsValue))
{
parameter.Value = settingsValue;
}
}
}
private void ClearServiceCredentials(string serviceId)
{
try
{
PasswordVault vault = new PasswordVault();
var resourceName = $"PowerToys.AdvancedPaste.{serviceId}";
var sensitiveParameters = new HashSet<string> { "endpoint", "apiKey" };
foreach (var parameterName in sensitiveParameters)
{
var userName = $"{serviceId}_{parameterName}";
RemoveCredential(vault, resourceName, userName);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to clear service credentials: {ex.Message}");
}
}
private bool OpenAIKeyExists()
{
PasswordVault vault = new PasswordVault();