Multi-Platform Package Manager for Stable Diffusion
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

586 lines
18 KiB

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Threading;
using AvaloniaEdit;
using AvaloniaEdit.TextMate;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Markdown.Avalonia;
using NLog;
using Refit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Services;
using TextMateSharp.Grammars;
using Process = FuzzySharp.Process;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia;
public static class DialogHelper
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// Create a generic textbox entry content dialog.
/// </summary>
public static BetterContentDialog CreateTextEntryDialog(
string title,
string description,
IReadOnlyList<TextBoxField> textFields
)
{
Dispatcher.UIThread.VerifyAccess();
var stackPanel = new StackPanel();
var grid = new Grid
{
RowDefinitions =
{
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Star)
},
Children =
{
new TextBlock { Text = description },
stackPanel
}
};
grid.Loaded += (_, _) =>
{
// Focus first textbox
var firstTextBox = stackPanel.Children.OfType<TextBox>().First();
firstTextBox.Focus();
firstTextBox.CaretIndex = firstTextBox.Text?.LastIndexOf('.') ?? 0;
};
// Disable primary button if any textboxes are invalid
var primaryCommand = new RelayCommand(
delegate { },
() =>
{
var invalidCount = textFields.Count(field => !field.IsValid);
Debug.WriteLine($"Checking can execute: {invalidCount} invalid fields");
return invalidCount == 0;
}
);
// Create textboxes
foreach (var field in textFields)
{
var label = new TextBlock { Text = field.Label };
stackPanel.Children.Add(label);
var textBox = new TextBox
{
[!TextBox.TextProperty] = new Binding("TextProperty"),
Watermark = field.Watermark,
DataContext = field,
};
stackPanel.Children.Add(textBox);
// When IsValid property changes, update invalid count and primary button
field.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(TextBoxField.IsValid))
{
primaryCommand.NotifyCanExecuteChanged();
}
};
// Set initial value
textBox.Text = field.Text;
// See if initial value is valid
try
{
field.Validator?.Invoke(field.Text);
}
catch (Exception)
{
field.IsValid = false;
}
}
return new BetterContentDialog
{
Title = title,
Content = grid,
PrimaryButtonText = Resources.Action_OK,
CloseButtonText = Resources.Action_Cancel,
IsPrimaryButtonEnabled = true,
PrimaryButtonCommand = primaryCommand,
DefaultButton = ContentDialogButton.Primary
};
}
/// <summary>
/// Create a generic dialog for showing a markdown document
/// </summary>
public static BetterContentDialog CreateMarkdownDialog(
string markdown,
string? title = null,
TextEditorPreset editorPreset = default
)
{
Dispatcher.UIThread.VerifyAccess();
var viewer = new MarkdownScrollViewer { Markdown = markdown };
// Apply syntax highlighting to code blocks if preset is provided
if (editorPreset != default)
{
using var _ = CodeTimer.StartDebug();
var appliedCount = 0;
if (
viewer.GetLogicalDescendants().FirstOrDefault()?.GetLogicalDescendants() is
{ } stackDescendants
)
{
foreach (var editor in stackDescendants.OfType<TextEditor>())
{
TextEditorConfigs.Configure(editor, editorPreset);
editor.FontFamily = "Cascadia Code,Consolas,Menlo,Monospace";
editor.Margin = new Thickness(0);
editor.Padding = new Thickness(4);
editor.IsEnabled = false;
if (editor.GetLogicalParent() is Border border)
{
border.BorderThickness = new Thickness(0);
border.CornerRadius = new CornerRadius(4);
}
appliedCount++;
}
}
Logger.Log(
appliedCount > 0 ? LogLevel.Trace : LogLevel.Warn,
$"Applied syntax highlighting to {appliedCount} code blocks"
);
}
return new BetterContentDialog
{
Title = title,
Content = viewer,
CloseButtonText = Resources.Action_Close,
IsPrimaryButtonEnabled = false,
};
}
/// <summary>
/// Create a dialog for displaying an ApiException
/// </summary>
public static BetterContentDialog CreateApiExceptionDialog(
ApiException exception,
string? title = null
)
{
Dispatcher.UIThread.VerifyAccess();
// Setup text editor
var textEditor = new TextEditor
{
IsReadOnly = true,
WordWrap = true,
Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false }
};
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
textEditor
.InstallTextMate(registryOptions)
.SetGrammar(registryOptions.GetScopeByLanguageId("json"));
var mainGrid = new StackPanel
{
Spacing = 8,
Margin = new Thickness(16),
Children =
{
new TextBlock
{
Text = $"{(int)exception.StatusCode} - {exception.ReasonPhrase}",
FontSize = 18,
FontWeight = FontWeight.Medium,
Margin = new Thickness(0, 8),
},
textEditor
}
};
var dialog = new BetterContentDialog
{
Title = title,
Content = mainGrid,
CloseButtonText = "Close",
IsPrimaryButtonEnabled = false,
};
// Try to deserialize to json element
if (exception.Content != null)
{
try
{
// Deserialize to json element then re-serialize to ensure indentation
var jsonElement = JsonSerializer.Deserialize<JsonElement>(
exception.Content,
new JsonSerializerOptions
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
}
);
var formatted = JsonSerializer.Serialize(
jsonElement,
new JsonSerializerOptions() { WriteIndented = true }
);
textEditor.Document.Text = formatted;
}
catch (JsonException)
{
// Otherwise just add the content as a code block
textEditor.Document.Text = exception.Content;
}
}
return dialog;
}
/// <summary>
/// Create a dialog for displaying json
/// </summary>
public static BetterContentDialog CreateJsonDialog(
string json,
string? title = null,
string? subTitle = null
)
{
Dispatcher.UIThread.VerifyAccess();
// Setup text editor
var textEditor = new TextEditor
{
IsReadOnly = true,
WordWrap = true,
Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false }
};
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
textEditor
.InstallTextMate(registryOptions)
.SetGrammar(registryOptions.GetScopeByLanguageId("json"));
var mainGrid = new StackPanel
{
Spacing = 8,
Margin = new Thickness(16),
Children = { textEditor }
};
if (subTitle is not null)
{
mainGrid.Children.Insert(
0,
new TextBlock
{
Text = subTitle,
FontSize = 18,
FontWeight = FontWeight.Medium,
Margin = new Thickness(0, 8),
}
);
}
var dialog = new BetterContentDialog
{
Title = title,
Content = mainGrid,
CloseButtonText = "Close",
PrimaryButtonText = "Copy",
IsPrimaryButtonEnabled = false,
};
// Try to deserialize to json element
try
{
// Deserialize to json element then re-serialize to ensure indentation
var jsonElement = JsonSerializer.Deserialize<JsonElement>(
json,
new JsonSerializerOptions
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
}
);
var formatted = JsonSerializer.Serialize(
jsonElement,
new JsonSerializerOptions { WriteIndented = true }
);
textEditor.Document.Text = formatted;
}
catch (JsonException)
{
// Otherwise just add the content as a code block
textEditor.Document.Text = json;
}
dialog.PrimaryButtonCommand = new AsyncRelayCommand(async () =>
{
// Copy the json to clipboard
var clipboard = App.Clipboard;
await clipboard.SetTextAsync(textEditor.Document.Text);
});
return dialog;
}
/// <summary>
/// Create a dialog for displaying a prompt error
/// </summary>
/// <param name="exception">Target exception to display</param>
/// <param name="sourceText">Full text of the target Document</param>
/// <param name="modelIndexService">Optional model index service to look for similar names</param>
public static BetterContentDialog CreatePromptErrorDialog(
PromptError exception,
string sourceText,
IModelIndexService? modelIndexService = null
)
{
Dispatcher.UIThread.VerifyAccess();
var title =
exception is PromptSyntaxError ? "Prompt Syntax Error" : "Prompt Validation Error";
// Get the index of the error
var errorIndex = exception.TextOffset;
// Get the line of error
var total = 0;
var errorLine = string.Empty;
var errorLineNum = 0;
var errorLineOffset = -1;
var errorLineEndOffset = -1;
foreach (var (i, line) in sourceText.Split(Environment.NewLine).Enumerate())
{
var lineLength = line.Length + Environment.NewLine.Length;
if (total + lineLength > errorIndex)
{
// Found, set the line text and number
errorLine = line;
errorLineNum = i + 1;
// Calculate line offset of the error
errorLineOffset = exception.TextOffset - total;
// Calculate line offset of the end of the error
errorLineEndOffset = exception.TextEndOffset - total;
break;
}
total += lineLength;
}
// Format the error line
var errorLineFormattedBuilder = new StringBuilder();
// Add line number
var errorLinePrefix = $"[{errorLineNum}] ";
errorLineFormattedBuilder.AppendLine(errorLinePrefix + errorLine);
// Add error indicator at line offset
errorLineFormattedBuilder.Append(' ', errorLinePrefix.Length + errorLineOffset);
errorLineFormattedBuilder.Append('^', errorLineEndOffset - errorLineOffset);
var errorLineFormatted = errorLineFormattedBuilder.ToString();
// Setup text editor
var textEditor = new TextEditor
{
IsReadOnly = true,
WordWrap = false,
IsEnabled = false,
ShowLineNumbers = false,
FontFamily = "Cascadia Code,Consolas,Menlo,Monospace",
FontSize = 15,
Options =
{
HighlightCurrentLine = true,
ShowColumnRulers = false,
AllowScrollBelowDocument = false
}
};
TextEditorConfigs.Configure(textEditor, TextEditorPreset.Prompt);
textEditor.Document.Text = errorLineFormatted;
textEditor.TextArea.Caret.Offset = textEditor.Document.Lines[0].EndOffset;
var mainGrid = new StackPanel
{
Spacing = 8,
Margin = new Thickness(16),
Children =
{
new TextBlock
{
Text =
$"{exception.Message} - at line {errorLineNum} [{errorLineOffset}:{errorLineEndOffset}]",
FontSize = 18,
FontWeight = FontWeight.Medium,
Margin = new Thickness(0, 8),
},
textEditor
}
};
// Check model typos
if (modelIndexService is not null && exception is PromptUnknownModelError unknownModelError)
{
var sharedFolderType = unknownModelError.ModelType.ConvertTo<SharedFolderType>();
if (modelIndexService.ModelIndex.TryGetValue(sharedFolderType, out var models))
{
var result = Process.ExtractOne(
unknownModelError.ModelName,
models.Select(m => m.FileNameWithoutExtension)
);
if (result.Score > 40)
{
mainGrid.Children.Add(
new InfoBar
{
Title = $"Did you mean: {result.Value}?",
IsClosable = false,
IsOpen = true
}
);
}
}
}
textEditor.ScrollToHorizontalOffset(errorLineEndOffset - errorLineOffset);
var dialog = new BetterContentDialog
{
Title = title,
Content = mainGrid,
CloseButtonText = "Close",
IsPrimaryButtonEnabled = false,
};
return dialog;
}
/// <summary>
/// Create a simple title and description task dialog.
/// Sets the XamlRoot to the current top level window.
/// </summary>
public static TaskDialog CreateTaskDialog(string title, string description)
{
Dispatcher.UIThread.VerifyAccess();
var content = new StackPanel
{
Children =
{
new TextBlock
{
Margin = new Thickness(0, 0, 0, 8),
FontSize = 16,
Text = title,
TextWrapping = TextWrapping.WrapWithOverflow,
},
new TextBlock { Text = description, TextWrapping = TextWrapping.WrapWithOverflow, }
}
};
return new TaskDialog
{
Title = title,
Content = content,
XamlRoot = App.VisualRoot
};
}
/// <summary>
/// Creates a connection help dialog.
/// </summary>
public static BetterContentDialog CreateConnectionHelpDialog()
{
// TODO
return new BetterContentDialog();
}
}
// Text fields
public sealed class TextBoxField : INotifyPropertyChanged
{
// Label above the textbox
public string Label { get; init; } = string.Empty;
// Actual text value
public string Text { get; set; } = string.Empty;
// Watermark text
public string Watermark { get; init; } = string.Empty;
/// <summary>
/// Validation action on text changes. Throw exception if invalid.
/// </summary>
public Action<string>? Validator { get; init; }
public string TextProperty
{
get => Text;
[DebuggerStepThrough]
set
{
try
{
Validator?.Invoke(value);
}
catch (Exception e)
{
IsValid = false;
throw new DataValidationException(e.Message);
}
Text = value;
IsValid = true;
OnPropertyChanged();
}
}
// Default to true if no validator is provided
private bool isValid;
public bool IsValid
{
get => Validator == null || isValid;
set
{
isValid = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}