mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 13:07:21 +00:00
Improved the dialogs for local data sources and the Batch Processing assistant (#1096)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
149395c1a0
commit
f7e6ceb915
45 files changed
+2514
-361
No files matched your search
@@ -0,0 +1,3 @@
|
||||
@inherits ConfigurationText
|
||||
|
||||
<FileExclusionEditor Text="@this.EditingText" TextChanged="@this.InternalUpdate" Label="@this.OptionDescription" Disabled="@this.IsDisabled" ShowHelp="@this.ShowHelp" ShowReset="@(this.ResetValue is not null)" ResetValue="@(this.ResetValue?.Invoke() ?? FileExclusions.DEFAULT_PATTERNS)" ResetButtonText="@this.ResetButtonText" ResetRequested="@this.ApplyTextAsync"/>
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Reuses ConfigurationText's persistence, validation, reset, and organization locks with the
|
||||
/// shared exclusion editor. The editor supplies its own outlined fields and heading.
|
||||
/// </summary>
|
||||
public partial class ConfigurationFileExclusions : ConfigurationText
|
||||
{
|
||||
[Parameter]
|
||||
public bool ShowHelp { get; set; } = true;
|
||||
|
||||
protected override Variant Variant => Variant.Text;
|
||||
|
||||
protected override string Label => string.Empty;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
Text="@(this.ExpandToContent ? this.EditingText : this.Text())"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Adornment="@this.IconAdornment"
|
||||
@@ -14,6 +14,7 @@
|
||||
Lines="@this.NumLines"
|
||||
AutoGrow="@this.AutoGrow"
|
||||
MaxLines="@this.GetMaxLines"
|
||||
Class="@this.InputClass"
|
||||
Immediate="@true"
|
||||
Validation="@this.Validation"
|
||||
Underline="false"
|
||||
@@ -25,20 +26,26 @@ else
|
||||
<div>
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
Text="@(this.ExpandToContent ? this.EditingText : this.Text())"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Adornment="@this.IconAdornment"
|
||||
AdornmentIcon="@this.Icon"
|
||||
Adornment="@(this.ResetButtonIconOnly ? Adornment.End : this.IconAdornment)"
|
||||
AdornmentIcon="@(this.ResetButtonIconOnly ? Icons.Material.Filled.Restore : this.Icon)"
|
||||
OnAdornmentClick="@this.ResetAdornmentClick"
|
||||
AdornmentAriaLabel="@(this.ResetButtonIconOnly ? this.ResetButtonText : null)"
|
||||
AdornmentColor="@this.IconColor"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Lines="@this.NumLines"
|
||||
AutoGrow="@this.AutoGrow"
|
||||
MaxLines="@this.GetMaxLines"
|
||||
Class="@this.InputClass"
|
||||
Immediate="@true"
|
||||
Validation="@this.Validation"
|
||||
Underline="false"
|
||||
/>
|
||||
<RestoreDefaultButton Text="@this.ResetButtonText" Disabled="@this.IsDisabled" OnClick="@this.ResetTextAsync"/>
|
||||
@if (!this.ResetButtonIconOnly)
|
||||
{
|
||||
<RestoreDefaultButton Text="@this.ResetButtonText" Disabled="@this.IsDisabled" OnClick="@this.ResetTextAsync"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
@@ -24,6 +25,24 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = Icons.Material.Filled.Info;
|
||||
|
||||
/// <summary>
|
||||
/// Removes the default bottom margin when the field is part of a compact group.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool Compact { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wraps and grows with its text, including while the field is not focused, up to the lines of a
|
||||
/// field for file patterns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Such a field shows the draft rather than the stored value. Otherwise, a reset which leaves the
|
||||
/// stored value as it was would not reach the field, e.g., restoring the default while the field
|
||||
/// shows a draft which the validation rejected.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool ExpandToContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The color of the icon to use.
|
||||
/// </summary>
|
||||
@@ -54,6 +73,12 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
[Parameter]
|
||||
public string ResetButtonText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Uses an accessible reset icon inside the field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ResetButtonIconOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the configured text before it is stored.
|
||||
/// </summary>
|
||||
@@ -61,10 +86,14 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
public Func<string, string?>? Validation { get; set; }
|
||||
|
||||
private string internalText = string.Empty;
|
||||
private string? lastSourceText;
|
||||
|
||||
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
|
||||
{
|
||||
AutoReset = false
|
||||
};
|
||||
|
||||
protected string EditingText => this.internalText;
|
||||
|
||||
#region Overrides of ConfigurationBase
|
||||
|
||||
@@ -72,6 +101,8 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
protected override bool Stretch => true;
|
||||
|
||||
protected override Variant Variant => Variant.Outlined;
|
||||
|
||||
protected override string MarginClass => this.Compact ? "mb-0" : base.MarginClass;
|
||||
|
||||
protected override string Label => this.OptionDescription;
|
||||
|
||||
@@ -83,15 +114,24 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
this.internalText = this.Text();
|
||||
var sourceText = this.Text();
|
||||
// Re-rendering the parent must not replace a draft which is waiting for its debounce.
|
||||
// A genuinely changed setting, e.g. from a configuration plugin, still replaces it.
|
||||
if (this.IsDisabled || !string.Equals(sourceText, this.lastSourceText, StringComparison.Ordinal))
|
||||
{
|
||||
this.internalText = sourceText;
|
||||
this.lastSourceText = sourceText;
|
||||
}
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool AutoGrow => this.NumLines > 1;
|
||||
private bool AutoGrow => this.ExpandToContent || this.NumLines > 1;
|
||||
|
||||
private int GetMaxLines => this.AutoGrow ? this.MaxLines : 1;
|
||||
private int GetMaxLines => this.ExpandToContent ? FilePatternSelection.MAX_FIELD_LINES : this.AutoGrow ? this.MaxLines : 1;
|
||||
|
||||
private string InputClass => this.ExpandToContent ? "pattern-text-field" : string.Empty;
|
||||
|
||||
//
|
||||
// MudBlazor centers the icon vertically, so in a field of several lines it floats in the middle
|
||||
@@ -99,29 +139,50 @@ public partial class ConfigurationText : ConfigurationBaseCore
|
||||
//
|
||||
private Adornment IconAdornment => this.NumLines == 1 ? Adornment.Start : Adornment.None;
|
||||
|
||||
private void InternalUpdate(string text)
|
||||
protected void InternalUpdate(string text)
|
||||
{
|
||||
if (this.IsDisabled)
|
||||
return;
|
||||
|
||||
this.timer.Stop();
|
||||
this.internalText = text;
|
||||
this.timer.Start();
|
||||
}
|
||||
|
||||
private async Task ResetTextAsync()
|
||||
private EventCallback<MouseEventArgs> ResetAdornmentClick => this.ResetButtonIconOnly
|
||||
? EventCallback.Factory.Create<MouseEventArgs>(this, this.ResetTextAsync)
|
||||
: default;
|
||||
|
||||
protected async Task ResetTextAsync()
|
||||
{
|
||||
if (this.ResetValue is null || this.IsDisabled)
|
||||
return;
|
||||
|
||||
await this.ApplyTextAsync(this.ResetValue());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies an explicit value immediately, including a reset of part of a compound setting.
|
||||
/// </summary>
|
||||
protected async Task ApplyTextAsync(string text)
|
||||
{
|
||||
if (this.IsDisabled)
|
||||
return;
|
||||
|
||||
this.timer.Stop();
|
||||
this.internalText = this.ResetValue();
|
||||
this.internalText = text;
|
||||
await this.OptionChanged(this.internalText);
|
||||
}
|
||||
|
||||
private async Task OptionChanged(string updatedText)
|
||||
protected async Task OptionChanged(string updatedText)
|
||||
{
|
||||
if (this.Validation?.Invoke(updatedText) is not null)
|
||||
if (this.IsDisabled || this.Validation?.Invoke(updatedText) is not null)
|
||||
return;
|
||||
|
||||
this.TextUpdate(updatedText);
|
||||
// A save may finish after another keystroke. Remember the value it applied before
|
||||
// awaiting storage, so its configuration-change notification cannot erase that keystroke.
|
||||
this.lastSourceText = this.Text();
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="file-exclusion-editor mb-3">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Spacing="1" Class="mb-1">
|
||||
<MudText Typo="Typo.subtitle2" Class="flex-grow-1">@this.EditorLabel</MudText>
|
||||
@if (this.ShowHelp)
|
||||
{
|
||||
<MudTooltip Text="@T("Exclusion help and examples")" RootStyle="display: inline-flex;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.HelpOutline" Color="@(this.showHelp ? Color.Info : Color.Default)" OnClick="@this.ToggleHelp" aria-label="@T("Exclusion help and examples")" aria-expanded="@(this.showHelp ? "true" : "false")"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudStack Spacing="1">
|
||||
<MudTextField T="string" InputId="@this.folderInputId" Text="@this.state.FolderText" TextChanged="@this.ChangeFoldersAsync" Immediate="@true" Disabled="@this.Disabled" Label="@T("Folders")" Lines="1" AutoGrow="@true" MaxLines="@FilePatternSelection.MAX_FIELD_LINES" Class="pattern-text-field" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@FileExclusions.ValidateFolders" UserAttributes="@INPUT_ATTRIBUTES" Placeholder="node_modules/;bin/;cmake-build-*/" Adornment="@(this.ShowReset ? Adornment.End : Adornment.None)" AdornmentIcon="@Icons.Material.Filled.Restore" OnAdornmentClick="@(() => this.RestoreDefaultsAsync(true))" AdornmentAriaLabel="@this.FolderRestoreLabel"/>
|
||||
<MudTextField T="string" InputId="@this.fileInputId" Text="@this.state.FileText" TextChanged="@this.ChangeFilesAsync" Immediate="@true" Disabled="@this.Disabled" Label="@T("Files")" Lines="1" AutoGrow="@true" MaxLines="@FilePatternSelection.MAX_FIELD_LINES" Class="pattern-text-field" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@FileExclusions.ValidateFiles" UserAttributes="@INPUT_ATTRIBUTES" Placeholder="*.min.js;*.webm;draft-*" Adornment="@(this.ShowReset ? Adornment.End : Adornment.None)" AdornmentIcon="@Icons.Material.Filled.Restore" OnAdornmentClick="@(() => this.RestoreDefaultsAsync(false))" AdornmentAriaLabel="@this.FileRestoreLabel"/>
|
||||
</MudStack>
|
||||
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">@T("These files and folders are left out, even when a file pattern selects them. Separate entries with a semicolon or a line break. End an entry with a slash to exclude every folder of that name, e.g., node_modules/.")</MudText>
|
||||
<MudCollapse Expanded="@(this.ShowHelp && this.showHelp)">
|
||||
<div class="mt-2">
|
||||
<FilePatternHelp ShowInclusionExamples="@false" ShowExclusionExamples="@true"/>
|
||||
</div>
|
||||
</MudCollapse>
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A compact, two-group editor for the exclusion string shared by folder-based features.
|
||||
/// </summary>
|
||||
public partial class FileExclusionEditor : MSGComponentBase
|
||||
{
|
||||
private static readonly Dictionary<string, object?> INPUT_ATTRIBUTES = new()
|
||||
{
|
||||
["spellcheck"] = false,
|
||||
["autocorrect"] = "off",
|
||||
["autocapitalize"] = "off",
|
||||
};
|
||||
|
||||
[Parameter]
|
||||
public string? Text { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> TextChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool ShowReset { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowHelp { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public string ResetValue { get; set; } = FileExclusions.DEFAULT_PATTERNS;
|
||||
|
||||
[Parameter]
|
||||
public string ResetButtonText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Immediately persists the combined value after restoring just one of its groups.
|
||||
/// Ordinary edits continue through TextChanged.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> ResetRequested { get; set; }
|
||||
|
||||
private readonly FileExclusionEditorState state = new();
|
||||
private readonly string folderInputId = $"exclusion-folders-{Guid.NewGuid():N}";
|
||||
private readonly string fileInputId = $"exclusion-files-{Guid.NewGuid():N}";
|
||||
private bool showHelp;
|
||||
|
||||
/// <summary>
|
||||
/// False while a field holds an invalid draft. The editor keeps such a draft to itself, so the
|
||||
/// bound text is still the last valid one and cannot tell.
|
||||
/// </summary>
|
||||
public bool IsValid => this.state.IsValid;
|
||||
|
||||
private string EditorLabel => string.IsNullOrWhiteSpace(this.Label) ? T("Excluded files and folders") : this.Label;
|
||||
|
||||
private string RestoreLabel => string.IsNullOrWhiteSpace(this.ResetButtonText) ? T("Restore default") : this.ResetButtonText;
|
||||
|
||||
private string FolderRestoreLabel => $"{this.RestoreLabel}: {T("Folders")}";
|
||||
|
||||
private string FileRestoreLabel => $"{this.RestoreLabel}: {T("Files")}";
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
this.state.Receive(this.Text, discardDraft: this.Disabled);
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
protected Task ChangeFoldersAsync(string text) => this.ChangePatternsAsync(text, folders: true);
|
||||
|
||||
protected Task ChangeFilesAsync(string text) => this.ChangePatternsAsync(text, folders: false);
|
||||
|
||||
private async Task ChangePatternsAsync(string text, bool folders)
|
||||
{
|
||||
if (this.Disabled)
|
||||
return;
|
||||
|
||||
var previousText = this.state.Text;
|
||||
var wasValid = this.state.IsValid;
|
||||
if (folders)
|
||||
this.state.EditFolders(text);
|
||||
else
|
||||
this.state.EditFiles(text);
|
||||
|
||||
// Moving a rule to the correct field can keep the combined text identical. It must still
|
||||
// publish the now-valid draft, because the previous, invalid arrangement was not sent.
|
||||
if (this.state.IsValid && (!wasValid || !string.Equals(previousText, this.state.Text, StringComparison.Ordinal)))
|
||||
await this.TextChanged.InvokeAsync(this.state.Text);
|
||||
}
|
||||
|
||||
protected async Task RestoreDefaultsAsync(bool folders)
|
||||
{
|
||||
if (this.Disabled || !this.ShowReset)
|
||||
return;
|
||||
|
||||
if (folders)
|
||||
this.state.RestoreFolders(this.ResetValue);
|
||||
else
|
||||
this.state.RestoreFiles(this.ResetValue);
|
||||
|
||||
if (!this.state.IsValid)
|
||||
return;
|
||||
|
||||
if (this.ResetRequested.HasDelegate)
|
||||
await this.ResetRequested.InvokeAsync(this.state.Text);
|
||||
else
|
||||
await this.TextChanged.InvokeAsync(this.state.Text);
|
||||
}
|
||||
|
||||
private void ToggleHelp() => this.showHelp = !this.showHelp;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Presents the existing exclusion string as two editable groups without losing the other group
|
||||
/// or letting delayed parameter echoes overwrite newer edits.
|
||||
/// </summary>
|
||||
internal sealed class FileExclusionEditorState
|
||||
{
|
||||
private string? receivedText;
|
||||
private readonly HashSet<string> pendingValues = new(StringComparer.Ordinal);
|
||||
|
||||
public string Text { get; private set; } = string.Empty;
|
||||
|
||||
public string FolderText { get; private set; } = string.Empty;
|
||||
|
||||
public string FileText { get; private set; } = string.Empty;
|
||||
|
||||
public bool IsValid => FileExclusions.IsValidFolders(this.FolderText) && FileExclusions.IsValidFiles(this.FileText);
|
||||
|
||||
/// <summary>
|
||||
/// Takes over the text of the parent, which renders again after every published edit and passes
|
||||
/// that edit back, possibly only after the user typed on. Such an echo must not replace the newer
|
||||
/// draft; any other text does, e.g., a change by a configuration plugin.
|
||||
/// </summary>
|
||||
/// <param name="text">The text the parent passes in.</param>
|
||||
/// <param name="discardDraft">Whether to take over the text in any case, e.g., while the editor is disabled.</param>
|
||||
/// <returns>Whether the text replaced the draft.</returns>
|
||||
public bool Receive(string? text, bool discardDraft = false)
|
||||
{
|
||||
text ??= string.Empty;
|
||||
if (discardDraft)
|
||||
{
|
||||
var changed = !string.Equals(text, this.Text, StringComparison.Ordinal);
|
||||
this.receivedText = text;
|
||||
this.Reset(text);
|
||||
return changed;
|
||||
}
|
||||
|
||||
// The parent passes the same text with every render; only a different one counts:
|
||||
if (string.Equals(text, this.receivedText, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
this.receivedText = text;
|
||||
|
||||
// The parent caught up with the draft. Its renders arrive in order, so no older echo follows:
|
||||
if (string.Equals(text, this.Text, StringComparison.Ordinal))
|
||||
{
|
||||
this.pendingValues.Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// The echo of an earlier edit, which a later one replaced already:
|
||||
if (this.pendingValues.Remove(text))
|
||||
return false;
|
||||
|
||||
this.Reset(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset(string text)
|
||||
{
|
||||
this.pendingValues.Clear();
|
||||
this.Text = text;
|
||||
this.Regroup();
|
||||
}
|
||||
|
||||
public void EditFolders(string? text)
|
||||
{
|
||||
this.FolderText = text ?? string.Empty;
|
||||
this.UpdateText();
|
||||
}
|
||||
|
||||
public void EditFiles(string? text)
|
||||
{
|
||||
this.FileText = text ?? string.Empty;
|
||||
this.UpdateText();
|
||||
}
|
||||
|
||||
public void RestoreFolders(string defaults) => this.EditFolders(FileExclusions.Group(defaults).Folders);
|
||||
|
||||
public void RestoreFiles(string defaults) => this.EditFiles(FileExclusions.Group(defaults).Files);
|
||||
|
||||
private void Regroup()
|
||||
{
|
||||
var groups = FileExclusions.Group(this.Text);
|
||||
this.FolderText = groups.Folders;
|
||||
this.FileText = groups.Files;
|
||||
}
|
||||
|
||||
private void UpdateText()
|
||||
{
|
||||
this.Text = string.Join(FileExclusions.ENTRY_SEPARATOR, FileExclusions.Parse(this.FolderText)
|
||||
.Concat(FileExclusions.Parse(this.FileText))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
this.pendingValues.Add(this.Text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudPaper Outlined="@true" Class="pa-3" Elevation="0">
|
||||
<MudText Typo="Typo.subtitle2">@T("Pattern examples")</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">@T("Separate patterns with semicolons. Exclusions take priority over included files.")</MudText>
|
||||
<MudSimpleTable Dense="@true" Elevation="0" aria-label="@T("Pattern examples")">
|
||||
<tbody>
|
||||
<tr><td><code>*.pdf;*.docx</code></td><td>@T("PDF and Word files")</td></tr>
|
||||
@if (this.ShowInclusionExamples)
|
||||
{
|
||||
<tr><td><code>:documents:</code></td><td>@T("Include all documents and readable text files")</td></tr>
|
||||
<tr><td><code>*</code></td><td>@T("Include all files, even audio and video")</td></tr>
|
||||
<tr><td><code>:text:</code></td><td>@T("Include readable text files, even those without an extension")</td></tr>
|
||||
}
|
||||
<tr><td><code>draft-*</code></td><td>@T("File names starting with draft-")</td></tr>
|
||||
@if (this.ShowExclusionExamples)
|
||||
{
|
||||
<tr><td><code>*build/</code></td><td>@T("Exclude folders ending in build, such as build or app-build")</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-2">@T("Patterns match file names regardless of letter case. Folder paths are not supported.")</MudText>
|
||||
</MudPaper>
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Examples of file patterns, shared by the fields for files to include and for exclusions, so both
|
||||
/// explain the patterns in the same words.
|
||||
/// </summary>
|
||||
public partial class FilePatternHelp : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the selectors and wildcards which only the files to include accept, such as :text:.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowInclusionExamples { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Shows an entry which excludes folders.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowExclusionExamples { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudStack Spacing="1" Class="mb-3">
|
||||
<div class="pattern-row">
|
||||
<div class="pattern-row-main">
|
||||
@this.AllowedContent
|
||||
</div>
|
||||
<MudTooltip Text="@T("Pattern help and examples")" RootStyle="display: inline-flex;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.HelpOutline" Color="@(this.showHelp ? Color.Info : Color.Default)" OnClick="@this.ToggleHelp" aria-label="@T("Pattern help and examples")" aria-expanded="@(this.showHelp ? "true" : "false")"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
@if (this.HasExclusions)
|
||||
{
|
||||
<div class="pattern-row">
|
||||
<div class="pattern-row-main mt-3">
|
||||
@this.ExcludedContent
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<MudCollapse Expanded="@this.showHelp">
|
||||
<div class="pattern-row">
|
||||
<div class="pattern-row-main">
|
||||
<FilePatternHelp ShowExclusionExamples="@this.HasExclusions"/>
|
||||
</div>
|
||||
</div>
|
||||
</MudCollapse>
|
||||
</MudStack>
|
||||
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A file-pattern field with shared, collapsible help and optional, always-visible exclusions.
|
||||
/// Callers supply their existing fields, preserving validation and managed-configuration locks.
|
||||
/// </summary>
|
||||
public partial class FilePatternSelection : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// How many lines a field for file patterns or exclusions grows to. A longer list scrolls.
|
||||
/// </summary>
|
||||
public const int MAX_FIELD_LINES = 6;
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public RenderFragment AllowedContent { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// When supplied, shows the existing exclusion field below the inclusion field.
|
||||
/// Both groups stay visible and use the same help control.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? ExcludedContent { get; set; }
|
||||
|
||||
private bool HasExclusions => this.ExcludedContent is not null;
|
||||
|
||||
private bool showHelp;
|
||||
|
||||
private void ToggleHelp() => this.showHelp = !this.showHelp;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudStack Row="false" Class="mb-3 pa-3 border-dashed border rounded-lg">
|
||||
<MudJustifiedText Typo="Typo.body1">
|
||||
@this.DetermineIntroText()
|
||||
</MudJustifiedText>
|
||||
<MudButton Color="@this.DetermineButtonColor()" Variant="Variant.Filled" OnClick="() => this.ShowPandocDialogAsync()">
|
||||
@this.DetermineButtonText()
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@if (ShouldShowInstallationPrompt(this.pandocInstallation))
|
||||
{
|
||||
<MudStack Row="false" Class="mb-3 pa-3 border-dashed border rounded-lg">
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@this.PromptText
|
||||
</MudJustifiedText>
|
||||
<MudButton Color="@this.ButtonColor" Variant="Variant.Filled" OnClick="() => this.ShowPandocDialogAsync()">
|
||||
@this.ButtonText
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
@@ -7,6 +7,11 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Asks to install or update Pandoc where a feature needs it for some files only, such as the HTML files
|
||||
/// of a local data source. It stays hidden while the check runs and once Pandoc is ready, so it does not
|
||||
/// distract from the rest of the dialog.
|
||||
/// </summary>
|
||||
public partial class ManagePandocDependency : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
@@ -18,7 +23,10 @@ public partial class ManagePandocDependency : MSGComponentBase
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
private PandocInstallation pandocInstallation;
|
||||
// Null means the silent availability check has not completed yet.
|
||||
private PandocInstallation? pandocInstallation;
|
||||
|
||||
internal static bool ShouldShowInstallationPrompt(PandocInstallation? installation) => installation is { IsAvailable: false };
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
@@ -30,60 +38,20 @@ public partial class ManagePandocDependency : MSGComponentBase
|
||||
|
||||
#endregion
|
||||
|
||||
private Color DetermineButtonColor()
|
||||
{
|
||||
if (this.pandocInstallation == default)
|
||||
return Color.Default;
|
||||
|
||||
switch (this.pandocInstallation)
|
||||
{
|
||||
case { CheckWasSuccessful: true, IsAvailable: true }:
|
||||
return Color.Default;
|
||||
|
||||
case { CheckWasSuccessful: true, IsAvailable: false }:
|
||||
return Color.Warning;
|
||||
|
||||
case { CheckWasSuccessful: false }:
|
||||
return Color.Error;
|
||||
}
|
||||
}
|
||||
|
||||
private string DetermineButtonText()
|
||||
{
|
||||
if(this.pandocInstallation == default)
|
||||
return T("Please wait while we check the availability of Pandoc.");
|
||||
|
||||
switch (this.pandocInstallation)
|
||||
{
|
||||
case { CheckWasSuccessful: true, IsAvailable: true }:
|
||||
return T("Check your Pandoc installation");
|
||||
|
||||
case { CheckWasSuccessful: true, IsAvailable: false }:
|
||||
return T("Update Pandoc");
|
||||
|
||||
case { CheckWasSuccessful: false }:
|
||||
return T("Install Pandoc");
|
||||
}
|
||||
}
|
||||
//
|
||||
// The prompt shows up only for an installation which is not available, so it is either outdated,
|
||||
// when the check found one, or missing:
|
||||
//
|
||||
private bool IsOutdated => this.pandocInstallation is { CheckWasSuccessful: true };
|
||||
|
||||
private Color ButtonColor => this.IsOutdated ? Color.Warning : Color.Error;
|
||||
|
||||
private string ButtonText => this.IsOutdated ? T("Update Pandoc") : T("Install Pandoc");
|
||||
|
||||
private string PromptText => this.IsOutdated
|
||||
? $"{this.IntroText} {T("Your Pandoc installation is outdated. Please update it to the latest version to ensure compatibility with all features.")}"
|
||||
: $"{this.IntroText} {T("Pandoc is not installed or not available. Please install it to use the features that require Pandoc.")}";
|
||||
|
||||
private string DetermineIntroText()
|
||||
{
|
||||
if (this.pandocInstallation == default)
|
||||
return $"{this.IntroText} {T("Please wait while we check the availability of Pandoc.")}";
|
||||
|
||||
switch (this.pandocInstallation)
|
||||
{
|
||||
case { CheckWasSuccessful: true, IsAvailable: true }:
|
||||
return $"{this.IntroText} {T("Your Pandoc installation meets the requirements.")}";
|
||||
|
||||
case { CheckWasSuccessful: true, IsAvailable: false }:
|
||||
return $"{this.IntroText} {T("Your Pandoc installation is outdated. Please update it to the latest version to ensure compatibility with all features.")}";
|
||||
|
||||
case { CheckWasSuccessful: false }:
|
||||
return $"{this.IntroText} {T("Pandoc is not installed or not available. Please install it to use the features that require Pandoc.")}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowPandocDialogAsync()
|
||||
{
|
||||
var dialogReference = await this.DialogService.ShowAsync<PandocDialog>(T("Pandoc Installation"), DialogOptions.FULLSCREEN);
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
Text="@this.Value"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="3"
|
||||
Lines="@this.Lines"
|
||||
MaxLines="@this.MaxLines"
|
||||
AutoGrow="@true"
|
||||
Adornment="@(string.IsNullOrEmpty(this.Icon) ? Adornment.None : Adornment.Start)"
|
||||
AdornmentIcon="@this.Icon"
|
||||
Style="@this.GetColor()"
|
||||
UserAttributes="@USER_INPUT_ATTRIBUTES" />
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ public partial class TextInfoLines : MSGComponentBase
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
@@ -18,6 +21,9 @@ public partial class TextInfoLines : MSGComponentBase
|
||||
[Parameter]
|
||||
public int MaxLines { get; set; } = 30;
|
||||
|
||||
[Parameter]
|
||||
public int Lines { get; set; } = 3;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowingCopyButton { get; set; } = true;
|
||||
|
||||
|
||||
Reference in new issue
Block a user