Added an assistant category block which derives its heading from the visible assistants

This commit is contained in:
Thorsten Sommer 2026-08-09 19:47:46 +02:00
parent 6e143aafaa
commit 628be073b8
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
4 changed files with 108 additions and 2 deletions

View File

@ -9,7 +9,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components;
public partial class AssistantBlock<TSettings> : MSGComponentBase where TSettings : IComponent
public partial class AssistantBlock<TSettings> : MSGComponentBase, IAssistantCategoryMember where TSettings : IComponent
{
/// <summary>
/// Describes the assistant session indicator shown on top of the assistant icon.
@ -58,6 +58,12 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
[Parameter]
public PreviewFeatures RequiredPreviewFeature { get; set; } = PreviewFeatures.NONE;
/// <summary>
/// Gets or sets the assistant category this block belongs to, if any.
/// </summary>
[CascadingParameter]
public AssistantCategoryBlock? Category { get; set; }
[Inject]
private MudTheme ColorTheme { get; init; } = null!;
@ -88,7 +94,8 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
private string BlockStyle => $"border-width: 3px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;";
private bool IsVisible => this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Name, requiredPreviewFeature: this.RequiredPreviewFeature);
/// <inheritdoc />
public bool IsVisible => this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Name, requiredPreviewFeature: this.RequiredPreviewFeature);
private bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel);
@ -153,6 +160,7 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
protected override async Task OnInitializedAsync()
{
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
this.Category?.RegisterAssistant(this);
await base.OnInitializedAsync();
}
@ -165,6 +173,7 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
protected override void DisposeResources()
{
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
this.Category?.UnregisterAssistant(this);
base.DisposeResources();
}

View File

@ -0,0 +1,11 @@
@if (this.HasVisibleAssistant)
{
<MudText Typo="Typo.h4" Class="@this.HeaderClass">
@this.Title
</MudText>
}
<CascadingValue Value="this" IsFixed="@true">
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="@this.StackClass">
@this.ChildContent
</MudStack>
</CascadingValue>

View File

@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
/// <summary>
/// Renders one category of assistants together with its heading.
/// </summary>
/// <remarks>
/// The heading is derived from the assistant blocks inside this category: it is rendered only when
/// at least one of them is visible. Thus, hiding assistants by configuration can never leave an
/// empty category heading behind.
/// </remarks>
public partial class AssistantCategoryBlock : ComponentBase
{
private readonly HashSet<IAssistantCategoryMember> members = [];
/// <summary>
/// The heading of this category.
/// </summary>
[Parameter]
public string Title { get; set; } = string.Empty;
/// <summary>
/// The CSS classes used for the heading.
/// </summary>
[Parameter]
public string HeaderClass { get; set; } = "mb-2 mr-3 mt-6";
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// Adds an assistant block to this category.
/// </summary>
/// <remarks>
/// Assistant blocks call this while they initialize, i.e. after this category was rendered for
/// the first time. Hence, we have to render again to show the heading.
/// </remarks>
/// <param name="member">The assistant block which belongs to this category.</param>
internal void RegisterAssistant(IAssistantCategoryMember member)
{
if (this.members.Add(member))
this.StateHasChanged();
}
/// <summary>
/// Removes an assistant block from this category.
/// </summary>
/// <param name="member">The assistant block which no longer belongs to this category.</param>
internal void UnregisterAssistant(IAssistantCategoryMember member) => this.members.Remove(member);
/// <summary>
/// Gets whether at least one assistant of this category is visible right now.
/// </summary>
/// <remarks>
/// We evaluate this live instead of caching it. That way, changes to the configuration take
/// effect as soon as the assistants page renders again.
/// </remarks>
private bool HasVisibleAssistant => this.members.Any(member => member.IsVisible);
/// <summary>
/// Gets the CSS classes used for the assistant stack.
/// </summary>
/// <remarks>
/// The stack must be rendered even when no assistant is visible, because the assistant blocks
/// register themselves while rendering. Without any visible assistant, we drop the margin so
/// that a hidden category leaves no gap behind.
/// </remarks>
private string StackClass => this.HasVisibleAssistant ? "mb-3" : string.Empty;
}

View File

@ -0,0 +1,16 @@
namespace AIStudio.Components;
/// <summary>
/// Represents an assistant block which belongs to an assistant category.
/// </summary>
/// <remarks>
/// Assistant blocks are generic over their settings dialog. This interface gives the category block
/// access to their visibility without the need to know that type parameter.
/// </remarks>
public interface IAssistantCategoryMember
{
/// <summary>
/// Gets whether the assistant is visible right now.
/// </summary>
bool IsVisible { get; }
}