From d2cf2be2cb7b7ede08a5c527f606983405c924f0 Mon Sep 17 00:00:00 2001 From: "Shawn Yuan (from Dev Box)" Date: Tue, 11 Nov 2025 16:37:34 +0800 Subject: [PATCH] add ui Signed-off-by: Shawn Yuan (from Dev Box) --- .../Mocks/IntegrationTestUserSettings.cs | 2 + .../AIServiceBatchIntegrationTests.cs | 2 +- .../AdvancedPaste/Helpers/IUserSettings.cs | 2 + .../AdvancedPaste/Helpers/UserSettings.cs | 41 +++++ .../AdvancedPaste/Models/PasteFormat.cs | 6 +- .../CustomActionTransformService.cs | 77 ++++++++- .../ICustomActionTransformService.cs | 3 +- .../CustomActions/LocalModelPasteProvider.cs | 122 ++++++++++++- .../Services/KernelServiceBase.cs | 10 +- .../Services/PasteFormatExecutor.cs | 2 +- .../ViewModels/OptionsViewModel.cs | 10 +- .../AdvancedPasteCustomAction.cs | 36 +++- .../AdvancedPasteProperties.cs | 32 ++++ .../SettingsXAML/Views/AdvancedPastePage.xaml | 39 +++++ .../Views/AdvancedPastePage.xaml.cs | 162 +++++++++++++++++- .../Settings.UI/Strings/en-us/Resources.resw | 30 ++++ .../ViewModels/AdvancedPasteViewModel.cs | 81 +++++++++ 17 files changed, 633 insertions(+), 24 deletions(-) diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs index f6c2f5098d..7a548f3ef1 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs @@ -55,6 +55,8 @@ internal sealed class IntegrationTestUserSettings : IUserSettings public PasteAIConfiguration PasteAIConfiguration => _configuration; + public string CustomModelStoragePath => AdvancedPasteProperties.GetDefaultCustomModelStoragePath(); + public event EventHandler Changed; public Task SetActiveAIProviderAsync(string providerId) diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs index 17b8139bad..62bfc76243 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs @@ -144,7 +144,7 @@ public sealed class AIServiceBatchIntegrationTests switch (format) { case PasteFormats.CustomTextTransformation: - var transformResult = await services.CustomActionTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress); + var transformResult = await services.CustomActionTransformService.TransformTextAsync(null, batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress); return DataPackageHelpers.CreateFromText(transformResult.Content ?? string.Empty); case PasteFormats.KernelQuery: diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs index e32cf61af4..9721bff1aa 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs @@ -25,6 +25,8 @@ namespace AdvancedPaste.Settings public PasteAIConfiguration PasteAIConfiguration { get; } + public string CustomModelStoragePath { get; } + public event EventHandler Changed; Task SetActiveAIProviderAsync(string providerId); diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs index b6b6c19734..2cad1023f5 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Abstractions; using System.Linq; using System.Threading; @@ -46,6 +47,8 @@ namespace AdvancedPaste.Settings public PasteAIConfiguration PasteAIConfiguration { get; private set; } + public string CustomModelStoragePath { get; private set; } + public UserSettings(IFileSystem fileSystem) { _settingsUtils = new SettingsUtils(fileSystem); @@ -54,6 +57,8 @@ namespace AdvancedPaste.Settings ShowCustomPreview = true; CloseAfterLosingFocus = false; PasteAIConfiguration = new PasteAIConfiguration(); + CustomModelStoragePath = NormalizeDirectoryPath(AdvancedPasteProperties.GetDefaultCustomModelStoragePath()); + EnsureDirectoryExists(CustomModelStoragePath); _additionalActions = []; _customActions = []; _taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); @@ -108,6 +113,8 @@ namespace AdvancedPaste.Settings ShowCustomPreview = properties.ShowCustomPreview; CloseAfterLosingFocus = properties.CloseAfterLosingFocus; PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration(); + CustomModelStoragePath = NormalizeDirectoryPath(properties.CustomModelStoragePath); + EnsureDirectoryExists(CustomModelStoragePath); var sourceAdditionalActions = properties.AdditionalActions; (PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats = @@ -288,5 +295,39 @@ namespace AdvancedPaste.Settings { Dispose(false); } + + private static string NormalizeDirectoryPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return AdvancedPasteProperties.GetDefaultCustomModelStoragePath(); + } + + try + { + path = Environment.ExpandEnvironmentVariables(path.Trim()); + return Path.GetFullPath(path); + } + catch (Exception) + { + return path; + } + } + + private static void EnsureDirectoryExists(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + try + { + Directory.CreateDirectory(path); + } + catch (Exception) + { + } + } } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs index e1df90897e..fd8a98a70e 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using Microsoft.PowerToys.Settings.UI.Library; namespace AdvancedPaste.Models; @@ -32,12 +33,13 @@ public sealed class PasteFormat IsSavedQuery = false, }; - public static PasteFormat CreateCustomAIFormat(PasteFormats format, string name, string prompt, bool isSavedQuery, ClipboardFormat clipboardFormats, bool isAIServiceEnabled) => + public static PasteFormat CreateCustomAIFormat(PasteFormats format, string name, string prompt, bool isSavedQuery, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, AdvancedPasteCustomAction customAction = null) => new(format, clipboardFormats, isAIServiceEnabled) { Name = name, Prompt = prompt, IsSavedQuery = isSavedQuery, + CustomAction = customAction, }; public PasteFormatMetadataAttribute Metadata => MetadataDict[Format]; @@ -52,6 +54,8 @@ public sealed class PasteFormat public bool IsSavedQuery { get; private init; } + public AdvancedPasteCustomAction CustomAction { get; private init; } + public bool IsEnabled { get; private init; } public string AccessibleName => $"{Name} ({ShortcutText})"; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs index 721a96070d..7cc91579f5 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -40,10 +41,12 @@ namespace AdvancedPaste.Services.CustomActions this.userSettings = userSettings; } - public async Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress) + public async Task TransformTextAsync(AdvancedPasteCustomAction customAction, string prompt, string inputText, CancellationToken cancellationToken, IProgress progress) { var pasteConfig = userSettings?.PasteAIConfiguration; - var providerConfig = BuildProviderConfig(pasteConfig); + var providerConfig = customAction?.UseCustomModel == true + ? BuildCustomModelConfig(customAction) + : BuildProviderConfig(pasteConfig); return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress); } @@ -164,6 +167,24 @@ namespace AdvancedPaste.Services.CustomActions return providerConfig; } + private PasteAIConfig BuildCustomModelConfig(AdvancedPasteCustomAction customAction) + { + ArgumentNullException.ThrowIfNull(customAction); + + var modelPath = ResolveCustomModelPath(customAction); + var systemPrompt = DetermineCustomModelSystemPrompt(); + + return new PasteAIConfig + { + ProviderType = AIServiceType.ML, + Model = Path.GetFileNameWithoutExtension(modelPath), + LocalModelPath = modelPath, + ModelPath = modelPath, + SystemPrompt = systemPrompt, + ModerationEnabled = false, + }; + } + private string AcquireApiKey(AIServiceType serviceType) { if (!RequiresApiKey(serviceType)) @@ -196,5 +217,57 @@ namespace AdvancedPaste.Services.CustomActions return providerConfig.ProviderType == AIServiceType.OpenAI || providerConfig.ProviderType == AIServiceType.AzureOpenAI; } + + private string ResolveCustomModelPath(AdvancedPasteCustomAction customAction) + { + var modelPath = customAction.CustomModelPath?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(modelPath)) + { + throw new PasteActionException(GetCustomModelMissingMessage(), new FileNotFoundException()); + } + + var storagePath = userSettings?.CustomModelStoragePath; + var expandedPath = Environment.ExpandEnvironmentVariables(modelPath); + if (!Path.IsPathRooted(expandedPath) && !string.IsNullOrEmpty(storagePath)) + { + expandedPath = Path.Combine(storagePath, expandedPath); + } + + string fullPath; + try + { + fullPath = Path.GetFullPath(expandedPath); + } + catch (Exception ex) + { + throw new PasteActionException(GetCustomModelMissingMessage(), ex); + } + + if (!File.Exists(fullPath)) + { + throw new PasteActionException(GetCustomModelMissingMessage(), new FileNotFoundException(fullPath)); + } + + return fullPath; + } + + private static string GetCustomModelMissingMessage() + { + var message = ResourceLoaderInstance.ResourceLoader.GetString("AdvancedPasteCustomModelNotFound"); + return string.IsNullOrWhiteSpace(message) ? "The selected custom model could not be found." : message; + } + + private string DetermineCustomModelSystemPrompt() + { + var customPrompt = userSettings?.PasteAIConfiguration?.ActiveProvider?.SystemPrompt + ?? userSettings?.PasteAIConfiguration?.Providers?.FirstOrDefault()?.SystemPrompt; + + if (string.IsNullOrWhiteSpace(customPrompt)) + { + return DefaultSystemPrompt; + } + + return customPrompt; + } } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs index 1c3ecb980c..8b0f0c88a8 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs @@ -7,11 +7,12 @@ using System.Threading; using System.Threading.Tasks; using AdvancedPaste.Settings; +using Microsoft.PowerToys.Settings.UI.Library; namespace AdvancedPaste.Services.CustomActions { public interface ICustomActionTransformService { - Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress); + Task TransformTextAsync(AdvancedPasteCustomAction customAction, string prompt, string inputText, CancellationToken cancellationToken, IProgress progress); } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs index f4d45ccd74..741a1ddfdb 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs @@ -3,11 +3,15 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using AdvancedPaste.Helpers; using AdvancedPaste.Models; using Microsoft.PowerToys.Settings.UI.Library; +using Windows.AI.MachineLearning; namespace AdvancedPaste.Services.CustomActions { @@ -23,6 +27,8 @@ namespace AdvancedPaste.Services.CustomActions private readonly PasteAIConfig _config; + private static readonly ConcurrentDictionary>> ModelCache = new(StringComparer.OrdinalIgnoreCase); + public LocalModelPasteProvider(PasteAIConfig config) { _config = config ?? throw new ArgumentNullException(nameof(config)); @@ -30,14 +36,120 @@ namespace AdvancedPaste.Services.CustomActions public Task IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true); - public Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress) + public async Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress 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); + cancellationToken.ThrowIfCancellationRequested(); + + var modelPath = _config.LocalModelPath ?? _config.ModelPath; + if (string.IsNullOrWhiteSpace(modelPath)) + { + throw new PasteActionException(GetInferenceErrorMessage(), new InvalidOperationException("Local model path not provided.")); + } + + try + { + var model = await GetOrLoadModelAsync(modelPath).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + using LearningModelSession session = new(model); + var binding = new LearningModelBinding(session); + + BindInputs(session.Model, binding, request); + + var correlationId = Guid.NewGuid().ToString(); + var evaluation = await session.EvaluateAsync(binding, correlationId).AsTask(cancellationToken).ConfigureAwait(false); + + var output = ExtractStringOutput(session.Model, evaluation); + request.Usage = AIServiceUsage.None; + progress?.Report(1.0); + return output ?? string.Empty; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + throw new PasteActionException(GetInferenceErrorMessage(), ex); + } + } + + private static async Task GetOrLoadModelAsync(string modelPath) + { + var loader = ModelCache.GetOrAdd(modelPath, path => new Lazy>(() => Task.Run(() => LearningModel.LoadFromFilePath(path)))); + return await loader.Value.ConfigureAwait(false); + } + + private static void BindInputs(LearningModel model, LearningModelBinding binding, PasteAIRequest request) + { + var stringInputs = model.InputFeatures + .OfType() + .Where(descriptor => descriptor.TensorKind == TensorKind.String) + .ToList(); + + if (stringInputs.Count == 0) + { + throw new PasteActionException(GetInferenceErrorMessage(), new InvalidOperationException("Model does not expose string inputs.")); + } + + var prompt = request.Prompt ?? string.Empty; + var input = request.InputText ?? string.Empty; + var combined = string.IsNullOrWhiteSpace(prompt) ? input : string.Join(Environment.NewLine + Environment.NewLine, prompt, input); + + foreach (var inputDescriptor in stringInputs) + { + var value = ResolveInputValue(inputDescriptor.Name, prompt, input, combined); + var tensor = TensorString.CreateFromArray(new long[] { 1 }, new[] { value }); + binding.Bind(inputDescriptor.Name, tensor); + } + } + + private static string ResolveInputValue(string featureName, string prompt, string input, string combined) + { + if (string.IsNullOrEmpty(featureName)) + { + return combined; + } + + switch (featureName.ToLowerInvariant()) + { + case "prompt": + case "instruction": + case "instructions": + return prompt; + case "input": + case "text": + case "input_text": + case "inputtext": + return input; + default: + return combined; + } + } + + private static string ExtractStringOutput(LearningModel model, LearningModelEvaluationResult evaluation) + { + foreach (var outputDescriptor in model.OutputFeatures.OfType().Where(descriptor => descriptor.TensorKind == TensorKind.String)) + { + if (evaluation.Outputs.TryGetValue(outputDescriptor.Name, out var value) && value is TensorString tensor) + { + var vector = tensor.GetAsVectorView(); + if (vector.Count > 0) + { + return vector[0]; + } + } + } + + throw new PasteActionException(GetInferenceErrorMessage(), new InvalidOperationException("Model did not return a string output.")); + } + + private static string GetInferenceErrorMessage() + { + var message = ResourceLoaderInstance.ResourceLoader.GetString("AdvancedPasteCustomModelInferenceError"); + return string.IsNullOrWhiteSpace(message) ? "The custom model failed to generate a response." : message; } } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs index 0ea9ef40bc..1697d0e5a4 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs @@ -237,7 +237,7 @@ public abstract class KernelServiceBase( ? $"Runs the \"{customAction.Name}\" custom action." : customAction.Description; return KernelFunctionFactory.CreateFromMethod( - method: async (Kernel kernel) => await ExecuteCustomActionAsync(kernel, customAction.Prompt), + method: async (Kernel kernel) => await ExecuteCustomActionAsync(kernel, customAction), functionName: functionName, description: description, parameters: null, @@ -286,14 +286,14 @@ public abstract class KernelServiceBase( return string.IsNullOrEmpty(sanitized) ? "_CustomAction" : sanitized; } - private Task ExecuteCustomActionAsync(Kernel kernel, string fixedPrompt) => + private Task ExecuteCustomActionAsync(Kernel kernel, AdvancedPasteCustomAction customAction) => ExecuteTransformAsync( kernel, - new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }), + new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, customAction?.Prompt ?? string.Empty } }), async dataPackageView => { var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken()); - var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress()); + var result = await _customActionTransformService.TransformTextAsync(customAction, customAction?.Prompt ?? string.Empty, input, kernel.GetCancellationToken(), kernel.GetProgress()); return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty); }); @@ -311,7 +311,7 @@ public abstract class KernelServiceBase( private async Task GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress progress) => format switch { - PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty, + PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(null, prompt, input, cancellationToken, progress))?.Content ?? string.Empty, _ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)), }; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs index aef9e39bb9..55dfd1f7ae 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs @@ -37,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct pasteFormat.Format switch { PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress), - PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty), + PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.CustomAction, pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty), _ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress), }); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index 0cda4fc6e5..b35884ccdb 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -334,8 +334,8 @@ namespace AdvancedPaste.ViewModels private PasteFormat CreateStandardPasteFormat(PasteFormats format) => PasteFormat.CreateStandardFormat(format, AvailableClipboardFormats, IsCustomAIServiceEnabled, ResourceLoaderInstance.ResourceLoader.GetString); - private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery) => - PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled); + private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery, AdvancedPasteCustomAction customAction = null) => + PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled, customAction); private void UpdateAIProviderActiveFlags() { @@ -407,7 +407,7 @@ namespace AdvancedPaste.ViewModels UpdateFormats( CustomActionPasteFormats, - IsCustomAIServiceEnabled ? _userSettings.CustomActions.Select(customAction => CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true)) : []); + IsCustomAIServiceEnabled ? _userSettings.CustomActions.Select(customAction => CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true, customAction)) : []); } public void Dispose() @@ -732,7 +732,7 @@ namespace AdvancedPaste.ViewModels if (customAction != null) { await ReadClipboardAsync(); - await ExecutePasteFormatAsync(CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true), source); + await ExecutePasteFormatAsync(CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true, customAction), source); } } @@ -741,7 +741,7 @@ namespace AdvancedPaste.ViewModels var customAction = _userSettings.CustomActions .FirstOrDefault(customAction => Models.KernelQueryCache.CacheKey.PromptComparer.Equals(customAction.Prompt, Query)); - await ExecutePasteFormatAsync(CreateCustomAIPasteFormat(customAction?.Name ?? "Default", Query, isSavedQuery: customAction != null), triggerSource); + await ExecutePasteFormatAsync(CreateCustomAIPasteFormat(customAction?.Name ?? "Default", Query, isSavedQuery: customAction != null, customAction), triggerSource); } private void HideWindow() diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs index c981295906..49000fe6a3 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs @@ -23,6 +23,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction private bool _isValid; private bool _hasConflict; private string _tooltip; + private bool _useCustomModel; + private string _customModelPath = string.Empty; [JsonPropertyName("id")] public int Id @@ -122,6 +124,32 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction set => Set(ref _tooltip, value); } + [JsonPropertyName("useCustomModel")] + public bool UseCustomModel + { + get => _useCustomModel; + set + { + if (Set(ref _useCustomModel, value)) + { + UpdateIsValid(); + } + } + } + + [JsonPropertyName("customModelPath")] + public string CustomModelPath + { + get => _customModelPath; + set + { + if (Set(ref _customModelPath, value ?? string.Empty)) + { + UpdateIsValid(); + } + } + } + [JsonIgnore] public IEnumerable SubActions => []; @@ -144,6 +172,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction CanMoveDown = other.CanMoveDown; HasConflict = other.HasConflict; Tooltip = other.Tooltip; + UseCustomModel = other.UseCustomModel; + CustomModelPath = other.CustomModelPath; } private HotkeySettings GetShortcutClone() @@ -159,6 +189,10 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction private void UpdateIsValid() { - IsValid = !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Prompt); + bool hasPrompt = !string.IsNullOrWhiteSpace(Prompt); + bool hasName = !string.IsNullOrWhiteSpace(Name); + bool hasModel = !UseCustomModel || !string.IsNullOrWhiteSpace(CustomModelPath); + + IsValid = hasName && hasPrompt && hasModel; } } diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs index 200e5e459d..429beca295 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs @@ -2,7 +2,9 @@ // 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.IO; using System.Text.Json; using System.Text.Json.Serialization; @@ -28,6 +30,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library ShowCustomPreview = true; CloseAfterLosingFocus = false; PasteAIConfiguration = new(); + CustomModelStoragePath = GetDefaultCustomModelStoragePath(); } [JsonConverter(typeof(BoolPropertyJsonConverter))] @@ -87,7 +90,36 @@ namespace Microsoft.PowerToys.Settings.UI.Library [CmdConfigureIgnoreAttribute] public PasteAIConfiguration PasteAIConfiguration { get; set; } + [JsonPropertyName("custom-model-storage-path")] + [CmdConfigureIgnoreAttribute] + public string CustomModelStoragePath { get; set; } + public override string ToString() => JsonSerializer.Serialize(this); + + public static string GetDefaultCustomModelStoragePath() + { + try + { + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrWhiteSpace(userProfile)) + { + userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); + } + + if (string.IsNullOrWhiteSpace(userProfile)) + { + var temp = Path.GetTempPath(); + return Path.Combine(temp, "PowerToys", "Models"); + } + + return Path.Combine(userProfile, ".cache", "PowerToys"); + } + catch (Exception) + { + var temp = Path.GetTempPath(); + return Path.Combine(temp, "PowerToys", "Models"); + } + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml index 8d6b0afa68..0424715d8b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml @@ -381,6 +381,20 @@ + + + +