mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 20:52:11 +00:00
Merged both plugin delete actions into one component
This commit is contained in:
parent
a72282ec45
commit
9cbb86e783
@ -1,92 +0,0 @@
|
|||||||
using AIStudio.Dialogs;
|
|
||||||
using AIStudio.Tools.Media;
|
|
||||||
using AIStudio.Tools.PluginSystem;
|
|
||||||
using AIStudio.Tools.Services;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
|
||||||
|
|
||||||
namespace AIStudio.Components;
|
|
||||||
|
|
||||||
public partial class AssistantPluginDeleteAction : MSGComponentBase
|
|
||||||
{
|
|
||||||
[Parameter, EditorRequired]
|
|
||||||
public IAvailablePlugin Plugin { get; set; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private IDialogService DialogService { get; init; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private PluginInstallService PluginInstallService { get; init; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private ILogger<AssistantPluginDeleteAction> Logger { get; init; } = null!;
|
|
||||||
|
|
||||||
// The type check keeps this action apart from the LocalPluginDeleteAction next to it, which
|
|
||||||
// covers every other deletable type. Both are merged into one component in a follow-up:
|
|
||||||
private bool CanDelete => this.Plugin.Type is PluginType.ASSISTANT && PluginInstallService.CanDeletePlugin(this.Plugin);
|
|
||||||
|
|
||||||
private bool IsBlockedByActiveWork => this.PluginInstallService.HasActiveAssistantWork(this.Plugin.Id);
|
|
||||||
|
|
||||||
private string Tooltip => this.IsBlockedByActiveWork
|
|
||||||
? this.T("The assistant cannot be deleted while background work is still running.")
|
|
||||||
: this.T("Delete assistant plugin");
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
this.ApplyFilters([], [ Event.ASSISTANT_SESSION_CHANGED, Event.ASSISTANT_SESSION_FINISHED ]);
|
|
||||||
this.MediaTranscriptionService.StateChanged += this.OnMediaTranscriptionStateChanged;
|
|
||||||
await base.OnInitializedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteAssistantPluginAsync()
|
|
||||||
{
|
|
||||||
if (!this.CanDelete || this.IsBlockedByActiveWork)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
|
||||||
{
|
|
||||||
{
|
|
||||||
x => x.Message,
|
|
||||||
string.Format(this.T("Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files."), this.Plugin.Name)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(this.T("Delete Assistant Plugin"), dialogParameters, DialogOptions.FULLSCREEN);
|
|
||||||
var dialogResult = await dialogReference.Result;
|
|
||||||
if (dialogResult is null || dialogResult.Canceled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var result = await this.PluginInstallService.DeletePluginAsync(this.Plugin, CancellationToken.None);
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
this.Logger.LogError("Failed to delete assistant plugin '{PluginName}' ({PluginId}) from '{PluginDirectory}' with issue '{Issue}'.", result.PluginName, result.PluginId, result.PluginDirectory, result.Issue);
|
|
||||||
await this.MessageBus.SendError(new(Icons.Material.Filled.DeleteForever, string.Format(this.T("The assistant plugin '{0}' could not be deleted: {1}"), this.Plugin.Name, result.Issue)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Check, string.Format(this.T("The '{0}' assistant plugin has been successfully removed."), result.PluginName)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMediaTranscriptionStateChanged(MediaImportOwner owner)
|
|
||||||
{
|
|
||||||
if (owner.Kind is MediaImportOwnerKind.ASSISTANT && owner.Id.EndsWith($":{this.Plugin.Id}", StringComparison.Ordinal))
|
|
||||||
_ = this.InvokeAsync(this.StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
|
||||||
{
|
|
||||||
if (triggeredEvent is Event.ASSISTANT_SESSION_CHANGED or Event.ASSISTANT_SESSION_FINISHED)
|
|
||||||
this.StateHasChanged();
|
|
||||||
|
|
||||||
return base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DisposeResources()
|
|
||||||
{
|
|
||||||
this.MediaTranscriptionService.StateChanged -= this.OnMediaTranscriptionStateChanged;
|
|
||||||
base.DisposeResources();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
@inherits MSGComponentBase
|
|
||||||
|
|
||||||
@if (this.CanDelete)
|
|
||||||
{
|
|
||||||
<MudTooltip Text="@this.Tooltip">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
|
||||||
Color="Color.Error"
|
|
||||||
Variant="Variant.Text"
|
|
||||||
Size="Size.Medium"
|
|
||||||
Disabled="@this.isDeleting"
|
|
||||||
OnClick="@this.DeleteLocalPluginAsync" />
|
|
||||||
</MudTooltip>
|
|
||||||
}
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
using AIStudio.Dialogs;
|
|
||||||
using AIStudio.Tools.PluginSystem;
|
|
||||||
using AIStudio.Tools.Services;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
|
|
||||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
|
||||||
|
|
||||||
namespace AIStudio.Components;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lets users remove a configuration or language plugin they installed or placed themselves.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Without this action, such a plugin could only be removed from the data directory by hand. That is
|
|
||||||
/// especially painful for configuration plugins, which have no activation switch at all. Plugins
|
|
||||||
/// shipped with AI Studio and plugins deployed by an organization stay untouched: the action does
|
|
||||||
/// not appear for them.
|
|
||||||
/// </remarks>
|
|
||||||
public partial class LocalPluginDeleteAction : MSGComponentBase
|
|
||||||
{
|
|
||||||
[Parameter, EditorRequired]
|
|
||||||
public IAvailablePlugin Plugin { get; set; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private IDialogService DialogService { get; init; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private PluginInstallService PluginInstallService { get; init; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private ILogger<LocalPluginDeleteAction> Logger { get; init; } = null!;
|
|
||||||
|
|
||||||
private bool isDeleting;
|
|
||||||
|
|
||||||
// The type check keeps this action apart from the AssistantPluginDeleteAction next to it, which
|
|
||||||
// covers assistants. Both are merged into one component in a follow-up:
|
|
||||||
private bool CanDelete => this.Plugin.Type is not PluginType.ASSISTANT && PluginInstallService.CanDeletePlugin(this.Plugin);
|
|
||||||
|
|
||||||
private string Tooltip => this.Plugin.Type is PluginType.CONFIGURATION
|
|
||||||
? this.T("Delete configuration plugin")
|
|
||||||
: this.T("Delete language plugin");
|
|
||||||
|
|
||||||
private async Task DeleteLocalPluginAsync()
|
|
||||||
{
|
|
||||||
if (!this.CanDelete || this.isDeleting)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!await this.ConfirmDeletionAsync())
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.isDeleting = true;
|
|
||||||
await this.InvokeAsync(this.StateHasChanged);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await this.PluginInstallService.DeletePluginAsync(this.Plugin, CancellationToken.None);
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
this.Logger.LogError("Failed to delete {PluginType} plugin '{PluginName}' ({PluginId}) from '{PluginDirectory}' with issue '{Issue}'.", this.Plugin.Type, result.PluginName, result.PluginId, result.PluginDirectory, result.Issue);
|
|
||||||
await this.MessageBus.SendError(new(Icons.Material.Filled.DeleteForever, string.Format(this.T("The plugin '{0}' could not be deleted: {1}"), this.Plugin.Name, result.Issue)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Check, string.Format(this.T("The plugin '{0}' has been successfully removed."), result.PluginName)));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.isDeleting = false;
|
|
||||||
await this.InvokeAsync(this.StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Asks the user before the deletion. A configuration gets the dialog listing its consequences,
|
|
||||||
/// because removing it also removes the providers and settings it brought. A language plugin
|
|
||||||
/// only owns its own files, so a plain confirmation is enough.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<bool> ConfirmDeletionAsync()
|
|
||||||
{
|
|
||||||
if (this.Plugin.Type is PluginType.CONFIGURATION)
|
|
||||||
{
|
|
||||||
var configurationParameters = new DialogParameters<ConfigurationPluginDeleteDialog>
|
|
||||||
{
|
|
||||||
{ x => x.PluginName, this.Plugin.Name },
|
|
||||||
{ x => x.Summary, this.PluginInstallService.BuildConfigurationDeleteSummary(this.Plugin) },
|
|
||||||
};
|
|
||||||
|
|
||||||
var configurationDialog = await this.DialogService.ShowAsync<ConfigurationPluginDeleteDialog>(this.T("Delete Configuration Plugin"), configurationParameters, DialogOptions.FULLSCREEN);
|
|
||||||
return await configurationDialog.Result is { Canceled: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
var parameters = new DialogParameters<ConfirmDialog>
|
|
||||||
{
|
|
||||||
{
|
|
||||||
x => x.Message,
|
|
||||||
string.Format(this.T("Do you really want to delete the language plugin '{0}'? This permanently deletes its local plugin files. When it is your chosen language, AI Studio returns to choosing the language automatically."), this.Plugin.Name)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var dialog = await this.DialogService.ShowAsync<ConfirmDialog>(this.T("Delete Language Plugin"), parameters, DialogOptions.FULLSCREEN);
|
|
||||||
return await dialog.Result is { Canceled: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,7 +7,7 @@
|
|||||||
Color="Color.Error"
|
Color="Color.Error"
|
||||||
Variant="Variant.Text"
|
Variant="Variant.Text"
|
||||||
Size="Size.Medium"
|
Size="Size.Medium"
|
||||||
Disabled="@this.IsBlockedByActiveWork"
|
Disabled="@(this.isDeleting || this.IsBlockedByActiveWork)"
|
||||||
OnClick="@this.DeleteAssistantPluginAsync" />
|
OnClick="@this.DeletePluginAsync" />
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
169
app/MindWork AI Studio/Components/PluginDeleteAction.razor.cs
Normal file
169
app/MindWork AI Studio/Components/PluginDeleteAction.razor.cs
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
using AIStudio.Dialogs;
|
||||||
|
using AIStudio.Tools.Media;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lets users remove a plugin they installed or placed themselves.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Without this action, such a plugin could only be removed from the data directory by hand. That is
|
||||||
|
/// especially painful for configuration plugins, which have no activation switch at all. Plugins
|
||||||
|
/// shipped with AI Studio and plugins deployed by an organization stay untouched: the action does
|
||||||
|
/// not appear for them.
|
||||||
|
/// </remarks>
|
||||||
|
public partial class PluginDeleteAction : MSGComponentBase
|
||||||
|
{
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public IAvailablePlugin Plugin { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ILogger<PluginDeleteAction> Logger { get; init; } = null!;
|
||||||
|
|
||||||
|
private bool isDeleting;
|
||||||
|
|
||||||
|
private bool IsAssistant => this.Plugin.Type is PluginType.ASSISTANT;
|
||||||
|
|
||||||
|
private bool CanDelete => PluginInstallService.CanDeletePlugin(this.Plugin);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True while an assistant still owns background work. We keep the action visible and block it
|
||||||
|
/// instead of hiding it, so that the tooltip can explain why it does nothing right now.
|
||||||
|
/// </summary>
|
||||||
|
private bool IsBlockedByActiveWork => this.IsAssistant && this.PluginInstallService.HasActiveAssistantWork(this.Plugin.Id);
|
||||||
|
|
||||||
|
private string Tooltip
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.IsBlockedByActiveWork)
|
||||||
|
return this.T("The assistant cannot be deleted while background work is still running.");
|
||||||
|
|
||||||
|
return this.Plugin.Type switch
|
||||||
|
{
|
||||||
|
PluginType.ASSISTANT => this.T("Delete assistant plugin"),
|
||||||
|
PluginType.CONFIGURATION => this.T("Delete configuration plugin"),
|
||||||
|
|
||||||
|
_ => this.T("Delete language plugin"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
// Only an assistant can be busy. We watch its sessions and transcriptions, so the action
|
||||||
|
// reflects the current state without the user reloading the page:
|
||||||
|
this.ApplyFilters([], this.IsAssistant ? [Event.ASSISTANT_SESSION_CHANGED, Event.ASSISTANT_SESSION_FINISHED] : []);
|
||||||
|
if (this.IsAssistant)
|
||||||
|
this.MediaTranscriptionService.StateChanged += this.OnMediaTranscriptionStateChanged;
|
||||||
|
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||||
|
{
|
||||||
|
if (triggeredEvent is Event.ASSISTANT_SESSION_CHANGED or Event.ASSISTANT_SESSION_FINISHED)
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
return base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DisposeResources()
|
||||||
|
{
|
||||||
|
if (this.IsAssistant)
|
||||||
|
this.MediaTranscriptionService.StateChanged -= this.OnMediaTranscriptionStateChanged;
|
||||||
|
|
||||||
|
base.DisposeResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private async Task DeletePluginAsync()
|
||||||
|
{
|
||||||
|
if (!this.CanDelete || this.isDeleting || this.IsBlockedByActiveWork)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!await this.ConfirmDeletionAsync())
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isDeleting = true;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await this.PluginInstallService.DeletePluginAsync(this.Plugin, CancellationToken.None);
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
this.Logger.LogError("Failed to delete {PluginType} plugin '{PluginName}' ({PluginId}) from '{PluginDirectory}' with issue '{Issue}'.", this.Plugin.Type, result.PluginName, result.PluginId, result.PluginDirectory, result.Issue);
|
||||||
|
await this.MessageBus.SendError(new(Icons.Material.Filled.DeleteForever, string.Format(this.T("The plugin '{0}' could not be deleted: {1}"), this.Plugin.Name, result.Issue)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Check, string.Format(this.T("The plugin '{0}' has been successfully removed."), result.PluginName)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isDeleting = false;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks the user before the deletion. A configuration gets the dialog listing its consequences,
|
||||||
|
/// because removing it also removes the providers and settings it brought. Assistants and
|
||||||
|
/// language plugins only own their own files, so a plain confirmation is enough.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ConfirmDeletionAsync()
|
||||||
|
{
|
||||||
|
if (this.Plugin.Type is PluginType.CONFIGURATION)
|
||||||
|
{
|
||||||
|
var configurationParameters = new DialogParameters<ConfigurationPluginDeleteDialog>
|
||||||
|
{
|
||||||
|
{ x => x.PluginName, this.Plugin.Name },
|
||||||
|
{ x => x.Summary, this.PluginInstallService.BuildConfigurationDeleteSummary(this.Plugin) },
|
||||||
|
};
|
||||||
|
|
||||||
|
var configurationDialog = await this.DialogService.ShowAsync<ConfigurationPluginDeleteDialog>(this.T("Delete Configuration Plugin"), configurationParameters, DialogOptions.FULLSCREEN);
|
||||||
|
return await configurationDialog.Result is { Canceled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = this.IsAssistant
|
||||||
|
? this.T("Delete Assistant Plugin")
|
||||||
|
: this.T("Delete Language Plugin");
|
||||||
|
|
||||||
|
var message = this.IsAssistant
|
||||||
|
? string.Format(this.T("Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files."), this.Plugin.Name)
|
||||||
|
: string.Format(this.T("Do you really want to delete the language plugin '{0}'? This permanently deletes its local plugin files. When it is your chosen language, AI Studio returns to choosing the language automatically."), this.Plugin.Name);
|
||||||
|
|
||||||
|
var parameters = new DialogParameters<ConfirmDialog>
|
||||||
|
{
|
||||||
|
{ x => x.Message, message },
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = await this.DialogService.ShowAsync<ConfirmDialog>(title, parameters, DialogOptions.FULLSCREEN);
|
||||||
|
return await dialog.Result is { Canceled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMediaTranscriptionStateChanged(MediaImportOwner owner)
|
||||||
|
{
|
||||||
|
if (owner.Kind is MediaImportOwnerKind.ASSISTANT && owner.Id.EndsWith($":{this.Plugin.Id}", StringComparison.Ordinal))
|
||||||
|
_ = this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -58,7 +58,7 @@
|
|||||||
<AdditionalActions>
|
<AdditionalActions>
|
||||||
@if (availablePlugin is not null)
|
@if (availablePlugin is not null)
|
||||||
{
|
{
|
||||||
<AssistantPluginDeleteAction Plugin="@availablePlugin" />
|
<PluginDeleteAction Plugin="@availablePlugin" />
|
||||||
}
|
}
|
||||||
</AdditionalActions>
|
</AdditionalActions>
|
||||||
<SecurityBadge>
|
<SecurityBadge>
|
||||||
|
|||||||
@ -138,8 +138,7 @@
|
|||||||
|
|
||||||
@if (context is IAvailablePlugin availablePlugin)
|
@if (context is IAvailablePlugin availablePlugin)
|
||||||
{
|
{
|
||||||
<AssistantPluginDeleteAction Plugin="@availablePlugin" />
|
<PluginDeleteAction Plugin="@availablePlugin" />
|
||||||
<LocalPluginDeleteAction Plugin="@availablePlugin" />
|
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user