Files
PowerToys/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml.cs
Shawn Yuan (from Dev Box) d2cf2be2cb add ui
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-11 16:37:34 +08:00

1314 lines
49 KiB
C#

// 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.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
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;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
namespace Microsoft.PowerToys.Settings.UI.Views
{
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 const string PasteAiDialogDefaultTitle = "Paste with AI provider configuration";
private const string AdvancedAISystemPrompt = "You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task. Call function when necessary to help user finish the transformation 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. Do not output anything else besides the reformatted clipboard content.";
private const string SimpleAISystemPrompt = "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 ObservableCollection<string> _customModelOptions = new();
private AdvancedPasteViewModel ViewModel { get; set; }
public ICommand EnableAdvancedPasteAICommand => new RelayCommand(EnableAdvancedPasteAI);
public ObservableCollection<string> CustomModelOptions => _customModelOptions;
public AdvancedPastePage()
{
var settingsUtils = new SettingsUtils();
ViewModel = new AdvancedPasteViewModel(
settingsUtils,
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
SettingsRepository<AdvancedPasteSettings>.GetInstance(settingsUtils),
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
ViewModel.PropertyChanged += AdvancedPasteViewModel_PropertyChanged;
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();
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 EnableAdvancedPasteAI() => ViewModel.EnableAI();
private void AdvancedPaste_EnableAIToggle_Toggled(object sender, RoutedEventArgs e)
{
if (ViewModel is null)
{
return;
}
var toggle = (ToggleSwitch)sender;
if (toggle.IsOn)
{
ViewModel.EnableAI();
}
else
{
ViewModel.DisableAI();
}
}
public async void DeleteCustomActionButton_Click(object sender, RoutedEventArgs e)
{
var customAction = GetBoundCustomAction(sender, e);
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
ContentDialog dialog = new()
{
XamlRoot = RootPage.XamlRoot,
Title = customAction.Name,
PrimaryButtonText = resourceLoader.GetString("Yes"),
CloseButtonText = resourceLoader.GetString("No"),
DefaultButton = ContentDialogButton.Primary,
Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Dialog_Description") },
};
dialog.PrimaryButtonClick += (_, _) => ViewModel.DeleteCustomAction(customAction);
await dialog.ShowAsync();
}
private async void AddCustomActionButton_Click(object sender, RoutedEventArgs e)
{
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
CustomActionDialog.Title = resourceLoader.GetString("AddCustomAction");
CustomActionDialog.DataContext = ViewModel.GetNewCustomAction(resourceLoader.GetString("AdvancedPasteUI_NewCustomActionPrefix"));
CustomActionDialog.PrimaryButtonText = resourceLoader.GetString("CustomActionSave");
RefreshCustomActionModelOptions(CustomActionDialog.DataContext as AdvancedPasteCustomAction);
await CustomActionDialog.ShowAsync();
}
private async void EditCustomActionButton_Click(object sender, RoutedEventArgs e)
{
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
CustomActionDialog.Title = resourceLoader.GetString("EditCustomAction");
CustomActionDialog.DataContext = GetBoundCustomAction(sender, e).Clone();
CustomActionDialog.PrimaryButtonText = resourceLoader.GetString("CustomActionUpdate");
RefreshCustomActionModelOptions(CustomActionDialog.DataContext as AdvancedPasteCustomAction);
await CustomActionDialog.ShowAsync();
}
private void ReorderButtonDown_Click(object sender, RoutedEventArgs e)
{
var index = ViewModel.CustomActions.IndexOf(GetBoundCustomAction(sender, e));
ViewModel.CustomActions.Move(index, index + 1);
}
private void ReorderButtonUp_Click(object sender, RoutedEventArgs e)
{
var index = ViewModel.CustomActions.IndexOf(GetBoundCustomAction(sender, e));
ViewModel.CustomActions.Move(index, index - 1);
}
private void CustomActionDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
if (args.Result != ContentDialogResult.Primary)
{
return;
}
var dialogCustomAction = GetBoundCustomAction(sender, args);
var existingCustomAction = ViewModel.CustomActions.FirstOrDefault(candidate => candidate.Id == dialogCustomAction.Id);
if (existingCustomAction == null)
{
ViewModel.AddCustomAction(dialogCustomAction);
}
else
{
existingCustomAction.Update(dialogCustomAction);
}
}
private AdvancedPasteCustomAction GetBoundCustomAction(object sender, object eventArgs = null)
{
if (TryResolveCustomAction(sender, out var action))
{
return action;
}
if (eventArgs is RoutedEventArgs routedEventArgs && TryResolveCustomAction(routedEventArgs.OriginalSource, out action))
{
return action;
}
if (CustomActionDialog?.DataContext is AdvancedPasteCustomAction dialogAction)
{
return dialogAction;
}
throw new InvalidOperationException("Unable to determine Advanced Paste custom action from sender.");
}
private static bool TryResolveCustomAction(object source, out AdvancedPasteCustomAction action)
{
action = ResolveCustomAction(source);
return action is not null;
}
private static AdvancedPasteCustomAction ResolveCustomAction(object source)
{
if (source is null)
{
return null;
}
if (source is AdvancedPasteCustomAction directAction)
{
return directAction;
}
if (source is MenuFlyoutItemBase menuItem && menuItem.Tag is AdvancedPasteCustomAction taggedAction)
{
return taggedAction;
}
if (source is FrameworkElement element)
{
return ResolveFromElement(element);
}
return null;
}
private static AdvancedPasteCustomAction ResolveFromElement(FrameworkElement element)
{
for (FrameworkElement current = element; current is not null; current = VisualTreeHelper.GetParent(current) as FrameworkElement)
{
if (current.Tag is AdvancedPasteCustomAction tagged)
{
return tagged;
}
if (current.DataContext is AdvancedPasteCustomAction contextual)
{
return contextual;
}
}
return null;
}
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))
{
if (ViewModel?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIProviderDraft.ModelPath = selectedFile;
}
}
}
private void BrowseCustomActionModel_Click(object sender, RoutedEventArgs e)
{
string selectedFile = PickFileDialog(
"Model Files\0*.onnx;*.zip;*.model\0All Files\0*.*\0",
"Select Model File",
ViewModel?.CustomModelStoragePath);
if (string.IsNullOrEmpty(selectedFile))
{
return;
}
if (CustomActionDialog?.DataContext is AdvancedPasteCustomAction action)
{
action.CustomModelPath = NormalizeCustomModelSelectionPath(selectedFile);
RefreshCustomActionModelOptions(action);
}
}
private void BrowseCustomModelStoragePath_Click(object sender, RoutedEventArgs e)
{
IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.GetSettingsWindow());
string selectedFolder = ShellGetFolder.GetFolderDialogWithFlags(hwnd, ShellGetFolder.FolderDialogFlags._BIF_NEWDIALOGSTYLE);
if (!string.IsNullOrEmpty(selectedFolder) && ViewModel is not null)
{
ViewModel.CustomModelStoragePath = selectedFolder;
RefreshCustomActionModelOptions(CustomActionDialog?.DataContext as AdvancedPasteCustomAction);
}
}
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 string NormalizeCustomModelSelectionPath(string selectedFile)
{
if (string.IsNullOrWhiteSpace(selectedFile))
{
return string.Empty;
}
try
{
var fullSelection = Path.GetFullPath(selectedFile);
var storagePath = ViewModel?.CustomModelStoragePath;
if (!string.IsNullOrWhiteSpace(storagePath))
{
var fullStorage = Path.GetFullPath(storagePath);
if (fullSelection.StartsWith(fullStorage, StringComparison.OrdinalIgnoreCase))
{
var relative = Path.GetRelativePath(fullStorage, fullSelection);
if (!string.IsNullOrEmpty(relative) && !relative.StartsWith("..", StringComparison.Ordinal))
{
return relative;
}
}
}
return fullSelection;
}
catch (Exception)
{
return selectedFile;
}
}
private void RefreshCustomActionModelOptions(AdvancedPasteCustomAction action)
{
var options = new List<string>();
var storageRoot = GetCustomModelStorageRoot();
if (!string.IsNullOrWhiteSpace(storageRoot) && Directory.Exists(storageRoot))
{
try
{
foreach (var file in Directory.EnumerateFiles(storageRoot, "*.onnx", SearchOption.AllDirectories))
{
var option = NormalizeCustomModelSelectionPath(file);
if (!string.IsNullOrWhiteSpace(option))
{
options.Add(option);
}
}
}
catch (Exception)
{
// Enumeration failures are non-fatal; leave the list empty and allow manual selection.
}
}
if (action?.UseCustomModel == true && !string.IsNullOrWhiteSpace(action.CustomModelPath))
{
options.Add(action.CustomModelPath);
}
var uniqueOptions = options
.Where(option => !string.IsNullOrWhiteSpace(option))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(option => option, StringComparer.OrdinalIgnoreCase)
.ToList();
_customModelOptions.Clear();
foreach (var option in uniqueOptions)
{
_customModelOptions.Add(option);
}
if (action?.UseCustomModel == true && string.IsNullOrWhiteSpace(action.CustomModelPath) && _customModelOptions.Count > 0)
{
action.CustomModelPath = _customModelOptions[0];
}
}
private string GetCustomModelStorageRoot()
{
var path = ViewModel?.CustomModelStoragePath;
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
try
{
path = Environment.ExpandEnvironmentVariables(path.Trim());
return Path.GetFullPath(path);
}
catch (Exception)
{
return path;
}
}
private void AdvancedPasteViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (string.Equals(e.PropertyName, nameof(AdvancedPasteViewModel.CustomModelStoragePath), StringComparison.Ordinal))
{
RefreshCustomActionModelOptions(CustomActionDialog?.DataContext as AdvancedPasteCustomAction);
}
}
private void CustomActionUseCustomModel_Toggled(object sender, RoutedEventArgs e)
{
if (CustomActionDialog?.DataContext is AdvancedPasteCustomAction action && action.UseCustomModel)
{
RefreshCustomActionModelOptions(action);
}
}
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 void UpdatePasteAIUIVisibility()
{
var draft = ViewModel?.PasteAIProviderDraft;
if (draft is null)
{
return;
}
string selectedType = draft.ServiceType ?? string.Empty;
AIServiceType serviceKind = draft.ServiceTypeKind;
bool requiresEndpoint = serviceKind is AIServiceType.AzureOpenAI
or AIServiceType.AzureAIInference
or AIServiceType.Mistral
or AIServiceType.HuggingFace
or AIServiceType.Ollama;
bool requiresDeployment = serviceKind == AIServiceType.AzureOpenAI;
bool requiresApiVersion = serviceKind == AIServiceType.AzureOpenAI;
bool requiresModelPath = serviceKind == AIServiceType.Onnx;
bool isFoundryLocal = serviceKind == AIServiceType.FoundryLocal;
bool requiresApiKey = RequiresApiKeyForService(selectedType);
bool showModerationToggle = serviceKind == AIServiceType.OpenAI;
bool showAdvancedAI = serviceKind == AIServiceType.OpenAI || serviceKind == AIServiceType.AzureOpenAI;
if (string.IsNullOrWhiteSpace(draft.EndpointUrl))
{
string storedEndpoint = ViewModel.GetPasteAIEndpoint(draft.Id, selectedType);
if (!string.IsNullOrWhiteSpace(storedEndpoint))
{
draft.EndpointUrl = storedEndpoint;
}
}
PasteAIEndpointUrlTextBox.Visibility = requiresEndpoint ? Visibility.Visible : Visibility.Collapsed;
if (requiresEndpoint)
{
PasteAIEndpointUrlTextBox.PlaceholderText = GetEndpointPlaceholder(serviceKind);
}
PasteAIDeploymentNameTextBox.Visibility = requiresDeployment ? Visibility.Visible : Visibility.Collapsed;
PasteAIApiVersionTextBox.Visibility = requiresApiVersion ? Visibility.Visible : Visibility.Collapsed;
PasteAIModelPanel.Visibility = requiresModelPath ? Visibility.Visible : Visibility.Collapsed;
PasteAIModerationToggle.Visibility = showModerationToggle ? Visibility.Visible : Visibility.Collapsed;
PasteAIEnableAdvancedAICheckBox.Visibility = showAdvancedAI ? Visibility.Visible : Visibility.Collapsed;
PasteAIApiKeyPasswordBox.Visibility = requiresApiKey ? Visibility.Visible : Visibility.Collapsed;
PasteAIModelNameTextBox.Visibility = isFoundryLocal ? Visibility.Collapsed : Visibility.Visible;
if (requiresApiKey)
{
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(draft.Id, selectedType);
}
else
{
PasteAIApiKeyPasswordBox.Password = string.Empty;
}
// Update system prompt placeholder based on EnableAdvancedAI state
UpdateSystemPromptPlaceholder();
// Disable Save button if GPO blocks this provider
if (PasteAIProviderConfigurationDialog is not null)
{
bool isAllowedByGPO = ViewModel?.IsServiceTypeAllowedByGPO(serviceKind) ?? true;
if (!isAllowedByGPO)
{
// GPO blocks this provider, disable save button
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
}
else if (isFoundryLocal)
{
// For Foundry Local, UpdateFoundrySaveButtonState will handle button state
// based on model selection status
}
else
{
// GPO allows this provider, enable save button
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
}
}
}
private Task UpdateFoundryLocalUIAsync(bool refreshFoundry = false)
{
string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
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;
}
if (PasteAIProviderConfigurationDialog is not null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
}
return Task.CompletedTask;
}
if (PasteAIProviderConfigurationDialog is not null)
{
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>();
UpdateFoundryCollections(cachedModels, []);
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?.PasteAIProviderDraft?.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?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIProviderDraft.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?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIProviderDraft.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(ViewModel?.PasteAIProviderDraft?.ServiceType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
if (!isFoundrySelected || ViewModel?.PasteAIProviderDraft is null)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = true;
return;
}
// Check GPO first
bool isAllowedByGPO = ViewModel?.IsServiceTypeAllowedByGPO(AIServiceType.FoundryLocal) ?? true;
if (!isAllowedByGPO)
{
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
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?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIProviderDraft.ModelName = NormalizeFoundryModelReference(selectedModel.Url ?? selectedModel.Name);
}
if (FoundryLocalPicker is not null)
{
FoundryLocalPicker.StatusText = $"{selectedModel.Name} selected.";
}
}
else
{
if (ViewModel?.PasteAIProviderDraft is not null)
{
ViewModel.PasteAIProviderDraft.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 void PasteAIProviderConfigurationDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
var draft = ViewModel?.PasteAIProviderDraft;
if (draft is null)
{
args.Cancel = true;
return;
}
string serviceType = draft.ServiceType ?? "OpenAI";
string apiKey = PasteAIApiKeyPasswordBox.Password;
string trimmedApiKey = apiKey?.Trim() ?? string.Empty;
string endpoint = (draft.EndpointUrl ?? string.Empty).Trim();
if (endpoint == string.Empty)
{
endpoint = GetEndpointPlaceholder(draft.ServiceTypeKind);
}
if (RequiresApiKeyForService(serviceType) && string.IsNullOrWhiteSpace(trimmedApiKey))
{
args.Cancel = true;
return;
}
ViewModel.CommitPasteAIProviderDraft(trimmedApiKey, endpoint);
PasteAIApiKeyPasswordBox.Password = string.Empty;
// Show success message
ShowApiKeySavedMessage("Paste AI");
}
private void PasteAIEnableAdvancedAICheckBox_Toggled(object sender, RoutedEventArgs e)
{
var draft = ViewModel?.PasteAIProviderDraft;
if (draft is null)
{
return;
}
bool isEmptyOrDefault = string.IsNullOrWhiteSpace(draft.SystemPrompt) ||
draft.SystemPrompt.Trim() == AdvancedAISystemPrompt.Trim() ||
draft.SystemPrompt.Trim() == SimpleAISystemPrompt.Trim();
if (isEmptyOrDefault)
{
if (!draft.EnableAdvancedAI)
{
// Now we'll switch
draft.SystemPrompt = AdvancedAISystemPrompt;
}
else
{
draft.SystemPrompt = SimpleAISystemPrompt;
}
}
}
private static bool RequiresApiKeyForService(string serviceType)
{
var serviceKind = serviceType.ToAIServiceType();
return serviceKind switch
{
AIServiceType.Onnx => false,
AIServiceType.Ollama => false,
AIServiceType.FoundryLocal => false,
AIServiceType.ML => false,
_ => true,
};
}
private static string GetEndpointPlaceholder(AIServiceType serviceKind)
{
return serviceKind switch
{
AIServiceType.AzureOpenAI => "https://your-resource.openai.azure.com/",
AIServiceType.AzureAIInference => "https://{resource-name}.cognitiveservices.azure.com/",
AIServiceType.Mistral => "https://api.mistral.ai/v1/",
AIServiceType.HuggingFace => "https://api-inference.huggingface.co/models/",
AIServiceType.Ollama => "http://localhost:11434/",
_ => "https://your-resource.openai.azure.com/",
};
}
private bool HasServiceLegalInfo(string serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
return metadata.HasLegalInfo;
}
private string GetServiceLegalDescription(string serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
if (string.IsNullOrWhiteSpace(metadata.LegalDescription))
{
return string.Empty;
}
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
return resourceLoader.GetString(metadata.LegalDescription);
}
private string GetServiceTermsLabel(string serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
if (string.IsNullOrWhiteSpace(metadata.TermsLabel))
{
return string.Empty;
}
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
return resourceLoader.GetString(metadata.TermsLabel);
}
private Uri GetServiceTermsUri(string serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
return metadata.TermsUri;
}
private string GetServicePrivacyLabel(string serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
if (string.IsNullOrWhiteSpace(metadata.PrivacyLabel))
{
return string.Empty;
}
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
return resourceLoader.GetString(metadata.PrivacyLabel);
}
private Uri GetServicePrivacyUri(string serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
return metadata.PrivacyUri;
}
private bool HasServiceTermsLink(string serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
return metadata.HasTermsLink;
}
private bool HasServicePrivacyLink(string serviceType)
{
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
return metadata.HasPrivacyLink;
}
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 void UpdateSystemPromptPlaceholder()
{
var draft = ViewModel?.PasteAIProviderDraft;
if (draft is null || PasteAISystemPromptTextBox is null)
{
return;
}
PasteAISystemPromptTextBox.PlaceholderText = draft.EnableAdvancedAI
? AdvancedAISystemPrompt
: SimpleAISystemPrompt;
}
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;
}
if (ViewModel is not null)
{
ViewModel.PropertyChanged -= AdvancedPasteViewModel_PropertyChanged;
ViewModel.Dispose();
}
_disposed = true;
GC.SuppressFinalize(this);
}
private void AddProviderMenuFlyout_Opening(object sender, object e)
{
if (sender is not MenuFlyout menuFlyout)
{
return;
}
// Clear existing items
menuFlyout.Items.Clear();
// Add online models header
var onlineHeader = new MenuFlyoutItem
{
Text = "Online models",
FontSize = 12,
IsEnabled = false,
IsHitTestVisible = false,
};
menuFlyout.Items.Add(onlineHeader);
// Add all online providers
var onlineProviders = AIServiceTypeRegistry.GetOnlineServiceTypes();
foreach (var metadata in onlineProviders)
{
var menuItem = new MenuFlyoutItem
{
Text = metadata.DisplayName,
Tag = metadata.ServiceType.ToConfigurationString(),
Icon = new ImageIcon { Source = new SvgImageSource(new Uri(metadata.IconPath)) },
};
menuItem.Click += ProviderMenuFlyoutItem_Click;
menuFlyout.Items.Add(menuItem);
}
// Add local models header
var localHeader = new MenuFlyoutItem
{
Text = "Local models",
FontSize = 12,
IsEnabled = false,
IsHitTestVisible = false,
Margin = new Thickness(0, 16, 0, 0),
};
menuFlyout.Items.Add(localHeader);
// Add all local providers
var localProviders = AIServiceTypeRegistry.GetLocalServiceTypes();
foreach (var metadata in localProviders)
{
var menuItem = new MenuFlyoutItem
{
Text = metadata.DisplayName,
Tag = metadata.ServiceType.ToConfigurationString(),
Icon = new ImageIcon { Source = new SvgImageSource(new Uri(metadata.IconPath)) },
};
menuItem.Click += ProviderMenuFlyoutItem_Click;
menuFlyout.Items.Add(menuItem);
}
}
private async void ProviderMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not string tag || string.IsNullOrWhiteSpace(tag))
{
return;
}
if (ViewModel is null || PasteAIProviderConfigurationDialog is null)
{
return;
}
string serviceType = tag.Trim();
string displayName = string.IsNullOrWhiteSpace(menuItem.Text) ? serviceType : menuItem.Text.Trim();
ViewModel.BeginAddPasteAIProvider(serviceType);
if (ViewModel.PasteAIProviderDraft is null)
{
return;
}
PasteAIProviderConfigurationDialog.Title = PasteAiDialogDefaultTitle;
if (!string.IsNullOrWhiteSpace(displayName))
{
PasteAIProviderConfigurationDialog.Title = $"{displayName} provider configuration";
}
await UpdateFoundryLocalUIAsync(refreshFoundry: true);
UpdatePasteAIUIVisibility();
RefreshDialogBindings();
PasteAIApiKeyPasswordBox.Password = string.Empty;
await PasteAIProviderConfigurationDialog.ShowAsync();
}
private async void EditPasteAIProviderButton_Click(object sender, RoutedEventArgs e)
{
// sender is MenuFlyoutItem with PasteAIProviderDefinition Tag
if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
{
return;
}
if (ViewModel is null || PasteAIProviderConfigurationDialog is null)
{
return;
}
ViewModel.BeginEditPasteAIProvider(provider);
string titlePrefix = string.IsNullOrWhiteSpace(provider.ModelName) ? provider.ServiceType : provider.ModelName;
PasteAIProviderConfigurationDialog.Title = string.IsNullOrWhiteSpace(titlePrefix)
? PasteAiDialogDefaultTitle
: $"{titlePrefix} provider configuration";
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync(refreshFoundry: false);
RefreshDialogBindings();
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
await PasteAIProviderConfigurationDialog.ShowAsync();
}
private void RemovePasteAIProviderButton_Click(object sender, RoutedEventArgs e)
{
// sender is MenuFlyoutItem with PasteAIProviderDefinition Tag
if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
{
return;
}
ViewModel?.RemovePasteAIProvider(provider);
}
private void PasteAIProviderConfigurationDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
ViewModel?.CancelPasteAIProviderDraft();
PasteAIProviderConfigurationDialog.Title = PasteAiDialogDefaultTitle;
PasteAIApiKeyPasswordBox.Password = string.Empty;
}
}
}