2024-05-09 10:32:03 -04:00
// 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 ;
2025-11-05 16:13:55 +08:00
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.ComponentModel ;
2024-08-22 16:17:12 +02:00
using System.Linq ;
2025-11-05 16:13:55 +08:00
using System.Runtime.InteropServices ;
using System.Threading ;
2024-05-09 10:32:03 -04:00
using System.Threading.Tasks ;
using System.Windows.Input ;
2024-10-17 05:14:57 -04:00
2025-11-05 16:13:55 +08:00
using LanguageModelProvider ;
using Microsoft.PowerToys.Settings.UI.Controls ;
2024-05-09 10:32:03 -04:00
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 ;
2025-11-05 16:13:55 +08:00
using Microsoft.UI.Xaml.Media ;
using Microsoft.UI.Xaml.Media.Imaging ;
2024-05-09 10:32:03 -04:00
namespace Microsoft.PowerToys.Settings.UI.Views
{
2025-11-05 16:13:55 +08:00
public sealed partial class AdvancedPastePage : NavigablePage , IRefreshablePage , IDisposable
2024-05-09 10:32:03 -04:00
{
2025-11-05 16:13:55 +08:00
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." ;
Fix AP system prompt textbox display issue (#43486)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request refactors how the system prompt is managed and
displayed in the Advanced Paste settings UI. The main improvements
center around normalizing the system prompt value, ensuring placeholder
prompts are handled consistently, and updating the logic for switching
between advanced and simple AI modes. These changes help prevent
placeholder text from being mistakenly saved as a custom system prompt
and improve maintainability.
**System prompt normalization and placeholder handling:**
* Added static normalization for both `AdvancedAISystemPrompt` and
`SimpleAISystemPrompt`, and introduced logic to clear the system prompt
if it matches a placeholder value. This prevents default placeholder
text from being persisted as a custom prompt.
[[1]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R39-R40)
[[2]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R942-R982)
* Refactored event handlers to use the new normalization logic and
updated the placeholder switching mechanism when toggling between
advanced and simple AI modes.
* Ensured system prompt normalization is applied when saving provider
configuration, further enforcing consistent prompt handling.
**UI improvements:**
* Removed hard-coded placeholder text from the XAML, ensuring that the
placeholder is now dynamically set based on the current mode and
normalized prompt value.
* Updated the logic for setting the system prompt textbox placeholder to
use the new normalization and mode detection methods, improving clarity
and reducing potential user confusion.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-13 10:19:43 +08:00
private static readonly string AdvancedAISystemPromptNormalized = AdvancedAISystemPrompt . Trim ( ) ;
private static readonly string SimpleAISystemPromptNormalized = SimpleAISystemPrompt . Trim ( ) ;
2025-11-05 16:13:55 +08:00
2024-05-09 10:32:03 -04:00
private AdvancedPasteViewModel ViewModel { get ; set ; }
2025-11-05 16:13:55 +08:00
public ICommand EnableAdvancedPasteAICommand = > new RelayCommand ( EnableAdvancedPasteAI ) ;
2024-05-09 10:32:03 -04:00
public AdvancedPastePage ( )
{
var settingsUtils = new SettingsUtils ( ) ;
ViewModel = new AdvancedPasteViewModel (
settingsUtils ,
SettingsRepository < GeneralSettings > . GetInstance ( settingsUtils ) ,
SettingsRepository < AdvancedPasteSettings > . GetInstance ( settingsUtils ) ,
ShellPage . SendDefaultIPCMessage ) ;
DataContext = ViewModel ;
InitializeComponent ( ) ;
2025-08-20 09:31:52 +08:00
2025-11-05 16:13:55 +08:00
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 ;
}
} ;
2024-05-09 10:32:03 -04:00
}
public void RefreshEnabledState ( )
{
ViewModel . RefreshEnabledState ( ) ;
2025-11-05 16:13:55 +08:00
UpdatePasteAIUIVisibility ( ) ;
_ = UpdateFoundryLocalUIAsync ( refreshFoundry : true ) ;
2024-05-09 10:32:03 -04:00
}
2025-11-05 16:13:55 +08:00
private void EnableAdvancedPasteAI ( ) = > ViewModel . EnableAI ( ) ;
private void AdvancedPaste_EnableAIToggle_Toggled ( object sender , RoutedEventArgs e )
2024-05-09 10:32:03 -04:00
{
2025-11-05 16:13:55 +08:00
if ( ViewModel is null )
2024-05-09 10:32:03 -04:00
{
2025-11-05 16:13:55 +08:00
return ;
2024-05-09 10:32:03 -04:00
}
2025-11-05 16:13:55 +08:00
var toggle = ( ToggleSwitch ) sender ;
2024-05-09 10:32:03 -04:00
2025-11-05 16:13:55 +08:00
if ( toggle . IsOn )
{
ViewModel . EnableAI ( ) ;
}
else
{
ViewModel . DisableAI ( ) ;
}
2024-08-22 16:17:12 +02:00
}
public async void DeleteCustomActionButton_Click ( object sender , RoutedEventArgs e )
{
2025-11-05 16:13:55 +08:00
var customAction = GetBoundCustomAction ( sender , e ) ;
2024-08-22 16:17:12 +02:00
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" ) ;
await CustomActionDialog . ShowAsync ( ) ;
}
private async void EditCustomActionButton_Click ( object sender , RoutedEventArgs e )
{
var resourceLoader = ResourceLoaderInstance . ResourceLoader ;
CustomActionDialog . Title = resourceLoader . GetString ( "EditCustomAction" ) ;
2025-11-05 16:13:55 +08:00
CustomActionDialog . DataContext = GetBoundCustomAction ( sender , e ) . Clone ( ) ;
2024-08-22 16:17:12 +02:00
CustomActionDialog . PrimaryButtonText = resourceLoader . GetString ( "CustomActionUpdate" ) ;
await CustomActionDialog . ShowAsync ( ) ;
}
private void ReorderButtonDown_Click ( object sender , RoutedEventArgs e )
{
2025-11-05 16:13:55 +08:00
var index = ViewModel . CustomActions . IndexOf ( GetBoundCustomAction ( sender , e ) ) ;
2024-08-22 16:17:12 +02:00
ViewModel . CustomActions . Move ( index , index + 1 ) ;
}
private void ReorderButtonUp_Click ( object sender , RoutedEventArgs e )
{
2025-11-05 16:13:55 +08:00
var index = ViewModel . CustomActions . IndexOf ( GetBoundCustomAction ( sender , e ) ) ;
2024-08-22 16:17:12 +02:00
ViewModel . CustomActions . Move ( index , index - 1 ) ;
}
private void CustomActionDialog_Closed ( ContentDialog sender , ContentDialogClosedEventArgs args )
{
if ( args . Result ! = ContentDialogResult . Primary )
2024-05-09 10:32:03 -04:00
{
2024-08-22 16:17:12 +02:00
return ;
}
2025-11-05 16:13:55 +08:00
var dialogCustomAction = GetBoundCustomAction ( sender , args ) ;
2024-08-22 16:17:12 +02:00
var existingCustomAction = ViewModel . CustomActions . FirstOrDefault ( candidate = > candidate . Id = = dialogCustomAction . Id ) ;
if ( existingCustomAction = = null )
{
ViewModel . AddCustomAction ( dialogCustomAction ) ;
2024-05-09 10:32:03 -04:00
}
else
{
2024-08-22 16:17:12 +02:00
existingCustomAction . Update ( dialogCustomAction ) ;
2024-05-09 10:32:03 -04:00
}
}
2024-08-22 16:17:12 +02:00
2025-11-05 16:13:55 +08:00
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 ) )
{
PasteAIModelPathTextBox . Text = selectedFile ;
if ( ViewModel ? . PasteAIProviderDraft is not null )
{
ViewModel . PasteAIProviderDraft . 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 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 . 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 ;
}
Fix AP system prompt textbox display issue (#43486)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request refactors how the system prompt is managed and
displayed in the Advanced Paste settings UI. The main improvements
center around normalizing the system prompt value, ensuring placeholder
prompts are handled consistently, and updating the logic for switching
between advanced and simple AI modes. These changes help prevent
placeholder text from being mistakenly saved as a custom system prompt
and improve maintainability.
**System prompt normalization and placeholder handling:**
* Added static normalization for both `AdvancedAISystemPrompt` and
`SimpleAISystemPrompt`, and introduced logic to clear the system prompt
if it matches a placeholder value. This prevents default placeholder
text from being persisted as a custom prompt.
[[1]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R39-R40)
[[2]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R942-R982)
* Refactored event handlers to use the new normalization logic and
updated the placeholder switching mechanism when toggling between
advanced and simple AI modes.
* Ensured system prompt normalization is applied when saving provider
configuration, further enforcing consistent prompt handling.
**UI improvements:**
* Removed hard-coded placeholder text from the XAML, ensuring that the
placeholder is now dynamically set based on the current mode and
normalized prompt value.
* Updated the logic for setting the system prompt textbox placeholder to
use the new normalization and mode detection methods, improving clarity
and reducing potential user confusion.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-13 10:19:43 +08:00
NormalizeSystemPrompt ( draft ) ;
2025-11-05 16:13:55 +08:00
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 ;
}
Fix AP system prompt textbox display issue (#43486)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request refactors how the system prompt is managed and
displayed in the Advanced Paste settings UI. The main improvements
center around normalizing the system prompt value, ensuring placeholder
prompts are handled consistently, and updating the logic for switching
between advanced and simple AI modes. These changes help prevent
placeholder text from being mistakenly saved as a custom system prompt
and improve maintainability.
**System prompt normalization and placeholder handling:**
* Added static normalization for both `AdvancedAISystemPrompt` and
`SimpleAISystemPrompt`, and introduced logic to clear the system prompt
if it matches a placeholder value. This prevents default placeholder
text from being persisted as a custom prompt.
[[1]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R39-R40)
[[2]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R942-R982)
* Refactored event handlers to use the new normalization logic and
updated the placeholder switching mechanism when toggling between
advanced and simple AI modes.
* Ensured system prompt normalization is applied when saving provider
configuration, further enforcing consistent prompt handling.
**UI improvements:**
* Removed hard-coded placeholder text from the XAML, ensuring that the
placeholder is now dynamically set based on the current mode and
normalized prompt value.
* Updated the logic for setting the system prompt textbox placeholder to
use the new normalization and mode detection methods, improving clarity
and reducing potential user confusion.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-13 10:19:43 +08:00
NormalizeSystemPrompt ( draft ) ;
UpdateSystemPromptPlaceholder ( ) ;
2025-11-05 16:13:55 +08:00
}
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 . 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 ;
Fix AP system prompt textbox display issue (#43486)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request refactors how the system prompt is managed and
displayed in the Advanced Paste settings UI. The main improvements
center around normalizing the system prompt value, ensuring placeholder
prompts are handled consistently, and updating the logic for switching
between advanced and simple AI modes. These changes help prevent
placeholder text from being mistakenly saved as a custom system prompt
and improve maintainability.
**System prompt normalization and placeholder handling:**
* Added static normalization for both `AdvancedAISystemPrompt` and
`SimpleAISystemPrompt`, and introduced logic to clear the system prompt
if it matches a placeholder value. This prevents default placeholder
text from being persisted as a custom prompt.
[[1]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R39-R40)
[[2]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R942-R982)
* Refactored event handlers to use the new normalization logic and
updated the placeholder switching mechanism when toggling between
advanced and simple AI modes.
* Ensured system prompt normalization is applied when saving provider
configuration, further enforcing consistent prompt handling.
**UI improvements:**
* Removed hard-coded placeholder text from the XAML, ensuring that the
placeholder is now dynamically set based on the current mode and
normalized prompt value.
* Updated the logic for setting the system prompt textbox placeholder to
use the new normalization and mode detection methods, improving clarity
and reducing potential user confusion.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-13 10:19:43 +08:00
private static bool IsPlaceholderSystemPrompt ( string prompt )
{
if ( string . IsNullOrWhiteSpace ( prompt ) )
{
return true ;
}
string trimmedPrompt = prompt . Trim ( ) ;
return string . Equals ( trimmedPrompt , AdvancedAISystemPromptNormalized , StringComparison . Ordinal )
| | string . Equals ( trimmedPrompt , SimpleAISystemPromptNormalized , StringComparison . Ordinal ) ;
}
private static void NormalizeSystemPrompt ( PasteAIProviderDefinition draft )
{
if ( draft is null )
{
return ;
}
if ( IsPlaceholderSystemPrompt ( draft . SystemPrompt ) )
{
draft . SystemPrompt = string . Empty ;
}
}
2025-11-05 16:13:55 +08:00
private void UpdateSystemPromptPlaceholder ( )
{
var draft = ViewModel ? . PasteAIProviderDraft ;
Fix AP system prompt textbox display issue (#43486)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request refactors how the system prompt is managed and
displayed in the Advanced Paste settings UI. The main improvements
center around normalizing the system prompt value, ensuring placeholder
prompts are handled consistently, and updating the logic for switching
between advanced and simple AI modes. These changes help prevent
placeholder text from being mistakenly saved as a custom system prompt
and improve maintainability.
**System prompt normalization and placeholder handling:**
* Added static normalization for both `AdvancedAISystemPrompt` and
`SimpleAISystemPrompt`, and introduced logic to clear the system prompt
if it matches a placeholder value. This prevents default placeholder
text from being persisted as a custom prompt.
[[1]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R39-R40)
[[2]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R942-R982)
* Refactored event handlers to use the new normalization logic and
updated the placeholder switching mechanism when toggling between
advanced and simple AI modes.
* Ensured system prompt normalization is applied when saving provider
configuration, further enforcing consistent prompt handling.
**UI improvements:**
* Removed hard-coded placeholder text from the XAML, ensuring that the
placeholder is now dynamically set based on the current mode and
normalized prompt value.
* Updated the logic for setting the system prompt textbox placeholder to
use the new normalization and mode detection methods, improving clarity
and reducing potential user confusion.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-13 10:19:43 +08:00
if ( draft is null )
{
return ;
}
NormalizeSystemPrompt ( draft ) ;
if ( PasteAISystemPromptTextBox is null )
2025-11-05 16:13:55 +08:00
{
return ;
}
Fix AP system prompt textbox display issue (#43486)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request refactors how the system prompt is managed and
displayed in the Advanced Paste settings UI. The main improvements
center around normalizing the system prompt value, ensuring placeholder
prompts are handled consistently, and updating the logic for switching
between advanced and simple AI modes. These changes help prevent
placeholder text from being mistakenly saved as a custom system prompt
and improve maintainability.
**System prompt normalization and placeholder handling:**
* Added static normalization for both `AdvancedAISystemPrompt` and
`SimpleAISystemPrompt`, and introduced logic to clear the system prompt
if it matches a placeholder value. This prevents default placeholder
text from being persisted as a custom prompt.
[[1]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R39-R40)
[[2]](diffhunk://#diff-14126907329c7fcd49dd33bab32283296c7dd68ddc3902163a482a3b3ce58d36R942-R982)
* Refactored event handlers to use the new normalization logic and
updated the placeholder switching mechanism when toggling between
advanced and simple AI modes.
* Ensured system prompt normalization is applied when saving provider
configuration, further enforcing consistent prompt handling.
**UI improvements:**
* Removed hard-coded placeholder text from the XAML, ensuring that the
placeholder is now dynamically set based on the current mode and
normalized prompt value.
* Updated the logic for setting the system prompt textbox placeholder to
use the new normalization and mode detection methods, improving clarity
and reducing potential user confusion.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx
<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2025-11-13 10:19:43 +08:00
bool useAdvancedPlaceholder = PasteAIEnableAdvancedAICheckBox ? . IsOn ? ? draft . EnableAdvancedAI ;
PasteAISystemPromptTextBox . PlaceholderText = useAdvancedPlaceholder
2025-11-05 16:13:55 +08:00
? 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 ;
}
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 ;
}
2024-05-09 10:32:03 -04:00
}
}