mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-06 18:09:40 +00:00
Added assistant plugins (#659)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
1 parent
a573409a24
commit
f6a128f2e4
100 files changed
+10146
-85
No files matched your search
@@ -0,0 +1,311 @@
|
||||
@using AIStudio.Agents.AssistantAudit
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog DefaultFocus="DefaultFocus.FirstChild">
|
||||
<DialogContent>
|
||||
@if (this.plugin is null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="true">
|
||||
@T("The assistant plugin could not be resolved for auditing.")
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Spacing="2">
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
@T("This security check uses a sample prompt preview. Empty or placeholder values in the preview are expected.")
|
||||
</MudAlert>
|
||||
|
||||
<MudPaper Class="pa-3 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.h6">@this.plugin.Name</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-2">@this.plugin.Description</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Audit provider"): <strong>@this.ProviderLabel</strong>
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Minimum required safety level"): <strong>@this.MinimumLevelLabel</strong>
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
|
||||
<MudExpansionPanels MultiExpansion="true">
|
||||
<MudExpansionPanel Expanded="true">
|
||||
<TitleContent>
|
||||
<div class="d-flex">
|
||||
<MudIcon Icon="@Icons.Material.Filled.EditNote" class="mr-3" Color="Color.Primary"></MudIcon>
|
||||
<MudText>@T("System Prompt")</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<MudTextField T="string" Text="@this.plugin.RawSystemPrompt" ReadOnly="true" Variant="Variant.Outlined" Lines="8" Class="mt-2"/>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
<MudExpansionPanel Expanded="false">
|
||||
<TitleContent>
|
||||
<div class="d-flex">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Preview" class="mr-3" Color="Color.Primary"></MudIcon>
|
||||
<MudText>@T("User Prompt Preview")</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
@{
|
||||
var promptBuilder = this.plugin.HasCustomPromptBuilder;
|
||||
var sortDirection = promptBuilder ? SortDirection.Ascending : SortDirection.Descending;
|
||||
var badgeColor = promptBuilder ? Color.Success : Color.Error;
|
||||
var fallbackBadgeColor = !promptBuilder ? Color.Success : Color.Error;
|
||||
|
||||
var fallbackText = promptBuilder ? T("Fallback Prompt") : T("User Prompt");
|
||||
|
||||
<MudTabs Centered="true" SortDirection="@sortDirection" Rounded="true" ApplyEffectsToContainer="true">
|
||||
<MudTabPanel SortKey="A" Text="@T("Advanced Prompt Building")" Icon="@Icons.Material.Filled.Build" BadgeDot="@true" BadgeColor="@badgeColor" Disabled="@(!promptBuilder)">
|
||||
<MudTextField T="string" Text="@this.promptPreview" ReadOnly="true" Variant="Variant.Outlined" Lines="10"/>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel SortKey="B" Text="@fallbackText" Icon="@Icons.Material.Filled.EditNote" BadgeDot="@true" BadgeColor="@fallbackBadgeColor">
|
||||
<MudTextField T="string" Text="@this.promptFallbackPreview" ReadOnly="true" Variant="Variant.Outlined" Lines="10"/>
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
}
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
<MudExpansionPanel KeepContentAlive="false">
|
||||
<TitleContent>
|
||||
<div class="d-flex">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountTree" class="mr-3" Color="Color.Primary"></MudIcon>
|
||||
<MudText>@T("Components")</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<MudTreeView T="ITreeItem" Items="@this.componentTreeItems" ReadOnly="true" Hover="true" Dense="true" Disabled="false" ExpandOnClick="true" Class="mt-3">
|
||||
<ItemTemplate Context="item">
|
||||
@if (item.Value is AssistantAuditTreeItem treeItem)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@item.Children">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@treeItem.Text
|
||||
</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(treeItem.Caption))
|
||||
{
|
||||
if (treeItem.IsComponent)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Style="justify-self: end;">
|
||||
@treeItem.Caption
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.overline" Color="Color.Primary" Style="justify-self: end;">
|
||||
@treeItem.Caption
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
<MudExpansionPanel KeepContentAlive="false">
|
||||
<TitleContent>
|
||||
<div class="d-flex">
|
||||
<MudIcon Icon="@Icons.Material.Filled.FolderZip" class="mr-3" Color="Color.Primary"></MudIcon>
|
||||
<MudText>@T("Plugin Structure")</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<MudTreeView T="ITreeItem" Items="@this.fileSystemTreeItems" ReadOnly="true" Hover="true" Dense="true" Disabled="false" ExpandOnClick="true" Class="mt-3">
|
||||
<ItemTemplate Context="item">
|
||||
@if (item.Value is AssistantAuditTreeItem treeItem)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@item.Children">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@treeItem.Text
|
||||
</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(treeItem.Caption))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Style="justify-self: end;">
|
||||
@treeItem.Caption
|
||||
</MudText>
|
||||
}
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
<MudExpansionPanel KeepContentAlive="false">
|
||||
<TitleContent>
|
||||
<div class="d-flex">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Code" class="mr-3" Color="Color.Primary"></MudIcon>
|
||||
<MudText>@T("Lua Manifest")</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<MudExpansionPanels Elevation="0" Dense="true">
|
||||
@foreach (var file in this.luaFiles)
|
||||
{
|
||||
var fileInfo = new FileInfo(Path.Combine(this.plugin.PluginPath, file.Key));
|
||||
<MudExpansionPanel Expanded="false" Icon="@Icons.Material.Outlined.ArrowDropDown">
|
||||
<TitleContent>
|
||||
<div class="d-flex align-center justify-start">
|
||||
<MudTooltip Placement="Placement.Left" Arrow="true">
|
||||
<ChildContent>
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.Info" Size="Size.Small" Color="Color.Info" Class="mr-1"/>
|
||||
</ChildContent>
|
||||
<TooltipContent>
|
||||
<MudPaper Class="pa-3" >
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">@file.Key</MudText>
|
||||
<MudDivider/>
|
||||
<MudText Typo="Typo.body2">@T("Size"): @this.FormatFileSize(fileInfo.Length)</MudText>
|
||||
<MudText Typo="Typo.body2">@T("Created"): @this.FormatFileTimestamp(fileInfo.CreationTime)</MudText>
|
||||
<MudText Typo="Typo.body2">@T("Last accessed"): @this.FormatFileTimestamp(fileInfo.LastAccessTime)</MudText>
|
||||
<MudText Typo="Typo.body2">@T("Last modified"): @this.FormatFileTimestamp(fileInfo.LastWriteTime)</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</TooltipContent>
|
||||
</MudTooltip>
|
||||
<MudText Class="">@file.Key</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<MudTextField T="string" Text="@file.Value" ReadOnly="true" Variant="Variant.Outlined" Lines="25" Class="mt-2" Style="font-family: monospace"/>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
}
|
||||
</MudExpansionPanels>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
|
||||
@if (this.audit is not null)
|
||||
{
|
||||
<MudStack Spacing="2" Class="mt-4">
|
||||
<MudText Typo="Typo.h6">@T("Audit Result")</MudText>
|
||||
|
||||
@if (this.audit.Findings.Count == 0 && this.audit.Level is not AssistantAuditLevel.UNKNOWN)
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Variant="Variant.Filled" Dense="true" Icon="@Icons.Material.Filled.VerifiedUser">
|
||||
<strong>@T("Safe")</strong><span>: @T("No security issues were found during this check.")</span>
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="@this.GetAuditResultSeverity()" Variant="Variant.Filled" Dense="true">
|
||||
<strong>@this.audit.Level.GetName()</strong><span>: @this.audit.Summary</span>
|
||||
</MudAlert>
|
||||
|
||||
@if (this.IsActivationBlockedBySettings)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Block">
|
||||
@T("This plugin cannot be activated because its audit result is below the required safety level and your settings block activation in this case.")
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.RequiresActivationConfirmation)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.WarningAmber">
|
||||
@T("This plugin is below the required safety level. Your settings still allow activation, but enabling it requires an extra confirmation because it may be unsafe.")
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.subtitle2">@T("Findings")</MudText>
|
||||
<MudStack Spacing="2">
|
||||
@foreach (var finding in this.audit.Findings)
|
||||
{
|
||||
var severityUi = finding.Severity switch
|
||||
{
|
||||
AssistantAuditLevel.UNKNOWN => (
|
||||
AlertStyling: "color: rgb(12,128,223); background-color: rgba(33,150,243,0.06);",
|
||||
AlertIcon: Icons.Material.Filled.QuestionMark,
|
||||
ChipColor: Color.Info
|
||||
),
|
||||
AssistantAuditLevel.DANGEROUS => (
|
||||
AlertStyling: "color: rgb(242,28,13); background-color: rgba(244,67,54,0.06);",
|
||||
AlertIcon: Icons.Material.Filled.Dangerous,
|
||||
ChipColor: Color.Error
|
||||
),
|
||||
AssistantAuditLevel.CAUTION => (
|
||||
AlertStyling: "color: rgb(214,129,0); background-color: rgba(255,152,0,0.06);",
|
||||
AlertIcon: Icons.Material.Filled.Warning,
|
||||
ChipColor: Color.Warning
|
||||
),
|
||||
AssistantAuditLevel.SAFE => (
|
||||
AlertStyling: "color: rgb(0,163,68); background-color: rgba(0,200,83,0.06);",
|
||||
AlertIcon: Icons.Material.Filled.Verified,
|
||||
ChipColor: Color.Success
|
||||
),
|
||||
_ => (
|
||||
AlertStyling: "color: rgb(12,128,223); background-color: rgba(33,150,243,0.06);",
|
||||
AlertIcon: Icons.Material.Filled.QuestionMark,
|
||||
ChipColor: Color.Info
|
||||
)
|
||||
};
|
||||
|
||||
<MudCard Class="pa-1 mud-alert mud-alert-text-error" Elevation="3" Style="@severityUi.AlertStyling">
|
||||
<MudCardContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Start" Justify="Justify.FlexStart">
|
||||
<MudElement HtmlTag="div" Class="mt-1 me-1">
|
||||
<MudIcon Icon="@severityUi.AlertIcon" Title="@finding.SeverityText" />
|
||||
</MudElement>
|
||||
<MudStack Spacing="1" Style="width: 100%">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle2">@finding.Category</MudText>
|
||||
<MudChip T="string" Variant="Variant.Text" Class="pt-n2" Size="Size.Small" Color="@severityUi.ChipColor">@finding.Severity.GetName()</MudChip>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Class="mt-n3 mb-3">@finding.Location</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mt-n2 mb-2">@finding.Description</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
@if (this.isAuditing)
|
||||
{
|
||||
<MudCard Class="pa-1 mt-4" Elevation="3" Style="width: 100%">
|
||||
<MudCardContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Start" Justify="Justify.FlexStart">
|
||||
<MudElement HtmlTag="div" Class="mt-1 me-1">
|
||||
<MudSkeleton SkeletonType="SkeletonType.Circle" Width="25px" Height="25px"/>
|
||||
</MudElement>
|
||||
<MudStack Spacing="1" Style="width: 100%">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudSkeleton SkeletonType="SkeletonType.Text" Width="50%"/>
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Width="15%" Height="25px" Class="pt-n2" Style="border-radius: 15px"/>
|
||||
</MudStack>
|
||||
<MudSkeleton SkeletonType="SkeletonType.Text" Width="25%" Class="mt-n2 mb-2"/>
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Width="100%" Height="30px"/>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<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 || this.justAudited)">
|
||||
@T("Start Security Check")
|
||||
</MudButton>
|
||||
@if (this.CanEnablePlugin)
|
||||
{
|
||||
<MudButton OnClick="@this.EnablePlugin" Variant="Variant.Filled" Color="@this.EnableButtonColor">
|
||||
@T("Enable Assistant Plugin")
|
||||
</MudButton>
|
||||
}
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,478 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class AssistantPluginAuditDialog : MSGComponentBase
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginAuditDialog).Namespace, nameof(AssistantPluginAuditDialog));
|
||||
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Parameter] public Guid PluginId { get; set; }
|
||||
|
||||
private PluginAssistants? plugin;
|
||||
private PluginAssistantAudit? audit;
|
||||
private string promptPreview = string.Empty;
|
||||
private string promptFallbackPreview = string.Empty;
|
||||
private ImmutableDictionary<string, string> luaFiles = ImmutableDictionary.Create<string, string>();
|
||||
private IReadOnlyCollection<TreeItemData<ITreeItem>> componentTreeItems = [];
|
||||
private IReadOnlyCollection<TreeItemData<ITreeItem>> fileSystemTreeItems = [];
|
||||
private CultureInfo currentCultureInfo = CultureInfo.InvariantCulture;
|
||||
private bool isAuditing;
|
||||
|
||||
private AIStudio.Settings.Provider CurrentProvider => this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_ASSISTANT_PLUGIN_AUDIT, null, true);
|
||||
|
||||
private string ProviderLabel => this.CurrentProvider == AIStudio.Settings.Provider.NONE
|
||||
? this.T("No provider configured")
|
||||
: $"{this.CurrentProvider.InstanceName} ({this.CurrentProvider.UsedLLMProvider.ToName()})";
|
||||
|
||||
private DataAssistantPluginAudit AuditSettings => this.SettingsManager.ConfigurationData.AssistantPluginAudit;
|
||||
|
||||
private AssistantAuditLevel MinimumLevel => this.SettingsManager.ConfigurationData.AssistantPluginAudit.MinimumLevel;
|
||||
|
||||
private string MinimumLevelLabel => this.MinimumLevel.GetName();
|
||||
|
||||
private bool CanRunAudit => this.plugin is not null && this.CurrentProvider != AIStudio.Settings.Provider.NONE && !this.isAuditing;
|
||||
|
||||
private bool IsAuditBelowMinimum => this.audit is not null && this.audit.Level < this.MinimumLevel;
|
||||
|
||||
private bool IsActivationBlockedBySettings => this.audit is null || this.IsAuditBelowMinimum && this.AuditSettings.BlockActivationBelowMinimum;
|
||||
|
||||
private bool RequiresActivationConfirmation => this.audit is not null && this.IsAuditBelowMinimum && !this.AuditSettings.BlockActivationBelowMinimum;
|
||||
|
||||
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.currentCultureInfo = CommonTools.DeriveActiveCultureOrInvariant(activeLanguagePlugin.IETFTag);
|
||||
|
||||
this.plugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>()
|
||||
.FirstOrDefault(x => x.Id == this.PluginId);
|
||||
if (this.plugin is not null)
|
||||
{
|
||||
this.promptPreview = await this.plugin.BuildAuditPromptPreviewAsync();
|
||||
this.promptFallbackPreview = this.plugin.BuildAuditPromptFallbackPreview();
|
||||
this.plugin.CreateAuditComponentSummary();
|
||||
this.componentTreeItems = this.CreateAuditTreeItems(this.plugin.RootComponent);
|
||||
this.fileSystemTreeItems = this.CreatePluginFileSystemTreeItems(this.plugin.PluginPath);
|
||||
this.luaFiles = this.plugin.ReadAllLuaFiles();
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private async Task RunAudit()
|
||||
{
|
||||
if (this.plugin is null || this.isAuditing)
|
||||
return;
|
||||
|
||||
this.isAuditing = true;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
this.audit = await this.AssistantPluginAuditService.RunAuditAsync(this.plugin);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isAuditing = false;
|
||||
this.justAudited = true;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseWithoutActivation()
|
||||
{
|
||||
if (this.audit is null)
|
||||
{
|
||||
this.MudDialog.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(new AssistantPluginAuditDialogResult(this.audit, false)));
|
||||
}
|
||||
|
||||
private async Task EnablePlugin()
|
||||
{
|
||||
if (this.audit is null)
|
||||
return;
|
||||
|
||||
if (this.IsActivationBlockedBySettings)
|
||||
return;
|
||||
|
||||
if (this.RequiresActivationConfirmation && !await this.ConfirmActivationBelowMinimumAsync())
|
||||
return;
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(new AssistantPluginAuditDialogResult(this.audit, true)));
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmActivationBelowMinimumAsync()
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{
|
||||
x => x.Message,
|
||||
string.Format(
|
||||
T("The assistant plugin \"{0}\" was audited with the level \"{1}\", which is below the required safety level \"{2}\". Your current settings still allow activation, but this may be unsafe. Do you really want to enable this plugin?"),
|
||||
this.plugin?.Name ?? T("Unknown plugin"),
|
||||
this.audit?.Level.GetName() ?? T("Unknown"),
|
||||
this.MinimumLevelLabel)
|
||||
},
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Potentially Dangerous Plugin"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
return dialogResult is not null && !dialogResult.Canceled;
|
||||
}
|
||||
|
||||
private Severity GetAuditResultSeverity() => this.audit?.Level switch
|
||||
{
|
||||
AssistantAuditLevel.DANGEROUS => Severity.Error,
|
||||
AssistantAuditLevel.CAUTION => Severity.Warning,
|
||||
AssistantAuditLevel.SAFE => Severity.Success,
|
||||
_ => Severity.Normal,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates the full audit tree for the assistant component hierarchy.
|
||||
/// The dialog owns this mapping because it is pure presentation logic for the audit UI.
|
||||
/// </summary>
|
||||
private IReadOnlyCollection<TreeItemData<ITreeItem>> CreateAuditTreeItems(IAssistantComponent? rootComponent)
|
||||
{
|
||||
if (rootComponent is null)
|
||||
return [];
|
||||
|
||||
return [this.CreateComponentTreeItem(rootComponent, index: 0, depth: 0)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps one assistant component into a tree node and recursively appends its value, props and child components.
|
||||
/// </summary>
|
||||
private TreeItemData<ITreeItem> CreateComponentTreeItem(IAssistantComponent component, int index, int depth)
|
||||
{
|
||||
var children = new List<TreeItemData<ITreeItem>>();
|
||||
|
||||
if (component.Props.TryGetValue("Value", out var value))
|
||||
children.Add(this.CreateValueTreeItem(TB("Value"), value, depth + 1));
|
||||
|
||||
if (component.Props.Count > 0)
|
||||
children.Add(this.CreatePropsTreeItem(component.Props, depth + 1));
|
||||
|
||||
children.AddRange(component.Children.Select((child, childIndex) =>
|
||||
this.CreateComponentTreeItem(child, childIndex, depth + 1)));
|
||||
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = depth < 2,
|
||||
Expandable = children.Count > 0,
|
||||
Value = new AssistantAuditTreeItem
|
||||
{
|
||||
Text = this.GetComponentTreeItemText(component),
|
||||
Caption = this.GetComponentTreeItemCaption(component, index),
|
||||
Icon = component.Type.GetIcon(),
|
||||
Expandable = children.Count > 0,
|
||||
},
|
||||
Children = children,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups all props of a component under a single "Props" branch to keep the component nodes compact.
|
||||
/// </summary>
|
||||
private TreeItemData<ITreeItem> CreatePropsTreeItem(IReadOnlyDictionary<string, object> props, int depth)
|
||||
{
|
||||
var children = props
|
||||
.OrderBy(prop => prop.Key, StringComparer.Ordinal)
|
||||
.Select(prop => this.CreateValueTreeItem(prop.Key, prop.Value, depth + 1))
|
||||
.ToList();
|
||||
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = depth < 2,
|
||||
Expandable = children.Count > 0,
|
||||
Value = new AssistantAuditTreeItem
|
||||
{
|
||||
Text = TB("Properties"),
|
||||
Caption = string.Format(TB("Count: {0}"), props.Count),
|
||||
Icon = Icons.Material.Filled.Code,
|
||||
Expandable = children.Count > 0,
|
||||
IsComponent = false,
|
||||
},
|
||||
Children = children,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a scalar or structured prop value into a tree node.
|
||||
/// Scalars stay on one line, while structured values recursively expose their children.
|
||||
/// </summary>
|
||||
private TreeItemData<ITreeItem> CreateValueTreeItem(string label, object? value, int depth)
|
||||
{
|
||||
var children = this.CreateValueChildren(value, depth + 1);
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = depth < 2,
|
||||
Expandable = children.Count > 0,
|
||||
Value = new AssistantAuditTreeItem
|
||||
{
|
||||
Text = label,
|
||||
Caption = children.Count == 0 ? this.FormatScalarValue(value) : this.GetStructuredValueCaption(value),
|
||||
Icon = this.GetValueIcon(value),
|
||||
Expandable = children.Count > 0,
|
||||
IsComponent = false,
|
||||
},
|
||||
Children = children,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively expands structured values for the tree.
|
||||
/// Lists, dictionaries and known DTO-style assistant values become nested tree branches.
|
||||
/// </summary>
|
||||
private List<TreeItemData<ITreeItem>> CreateValueChildren(object? value, int depth)
|
||||
{
|
||||
if (value is null || IsScalarValue(value))
|
||||
return [];
|
||||
|
||||
if (value is IDictionary dictionary)
|
||||
return this.CreateDictionaryChildren(dictionary, depth);
|
||||
|
||||
if (value is IEnumerable enumerable and not string)
|
||||
return this.CreateEnumerableChildren(enumerable, depth);
|
||||
|
||||
return this.CreateObjectChildren(value, depth);
|
||||
}
|
||||
|
||||
private List<TreeItemData<ITreeItem>> CreateDictionaryChildren(IDictionary dictionary, int depth)
|
||||
{
|
||||
var children = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
var keyText = entry.Key.ToString() ?? TB("Unknown key");
|
||||
children.Add(this.CreateValueTreeItem(keyText, entry.Value, depth));
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a tree for the plugin directory so the audit can show unexpected folders and files, while excluding irrelevant dependency folders.
|
||||
/// </summary>
|
||||
private IReadOnlyCollection<TreeItemData<ITreeItem>> CreatePluginFileSystemTreeItems(string pluginPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pluginPath) || !Directory.Exists(pluginPath))
|
||||
return [];
|
||||
|
||||
return [this.CreateDirectoryTreeItem(pluginPath, pluginPath, depth: 0)];
|
||||
}
|
||||
|
||||
private TreeItemData<ITreeItem> CreateDirectoryTreeItem(string directoryPath, string rootPath, int depth)
|
||||
{
|
||||
var childDirectories = Directory.EnumerateDirectories(directoryPath)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.Select(path => this.CreateDirectoryTreeItem(path, rootPath, depth + 1))
|
||||
.ToList();
|
||||
|
||||
var childFiles = Directory.EnumerateFiles(directoryPath)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.Select(path => this.CreateFileTreeItem(path, depth + 1))
|
||||
.ToList();
|
||||
|
||||
var children = new List<TreeItemData<ITreeItem>>(childDirectories.Count + childFiles.Count);
|
||||
children.AddRange(childDirectories);
|
||||
children.AddRange(childFiles);
|
||||
|
||||
var relativePath = Path.GetRelativePath(rootPath, directoryPath);
|
||||
var displayName = depth == 0
|
||||
? Path.GetFileName(directoryPath)
|
||||
: relativePath.Split(Path.DirectorySeparatorChar).Last();
|
||||
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expanded = depth < 2,
|
||||
Expandable = children.Count > 0,
|
||||
Value = new AssistantAuditTreeItem
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(displayName) ? directoryPath : displayName,
|
||||
Caption = depth == 0 ? TB("Plugin root") : string.Format(TB("Items: {0}"), children.Count),
|
||||
Icon = children.Count > 0 ? Icons.Material.Filled.FolderCopy : Icons.Material.Filled.Folder,
|
||||
Expandable = children.Count > 0,
|
||||
IsComponent = false,
|
||||
},
|
||||
Children = children,
|
||||
};
|
||||
}
|
||||
|
||||
private TreeItemData<ITreeItem> CreateFileTreeItem(string filePath, int depth) => new()
|
||||
{
|
||||
Expanded = depth < 2,
|
||||
Expandable = false,
|
||||
Value = new AssistantAuditTreeItem
|
||||
{
|
||||
Text = Path.GetFileName(filePath),
|
||||
Caption = string.Empty,
|
||||
Icon = GetFileIcon(filePath),
|
||||
Expandable = false,
|
||||
IsComponent = false,
|
||||
},
|
||||
};
|
||||
|
||||
private static string GetFileIcon(string filePath)
|
||||
{
|
||||
var extension = Path.GetExtension(filePath);
|
||||
return extension.ToLowerInvariant() switch
|
||||
{
|
||||
".lua" => Icons.Material.Filled.Code,
|
||||
".md" => Icons.Material.Filled.Article,
|
||||
".json" => Icons.Material.Filled.DataObject,
|
||||
".png" or ".jpg" or ".jpeg" or ".svg" or ".webp" => Icons.Material.Filled.Image,
|
||||
_ => Icons.Material.Filled.InsertDriveFile,
|
||||
};
|
||||
}
|
||||
|
||||
private List<TreeItemData<ITreeItem>> CreateEnumerableChildren(IEnumerable enumerable, int depth)
|
||||
{
|
||||
var children = new List<TreeItemData<ITreeItem>>();
|
||||
var index = 0;
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
children.Add(this.CreateValueTreeItem($"[{index}]", item, depth));
|
||||
index++;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Falls back to public instance properties for simple DTO-style values such as dropdown items.
|
||||
/// Getter failures are treated defensively, so the audit dialog never crashes because of a problematic property.
|
||||
/// </summary>
|
||||
private List<TreeItemData<ITreeItem>> CreateObjectChildren(object value, int depth)
|
||||
{
|
||||
var children = new List<TreeItemData<ITreeItem>>();
|
||||
|
||||
foreach (var property in value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
if (!property.CanRead || property.GetIndexParameters().Length != 0)
|
||||
continue;
|
||||
|
||||
object? propertyValue;
|
||||
try
|
||||
{
|
||||
propertyValue = property.GetValue(value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
propertyValue = TB("Unavailable");
|
||||
}
|
||||
|
||||
children.Add(this.CreateValueTreeItem(property.Name, propertyValue, depth));
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
private string GetComponentTreeItemText(IAssistantComponent component)
|
||||
{
|
||||
var type = component.Type.GetDisplayName();
|
||||
if (component is INamedAssistantComponent named && !string.IsNullOrWhiteSpace(named.Name))
|
||||
return $"{type}: {named.Name}";
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private string GetComponentTreeItemCaption(IAssistantComponent component, int index)
|
||||
{
|
||||
var details = new List<string> { $"#{index + 1}" };
|
||||
|
||||
if (component is IStatefulAssistantComponent stateful)
|
||||
details.Add(string.IsNullOrWhiteSpace(stateful.UserPrompt) ? TB("Prompt: empty") : TB("Prompt: set"));
|
||||
|
||||
if (component.Children.Count > 0)
|
||||
details.Add(string.Format(TB("Children: {0}"), component.Children.Count));
|
||||
|
||||
return string.Join(" | ", details);
|
||||
}
|
||||
|
||||
private static bool IsScalarValue(object value)
|
||||
{
|
||||
return value is string or bool or char or Enum
|
||||
or byte or sbyte or short or ushort or int or uint or long or ulong
|
||||
or float or double or decimal
|
||||
or DateTime or DateTimeOffset or TimeSpan or Guid;
|
||||
}
|
||||
|
||||
private string FormatScalarValue(object? value) => value switch
|
||||
{
|
||||
null => TB("null"),
|
||||
string stringValue when string.IsNullOrWhiteSpace(stringValue) => TB("empty"),
|
||||
string stringValue => stringValue,
|
||||
bool boolValue => boolValue ? "true" : "false",
|
||||
_ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty,
|
||||
};
|
||||
|
||||
private string GetStructuredValueCaption(object? value) => value switch
|
||||
{
|
||||
null => TB("null"),
|
||||
IDictionary dictionary => string.Format(TB("Entries: {0}"), dictionary.Count),
|
||||
IEnumerable enumerable when value is not string => string.Format(TB("Items: {0}"),
|
||||
enumerable.Cast<object?>().Count()),
|
||||
_ => value.GetType().Name,
|
||||
};
|
||||
|
||||
private string GetValueIcon(object? value) => value switch
|
||||
{
|
||||
null => Icons.Material.Filled.Block,
|
||||
bool => Icons.Material.Outlined.ToggleOn,
|
||||
string => Icons.Material.Outlined.Abc,
|
||||
int => Icons.Material.Filled.Numbers,
|
||||
Enum => Icons.Material.Filled.Label,
|
||||
IDictionary => Icons.Material.Filled.DataObject,
|
||||
IEnumerable when value is not string => Icons.Material.Filled.FormatListBulleted,
|
||||
_ => Icons.Material.Filled.DataArray,
|
||||
};
|
||||
|
||||
private string FormatFileTimestamp(DateTime timestamp) => CommonTools.FormatTimestampToGeneral(timestamp, this.currentCultureInfo);
|
||||
|
||||
private string FormatFileSize(long bytes)
|
||||
{
|
||||
if (bytes < BYTES_PER_KILOBYTE)
|
||||
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.currentCultureInfo, TB("{0:0.##} KB"), kilobyte);
|
||||
|
||||
var megabyte = kilobyte / BYTES_PER_KILOBYTE;
|
||||
if (megabyte < BYTES_PER_KILOBYTE)
|
||||
return string.Format(this.currentCultureInfo, TB("{0:0.##} MB"), megabyte);
|
||||
|
||||
var gigabyte = megabyte / BYTES_PER_KILOBYTE;
|
||||
return string.Format(this.currentCultureInfo, TB("{0:0.##} GB"), gigabyte);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public sealed record AssistantPluginAuditDialogResult(PluginAssistantAudit? Audit, bool ActivatePlugin);
|
||||
Reference in new issue
Block a user