diff --git a/app/MindWork AI Studio/Components/AssistantBlock.razor.cs b/app/MindWork AI Studio/Components/AssistantBlock.razor.cs index ff639a0c..4486ae6c 100644 --- a/app/MindWork AI Studio/Components/AssistantBlock.razor.cs +++ b/app/MindWork AI Studio/Components/AssistantBlock.razor.cs @@ -9,7 +9,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Components; -public partial class AssistantBlock : MSGComponentBase where TSettings : IComponent +public partial class AssistantBlock : MSGComponentBase, IAssistantCategoryMember where TSettings : IComponent { /// /// Describes the assistant session indicator shown on top of the assistant icon. @@ -58,6 +58,12 @@ public partial class AssistantBlock : MSGComponentBase where TSetting [Parameter] public PreviewFeatures RequiredPreviewFeature { get; set; } = PreviewFeatures.NONE; + /// + /// Gets or sets the assistant category this block belongs to, if any. + /// + [CascadingParameter] + public AssistantCategoryBlock? Category { get; set; } + [Inject] private MudTheme ColorTheme { get; init; } = null!; @@ -88,7 +94,8 @@ public partial class AssistantBlock : 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); + /// + 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 : 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 : MSGComponentBase where TSetting protected override void DisposeResources() { this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged; + this.Category?.UnregisterAssistant(this); base.DisposeResources(); } diff --git a/app/MindWork AI Studio/Components/AssistantCategoryBlock.razor b/app/MindWork AI Studio/Components/AssistantCategoryBlock.razor new file mode 100644 index 00000000..f6002b92 --- /dev/null +++ b/app/MindWork AI Studio/Components/AssistantCategoryBlock.razor @@ -0,0 +1,11 @@ +@if (this.HasVisibleAssistant) +{ + + @this.Title + +} + + + @this.ChildContent + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AssistantCategoryBlock.razor.cs b/app/MindWork AI Studio/Components/AssistantCategoryBlock.razor.cs new file mode 100644 index 00000000..a204bb66 --- /dev/null +++ b/app/MindWork AI Studio/Components/AssistantCategoryBlock.razor.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +/// +/// Renders one category of assistants together with its heading. +/// +/// +/// 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. +/// +public partial class AssistantCategoryBlock : ComponentBase +{ + private readonly HashSet members = []; + + /// + /// The heading of this category. + /// + [Parameter] + public string Title { get; set; } = string.Empty; + + /// + /// The CSS classes used for the heading. + /// + [Parameter] + public string HeaderClass { get; set; } = "mb-2 mr-3 mt-6"; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Adds an assistant block to this category. + /// + /// + /// 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. + /// + /// The assistant block which belongs to this category. + internal void RegisterAssistant(IAssistantCategoryMember member) + { + if (this.members.Add(member)) + this.StateHasChanged(); + } + + /// + /// Removes an assistant block from this category. + /// + /// The assistant block which no longer belongs to this category. + internal void UnregisterAssistant(IAssistantCategoryMember member) => this.members.Remove(member); + + /// + /// Gets whether at least one assistant of this category is visible right now. + /// + /// + /// We evaluate this live instead of caching it. That way, changes to the configuration take + /// effect as soon as the assistants page renders again. + /// + private bool HasVisibleAssistant => this.members.Any(member => member.IsVisible); + + /// + /// Gets the CSS classes used for the assistant stack. + /// + /// + /// 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. + /// + private string StackClass => this.HasVisibleAssistant ? "mb-3" : string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/IAssistantCategoryMember.cs b/app/MindWork AI Studio/Components/IAssistantCategoryMember.cs new file mode 100644 index 00000000..f4dd3033 --- /dev/null +++ b/app/MindWork AI Studio/Components/IAssistantCategoryMember.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Components; + +/// +/// Represents an assistant block which belongs to an assistant category. +/// +/// +/// 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. +/// +public interface IAssistantCategoryMember +{ + /// + /// Gets whether the assistant is visible right now. + /// + bool IsVisible { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index e8b622c9..26acd14d 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -12,36 +12,19 @@ - @if (this.SettingsManager.IsAnyCategoryAssistantVisible("General", - (Components.TEXT_SUMMARIZER_ASSISTANT, PreviewFeatures.NONE), - (Components.TRANSLATION_ASSISTANT, PreviewFeatures.NONE), - (Components.GRAMMAR_SPELLING_ASSISTANT, PreviewFeatures.NONE), - (Components.REWRITE_ASSISTANT, PreviewFeatures.NONE), - (Components.PROMPT_OPTIMIZER_ASSISTANT, PreviewFeatures.NONE), - (Components.SYNONYMS_ASSISTANT, PreviewFeatures.NONE), - (Components.META_ASSISTANT, PreviewFeatures.PRE_META_ASSISTANT_V1) - )) - { - - @T("General") - - - - - - - - - - - } + + + + + + + + + @if (this.AssistantPlugins.Count > 0) { - - @T("Installed Assistants") - - + @foreach (var assistantPlugin in this.AssistantPlugins) { var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, assistantPlugin); @@ -66,76 +49,34 @@ } - + } - @if (this.SettingsManager.IsAnyCategoryAssistantVisible("Business", - (Components.EMAIL_ASSISTANT, PreviewFeatures.NONE), - (Components.DOCUMENT_ANALYSIS_ASSISTANT, PreviewFeatures.NONE), - (Components.MY_TASKS_ASSISTANT, PreviewFeatures.NONE), - (Components.AGENDA_ASSISTANT, PreviewFeatures.NONE), - (Components.JOB_POSTING_ASSISTANT, PreviewFeatures.NONE), - (Components.LEGAL_CHECK_ASSISTANT, PreviewFeatures.NONE), - (Components.ICON_FINDER_ASSISTANT, PreviewFeatures.NONE), - (Components.SLIDE_BUILDER_ASSISTANT, PreviewFeatures.NONE), - (Components.VISUAL_BRIEFING_ASSISTANT, Components.VISUAL_BRIEFING_ASSISTANT.RequiredPreviewFeature()) - )) - { - - @T("Business") - - - - - - - - - - - - - } + + + + + + + + + + + - @if (this.SettingsManager.IsAnyCategoryAssistantVisible("Learning", - (Components.BIAS_DAY_ASSISTANT, PreviewFeatures.NONE) - )) - { - - @T("Learning") - - - - - } + + + - @if (this.SettingsManager.IsAnyCategoryAssistantVisible("Software Engineering", - (Components.CODING_ASSISTANT, PreviewFeatures.NONE), - (Components.ERI_ASSISTANT, PreviewFeatures.PRE_RAG_2024), - (Components.LOG_VIEWER_ASSISTANT, PreviewFeatures.NONE) - )) - { - - @T("Software Engineering") - - - - - - } + + + + - @if (this.SettingsManager.IsAnyCategoryAssistantVisible("AI Studio Development", - (Components.I18N_ASSISTANT, PreviewFeatures.NONE) - )) - { - - @T("AI Studio Development") - - - - - - } + + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs index aa10a0b0..51066283 100644 --- a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs +++ b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs @@ -84,21 +84,4 @@ public static class AssistantVisibilityExtensions return !isHidden; } - - /// - /// Checks if any assistant in a category should be visible. - /// - /// The settings manager to check configuration against. - /// The name of the assistant category (for logging purposes). - /// The assistants in the category with their optional preview feature requirements. - /// True if at least one assistant in the category should be visible, false otherwise. - public static bool IsAnyCategoryAssistantVisible(this SettingsManager settingsManager, string categoryName, params (Components Component, PreviewFeatures RequiredPreviewFeature)[] assistants) - { - foreach (var (component, requiredPreviewFeature) in assistants) - if (settingsManager.IsAssistantVisible(component, withLogging: false, requiredPreviewFeature: requiredPreviewFeature)) - return true; - - LOGGER.LogInformation("No assistants in category '{CategoryName}' are visible.", categoryName); - return false; - } } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index 9896d9c9..fc42ee57 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -18,4 +18,5 @@ - Fixed withdrawing a configuration your organization deployed. A configuration that declared itself as locally managed stayed on the device even after the IT department stopped deploying it, and it kept every right of an organization configuration, such as approving assistant plugins. Where a configuration is stored now decides this instead of what the configuration says about itself, so withdrawing one always takes effect. This also applies to a device that was offline while the organization changed its policy: the withdrawal is applied when AI Studio starts again. - Fixed preview features contributed by several configuration plugins at once. Only the most recent contribution was recognized as coming from your organization, so features enabled by another configuration looked as if you had switched them on yourself. Each configuration is now tracked separately, which lets your organization enable one preview feature company-wide and another one for a single department. - Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log. +- Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it. - Upgraded dependencies to their latest versions to improve security and stability. \ No newline at end of file