diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index efb0216f..23ffdba3 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -662,6 +662,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Auto Completion. + /// + public static string Label_AutoCompletion { + get { + return ResourceManager.GetString("Label_AutoCompletion", resourceCulture); + } + } + /// /// Looks up a localized string similar to Automatically scroll to end of console output. /// @@ -797,6 +806,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Replace underscores with spaces when inserting completions. + /// + public static string Label_CompletionReplaceUnderscoresWithSpaces { + get { + return ResourceManager.GetString("Label_CompletionReplaceUnderscoresWithSpaces", resourceCulture); + } + } + /// /// Looks up a localized string similar to Confirm Delete. /// @@ -1094,6 +1112,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to General. + /// + public static string Label_General { + get { + return ResourceManager.GetString("Label_General", resourceCulture); + } + } + /// /// Looks up a localized string similar to Height. /// @@ -1112,6 +1139,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Image Viewer. + /// + public static string Label_ImageViewer { + get { + return ResourceManager.GetString("Label_ImageViewer", resourceCulture); + } + } + /// /// Looks up a localized string similar to Import with Metadata. /// @@ -1148,6 +1184,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Inference. + /// + public static string Label_Inference { + get { + return ResourceManager.GetString("Label_Inference", resourceCulture); + } + } + /// /// Looks up a localized string similar to Inner exception. /// @@ -1418,6 +1463,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Output Image Files. + /// + public static string Label_OutputImageFiles { + get { + return ResourceManager.GetString("Label_OutputImageFiles", resourceCulture); + } + } + /// /// Looks up a localized string similar to Output Browser. /// @@ -1535,6 +1589,42 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Prompt. + /// + public static string Label_Prompt { + get { + return ResourceManager.GetString("Label_Prompt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prompt Tags. + /// + public static string Label_PromptTags { + get { + return ResourceManager.GetString("Label_PromptTags", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tags file to use for suggesting completions (Supports the a1111-sd-webui-tagcomplete .csv format). + /// + public static string Label_PromptTagsDescription { + get { + return ResourceManager.GetString("Label_PromptTagsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Import Prompt tags. + /// + public static string Label_PromptTagsImport { + get { + return ResourceManager.GetString("Label_PromptTagsImport", resourceCulture); + } + } + /// /// Looks up a localized string similar to Python Packages. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 76da7ad6..be55b0f1 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -813,4 +813,34 @@ Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here + + GeneralA general settings category + + + InferenceThe Inference feature page + + + PromptA settings category for Inference generation prompts + + + Output Image Files + + + Image Viewer + + + Auto Completion + + + Replace underscores with spaces when inserting completions + + + Prompt TagsTags for image generation prompts + + + Import Prompt tags + + + Tags file to use for suggesting completions (Supports the a1111-sd-webui-tagcomplete .csv format) + diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs index 4ce798cf..9e3b79ce 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs @@ -1,7 +1,28 @@ -using FluentAvalonia.UI.Controls; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Controls.Notifications; +using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DynamicData.Binding; +using FluentAvalonia.UI.Controls; +using NLog; +using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Models.Inference; +using StabilityMatrix.Avalonia.Models.TagCompletion; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Settings; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Python; +using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; @@ -9,12 +30,202 @@ namespace StabilityMatrix.Avalonia.ViewModels.Settings; [View(typeof(InferenceSettingsPage))] [Singleton, ManagedService] -public class InferenceSettingsViewModel : PageViewModelBase +public partial class InferenceSettingsViewModel : PageViewModelBase { + private readonly INotificationService notificationService; + private readonly ISettingsManager settingsManager; + private readonly ICompletionProvider completionProvider; + /// public override string Title => "Inference"; /// public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true }; + + [ObservableProperty] + private bool isPromptCompletionEnabled = true; + + [ObservableProperty] + private IReadOnlyList availableTagCompletionCsvs = Array.Empty(); + + [ObservableProperty] + private string? selectedTagCompletionCsv; + + [ObservableProperty] + private bool isCompletionRemoveUnderscoresEnabled = true; + + [ObservableProperty] + [CustomValidation(typeof(InferenceSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))] + private string? outputImageFileNameFormat; + + [ObservableProperty] + private string? outputImageFileNameFormatSample; + + public IEnumerable OutputImageFileNameFormatVars => + FileNameFormatProvider + .GetSample() + .Substitutions.Select( + kv => + new FileNameFormatVar + { + Variable = $"{{{kv.Key}}}", + Example = kv.Value.Invoke() + } + ); + + [ObservableProperty] + private bool isImageViewerPixelGridEnabled = true; + + public InferenceSettingsViewModel(INotificationService notificationService, IPrerequisiteHelper prerequisiteHelper, IPyRunner pyRunner, ServiceManager dialogFactory, ICompletionProvider completionProvider, ITrackedDownloadService trackedDownloadService, IModelIndexService modelIndexService, INavigationService settingsNavigationService, IAccountsService accountsService, ISettingsManager settingsManager) + { + this.settingsManager = settingsManager; + this.notificationService = notificationService; + this.completionProvider = completionProvider; + + settingsManager.RelayPropertyFor( + this, + vm => vm.SelectedTagCompletionCsv, + settings => settings.TagCompletionCsv + ); + + settingsManager.RelayPropertyFor( + this, + vm => vm.IsPromptCompletionEnabled, + settings => settings.IsPromptCompletionEnabled, + true + ); + + settingsManager.RelayPropertyFor( + this, + vm => vm.IsCompletionRemoveUnderscoresEnabled, + settings => settings.IsCompletionRemoveUnderscoresEnabled, + true + ); + + this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat) + .Throttle(TimeSpan.FromMilliseconds(50)) + .Subscribe(formatProperty => + { + var provider = FileNameFormatProvider.GetSample(); + var template = formatProperty.Value ?? string.Empty; + + if ( + !string.IsNullOrEmpty(template) + && provider.Validate(template) == ValidationResult.Success + ) + { + var format = FileNameFormat.Parse(template, provider); + OutputImageFileNameFormatSample = format.GetFileName() + ".png"; + } + else + { + // Use default format if empty + var defaultFormat = FileNameFormat.Parse( + FileNameFormat.DefaultTemplate, + provider + ); + OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png"; + } + }); + + settingsManager.RelayPropertyFor( + this, + vm => vm.OutputImageFileNameFormat, + settings => settings.InferenceOutputImageFileNameFormat, + true + ); + + settingsManager.RelayPropertyFor( + this, + vm => vm.IsImageViewerPixelGridEnabled, + settings => settings.IsImageViewerPixelGridEnabled, + true + ); + + ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); + } + + /// + /// Validator for + /// + public static ValidationResult ValidateOutputImageFileNameFormat( + string? format, + ValidationContext context + ) + { + return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty); + } + + /// + public override void OnLoaded() + { + base.OnLoaded(); + + UpdateAvailableTagCompletionCsvs(); + } + + #region Commands + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task ImportTagCsv() + { + var storage = App.StorageProvider; + var files = await storage.OpenFilePickerAsync( + new FilePickerOpenOptions + { + FileTypeFilter = new List + { + new("CSV") + { + Patterns = ["*.csv"] + } + } + } + ); + + if (files.Count == 0) + return; + + var sourceFile = new FilePath(files[0].TryGetLocalPath()!); + + var tagsDir = settingsManager.TagsDirectory; + tagsDir.Create(); + + // Copy to tags directory + var targetFile = tagsDir.JoinFile(sourceFile.Name); + await sourceFile.CopyToAsync(targetFile); + + // Update index + UpdateAvailableTagCompletionCsvs(); + + // Trigger load + completionProvider.BackgroundLoadFromFile(targetFile, true); + + notificationService.Show( + $"Imported {sourceFile.Name}", + $"The {sourceFile.Name} file has been imported.", + NotificationType.Success + ); + } + #endregion + + private void UpdateAvailableTagCompletionCsvs() + { + if (!settingsManager.IsLibraryDirSet) + return; + + if (settingsManager.TagsDirectory is not { Exists: true } tagsDir) + return; + + var csvFiles = tagsDir.Info.EnumerateFiles("*.csv"); + AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray(); + + // Set selected to current if exists + var settingsCsv = settingsManager.Settings.TagCompletionCsv; + if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv)) + { + SelectedTagCompletionCsv = settingsCsv; + } + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs index 8ef5c63f..47408a22 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs @@ -102,41 +102,6 @@ public partial class MainSettingsViewModel : PageViewModelBase [ObservableProperty] private bool removeSymlinksOnShutdown; - // Inference UI section - [ObservableProperty] - private bool isPromptCompletionEnabled = true; - - [ObservableProperty] - private IReadOnlyList availableTagCompletionCsvs = Array.Empty(); - - [ObservableProperty] - private string? selectedTagCompletionCsv; - - [ObservableProperty] - private bool isCompletionRemoveUnderscoresEnabled = true; - - [ObservableProperty] - [CustomValidation(typeof(MainSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))] - private string? outputImageFileNameFormat; - - [ObservableProperty] - private string? outputImageFileNameFormatSample; - - public IEnumerable OutputImageFileNameFormatVars => - FileNameFormatProvider - .GetSample() - .Substitutions.Select( - kv => - new FileNameFormatVar - { - Variable = $"{{{kv.Key}}}", - Example = kv.Value.Invoke() - } - ); - - [ObservableProperty] - private bool isImageViewerPixelGridEnabled = true; - // Integrations section [ObservableProperty] private bool isDiscordRichPresenceEnabled; @@ -217,71 +182,10 @@ public partial class MainSettingsViewModel : PageViewModelBase settings => settings.AnimationScale ); - settingsManager.RelayPropertyFor( - this, - vm => vm.SelectedTagCompletionCsv, - settings => settings.TagCompletionCsv - ); - - settingsManager.RelayPropertyFor( - this, - vm => vm.IsPromptCompletionEnabled, - settings => settings.IsPromptCompletionEnabled, - true - ); - - settingsManager.RelayPropertyFor( - this, - vm => vm.IsCompletionRemoveUnderscoresEnabled, - settings => settings.IsCompletionRemoveUnderscoresEnabled, - true - ); - - this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat) - .Throttle(TimeSpan.FromMilliseconds(50)) - .Subscribe(formatProperty => - { - var provider = FileNameFormatProvider.GetSample(); - var template = formatProperty.Value ?? string.Empty; - - if ( - !string.IsNullOrEmpty(template) - && provider.Validate(template) == ValidationResult.Success - ) - { - var format = FileNameFormat.Parse(template, provider); - OutputImageFileNameFormatSample = format.GetFileName() + ".png"; - } - else - { - // Use default format if empty - var defaultFormat = FileNameFormat.Parse( - FileNameFormat.DefaultTemplate, - provider - ); - OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png"; - } - }); - - settingsManager.RelayPropertyFor( - this, - vm => vm.OutputImageFileNameFormat, - settings => settings.InferenceOutputImageFileNameFormat, - true - ); - - settingsManager.RelayPropertyFor( - this, - vm => vm.IsImageViewerPixelGridEnabled, - settings => settings.IsImageViewerPixelGridEnabled, - true - ); - DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler( notificationService, LogLevel.Warn ); - ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); } /// @@ -291,20 +195,10 @@ public partial class MainSettingsViewModel : PageViewModelBase await notificationService.TryAsync(completionProvider.Setup()); - UpdateAvailableTagCompletionCsvs(); - // Start accounts update accountsService.RefreshAsync().SafeFireAndForget(); } - public static ValidationResult ValidateOutputImageFileNameFormat( - string? format, - ValidationContext context - ) - { - return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty); - } - partial void OnSelectedThemeChanged(string? value) { // In case design / tests @@ -453,68 +347,6 @@ public partial class MainSettingsViewModel : PageViewModelBase #endregion - #region Inference UI - - private void UpdateAvailableTagCompletionCsvs() - { - if (!settingsManager.IsLibraryDirSet) - return; - - var tagsDir = settingsManager.TagsDirectory; - if (!tagsDir.Exists) - return; - - var csvFiles = tagsDir.Info.EnumerateFiles("*.csv"); - AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray(); - - // Set selected to current if exists - var settingsCsv = settingsManager.Settings.TagCompletionCsv; - if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv)) - { - SelectedTagCompletionCsv = settingsCsv; - } - } - - [RelayCommand(FlowExceptionsToTaskScheduler = true)] - private async Task ImportTagCsv() - { - var storage = App.StorageProvider; - var files = await storage.OpenFilePickerAsync( - new FilePickerOpenOptions - { - FileTypeFilter = new List - { - new("CSV") { Patterns = new[] { "*.csv" }, } - } - } - ); - - if (files.Count == 0) - return; - - var sourceFile = new FilePath(files[0].TryGetLocalPath()!); - - var tagsDir = settingsManager.TagsDirectory; - tagsDir.Create(); - - // Copy to tags directory - var targetFile = tagsDir.JoinFile(sourceFile.Name); - await sourceFile.CopyToAsync(targetFile); - - // Update index - UpdateAvailableTagCompletionCsvs(); - - // Trigger load - completionProvider.BackgroundLoadFromFile(targetFile, true); - - notificationService.Show( - $"Imported {sourceFile.Name}", - $"The {sourceFile.Name} file has been imported.", - NotificationType.Success - ); - } - #endregion - #region System /// diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index a7fdf1a8..da966284 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -24,7 +24,7 @@ public partial class SettingsViewModel : PageViewModelBase public IReadOnlyList SubPages { get; } [ObservableProperty] - private ObservableCollection currentPagePath = new(); + private ObservableCollection currentPagePath = []; [ObservableProperty] private PageViewModelBase? currentPage; diff --git a/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml index a5abbfc3..9eb69ab6 100644 --- a/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml @@ -4,66 +4,144 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" + xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" + xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia" xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings" d:DataContext="{x:Static mocks:DesignData.InferenceSettingsViewModel}" - d:DesignHeight="450" - d:DesignWidth="800" + d:DesignHeight="650" + d:DesignWidth="900" x:DataType="vmSettings:InferenceSettingsViewModel" mc:Ignorable="d"> + + + + + - - + + + + Text="{x:Static lang:Resources.Label_Prompt}" /> + - - - - - + Margin="8,0" + Header="{x:Static lang:Resources.Label_AutoCompletion}"> + + + + + - + + + + + + + + + + + + +