small ui fixes

This commit is contained in:
nilsk 2026-03-31 15:46:04 +02:00
parent 3f2d76337a
commit 92267b129f
5 changed files with 74 additions and 33 deletions

View File

@ -9,11 +9,11 @@ public static class AssistantAuditLevelExtensions
public static string GetName(this AssistantAuditLevel level) => level switch
{
AssistantAuditLevel.DANGEROUS => TB("Dangerous"),
AssistantAuditLevel.CAUTION => TB("Needs Review"),
AssistantAuditLevel.CAUTION => TB("Concerning"),
AssistantAuditLevel.SAFE => TB("Safe"),
_ => TB("Unknown"),
};
public static Severity GetSeverity(this AssistantAuditLevel level) => level switch
{
AssistantAuditLevel.DANGEROUS => Severity.Error,
@ -22,5 +22,26 @@ public static class AssistantAuditLevelExtensions
_ => Severity.Info,
};
public static Color GetColor(this AssistantAuditLevel level) => level switch
{
AssistantAuditLevel.DANGEROUS => Color.Error,
AssistantAuditLevel.CAUTION => Color.Warning,
AssistantAuditLevel.SAFE => Color.Success,
_ => Color.Default,
};
public static string GetIcon(this AssistantAuditLevel level) => level switch
{
AssistantAuditLevel.DANGEROUS => Icons.Material.Filled.Dangerous,
AssistantAuditLevel.CAUTION => Icons.Material.Filled.Warning,
AssistantAuditLevel.SAFE => Icons.Material.Filled.Verified,
_ => Icons.Material.Filled.HelpOutline,
};
/// <summary>
/// Parses an audit level string and falls back to <see cref="AssistantAuditLevel.UNKNOWN"/> when parsing fails.
/// </summary>
/// <param name="value">The audit level text to parse.</param>
/// <returns>The parsed audit level, or <see cref="AssistantAuditLevel.UNKNOWN"/> for null, empty, or invalid values.</returns>
public static AssistantAuditLevel Parse(string? value) => Enum.TryParse<AssistantAuditLevel>(value, true, out var level) ? level : AssistantAuditLevel.UNKNOWN;
}

View File

@ -5,7 +5,21 @@
@using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogDynamic>
@if (this.RootComponent is null)
@if (!string.IsNullOrWhiteSpace(this.securityMessage))
{
<MudPaper Class="pa-4 ma-4" Elevation="0">
<MudAlert Severity="Severity.Error" Variant="Variant.Filled" Square="false" Elevation="6" Class="pa-4">
@this.securityMessage
</MudAlert>
@if (this.assistantPlugin is not null)
{
<div class="mt-4">
<AssistantPluginSecurityCard Plugin="@this.assistantPlugin"/>
</div>
}
</MudPaper>
}
else if (this.RootComponent is null)
{
<MudAlert Severity="Severity.Warning">
@this.T("No assistant plugin are currently installed.")

View File

@ -22,6 +22,7 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
protected override bool ShowProfileSelection => this.showFooterProfileSelection;
protected override string SubmitText => this.submitText;
protected override Func<Task> SubmitAction => this.Submit;
protected override bool SubmitDisabled => this.isSecurityBlocked;
// Dynamic assistants do not have dedicated settings yet.
// Reuse chat-level provider filtering/preselection instead of NONE.
protected override Tools.Components Component => Tools.Components.CHAT;
@ -40,6 +41,8 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
private readonly HashSet<string> executingSwitchActions = [];
private string pluginPath = string.Empty;
private PluginAssistantAudit? audit;
private string securityMessage = string.Empty;
private bool isSecurityBlocked;
private const string ASSISTANT_QUERY_KEY = "assistantId";
#region Implementation of AssistantBase
@ -66,6 +69,16 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
var pluginHash = pluginAssistant.ComputeAuditHash();
this.audit = this.SettingsManager.ConfigurationData.AssistantPluginAudits.FirstOrDefault(x => x.PluginId == pluginAssistant.Id && x.PluginHash == pluginHash);
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, pluginAssistant);
if (!securityState.CanStartAssistant)
{
this.assistantPlugin = pluginAssistant;
this.securityMessage = securityState.Description;
this.isSecurityBlocked = true;
base.OnInitialized();
return;
}
var rootComponent = this.RootComponent;
if (rootComponent is not null)
{
@ -387,6 +400,13 @@ public partial class AssistantDynamic : AssistantBaseCore<SettingsDialogDynamic>
private async Task Submit()
{
if (this.assistantPlugin is not null)
{
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.assistantPlugin);
if (!securityState.CanStartAssistant)
return;
}
this.CreateChatThread();
var time = this.AddUserRequest(await this.CollectUserPromptAsync());
await this.AddAIResponseAsync(time);

View File

@ -1,5 +1,4 @@
@using AIStudio.Agents.AssistantAudit
@using AIStudio.Components
@inherits MSGComponentBase
<MudDialog DefaultFocus="DefaultFocus.FirstChild">
@ -299,7 +298,7 @@
<MudButton OnClick="@this.CloseWithoutActivation" Variant="Variant.Filled">
@(this.audit is null ? T("Cancel") : T("Close"))
</MudButton>
<MudButton OnClick="@this.RunAudit" Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!this.CanRunAudit)">
<MudButton OnClick="@this.RunAudit" Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!this.CanRunAudit || this.justAudited)">
@T("Start Security Check")
</MudButton>
@if (this.CanEnablePlugin)

