Compare commits
42 Commits
stable-bac
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f339ec0fb5 | ||
|
|
00cd91344d | ||
|
|
72a6057859 | ||
|
|
be07e97cf6 | ||
|
|
02a60bd098 | ||
|
|
207f294e1e | ||
|
|
03f3206a44 | ||
|
|
5171b78eae | ||
|
|
f36e8747fd | ||
|
|
211cbc6438 | ||
|
|
0e30d2705c | ||
|
|
4ece86c4d1 | ||
|
|
55d8e726a0 | ||
|
|
b3c1170040 | ||
|
|
ef1717c97f | ||
|
|
bfff31e657 | ||
|
|
b761f95ba3 | ||
|
|
dda9cd8e18 | ||
|
|
14282b9df1 | ||
|
|
81a6e3bfdc | ||
|
|
06279a2102 | ||
|
|
6fb3ee5e3a | ||
|
|
4df18234a7 | ||
|
|
571cb3cb22 | ||
|
|
9c76d8a667 | ||
|
|
e8cbb1bd66 | ||
|
|
5734afcf89 | ||
|
|
c63b29a777 | ||
|
|
4309006a92 | ||
|
|
d24a1d99ad | ||
|
|
439023af68 | ||
|
|
4e5a2db985 | ||
|
|
8a7c944ec9 | ||
|
|
49cfcb1349 | ||
|
|
320b7eca7c | ||
|
|
d60923bc9a | ||
|
|
a5097c7525 | ||
|
|
7ccbef0298 | ||
|
|
665d7ca535 | ||
|
|
debbc72825 | ||
|
|
46a4e32fb6 | ||
|
|
c832862b9a |
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
@@ -9,7 +9,6 @@
|
||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="1.0.0-beta.17" />
|
||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
@@ -40,12 +39,21 @@
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Amazon" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.66.0-beta" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.HuggingFace" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
@@ -72,7 +80,7 @@
|
||||
<PackageVersion Include="NLog" Version="5.2.8" />
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.0.0" />
|
||||
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
|
||||
@@ -1495,7 +1495,6 @@ SOFTWARE.
|
||||
- AdaptiveCards.Rendering.WinUI3
|
||||
- AdaptiveCards.Templating
|
||||
- Appium.WebDriver
|
||||
- Azure.AI.OpenAI
|
||||
- CoenM.ImageSharp.ImageHash
|
||||
- CommunityToolkit.Common
|
||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
||||
|
||||
@@ -825,6 +825,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageModelProvider", "src\common\LanguageModelProvider\LanguageModelProvider.csproj", "{45354F4F-1414-45CE-B600-51CD1209FD19}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -2995,6 +2997,14 @@ Global
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|x64.Build.0 = Debug|x64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|x64.ActiveCfg = Release|x64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -3323,6 +3333,7 @@ Global
|
||||
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
|
||||
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="PowerToysPublicDependencies" value="https://pkgs.dev.azure.com/shine-oss/PowerToys/_packaging/PowerToysPublicDependencies/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="Microsoft.SemanticKernel*" />
|
||||
<package pattern="Microsoft.Extensions.*" />
|
||||
<package pattern="System.*" />
|
||||
<package pattern="OpenAI" />
|
||||
<package pattern="Azure.*" />
|
||||
</packageSource>
|
||||
<packageSource key="PowerToysPublicDependencies">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
|
||||
14
src/common/LanguageModelProvider/AppUtils.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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.
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
internal static class AppUtils
|
||||
{
|
||||
public static string GetThemeAssetSuffix()
|
||||
{
|
||||
// Default suffix for assets that are theme-agnostic today.
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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.
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record FoundryCachedModel(string Name, string? Id);
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record FoundryCatalogModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("displayName")]
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("providerType")]
|
||||
public string ProviderType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("uri")]
|
||||
public string Uri { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("modelType")]
|
||||
public string ModelType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("promptTemplate")]
|
||||
public PromptTemplate PromptTemplate { get; init; } = default!;
|
||||
|
||||
[JsonPropertyName("publisher")]
|
||||
public string Publisher { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("task")]
|
||||
public string Task { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("runtime")]
|
||||
public Runtime Runtime { get; init; } = default!;
|
||||
|
||||
[JsonPropertyName("fileSizeMb")]
|
||||
public long FileSizeMb { get; init; }
|
||||
|
||||
[JsonPropertyName("modelSettings")]
|
||||
public ModelSettings ModelSettings { get; init; } = default!;
|
||||
|
||||
[JsonPropertyName("alias")]
|
||||
public string Alias { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("supportsToolCalling")]
|
||||
public bool SupportsToolCalling { get; init; }
|
||||
|
||||
[JsonPropertyName("license")]
|
||||
public string License { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("licenseDescription")]
|
||||
public string LicenseDescription { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("parentModelUri")]
|
||||
public string ParentModelUri { get; init; } = string.Empty;
|
||||
}
|
||||
229
src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
// 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.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed class FoundryClient
|
||||
{
|
||||
public static async Task<FoundryClient?> CreateAsync()
|
||||
{
|
||||
var serviceManager = FoundryServiceManager.TryCreate();
|
||||
if (serviceManager is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!await serviceManager.IsRunning().ConfigureAwait(false))
|
||||
{
|
||||
if (!await serviceManager.StartService().ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var serviceUrl = await serviceManager.GetServiceUrl().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(serviceUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var serviceUri = new Uri(serviceUrl, UriKind.Absolute);
|
||||
var baseAddress = serviceUri.AbsoluteUri.EndsWith('/')
|
||||
? serviceUri
|
||||
: new Uri(serviceUri, "/");
|
||||
|
||||
var httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = baseAddress,
|
||||
Timeout = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
var assemblyVersion = typeof(FoundryClient).Assembly.GetName().Version?.ToString() ?? "unknown";
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"foundry-local-cs-sdk/{assemblyVersion}");
|
||||
|
||||
return new FoundryClient(serviceManager, httpClient);
|
||||
}
|
||||
|
||||
public FoundryServiceManager ServiceManager { get; }
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly List<FoundryCatalogModel> _catalogModels = [];
|
||||
|
||||
private FoundryClient(FoundryServiceManager serviceManager, HttpClient httpClient)
|
||||
{
|
||||
ServiceManager = serviceManager;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<FoundryCatalogModel>> ListCatalogModels()
|
||||
{
|
||||
if (_catalogModels.Count > 0)
|
||||
{
|
||||
return _catalogModels;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/foundry/list").ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var models = await JsonSerializer.DeserializeAsync(
|
||||
response.Content.ReadAsStream(),
|
||||
FoundryJsonContext.Default.ListFoundryCatalogModel).ConfigureAwait(false);
|
||||
|
||||
if (models is { Count: > 0 })
|
||||
{
|
||||
models.ForEach(_catalogModels.Add);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Surfacing errors here prevents listing other providers; swallow and return cached list instead.
|
||||
}
|
||||
|
||||
return _catalogModels;
|
||||
}
|
||||
|
||||
public async Task<List<FoundryCachedModel>> ListCachedModels()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/openai/models").ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var catalogModels = await ListCatalogModels().ConfigureAwait(false);
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var modelIds = content
|
||||
.Trim('[', ']')
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(id => id.Trim('"'));
|
||||
|
||||
List<FoundryCachedModel> models = [];
|
||||
|
||||
foreach (var id in modelIds)
|
||||
{
|
||||
var model = catalogModels.FirstOrDefault(m => m.Name == id);
|
||||
models.Add(model != null ? new FoundryCachedModel(id, model.Alias) : new FoundryCachedModel(id, null));
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
public async Task<FoundryDownloadResult> DownloadModel(FoundryCatalogModel model, IProgress<float>? progress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var models = await ListCachedModels().ConfigureAwait(false);
|
||||
|
||||
if (models.Any(m => m.Name == model.Name))
|
||||
{
|
||||
return new(true, "Model already downloaded");
|
||||
}
|
||||
|
||||
return await Task.Run(
|
||||
async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var providerType = model.ProviderType.EndsWith("Local", StringComparison.OrdinalIgnoreCase)
|
||||
? model.ProviderType
|
||||
: $"{model.ProviderType}Local";
|
||||
|
||||
var downloadRequest = new FoundryDownloadBody
|
||||
{
|
||||
Model = new FoundryModelDownload
|
||||
{
|
||||
Name = model.Name,
|
||||
Uri = model.Uri,
|
||||
Publisher = model.Publisher,
|
||||
ProviderType = providerType,
|
||||
PromptTemplate = model.PromptTemplate,
|
||||
},
|
||||
Token = string.Empty,
|
||||
IgnorePipeReport = true,
|
||||
};
|
||||
|
||||
var downloadBodyContext = FoundryJsonContext.Default.FoundryDownloadBody;
|
||||
string body = JsonSerializer.Serialize(downloadRequest, downloadBodyContext);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/openai/download")
|
||||
{
|
||||
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
||||
};
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
StringBuilder jsonBuilder = new();
|
||||
var collectingJson = false;
|
||||
var completed = false;
|
||||
|
||||
while (!completed && (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("Total", StringComparison.CurrentCultureIgnoreCase) &&
|
||||
line.Contains("Downloading", StringComparison.OrdinalIgnoreCase) &&
|
||||
line.Contains('%'))
|
||||
{
|
||||
var percentStr = line.Split('%')[0].Split(' ').Last();
|
||||
if (double.TryParse(percentStr, NumberStyles.Float, CultureInfo.CurrentCulture, out var percentage))
|
||||
{
|
||||
progress?.Report((float)(percentage / 100));
|
||||
}
|
||||
}
|
||||
else if (line.Contains("[DONE]", StringComparison.OrdinalIgnoreCase) ||
|
||||
line.Contains("All Completed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
collectingJson = true;
|
||||
}
|
||||
else if (collectingJson && line.TrimStart().StartsWith('{'))
|
||||
{
|
||||
jsonBuilder.AppendLine(line);
|
||||
}
|
||||
else if (collectingJson && jsonBuilder.Length > 0)
|
||||
{
|
||||
jsonBuilder.AppendLine(line);
|
||||
if (line.Trim() == "}")
|
||||
{
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var downloadResultContext = FoundryJsonContext.Default.FoundryDownloadResult;
|
||||
var jsonPayload = jsonBuilder.Length > 0 ? jsonBuilder.ToString() : null;
|
||||
|
||||
if (jsonPayload is null)
|
||||
{
|
||||
return new FoundryDownloadResult(false, "No completion response received from server.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize(jsonPayload, downloadResultContext)
|
||||
?? new FoundryDownloadResult(false, "Failed to parse completion response.");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return new FoundryDownloadResult(false, $"Failed to parse completion response: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FoundryDownloadResult(false, e.Message);
|
||||
}
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed class FoundryDownloadBody
|
||||
{
|
||||
[JsonPropertyName("Model")]
|
||||
public required FoundryModelDownload Model { get; init; }
|
||||
|
||||
[JsonPropertyName("token")]
|
||||
public required string Token { get; init; }
|
||||
|
||||
[JsonPropertyName("IgnorePipeReport")]
|
||||
public required bool IgnorePipeReport { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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.
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record FoundryDownloadResult(bool Success, string? ErrorMessage);
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(FoundryCatalogModel))]
|
||||
[JsonSerializable(typeof(List<FoundryCatalogModel>))]
|
||||
[JsonSerializable(typeof(FoundryDownloadResult))]
|
||||
[JsonSerializable(typeof(FoundryModelDownload))]
|
||||
[JsonSerializable(typeof(FoundryDownloadBody))]
|
||||
internal sealed partial class FoundryJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed class FoundryModelDownload
|
||||
{
|
||||
[JsonPropertyName("Name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("Uri")]
|
||||
public required string Uri { get; init; }
|
||||
|
||||
[JsonPropertyName("Publisher")]
|
||||
public required string Publisher { get; init; }
|
||||
|
||||
[JsonPropertyName("ProviderType")]
|
||||
public required string ProviderType { get; init; }
|
||||
|
||||
[JsonPropertyName("PromptTemplate")]
|
||||
public required PromptTemplate? PromptTemplate { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed class FoundryServiceManager
|
||||
{
|
||||
public static FoundryServiceManager? TryCreate()
|
||||
{
|
||||
return IsAvailable() ? new FoundryServiceManager() : null;
|
||||
}
|
||||
|
||||
private static bool IsAvailable()
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo.FileName = "where";
|
||||
process.StartInfo.Arguments = "foundry";
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
|
||||
private static string? GetUrl(string output)
|
||||
{
|
||||
var match = Regex.Match(output, @"https?:\/\/[^\/]+:\d+");
|
||||
return match.Success ? match.Value : null;
|
||||
}
|
||||
|
||||
public async Task<string?> GetServiceUrl()
|
||||
{
|
||||
var status = await Utils.RunFoundryWithArguments("service status").ConfigureAwait(false);
|
||||
|
||||
if (status.ExitCode != 0 || string.IsNullOrWhiteSpace(status.Output))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetUrl(status.Output);
|
||||
}
|
||||
|
||||
public async Task<bool> IsRunning()
|
||||
{
|
||||
var url = await GetServiceUrl().ConfigureAwait(false);
|
||||
return url is not null;
|
||||
}
|
||||
|
||||
public async Task<bool> StartService()
|
||||
{
|
||||
if (await IsRunning().ConfigureAwait(false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var status = await Utils.RunFoundryWithArguments("service start").ConfigureAwait(false);
|
||||
if (status.ExitCode != 0 || string.IsNullOrWhiteSpace(status.Output))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return GetUrl(status.Output) is not null;
|
||||
}
|
||||
}
|
||||
@@ -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.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record ModelSettings
|
||||
{
|
||||
// The sample shows an empty array; keep it open-ended.
|
||||
[JsonPropertyName("parameters")]
|
||||
public List<JsonElement> Parameters { get; init; } = [];
|
||||
}
|
||||
@@ -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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record PromptTemplate
|
||||
{
|
||||
[JsonPropertyName("assistant")]
|
||||
public string Assistant { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("prompt")]
|
||||
public string Prompt { get; init; } = string.Empty;
|
||||
}
|
||||
16
src/common/LanguageModelProvider/FoundryLocal/Runtime.cs
Normal 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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record Runtime
|
||||
{
|
||||
[JsonPropertyName("deviceType")]
|
||||
public string DeviceType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("executionProvider")]
|
||||
public string ExecutionProvider { get; init; } = string.Empty;
|
||||
}
|
||||
37
src/common/LanguageModelProvider/FoundryLocal/Utils.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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.Diagnostics;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal static class Utils
|
||||
{
|
||||
public static async Task<(string? Output, string? Error, int ExitCode)> RunFoundryWithArguments(string arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo.FileName = "foundry";
|
||||
process.StartInfo.Arguments = arguments;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
|
||||
process.Start();
|
||||
|
||||
string? output = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
|
||||
string? error = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
await process.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
return (output, error, process.ExitCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (null, null, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/common/LanguageModelProvider/FoundryLocalModelProvider.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
// 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.ClientModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LanguageModelProvider.FoundryLocal;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
{
|
||||
private IEnumerable<ModelDetails>? _downloadedModels;
|
||||
private IEnumerable<ModelDetails>? _catalogModels;
|
||||
private FoundryClient? _foundryManager;
|
||||
private string? _serviceUrl;
|
||||
|
||||
public static FoundryLocalModelProvider Instance { get; } = new();
|
||||
|
||||
public string Name => "FoundryLocal";
|
||||
|
||||
public HardwareAccelerator ModelHardwareAccelerator => HardwareAccelerator.FOUNDRYLOCAL;
|
||||
|
||||
public string ProviderDescription => "The model will run locally via Foundry Local";
|
||||
|
||||
public string UrlPrefix => "fl://";
|
||||
|
||||
public string Icon => $"fl{AppUtils.GetThemeAssetSuffix()}.svg";
|
||||
|
||||
public string Url => _serviceUrl ?? string.Empty;
|
||||
|
||||
public string GetDetailsUrl(ModelDetails details)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IChatClient? GetIChatClient(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_serviceUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var modelId = url.Split('/').LastOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new OpenAIClient(
|
||||
new ApiKeyCredential("none"),
|
||||
new OpenAIClientOptions { Endpoint = new Uri($"{_serviceUrl}/v1") })
|
||||
.GetChatClient(modelId)
|
||||
.AsIChatClient();
|
||||
}
|
||||
|
||||
public string GetIChatClientString(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var modelId = url.Split('/').LastOrDefault();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_serviceUrl) || string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (ignoreCached)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
await InitializeAsync(cancelationToken);
|
||||
|
||||
return _downloadedModels ?? [];
|
||||
}
|
||||
|
||||
public IEnumerable<ModelDetails> GetAllModelsInCatalog()
|
||||
{
|
||||
return _catalogModels ?? [];
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadModel(ModelDetails modelDetails, IProgress<float>? progress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_foundryManager == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (modelDetails.ProviderModelDetails is not FoundryCatalogModel model)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (await _foundryManager.DownloadModel(model, progress, cancellationToken)).Success;
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_downloadedModels = null;
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (_foundryManager != null && _downloadedModels != null && _downloadedModels.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_foundryManager ??= await FoundryClient.CreateAsync();
|
||||
|
||||
if (_foundryManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_serviceUrl ??= await _foundryManager.ServiceManager.GetServiceUrl();
|
||||
|
||||
if (_catalogModels == null || !_catalogModels.Any())
|
||||
{
|
||||
_catalogModels = (await _foundryManager.ListCatalogModels()).Select(ToModelDetails).ToArray();
|
||||
}
|
||||
|
||||
var cachedModels = await _foundryManager.ListCachedModels();
|
||||
|
||||
List<ModelDetails> downloadedModels = [];
|
||||
|
||||
foreach (var model in _catalogModels)
|
||||
{
|
||||
var cachedModel = cachedModels.FirstOrDefault(m => m.Name == model.Name);
|
||||
|
||||
if (cachedModel != default)
|
||||
{
|
||||
model.Id = $"{UrlPrefix}{cachedModel.Id}";
|
||||
downloadedModels.Add(model);
|
||||
cachedModels.Remove(cachedModel);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var model in cachedModels)
|
||||
{
|
||||
downloadedModels.Add(new ModelDetails
|
||||
{
|
||||
Id = $"fl-{model.Name}",
|
||||
Name = model.Name,
|
||||
Url = $"{UrlPrefix}{model.Name}",
|
||||
Description = $"{model.Name} running locally with Foundry Local",
|
||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||
SupportedOnQualcomm = true,
|
||||
ProviderModelDetails = model,
|
||||
});
|
||||
}
|
||||
|
||||
_downloadedModels = downloadedModels;
|
||||
}
|
||||
|
||||
private ModelDetails ToModelDetails(FoundryCatalogModel model)
|
||||
{
|
||||
return new ModelDetails
|
||||
{
|
||||
Id = $"fl-{model.Name}",
|
||||
Name = model.Name,
|
||||
Url = $"{UrlPrefix}{model.Name}",
|
||||
Description = $"{model.Alias} running locally with Foundry Local",
|
||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||
Size = model.FileSizeMb * 1024 * 1024,
|
||||
SupportedOnQualcomm = true,
|
||||
License = model.License?.ToLowerInvariant() ?? string.Empty,
|
||||
ProviderModelDetails = model,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> IsAvailable()
|
||||
{
|
||||
await InitializeAsync();
|
||||
return _foundryManager != null;
|
||||
}
|
||||
}
|
||||
22
src/common/LanguageModelProvider/HardwareAccelerator.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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.
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public enum HardwareAccelerator
|
||||
{
|
||||
CPU,
|
||||
DML,
|
||||
QNN,
|
||||
WCRAPI,
|
||||
OLLAMA,
|
||||
OPENAI,
|
||||
FOUNDRYLOCAL,
|
||||
LEMONADE,
|
||||
NPU,
|
||||
GPU,
|
||||
VitisAI,
|
||||
OpenVINO,
|
||||
NvTensorRT,
|
||||
}
|
||||
30
src/common/LanguageModelProvider/ILanguageModelProvider.cs
Normal 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 Microsoft.Extensions.AI;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public interface ILanguageModelProvider
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
string UrlPrefix { get; }
|
||||
|
||||
string Icon { get; }
|
||||
|
||||
HardwareAccelerator ModelHardwareAccelerator { get; }
|
||||
|
||||
string ProviderDescription { get; }
|
||||
|
||||
Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
|
||||
|
||||
IChatClient? GetIChatClient(string url);
|
||||
|
||||
string GetIChatClientString(string url);
|
||||
|
||||
string GetDetailsUrl(ModelDetails details);
|
||||
|
||||
string Url { get; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="OpenAI" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
106
src/common/LanguageModelProvider/LanguageModelService.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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.Concurrent;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public sealed class LanguageModelService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ILanguageModelProvider> _providersByPrefix;
|
||||
|
||||
public LanguageModelService(IEnumerable<ILanguageModelProvider> providers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providers);
|
||||
|
||||
_providersByPrefix = new ConcurrentDictionary<string, ILanguageModelProvider>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(provider.UrlPrefix))
|
||||
{
|
||||
_providersByPrefix[provider.UrlPrefix] = provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static LanguageModelService CreateDefault()
|
||||
{
|
||||
return new LanguageModelService(new[]
|
||||
{
|
||||
FoundryLocalModelProvider.Instance,
|
||||
});
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ILanguageModelProvider> Providers => _providersByPrefix.Values.ToArray();
|
||||
|
||||
public bool RegisterProvider(ILanguageModelProvider provider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider.UrlPrefix))
|
||||
{
|
||||
throw new ArgumentException("Provider must supply a URL prefix.", nameof(provider));
|
||||
}
|
||||
|
||||
_providersByPrefix[provider.UrlPrefix] = provider;
|
||||
return true;
|
||||
}
|
||||
|
||||
public ILanguageModelProvider? GetProviderFor(string? modelReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var provider in _providersByPrefix.Values)
|
||||
{
|
||||
if (modelReference.StartsWith(provider.UrlPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ModelDetails>> GetModelsAsync(bool refresh = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<ModelDetails> models = [];
|
||||
|
||||
foreach (var provider in _providersByPrefix.Values)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var providerModels = await provider.GetModelsAsync(refresh, cancellationToken).ConfigureAwait(false);
|
||||
models.AddRange(providerModels);
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
public IChatClient? GetClient(ModelDetails model)
|
||||
{
|
||||
if (model is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var reference = !string.IsNullOrWhiteSpace(model.Url) ? model.Url : model.Id;
|
||||
return GetClient(reference);
|
||||
}
|
||||
|
||||
public IChatClient? GetClient(string? modelReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = GetProviderFor(modelReference);
|
||||
|
||||
return provider?.GetIChatClient(modelReference);
|
||||
}
|
||||
}
|
||||
32
src/common/LanguageModelProvider/ModelDetails.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public class ModelDetails
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
public bool IsUserAdded { get; set; }
|
||||
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
public List<HardwareAccelerator> HardwareAccelerators { get; set; } = [];
|
||||
|
||||
public bool SupportedOnQualcomm { get; set; }
|
||||
|
||||
public string License { get; set; } = string.Empty;
|
||||
|
||||
public object? ProviderModelDetails { get; set; }
|
||||
}
|
||||
@@ -49,7 +49,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenAI" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
@@ -57,6 +56,12 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageReference Include="MessagePack" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Amazon" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Google" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.HuggingFace" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.MistralAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
@@ -102,6 +107,7 @@
|
||||
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.ViewModels;
|
||||
using ManagedCommon;
|
||||
@@ -77,11 +78,12 @@ namespace AdvancedPaste
|
||||
{
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
services.AddSingleton<IUserSettings, UserSettings>();
|
||||
services.AddSingleton<IAICredentialsProvider, Services.OpenAI.VaultCredentialsProvider>();
|
||||
services.AddSingleton<IAICredentialsProvider, EnhancedVaultCredentialsProvider>();
|
||||
services.AddSingleton<IPromptModerationService, Services.OpenAI.PromptModerationService>();
|
||||
services.AddSingleton<ICustomTextTransformService, Services.OpenAI.CustomTextTransformService>();
|
||||
services.AddSingleton<IKernelQueryCacheService, CustomActionKernelQueryCacheService>();
|
||||
services.AddSingleton<IKernelService, Services.OpenAI.KernelService>();
|
||||
services.AddSingleton<IPasteAIProviderFactory, PasteAIProviderFactory>();
|
||||
services.AddSingleton<ICustomActionTransformService, CustomActionTransformService>();
|
||||
services.AddSingleton<IKernelService, AdvancedAIKernelService>();
|
||||
services.AddSingleton<IPasteFormatExecutor, PasteFormatExecutor>();
|
||||
services.AddSingleton<OptionsViewModel>();
|
||||
}).Build();
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace AdvancedPaste.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for extracting AI service usage information from chat messages.
|
||||
/// </summary>
|
||||
public static class AIServiceUsageHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts AI service usage information from OpenAI chat message metadata.
|
||||
/// </summary>
|
||||
/// <param name="chatMessage">The chat message containing usage metadata.</param>
|
||||
/// <returns>AI service usage information or AIServiceUsage.None if extraction fails.</returns>
|
||||
public static AIServiceUsage GetOpenAIServiceUsage(ChatMessageContent chatMessage)
|
||||
{
|
||||
// Try to get usage information from metadata
|
||||
if (chatMessage.Metadata?.TryGetValue("Usage", out var usageObj) == true)
|
||||
{
|
||||
// Handle different possible usage types through reflection to be version-agnostic
|
||||
var usageType = usageObj.GetType();
|
||||
|
||||
try
|
||||
{
|
||||
// Try common property names for prompt tokens
|
||||
var promptTokensProp = usageType.GetProperty("PromptTokens") ??
|
||||
usageType.GetProperty("InputTokens") ??
|
||||
usageType.GetProperty("InputTokenCount");
|
||||
|
||||
var completionTokensProp = usageType.GetProperty("CompletionTokens") ??
|
||||
usageType.GetProperty("OutputTokens") ??
|
||||
usageType.GetProperty("OutputTokenCount");
|
||||
|
||||
if (promptTokensProp != null && completionTokensProp != null)
|
||||
{
|
||||
var promptTokens = (int)(promptTokensProp.GetValue(usageObj) ?? 0);
|
||||
var completionTokens = (int)(completionTokensProp.GetValue(usageObj) ?? 0);
|
||||
return new AIServiceUsage(promptTokens, completionTokens);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If reflection fails, fall back to no usage
|
||||
}
|
||||
}
|
||||
|
||||
return AIServiceUsage.None;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Win32;
|
||||
@@ -180,6 +181,46 @@ internal static class DataPackageHelpers
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<string> GetClipboardTextOrThrowAsync(this DataPackageView dataPackageView, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dataPackageView);
|
||||
|
||||
try
|
||||
{
|
||||
if (dataPackageView.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
return await dataPackageView.GetTextAsync();
|
||||
}
|
||||
|
||||
if (dataPackageView.Contains(StandardDataFormats.Html))
|
||||
{
|
||||
var html = await dataPackageView.GetHtmlFormatAsync();
|
||||
return HtmlUtilities.ConvertToText(html);
|
||||
}
|
||||
|
||||
if (dataPackageView.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
var bitmap = await dataPackageView.GetImageContentAsync();
|
||||
if (bitmap != null)
|
||||
{
|
||||
return await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is COMException or InvalidOperationException)
|
||||
{
|
||||
throw CreateClipboardTextMissingException(ex);
|
||||
}
|
||||
|
||||
throw CreateClipboardTextMissingException();
|
||||
}
|
||||
|
||||
private static PasteActionException CreateClipboardTextMissingException(Exception innerException = null)
|
||||
{
|
||||
var message = ResourceLoaderInstance.ResourceLoader.GetString("ClipboardEmptyWarning");
|
||||
return new PasteActionException(message, innerException ?? new InvalidOperationException("Clipboard does not contain text content."));
|
||||
}
|
||||
|
||||
internal static async Task<string> GetHtmlContentAsync(this DataPackageView dataPackageView) =>
|
||||
dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace AdvancedPaste.Settings
|
||||
{
|
||||
public bool IsAdvancedAIEnabled { get; }
|
||||
|
||||
public bool IsAIEnabled { get; }
|
||||
|
||||
public bool ShowCustomPreview { get; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
@@ -22,6 +24,10 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions { get; }
|
||||
|
||||
public AdvancedAIConfiguration AdvancedAIConfiguration { get; }
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; }
|
||||
|
||||
public event EventHandler Changed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using AdvancedPaste.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
||||
using Windows.Security.Credentials;
|
||||
|
||||
namespace AdvancedPaste.Settings
|
||||
{
|
||||
@@ -35,6 +36,8 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool IsAdvancedAIEnabled { get; private set; }
|
||||
|
||||
public bool IsAIEnabled { get; private set; }
|
||||
|
||||
public bool ShowCustomPreview { get; private set; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; private set; }
|
||||
@@ -43,13 +46,20 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
|
||||
public AdvancedAIConfiguration AdvancedAIConfiguration { get; private set; }
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; private set; }
|
||||
|
||||
public UserSettings(IFileSystem fileSystem)
|
||||
{
|
||||
_settingsUtils = new SettingsUtils(fileSystem);
|
||||
|
||||
IsAdvancedAIEnabled = false;
|
||||
IsAIEnabled = false;
|
||||
ShowCustomPreview = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
AdvancedAIConfiguration = new AdvancedAIConfiguration();
|
||||
PasteAIConfiguration = new PasteAIConfiguration();
|
||||
_additionalActions = [];
|
||||
_customActions = [];
|
||||
_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
@@ -94,13 +104,18 @@ namespace AdvancedPaste.Settings
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteModuleName);
|
||||
if (settings != null)
|
||||
{
|
||||
bool migratedLegacyEnablement = TryMigrateLegacyAIEnablement(settings);
|
||||
|
||||
void UpdateSettings()
|
||||
{
|
||||
var properties = settings.Properties;
|
||||
|
||||
IsAdvancedAIEnabled = properties.IsAdvancedAIEnabled;
|
||||
IsAIEnabled = properties.IsAIEnabled;
|
||||
ShowCustomPreview = properties.ShowCustomPreview;
|
||||
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
|
||||
AdvancedAIConfiguration = properties.AdvancedAIConfiguration ?? new AdvancedAIConfiguration();
|
||||
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
|
||||
|
||||
var sourceAdditionalActions = properties.AdditionalActions;
|
||||
(PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats =
|
||||
@@ -126,6 +141,11 @@ namespace AdvancedPaste.Settings
|
||||
Task.Factory
|
||||
.StartNew(UpdateSettings, CancellationToken.None, TaskCreationOptions.None, _taskScheduler)
|
||||
.Wait();
|
||||
|
||||
if (migratedLegacyEnablement)
|
||||
{
|
||||
settings.Save(_settingsUtils);
|
||||
}
|
||||
}
|
||||
|
||||
retry = false;
|
||||
@@ -144,6 +164,35 @@ namespace AdvancedPaste.Settings
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMigrateLegacyAIEnablement(AdvancedPasteSettings settings)
|
||||
{
|
||||
if (settings?.Properties is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (settings.Properties.IsAIEnabled || !LegacyOpenAIKeyExists())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
settings.Properties.IsAIEnabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool LegacyOpenAIKeyExists()
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordVault vault = new();
|
||||
return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// 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 AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Settings;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.Amazon;
|
||||
using Microsoft.SemanticKernel.Connectors.AzureAIInference;
|
||||
using Microsoft.SemanticKernel.Connectors.Google;
|
||||
using Microsoft.SemanticKernel.Connectors.HuggingFace;
|
||||
using Microsoft.SemanticKernel.Connectors.MistralAI;
|
||||
using Microsoft.SemanticKernel.Connectors.Ollama;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public sealed class AdvancedAIKernelService : KernelServiceBase
|
||||
{
|
||||
private readonly IAICredentialsProvider credentialsProvider;
|
||||
|
||||
public AdvancedAIKernelService(
|
||||
IAICredentialsProvider credentialsProvider,
|
||||
IKernelQueryCacheService queryCacheService,
|
||||
IPromptModerationService promptModerationService,
|
||||
IUserSettings userSettings,
|
||||
ICustomActionTransformService customActionTransformService)
|
||||
: base(queryCacheService, promptModerationService, userSettings, customActionTransformService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(credentialsProvider);
|
||||
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
}
|
||||
|
||||
protected override string AdvancedAIModelName => GetModelName();
|
||||
|
||||
protected override PromptExecutionSettings PromptExecutionSettings => CreatePromptExecutionSettings();
|
||||
|
||||
protected override void AddChatCompletionService(IKernelBuilder kernelBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(kernelBuilder);
|
||||
|
||||
var config = this.GetConfiguration();
|
||||
var serviceType = config.ServiceTypeKind;
|
||||
var modelName = GetModelName(config);
|
||||
var requiresApiKey = RequiresApiKey(serviceType);
|
||||
var apiKey = string.Empty;
|
||||
if (requiresApiKey)
|
||||
{
|
||||
this.credentialsProvider.Refresh(AICredentialScope.AdvancedAI);
|
||||
apiKey = (this.credentialsProvider.GetKey(AICredentialScope.AdvancedAI) ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
throw new InvalidOperationException($"An API key is required for {serviceType} but none was found in the credential vault.");
|
||||
}
|
||||
}
|
||||
|
||||
var endpoint = string.IsNullOrWhiteSpace(config.EndpointUrl) ? null : config.EndpointUrl.Trim();
|
||||
|
||||
switch (serviceType)
|
||||
{
|
||||
case AIServiceType.OpenAI:
|
||||
kernelBuilder.AddOpenAIChatCompletion(modelName, apiKey, serviceId: modelName);
|
||||
break;
|
||||
case AIServiceType.AzureOpenAI:
|
||||
var deploymentName = string.IsNullOrWhiteSpace(config.DeploymentName) ? modelName : config.DeploymentName;
|
||||
kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName, RequireEndpoint(endpoint, serviceType), apiKey, serviceId: modelName);
|
||||
break;
|
||||
case AIServiceType.Mistral:
|
||||
kernelBuilder.AddMistralChatCompletion(modelName, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.Google:
|
||||
kernelBuilder.AddGoogleAIGeminiChatCompletion(modelName, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.HuggingFace:
|
||||
kernelBuilder.AddHuggingFaceChatCompletion(modelName, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.AzureAIInference:
|
||||
kernelBuilder.AddAzureAIInferenceChatCompletion(modelName, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.Ollama:
|
||||
kernelBuilder.AddOllamaChatCompletion(modelName, endpoint: new Uri(endpoint));
|
||||
break;
|
||||
case AIServiceType.Anthropic:
|
||||
kernelBuilder.AddBedrockChatCompletionService(modelName);
|
||||
break;
|
||||
case AIServiceType.AmazonBedrock:
|
||||
kernelBuilder.AddBedrockChatCompletionService(modelName);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Service type '{config.ServiceType}' is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage)
|
||||
{
|
||||
return AIServiceUsageHelper.GetOpenAIServiceUsage(chatMessage);
|
||||
}
|
||||
|
||||
private AdvancedAIConfiguration GetConfiguration()
|
||||
{
|
||||
var config = this.UserSettings?.AdvancedAIConfiguration;
|
||||
if (config is null)
|
||||
{
|
||||
return new AdvancedAIConfiguration();
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private string GetModelName()
|
||||
{
|
||||
return GetModelName(this.GetConfiguration());
|
||||
}
|
||||
|
||||
private static string GetModelName(AdvancedAIConfiguration config)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(config?.ModelName))
|
||||
{
|
||||
return config.ModelName;
|
||||
}
|
||||
|
||||
return "gpt-4o";
|
||||
}
|
||||
|
||||
private static bool RequiresApiKey(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private static string RequireEndpoint(string endpoint, AIServiceType serviceType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Endpoint is required for {serviceType} configuration but was not provided.");
|
||||
}
|
||||
|
||||
private PromptExecutionSettings CreatePromptExecutionSettings()
|
||||
{
|
||||
var serviceType = GetConfiguration().ServiceTypeKind;
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
||||
Temperature = 0.01,
|
||||
},
|
||||
_ => new PromptExecutionSettings(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class CustomActionTransformResult
|
||||
{
|
||||
public CustomActionTransformResult(string content, AIServiceUsage usage)
|
||||
{
|
||||
Content = content;
|
||||
Usage = usage;
|
||||
}
|
||||
|
||||
public string Content { get; }
|
||||
|
||||
public AIServiceUsage Usage { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Settings;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class CustomActionTransformService : ICustomActionTransformService
|
||||
{
|
||||
private const string DefaultSystemPrompt = """
|
||||
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.
|
||||
""";
|
||||
|
||||
private readonly IPromptModerationService promptModerationService;
|
||||
private readonly IPasteAIProviderFactory providerFactory;
|
||||
private readonly IAICredentialsProvider credentialsProvider;
|
||||
private readonly IUserSettings userSettings;
|
||||
|
||||
public CustomActionTransformService(IPromptModerationService promptModerationService, IPasteAIProviderFactory providerFactory, IAICredentialsProvider credentialsProvider, IUserSettings userSettings)
|
||||
{
|
||||
this.promptModerationService = promptModerationService;
|
||||
this.providerFactory = providerFactory;
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
this.userSettings = userSettings;
|
||||
}
|
||||
|
||||
public async Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var pasteConfig = userSettings?.PasteAIConfiguration;
|
||||
var providerConfig = BuildProviderConfig(pasteConfig);
|
||||
|
||||
return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress);
|
||||
}
|
||||
|
||||
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providerConfig);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
Logger.LogWarning("Clipboard has no usable text data");
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
var systemPrompt = providerConfig.SystemPrompt ?? DefaultSystemPrompt;
|
||||
|
||||
var fullPrompt = (systemPrompt ?? string.Empty) + "\n\n" + (inputText ?? string.Empty);
|
||||
|
||||
if (ShouldModerate(providerConfig))
|
||||
{
|
||||
await promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var provider = providerFactory.CreateProvider(providerConfig);
|
||||
|
||||
var request = new PasteAIRequest
|
||||
{
|
||||
Prompt = prompt,
|
||||
InputText = inputText,
|
||||
SystemPrompt = systemPrompt,
|
||||
};
|
||||
|
||||
var providerContent = await provider.ProcessPasteAsync(
|
||||
request,
|
||||
cancellationToken,
|
||||
progress);
|
||||
|
||||
var usage = request.Usage;
|
||||
var content = providerContent ?? string.Empty;
|
||||
|
||||
Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}");
|
||||
|
||||
return new CustomActionTransformResult(content, usage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} failed", ex);
|
||||
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
throw new PasteActionException(ErrorHelpers.TranslateErrorText(-1), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
|
||||
}
|
||||
|
||||
private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config)
|
||||
{
|
||||
config ??= new PasteAIConfiguration();
|
||||
var serviceType = NormalizeServiceType(config.ServiceTypeKind);
|
||||
var systemPrompt = string.IsNullOrWhiteSpace(config.SystemPrompt) ? DefaultSystemPrompt : config.SystemPrompt;
|
||||
var apiKey = AcquireApiKey(serviceType);
|
||||
var modelName = config.ModelName;
|
||||
|
||||
var providerConfig = new PasteAIConfig
|
||||
{
|
||||
ProviderType = serviceType,
|
||||
ApiKey = apiKey,
|
||||
Model = modelName,
|
||||
Endpoint = config.EndpointUrl,
|
||||
DeploymentName = config.DeploymentName,
|
||||
ModelPath = config.ModelPath,
|
||||
SystemPrompt = systemPrompt,
|
||||
ModerationEnabled = config.ModerationEnabled,
|
||||
};
|
||||
|
||||
return providerConfig;
|
||||
}
|
||||
|
||||
private string AcquireApiKey(AIServiceType serviceType)
|
||||
{
|
||||
if (!RequiresApiKey(serviceType))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
credentialsProvider.Refresh(AICredentialScope.PasteAI);
|
||||
return credentialsProvider.GetKey(AICredentialScope.PasteAI) ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool RequiresApiKey(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.Onnx => false,
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ShouldModerate(PasteAIConfig providerConfig)
|
||||
{
|
||||
if (providerConfig is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return providerConfig.ProviderType == AIServiceType.OpenAI && providerConfig.ModerationEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// 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;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Models;
|
||||
using LanguageModelProvider;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions;
|
||||
|
||||
public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
{
|
||||
private static readonly IReadOnlyCollection<AIServiceType> SupportedTypes = new[]
|
||||
{
|
||||
AIServiceType.FoundryLocal,
|
||||
};
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new FoundryLocalPasteProvider(config));
|
||||
|
||||
private static readonly LanguageModelService LanguageModels = LanguageModelService.CreateDefault();
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
|
||||
public FoundryLocalPasteProvider(PasteAIConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey();
|
||||
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model;
|
||||
|
||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await FoundryLocalModelProvider.Instance.IsAvailable().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var systemPrompt = request.SystemPrompt;
|
||||
if (string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
throw new ArgumentException("System prompt must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var prompt = request.Prompt;
|
||||
var inputText = request.InputText;
|
||||
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
throw new ArgumentException("Prompt and input text must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var modelReference = _config?.Model;
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
throw new InvalidOperationException("Foundry Local requires a model identifier (for example, 'fl://model-name').");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var chatClient = LanguageModels.GetClient(modelReference);
|
||||
if (chatClient is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to resolve Foundry Local client for '{modelReference}'. Ensure the model is downloaded.");
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Text:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
|
||||
var chatMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.System, systemPrompt),
|
||||
new(ChatRole.User, userMessageContent),
|
||||
};
|
||||
|
||||
var chatOptions = CreateChatOptions(_config?.SystemPrompt, modelReference);
|
||||
|
||||
progress?.Report(0.1);
|
||||
|
||||
var response = await chatClient.GetResponseAsync(chatMessages, chatOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress?.Report(0.8);
|
||||
|
||||
var responseText = GetResponseText(response);
|
||||
request.Usage = ToUsage(response.Usage);
|
||||
|
||||
progress?.Report(1.0);
|
||||
|
||||
return responseText ?? string.Empty;
|
||||
}
|
||||
|
||||
private static ChatOptions CreateChatOptions(string systemPrompt, string modelReference)
|
||||
{
|
||||
var options = new ChatOptions
|
||||
{
|
||||
ModelId = modelReference,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
options.Instructions = systemPrompt;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static string GetResponseText(ChatResponse response)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(response.Text))
|
||||
{
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
if (response.Messages is { Count: > 0 })
|
||||
{
|
||||
var lastMessage = response.Messages.LastOrDefault(m => !string.IsNullOrWhiteSpace(m.Text));
|
||||
if (!string.IsNullOrWhiteSpace(lastMessage?.Text))
|
||||
{
|
||||
return lastMessage.Text;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static AIServiceUsage ToUsage(UsageDetails usageDetails)
|
||||
{
|
||||
if (usageDetails is null)
|
||||
{
|
||||
return AIServiceUsage.None;
|
||||
}
|
||||
|
||||
int promptTokens = (int)(usageDetails.InputTokenCount ?? 0);
|
||||
int completionTokens = (int)(usageDetails.OutputTokenCount ?? 0);
|
||||
|
||||
if (promptTokens == 0 && completionTokens == 0)
|
||||
{
|
||||
return AIServiceUsage.None;
|
||||
}
|
||||
|
||||
return new AIServiceUsage(promptTokens, completionTokens);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Settings;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface ICustomActionTransformService
|
||||
{
|
||||
Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface IPasteAIProvider
|
||||
{
|
||||
Task<bool> IsAvailableAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface IPasteAIProviderFactory
|
||||
{
|
||||
IPasteAIProvider CreateProvider(PasteAIConfig config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class LocalModelPasteProvider : IPasteAIProvider
|
||||
{
|
||||
private static readonly IReadOnlyCollection<AIServiceType> SupportedTypes = new[]
|
||||
{
|
||||
AIServiceType.Onnx,
|
||||
AIServiceType.ML,
|
||||
};
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new LocalModelPasteProvider(config));
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
|
||||
public LocalModelPasteProvider(PasteAIConfig config)
|
||||
{
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
public Task<bool> IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true);
|
||||
|
||||
public Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
// TODO: Implement local model inference logic using _config.LocalModelPath/_config.ModelPath
|
||||
var content = request.InputText ?? string.Empty;
|
||||
request.Usage = AIServiceUsage.None;
|
||||
return Task.FromResult(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public class PasteAIConfig
|
||||
{
|
||||
public AIServiceType ProviderType { get; set; }
|
||||
|
||||
public string Model { get; set; }
|
||||
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
public string DeploymentName { get; set; }
|
||||
|
||||
public string LocalModelPath { get; set; }
|
||||
|
||||
public string ModelPath { get; set; }
|
||||
|
||||
public string SystemPrompt { get; set; }
|
||||
|
||||
public bool ModerationEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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 Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class PasteAIProviderFactory : IPasteAIProviderFactory
|
||||
{
|
||||
private static readonly IReadOnlyList<PasteAIProviderRegistration> ProviderRegistrations = new[]
|
||||
{
|
||||
SemanticKernelPasteProvider.Registration,
|
||||
LocalModelPasteProvider.Registration,
|
||||
FoundryLocalPasteProvider.Registration,
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>> ProviderFactories = CreateProviderFactories();
|
||||
|
||||
public IPasteAIProvider CreateProvider(PasteAIConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
var serviceType = config.ProviderType;
|
||||
if (serviceType == AIServiceType.Unknown)
|
||||
{
|
||||
serviceType = AIServiceType.OpenAI;
|
||||
config.ProviderType = serviceType;
|
||||
}
|
||||
|
||||
if (!ProviderFactories.TryGetValue(serviceType, out var factory))
|
||||
{
|
||||
throw new NotSupportedException($"Provider {config.ProviderType} not supported");
|
||||
}
|
||||
|
||||
return factory(config);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>> CreateProviderFactories()
|
||||
{
|
||||
var map = new Dictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>>();
|
||||
|
||||
foreach (var registration in ProviderRegistrations)
|
||||
{
|
||||
Register(map, registration.SupportedTypes, registration.Factory);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void Register(Dictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>> map, IReadOnlyCollection<AIServiceType> types, Func<PasteAIConfig, IPasteAIProvider> factory)
|
||||
{
|
||||
foreach (var type in types)
|
||||
{
|
||||
map[type] = factory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class PasteAIProviderRegistration
|
||||
{
|
||||
public PasteAIProviderRegistration(IReadOnlyCollection<Microsoft.PowerToys.Settings.UI.Library.AIServiceType> supportedTypes, Func<PasteAIConfig, IPasteAIProvider> factory)
|
||||
{
|
||||
SupportedTypes = supportedTypes ?? throw new ArgumentNullException(nameof(supportedTypes));
|
||||
Factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<Microsoft.PowerToys.Settings.UI.Library.AIServiceType> SupportedTypes { get; }
|
||||
|
||||
public Func<PasteAIConfig, IPasteAIProvider> Factory { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class PasteAIRequest
|
||||
{
|
||||
public string Prompt { get; init; }
|
||||
|
||||
public string InputText { get; init; }
|
||||
|
||||
public string SystemPrompt { get; init; }
|
||||
|
||||
public AIServiceUsage Usage { get; set; } = AIServiceUsage.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.Amazon;
|
||||
using Microsoft.SemanticKernel.Connectors.AzureAIInference;
|
||||
using Microsoft.SemanticKernel.Connectors.Google;
|
||||
using Microsoft.SemanticKernel.Connectors.HuggingFace;
|
||||
using Microsoft.SemanticKernel.Connectors.MistralAI;
|
||||
using Microsoft.SemanticKernel.Connectors.Ollama;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class SemanticKernelPasteProvider : IPasteAIProvider
|
||||
{
|
||||
private static readonly IReadOnlyCollection<AIServiceType> SupportedTypes = new[]
|
||||
{
|
||||
AIServiceType.OpenAI,
|
||||
AIServiceType.AzureOpenAI,
|
||||
AIServiceType.Mistral,
|
||||
AIServiceType.Google,
|
||||
AIServiceType.HuggingFace,
|
||||
AIServiceType.AzureAIInference,
|
||||
AIServiceType.Ollama,
|
||||
AIServiceType.Anthropic,
|
||||
AIServiceType.AmazonBedrock,
|
||||
};
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new SemanticKernelPasteProvider(config));
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
private readonly AIServiceType _serviceType;
|
||||
|
||||
public SemanticKernelPasteProvider(PasteAIConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_config = config;
|
||||
_serviceType = config.ProviderType;
|
||||
if (_serviceType == AIServiceType.Unknown)
|
||||
{
|
||||
_serviceType = AIServiceType.OpenAI;
|
||||
_config.ProviderType = _serviceType;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<AIServiceType> SupportedServiceTypes => SupportedTypes;
|
||||
|
||||
public Task<bool> IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true);
|
||||
|
||||
public async Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var systemPrompt = request.SystemPrompt;
|
||||
if (string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
throw new ArgumentException("System prompt must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var prompt = request.Prompt;
|
||||
var inputText = request.InputText;
|
||||
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
throw new ArgumentException("Prompt and input text must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
|
||||
var executionSettings = CreateExecutionSettings();
|
||||
var kernel = CreateKernel();
|
||||
var modelId = _config.Model;
|
||||
|
||||
IChatCompletionService chatService;
|
||||
if (!string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
try
|
||||
{
|
||||
chatService = kernel.GetRequiredService<IChatCompletionService>(modelId);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
chatService = kernel.GetRequiredService<IChatCompletionService>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
chatService = kernel.GetRequiredService<IChatCompletionService>();
|
||||
}
|
||||
|
||||
var chatHistory = new ChatHistory();
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddUserMessage(userMessageContent);
|
||||
|
||||
var response = await chatService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel, cancellationToken);
|
||||
chatHistory.Add(response);
|
||||
|
||||
request.Usage = AIServiceUsageHelper.GetOpenAIServiceUsage(response);
|
||||
return response.Content;
|
||||
}
|
||||
|
||||
private Kernel CreateKernel()
|
||||
{
|
||||
var kernelBuilder = Kernel.CreateBuilder();
|
||||
var endpoint = string.IsNullOrWhiteSpace(_config.Endpoint) ? null : _config.Endpoint.Trim();
|
||||
var apiKey = _config.ApiKey?.Trim() ?? string.Empty;
|
||||
|
||||
if (RequiresApiKey(_serviceType) && string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
throw new InvalidOperationException($"API key is required for {_serviceType} but was not provided.");
|
||||
}
|
||||
|
||||
switch (_serviceType)
|
||||
{
|
||||
case AIServiceType.OpenAI:
|
||||
kernelBuilder.AddOpenAIChatCompletion(_config.Model, apiKey, serviceId: _config.Model);
|
||||
break;
|
||||
case AIServiceType.AzureOpenAI:
|
||||
var deploymentName = string.IsNullOrWhiteSpace(_config.DeploymentName) ? _config.Model : _config.DeploymentName;
|
||||
kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey, serviceId: _config.Model);
|
||||
break;
|
||||
case AIServiceType.Mistral:
|
||||
kernelBuilder.AddMistralChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.Google:
|
||||
kernelBuilder.AddGoogleAIGeminiChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.HuggingFace:
|
||||
kernelBuilder.AddHuggingFaceChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.AzureAIInference:
|
||||
kernelBuilder.AddAzureAIInferenceChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.Ollama:
|
||||
kernelBuilder.AddOllamaChatCompletion(_config.Model, endpoint: new Uri(endpoint));
|
||||
break;
|
||||
case AIServiceType.Anthropic:
|
||||
kernelBuilder.AddBedrockChatCompletionService(_config.Model);
|
||||
break;
|
||||
case AIServiceType.AmazonBedrock:
|
||||
kernelBuilder.AddBedrockChatCompletionService(_config.Model);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Provider '{_config.ProviderType}' is not supported by {nameof(SemanticKernelPasteProvider)}");
|
||||
}
|
||||
|
||||
return kernelBuilder.Build();
|
||||
}
|
||||
|
||||
private PromptExecutionSettings CreateExecutionSettings()
|
||||
{
|
||||
return _serviceType switch
|
||||
{
|
||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
|
||||
{
|
||||
Temperature = 0.01,
|
||||
MaxTokens = 2000,
|
||||
FunctionChoiceBehavior = null,
|
||||
},
|
||||
_ => new PromptExecutionSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool RequiresApiKey(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private static string RequireEndpoint(string endpoint, AIServiceType serviceType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Endpoint is required for {serviceType} but was not provided.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// 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 AdvancedPaste.Settings;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Windows.Security.Credentials;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Enhanced credentials provider that supports different AI service types
|
||||
/// Keys are stored in Windows Credential Vault with service-specific identifiers
|
||||
/// </summary>
|
||||
public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
|
||||
{
|
||||
private sealed class CredentialSlot
|
||||
{
|
||||
public AIServiceType ServiceType { get; set; } = AIServiceType.Unknown;
|
||||
|
||||
public (string Resource, string Username)? Entry { get; set; }
|
||||
|
||||
public string Key { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly Dictionary<AICredentialScope, CredentialSlot> _slots;
|
||||
private readonly object _syncRoot = new();
|
||||
|
||||
public EnhancedVaultCredentialsProvider(IUserSettings userSettings)
|
||||
{
|
||||
_userSettings = userSettings ?? throw new ArgumentNullException(nameof(userSettings));
|
||||
|
||||
_slots = new Dictionary<AICredentialScope, CredentialSlot>
|
||||
{
|
||||
[AICredentialScope.PasteAI] = new CredentialSlot(),
|
||||
[AICredentialScope.AdvancedAI] = new CredentialSlot(),
|
||||
};
|
||||
|
||||
Refresh(AICredentialScope.PasteAI);
|
||||
Refresh(AICredentialScope.AdvancedAI);
|
||||
}
|
||||
|
||||
public string GetKey(AICredentialScope scope)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
UpdateSlot(scope, forceRefresh: false);
|
||||
return _slots[scope].Key;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsConfigured(AICredentialScope scope)
|
||||
{
|
||||
return !string.IsNullOrEmpty(GetKey(scope));
|
||||
}
|
||||
|
||||
public bool Refresh(AICredentialScope scope)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return UpdateSlot(scope, forceRefresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UpdateSlot(AICredentialScope scope, bool forceRefresh)
|
||||
{
|
||||
var slot = _slots[scope];
|
||||
var desiredServiceType = NormalizeServiceType(ResolveServiceType(scope));
|
||||
|
||||
var hasChanged = false;
|
||||
|
||||
if (slot.ServiceType != desiredServiceType)
|
||||
{
|
||||
slot.ServiceType = desiredServiceType;
|
||||
slot.Entry = BuildCredentialEntry(desiredServiceType, scope);
|
||||
forceRefresh = true;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
if (!forceRefresh)
|
||||
{
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
var newKey = LoadKey(slot.Entry);
|
||||
if (!string.Equals(slot.Key, newKey, StringComparison.Ordinal))
|
||||
{
|
||||
slot.Key = newKey;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
private AIServiceType ResolveServiceType(AICredentialScope scope)
|
||||
{
|
||||
return scope switch
|
||||
{
|
||||
AICredentialScope.AdvancedAI => _userSettings.AdvancedAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI,
|
||||
AICredentialScope.PasteAI => _userSettings.PasteAIConfiguration?.ServiceTypeKind ?? AIServiceType.OpenAI,
|
||||
_ => AIServiceType.OpenAI,
|
||||
};
|
||||
}
|
||||
|
||||
private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
|
||||
}
|
||||
|
||||
private static string LoadKey((string Resource, string Username)? entry)
|
||||
{
|
||||
if (entry is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var credential = new PasswordVault().Retrieve(entry.Value.Resource, entry.Value.Username);
|
||||
return credential?.Password ?? string.Empty;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static (string Resource, string Username)? BuildCredentialEntry(AIServiceType serviceType, AICredentialScope scope)
|
||||
{
|
||||
return scope switch
|
||||
{
|
||||
AICredentialScope.AdvancedAI => GetAdvancedAiEntry(serviceType),
|
||||
AICredentialScope.PasteAI => GetPasteAiEntry(serviceType),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static (string Resource, string Username)? GetAdvancedAiEntry(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.OpenAI => ("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_AdvancedAI_OpenAI"),
|
||||
AIServiceType.AzureOpenAI => ("https://azure.microsoft.com/products/ai-services/openai-service", "PowerToys_AdvancedPaste_AdvancedAI_AzureOpenAI"),
|
||||
AIServiceType.AzureAIInference => ("https://azure.microsoft.com/products/ai-services/ai-inference", "PowerToys_AdvancedPaste_AdvancedAI_AzureAIInference"),
|
||||
AIServiceType.Mistral => ("https://console.mistral.ai/account/api-keys", "PowerToys_AdvancedPaste_AdvancedAI_Mistral"),
|
||||
AIServiceType.Google => ("https://ai.google.dev/", "PowerToys_AdvancedPaste_AdvancedAI_Google"),
|
||||
AIServiceType.HuggingFace => ("https://huggingface.co/settings/tokens", "PowerToys_AdvancedPaste_AdvancedAI_HuggingFace"),
|
||||
AIServiceType.Ollama => null,
|
||||
AIServiceType.Anthropic => null,
|
||||
AIServiceType.AmazonBedrock => null,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static (string Resource, string Username)? GetPasteAiEntry(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,38 @@
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the scope a credential lookup is targeting.
|
||||
/// </summary>
|
||||
public enum AICredentialScope
|
||||
{
|
||||
PasteAI,
|
||||
AdvancedAI,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to AI credentials stored for Advanced Paste scenarios.
|
||||
/// </summary>
|
||||
public interface IAICredentialsProvider
|
||||
{
|
||||
bool IsConfigured { get; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the specified scope has a configured credential.
|
||||
/// </summary>
|
||||
/// <param name="scope">Scope to evaluate.</param>
|
||||
/// <returns><see langword="true"/> when a non-empty credential exists for the scope.</returns>
|
||||
bool IsConfigured(AICredentialScope scope);
|
||||
|
||||
string Key { get; }
|
||||
/// <summary>
|
||||
/// Retrieves the credential for the requested scope.
|
||||
/// </summary>
|
||||
/// <param name="scope">Scope to evaluate.</param>
|
||||
/// <returns>Credential string or <see cref="string.Empty"/> when missing.</returns>
|
||||
string GetKey(AICredentialScope scope);
|
||||
|
||||
bool Refresh();
|
||||
/// <summary>
|
||||
/// Refreshes the cached credential for the provided scope.
|
||||
/// </summary>
|
||||
/// <param name="scope">Scope to refresh.</param>
|
||||
/// <returns><see langword="true"/> when the credential changed.</returns>
|
||||
bool Refresh(AICredentialScope scope);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public interface ICustomTextTransformService
|
||||
{
|
||||
Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
@@ -5,15 +5,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Models.KernelQueryCache;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.Telemetry;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
@@ -21,15 +22,20 @@ using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheService, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) : IKernelService
|
||||
public abstract class KernelServiceBase(
|
||||
IKernelQueryCacheService queryCacheService,
|
||||
IPromptModerationService promptModerationService,
|
||||
IUserSettings userSettings,
|
||||
ICustomActionTransformService customActionTransformService) : IKernelService
|
||||
{
|
||||
private const string PromptParameterName = "prompt";
|
||||
|
||||
private readonly IKernelQueryCacheService _queryCacheService = queryCacheService;
|
||||
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
||||
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
|
||||
private readonly IUserSettings _userSettings = userSettings;
|
||||
private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
|
||||
|
||||
protected abstract string ModelName { get; }
|
||||
protected abstract string AdvancedAIModelName { get; }
|
||||
|
||||
protected abstract PromptExecutionSettings PromptExecutionSettings { get; }
|
||||
|
||||
@@ -144,9 +150,12 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
|
||||
if (ShouldModerateAdvancedAI())
|
||||
{
|
||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
|
||||
}
|
||||
|
||||
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
|
||||
var chatResult = await kernel.GetRequiredService<IChatCompletionService>(AdvancedAIModelName)
|
||||
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken);
|
||||
chatHistory.Add(chatResult);
|
||||
|
||||
@@ -175,9 +184,11 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
return ([], AIServiceUsage.None);
|
||||
}
|
||||
|
||||
protected IUserSettings UserSettings => _userSettings;
|
||||
|
||||
private void LogResult(bool cacheUsed, bool isSavedQuery, IEnumerable<ActionChainItem> actionChain, AIServiceUsage usage)
|
||||
{
|
||||
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, ModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
|
||||
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, AdvancedAIModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
var logEvent = new AIServiceFormatEvent(telemetryEvent);
|
||||
Logger.LogDebug($"{nameof(TransformClipboardAsync)} complete; {logEvent.ToJsonString()}");
|
||||
@@ -191,20 +202,96 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
return kernelBuilder.Build();
|
||||
}
|
||||
|
||||
private IEnumerable<KernelFunction> GetKernelFunctions() =>
|
||||
from format in Enum.GetValues<PasteFormats>()
|
||||
let metadata = PasteFormat.MetadataDict[format]
|
||||
let coreDescription = metadata.KernelFunctionDescription
|
||||
where !string.IsNullOrEmpty(coreDescription)
|
||||
let requiresPrompt = metadata.RequiresPrompt
|
||||
orderby requiresPrompt descending
|
||||
select KernelFunctionFactory.CreateFromMethod(
|
||||
method: requiresPrompt ? async (Kernel kernel, string prompt) => await ExecutePromptTransformAsync(kernel, format, prompt)
|
||||
: async (Kernel kernel) => await ExecuteStandardTransformAsync(kernel, format),
|
||||
functionName: format.ToString(),
|
||||
description: requiresPrompt ? coreDescription : $"{coreDescription} Puts the result back on the clipboard.",
|
||||
parameters: requiresPrompt ? [new(PromptParameterName) { Description = "Input instructions to AI", ParameterType = typeof(string) }] : null,
|
||||
returnParameter: new() { Description = "Array of available clipboard formats after operation" });
|
||||
private IEnumerable<KernelFunction> GetKernelFunctions()
|
||||
{
|
||||
// Get standard format functions
|
||||
var standardFunctions =
|
||||
from format in Enum.GetValues<PasteFormats>()
|
||||
let metadata = PasteFormat.MetadataDict[format]
|
||||
let coreDescription = metadata.KernelFunctionDescription
|
||||
where !string.IsNullOrEmpty(coreDescription)
|
||||
let requiresPrompt = metadata.RequiresPrompt
|
||||
orderby requiresPrompt descending
|
||||
select KernelFunctionFactory.CreateFromMethod(
|
||||
method: requiresPrompt ? async (Kernel kernel, string prompt) => await ExecutePromptTransformAsync(kernel, format, prompt)
|
||||
: async (Kernel kernel) => await ExecuteStandardTransformAsync(kernel, format),
|
||||
functionName: format.ToString(),
|
||||
description: requiresPrompt ? coreDescription : $"{coreDescription} Puts the result back on the clipboard.",
|
||||
parameters: requiresPrompt ? [new(PromptParameterName) { Description = "Input instructions to AI", ParameterType = typeof(string) }] : null,
|
||||
returnParameter: new() { Description = "Array of available clipboard formats after operation" });
|
||||
|
||||
HashSet<string> usedFunctionNames = new(Enum.GetNames<PasteFormats>(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Get custom action functions
|
||||
var customActionFunctions = _userSettings.CustomActions
|
||||
.Where(customAction => !string.IsNullOrWhiteSpace(customAction.Name) && !string.IsNullOrWhiteSpace(customAction.Prompt))
|
||||
.Select(customAction =>
|
||||
{
|
||||
var sanitizedBaseName = SanitizeFunctionName(customAction.Name);
|
||||
var functionName = GetUniqueFunctionName(sanitizedBaseName, usedFunctionNames, customAction.Id);
|
||||
var description = string.IsNullOrWhiteSpace(customAction.Description)
|
||||
? $"Runs the \"{customAction.Name}\" custom action."
|
||||
: customAction.Description;
|
||||
return KernelFunctionFactory.CreateFromMethod(
|
||||
method: async (Kernel kernel) => await ExecuteCustomActionAsync(kernel, customAction.Prompt),
|
||||
functionName: functionName,
|
||||
description: description,
|
||||
parameters: null,
|
||||
returnParameter: new() { Description = "Array of available clipboard formats after operation" });
|
||||
});
|
||||
|
||||
return standardFunctions.Concat(customActionFunctions);
|
||||
}
|
||||
|
||||
private static string GetUniqueFunctionName(string baseName, HashSet<string> usedFunctionNames, int customActionId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(usedFunctionNames);
|
||||
|
||||
var candidate = string.IsNullOrEmpty(baseName) ? "_CustomAction" : baseName;
|
||||
|
||||
if (usedFunctionNames.Add(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
int suffix = 1;
|
||||
while (true)
|
||||
{
|
||||
var nextCandidate = $"{candidate}_{customActionId}_{suffix}";
|
||||
if (usedFunctionNames.Add(nextCandidate))
|
||||
{
|
||||
return nextCandidate;
|
||||
}
|
||||
|
||||
suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeFunctionName(string name)
|
||||
{
|
||||
// Remove invalid characters and ensure the function name is valid for kernel
|
||||
var sanitized = new string(name.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray());
|
||||
|
||||
// Ensure it starts with a letter or underscore
|
||||
if (sanitized.Length > 0 && !char.IsLetter(sanitized[0]) && sanitized[0] != '_')
|
||||
{
|
||||
sanitized = "_" + sanitized;
|
||||
}
|
||||
|
||||
// Ensure it's not empty
|
||||
return string.IsNullOrEmpty(sanitized) ? "_CustomAction" : sanitized;
|
||||
}
|
||||
|
||||
private Task<string> ExecuteCustomActionAsync(Kernel kernel, string fixedPrompt) =>
|
||||
ExecuteTransformAsync(
|
||||
kernel,
|
||||
new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty);
|
||||
});
|
||||
|
||||
private Task<string> ExecutePromptTransformAsync(Kernel kernel, PasteFormats format, string prompt) =>
|
||||
ExecuteTransformAsync(
|
||||
@@ -212,7 +299,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetTextAsync();
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(output);
|
||||
});
|
||||
@@ -220,7 +307,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
format switch
|
||||
{
|
||||
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty,
|
||||
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
||||
};
|
||||
|
||||
@@ -281,4 +368,16 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
var usageString = usage.HasUsage ? $" [{usage}]" : string.Empty;
|
||||
return $"-> {role}: {redactedContent}{usageString}";
|
||||
}
|
||||
|
||||
private bool ShouldModerateAdvancedAI()
|
||||
{
|
||||
var config = _userSettings?.AdvancedAIConfiguration ?? new AdvancedAIConfiguration();
|
||||
var serviceType = NormalizeServiceType(config.ServiceTypeKind);
|
||||
return serviceType == AIServiceType.OpenAI && config.ModerationEnabled;
|
||||
}
|
||||
|
||||
private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// 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.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Telemetry;
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
|
||||
namespace AdvancedPaste.Services.OpenAI;
|
||||
|
||||
public sealed class CustomTextTransformService(IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService) : ICustomTextTransformService
|
||||
{
|
||||
private const string ModelName = "gpt-3.5-turbo-instruct";
|
||||
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
||||
|
||||
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
var fullPrompt = systemInstructions + "\n\n" + userMessage;
|
||||
|
||||
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
|
||||
|
||||
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
|
||||
|
||||
var response = await azureAIClient.GetCompletionsAsync(
|
||||
new()
|
||||
{
|
||||
DeploymentName = ModelName,
|
||||
Prompts =
|
||||
{
|
||||
fullPrompt,
|
||||
},
|
||||
Temperature = 0.01F,
|
||||
MaxTokens = 2000,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
if (response.Value.Choices[0].FinishReason == "length")
|
||||
{
|
||||
Logger.LogDebug("Cut off due to length constraints");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
Logger.LogWarning("Clipboard has no usable text data");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string systemInstructions =
|
||||
$@"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.";
|
||||
|
||||
string userMessage =
|
||||
$@"User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
|
||||
|
||||
var usage = response.Usage;
|
||||
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
var logEvent = new AIServiceFormatEvent(telemetryEvent);
|
||||
|
||||
Logger.LogDebug($"{nameof(TransformTextAsync)} complete; {logEvent.ToJsonString()}");
|
||||
|
||||
return response.Choices[0].Text;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{nameof(TransformTextAsync)} failed", ex);
|
||||
|
||||
AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PasteActionException(ErrorHelpers.TranslateErrorText((ex as RequestFailedException)?.Status ?? -1), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
using Azure.AI.OpenAI;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services.OpenAI;
|
||||
|
||||
public sealed class KernelService(IKernelQueryCacheService queryCacheService, IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) :
|
||||
KernelServiceBase(queryCacheService, promptModerationService, customTextTransformService)
|
||||
{
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||
|
||||
protected override string ModelName => "gpt-4o";
|
||||
|
||||
protected override PromptExecutionSettings PromptExecutionSettings =>
|
||||
new OpenAIPromptExecutionSettings()
|
||||
{
|
||||
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
|
||||
Temperature = 0.01,
|
||||
};
|
||||
|
||||
protected override void AddChatCompletionService(IKernelBuilder kernelBuilder) => kernelBuilder.AddOpenAIChatCompletion(ModelName, _aiCredentialsProvider.Key);
|
||||
|
||||
protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage) =>
|
||||
chatMessage.Metadata?.GetValueOrDefault("Usage") is CompletionsUsage completionsUsage
|
||||
? new(PromptTokens: completionsUsage.PromptTokens, CompletionTokens: completionsUsage.CompletionTokens)
|
||||
: AIServiceUsage.None;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using ManagedCommon;
|
||||
using OpenAI.Moderations;
|
||||
|
||||
@@ -23,7 +24,24 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials
|
||||
{
|
||||
try
|
||||
{
|
||||
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
|
||||
_aiCredentialsProvider.Refresh(AICredentialScope.AdvancedAI);
|
||||
var apiKey = _aiCredentialsProvider.GetKey(AICredentialScope.AdvancedAI);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
_aiCredentialsProvider.Refresh(AICredentialScope.PasteAI);
|
||||
apiKey = _aiCredentialsProvider.GetKey(AICredentialScope.PasteAI);
|
||||
}
|
||||
|
||||
apiKey = apiKey?.Trim() ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
Logger.LogWarning("Skipping OpenAI moderation because no credential is configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
ModerationClient moderationClient = new(ModelName, apiKey);
|
||||
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
|
||||
var moderationResult = moderationClientResult.Value;
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// 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 Windows.Security.Credentials;
|
||||
|
||||
namespace AdvancedPaste.Services.OpenAI;
|
||||
|
||||
public sealed class VaultCredentialsProvider : IAICredentialsProvider
|
||||
{
|
||||
public VaultCredentialsProvider() => Refresh();
|
||||
|
||||
public string Key { get; private set; }
|
||||
|
||||
public bool IsConfigured => !string.IsNullOrEmpty(Key);
|
||||
|
||||
public bool Refresh()
|
||||
{
|
||||
var oldKey = Key;
|
||||
Key = LoadKey();
|
||||
return oldKey != Key;
|
||||
}
|
||||
|
||||
private static string LoadKey()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new PasswordVault().Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey")?.Password ?? string.Empty;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,16 @@ using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTextTransformService customTextTransformService) : IPasteFormatExecutor
|
||||
public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService) : IPasteFormatExecutor
|
||||
{
|
||||
private readonly IKernelService _kernelService = kernelService;
|
||||
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
|
||||
private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
|
||||
|
||||
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
@@ -36,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
|
||||
pasteFormat.Format switch
|
||||
{
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace AdvancedPaste.ViewModels
|
||||
private readonly DispatcherTimer _clipboardTimer;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider;
|
||||
private readonly IAICredentialsProvider _credentialsProvider;
|
||||
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
@@ -79,11 +79,24 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public ObservableCollection<PasteFormat> CustomActionPasteFormats { get; } = [];
|
||||
|
||||
public bool IsCustomAIServiceEnabled => IsAllowedByGPO && _aiCredentialsProvider.IsConfigured;
|
||||
public bool IsCustomAIServiceEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsAllowedByGPO || !_userSettings.IsAIEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We should handle the IsAIEnabled logic in settings, don't check again here.
|
||||
// If setting says yes, and here should pass check, and if error happens, it happens.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCustomAIAvailable => IsCustomAIServiceEnabled && ClipboardHasDataForCustomAI;
|
||||
|
||||
public bool IsAdvancedAIEnabled => IsCustomAIServiceEnabled && _userSettings.IsAdvancedAIEnabled;
|
||||
public bool IsAdvancedAIEnabled => IsAllowedByGPO && _userSettings.IsAIEnabled && _userSettings.IsAdvancedAIEnabled && _credentialsProvider.IsConfigured(AICredentialScope.AdvancedAI);
|
||||
|
||||
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
|
||||
|
||||
@@ -91,7 +104,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
|
||||
|
||||
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
|
||||
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled && _userSettings.IsAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
|
||||
|
||||
private bool Visible
|
||||
{
|
||||
@@ -110,9 +123,9 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public event EventHandler PreviewRequested;
|
||||
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider aiCredentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
{
|
||||
_aiCredentialsProvider = aiCredentialsProvider;
|
||||
_credentialsProvider = credentialsProvider;
|
||||
_userSettings = userSettings;
|
||||
_pasteFormatExecutor = pasteFormatExecutor;
|
||||
|
||||
@@ -164,6 +177,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
private void UserSettings_Changed(object sender, EventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsCustomAIServiceEnabled));
|
||||
OnPropertyChanged(nameof(ClipboardHasDataForCustomAI));
|
||||
OnPropertyChanged(nameof(IsCustomAIAvailable));
|
||||
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
|
||||
@@ -270,7 +284,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
GetMainWindow()?.FinishLoading(_aiCredentialsProvider.IsConfigured);
|
||||
GetMainWindow()?.FinishLoading(IsCustomAIServiceEnabled);
|
||||
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
|
||||
OnPropertyChanged(nameof(CustomAIUnavailableErrorText));
|
||||
OnPropertyChanged(nameof(IsCustomAIServiceEnabled));
|
||||
@@ -319,7 +333,7 @@ namespace AdvancedPaste.ViewModels
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
|
||||
}
|
||||
|
||||
if (!_aiCredentialsProvider.IsConfigured)
|
||||
if (!IsCustomAIServiceEnabled)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
|
||||
}
|
||||
@@ -519,7 +533,10 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
UpdateAllowedByGPO();
|
||||
|
||||
return IsAllowedByGPO && _aiCredentialsProvider.Refresh();
|
||||
var pasteKeyChanged = _credentialsProvider.Refresh(AICredentialScope.PasteAI);
|
||||
var advancedKeyChanged = _credentialsProvider.Refresh(AICredentialScope.AdvancedAI);
|
||||
|
||||
return pasteKeyChanged || advancedKeyChanged;
|
||||
}
|
||||
|
||||
public async Task CancelPasteActionAsync()
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/gpo.h>
|
||||
|
||||
#include <winrt/Windows.Security.Credentials.h>
|
||||
#include <vector>
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
|
||||
@@ -55,11 +54,10 @@ namespace
|
||||
const wchar_t JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY[] = L"paste-as-markdown-hotkey";
|
||||
const wchar_t JSON_KEY_PASTE_AS_JSON_HOTKEY[] = L"paste-as-json-hotkey";
|
||||
const wchar_t JSON_KEY_IS_ADVANCED_AI_ENABLED[] = L"IsAdvancedAIEnabled";
|
||||
const wchar_t JSON_KEY_IS_AI_ENABLED[] = L"IsAIEnabled";
|
||||
const wchar_t JSON_KEY_IS_OPEN_AI_ENABLED[] = L"IsOpenAIEnabled";
|
||||
const wchar_t JSON_KEY_SHOW_CUSTOM_PREVIEW[] = L"ShowCustomPreview";
|
||||
const wchar_t JSON_KEY_VALUE[] = L"value";
|
||||
|
||||
const wchar_t OPENAI_VAULT_RESOURCE[] = L"https://platform.openai.com/api-keys";
|
||||
const wchar_t OPENAI_VAULT_USERNAME[] = L"PowerToys_AdvancedPaste_OpenAIKey";
|
||||
}
|
||||
|
||||
class AdvancedPaste : public PowertoyModuleIface
|
||||
@@ -94,6 +92,7 @@ private:
|
||||
using CustomAction = ActionData<int>;
|
||||
std::vector<CustomAction> m_custom_actions;
|
||||
|
||||
bool m_is_ai_enabled = false;
|
||||
bool m_is_advanced_ai_enabled = false;
|
||||
bool m_preview_custom_format_output = true;
|
||||
|
||||
@@ -145,32 +144,11 @@ private:
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
static bool open_ai_key_exists()
|
||||
{
|
||||
try
|
||||
{
|
||||
winrt::Windows::Security::Credentials::PasswordVault().Retrieve(OPENAI_VAULT_RESOURCE, OPENAI_VAULT_USERNAME);
|
||||
return true;
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
// Looks like the only way to access the PasswordVault is through an API that throws an exception in case the resource doesn't exist.
|
||||
// If the debugger breaks here, just continue.
|
||||
// If you want to disable breaking here in a more permanent way, just add a condition in Visual Studio's Exception Settings to not break on win::hresult_error, but that might make you not hit other exceptions you might want to catch.
|
||||
if (ex.code() == HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
|
||||
{
|
||||
return false; // Credential doesn't exist.
|
||||
}
|
||||
Logger::error("Unexpected error while retrieving OpenAI key from vault: {}", winrt::to_string(ex.message()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_open_ai_enabled()
|
||||
bool is_ai_enabled()
|
||||
{
|
||||
return gpo_policy_enabled_configuration() != powertoys_gpo::gpo_rule_configured_disabled &&
|
||||
powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue() != powertoys_gpo::gpo_rule_configured_disabled &&
|
||||
open_ai_key_exists();
|
||||
m_is_ai_enabled;
|
||||
}
|
||||
|
||||
static std::wstring kebab_to_pascal_case(const std::wstring& kebab_str)
|
||||
@@ -341,7 +319,7 @@ private:
|
||||
if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS))
|
||||
{
|
||||
const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE);
|
||||
if (customActions.Size() > 0 && is_open_ai_enabled())
|
||||
if (customActions.Size() > 0 && is_ai_enabled())
|
||||
{
|
||||
for (const auto& customAction : customActions)
|
||||
{
|
||||
@@ -369,6 +347,23 @@ private:
|
||||
{
|
||||
m_is_advanced_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_ADVANCED_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_is_advanced_ai_enabled = false;
|
||||
}
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_is_ai_enabled = false;
|
||||
}
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
|
||||
{
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"properties":{"IsAdvancedAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}}},"name":"AdvancedPaste","version":"1"}
|
||||
{"properties":{"IsAdvancedAIEnabled":{"value":false},"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}}},"name":"AdvancedPaste","version":"1"}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores provider-specific configuration overrides so each AI service can keep distinct settings.
|
||||
/// </summary>
|
||||
public class AIProviderConfigurationSnapshot
|
||||
{
|
||||
[JsonPropertyName("model-name")]
|
||||
public string ModelName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("endpoint-url")]
|
||||
public string EndpointUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("api-version")]
|
||||
public string ApiVersion { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("deployment-name")]
|
||||
public string DeploymentName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("model-path")]
|
||||
public string ModelPath { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("system-prompt")]
|
||||
public string SystemPrompt { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("moderation-enabled")]
|
||||
public bool ModerationEnabled { get; set; } = true;
|
||||
}
|
||||
}
|
||||
26
src/settings-ui/Settings.UI.Library/AIServiceType.cs
Normal 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.
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Supported AI service types for PowerToys AI experiences.
|
||||
/// </summary>
|
||||
public enum AIServiceType
|
||||
{
|
||||
Unknown = 0,
|
||||
OpenAI,
|
||||
AzureOpenAI,
|
||||
Onnx,
|
||||
ML,
|
||||
FoundryLocal,
|
||||
Mistral,
|
||||
Google,
|
||||
HuggingFace,
|
||||
AzureAIInference,
|
||||
Ollama,
|
||||
Anthropic,
|
||||
AmazonBedrock,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public static class AIServiceTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a persisted string value into an <see cref="AIServiceType"/>.
|
||||
/// Supports historical casing and aliases.
|
||||
/// </summary>
|
||||
public static AIServiceType ToAIServiceType(this string serviceType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serviceType))
|
||||
{
|
||||
return AIServiceType.OpenAI;
|
||||
}
|
||||
|
||||
var normalized = serviceType.Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"openai" => AIServiceType.OpenAI,
|
||||
"azureopenai" or "azure" => AIServiceType.AzureOpenAI,
|
||||
"onnx" => AIServiceType.Onnx,
|
||||
"foundrylocal" or "foundry" or "fl" => AIServiceType.FoundryLocal,
|
||||
"ml" => AIServiceType.ML,
|
||||
"mistral" => AIServiceType.Mistral,
|
||||
"google" or "googleai" or "googlegemini" => AIServiceType.Google,
|
||||
"huggingface" => AIServiceType.HuggingFace,
|
||||
"azureaiinference" or "azureinference" => AIServiceType.AzureAIInference,
|
||||
"ollama" => AIServiceType.Ollama,
|
||||
"anthropic" => AIServiceType.Anthropic,
|
||||
"amazonbedrock" or "bedrock" => AIServiceType.AmazonBedrock,
|
||||
_ => AIServiceType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an <see cref="AIServiceType"/> to the canonical string used for persistence.
|
||||
/// </summary>
|
||||
public static string ToConfigurationString(this AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.OpenAI => "OpenAI",
|
||||
AIServiceType.AzureOpenAI => "AzureOpenAI",
|
||||
AIServiceType.Onnx => "Onnx",
|
||||
AIServiceType.FoundryLocal => "FoundryLocal",
|
||||
AIServiceType.ML => "ML",
|
||||
AIServiceType.Mistral => "Mistral",
|
||||
AIServiceType.Google => "Google",
|
||||
AIServiceType.HuggingFace => "HuggingFace",
|
||||
AIServiceType.AzureAIInference => "AzureAIInference",
|
||||
AIServiceType.Ollama => "Ollama",
|
||||
AIServiceType.Anthropic => "Anthropic",
|
||||
AIServiceType.AmazonBedrock => "AmazonBedrock",
|
||||
AIServiceType.Unknown => string.Empty,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, "Unsupported AI service type."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an <see cref="AIServiceType"/> into the normalized key used internally.
|
||||
/// </summary>
|
||||
public static string ToNormalizedKey(this AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.OpenAI => "openai",
|
||||
AIServiceType.AzureOpenAI => "azureopenai",
|
||||
AIServiceType.Onnx => "onnx",
|
||||
AIServiceType.FoundryLocal => "foundrylocal",
|
||||
AIServiceType.ML => "ml",
|
||||
AIServiceType.Mistral => "mistral",
|
||||
AIServiceType.Google => "google",
|
||||
AIServiceType.HuggingFace => "huggingface",
|
||||
AIServiceType.AzureAIInference => "azureaiinference",
|
||||
AIServiceType.Ollama => "ollama",
|
||||
AIServiceType.Anthropic => "anthropic",
|
||||
AIServiceType.AmazonBedrock => "amazonbedrock",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/settings-ui/Settings.UI.Library/AdvancedAIConfiguration.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for Advanced AI features (general AI transformations like smart paste, content understanding)
|
||||
/// </summary>
|
||||
public class AdvancedAIConfiguration : INotifyPropertyChanged
|
||||
{
|
||||
private string _serviceType = "OpenAI";
|
||||
private string _modelName = "gpt-4";
|
||||
private string _endpointUrl = string.Empty;
|
||||
private string _apiVersion = string.Empty;
|
||||
private string _deploymentName = string.Empty;
|
||||
private string _modelPath = string.Empty;
|
||||
private bool _useSharedCredentials = true;
|
||||
private string _systemPrompt = string.Empty;
|
||||
private bool _moderationEnabled = true;
|
||||
private Dictionary<string, AIProviderConfigurationSnapshot> _providerConfigurations = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
[JsonPropertyName("service-type")]
|
||||
public string ServiceType
|
||||
{
|
||||
get => _serviceType;
|
||||
set => SetProperty(ref _serviceType, 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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[JsonPropertyName("use-shared-credentials")]
|
||||
public bool UseSharedCredentials
|
||||
{
|
||||
get => _useSharedCredentials;
|
||||
set => SetProperty(ref _useSharedCredentials, value);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[JsonPropertyName("provider-configurations")]
|
||||
public Dictionary<string, AIProviderConfigurationSnapshot> ProviderConfigurations
|
||||
{
|
||||
get => _providerConfigurations;
|
||||
set => SetProperty(ref _providerConfigurations, value ?? new Dictionary<string, AIProviderConfigurationSnapshot>(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public bool HasProviderConfiguration(string serviceType)
|
||||
{
|
||||
return _providerConfigurations.ContainsKey(NormalizeServiceType(serviceType));
|
||||
}
|
||||
|
||||
public AIProviderConfigurationSnapshot GetOrCreateProviderConfiguration(string serviceType)
|
||||
{
|
||||
var key = NormalizeServiceType(serviceType);
|
||||
if (!_providerConfigurations.TryGetValue(key, out var snapshot))
|
||||
{
|
||||
snapshot = new AIProviderConfigurationSnapshot();
|
||||
_providerConfigurations[key] = snapshot;
|
||||
OnPropertyChanged(nameof(ProviderConfigurations));
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public void SetProviderConfiguration(string serviceType, AIProviderConfigurationSnapshot snapshot)
|
||||
{
|
||||
_providerConfigurations[NormalizeServiceType(serviceType)] = snapshot ?? new AIProviderConfigurationSnapshot();
|
||||
OnPropertyChanged(nameof(ProviderConfigurations));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private static string NormalizeServiceType(string serviceType)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
|
||||
{
|
||||
private int _id;
|
||||
private string _name = string.Empty;
|
||||
private string _description = string.Empty;
|
||||
private string _prompt = string.Empty;
|
||||
private HotkeySettings _shortcut = new();
|
||||
private bool _isShown;
|
||||
@@ -43,6 +44,13 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description
|
||||
{
|
||||
get => _description;
|
||||
set => Set(ref _description, value ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonPropertyName("prompt")]
|
||||
public string Prompt
|
||||
{
|
||||
@@ -128,6 +136,7 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
|
||||
{
|
||||
Id = other.Id;
|
||||
Name = other.Name;
|
||||
Description = other.Description;
|
||||
Prompt = other.Prompt;
|
||||
Shortcut = other.GetShortcutClone();
|
||||
IsShown = other.IsShown;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,11 +24,41 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
PasteAsJsonShortcut = new();
|
||||
CustomActions = new();
|
||||
AdditionalActions = new();
|
||||
IsAIEnabled = false;
|
||||
IsAdvancedAIEnabled = false;
|
||||
ShowCustomPreview = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
AdvancedAIConfiguration = new();
|
||||
PasteAIConfiguration = new();
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(BoolPropertyJsonConverter))]
|
||||
public bool IsAIEnabled { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, JsonElement> ExtensionData
|
||||
{
|
||||
get => _extensionData;
|
||||
set
|
||||
{
|
||||
_extensionData = value;
|
||||
|
||||
if (_extensionData != null && _extensionData.TryGetValue("IsOpenAIEnabled", out var legacyElement) && legacyElement.ValueKind == JsonValueKind.Object && legacyElement.TryGetProperty("value", out var valueElement))
|
||||
{
|
||||
IsAIEnabled = valueElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
_ => IsAIEnabled,
|
||||
};
|
||||
|
||||
_extensionData.Remove("IsOpenAIEnabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, JsonElement> _extensionData;
|
||||
|
||||
[JsonConverter(typeof(BoolPropertyJsonConverter))]
|
||||
public bool IsAdvancedAIEnabled { get; set; }
|
||||
|
||||
@@ -57,6 +88,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
|
||||
|
||||
[JsonPropertyName("advanced-ai-configuration")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedAIConfiguration AdvancedAIConfiguration { get; set; }
|
||||
|
||||
[JsonPropertyName("paste-ai-configuration")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
}
|
||||
|
||||
158
src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for Paste AI features (custom action transformations like custom prompt processing)
|
||||
/// </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 bool _useSharedCredentials = true;
|
||||
private string _systemPrompt = string.Empty;
|
||||
private bool _moderationEnabled = true;
|
||||
private Dictionary<string, AIProviderConfigurationSnapshot> _providerConfigurations = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
[JsonPropertyName("service-type")]
|
||||
public string ServiceType
|
||||
{
|
||||
get => _serviceType;
|
||||
set => SetProperty(ref _serviceType, 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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[JsonPropertyName("use-shared-credentials")]
|
||||
public bool UseSharedCredentials
|
||||
{
|
||||
get => _useSharedCredentials;
|
||||
set => SetProperty(ref _useSharedCredentials, value);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[JsonPropertyName("provider-configurations")]
|
||||
public Dictionary<string, AIProviderConfigurationSnapshot> ProviderConfigurations
|
||||
{
|
||||
get => _providerConfigurations;
|
||||
set => SetProperty(ref _providerConfigurations, value ?? new Dictionary<string, AIProviderConfigurationSnapshot>(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public bool HasProviderConfiguration(string serviceType)
|
||||
{
|
||||
return _providerConfigurations.ContainsKey(NormalizeServiceType(serviceType));
|
||||
}
|
||||
|
||||
public AIProviderConfigurationSnapshot GetOrCreateProviderConfiguration(string serviceType)
|
||||
{
|
||||
var key = NormalizeServiceType(serviceType);
|
||||
if (!_providerConfigurations.TryGetValue(key, out var snapshot))
|
||||
{
|
||||
snapshot = new AIProviderConfigurationSnapshot();
|
||||
_providerConfigurations[key] = snapshot;
|
||||
OnPropertyChanged(nameof(ProviderConfigurations));
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public void SetProviderConfiguration(string serviceType, AIProviderConfigurationSnapshot snapshot)
|
||||
{
|
||||
_providerConfigurations[NormalizeServiceType(serviceType)] = snapshot ?? new AIProviderConfigurationSnapshot();
|
||||
OnPropertyChanged(nameof(ProviderConfigurations));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private static string NormalizeServiceType(string serviceType)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1822)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.718 2.34668H12.12L16.5 13.3333H14.098L9.718 2.34668ZM4.87933 2.34668H7.39067L11.7707 13.3333H9.32133L8.426 11.026H3.84467L2.94867 13.3327H0.5L4.88 2.34801L4.87933 2.34668ZM7.634 8.98601L6.13533 5.12468L4.63667 8.98668H7.63333L7.634 8.98601Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1822">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
|
After Width: | Height: | Size: 611 B |
@@ -0,0 +1,23 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.05607 1.09062H10.3957L5.89074 14.4385C5.84444 14.5756 5.75629 14.6948 5.6387 14.7792C5.52111 14.8637 5.38 14.9091 5.23524 14.9091H1.85791C1.74822 14.9091 1.64011 14.883 1.54252 14.833C1.44493 14.7829 1.36066 14.7103 1.29669 14.6212C1.23271 14.5322 1.19087 14.4291 1.17462 14.3206C1.15837 14.2122 1.16818 14.1014 1.20324 13.9975L5.40041 1.56129C5.44669 1.42407 5.53485 1.30483 5.65248 1.22037C5.7701 1.1359 5.91126 1.09063 6.05607 1.09062Z" fill="url(#paint0_linear_2092_1811)"/>
|
||||
<path d="M12.3626 10.0435H5.48096C5.41698 10.0434 5.35447 10.0626 5.30156 10.0986C5.24864 10.1345 5.20779 10.1856 5.18432 10.2451C5.16085 10.3046 5.15584 10.3698 5.16996 10.4322C5.18408 10.4946 5.21666 10.5513 5.26346 10.595L9.68546 14.7223C9.81421 14.8424 9.98373 14.9092 10.1598 14.9091H14.0565L12.3626 10.0435Z" fill="#0078D4"/>
|
||||
<path d="M6.05617 1.0907C5.90978 1.09014 5.76704 1.1364 5.64881 1.22273C5.53058 1.30906 5.44305 1.43093 5.399 1.57054L1.2085 13.9862C1.17108 14.0905 1.15933 14.2023 1.17425 14.3121C1.18917 14.4219 1.23031 14.5265 1.2942 14.617C1.3581 14.7076 1.44285 14.7814 1.54131 14.8323C1.63976 14.8831 1.74902 14.9095 1.85983 14.9092H5.32433C5.45337 14.8861 5.57397 14.8293 5.67382 14.7443C5.77367 14.6594 5.84919 14.5495 5.89267 14.4259L6.72833 11.963L9.71333 14.7472C9.83842 14.8507 9.99534 14.9079 10.1577 14.9092H14.0398L12.3372 10.0435L7.37367 10.0447L10.4115 1.0907H6.05617Z" fill="url(#paint1_linear_2092_1811)"/>
|
||||
<path d="M11.5996 1.5607C11.5533 1.4237 11.4653 1.30466 11.3479 1.22034C11.2304 1.13603 11.0895 1.09068 10.9449 1.0907H6.1084C6.25297 1.09071 6.3939 1.13606 6.51135 1.22038C6.62879 1.30469 6.71683 1.42372 6.76307 1.5607L10.9604 13.9974C10.9955 14.1013 11.0053 14.2121 10.9891 14.3206C10.9729 14.4291 10.931 14.5322 10.867 14.6213C10.8031 14.7104 10.7188 14.7831 10.6212 14.8331C10.5236 14.8832 10.4154 14.9094 10.3057 14.9094H15.1424C15.2521 14.9093 15.3602 14.8832 15.4578 14.8331C15.5554 14.783 15.6396 14.7104 15.7036 14.6213C15.7675 14.5321 15.8094 14.4291 15.8256 14.3206C15.8418 14.2121 15.832 14.1013 15.7969 13.9974L11.5996 1.5607Z" fill="url(#paint2_linear_2092_1811)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1811" x1="7.63774" y1="2.11462" x2="3.1309" y2="15.429" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#114A8B"/>
|
||||
<stop offset="1" stop-color="#0669BC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1811" x1="9.04567" y1="8.31954" x2="8.00317" y2="8.67204" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-opacity="0.3"/>
|
||||
<stop offset="0.071" stop-opacity="0.2"/>
|
||||
<stop offset="0.321" stop-opacity="0.1"/>
|
||||
<stop offset="0.623" stop-opacity="0.05"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1811" x1="8.4729" y1="1.72636" x2="13.4201" y2="14.9065" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3CCBF4"/>
|
||||
<stop offset="1" stop-color="#2892DF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 617 B |
@@ -0,0 +1,49 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1818)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3222 0C11.7976 0 12.2189 0.367333 12.3702 0.886C12.5216 1.40467 13.4069 4.61267 13.4069 4.61267V10.9873H10.1982L10.2636 0H11.3222Z" fill="url(#paint0_linear_2092_1818)"/>
|
||||
<path d="M16.0323 4.97996C16.0323 4.75329 15.849 4.57996 15.6323 4.57996H13.7423C13.1034 4.58049 12.4908 4.83459 12.039 5.28645C11.5873 5.73832 11.3334 6.35101 11.333 6.98996V10.9873H13.6237C14.2624 10.9866 14.8747 10.7325 15.3263 10.2808C15.7779 9.82909 16.0318 9.21667 16.0323 8.57796V4.97996Z" fill="url(#paint1_linear_2092_1818)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3216 1.6754e-05C11.2349 -0.00060392 11.1489 0.0160265 11.0686 0.0489413C10.9883 0.0818561 10.9154 0.130399 10.854 0.191747C10.7927 0.253096 10.7442 0.326027 10.7112 0.406302C10.6783 0.486576 10.6617 0.572592 10.6623 0.65935L10.5976 12.7914C10.5975 13.6423 10.2594 14.4583 9.65765 15.06C9.05595 15.6617 8.23992 15.9998 7.38898 16H1.56631C1.50256 16.0004 1.43966 15.9854 1.3829 15.9564C1.32613 15.9274 1.27717 15.8852 1.24012 15.8333C1.20308 15.7814 1.17903 15.7214 1.17002 15.6583C1.161 15.5952 1.16727 15.5309 1.18831 15.4707L5.85498 2.15002C6.07482 1.5228 6.48378 0.979202 7.02549 0.594138C7.56721 0.209074 8.21502 0.00149836 8.87964 1.6754e-05H11.3323H11.3216Z" fill="url(#paint2_linear_2092_1818)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1818" x1="12.6616" y1="11.2247" x2="9.96091" y2="0.410667" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#712575"/>
|
||||
<stop offset="0.09" stop-color="#9A2884"/>
|
||||
<stop offset="0.18" stop-color="#BF2C92"/>
|
||||
<stop offset="0.27" stop-color="#DA2E9C"/>
|
||||
<stop offset="0.34" stop-color="#EB30A2"/>
|
||||
<stop offset="0.4" stop-color="#F131A5"/>
|
||||
<stop offset="0.5" stop-color="#EC30A3"/>
|
||||
<stop offset="0.61" stop-color="#DF2F9E"/>
|
||||
<stop offset="0.72" stop-color="#C92D96"/>
|
||||
<stop offset="0.83" stop-color="#AA2A8A"/>
|
||||
<stop offset="0.95" stop-color="#83267C"/>
|
||||
<stop offset="1" stop-color="#712575"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1818" x1="13.6883" y1="0.226623" x2="13.6883" y2="15.4813" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.08" stop-color="#B17BD5"/>
|
||||
<stop offset="0.19" stop-color="#8778DB"/>
|
||||
<stop offset="0.3" stop-color="#6276E1"/>
|
||||
<stop offset="0.41" stop-color="#4574E5"/>
|
||||
<stop offset="0.54" stop-color="#2E72E8"/>
|
||||
<stop offset="0.67" stop-color="#1D71EB"/>
|
||||
<stop offset="0.81" stop-color="#1471EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1818" x1="12.769" y1="0.572683" x2="2.65698" y2="16.7887" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.05" stop-color="#B77BD4"/>
|
||||
<stop offset="0.11" stop-color="#9079DA"/>
|
||||
<stop offset="0.18" stop-color="#6E77DF"/>
|
||||
<stop offset="0.25" stop-color="#5175E3"/>
|
||||
<stop offset="0.33" stop-color="#3973E7"/>
|
||||
<stop offset="0.42" stop-color="#2772E9"/>
|
||||
<stop offset="0.54" stop-color="#1A71EB"/>
|
||||
<stop offset="0.68" stop-color="#1371EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2092_1818">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 589 B |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 624 B |
@@ -0,0 +1,59 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1741)">
|
||||
<mask id="mask0_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1741)">
|
||||
<mask id="mask1_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-1" y="-2" width="19" height="20">
|
||||
<path d="M17.8337 -1.33337H-0.833008V17.3333H17.8337V-1.33337Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2092_1741)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1137 0.315668C11.57 0.315668 11.9744 0.657891 12.1196 1.15567C12.2648 1.65345 13.1152 4.73345 13.1152 4.73345V10.852H10.0352L10.0974 0.305298H11.1137V0.315668Z" fill="url(#paint0_linear_2092_1741)"/>
|
||||
<path d="M15.6352 5.09586C15.6352 4.87808 15.4589 4.71216 15.2515 4.71216H13.4366C12.1611 4.71216 11.124 5.7492 11.124 7.02472V10.8618H13.3226C14.5982 10.8618 15.6352 9.82472 15.6352 8.54919V5.09586Z" fill="url(#paint1_linear_2092_1741)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1133 0.315674C10.7607 0.315674 10.4807 0.595674 10.4807 0.948265L10.4185 12.5942C10.4185 14.2949 9.0392 15.6742 7.33847 15.6742H1.74885C1.47921 15.6742 1.30292 15.4149 1.38589 15.1661L5.86589 2.37938C6.30144 1.14531 7.46293 0.315674 8.7696 0.315674H11.1237H11.1133Z" fill="url(#paint2_linear_2092_1741)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1741" x1="12.3996" y1="11.0801" x2="9.80702" y2="0.699373" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#712575"/>
|
||||
<stop offset="0.09" stop-color="#9A2884"/>
|
||||
<stop offset="0.18" stop-color="#BF2C92"/>
|
||||
<stop offset="0.27" stop-color="#DA2E9C"/>
|
||||
<stop offset="0.34" stop-color="#EB30A2"/>
|
||||
<stop offset="0.4" stop-color="#F131A5"/>
|
||||
<stop offset="0.5" stop-color="#EC30A3"/>
|
||||
<stop offset="0.61" stop-color="#DF2F9E"/>
|
||||
<stop offset="0.72" stop-color="#C92D96"/>
|
||||
<stop offset="0.83" stop-color="#AA2A8A"/>
|
||||
<stop offset="0.95" stop-color="#83267C"/>
|
||||
<stop offset="1" stop-color="#712575"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1741" x1="13.3848" y1="0.532897" x2="13.3848" y2="15.1759" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.08" stop-color="#B17BD5"/>
|
||||
<stop offset="0.19" stop-color="#8778DB"/>
|
||||
<stop offset="0.3" stop-color="#6276E1"/>
|
||||
<stop offset="0.41" stop-color="#4574E5"/>
|
||||
<stop offset="0.54" stop-color="#2E72E8"/>
|
||||
<stop offset="0.67" stop-color="#1D71EB"/>
|
||||
<stop offset="0.81" stop-color="#1471EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1741" x1="12.5029" y1="0.865306" x2="2.79625" y2="16.4313" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.05" stop-color="#B77BD4"/>
|
||||
<stop offset="0.11" stop-color="#9079DA"/>
|
||||
<stop offset="0.18" stop-color="#6E77DF"/>
|
||||
<stop offset="0.25" stop-color="#5175E3"/>
|
||||
<stop offset="0.33" stop-color="#3973E7"/>
|
||||
<stop offset="0.42" stop-color="#2772E9"/>
|
||||
<stop offset="0.54" stop-color="#1A71EB"/>
|
||||
<stop offset="0.68" stop-color="#1371EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2092_1741">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 566 B |
@@ -0,0 +1,20 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.2445 7.22331C13.137 6.75184 12.13 6.07273 11.2778 5.22264C10.0911 4.03353 9.2444 2.54831 8.8258 0.921308C8.80744 0.849046 8.76551 0.784967 8.70665 0.739197C8.6478 0.693428 8.57536 0.668579 8.5008 0.668579C8.42624 0.668579 8.35381 0.693428 8.29495 0.739197C8.2361 0.784967 8.19417 0.849046 8.1758 0.921308C7.75632 2.5481 6.90952 4.03315 5.72314 5.22264C4.87089 6.07263 3.8639 6.75172 2.75647 7.22331C2.32314 7.40998 1.8778 7.55998 1.4218 7.67531C1.3491 7.69317 1.28448 7.7349 1.23829 7.79382C1.1921 7.85274 1.16699 7.92544 1.16699 8.00031C1.16699 8.07518 1.1921 8.14788 1.23829 8.2068C1.28448 8.26572 1.3491 8.30744 1.4218 8.32531C1.8778 8.43998 2.3218 8.58998 2.75647 8.77664C3.86397 9.24811 4.87098 9.92722 5.72314 10.7773C6.9102 11.9666 7.75709 13.452 8.1758 15.0793C8.19367 15.152 8.2354 15.2166 8.29431 15.2628C8.35323 15.309 8.42594 15.3341 8.5008 15.3341C8.57567 15.3341 8.64838 15.309 8.7073 15.2628C8.76621 15.2166 8.80794 15.152 8.8258 15.0793C8.94047 14.6226 9.09047 14.1786 9.27714 13.744C9.74858 12.6365 10.4277 11.6294 11.2778 10.7773C12.4671 9.59052 13.9526 8.74386 15.5798 8.32531C15.6521 8.30694 15.7161 8.26502 15.7619 8.20616C15.8077 8.1473 15.8325 8.07487 15.8325 8.00031C15.8325 7.92575 15.8077 7.85332 15.7619 7.79446C15.7161 7.7356 15.6521 7.69367 15.5798 7.67531C15.1234 7.56047 14.6768 7.40932 14.2445 7.22331Z" fill="#3186FF"/>
|
||||
<path d="M14.2445 7.22331C13.137 6.75184 12.13 6.07273 11.2778 5.22264C10.0911 4.03353 9.2444 2.54831 8.8258 0.921308C8.80744 0.849046 8.76551 0.784967 8.70665 0.739197C8.6478 0.693428 8.57536 0.668579 8.5008 0.668579C8.42624 0.668579 8.35381 0.693428 8.29495 0.739197C8.2361 0.784967 8.19417 0.849046 8.1758 0.921308C7.75632 2.5481 6.90952 4.03315 5.72314 5.22264C4.87089 6.07263 3.8639 6.75172 2.75647 7.22331C2.32314 7.40998 1.8778 7.55998 1.4218 7.67531C1.3491 7.69317 1.28448 7.7349 1.23829 7.79382C1.1921 7.85274 1.16699 7.92544 1.16699 8.00031C1.16699 8.07518 1.1921 8.14788 1.23829 8.2068C1.28448 8.26572 1.3491 8.30744 1.4218 8.32531C1.8778 8.43998 2.3218 8.58998 2.75647 8.77664C3.86397 9.24811 4.87098 9.92722 5.72314 10.7773C6.9102 11.9666 7.75709 13.452 8.1758 15.0793C8.19367 15.152 8.2354 15.2166 8.29431 15.2628C8.35323 15.309 8.42594 15.3341 8.5008 15.3341C8.57567 15.3341 8.64838 15.309 8.7073 15.2628C8.76621 15.2166 8.80794 15.152 8.8258 15.0793C8.94047 14.6226 9.09047 14.1786 9.27714 13.744C9.74858 12.6365 10.4277 11.6294 11.2778 10.7773C12.4671 9.59052 13.9526 8.74386 15.5798 8.32531C15.6521 8.30694 15.7161 8.26502 15.7619 8.20616C15.8077 8.1473 15.8325 8.07487 15.8325 8.00031C15.8325 7.92575 15.8077 7.85332 15.7619 7.79446C15.7161 7.7356 15.6521 7.69367 15.5798 7.67531C15.1234 7.56047 14.6768 7.40932 14.2445 7.22331Z" fill="url(#paint0_linear_2092_1806)"/>
|
||||
<path d="M14.2445 7.22331C13.137 6.75184 12.13 6.07273 11.2778 5.22264C10.0911 4.03353 9.2444 2.54831 8.8258 0.921308C8.80744 0.849046 8.76551 0.784967 8.70665 0.739197C8.6478 0.693428 8.57536 0.668579 8.5008 0.668579C8.42624 0.668579 8.35381 0.693428 8.29495 0.739197C8.2361 0.784967 8.19417 0.849046 8.1758 0.921308C7.75632 2.5481 6.90952 4.03315 5.72314 5.22264C4.87089 6.07263 3.8639 6.75172 2.75647 7.22331C2.32314 7.40998 1.8778 7.55998 1.4218 7.67531C1.3491 7.69317 1.28448 7.7349 1.23829 7.79382C1.1921 7.85274 1.16699 7.92544 1.16699 8.00031C1.16699 8.07518 1.1921 8.14788 1.23829 8.2068C1.28448 8.26572 1.3491 8.30744 1.4218 8.32531C1.8778 8.43998 2.3218 8.58998 2.75647 8.77664C3.86397 9.24811 4.87098 9.92722 5.72314 10.7773C6.9102 11.9666 7.75709 13.452 8.1758 15.0793C8.19367 15.152 8.2354 15.2166 8.29431 15.2628C8.35323 15.309 8.42594 15.3341 8.5008 15.3341C8.57567 15.3341 8.64838 15.309 8.7073 15.2628C8.76621 15.2166 8.80794 15.152 8.8258 15.0793C8.94047 14.6226 9.09047 14.1786 9.27714 13.744C9.74858 12.6365 10.4277 11.6294 11.2778 10.7773C12.4671 9.59052 13.9526 8.74386 15.5798 8.32531C15.6521 8.30694 15.7161 8.26502 15.7619 8.20616C15.8077 8.1473 15.8325 8.07487 15.8325 8.00031C15.8325 7.92575 15.8077 7.85332 15.7619 7.79446C15.7161 7.7356 15.6521 7.69367 15.5798 7.67531C15.1234 7.56047 14.6768 7.40932 14.2445 7.22331Z" fill="url(#paint1_linear_2092_1806)"/>
|
||||
<path d="M14.2445 7.22331C13.137 6.75184 12.13 6.07273 11.2778 5.22264C10.0911 4.03353 9.2444 2.54831 8.8258 0.921308C8.80744 0.849046 8.76551 0.784967 8.70665 0.739197C8.6478 0.693428 8.57536 0.668579 8.5008 0.668579C8.42624 0.668579 8.35381 0.693428 8.29495 0.739197C8.2361 0.784967 8.19417 0.849046 8.1758 0.921308C7.75632 2.5481 6.90952 4.03315 5.72314 5.22264C4.87089 6.07263 3.8639 6.75172 2.75647 7.22331C2.32314 7.40998 1.8778 7.55998 1.4218 7.67531C1.3491 7.69317 1.28448 7.7349 1.23829 7.79382C1.1921 7.85274 1.16699 7.92544 1.16699 8.00031C1.16699 8.07518 1.1921 8.14788 1.23829 8.2068C1.28448 8.26572 1.3491 8.30744 1.4218 8.32531C1.8778 8.43998 2.3218 8.58998 2.75647 8.77664C3.86397 9.24811 4.87098 9.92722 5.72314 10.7773C6.9102 11.9666 7.75709 13.452 8.1758 15.0793C8.19367 15.152 8.2354 15.2166 8.29431 15.2628C8.35323 15.309 8.42594 15.3341 8.5008 15.3341C8.57567 15.3341 8.64838 15.309 8.7073 15.2628C8.76621 15.2166 8.80794 15.152 8.8258 15.0793C8.94047 14.6226 9.09047 14.1786 9.27714 13.744C9.74858 12.6365 10.4277 11.6294 11.2778 10.7773C12.4671 9.59052 13.9526 8.74386 15.5798 8.32531C15.6521 8.30694 15.7161 8.26502 15.7619 8.20616C15.8077 8.1473 15.8325 8.07487 15.8325 8.00031C15.8325 7.92575 15.8077 7.85332 15.7619 7.79446C15.7161 7.7356 15.6521 7.69367 15.5798 7.67531C15.1234 7.56047 14.6768 7.40932 14.2445 7.22331Z" fill="url(#paint2_linear_2092_1806)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1806" x1="5.16714" y1="10.3333" x2="7.8338" y2="7.99997" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#08B962"/>
|
||||
<stop offset="1" stop-color="#08B962" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1806" x1="5.8338" y1="3.66664" x2="8.16714" y2="7.33331" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F94543"/>
|
||||
<stop offset="1" stop-color="#F94543" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1806" x1="2.8338" y1="8.99998" x2="12.1671" y2="7.99998" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FABC12"/>
|
||||
<stop offset="0.46" stop-color="#FABC12" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,24 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1755)">
|
||||
<mask id="mask0_2092_1755" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.428 0H0.5V16H16.428V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1755)">
|
||||
<path d="M5.05035 0H2.77441V3.21057H5.05035V0Z" fill="#FFD800"/>
|
||||
<path d="M14.1529 0H11.877V3.21057H14.1529V0Z" fill="#FFD800"/>
|
||||
<path d="M7.32555 3.21082H2.77441V6.42139H7.32555V3.21082Z" fill="#FFAF00"/>
|
||||
<path d="M14.1537 3.21082H9.60254V6.42139H14.1537V3.21082Z" fill="#FFAF00"/>
|
||||
<path d="M14.1519 6.41992H2.77441V9.63049H14.1519V6.41992Z" fill="#FF8205"/>
|
||||
<path d="M5.05035 9.63074H2.77441V12.8414H5.05035V9.63074Z" fill="#FA500F"/>
|
||||
<path d="M9.60213 9.63074H7.32617V12.8414H9.60213V9.63074Z" fill="#FA500F"/>
|
||||
<path d="M14.1529 9.63074H11.877V12.8414H14.1529V9.63074Z" fill="#FA500F"/>
|
||||
<path d="M7.32633 12.8402H0.5V16.0509H7.32633V12.8402Z" fill="#E10500"/>
|
||||
<path d="M16.4296 12.8402H9.60254V16.0509H16.4296V12.8402Z" fill="#E10500"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1755">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 336 B |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 780 B |
@@ -0,0 +1,18 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1790)">
|
||||
<path d="M15.85 7.50005H15.75L13.08 2.54505C13.1287 2.45432 13.1544 2.35303 13.155 2.25005C13.155 2.16427 13.138 2.07933 13.105 2.00014C13.072 1.92095 13.0237 1.84907 12.9628 1.78865C12.9019 1.72823 12.8297 1.68045 12.7502 1.64808C12.6708 1.61571 12.5857 1.59939 12.5 1.60005C12.4129 1.59972 12.3267 1.6173 12.2467 1.65171C12.1667 1.68612 12.0946 1.73661 12.035 1.80005L6.71496 0.740047C6.70184 0.64745 6.66874 0.558814 6.61795 0.480283C6.56716 0.401752 6.4999 0.335204 6.42084 0.285253C6.34177 0.235302 6.25279 0.203143 6.16006 0.191003C6.06733 0.178864 5.97307 0.187036 5.88381 0.214952C5.79455 0.242869 5.71243 0.289862 5.64314 0.352674C5.57385 0.415486 5.51905 0.492615 5.48253 0.578715C5.44602 0.664814 5.42867 0.757825 5.43167 0.851299C5.43468 0.944773 5.45798 1.03647 5.49996 1.12005L1.34996 7.06505C1.29477 7.04867 1.23753 7.04025 1.17996 7.04005C1.01805 7.05429 0.867358 7.12867 0.757581 7.24853C0.647805 7.36838 0.586914 7.52502 0.586914 7.68755C0.586914 7.85008 0.647805 8.00671 0.757581 8.12657C0.867358 8.24643 1.01805 8.32081 1.17996 8.33505L3.42996 13.87C3.39194 13.9551 3.37153 14.0469 3.36996 14.14C3.37128 14.3116 3.44034 14.4756 3.5621 14.5964C3.68386 14.7173 3.84843 14.7851 4.01996 14.785C4.10788 14.7861 4.19505 14.7688 4.27596 14.7344C4.35686 14.7 4.42973 14.6491 4.48996 14.585L11.21 15.24C11.2312 15.4111 11.3195 15.5667 11.4554 15.6727C11.5914 15.7787 11.7639 15.8263 11.935 15.805C12.106 15.7838 12.2617 15.6955 12.3676 15.5596C12.4736 15.4236 12.5212 15.2511 12.5 15.08C12.498 14.9264 12.4433 14.7781 12.345 14.66L15.73 8.76005H15.84C15.9253 8.76137 16.0101 8.74587 16.0895 8.71442C16.1688 8.68297 16.2412 8.63619 16.3025 8.57676C16.3638 8.51733 16.4128 8.44641 16.4467 8.36804C16.4805 8.28968 16.4987 8.20541 16.5 8.12005C16.4947 7.95205 16.4236 7.79286 16.302 7.67686C16.1804 7.56085 16.018 7.49734 15.85 7.50005ZM12 2.64505C12.0769 2.74772 12.1833 2.82448 12.305 2.86505L11.305 10.55C11.2418 10.5642 11.1811 10.5878 11.125 10.62L5.49996 5.76005C5.51878 5.70543 5.52726 5.64778 5.52496 5.59005C5.52496 5.55005 5.52496 5.50505 5.52496 5.46505L12 2.64505ZM15.235 8.30505L11.79 10.66C11.763 10.6412 11.7345 10.6245 11.705 10.61L12.725 2.84505L15.355 7.69505C15.2499 7.81343 15.1928 7.96678 15.195 8.12505L15.235 8.30505ZM4.78496 4.95005C4.63217 4.97297 4.49283 5.0504 4.39266 5.16803C4.29249 5.28566 4.23825 5.43555 4.23996 5.59005V5.63505L1.92996 7.00005L5.49996 1.87005L4.78496 4.95005ZM4.99996 6.22505C5.08363 6.20348 5.16307 6.16798 5.23496 6.12005L10.79 10.955C10.7638 11.0306 10.7502 11.11 10.75 11.19V11.225L4.54996 13.775C4.46194 13.6393 4.32644 13.5412 4.16996 13.5L4.99996 6.22505ZM10.935 11.62C11.0198 11.7179 11.1337 11.7862 11.26 11.815L11.565 14.5C11.4362 14.5671 11.3327 14.6741 11.27 14.805L4.76996 14.165L10.935 11.62ZM11.7 11.765C11.8068 11.7099 11.8967 11.6269 11.9601 11.5248C12.0235 11.4226 12.058 11.3052 12.06 11.185C12.0622 11.1306 12.0537 11.0762 12.035 11.025L15.185 8.86005L12 14.4L11.7 11.765ZM11.86 2.22505L5.28996 5.08505L5.21496 5.03505L6.04996 1.45505H6.07496C6.18268 1.45615 6.28889 1.42961 6.38342 1.37796C6.47796 1.32631 6.55768 1.25129 6.61496 1.16005L11.86 2.20505V2.22505ZM1.82996 7.69005C1.82996 7.64505 1.82996 7.60505 1.82996 7.57005L4.42496 6.04005C4.47735 6.09439 4.53812 6.13997 4.60496 6.17505L3.74996 13.43L1.60996 8.17005C1.67867 8.11029 1.73383 8.03657 1.77177 7.95379C1.80971 7.87102 1.82955 7.7811 1.82996 7.69005Z" fill="#333333"/>
|
||||
<path d="M12.7446 2.84497L15.3696 7.69497C15.2665 7.81456 15.2098 7.96712 15.2096 8.12497C15.2023 8.18475 15.2023 8.24519 15.2096 8.30497L11.7696 10.66L11.6846 10.61L12.6846 2.84497H12.7446Z" fill="#DEDEDD"/>
|
||||
<path d="M11.7002 11.765C11.807 11.7099 11.8969 11.6268 11.9603 11.5247C12.0237 11.4226 12.0582 11.3052 12.0602 11.185C12.0625 11.1305 12.054 11.0762 12.0352 11.025L15.1852 8.85999L12.0002 14.4L11.7002 11.765Z" fill="#B2B2B2"/>
|
||||
<path d="M10.9345 11.62C11.0194 11.7179 11.1332 11.7862 11.2595 11.815L11.5645 14.5C11.4358 14.567 11.3322 14.6741 11.2695 14.805L4.76953 14.165L10.9345 11.62Z" fill="#D1D1D1"/>
|
||||
<path d="M4.99992 6.225C5.08359 6.20343 5.16303 6.16793 5.23492 6.12L10.7899 10.955C10.7637 11.0306 10.7502 11.11 10.7499 11.19V11.225L4.54992 13.775C4.4619 13.6392 4.3264 13.5412 4.16992 13.5L4.99992 6.225Z" fill="#F2F2F2"/>
|
||||
<path d="M1.83035 7.69004C1.83035 7.64504 1.83035 7.60504 1.83035 7.57004L4.42535 6.04004C4.47774 6.09439 4.53851 6.13997 4.60535 6.17504L3.75035 13.43L1.61035 8.17004C1.67906 8.11029 1.73422 8.03656 1.77216 7.95378C1.8101 7.87101 1.82994 7.78109 1.83035 7.69004Z" fill="#D8D8D7"/>
|
||||
<path d="M4.78469 4.95C4.6319 4.97292 4.49255 5.05034 4.39238 5.16797C4.29221 5.28561 4.23798 5.4355 4.23969 5.59V5.635L1.92969 7L5.49969 1.87L4.78469 4.95Z" fill="#B2B2B2"/>
|
||||
<path d="M11.8598 2.22503L5.28984 5.08503L5.21484 5.03503L6.04984 1.45503H6.07484C6.18256 1.45613 6.28877 1.42959 6.38331 1.37795C6.47785 1.3263 6.55757 1.25127 6.61484 1.16003L11.8598 2.20503V2.22503Z" fill="#D1D1D1"/>
|
||||
<path d="M12 2.64502C12.0769 2.74769 12.1833 2.82445 12.305 2.86502L11.305 10.55C11.2418 10.5642 11.1811 10.5878 11.125 10.62L5.5 5.76002C5.51882 5.7054 5.5273 5.64775 5.525 5.59002C5.525 5.55002 5.525 5.50502 5.525 5.46502L12 2.64502Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1790">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 445 B |
@@ -0,0 +1,15 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1800)">
|
||||
<mask id="mask0_2092_1800" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1800)">
|
||||
<path d="M6.63673 5.772V4.26557C6.63673 4.13867 6.68433 4.0435 6.79527 3.98013L9.8241 2.23585C10.2364 1.998 10.728 1.88706 11.2353 1.88706C13.1382 1.88706 14.3434 3.3618 14.3434 4.9316C14.3434 5.0426 14.3434 5.16947 14.3275 5.29633L11.1877 3.45687C10.9975 3.3459 10.8071 3.3459 10.6169 3.45687L6.63673 5.772ZM13.709 11.6392V8.03953C13.709 7.8175 13.6138 7.65893 13.4236 7.54793L9.44343 5.2328L10.7437 4.48747C10.8547 4.42413 10.9499 4.42413 11.0609 4.48747L14.0897 6.23177C14.9619 6.73927 15.5485 7.8175 15.5485 8.864C15.5485 10.0691 14.835 11.1793 13.709 11.6392ZM5.70117 8.46777L4.40087 7.70667C4.28993 7.64333 4.2423 7.5481 4.2423 7.42123V3.9327C4.2423 2.23602 5.5426 0.951497 7.30277 0.951497C7.96887 0.951497 8.58717 1.17355 9.1106 1.56996L5.98673 3.37773C5.7965 3.4887 5.7013 3.64723 5.7013 3.86933L5.70117 8.46777ZM8.5 10.0852L6.63673 9.03863V6.8187L8.5 5.77217L10.3631 6.8187V9.03863L8.5 10.0852ZM9.6972 14.9058C9.03113 14.9058 8.41283 14.6838 7.8894 14.2874L11.0132 12.4796C11.2035 12.3686 11.2987 12.2101 11.2987 11.988V7.3894L12.6149 8.15053C12.7258 8.21387 12.7735 8.30907 12.7735 8.43597V11.9245C12.7735 13.6212 11.4572 14.9058 9.6972 14.9058ZM5.939 11.3697L2.91018 9.62543C2.03797 9.1179 1.45133 8.0397 1.45133 6.99317C1.45133 5.77217 2.18077 4.67803 3.30657 4.21813V7.83357C3.30657 8.0556 3.40178 8.21417 3.592 8.32513L7.5564 10.6244L6.2561 11.3697C6.14517 11.433 6.04993 11.433 5.939 11.3697ZM5.76467 13.9703C3.9728 13.9703 2.6566 12.6224 2.6566 10.9574C2.6566 10.8305 2.6725 10.7036 2.68826 10.5768L5.81213 12.3845C6.00237 12.4955 6.19273 12.4955 6.38297 12.3845L10.3631 10.0853V11.5918C10.3631 11.7186 10.3155 11.8138 10.2046 11.8772L7.17577 13.6215C6.76347 13.8593 6.27203 13.9703 5.76467 13.9703ZM9.6972 15.8572C11.6159 15.8572 13.2174 14.4935 13.5823 12.6857C15.3583 12.2258 16.5 10.5608 16.5 8.86417C16.5 7.7541 16.0243 6.6759 15.168 5.89887C15.2473 5.56583 15.2949 5.2328 15.2949 4.89993C15.2949 2.6324 13.4554 0.935567 11.3305 0.935567C10.9025 0.935567 10.4902 0.99892 10.0778 1.14172C9.36417 0.443973 8.381 0 7.30277 0C5.38407 0 3.78256 1.36364 3.41771 3.17142C1.64172 3.63133 0.5 5.29633 0.5 6.993C0.5 8.10307 0.975663 9.18127 1.83198 9.9583C1.7527 10.2913 1.70511 10.6244 1.70511 10.9572C1.70511 13.2248 3.54458 14.9216 5.66947 14.9216C6.09753 14.9216 6.50983 14.8582 6.92217 14.7154C7.63567 15.4132 8.61883 15.8572 9.6972 15.8572Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1800">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 418 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1732)">
|
||||
<path d="M6.63673 5.772V4.26557C6.63673 4.13867 6.68433 4.0435 6.79527 3.98013L9.8241 2.23585C10.2364 1.998 10.728 1.88706 11.2353 1.88706C13.1382 1.88706 14.3434 3.3618 14.3434 4.9316C14.3434 5.0426 14.3434 5.16947 14.3275 5.29633L11.1877 3.45687C10.9975 3.3459 10.8071 3.3459 10.6169 3.45687L6.63673 5.772ZM13.709 11.6392V8.03953C13.709 7.8175 13.6138 7.65893 13.4236 7.54793L9.44343 5.2328L10.7437 4.48747C10.8547 4.42413 10.9499 4.42413 11.0609 4.48747L14.0897 6.23177C14.9619 6.73927 15.5485 7.8175 15.5485 8.864C15.5485 10.0691 14.835 11.1793 13.709 11.6392ZM5.70117 8.46777L4.40087 7.70667C4.28993 7.64333 4.2423 7.5481 4.2423 7.42123V3.9327C4.2423 2.23602 5.5426 0.951497 7.30277 0.951497C7.96887 0.951497 8.58717 1.17355 9.1106 1.56996L5.98673 3.37773C5.7965 3.4887 5.7013 3.64723 5.7013 3.86933L5.70117 8.46777ZM8.5 10.0852L6.63673 9.03863V6.8187L8.5 5.77217L10.3631 6.8187V9.03863L8.5 10.0852ZM9.6972 14.9058C9.03113 14.9058 8.41283 14.6838 7.8894 14.2874L11.0132 12.4796C11.2035 12.3686 11.2987 12.2101 11.2987 11.988V7.3894L12.6149 8.15053C12.7258 8.21387 12.7735 8.30907 12.7735 8.43597V11.9245C12.7735 13.6212 11.4572 14.9058 9.6972 14.9058ZM5.939 11.3697L2.91018 9.62543C2.03797 9.1179 1.45133 8.0397 1.45133 6.99317C1.45133 5.77217 2.18077 4.67803 3.30657 4.21813V7.83357C3.30657 8.0556 3.40178 8.21417 3.592 8.32513L7.5564 10.6244L6.2561 11.3697C6.14517 11.433 6.04993 11.433 5.939 11.3697ZM5.76467 13.9703C3.9728 13.9703 2.6566 12.6224 2.6566 10.9574C2.6566 10.8305 2.6725 10.7036 2.68826 10.5768L5.81213 12.3845C6.00237 12.4955 6.19273 12.4955 6.38297 12.3845L10.3631 10.0853V11.5918C10.3631 11.7186 10.3155 11.8138 10.2046 11.8772L7.17577 13.6215C6.76347 13.8593 6.27203 13.9703 5.76467 13.9703ZM9.6972 15.8572C11.6159 15.8572 13.2174 14.4935 13.5823 12.6857C15.3583 12.2258 16.5 10.5608 16.5 8.86417C16.5 7.7541 16.0243 6.6759 15.168 5.89887C15.2473 5.56583 15.2949 5.2328 15.2949 4.89993C15.2949 2.6324 13.4554 0.935567 11.3305 0.935567C10.9025 0.935567 10.4902 0.99892 10.0778 1.14172C9.36417 0.443973 8.381 0 7.30277 0C5.38407 0 3.78256 1.36364 3.41771 3.17142C1.64172 3.63133 0.5 5.29633 0.5 6.993C0.5 8.10307 0.975663 9.18127 1.83198 9.9583C1.7527 10.2913 1.70511 10.6244 1.70511 10.9572C1.70511 13.2248 3.54458 14.9216 5.66947 14.9216C6.09753 14.9216 6.50983 14.8582 6.92217 14.7154C7.63567 15.4132 8.61883 15.8572 9.6972 15.8572Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1732">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1,74 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1770)">
|
||||
<mask id="mask0_2092_1770" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1770)">
|
||||
<mask id="mask1_2092_1770" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2092_1770)">
|
||||
<path d="M16.131 10.9838L9.24712 15.124C9.02152 15.2597 8.76328 15.3313 8.5 15.3313C8.23675 15.3313 7.97846 15.2597 7.75286 15.124L0.869003 10.9838C0.640038 10.8461 0.5 10.5985 0.5 10.3313C0.5 10.0641 0.640038 9.81645 0.869003 9.67877L7.75286 5.53867C7.97846 5.40299 8.23675 5.3313 8.5 5.3313C8.76328 5.3313 9.02152 5.40299 9.24712 5.53867L16.131 9.67877C16.36 9.81645 16.5 10.0641 16.5 10.3313C16.5 10.5985 16.36 10.8461 16.131 10.9838Z" fill="url(#paint0_linear_2092_1770)"/>
|
||||
<path d="M16.131 8.65256L9.24712 12.7926C9.02152 12.9283 8.76328 13 8.5 13C8.23675 13 7.97846 12.9283 7.75286 12.7926L0.869003 8.65256C0.640038 8.5148 0.5 8.2672 0.5 8C0.5 7.73282 0.640038 7.48518 0.869003 7.34746L7.75286 3.20737C7.97846 3.07168 8.23675 3 8.5 3C8.76328 3 9.02152 3.07168 9.24712 3.20737L16.131 7.34746C16.36 7.48518 16.5 7.73282 16.5 8C16.5 8.2672 16.36 8.5148 16.131 8.65256Z" fill="url(#paint1_linear_2092_1770)"/>
|
||||
<path d="M16.131 6.31818L9.24712 10.4583C9.02152 10.5939 8.76328 10.6656 8.5 10.6656C8.23675 10.6656 7.97846 10.5939 7.75286 10.4583L0.869003 6.31818C0.640038 6.18046 0.5 5.93283 0.5 5.66565C0.5 5.39846 0.640038 5.15083 0.869003 5.01311L7.75286 0.873017C7.97846 0.737337 8.23675 0.665649 8.5 0.665649C8.76328 0.665649 9.02152 0.737337 9.24712 0.873017L16.131 5.01311C16.36 5.15083 16.5 5.39846 16.5 5.66565C16.5 5.93283 16.36 6.18046 16.131 6.31818Z" fill="url(#paint2_linear_2092_1770)"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint3_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint4_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint5_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint6_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint7_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint8_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M8.01858 3.31028L7.76982 2.80011C7.749 2.7608 7.7104 2.72675 7.65934 2.70267C7.60826 2.67859 7.54725 2.66565 7.48468 2.66565C7.42214 2.66565 7.36112 2.67859 7.31005 2.70267C7.25898 2.72675 7.22038 2.7608 7.19957 2.80011L6.9508 3.31028C6.87498 3.46428 6.74675 3.60454 6.57612 3.72002C6.40548 3.83551 6.19708 3.92314 5.9672 3.97603L5.20177 4.14185C5.14277 4.15571 5.09168 4.18143 5.05555 4.21548C5.0194 4.24951 5 4.29019 5 4.33188C5 4.37357 5.0194 4.41423 5.05555 4.44828C5.09168 4.48231 5.14277 4.50803 5.20177 4.52191L5.9672 4.68771C6.16372 4.73139 6.34502 4.80035 6.501 4.89045C6.53005 4.90723 6.55823 4.92475 6.58548 4.94297C6.72874 5.03882 6.84258 5.152 6.92118 5.27615C6.93774 5.30231 6.95274 5.32895 6.9661 5.35602L7.21486 5.86619C7.23363 5.90162 7.26685 5.93279 7.31062 5.95622C7.3154 5.95879 7.32032 5.96127 7.32535 5.96363C7.37642 5.98771 7.43743 6.00065 7.5 6.00065C7.56257 6.00065 7.62358 5.98771 7.67465 5.96363C7.72572 5.93955 7.76432 5.9055 7.78514 5.86619L8.0339 5.35602C8.11125 5.20091 8.24182 5.05999 8.41522 4.94442C8.58864 4.82885 8.80008 4.74182 9.0328 4.69027L9.79824 4.52447C9.8572 4.51059 9.90832 4.48487 9.94448 4.45083C9.98056 4.41679 10 4.37611 10 4.33443C10 4.29274 9.98056 4.25207 9.94448 4.21803C9.90832 4.18399 9.8572 4.15827 9.79824 4.1444L9.78296 4.14185L9.01752 3.97603C8.7848 3.92448 8.57328 3.83747 8.39992 3.72188C8.22652 3.60631 8.09595 3.46539 8.01858 3.31028Z" fill="url(#paint9_linear_2092_1770)"/>
|
||||
<path d="M11.0775 6.78623L11.5368 6.88572L11.546 6.88725C11.5813 6.89557 11.612 6.911 11.6336 6.93143C11.6553 6.95185 11.667 6.97625 11.667 7.00126C11.667 7.02628 11.6553 7.05068 11.6336 7.0711C11.612 7.09154 11.5813 7.10697 11.546 7.11528L11.0867 7.21477C10.947 7.2457 10.8202 7.29792 10.7161 7.36726C10.6121 7.4366 10.5337 7.52117 10.4873 7.61422L10.338 7.92032C10.3256 7.94392 10.3024 7.96434 10.2718 7.97878C10.2412 7.99323 10.2045 8.00104 10.167 8.00104C10.1295 8.00104 10.0928 7.99323 10.0622 7.97878C10.0316 7.96434 10.0084 7.94392 9.99587 7.92032L9.99539 7.91937L9.84667 7.61422C9.80051 7.52088 9.72235 7.43602 9.61827 7.3664C9.51419 7.29677 9.38715 7.24434 9.24731 7.21323L8.78803 7.11375C8.75267 7.10543 8.72195 7.09 8.70035 7.06958C8.67859 7.04915 8.66699 7.02475 8.66699 6.99974C8.66699 6.97472 8.67859 6.95032 8.70035 6.9299C8.72195 6.90946 8.75267 6.89403 8.78803 6.88572L9.24731 6.78623C9.38523 6.7545 9.51027 6.70192 9.61267 6.63262C9.71499 6.56334 9.79195 6.47918 9.83747 6.38678L9.98675 6.08068C9.99923 6.05708 10.0224 6.03666 10.053 6.02222C10.0836 6.00777 10.1203 6 10.1578 6C10.1953 6 10.232 6.00777 10.2626 6.02222C10.2932 6.03666 10.3164 6.05708 10.3288 6.08068L10.4781 6.38678C10.5245 6.47983 10.6029 6.5644 10.7069 6.63375C10.811 6.70308 10.9379 6.7553 11.0775 6.78623Z" fill="url(#paint10_linear_2092_1770)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1770" x1="0.5" y1="5.3313" x2="9.4888" y2="19.7133" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#004695"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1770" x1="0.5" y1="3" x2="9.4888" y2="17.382" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0078D4"/>
|
||||
<stop offset="1" stop-color="#0FAFFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1770" x1="0.9" y1="0.66565" x2="9.8888" y2="15.0477" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3BD5FF"/>
|
||||
<stop offset="1" stop-color="#0FAFFF"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint3_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7.5 5.33231) rotate(90) scale(1 0.97124)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint4_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.5 4.99899) rotate(-14.0362) scale(1.37437 0.380635)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.5 4.99899) rotate(-165.964) scale(1.37437 0.384775)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint6_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.5 7.33231) rotate(-153.435) scale(0.745357 0.359002)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint7_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.8334 7.33231) rotate(-26.565) scale(0.745357 0.290223)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint8_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.1666 7.66565) rotate(90) scale(0.666666 0.627526)">
|
||||
<stop offset="0.0638343" stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint9_linear_2092_1770" x1="6.43217" y1="3.09882" x2="8.32475" y2="8.55931" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#DFFAFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_2092_1770" x1="6.43248" y1="3.09983" x2="8.32506" y2="8.56032" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#DFFAFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2092_1770">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
@@ -20,6 +20,25 @@
|
||||
<ProjectPriFileName>PowerToys.Settings.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Settings\Icons\Models\Anthropic.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\Azure.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\Azure.svg" />
|
||||
<None Remove="Assets\Settings\Icons\Models\AzureAI.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\Bedrock.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\FoundryLocal.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\FoundryLocal.svg" />
|
||||
<None Remove="Assets\Settings\Icons\Models\Gemini.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\HuggingFace.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\Mistral.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\Ollama.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\Onnx.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\Onnx.svg" />
|
||||
<None Remove="Assets\Settings\Icons\Models\OpenAI.dark.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\OpenAI.dark.svg" />
|
||||
<None Remove="Assets\Settings\Icons\Models\OpenAI.light.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\OpenAI.light.svg" />
|
||||
<None Remove="Assets\Settings\Icons\Models\WindowsML.png" />
|
||||
<None Remove="Assets\Settings\Icons\Models\WindowsML.svg" />
|
||||
<None Remove="Assets\Settings\Modules\APDialog.dark.png" />
|
||||
<None Remove="Assets\Settings\Modules\APDialog.light.png" />
|
||||
<None Remove="Assets\Settings\Modules\LightSwitch.png" />
|
||||
@@ -105,6 +124,7 @@
|
||||
<ProjectReference Include="..\..\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj" />
|
||||
<ProjectReference Include="..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
|
||||
<ProjectReference Include="..\..\modules\MouseUtils\MouseJump.Common\MouseJump.Common.csproj" />
|
||||
<ProjectReference Include="..\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
@@ -197,4 +217,4 @@
|
||||
<Message Importance="high" Text="[Settings] Building XamlIndexBuilder prior to compile. Views='$(MSBuildProjectDirectory)\SettingsXAML\Views' Out='$(GeneratedJsonFile)'" />
|
||||
<MSBuild Projects="..\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj" Targets="Build" Properties="Configuration=$(Configuration);Platform=Any CPU;TargetFramework=net9.0;XamlViewsDir=$(MSBuildProjectDirectory)\SettingsXAML\Views;GeneratedJsonFile=$(GeneratedJsonFile)" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.Controls.FoundryLocalModelPicker"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:LanguageModelProvider"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
x:Name="Root"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<tkconverters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<Style x:Key="TagBorderStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource LayerFillColorDefaultBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrongStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Padding" Value="8,2" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style x:Key="TagTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<Setter Property="TextWrapping" Value="NoWrap" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<StackPanel
|
||||
x:Name="LoadingPanel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="12">
|
||||
<ProgressRing
|
||||
x:Name="LoadingIndicator"
|
||||
Width="36"
|
||||
Height="36"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock
|
||||
x:Name="LoadingStatusTextBlock"
|
||||
Text="Loading Foundry Local status..."
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer
|
||||
x:Name="ModelsView"
|
||||
Visibility="Collapsed">
|
||||
<Grid Padding="16,12,16,16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel
|
||||
x:Name="NoModelsPanel"
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,16"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="8">
|
||||
<FontIcon
|
||||
FontSize="24"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="No models downloaded"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Run Foundry Local to download or add a local model below."
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="LaunchFoundryModelListButton"
|
||||
Content="Open Foundry model list"
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
Click="LaunchFoundryModelListButton_Click" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1" Spacing="12">
|
||||
<ComboBox
|
||||
x:Name="CachedModelsComboBox"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{x:Bind CachedModels, Mode=OneWay}"
|
||||
PlaceholderText="Select a downloaded model"
|
||||
SelectedItem="{x:Bind SelectedModel, Mode=TwoWay}"
|
||||
SelectionChanged="CachedModelsComboBox_SelectionChanged"
|
||||
DisplayMemberPath="Name" />
|
||||
<StackPanel
|
||||
x:Name="SelectedModelDetailsPanel"
|
||||
Spacing="8"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock
|
||||
x:Name="SelectedModelDescriptionText"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
<toolkit:WrapPanel
|
||||
x:Name="SelectedModelTagsPanel"
|
||||
HorizontalSpacing="4"
|
||||
VerticalSpacing="4"
|
||||
Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid
|
||||
x:Name="NotAvailableGrid"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel
|
||||
Margin="48,0,48,48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="8">
|
||||
<Image
|
||||
Width="36"
|
||||
Source="ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg" />
|
||||
<TextBlock
|
||||
FontWeight="SemiBold"
|
||||
Text="Foundry Local is not available on this device yet."
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
IsTextSelectionEnabled="True"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap">
|
||||
<Run Text="Start the Foundry Local service before returning to PowerToys." />
|
||||
</TextBlock>
|
||||
<HyperlinkButton
|
||||
Content="Follow the Foundry Local CLI guide"
|
||||
NavigateUri="https://learn.microsoft.com/en-us/azure/ai-foundry/foundry-local/reference/reference-cli" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="StateGroup">
|
||||
<VisualState x:Name="ShowLoading" />
|
||||
<VisualState x:Name="ShowModels">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="LoadingPanel.Visibility" Value="Collapsed" />
|
||||
<Setter Target="NotAvailableGrid.Visibility" Value="Collapsed" />
|
||||
<Setter Target="ModelsView.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="ShowNotAvailable">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="LoadingPanel.Visibility" Value="Collapsed" />
|
||||
<Setter Target="NotAvailableGrid.Visibility" Value="Visible" />
|
||||
<Setter Target="ModelsView.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,452 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using LanguageModelProvider;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Controls;
|
||||
|
||||
public sealed partial class FoundryLocalModelPicker : UserControl
|
||||
{
|
||||
private INotifyCollectionChanged _cachedModelsSubscription;
|
||||
private INotifyCollectionChanged _downloadableModelsSubscription;
|
||||
private bool _suppressSelection;
|
||||
|
||||
public FoundryLocalModelPicker()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => UpdateVisualStates();
|
||||
}
|
||||
|
||||
public delegate void ModelSelectionChangedEventHandler(object sender, ModelDetails model);
|
||||
|
||||
public delegate void DownloadRequestedEventHandler(object sender, object payload);
|
||||
|
||||
public delegate void LoadRequestedEventHandler(object sender, FoundryLoadRequestedEventArgs args);
|
||||
|
||||
public event ModelSelectionChangedEventHandler SelectionChanged;
|
||||
|
||||
public event LoadRequestedEventHandler LoadRequested;
|
||||
|
||||
public IEnumerable<ModelDetails> CachedModels
|
||||
{
|
||||
get => (IEnumerable<ModelDetails>)GetValue(CachedModelsProperty);
|
||||
set => SetValue(CachedModelsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty CachedModelsProperty =
|
||||
DependencyProperty.Register(nameof(CachedModels), typeof(IEnumerable<ModelDetails>), typeof(FoundryLocalModelPicker), new PropertyMetadata(null, OnCachedModelsChanged));
|
||||
|
||||
public IEnumerable DownloadableModels
|
||||
{
|
||||
get => (IEnumerable)GetValue(DownloadableModelsProperty);
|
||||
set => SetValue(DownloadableModelsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DownloadableModelsProperty =
|
||||
DependencyProperty.Register(nameof(DownloadableModels), typeof(IEnumerable), typeof(FoundryLocalModelPicker), new PropertyMetadata(null, OnDownloadableModelsChanged));
|
||||
|
||||
public ModelDetails SelectedModel
|
||||
{
|
||||
get => (ModelDetails)GetValue(SelectedModelProperty);
|
||||
set => SetValue(SelectedModelProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SelectedModelProperty =
|
||||
DependencyProperty.Register(nameof(SelectedModel), typeof(ModelDetails), typeof(FoundryLocalModelPicker), new PropertyMetadata(null, OnSelectedModelChanged));
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => (bool)GetValue(IsLoadingProperty);
|
||||
set => SetValue(IsLoadingProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsLoadingProperty =
|
||||
DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(FoundryLocalModelPicker), new PropertyMetadata(false, OnStatePropertyChanged));
|
||||
|
||||
public bool IsAvailable
|
||||
{
|
||||
get => (bool)GetValue(IsAvailableProperty);
|
||||
set => SetValue(IsAvailableProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsAvailableProperty =
|
||||
DependencyProperty.Register(nameof(IsAvailable), typeof(bool), typeof(FoundryLocalModelPicker), new PropertyMetadata(false, OnStatePropertyChanged));
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => (string)GetValue(StatusTextProperty);
|
||||
set => SetValue(StatusTextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty StatusTextProperty =
|
||||
DependencyProperty.Register(nameof(StatusText), typeof(string), typeof(FoundryLocalModelPicker), new PropertyMetadata(string.Empty, OnStatePropertyChanged));
|
||||
|
||||
public bool HasCachedModels => CachedModels?.Any() ?? false;
|
||||
|
||||
public bool HasDownloadableModels => DownloadableModels?.Cast<object>().Any() ?? false;
|
||||
|
||||
public void RequestLoad(bool refresh)
|
||||
{
|
||||
if (IsLoading)
|
||||
{
|
||||
// Allow refresh requests to continue even if already loading by cancelling via host.
|
||||
}
|
||||
else
|
||||
{
|
||||
IsLoading = true;
|
||||
}
|
||||
|
||||
IsAvailable = false;
|
||||
StatusText = "Loading Foundry Local status...";
|
||||
LoadRequested?.Invoke(this, new FoundryLoadRequestedEventArgs(refresh));
|
||||
}
|
||||
|
||||
private static void OnCachedModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (FoundryLocalModelPicker)d;
|
||||
control.SubscribeToCachedModels(e.OldValue as IEnumerable<ModelDetails>, e.NewValue as IEnumerable<ModelDetails>);
|
||||
control.UpdateVisualStates();
|
||||
}
|
||||
|
||||
private static void OnDownloadableModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (FoundryLocalModelPicker)d;
|
||||
control.SubscribeToDownloadableModels(e.OldValue as IEnumerable, e.NewValue as IEnumerable);
|
||||
control.UpdateVisualStates();
|
||||
}
|
||||
|
||||
private static void OnSelectedModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (FoundryLocalModelPicker)d;
|
||||
if (control._suppressSelection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
control._suppressSelection = true;
|
||||
if (control.CachedModelsComboBox is not null)
|
||||
{
|
||||
control.CachedModelsComboBox.SelectedItem = e.NewValue;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
control._suppressSelection = false;
|
||||
}
|
||||
|
||||
control.UpdateSelectedModelDetails();
|
||||
}
|
||||
|
||||
private static void OnStatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (FoundryLocalModelPicker)d;
|
||||
control.UpdateVisualStates();
|
||||
}
|
||||
|
||||
private void SubscribeToCachedModels(IEnumerable<ModelDetails> oldValue, IEnumerable<ModelDetails> newValue)
|
||||
{
|
||||
if (_cachedModelsSubscription is not null)
|
||||
{
|
||||
_cachedModelsSubscription.CollectionChanged -= CachedModels_CollectionChanged;
|
||||
_cachedModelsSubscription = null;
|
||||
}
|
||||
|
||||
if (newValue is INotifyCollectionChanged observable)
|
||||
{
|
||||
observable.CollectionChanged += CachedModels_CollectionChanged;
|
||||
_cachedModelsSubscription = observable;
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeToDownloadableModels(IEnumerable oldValue, IEnumerable newValue)
|
||||
{
|
||||
if (_downloadableModelsSubscription is not null)
|
||||
{
|
||||
_downloadableModelsSubscription.CollectionChanged -= DownloadableModels_CollectionChanged;
|
||||
_downloadableModelsSubscription = null;
|
||||
}
|
||||
|
||||
if (newValue is INotifyCollectionChanged observable)
|
||||
{
|
||||
observable.CollectionChanged += DownloadableModels_CollectionChanged;
|
||||
_downloadableModelsSubscription = observable;
|
||||
}
|
||||
}
|
||||
|
||||
private void CachedModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateVisualStates();
|
||||
}
|
||||
|
||||
private void DownloadableModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateVisualStates();
|
||||
}
|
||||
|
||||
private void CachedModelsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressSelection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_suppressSelection = true;
|
||||
var selected = CachedModelsComboBox.SelectedItem as ModelDetails;
|
||||
SetValue(SelectedModelProperty, selected);
|
||||
SelectionChanged?.Invoke(this, selected);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressSelection = false;
|
||||
}
|
||||
|
||||
UpdateSelectedModelDetails();
|
||||
}
|
||||
|
||||
private void UpdateSelectedModelDetails()
|
||||
{
|
||||
if (SelectedModelDetailsPanel is null || SelectedModelDescriptionText is null || SelectedModelTagsPanel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasCachedModels || SelectedModel is not ModelDetails model)
|
||||
{
|
||||
SelectedModelDetailsPanel.Visibility = Visibility.Collapsed;
|
||||
SelectedModelDescriptionText.Text = string.Empty;
|
||||
SelectedModelTagsPanel.Children.Clear();
|
||||
SelectedModelTagsPanel.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedModelDetailsPanel.Visibility = Visibility.Visible;
|
||||
SelectedModelDescriptionText.Text = string.IsNullOrWhiteSpace(model.Description)
|
||||
? "No description provided."
|
||||
: model.Description;
|
||||
|
||||
SelectedModelTagsPanel.Children.Clear();
|
||||
|
||||
AddTag(GetModelSizeText(model.Size));
|
||||
AddTag(GetLicenseShortText(model.License), model.License);
|
||||
|
||||
foreach (var deviceTag in GetDeviceTags(model.HardwareAccelerators))
|
||||
{
|
||||
AddTag(deviceTag);
|
||||
}
|
||||
|
||||
SelectedModelTagsPanel.Visibility = SelectedModelTagsPanel.Children.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
void AddTag(string text, string tooltip = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || SelectedModelTagsPanel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Border tag = new();
|
||||
if (Resources.TryGetValue("TagBorderStyle", out var borderStyleObj) && borderStyleObj is Style borderStyle)
|
||||
{
|
||||
tag.Style = borderStyle;
|
||||
}
|
||||
|
||||
TextBlock label = new()
|
||||
{
|
||||
Text = text,
|
||||
};
|
||||
|
||||
if (Resources.TryGetValue("TagTextStyle", out var textStyleObj) && textStyleObj is Style textStyle)
|
||||
{
|
||||
label.Style = textStyle;
|
||||
}
|
||||
|
||||
tag.Child = label;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tooltip))
|
||||
{
|
||||
ToolTipService.SetToolTip(tag, new TextBlock
|
||||
{
|
||||
Text = tooltip,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
}
|
||||
|
||||
SelectedModelTagsPanel.Children.Add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchFoundryModelListButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessStartInfo processInfo = new()
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
Arguments = "-NoExit -Command \"foundry model list\"",
|
||||
UseShellExecute = true,
|
||||
};
|
||||
|
||||
Process.Start(processInfo);
|
||||
StatusText = "Opening PowerShell and running 'foundry model list'...";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Unable to start PowerShell. {ex.Message}";
|
||||
Debug.WriteLine($"[FoundryLocalModelPicker] Failed to run 'foundry model list': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisualStates()
|
||||
{
|
||||
LoadingIndicator.IsActive = IsLoading;
|
||||
|
||||
if (IsLoading)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "ShowLoading", true);
|
||||
}
|
||||
else if (!IsAvailable)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "ShowNotAvailable", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
VisualStateManager.GoToState(this, "ShowModels", true);
|
||||
}
|
||||
|
||||
if (LoadingStatusTextBlock is not null)
|
||||
{
|
||||
LoadingStatusTextBlock.Text = string.IsNullOrWhiteSpace(StatusText)
|
||||
? "Loading Foundry Local status..."
|
||||
: StatusText;
|
||||
}
|
||||
|
||||
NoModelsPanel.Visibility = HasCachedModels ? Visibility.Collapsed : Visibility.Visible;
|
||||
if (CachedModelsComboBox is not null)
|
||||
{
|
||||
CachedModelsComboBox.Visibility = HasCachedModels ? Visibility.Visible : Visibility.Collapsed;
|
||||
CachedModelsComboBox.IsEnabled = HasCachedModels;
|
||||
}
|
||||
|
||||
UpdateSelectedModelDetails();
|
||||
|
||||
Bindings.Update();
|
||||
}
|
||||
|
||||
public static string GetModelSizeText(long size)
|
||||
{
|
||||
if (size <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
const long kiloByte = 1024;
|
||||
const long megaByte = kiloByte * 1024;
|
||||
const long gigaByte = megaByte * 1024;
|
||||
|
||||
if (size >= gigaByte)
|
||||
{
|
||||
return $"{size / (double)gigaByte:0.##} GB";
|
||||
}
|
||||
|
||||
if (size >= megaByte)
|
||||
{
|
||||
return $"{size / (double)megaByte:0.##} MB";
|
||||
}
|
||||
|
||||
if (size >= kiloByte)
|
||||
{
|
||||
return $"{size / (double)kiloByte:0.##} KB";
|
||||
}
|
||||
|
||||
return $"{size} B";
|
||||
}
|
||||
|
||||
public static Visibility GetModelSizeVisibility(long size)
|
||||
{
|
||||
return size > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetDeviceTags(IReadOnlyCollection<HardwareAccelerator> accelerators)
|
||||
{
|
||||
if (accelerators is null || accelerators.Count == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
HashSet<string> tags = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var accelerator in accelerators)
|
||||
{
|
||||
switch (accelerator)
|
||||
{
|
||||
case HardwareAccelerator.CPU:
|
||||
tags.Add("CPU");
|
||||
break;
|
||||
case HardwareAccelerator.GPU:
|
||||
case HardwareAccelerator.DML:
|
||||
tags.Add("GPU");
|
||||
break;
|
||||
case HardwareAccelerator.NPU:
|
||||
case HardwareAccelerator.QNN:
|
||||
tags.Add("NPU");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return tags.Count > 0 ? tags.ToArray() : Array.Empty<string>();
|
||||
}
|
||||
|
||||
public static Visibility GetDeviceVisibility(IReadOnlyCollection<HardwareAccelerator> accelerators)
|
||||
{
|
||||
return GetDeviceTags(accelerators).Any() ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public static string GetLicenseShortText(string license)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(license))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmed = license.Trim();
|
||||
int separatorIndex = trimmed.IndexOfAny(['(', '[', ':']);
|
||||
if (separatorIndex > 0)
|
||||
{
|
||||
trimmed = trimmed[..separatorIndex].Trim();
|
||||
}
|
||||
|
||||
if (trimmed.Length > 24)
|
||||
{
|
||||
trimmed = $"{trimmed[..24].TrimEnd()}…";
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
public static Visibility GetLicenseVisibility(string license)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(license) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public sealed class FoundryLoadRequestedEventArgs : EventArgs
|
||||
{
|
||||
public FoundryLoadRequestedEventArgs(bool refresh)
|
||||
{
|
||||
Refresh = refresh;
|
||||
}
|
||||
|
||||
public bool Refresh { get; }
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,50 @@
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<ImageSource x:Key="DialogHeaderImage">ms-appx:///Assets/Settings/Modules/APDialog.dark.png</ImageSource>
|
||||
<ImageSource x:Key="OpenAIIconImage">ms-appx:///Assets/Settings/Icons/Models/OpenAI.dark.svg</ImageSource>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<ImageSource x:Key="DialogHeaderImage">ms-appx:///Assets/Settings/Modules/APDialog.light.png</ImageSource>
|
||||
<ImageSource x:Key="OpenAIIconImage">ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg</ImageSource>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<ImageSource x:Key="DialogHeaderImage">ms-appx:///Assets/Settings/Modules/APDialog.light.png</ImageSource>
|
||||
<ImageSource x:Key="OpenAIIconImage">ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg</ImageSource>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
<Style x:Name="ModelHeaderStyle" TargetType="ListViewHeaderItem">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<Setter Property="Margin" Value="0,0,0,4" />
|
||||
<Setter Property="Padding" Value="16,12,12,0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Top" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewHeaderItem">
|
||||
<StackPanel
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</StackPanel>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<DataTemplate x:Key="AdditionalActionTemplate" x:DataType="models:AdvancedPasteAdditionalAction">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<controls:ShortcutControl
|
||||
@@ -65,171 +101,235 @@
|
||||
<FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||
</InfoBar.IconSource>
|
||||
</InfoBar>
|
||||
<tkcontrols:SettingsCard
|
||||
|
||||
<!-- Paste with AI -->
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="AdvancedPasteEnableAISettingsCard"
|
||||
x:Uid="AdvancedPaste_EnableAISettingsCard"
|
||||
IsEnabled="{x:Bind ViewModel.IsOnlineAIModelsDisallowedByGPO, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<tkcontrols:SettingsCard.HeaderIcon>
|
||||
IsEnabled="{x:Bind ViewModel.IsOnlineAIModelsDisallowedByGPO, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
|
||||
IsExpanded="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}">
|
||||
|
||||
<!-- TO DO: Bind the list of Endpoints to the "ItemsSource" of this control -->
|
||||
|
||||
<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:SettingsCard.HeaderIcon>
|
||||
<tkcontrols:SwitchPresenter TargetType="x:Boolean" Value="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}">
|
||||
<tkcontrols:Case Value="True">
|
||||
<Button x:Uid="AdvancedPaste_DisableAIButton" Click="AdvancedPaste_DisableAIButton_Click" />
|
||||
</tkcontrols:Case>
|
||||
<tkcontrols:Case Value="False">
|
||||
<Button
|
||||
x:Uid="AdvancedPaste_EnableAIButton"
|
||||
Click="AdvancedPaste_EnableAIButton_Click"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</tkcontrols:Case>
|
||||
</tkcontrols:SwitchPresenter>
|
||||
<tkcontrols:SettingsCard.Description>
|
||||
</tkcontrols:SettingsExpander.HeaderIcon>
|
||||
<ToggleSwitch
|
||||
x:Name="AdvancedPaste_EnableAIToggle"
|
||||
x:Uid="AdvancedPaste_EnableAIToggle"
|
||||
IsOn="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}"
|
||||
Toggled="AdvancedPaste_EnableAIToggle_Toggled" />
|
||||
<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:SettingsCard.Description>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
</tkcontrols:SettingsExpander.Description>
|
||||
<tkcontrols:SettingsExpander.ItemsHeader>
|
||||
<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>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.ItemsHeader>
|
||||
<tkcontrols:SettingsExpander.Items />
|
||||
<!--<tkcontrols:SettingsExpander.ItemTemplate>
|
||||
<DataTemplate x:DataType="...">
|
||||
<controls:SettingsCard Description="{x:Bind ModelProvider}" Header="{x:Bind ModelName}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<ImageIcon Source="{x:Bind ModelIcon}" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<Button Content="{ui:FontIcon Glyph=, FontSize=16}">
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem Icon="{ui:FontIcon Glyph=}" Text="Edit" />
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem Icon="{ui:FontIcon Glyph=}" Text="Remove" />
|
||||
</MenuFlyout>
|
||||
</Button>
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
</tkcontrols:SettingsExpander.ItemTemplate>-->
|
||||
</tkcontrols:SettingsExpander>
|
||||
|
||||
<!-- Advanced AI -->
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="AdvancedPasteEnableAdvancedAI"
|
||||
x:Uid="AdvancedPaste_EnableAdvancedAI"
|
||||
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/SemanticKernel.png}"
|
||||
IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}">
|
||||
IsEnabled="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}"
|
||||
IsExpanded="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsAdvancedAIEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard
|
||||
Description="Choose which cloud or local model to use"
|
||||
Header="Model provider"
|
||||
IsEnabled="{x:Bind ViewModel.IsAdvancedAIEnabled, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.AdvancedAIConfiguration.ServiceType, Mode=OneWay}" />
|
||||
<Button Click="AdvancedAIProviderConfigureButton_Click" Content="Configure" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<!-- Activation and behavior -->
|
||||
<controls:SettingsGroup x:Uid="AdvancedPaste_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="AdvancedPasteClipboardHistoryEnabledSettingsCard"
|
||||
x:Uid="AdvancedPaste_Clipboard_History_Enabled_SettingsCard"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:GPOInfoControl>
|
||||
<tkcontrols:SettingsCard Name="AdvancedPasteCloseAfterLosingFocus" x:Uid="AdvancedPaste_CloseAfterLosingFocus">
|
||||
<tkcontrols:SettingsCard.HeaderIcon>
|
||||
<PathIcon Data="M 4 16.284 C 1 22.284 29 59.284 71 101.284 L 143 174.284 L 101 220.284 C 54 271.284 5 367.284 14 390.284 C 23 416.284 40 406.284 56 367.284 C 64 347.284 76 320.284 82 307.284 C 97 278.284 160 215.284 175 215.284 C 181 215.284 199 228.284 214 243.284 C 239 270.284 240 273.284 224 286.284 C 202 304.284 180 357.284 180 392.284 C 180 430.284 213 481.284 252 505.284 C 297 532.284 349 531.284 394 500.284 C 414 486.284 434 475.284 438 475.284 C 442 475.284 484 514.284 532 562.284 C 602 631.284 622 647.284 632 637.284 C 642 627.284 581 561.284 335 315.284 C 164 144.284 22 5.284 18 5.284 C 14 5.284 8 10.284 4 16.284 Z M 337 367.284 C 372 401.284 400 435.284 400 442.284 C 400 457.284 349 485.284 321 485.284 C 269 485.284 220 437.284 220 385.284 C 220 357.284 248 305.284 262 305.284 C 269 305.284 303 333.284 337 367.284 Z M 248 132.284 C 228 137.284 225 151.284 241 161.284 C 247 164.284 284 168.284 324 169.284 C 393 171.284 442 188.284 491 227.284 C 522 252.284 578 335.284 585 364.284 C 592 399.284 607 412.284 622 397.284 C 629 390.284 627 370.284 615 333.284 C 590 260.284 506 176.284 427 147.284 C 373 127.284 293 120.284 248 132.284 Z" />
|
||||
</tkcontrols:SettingsCard.HeaderIcon>
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
Name="AdvancedPasteShowCustomPreviewSettingsCard"
|
||||
x:Uid="AdvancedPaste_ShowCustomPreviewSettingsCard"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowCustomPreview, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
<controls:SettingsGroup x:Uid="AdvancedPaste_Direct_Access_Hotkeys_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="AdvancedPasteUIActions"
|
||||
x:Uid="AdvancedPasteUI_Actions"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}">
|
||||
<Button
|
||||
x:Uid="AdvancedPasteUI_AddCustomActionButton"
|
||||
Click="AddCustomActionButton_Click"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="AdvancedPasteUIShortcut"
|
||||
x:Uid="AdvancedPasteUI_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.AdvancedPasteUIShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="PasteAsPlainTextShortcut" x:Uid="PasteAsPlainText_Shortcut">
|
||||
<tkcontrols:SettingsExpander.ItemsHeader>
|
||||
<InfoBar
|
||||
x:Uid="GPO_SettingIsManaged"
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}"
|
||||
IsTabStop="{x:Bind ViewModel.ShowClipboardHistoryIsGpoConfiguredInfoBar, Mode=OneWay}"
|
||||
Severity="Informational">
|
||||
<InfoBar.IconSource>
|
||||
<FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||
</InfoBar.IconSource>
|
||||
</InfoBar>
|
||||
</tkcontrols:SettingsExpander.ItemsHeader>
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard
|
||||
Name="AdvancedPasteClipboardHistoryEnabledSettingsCard"
|
||||
ContentAlignment="Left"
|
||||
IsEnabled="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=OneWay}">
|
||||
<controls:CheckBoxWithDescriptionControl x:Uid="AdvancedPaste_Clipboard_History_Enabled_SettingsCard" IsChecked="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="AdvancedPasteCloseAfterLosingFocus" ContentAlignment="Left">
|
||||
<CheckBox x:Uid="AdvancedPaste_CloseAfterLosingFocus" IsChecked="{x:Bind ViewModel.CloseAfterLosingFocus, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="AdvancedPasteShowCustomPreviewSettingsCard" ContentAlignment="Left">
|
||||
<controls:CheckBoxWithDescriptionControl x:Uid="AdvancedPaste_ShowCustomPreviewSettingsCard" IsChecked="{x:Bind ViewModel.ShowCustomPreview, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<!-- Built-in actions -->
|
||||
<controls:SettingsGroup x:Uid="AdvancedPaste_Direct_Access_Hotkeys_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="PasteAsPlainTextShortcut"
|
||||
x:Uid="PasteAsPlainText_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.PasteAsPlainTextShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="PasteAsMarkdownShortcut" x:Uid="PasteAsMarkdown_Shortcut">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="PasteAsMarkdownShortcut"
|
||||
x:Uid="PasteAsMarkdown_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.PasteAsMarkdownShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="PasteAsJsonShortcut" x:Uid="PasteAsJson_Shortcut">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="PasteAsJsonShortcut"
|
||||
x:Uid="PasteAsJson_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.PasteAsJsonShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<ItemsControl
|
||||
x:Name="CustomActions"
|
||||
x:Uid="CustomActions"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsEnabled="{x:Bind ViewModel.IsOpenAIEnabled, Mode=OneWay}"
|
||||
IsTabStop="False"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActions, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:AdvancedPasteCustomAction">
|
||||
<tkcontrols:SettingsCard
|
||||
Margin="0,0,0,2"
|
||||
Click="EditCustomActionButton_Click"
|
||||
Description="{x:Bind Prompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsActionIconVisible="False"
|
||||
IsClickEnabled="True">
|
||||
<tkcontrols:SettingsCard.Resources>
|
||||
<x:Double x:Key="SettingsCardActionButtonWidth">0</x:Double>
|
||||
</tkcontrols:SettingsCard.Resources>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=Shortcut, Mode=TwoWay}" />
|
||||
<ToggleSwitch
|
||||
x:Uid="Enable_CustomAction"
|
||||
AutomationProperties.HelpText="{x:Bind Name, Mode=OneWay}"
|
||||
IsOn="{x:Bind IsShown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
<Button
|
||||
x:Uid="More_Options_Button"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Content=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveUp"
|
||||
Click="ReorderButtonUp_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind CanMoveUp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveDown"
|
||||
Click="ReorderButtonDown_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind CanMoveDown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="RemoveItem"
|
||||
Click="DeleteCustomActionButton_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="true" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="More_Options_ButtonTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<InfoBar
|
||||
x:Uid="AdvancedPaste_ShortcutWarning"
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind ViewModel.IsConflictingCopyShortcut, Mode=OneWay}"
|
||||
IsTabStop="{x:Bind ViewModel.IsConflictingCopyShortcut, Mode=OneWay}"
|
||||
Severity="Warning" />
|
||||
</controls:SettingsGroup>
|
||||
<controls:SettingsGroup x:Uid="AdvancedPaste_Additional_Actions_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="ImageToText"
|
||||
x:Uid="ImageToText"
|
||||
DataContext="{x:Bind ViewModel.AdditionalActions.ImageToText, Mode=OneWay}">
|
||||
DataContext="{x:Bind ViewModel.AdditionalActions.ImageToText, Mode=OneWay}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsExpander
|
||||
@@ -312,74 +412,98 @@
|
||||
IsTabStop="{x:Bind ViewModel.IsAdditionalActionConflictingCopyShortcut, Mode=OneWay}"
|
||||
Severity="Warning" />
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<!-- Custom actions -->
|
||||
<controls:SettingsGroup x:Uid="AdvancedPaste_Additional_Actions_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="AdvancedPasteUIActions"
|
||||
x:Uid="AdvancedPasteUI_Actions"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}"
|
||||
IsExpanded="True"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActions, Mode=OneWay}">
|
||||
<Button
|
||||
x:Uid="AdvancedPasteUI_AddCustomActionButton"
|
||||
Click="AddCustomActionButton_Click"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
<tkcontrols:SettingsExpander.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:AdvancedPasteCustomAction">
|
||||
<tkcontrols:SettingsCard
|
||||
Margin="0,0,0,2"
|
||||
Click="EditCustomActionButton_Click"
|
||||
Description="{x:Bind Description, Mode=OneWay}"
|
||||
Header="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsActionIconVisible="False"
|
||||
IsClickEnabled="True">
|
||||
<tkcontrols:SettingsCard.Resources>
|
||||
<x:Double x:Key="SettingsCardActionButtonWidth">0</x:Double>
|
||||
</tkcontrols:SettingsCard.Resources>
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{x:Bind Prompt, Mode=OneWay}" TextWrapping="Wrap" />
|
||||
</ToolTipService.ToolTip>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=Shortcut, Mode=TwoWay}" />
|
||||
<ToggleSwitch
|
||||
x:Uid="Enable_CustomAction"
|
||||
AutomationProperties.HelpText="{x:Bind Name, Mode=OneWay}"
|
||||
IsOn="{x:Bind IsShown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
<Button
|
||||
x:Uid="More_Options_Button"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Content=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveUp"
|
||||
Click="ReorderButtonUp_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind CanMoveUp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Tag="{x:Bind}" />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="MoveDown"
|
||||
Click="ReorderButtonDown_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind CanMoveDown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Tag="{x:Bind}" />
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="RemoveItem"
|
||||
Click="DeleteCustomActionButton_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="true"
|
||||
Tag="{x:Bind}" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="More_Options_ButtonTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
</DataTemplate>
|
||||
</tkcontrols:SettingsExpander.ItemTemplate>
|
||||
</tkcontrols:SettingsExpander>
|
||||
<InfoBar
|
||||
x:Uid="AdvancedPaste_ShortcutWarning"
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind ViewModel.IsConflictingCopyShortcut, Mode=OneWay}"
|
||||
IsTabStop="{x:Bind ViewModel.IsConflictingCopyShortcut, Mode=OneWay}"
|
||||
Severity="Warning" />
|
||||
</controls:SettingsGroup>
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
<controls:SettingsPageControl.PrimaryLinks>
|
||||
<controls:PageLink x:Uid="LearnMore_AdvancedPaste" Link="https://aka.ms/PowerToysOverview_AdvancedPaste" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
<ContentDialog
|
||||
x:Name="EnableAIDialog"
|
||||
x:Uid="EnableAIDialog"
|
||||
IsPrimaryButtonEnabled="False"
|
||||
IsSecondaryButtonEnabled="True"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<Grid RowSpacing="24">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Margin="-24,-24,-24,0" Source="{ThemeResource DialogHeaderImage}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap">
|
||||
<Run x:Uid="AdvancedPaste_EnableAIDialog_Description" />
|
||||
<Hyperlink NavigateUri="https://openai.com/policies/terms-of-use" TabIndex="3">
|
||||
<Run x:Uid="TermsLink" />
|
||||
</Hyperlink>
|
||||
<Run x:Uid="AIFooterSeparator" Foreground="{ThemeResource TextFillColorSecondaryBrush}">|</Run>
|
||||
<Hyperlink NavigateUri="https://openai.com/policies/privacy-policy" TabIndex="3">
|
||||
<Run x:Uid="PrivacyLink" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<StackPanel Grid.Row="2" Orientation="Vertical">
|
||||
<TextBlock x:Uid="AdvancedPaste_EnableAIDialog_ConfigureOpenAIKey" FontWeight="SemiBold" />
|
||||
<TextBlock Grid.Row="2" TextWrapping="Wrap">
|
||||
<Run x:Uid="AdvancedPaste_EnableAIDialog_LoginIntoText" />
|
||||
<Hyperlink NavigateUri="https://platform.openai.com/api-keys">
|
||||
<Run x:Uid="AdvancedPaste_EnableAIDialog_OpenAIApiKeysOverviewText" />
|
||||
</Hyperlink>
|
||||
<LineBreak />
|
||||
<Run x:Uid="AdvancedPaste_EnableAIDialog_CreateNewKeyText" />
|
||||
<LineBreak />
|
||||
<Run x:Uid="AdvancedPaste_EnableAIDialog_NoteAICreditsText" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="3" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
x:Uid="AdvancedPaste_EnableAIDialogOpenAIApiKey"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBox
|
||||
x:Name="AdvancedPaste_EnableAIDialogOpenAIApiKey"
|
||||
Grid.Column="1"
|
||||
MinWidth="248"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
TextChanged="AdvancedPaste_EnableAIDialogOpenAIApiKey_TextChanged" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</ContentDialog>
|
||||
<ContentDialog
|
||||
x:Name="CustomActionDialog"
|
||||
x:Uid="CustomActionDialog"
|
||||
@@ -396,6 +520,11 @@
|
||||
Width="340"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox
|
||||
x:Uid="AdvancedPasteUI_CustomAction_Description"
|
||||
Width="340"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding Description, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox
|
||||
x:Uid="AdvancedPasteUI_CustomAction_Prompt"
|
||||
Width="340"
|
||||
@@ -406,5 +535,367 @@
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
|
||||
<!-- Advanced AI provider dialog -->
|
||||
<ContentDialog
|
||||
x:Name="AdvancedAIProviderConfigurationDialog"
|
||||
Title="Advanced AI provider configuration"
|
||||
PrimaryButtonClick="AdvancedAIProviderConfigurationDialog_PrimaryButtonClick"
|
||||
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
|
||||
PrimaryButtonText="Save"
|
||||
SecondaryButtonText="Cancel">
|
||||
<ContentDialog.Resources>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">900</x:Double>
|
||||
<StaticResource x:Key="ContentDialogTopOverlay" ResourceKey="NavigationViewContentBackground" />
|
||||
</ContentDialog.Resources>
|
||||
<Grid
|
||||
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="AdvancedAIServiceTypeListView"
|
||||
Grid.Row="1"
|
||||
Margin="0,4,0,0"
|
||||
SelectedValue="{x:Bind ViewModel.AdvancedAIConfiguration.ServiceType, Mode=TwoWay}"
|
||||
SelectedValuePath="Tag"
|
||||
SelectionChanged="AdvancedAIServiceTypeListView_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>
|
||||
<!--<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">
|
||||
<StackPanel
|
||||
Margin="0,16,0,0"
|
||||
Orientation="Vertical"
|
||||
Spacing="24">
|
||||
<TextBox
|
||||
x:Name="AdvancedAIModelNameTextBox"
|
||||
MinWidth="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="Model name"
|
||||
PlaceholderText="gpt-4"
|
||||
Text="{x:Bind ViewModel.AdvancedAIConfiguration.ModelName, Mode=TwoWay}" />
|
||||
|
||||
<TextBox
|
||||
x:Name="AdvancedAISystemPromptTextBox"
|
||||
MinWidth="200"
|
||||
MinHeight="98"
|
||||
HorizontalAlignment="Stretch"
|
||||
AcceptsReturn="True"
|
||||
Header="System prompt"
|
||||
PlaceholderText="You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task. You never need to ask permission, always try to do as the user asks. The user will only input one message and will not be available for further questions, so try your best. The user will put in a request to format their clipboard data and you will fulfill it. You will not directly see the output clipboard content, and do not need to provide it in the chat. You just need to do the transform operations as needed. If you are unable to fulfill the request, end with an error message in the language of the user's request."
|
||||
Text="{x:Bind ViewModel.AdvancedAIConfiguration.SystemPrompt, Mode=TwoWay}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBox
|
||||
x:Name="AdvancedAIEndpointUrlTextBox"
|
||||
MinWidth="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="Endpoint URL"
|
||||
PlaceholderText="https://your-resource.openai.azure.com/"
|
||||
Text="{x:Bind ViewModel.AdvancedAIConfiguration.EndpointUrl, Mode=TwoWay}" />
|
||||
<TextBox
|
||||
x:Name="AdvancedAIDeploymentNameTextBox"
|
||||
MinWidth="200"
|
||||
Header="Deployment name"
|
||||
PlaceholderText="gpt-4"
|
||||
Text="{x:Bind ViewModel.AdvancedAIConfiguration.DeploymentName, Mode=TwoWay}" />
|
||||
|
||||
<PasswordBox
|
||||
x:Name="AdvancedAIApiKeyPasswordBox"
|
||||
MinWidth="200"
|
||||
Header="API key"
|
||||
PlaceholderText="Enter API Key" />
|
||||
|
||||
<CheckBox
|
||||
x:Name="AdvancedAIModerationToggle"
|
||||
x:Uid="AdvancedPaste_EnableAdvancedAIModerationToggle"
|
||||
IsChecked="{x:Bind ViewModel.AdvancedAIConfiguration.ModerationEnabled, Mode=TwoWay}"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<StackPanel
|
||||
Orientation="Vertical"
|
||||
Spacing="8"
|
||||
Visibility="{x:Bind GetServiceLegalVisibility(ViewModel.AdvancedAIConfiguration.ServiceType), Mode=OneWay}">
|
||||
<TextBlock Text="{x:Bind GetServiceLegalDescription(ViewModel.AdvancedAIConfiguration.ServiceType), Mode=OneWay}" TextWrapping="Wrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<HyperlinkButton
|
||||
Content="{x:Bind GetServiceTermsLabel(ViewModel.AdvancedAIConfiguration.ServiceType), Mode=OneWay}"
|
||||
NavigateUri="{x:Bind GetServiceTermsUri(ViewModel.AdvancedAIConfiguration.ServiceType), Mode=OneWay}"
|
||||
Visibility="{x:Bind GetServiceTermsVisibility(ViewModel.AdvancedAIConfiguration.ServiceType), Mode=OneWay}" />
|
||||
<HyperlinkButton
|
||||
Content="{x:Bind GetServicePrivacyLabel(ViewModel.AdvancedAIConfiguration.ServiceType), Mode=OneWay}"
|
||||
NavigateUri="{x:Bind GetServicePrivacyUri(ViewModel.AdvancedAIConfiguration.ServiceType), Mode=OneWay}"
|
||||
Visibility="{x:Bind GetServicePrivacyVisibility(ViewModel.AdvancedAIConfiguration.ServiceType), Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
|
||||
<!-- Paste AI provider dialog -->
|
||||
<ContentDialog
|
||||
x:Name="PasteAIProviderConfigurationDialog"
|
||||
Title="Paste with AI provider configuration"
|
||||
PrimaryButtonClick="PasteAIProviderConfigurationDialog_PrimaryButtonClick"
|
||||
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
|
||||
PrimaryButtonText="Save"
|
||||
SecondaryButtonText="Cancel">
|
||||
<ContentDialog.Resources>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">900</x:Double>
|
||||
<StaticResource x:Key="ContentDialogTopOverlay" ResourceKey="NavigationViewContentBackground" />
|
||||
</ContentDialog.Resources>
|
||||
<Grid
|
||||
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">
|
||||
<StackPanel
|
||||
Margin="0,16,0,0"
|
||||
Orientation="Vertical"
|
||||
Spacing="24">
|
||||
<TextBox
|
||||
x:Name="PasteAIModelNameTextBox"
|
||||
MinWidth="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="Model name"
|
||||
PlaceholderText="gpt-4"
|
||||
Text="{x:Bind ViewModel.PasteAIConfiguration.ModelName, Mode=TwoWay}" />
|
||||
<TextBox
|
||||
x:Name="PasteAISystemPromptTextBox"
|
||||
MinWidth="200"
|
||||
MinHeight="98"
|
||||
HorizontalAlignment="Stretch"
|
||||
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}"
|
||||
TextWrapping="Wrap" />
|
||||
<Grid
|
||||
x:Name="FoundryLocalPanel"
|
||||
Margin="0,8,0,0"
|
||||
Visibility="Collapsed">
|
||||
<controls:FoundryLocalModelPicker x:Name="FoundryLocalPicker" />
|
||||
</Grid>
|
||||
<TextBox
|
||||
x:Name="PasteAIEndpointUrlTextBox"
|
||||
MinWidth="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="Endpoint URL"
|
||||
PlaceholderText="https://your-resource.openai.azure.com/"
|
||||
Text="{x:Bind ViewModel.PasteAIConfiguration.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}"
|
||||
Visibility="Collapsed" />
|
||||
<TextBox
|
||||
x:Name="PasteAIDeploymentNameTextBox"
|
||||
MinWidth="200"
|
||||
Header="Deployment name"
|
||||
PlaceholderText="gpt-4"
|
||||
Text="{x:Bind ViewModel.PasteAIConfiguration.DeploymentName, Mode=TwoWay}" />
|
||||
<StackPanel
|
||||
x:Name="PasteAIModelPanel"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<TextBox
|
||||
x:Name="PasteAIModelPathTextBox"
|
||||
MinWidth="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="Model path"
|
||||
PlaceholderText="C:\Models\phi-3.onnx"
|
||||
Text="{x:Bind ViewModel.PasteAIConfiguration.ModelPath, Mode=TwoWay}" />
|
||||
<Button
|
||||
VerticalAlignment="Bottom"
|
||||
Click="BrowsePasteAIModelPath_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<PasswordBox
|
||||
x:Name="PasteAIApiKeyPasswordBox"
|
||||
MinWidth="200"
|
||||
Header="API key"
|
||||
PlaceholderText="Enter API Key" />
|
||||
<CheckBox
|
||||
x:Name="PasteAIModerationToggle"
|
||||
x:Uid="AdvancedPaste_EnablePasteAIModerationToggle"
|
||||
IsChecked="{x:Bind ViewModel.PasteAIConfiguration.ModerationEnabled, Mode=TwoWay}"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<StackPanel
|
||||
Margin="0,12,0,0"
|
||||
Orientation="Vertical"
|
||||
Spacing="8"
|
||||
Visibility="{x:Bind GetServiceLegalVisibility(ViewModel.PasteAIConfiguration.ServiceType), Mode=OneWay}">
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind GetServiceLegalDescription(ViewModel.PasteAIConfiguration.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}" />
|
||||
<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}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
</Grid>
|
||||
</local:NavigablePage>
|
||||
|
||||
@@ -3,10 +3,17 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
using LanguageModelProvider;
|
||||
using Microsoft.PowerToys.Settings.UI.Controls;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
@@ -15,11 +22,70 @@ using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
public sealed partial class AdvancedPastePage : NavigablePage, IRefreshablePage
|
||||
public sealed partial class AdvancedPastePage : NavigablePage, IRefreshablePage, IDisposable
|
||||
{
|
||||
private readonly ObservableCollection<ModelDetails> _foundryCachedModels = new();
|
||||
private readonly ObservableCollection<FoundryDownloadableModel> _foundryDownloadableModels = new();
|
||||
private CancellationTokenSource _foundryModelLoadCts;
|
||||
private bool _suppressFoundrySelectionChanged;
|
||||
private bool _isFoundryLocalAvailable;
|
||||
private bool _disposed;
|
||||
|
||||
private static readonly Dictionary<string, ServiceLegalInfo> ServiceLegalInformation = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["OpenAI"] = new ServiceLegalInfo(
|
||||
"Your API key connects directly to OpenAI services. By configuring this provider you agree to follow OpenAI usage policies and data handling rules.",
|
||||
"Terms of Use",
|
||||
"https://openai.com/terms",
|
||||
"Privacy Policy",
|
||||
"https://openai.com/privacy"),
|
||||
["AzureOpenAI"] = new ServiceLegalInfo(
|
||||
"This connector routes requests to OpenAI models hosted on Microsoft Azure. Saving this configuration means you accept Microsoft terms and data protections.",
|
||||
"Microsoft Azure Terms of Service",
|
||||
"https://azure.microsoft.com/support/legal/",
|
||||
"Microsoft Privacy Statement",
|
||||
"https://privacy.microsoft.com/privacystatement"),
|
||||
["AzureAIInference"] = new ServiceLegalInfo(
|
||||
"Azure AI Inference is governed by Microsoft service and privacy commitments. Continuing indicates you accept Microsoft terms for this offering.",
|
||||
"Microsoft Azure Terms of Service",
|
||||
"https://azure.microsoft.com/support/legal/",
|
||||
"Microsoft Privacy Statement",
|
||||
"https://privacy.microsoft.com/privacystatement"),
|
||||
["Google"] = new ServiceLegalInfo(
|
||||
"Connecting to Gemini requires a Google API key. Using this integration means you agree to Google's general terms and privacy policies.",
|
||||
"Google Terms of Service",
|
||||
"https://policies.google.com/terms",
|
||||
"Google Privacy Policy",
|
||||
"https://policies.google.com/privacy"),
|
||||
["Anthropic"] = new ServiceLegalInfo(
|
||||
"This integration accesses Anthropic Claude models. You are responsible for complying with Anthropic policies whenever you use this provider.",
|
||||
"Anthropic Terms of Service",
|
||||
"https://www.anthropic.com/legal/terms-of-service",
|
||||
"Anthropic Privacy Policy",
|
||||
"https://www.anthropic.com/legal/privacy"),
|
||||
["Mistral"] = new ServiceLegalInfo(
|
||||
"You can connect with a personal Mistral API key. Configuring this provider indicates you accept Mistral's published legal terms.",
|
||||
"Mistral Terms of Use",
|
||||
"https://mistral.ai/terms-of-service/",
|
||||
"Mistral Privacy Policy",
|
||||
"https://mistral.ai/privacy-policy/"),
|
||||
["AmazonBedrock"] = new ServiceLegalInfo(
|
||||
"AWS credentials let you invoke Amazon Bedrock models. Saving this setup confirms you will follow AWS service terms and privacy commitments.",
|
||||
"AWS Service Terms",
|
||||
"https://aws.amazon.com/service-terms/",
|
||||
"AWS Privacy Notice",
|
||||
"https://aws.amazon.com/privacy/"),
|
||||
["Ollama"] = new ServiceLegalInfo(
|
||||
"Ollama usage, local or remote, is bound by its license and usage policies. Continuing means you accept Ollama's terms and privacy commitments.",
|
||||
"Ollama Terms of Service",
|
||||
"https://ollama.com/terms",
|
||||
"Ollama Privacy Policy",
|
||||
"https://ollama.com/privacy"),
|
||||
};
|
||||
|
||||
private AdvancedPasteViewModel ViewModel { get; set; }
|
||||
|
||||
public ICommand SaveOpenAIKeyCommand => new RelayCommand(SaveOpenAIKey);
|
||||
public ICommand EnableAdvancedPasteAICommand => new RelayCommand(EnableAdvancedPasteAI);
|
||||
|
||||
public AdvancedPastePage()
|
||||
{
|
||||
@@ -32,47 +98,59 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
|
||||
Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.CachedModels = _foundryCachedModels;
|
||||
FoundryLocalPicker.DownloadableModels = _foundryDownloadableModels;
|
||||
FoundryLocalPicker.SelectionChanged += FoundryLocalPicker_SelectionChanged;
|
||||
FoundryLocalPicker.LoadRequested += FoundryLocalPicker_LoadRequested;
|
||||
}
|
||||
|
||||
Loaded += async (s, e) =>
|
||||
{
|
||||
ViewModel.OnPageLoaded();
|
||||
UpdateAdvancedAIUIVisibility();
|
||||
UpdatePasteAIUIVisibility();
|
||||
await UpdateFoundryLocalUIAsync();
|
||||
};
|
||||
|
||||
Unloaded += (_, _) =>
|
||||
{
|
||||
if (_foundryModelLoadCts is not null)
|
||||
{
|
||||
_foundryModelLoadCts.Cancel();
|
||||
_foundryModelLoadCts.Dispose();
|
||||
_foundryModelLoadCts = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
ViewModel.RefreshEnabledState();
|
||||
UpdatePasteAIUIVisibility();
|
||||
_ = UpdateFoundryLocalUIAsync(refreshFoundry: true);
|
||||
}
|
||||
|
||||
private void SaveOpenAIKey()
|
||||
private void EnableAdvancedPasteAI() => ViewModel.EnableAI();
|
||||
|
||||
private void AdvancedPaste_EnableAIToggle_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(AdvancedPaste_EnableAIDialogOpenAIApiKey.Text))
|
||||
if (ViewModel is null)
|
||||
{
|
||||
ViewModel.EnableAI(AdvancedPaste_EnableAIDialogOpenAIApiKey.Text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async void AdvancedPaste_EnableAIButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
EnableAIDialog.PrimaryButtonText = resourceLoader.GetString("EnableAIDialog_SaveBtnText");
|
||||
EnableAIDialog.SecondaryButtonText = resourceLoader.GetString("EnableAIDialog_CancelBtnText");
|
||||
EnableAIDialog.PrimaryButtonCommand = SaveOpenAIKeyCommand;
|
||||
var toggle = (ToggleSwitch)sender;
|
||||
|
||||
AdvancedPaste_EnableAIDialogOpenAIApiKey.Text = string.Empty;
|
||||
|
||||
await ShowEnableDialogAsync();
|
||||
}
|
||||
|
||||
private async Task ShowEnableDialogAsync()
|
||||
{
|
||||
await EnableAIDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void AdvancedPaste_DisableAIButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.DisableAI();
|
||||
}
|
||||
|
||||
private void AdvancedPaste_EnableAIDialogOpenAIApiKey_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
EnableAIDialog.IsPrimaryButtonEnabled = AdvancedPaste_EnableAIDialogOpenAIApiKey.Text.Length > 0;
|
||||
if (toggle.IsOn)
|
||||
{
|
||||
ViewModel.EnableAI();
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel.DisableAI();
|
||||
}
|
||||
}
|
||||
|
||||
public async void DeleteCustomActionButton_Click(object sender, RoutedEventArgs e)
|
||||
@@ -140,10 +218,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
if (existingCustomAction == null)
|
||||
{
|
||||
ViewModel.AddCustomAction(dialogCustomAction);
|
||||
|
||||
var element = (ContentPresenter)CustomActions.ContainerFromIndex(CustomActions.Items.Count - 1);
|
||||
element.StartBringIntoView(new BringIntoViewOptions { VerticalOffset = -60, AnimationDesired = true });
|
||||
element.Focus(FocusState.Programmatic);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -151,6 +225,764 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
}
|
||||
}
|
||||
|
||||
private static AdvancedPasteCustomAction GetBoundCustomAction(object sender) => (AdvancedPasteCustomAction)((FrameworkElement)sender).DataContext;
|
||||
private static AdvancedPasteCustomAction GetBoundCustomAction(object sender)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
if (element.DataContext is AdvancedPasteCustomAction action)
|
||||
{
|
||||
return action;
|
||||
}
|
||||
|
||||
if (element.Tag is AdvancedPasteCustomAction taggedAction)
|
||||
{
|
||||
return taggedAction;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to determine Advanced Paste custom action from sender.");
|
||||
}
|
||||
|
||||
private void BrowsePasteAIModelPath_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Use Win32 file dialog to work around FileOpenPicker issues with elevated permissions
|
||||
string selectedFile = PickFileDialog(
|
||||
"ONNX Model Files\0*.onnx\0All Files\0*.*\0",
|
||||
"Select ONNX Model File");
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedFile))
|
||||
{
|
||||
PasteAIModelPathTextBox.Text = selectedFile;
|
||||
ViewModel.PasteAIConfiguration.ModelPath = selectedFile;
|
||||
}
|
||||
}
|
||||
|
||||
private static string PickFileDialog(string filter, string title, string initialDir = null, int initialFilter = 0)
|
||||
{
|
||||
// Use Win32 OpenFileName dialog as FileOpenPicker doesn't work with elevated permissions
|
||||
OpenFileName openFileName = new OpenFileName();
|
||||
openFileName.StructSize = Marshal.SizeOf(openFileName);
|
||||
openFileName.Filter = filter;
|
||||
|
||||
// Make buffer double MAX_PATH since it can use 2 chars per char
|
||||
openFileName.File = new string(new char[260 * 2]);
|
||||
openFileName.MaxFile = openFileName.File.Length;
|
||||
openFileName.FileTitle = new string(new char[260 * 2]);
|
||||
openFileName.MaxFileTitle = openFileName.FileTitle.Length;
|
||||
openFileName.InitialDir = initialDir;
|
||||
openFileName.Title = title;
|
||||
openFileName.FilterIndex = initialFilter;
|
||||
openFileName.DefExt = null;
|
||||
openFileName.Flags = (int)OpenFileNameFlags.OFN_NOCHANGEDIR; // OFN_NOCHANGEDIR flag is needed
|
||||
IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(App.GetSettingsWindow());
|
||||
openFileName.Hwnd = windowHandle;
|
||||
|
||||
bool result = NativeMethods.GetOpenFileName(openFileName);
|
||||
if (result)
|
||||
{
|
||||
return openFileName.File;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ShowApiKeySavedMessage(string configType)
|
||||
{
|
||||
// This would typically show a TeachingTip or InfoBar
|
||||
// For now, we'll use a simple approach
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
|
||||
// In a real implementation, you'd want to show a proper notification
|
||||
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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string selectedType = PasteAIServiceTypeListView.SelectedValue.ToString();
|
||||
|
||||
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 requiresApiKey = RequiresApiKeyForService(selectedType);
|
||||
bool showModerationToggle = string.Equals(selectedType, "OpenAI", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (ViewModel.PasteAIConfiguration is not null)
|
||||
{
|
||||
ViewModel.PasteAIConfiguration.EndpointUrl = ViewModel.GetPasteAIEndpoint(selectedType);
|
||||
}
|
||||
|
||||
PasteAIEndpointUrlTextBox.Visibility = showEndpoint ? Visibility.Visible : Visibility.Collapsed;
|
||||
PasteAIDeploymentNameTextBox.Visibility = showDeployment ? Visibility.Visible : Visibility.Collapsed;
|
||||
PasteAIModelPanel.Visibility = isOnnx ? 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);
|
||||
}
|
||||
else
|
||||
{
|
||||
PasteAIApiKeyPasswordBox.Password = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAdvancedAIUIVisibility()
|
||||
{
|
||||
if (AdvancedAIServiceTypeListView?.SelectedValue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string selectedType = AdvancedAIServiceTypeListView.SelectedValue.ToString();
|
||||
|
||||
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 requiresApiKey = RequiresApiKeyForService(selectedType);
|
||||
bool showModerationToggle = string.Equals(selectedType, "OpenAI", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (ViewModel.AdvancedAIConfiguration is not null)
|
||||
{
|
||||
ViewModel.AdvancedAIConfiguration.EndpointUrl = ViewModel.GetAdvancedAIEndpoint(selectedType);
|
||||
}
|
||||
|
||||
AdvancedAIEndpointUrlTextBox.Visibility = showEndpoint ? Visibility.Visible : Visibility.Collapsed;
|
||||
AdvancedAIDeploymentNameTextBox.Visibility = showDeployment ? Visibility.Visible : Visibility.Collapsed;
|
||||
AdvancedAIModerationToggle.Visibility = showModerationToggle ? Visibility.Visible : Visibility.Collapsed;
|
||||
AdvancedAIApiKeyPasswordBox.Visibility = requiresApiKey ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
if (requiresApiKey)
|
||||
{
|
||||
AdvancedAIApiKeyPasswordBox.Password = ViewModel.GetAdvancedAIApiKey(selectedType);
|
||||
}
|
||||
else
|
||||
{
|
||||
AdvancedAIApiKeyPasswordBox.Password = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (FoundryLocalPanel is not null)
|
||||
{
|
||||
FoundryLocalPanel.Visibility = isFoundryLocal ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (!isFoundryLocal)
|
||||
{
|
||||
_foundryModelLoadCts?.Cancel();
|
||||
_isFoundryLocalAvailable = false;
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.IsLoading = false;
|
||||
FoundryLocalPicker.IsAvailable = false;
|
||||
FoundryLocalPicker.StatusText = string.Empty;
|
||||
FoundryLocalPicker.SelectedModel = null;
|
||||
}
|
||||
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
|
||||
|
||||
FoundryLocalPicker?.RequestLoad(refreshFoundry);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task LoadFoundryLocalModelsAsync(bool refresh = false)
|
||||
{
|
||||
if (FoundryLocalPanel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_foundryModelLoadCts?.Cancel();
|
||||
_foundryModelLoadCts?.Dispose();
|
||||
_foundryModelLoadCts = new CancellationTokenSource();
|
||||
var cancellationToken = _foundryModelLoadCts.Token;
|
||||
|
||||
ShowFoundryLoadingState();
|
||||
|
||||
try
|
||||
{
|
||||
var provider = FoundryLocalModelProvider.Instance;
|
||||
|
||||
var isAvailable = await provider.IsAvailable();
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isFoundryLocalAvailable = isAvailable;
|
||||
|
||||
if (!isAvailable)
|
||||
{
|
||||
ShowFoundryUnavailableState();
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<ModelDetails> cachedModelsEnumerable = refresh
|
||||
? await provider.GetModelsAsync(ignoreCached: true, cancelationToken: cancellationToken)
|
||||
: await provider.GetModelsAsync(cancelationToken: cancellationToken);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cachedModels = cachedModelsEnumerable?.ToList() ?? new List<ModelDetails>();
|
||||
var catalogModels = provider.GetAllModelsInCatalog()?.ToList() ?? new List<ModelDetails>();
|
||||
|
||||
UpdateFoundryCollections(cachedModels, catalogModels);
|
||||
ShowFoundryAvailableState();
|
||||
RestoreFoundrySelection(cachedModels);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Loading cancelled; no action required.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMessage = $"Unable to load Foundry Local models. {ex.Message}";
|
||||
ShowFoundryUnavailableState(errorMessage);
|
||||
System.Diagnostics.Debug.WriteLine($"[AdvancedPastePage] Failed to load Foundry Local models: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
UpdateFoundrySaveButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowFoundryLoadingState()
|
||||
{
|
||||
_isFoundryLocalAvailable = false;
|
||||
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.IsLoading = true;
|
||||
FoundryLocalPicker.IsAvailable = false;
|
||||
FoundryLocalPicker.StatusText = "Loading Foundry Local status...";
|
||||
FoundryLocalPicker.SelectedModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowFoundryUnavailableState(string message = null)
|
||||
{
|
||||
_isFoundryLocalAvailable = false;
|
||||
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.IsLoading = false;
|
||||
FoundryLocalPicker.IsAvailable = false;
|
||||
FoundryLocalPicker.SelectedModel = null;
|
||||
FoundryLocalPicker.StatusText = message ?? "Foundry Local was not detected. Follow the CLI guide to install and start it.";
|
||||
}
|
||||
|
||||
_foundryCachedModels.Clear();
|
||||
}
|
||||
|
||||
private void ShowFoundryAvailableState()
|
||||
{
|
||||
_isFoundryLocalAvailable = true;
|
||||
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.IsLoading = false;
|
||||
FoundryLocalPicker.IsAvailable = true;
|
||||
if (_foundryCachedModels.Count == 0)
|
||||
{
|
||||
FoundryLocalPicker.StatusText = "No local models detected. Use the button below to list models and download them with Foundry Local.";
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(FoundryLocalPicker.StatusText))
|
||||
{
|
||||
FoundryLocalPicker.StatusText = "Select a downloaded model from the list to enable Advanced Paste.";
|
||||
}
|
||||
}
|
||||
|
||||
UpdateFoundrySaveButtonState();
|
||||
}
|
||||
|
||||
private void UpdateFoundryCollections(IReadOnlyCollection<ModelDetails> cachedModels, IReadOnlyCollection<ModelDetails> catalogModels)
|
||||
{
|
||||
_foundryCachedModels.Clear();
|
||||
|
||||
foreach (var model in cachedModels.OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_foundryCachedModels.Add(model);
|
||||
}
|
||||
|
||||
var cachedReferences = new HashSet<string>(_foundryCachedModels.Select(m => NormalizeFoundryModelReference(m.Url ?? m.Name)), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_foundryDownloadableModels.Clear();
|
||||
|
||||
foreach (var model in catalogModels.OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var reference = NormalizeFoundryModelReference(model.Url ?? model.Name);
|
||||
if (cachedReferences.Contains(reference))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_foundryDownloadableModels.Add(new FoundryDownloadableModel(model));
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreFoundrySelection(IReadOnlyCollection<ModelDetails> cachedModels)
|
||||
{
|
||||
if (FoundryLocalPicker is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentModelReference = ViewModel?.PasteAIConfiguration?.ModelName;
|
||||
|
||||
ModelDetails matchingModel = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(currentModelReference))
|
||||
{
|
||||
var normalizedReference = NormalizeFoundryModelReference(currentModelReference);
|
||||
matchingModel = cachedModels.FirstOrDefault(model =>
|
||||
string.Equals(NormalizeFoundryModelReference(model.Url ?? model.Name), normalizedReference, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (FoundryLocalPicker is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_suppressFoundrySelectionChanged = true;
|
||||
FoundryLocalPicker.SelectedModel = matchingModel;
|
||||
_suppressFoundrySelectionChanged = false;
|
||||
|
||||
if (matchingModel is null)
|
||||
{
|
||||
if (ViewModel?.PasteAIConfiguration is not null)
|
||||
{
|
||||
ViewModel.PasteAIConfiguration.ModelName = string.Empty;
|
||||
}
|
||||
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.StatusText = _foundryCachedModels.Count == 0
|
||||
? "No local models detected. Use the button below to list models and download them with Foundry Local."
|
||||
: "Select a downloaded model from the list to enable Advanced Paste.";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ViewModel?.PasteAIConfiguration is not null)
|
||||
{
|
||||
ViewModel.PasteAIConfiguration.ModelName = NormalizeFoundryModelReference(matchingModel.Url ?? matchingModel.Name);
|
||||
}
|
||||
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.StatusText = $"{matchingModel.Name} selected.";
|
||||
}
|
||||
}
|
||||
|
||||
UpdateFoundrySaveButtonState();
|
||||
}
|
||||
|
||||
private static string NormalizeFoundryModelReference(string modelReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var prefix = FoundryLocalModelProvider.Instance.UrlPrefix;
|
||||
return modelReference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
? modelReference
|
||||
: $"{prefix}{modelReference}";
|
||||
}
|
||||
|
||||
private void UpdateFoundrySaveButtonState()
|
||||
{
|
||||
if (PasteAIProviderConfigurationDialog is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isFoundrySelected = string.Equals(PasteAIServiceTypeListView?.SelectedValue?.ToString(), "FoundryLocal", StringComparison.Ordinal);
|
||||
|
||||
if (!isFoundrySelected)
|
||||
{
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isFoundryLocalAvailable || _foundryDownloadableModels.Any(model => model.IsDownloading))
|
||||
{
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasSelection = FoundryLocalPicker?.SelectedModel is ModelDetails;
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = hasSelection;
|
||||
}
|
||||
|
||||
private void FoundryLocalPicker_SelectionChanged(object sender, ModelDetails selectedModel)
|
||||
{
|
||||
if (_suppressFoundrySelectionChanged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedModel is not null)
|
||||
{
|
||||
if (ViewModel?.PasteAIConfiguration is not null)
|
||||
{
|
||||
ViewModel.PasteAIConfiguration.ModelName = NormalizeFoundryModelReference(selectedModel.Url ?? selectedModel.Name);
|
||||
}
|
||||
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.StatusText = $"{selectedModel.Name} selected.";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ViewModel?.PasteAIConfiguration is not null)
|
||||
{
|
||||
ViewModel.PasteAIConfiguration.ModelName = string.Empty;
|
||||
}
|
||||
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.StatusText = "Select a downloaded model from the list to enable Advanced Paste.";
|
||||
}
|
||||
}
|
||||
|
||||
UpdateFoundrySaveButtonState();
|
||||
}
|
||||
|
||||
private async void FoundryLocalPicker_LoadRequested(object sender, FoundryLocalModelPicker.FoundryLoadRequestedEventArgs args)
|
||||
{
|
||||
await LoadFoundryLocalModelsAsync(args?.Refresh ?? false);
|
||||
}
|
||||
|
||||
private sealed class FoundryDownloadableModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly List<string> _deviceTags;
|
||||
private double _progress;
|
||||
private bool _isDownloading;
|
||||
private bool _isDownloaded;
|
||||
|
||||
public FoundryDownloadableModel(ModelDetails modelDetails)
|
||||
{
|
||||
ModelDetails = modelDetails ?? throw new ArgumentNullException(nameof(modelDetails));
|
||||
SizeTag = FoundryLocalModelPicker.GetModelSizeText(ModelDetails.Size);
|
||||
LicenseTag = FoundryLocalModelPicker.GetLicenseShortText(ModelDetails.License);
|
||||
_deviceTags = FoundryLocalModelPicker
|
||||
.GetDeviceTags(ModelDetails.HardwareAccelerators)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public ModelDetails ModelDetails { get; }
|
||||
|
||||
public string Name => string.IsNullOrWhiteSpace(ModelDetails.Name) ? "Model" : ModelDetails.Name;
|
||||
|
||||
public string Description => string.IsNullOrWhiteSpace(ModelDetails.Description) ? "No description provided." : ModelDetails.Description;
|
||||
|
||||
public string SizeTag { get; }
|
||||
|
||||
public bool HasSizeTag => !string.IsNullOrWhiteSpace(SizeTag);
|
||||
|
||||
public string LicenseTag { get; }
|
||||
|
||||
public bool HasLicenseTag => !string.IsNullOrWhiteSpace(LicenseTag);
|
||||
|
||||
public IReadOnlyList<string> DeviceTags => _deviceTags;
|
||||
|
||||
public bool HasDeviceTags => _deviceTags.Count > 0;
|
||||
|
||||
public double ProgressPercent => Math.Round(_progress * 100, 2);
|
||||
|
||||
public Visibility ProgressVisibility => _isDownloading ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public string ActionLabel => _isDownloaded ? "Downloaded" : _isDownloading ? "Downloading..." : "Download";
|
||||
|
||||
public bool CanDownload => !_isDownloading && !_isDownloaded;
|
||||
|
||||
internal bool IsDownloading => _isDownloading;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void StartDownload()
|
||||
{
|
||||
_isDownloading = true;
|
||||
_isDownloaded = false;
|
||||
_progress = 0;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public void ReportProgress(float value)
|
||||
{
|
||||
_progress = Math.Clamp(value, 0f, 1f);
|
||||
RaisePropertyChanged(nameof(ProgressPercent));
|
||||
}
|
||||
|
||||
public void MarkDownloaded()
|
||||
{
|
||||
_isDownloading = false;
|
||||
_isDownloaded = true;
|
||||
_progress = 1;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_isDownloading = false;
|
||||
_isDownloaded = false;
|
||||
_progress = 0;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
private void NotifyStateChanged()
|
||||
{
|
||||
RaisePropertyChanged(nameof(ProgressPercent));
|
||||
RaisePropertyChanged(nameof(ProgressVisibility));
|
||||
RaisePropertyChanged(nameof(ActionLabel));
|
||||
RaisePropertyChanged(nameof(CanDownload));
|
||||
}
|
||||
|
||||
private void RaisePropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
private async void AdvancedAIProviderConfigureButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateAdvancedAIUIVisibility();
|
||||
await AdvancedAIProviderConfigurationDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void AdvancedAIProviderConfigurationDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
string serviceType = AdvancedAIServiceTypeListView.SelectedValue?.ToString() ?? "OpenAI";
|
||||
string apiKey = AdvancedAIApiKeyPasswordBox.Password;
|
||||
string trimmedApiKey = apiKey?.Trim() ?? string.Empty;
|
||||
string endpoint = (ViewModel.AdvancedAIConfiguration.EndpointUrl ?? string.Empty).Trim();
|
||||
|
||||
if (RequiresApiKeyForService(serviceType) && string.IsNullOrWhiteSpace(trimmedApiKey))
|
||||
{
|
||||
args.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.AdvancedAIConfiguration.EndpointUrl = endpoint;
|
||||
ViewModel.SaveAdvancedAICredential(serviceType, endpoint, trimmedApiKey);
|
||||
ViewModel.AdvancedAIConfiguration.EndpointUrl = ViewModel.GetAdvancedAIEndpoint(serviceType);
|
||||
AdvancedAIApiKeyPasswordBox.Password = ViewModel.GetAdvancedAIApiKey(serviceType);
|
||||
|
||||
// Show success message
|
||||
ShowApiKeySavedMessage("Advanced AI");
|
||||
}
|
||||
|
||||
private void PasteAIProviderConfigurationDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
string serviceType = PasteAIServiceTypeListView.SelectedValue?.ToString() ?? "OpenAI";
|
||||
string apiKey = PasteAIApiKeyPasswordBox.Password;
|
||||
string trimmedApiKey = apiKey?.Trim() ?? string.Empty;
|
||||
string endpoint = (ViewModel.PasteAIConfiguration.EndpointUrl ?? string.Empty).Trim();
|
||||
|
||||
if (RequiresApiKeyForService(serviceType) && string.IsNullOrWhiteSpace(trimmedApiKey))
|
||||
{
|
||||
args.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.PasteAIConfiguration.EndpointUrl = endpoint;
|
||||
ViewModel.SavePasteAICredential(serviceType, endpoint, trimmedApiKey);
|
||||
ViewModel.PasteAIConfiguration.EndpointUrl = ViewModel.GetPasteAIEndpoint(serviceType);
|
||||
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(serviceType);
|
||||
|
||||
// Show success message
|
||||
ShowApiKeySavedMessage("Paste AI");
|
||||
}
|
||||
|
||||
private async void PasteAIProviderConfigureButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdatePasteAIUIVisibility();
|
||||
await UpdateFoundryLocalUIAsync(refreshFoundry: true);
|
||||
await PasteAIProviderConfigurationDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void AdvancedAIServiceTypeListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
UpdateAdvancedAIUIVisibility();
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private bool HasServiceLegalInfo(string serviceType) => TryGetServiceLegalInfo(serviceType, out _);
|
||||
|
||||
private string GetServiceLegalDescription(string serviceType) => TryGetServiceLegalInfo(serviceType, out var info) ? info.Description : string.Empty;
|
||||
|
||||
private string GetServiceTermsLabel(string serviceType) => TryGetServiceLegalInfo(serviceType, out var info) ? info.TermsLabel : string.Empty;
|
||||
|
||||
private Uri GetServiceTermsUri(string serviceType) => TryGetServiceLegalInfo(serviceType, out var info) ? info.TermsUri : null;
|
||||
|
||||
private string GetServicePrivacyLabel(string serviceType) => TryGetServiceLegalInfo(serviceType, out var info) ? info.PrivacyLabel : string.Empty;
|
||||
|
||||
private Uri GetServicePrivacyUri(string serviceType) => TryGetServiceLegalInfo(serviceType, out var info) ? info.PrivacyUri : null;
|
||||
|
||||
private bool HasServiceTermsLink(string serviceType) => TryGetServiceLegalInfo(serviceType, out var info) && info.TermsUri is not null && !string.IsNullOrEmpty(info.TermsLabel);
|
||||
|
||||
private bool HasServicePrivacyLink(string serviceType) => TryGetServiceLegalInfo(serviceType, out var info) && info.PrivacyUri is not null && !string.IsNullOrEmpty(info.PrivacyLabel);
|
||||
|
||||
private Visibility GetServiceLegalVisibility(string serviceType) => HasServiceLegalInfo(serviceType) ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
private Visibility GetServiceTermsVisibility(string serviceType) => HasServiceTermsLink(serviceType) ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
private Visibility GetServicePrivacyVisibility(string serviceType) => HasServicePrivacyLink(serviceType) ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
private static bool TryGetServiceLegalInfo(string serviceType, out ServiceLegalInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serviceType))
|
||||
{
|
||||
return ServiceLegalInformation.TryGetValue("OpenAI", out info);
|
||||
}
|
||||
|
||||
return ServiceLegalInformation.TryGetValue(serviceType, out info);
|
||||
}
|
||||
|
||||
private void RefreshDialogBindings()
|
||||
{
|
||||
try
|
||||
{
|
||||
Bindings?.Update();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Best-effort refresh only; ignore refresh failures.
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_foundryModelLoadCts?.Cancel();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore cancellation failures during disposal.
|
||||
}
|
||||
|
||||
_foundryModelLoadCts?.Dispose();
|
||||
_foundryModelLoadCts = null;
|
||||
|
||||
if (FoundryLocalPicker is not null)
|
||||
{
|
||||
FoundryLocalPicker.SelectionChanged -= FoundryLocalPicker_SelectionChanged;
|
||||
FoundryLocalPicker.LoadRequested -= FoundryLocalPicker_LoadRequested;
|
||||
}
|
||||
|
||||
ViewModel?.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private sealed class ServiceLegalInfo
|
||||
{
|
||||
public ServiceLegalInfo(string description, string termsLabel, string termsUri, string privacyLabel, string privacyUri)
|
||||
{
|
||||
Description = description ?? string.Empty;
|
||||
TermsLabel = termsLabel ?? string.Empty;
|
||||
TermsUri = CreateUriOrNull(termsUri);
|
||||
PrivacyLabel = privacyLabel ?? string.Empty;
|
||||
PrivacyUri = CreateUriOrNull(privacyUri);
|
||||
}
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public string TermsLabel { get; }
|
||||
|
||||
public Uri TermsUri { get; }
|
||||
|
||||
public string PrivacyLabel { get; }
|
||||
|
||||
public Uri PrivacyUri { get; }
|
||||
|
||||
private static Uri CreateUriOrNull(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Uri.TryCreate(value, UriKind.Absolute, out var uri) ? uri : null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProviderMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuFlyoutItem menuItem && menuItem.Tag is string tag)
|
||||
{
|
||||
// TODO: Open dialog and set the right fields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,9 @@
|
||||
AutomationProperties.AutomationId="SystemToolsNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/SystemTools.png}"
|
||||
SelectsOnInvoked="False">
|
||||
<NavigationViewItem.InfoBadge>
|
||||
<InfoBadge Style="{StaticResource NewInfoBadge}" />
|
||||
</NavigationViewItem.InfoBadge>
|
||||
<NavigationViewItem.MenuItems>
|
||||
<NavigationViewItem
|
||||
x:Name="AdvancedPasteNavigationItem"
|
||||
@@ -207,7 +210,11 @@
|
||||
x:Uid="Shell_LightSwitch"
|
||||
helpers:NavHelper.NavigateTo="views:LightSwitchPage"
|
||||
AutomationProperties.AutomationId="LightSwitchNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}" />
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}">
|
||||
<NavigationViewItem.InfoBadge>
|
||||
<InfoBadge Style="{StaticResource NewInfoBadge}" />
|
||||
</NavigationViewItem.InfoBadge>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem
|
||||
x:Name="PowerLauncherNavigationItem"
|
||||
x:Uid="Shell_PowerLauncher"
|
||||
|
||||
@@ -626,17 +626,35 @@ opera.exe</value>
|
||||
<data name="AdvancedPaste_EnableAISettingsCard.Header" xml:space="preserve">
|
||||
<value>Enable Paste with AI</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAIDialogMarkdown.Text" xml:space="preserve">
|
||||
<value>## Preview Terms
|
||||
|
||||
Please review the placeholder content that represents the final terms and usage guidance for Advanced Paste.
|
||||
|
||||
1. This is sample information.
|
||||
2. Real policy content will be provided later.
|
||||
3. Continue only if you are comfortable with the above.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAIDialogAcceptanceCheckBox.Content" xml:space="preserve">
|
||||
<value>I have read and accept the information above.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAdvancedAIModerationToggle.Content" xml:space="preserve">
|
||||
<value>Enable OpenAI content moderation</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnablePasteAIModerationToggle.Content" xml:space="preserve">
|
||||
<value>Enable OpenAI content moderation</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_Clipboard_History_Enabled_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Clipboard history</value>
|
||||
<value>Access Clipboard History</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_Clipboard_History_Enabled_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Save multiple items to your clipboard. This is an OS feature.</value>
|
||||
<value>Clipboard History shows a list of previously copied items.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_Direct_Access_Hotkeys_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Actions</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_Additional_Actions_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Additional actions</value>
|
||||
<value>Custom actions</value>
|
||||
</data>
|
||||
<data name="RemapKeysList.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Current key remappings</value>
|
||||
@@ -1956,6 +1974,9 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
|
||||
<data name="AdvancedPasteUI_CustomAction_Name.Header" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="AdvancedPasteUI_CustomAction_Description.Header" xml:space="preserve">
|
||||
<value>Description</value>
|
||||
</data>
|
||||
<data name="AdvancedPasteUI_CustomAction_Prompt.Header" xml:space="preserve">
|
||||
<value>Prompt</value>
|
||||
</data>
|
||||
@@ -3301,7 +3322,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<value>An AI powered tool to put your clipboard content into any format you need, focused towards developer workflows.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAISettingsCardDescription.Text" xml:space="preserve">
|
||||
<value>This feature allows you to format your clipboard content with the power of AI. An OpenAI API key is required.</value>
|
||||
<value>Transform your clipboard content with the power of AI. An cloud or local endpoint is required.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore.Content" xml:space="preserve">
|
||||
<value>Learn more</value>
|
||||
@@ -3935,7 +3956,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<value>Paste with AI</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_BehaviorSettingsGroup.Header" xml:space="preserve">
|
||||
<value>Behavior</value>
|
||||
<value>Activation & behavior</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_ShowCustomPreviewSettingsCard.Header" xml:space="preserve">
|
||||
<value>Custom format preview</value>
|
||||
@@ -3944,10 +3965,10 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<value>Preview the output of AI formats and Image to text before pasting</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAdvancedAI.Header" xml:space="preserve">
|
||||
<value>Enable advanced AI</value>
|
||||
<value>Advanced AI</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAdvancedAI.Description" xml:space="preserve">
|
||||
<value>Add advanced capabilities when using 'Paste with AI' including the power to 'chain' multiple transformations together and work with images and files. This feature may consume more API credits when used.</value>
|
||||
<value>Supports advanced workflows by chaining transformations and working with files and images. May use additional API credits.</value>
|
||||
</data>
|
||||
<data name="Oobe_AdvancedPaste.Description" xml:space="preserve">
|
||||
<value>Advanced Paste is a tool to put your clipboard content into any format you need, focused towards developer workflows. It can paste as plain text, markdown, or json directly with the UX or with a direct keystroke invoke. These are fully locally executed. In addition, it has an AI powered option that is 100% opt-in and requires an Open AI key. Note: this will replace the formatted text in your clipboard with the selected format.</value>
|
||||
@@ -4476,9 +4497,9 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<data name="AdvancedPaste_EnableAIDialog_NoteAICreditsErrorText.Text" xml:space="preserve">
|
||||
<value>If you do not have credits you will see an 'API key quota exceeded' error</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_CloseAfterLosingFocus.Header" xml:space="preserve">
|
||||
<value>Automatically close the AdvancedPaste window after it loses focus</value>
|
||||
<comment>AdvancedPaste is a product name, do not loc</comment>
|
||||
<data name="AdvancedPaste_CloseAfterLosingFocus.Content" xml:space="preserve">
|
||||
<value>Automatically close the Advanced Paste window after it loses focus</value>
|
||||
<comment>Advanced Paste is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="GPO_CommandNotFound_ForceDisabled.Title" xml:space="preserve">
|
||||
<value>The Command Not Found module is disabled by your organization.</value>
|
||||
|
||||
@@ -12,7 +12,6 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Timers;
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
@@ -27,22 +26,41 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
public partial class AdvancedPasteViewModel : PageViewModelBase
|
||||
{
|
||||
private static readonly HashSet<string> WarnHotkeys = ["Ctrl + V", "Ctrl + Shift + V"];
|
||||
private bool _disposed;
|
||||
private static readonly HashSet<string> AdvancedAITrackedProperties = new(StringComparer.Ordinal)
|
||||
{
|
||||
nameof(AdvancedAIConfiguration.ModelName),
|
||||
nameof(AdvancedAIConfiguration.EndpointUrl),
|
||||
nameof(AdvancedAIConfiguration.ApiVersion),
|
||||
nameof(AdvancedAIConfiguration.DeploymentName),
|
||||
nameof(AdvancedAIConfiguration.ModelPath),
|
||||
nameof(AdvancedAIConfiguration.SystemPrompt),
|
||||
nameof(AdvancedAIConfiguration.ModerationEnabled),
|
||||
};
|
||||
|
||||
// Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval
|
||||
private const int SaveSettingsDelayInMs = 500;
|
||||
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;
|
||||
|
||||
protected override string ModuleName => AdvancedPasteSettings.ModuleName;
|
||||
|
||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
private readonly System.Threading.Lock _delayedActionLock = new System.Threading.Lock();
|
||||
|
||||
private readonly AdvancedPasteSettings _advancedPasteSettings;
|
||||
private readonly AdvancedPasteAdditionalActions _additionalActions;
|
||||
private readonly ObservableCollection<AdvancedPasteCustomAction> _customActions;
|
||||
private Timer _delayedTimer;
|
||||
|
||||
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
@@ -71,19 +89,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
|
||||
|
||||
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
|
||||
SeedProviderConfigurationSnapshots();
|
||||
|
||||
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
|
||||
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
|
||||
|
||||
InitializeEnabledValue();
|
||||
AttachConfigurationHandlers();
|
||||
|
||||
// set the callback functions value to handle outgoing IPC message.
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
_delayedTimer = new Timer();
|
||||
_delayedTimer.Interval = SaveSettingsDelayInMs;
|
||||
_delayedTimer.Elapsed += DelayedTimer_Tick;
|
||||
_delayedTimer.AutoReset = false;
|
||||
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
|
||||
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
|
||||
|
||||
LoadAdvancedAIProviderConfiguration();
|
||||
LoadPasteAIProviderConfiguration();
|
||||
|
||||
InitializeEnabledValue();
|
||||
MigrateLegacyAIEnablement();
|
||||
|
||||
foreach (var action in _additionalActions.GetAllActions())
|
||||
{
|
||||
@@ -144,15 +164,32 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
|
||||
_onlineAIModelsGpoRuleConfiguration = GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue();
|
||||
if (_onlineAIModelsGpoRuleConfiguration == GpoRuleConfigured.Disabled)
|
||||
{
|
||||
_onlineAIModelsDisallowedByGPO = true;
|
||||
_onlineAIModelsDisallowedByGPO = _onlineAIModelsGpoRuleConfiguration == GpoRuleConfigured.Disabled;
|
||||
|
||||
if (_onlineAIModelsDisallowedByGPO)
|
||||
{
|
||||
// disable AI if it was enabled
|
||||
DisableAI();
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateLegacyAIEnablement()
|
||||
{
|
||||
if (_advancedPasteSettings.Properties.IsAIEnabled || IsOnlineAIModelsDisallowedByGPO)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LegacyOpenAIKeyExists())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_advancedPasteSettings.Properties.IsAIEnabled = true;
|
||||
SaveAndNotifySettings();
|
||||
OnPropertyChanged(nameof(IsAIEnabled));
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
@@ -184,25 +221,36 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
|
||||
|
||||
private bool OpenAIKeyExists()
|
||||
{
|
||||
PasswordVault vault = new PasswordVault();
|
||||
PasswordCredential cred = null;
|
||||
public bool IsAIEnabled => _advancedPasteSettings.Properties.IsAIEnabled && !IsOnlineAIModelsDisallowedByGPO;
|
||||
|
||||
private bool LegacyOpenAIKeyExists()
|
||||
{
|
||||
try
|
||||
{
|
||||
cred = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
||||
PasswordVault vault = new();
|
||||
|
||||
// return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
|
||||
var legacyOpenAIKey = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
||||
if (legacyOpenAIKey != null)
|
||||
{
|
||||
string credentialResource = GetAICredentialResource("OpenAI");
|
||||
string credentialUserName = GetPasteAICredentialUserName("OpenAI");
|
||||
PasswordCredential cred = new(credentialResource, credentialUserName, legacyOpenAIKey.Password);
|
||||
vault.Add(cred);
|
||||
|
||||
// delete old key
|
||||
TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return cred is not null;
|
||||
}
|
||||
|
||||
public bool IsOpenAIEnabled => OpenAIKeyExists() && !IsOnlineAIModelsDisallowedByGPO;
|
||||
|
||||
public bool IsEnabledGpoConfigured
|
||||
{
|
||||
get => _enabledStateIsGPOConfigured;
|
||||
@@ -361,6 +409,44 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public AdvancedAIConfiguration AdvancedAIConfiguration
|
||||
{
|
||||
get => _advancedPasteSettings.Properties.AdvancedAIConfiguration;
|
||||
set
|
||||
{
|
||||
if (!ReferenceEquals(value, _advancedPasteSettings.Properties.AdvancedAIConfiguration))
|
||||
{
|
||||
UnsubscribeFromAdvancedAIConfiguration(_advancedPasteSettings.Properties.AdvancedAIConfiguration);
|
||||
|
||||
var newValue = value ?? new AdvancedAIConfiguration();
|
||||
_advancedPasteSettings.Properties.AdvancedAIConfiguration = newValue;
|
||||
SubscribeToAdvancedAIConfiguration(newValue);
|
||||
|
||||
OnPropertyChanged(nameof(AdvancedAIConfiguration));
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration
|
||||
{
|
||||
get => _advancedPasteSettings.Properties.PasteAIConfiguration;
|
||||
set
|
||||
{
|
||||
if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration))
|
||||
{
|
||||
UnsubscribeFromPasteAIConfiguration(_advancedPasteSettings.Properties.PasteAIConfiguration);
|
||||
|
||||
var newValue = value ?? new PasteAIConfiguration();
|
||||
_advancedPasteSettings.Properties.PasteAIConfiguration = newValue;
|
||||
SubscribeToPasteAIConfiguration(newValue);
|
||||
|
||||
OnPropertyChanged(nameof(PasteAIConfiguration));
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowCustomPreview
|
||||
{
|
||||
get => _advancedPasteSettings.Properties.ShowCustomPreview;
|
||||
@@ -398,15 +484,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
.Select(additionalAction => additionalAction.Shortcut)
|
||||
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
||||
|
||||
private void DelayedTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
lock (_delayedActionLock)
|
||||
{
|
||||
_delayedTimer.Stop();
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifySettingsChanged()
|
||||
{
|
||||
// Using InvariantCulture as this is an IPC message
|
||||
@@ -421,9 +498,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
InitializeEnabledValue();
|
||||
MigrateLegacyAIEnablement();
|
||||
OnPropertyChanged(nameof(IsEnabled));
|
||||
OnPropertyChanged(nameof(ShowOnlineAIModelsGpoConfiguredInfoBar));
|
||||
OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar));
|
||||
OnPropertyChanged(nameof(IsAIEnabled));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -432,7 +511,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_delayedTimer?.Dispose();
|
||||
UnsubscribeFromAdvancedAIConfiguration(_advancedPasteSettings?.Properties.AdvancedAIConfiguration);
|
||||
UnsubscribeFromPasteAIConfiguration(_advancedPasteSettings?.Properties.PasteAIConfiguration);
|
||||
|
||||
foreach (var action in _additionalActions.GetAllActions())
|
||||
{
|
||||
@@ -457,10 +537,92 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordVault vault = new PasswordVault();
|
||||
PasswordCredential cred = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
||||
vault.Remove(cred);
|
||||
OnPropertyChanged(nameof(IsOpenAIEnabled));
|
||||
bool stateChanged = false;
|
||||
|
||||
if (_advancedPasteSettings.Properties.IsAIEnabled)
|
||||
{
|
||||
_advancedPasteSettings.Properties.IsAIEnabled = false;
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
if (stateChanged)
|
||||
{
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsAIEnabled));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal void EnableAI()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsOnlineAIModelsDisallowedByGPO)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool stateChanged = false;
|
||||
|
||||
if (!_advancedPasteSettings.Properties.IsAIEnabled)
|
||||
{
|
||||
_advancedPasteSettings.Properties.IsAIEnabled = true;
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
if (stateChanged)
|
||||
{
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsAIEnabled));
|
||||
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal void SaveAdvancedAICredential(string serviceType, string endpoint, string apiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
endpoint = endpoint?.Trim() ?? string.Empty;
|
||||
apiKey = apiKey?.Trim() ?? string.Empty;
|
||||
string credentialResource = GetAICredentialResource(serviceType);
|
||||
string credentialUserName = GetAdvancedAICredentialUserName(serviceType);
|
||||
string endpointCredentialUserName = GetAdvancedAIEndpointCredentialUserName(serviceType);
|
||||
|
||||
PasswordVault vault = new();
|
||||
TryRemoveCredential(vault, credentialResource, credentialUserName);
|
||||
TryRemoveCredential(vault, credentialResource, endpointCredentialUserName);
|
||||
|
||||
bool storeApiKey = RequiresCredentialStorage(serviceType) && !string.IsNullOrWhiteSpace(apiKey);
|
||||
if (storeApiKey)
|
||||
{
|
||||
PasswordCredential cred = new(credentialResource, credentialUserName, apiKey);
|
||||
vault.Add(cred);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
PasswordCredential endpointCred = new(credentialResource, endpointCredentialUserName, endpoint);
|
||||
vault.Add(endpointCred);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsAIEnabled));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -468,15 +630,33 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
internal void EnableAI(string password)
|
||||
internal void SavePasteAICredential(string serviceType, string endpoint, string apiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
endpoint = endpoint?.Trim() ?? string.Empty;
|
||||
apiKey = apiKey?.Trim() ?? string.Empty;
|
||||
string credentialResource = GetAICredentialResource(serviceType);
|
||||
string credentialUserName = GetPasteAICredentialUserName(serviceType);
|
||||
string endpointCredentialUserName = GetPasteAIEndpointCredentialUserName(serviceType);
|
||||
PasswordVault vault = new();
|
||||
PasswordCredential cred = new("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey", password);
|
||||
vault.Add(cred);
|
||||
OnPropertyChanged(nameof(IsOpenAIEnabled));
|
||||
IsAdvancedAIEnabled = true; // new users should get Semantic Kernel benefits immediately
|
||||
TryRemoveCredential(vault, credentialResource, credentialUserName);
|
||||
TryRemoveCredential(vault, credentialResource, endpointCredentialUserName);
|
||||
|
||||
bool storeApiKey = RequiresCredentialStorage(serviceType) && !string.IsNullOrWhiteSpace(apiKey);
|
||||
if (storeApiKey)
|
||||
{
|
||||
PasswordCredential cred = new(credentialResource, credentialUserName, apiKey);
|
||||
vault.Add(cred);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
PasswordCredential endpointCred = new(credentialResource, endpointCredentialUserName, endpoint);
|
||||
vault.Add(endpointCred);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsAIEnabled));
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -484,6 +664,135 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
internal string GetAdvancedAIApiKey(string serviceType)
|
||||
{
|
||||
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
||||
return RetrieveCredentialValue(
|
||||
GetAICredentialResource(serviceType),
|
||||
GetAdvancedAICredentialUserName(serviceType));
|
||||
}
|
||||
|
||||
internal string GetPasteAIApiKey(string serviceType)
|
||||
{
|
||||
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
||||
return RetrieveCredentialValue(
|
||||
GetAICredentialResource(serviceType),
|
||||
GetPasteAICredentialUserName(serviceType));
|
||||
}
|
||||
|
||||
internal string GetAdvancedAIEndpoint(string serviceType)
|
||||
{
|
||||
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
||||
return RetrieveCredentialValue(
|
||||
GetAICredentialResource(serviceType),
|
||||
GetAdvancedAIEndpointCredentialUserName(serviceType));
|
||||
}
|
||||
|
||||
internal string GetPasteAIEndpoint(string serviceType)
|
||||
{
|
||||
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
||||
return RetrieveCredentialValue(
|
||||
GetAICredentialResource(serviceType),
|
||||
GetPasteAIEndpointCredentialUserName(serviceType));
|
||||
}
|
||||
|
||||
private static string GetAdvancedAICredentialUserName(string serviceType)
|
||||
{
|
||||
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
||||
return serviceType.ToLowerInvariant() switch
|
||||
{
|
||||
"openai" => "PowerToys_AdvancedPaste_AdvancedAI_OpenAI",
|
||||
"azureopenai" => "PowerToys_AdvancedPaste_AdvancedAI_AzureOpenAI",
|
||||
"azureaiinference" => "PowerToys_AdvancedPaste_AdvancedAI_AzureAIInference",
|
||||
"mistral" => "PowerToys_AdvancedPaste_AdvancedAI_Mistral",
|
||||
"google" => "PowerToys_AdvancedPaste_AdvancedAI_Google",
|
||||
"huggingface" => "PowerToys_AdvancedPaste_AdvancedAI_HuggingFace",
|
||||
"anthropic" => "PowerToys_AdvancedPaste_AdvancedAI_Anthropic",
|
||||
"amazonbedrock" => "PowerToys_AdvancedPaste_AdvancedAI_AmazonBedrock",
|
||||
"ollama" => "PowerToys_AdvancedPaste_AdvancedAI_Ollama",
|
||||
_ => "PowerToys_AdvancedPaste_AdvancedAI_OpenAI",
|
||||
};
|
||||
}
|
||||
|
||||
private string GetAdvancedAIEndpointCredentialUserName(string serviceType)
|
||||
{
|
||||
return GetAdvancedAICredentialUserName(serviceType) + "_Endpoint";
|
||||
}
|
||||
|
||||
private string GetAICredentialResource(string serviceType)
|
||||
{
|
||||
serviceType = string.IsNullOrWhiteSpace(serviceType) ? "OpenAI" : serviceType;
|
||||
return serviceType.ToLowerInvariant() switch
|
||||
{
|
||||
"openai" => "https://platform.openai.com/api-keys",
|
||||
"azureopenai" => "https://azure.microsoft.com/products/ai-services/openai-service",
|
||||
"azureaiinference" => "https://azure.microsoft.com/products/ai-services/ai-inference",
|
||||
"mistral" => "https://console.mistral.ai/account/api-keys",
|
||||
"google" => "https://ai.google.dev/",
|
||||
"huggingface" => "https://huggingface.co/settings/tokens",
|
||||
"anthropic" => "https://console.anthropic.com/account/keys",
|
||||
"amazonbedrock" => "https://aws.amazon.com/bedrock/",
|
||||
"ollama" => "https://ollama.com/",
|
||||
_ => "https://platform.openai.com/api-keys",
|
||||
};
|
||||
}
|
||||
|
||||
private string GetPasteAICredentialUserName(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",
|
||||
};
|
||||
}
|
||||
|
||||
private string GetPasteAIEndpointCredentialUserName(string serviceType)
|
||||
{
|
||||
return GetPasteAICredentialUserName(serviceType) + "_Endpoint";
|
||||
}
|
||||
|
||||
private static bool RequiresCredentialStorage(string serviceType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serviceType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return serviceType.ToLowerInvariant() switch
|
||||
{
|
||||
"onnx" => false,
|
||||
"ollama" => false,
|
||||
"foundrylocal" => false,
|
||||
"windowsml" => false,
|
||||
"anthropic" => false,
|
||||
"amazonbedrock" => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
|
||||
vault.Remove(existingCred);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Credential doesn't exist, which is fine
|
||||
}
|
||||
}
|
||||
|
||||
internal AdvancedPasteCustomAction GetNewCustomAction(string namePrefix)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(namePrefix);
|
||||
@@ -593,6 +902,249 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
SaveCustomActions();
|
||||
}
|
||||
|
||||
private void AttachConfigurationHandlers()
|
||||
{
|
||||
SubscribeToAdvancedAIConfiguration(_advancedPasteSettings.Properties.AdvancedAIConfiguration);
|
||||
SubscribeToPasteAIConfiguration(_advancedPasteSettings.Properties.PasteAIConfiguration);
|
||||
}
|
||||
|
||||
private void SubscribeToAdvancedAIConfiguration(AdvancedAIConfiguration configuration)
|
||||
{
|
||||
if (configuration is not null)
|
||||
{
|
||||
configuration.PropertyChanged += OnAdvancedAIConfigurationPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromAdvancedAIConfiguration(AdvancedAIConfiguration configuration)
|
||||
{
|
||||
if (configuration is not null)
|
||||
{
|
||||
configuration.PropertyChanged -= OnAdvancedAIConfigurationPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeToPasteAIConfiguration(PasteAIConfiguration configuration)
|
||||
{
|
||||
if (configuration is not null)
|
||||
{
|
||||
configuration.PropertyChanged += OnPasteAIConfigurationPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeFromPasteAIConfiguration(PasteAIConfiguration configuration)
|
||||
{
|
||||
if (configuration is not null)
|
||||
{
|
||||
configuration.PropertyChanged -= OnPasteAIConfigurationPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAdvancedAIConfigurationPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_isLoadingAdvancedAIProviderConfiguration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(e.PropertyName, nameof(AdvancedAIConfiguration.ServiceType), StringComparison.Ordinal))
|
||||
{
|
||||
LoadAdvancedAIProviderConfiguration();
|
||||
SaveAndNotifySettings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.PropertyName is not null && AdvancedAITrackedProperties.Contains(e.PropertyName))
|
||||
{
|
||||
PersistAdvancedAIProviderConfiguration();
|
||||
}
|
||||
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
|
||||
private void OnPasteAIConfigurationPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_isLoadingPasteAIProviderConfiguration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ServiceType), StringComparison.Ordinal))
|
||||
{
|
||||
LoadPasteAIProviderConfiguration();
|
||||
SaveAndNotifySettings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.PropertyName is not null && PasteAITrackedProperties.Contains(e.PropertyName))
|
||||
{
|
||||
PersistPasteAIProviderConfiguration();
|
||||
}
|
||||
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
|
||||
private void SeedProviderConfigurationSnapshots()
|
||||
{
|
||||
var advancedConfig = _advancedPasteSettings?.Properties?.AdvancedAIConfiguration;
|
||||
if (advancedConfig is not null && !advancedConfig.HasProviderConfiguration(advancedConfig.ServiceType))
|
||||
{
|
||||
advancedConfig.SetProviderConfiguration(
|
||||
advancedConfig.ServiceType,
|
||||
new AIProviderConfigurationSnapshot
|
||||
{
|
||||
ModelName = advancedConfig.ModelName,
|
||||
EndpointUrl = advancedConfig.EndpointUrl,
|
||||
ApiVersion = advancedConfig.ApiVersion,
|
||||
DeploymentName = advancedConfig.DeploymentName,
|
||||
ModelPath = advancedConfig.ModelPath,
|
||||
SystemPrompt = advancedConfig.SystemPrompt,
|
||||
ModerationEnabled = advancedConfig.ModerationEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
var pasteConfig = _advancedPasteSettings?.Properties?.PasteAIConfiguration;
|
||||
if (pasteConfig is not null && !pasteConfig.HasProviderConfiguration(pasteConfig.ServiceType))
|
||||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadAdvancedAIProviderConfiguration()
|
||||
{
|
||||
var config = _advancedPasteSettings?.Properties?.AdvancedAIConfiguration;
|
||||
if (config is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshot = config.GetOrCreateProviderConfiguration(config.ServiceType);
|
||||
_isLoadingAdvancedAIProviderConfiguration = 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 = GetAdvancedAIEndpoint(config.ServiceType);
|
||||
config.EndpointUrl = storedEndpoint;
|
||||
snapshot.EndpointUrl = storedEndpoint;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingAdvancedAIProviderConfiguration = false;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var config = _advancedPasteSettings?.Properties?.AdvancedAIConfiguration;
|
||||
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 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))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
PasswordVault vault = new();
|
||||
PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
|
||||
existingCred?.RetrievePassword();
|
||||
return existingCred?.Password?.Trim() ?? string.Empty;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCustomActionsCanMoveUpDown()
|
||||
{
|
||||
for (int index = 0; index < _customActions.Count; index++)
|
||||
|
||||