View File

@ -1,3 +1,7 @@
using System.Collections;
using System.Collections.Immutable;
using System.Globalization;
using System.Reflection;
using AIStudio.Agents.AssistantAudit;
using AIStudio.Components;
using AIStudio.Provider;
@ -6,17 +10,12 @@ using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.PluginSystem.Assistants;
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
using Microsoft.AspNetCore.Components;
using System.Collections;
using System.Collections.Immutable;
using System.Globalization;
using System.Reflection;
namespace AIStudio.Dialogs;
public partial class AssistantPluginAuditDialog : MSGComponentBase
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginAuditDialog).Namespace,
nameof(AssistantPluginAuditDialog));
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginAuditDialog).Namespace, nameof(AssistantPluginAuditDialog));
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
@ -37,7 +36,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
private ImmutableDictionary<string, string> luaFiles = ImmutableDictionary.Create<string, string>();
private IReadOnlyCollection<TreeItemData<ITreeItem>> componentTreeItems = [];
private IReadOnlyCollection<TreeItemData<ITreeItem>> fileSystemTreeItems = [];
private CultureInfo fileInfoCulture = CultureInfo.InvariantCulture;
private CultureInfo currentCultureInfo = CultureInfo.InvariantCulture;
private bool isAuditing;
private AIStudio.Settings.Provider CurrentProvider => this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_ASSISTANT_PLUGIN_AUDIT, null, true);
@ -63,13 +62,14 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
private bool CanEnablePlugin => this.audit is not null && !this.isAuditing && !this.IsActivationBlockedBySettings;
private Color EnableButtonColor => this.RequiresActivationConfirmation ? Color.Warning : Color.Success;
private bool justAudited;
private const ushort BYTES_PER_KILOBYTE = 1024;
protected override async Task OnInitializedAsync()
{
var activeLanguagePlugin = await this.SettingsManager.GetActiveLanguagePlugin();
this.fileInfoCulture = this.CreateFileInfoCulture(activeLanguagePlugin.IETFTag);
this.currentCultureInfo = CommonTools.DeriveActiveCultureOrInvariant(activeLanguagePlugin.IETFTag);
this.plugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>()
.FirstOrDefault(x => x.Id == this.PluginId);
@ -116,6 +116,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
finally
{
this.isAuditing = false;
this.justAudited = true;
await this.InvokeAsync(this.StateHasChanged);
}
}
@ -453,7 +454,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
string stringValue when string.IsNullOrWhiteSpace(stringValue) => TB("empty"),
string stringValue => stringValue,
bool boolValue => boolValue ? "true" : "false",
_ => Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty,
_ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty,
};
private string GetStructuredValueCaption(object? value) => value switch
@ -477,37 +478,23 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
_ => Icons.Material.Filled.DataArray,
};
private string FormatFileTimestamp(DateTime timestamp) => timestamp.ToString("g", this.fileInfoCulture);
private string FormatFileTimestamp(DateTime timestamp) => CommonTools.FormatTimestampToGeneral(timestamp, this.currentCultureInfo);
private string FormatFileSize(long bytes)
{
if (bytes < BYTES_PER_KILOBYTE)
return string.Format(this.fileInfoCulture, TB("{0} B"), bytes);
return string.Format(this.currentCultureInfo, TB("{0} B"), bytes);
var kilobyte = bytes / (double)BYTES_PER_KILOBYTE;
if (kilobyte < BYTES_PER_KILOBYTE)
return string.Format(this.fileInfoCulture, TB("{0:0.##} KB"), kilobyte);
return string.Format(this.currentCultureInfo, TB("{0:0.##} KB"), kilobyte);
var megabyte = kilobyte / BYTES_PER_KILOBYTE;
if (megabyte < BYTES_PER_KILOBYTE)
return string.Format(this.fileInfoCulture, TB("{0:0.##} MB"), megabyte);
return string.Format(this.currentCultureInfo, TB("{0:0.##} MB"), megabyte);
var gigabyte = megabyte / BYTES_PER_KILOBYTE;
return string.Format(this.fileInfoCulture, TB("{0:0.##} GB"), gigabyte);
return string.Format(this.currentCultureInfo, TB("{0:0.##} GB"), gigabyte);
}
private CultureInfo CreateFileInfoCulture(string ietfTag)
{
if (string.IsNullOrWhiteSpace(ietfTag))
return CultureInfo.InvariantCulture;
try
{
return CultureInfo.GetCultureInfo(ietfTag);
}
catch (CultureNotFoundException)
{
return CultureInfo.InvariantCulture;
}
}
}