using AIStudio.Components; using AIStudio.Dialogs.Settings; using Microsoft.AspNetCore.Components; using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Assistants.BeyondFactsLab; /// /// The page of the BeyondFacts Lab, which guides researchers through the preparation of a challenging appearance. /// /// /// The lab keeps several preparations, each with steps of their own, so it does not inherit AssistantBase, /// which knows one input and one result. Like the Visual Briefing assistant, it checks by itself whether it /// may be opened. /// public partial class BeyondFactsLabAssistant : MSGComponentBase { [Inject] private IDialogService DialogService { get; init; } = null!; [Inject] private NavigationManager NavigationManager { get; init; } = null!; [Inject] private BeyondFactsLabStore Store { get; init; } = null!; [Inject] private BflConversationService ConversationService { get; init; } = null!; [Inject] private BflMaterialService MaterialService { get; init; } = null!; [Inject] private BflWorkStateGenerator WorkStateGenerator { get; init; } = null!; [Inject] private ILogger Logger { get; init; } = null!; [Inject] private IJSRuntime JsRuntime { get; init; } = null!; /// /// Whether the person may open the lab, so that the page did not send them away. /// private bool mayOpen; /// /// The empty element right before the header of the preparation, which marks where the header sits in the flow. /// private ElementReference stickyHeaderAnchor; /// /// The header of the preparation, which stays at the top while the person scrolls. /// private ElementReference stickyHeader; /// /// The ID of the header element whose height the page follows, so that only a new header element needs following. /// private string? followedStickyHeaderId; /// /// Whether the next render shows another step, which then starts at the top. /// private bool scrollToActiveStep; /// /// Whether the area which tells how to work with the lab is open. /// private bool introductionExpanded; /// protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); if (!BflAccess.CanOpen(this.SettingsManager, T("BeyondFacts Lab"))) { this.NavigationManager.NavigateTo(Routes.ASSISTANTS); return; } this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]); this.MaterialService.MaterialsChanged += this.OnMaterialsChangedAsync; this.WorkStateGenerator.WorkStateChanged += this.OnWorkStateChangedAsync; this.mayOpen = true; await this.RunStoreActionAsync(() => this.ReloadProjectsAsync(null)); // Whoever has no preparation yet needs to know how the lab works first: this.introductionExpanded = this.projects.Count is 0; } /// protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (this.scrollToActiveStep) { // The person scrolled down in one step and chose another one; it starts right under the header: this.scrollToActiveStep = false; await this.stickyHeaderAnchor.ScrollToStickyHeaderAsync(this.JsRuntime); } // // The material sticks right below the header, so the page has to know how tall the header is. Opening a // preparation after none, or after one which is unavailable, renders a new header element: // if (this.stickyHeader.Id is { } headerId && headerId != this.followedStickyHeaderId) { this.followedStickyHeaderId = headerId; await this.stickyHeader.FollowStickyHeaderAsync(this.JsRuntime); } if (firstRender && this.mayOpen) await this.ShowNoticeIfNeededAsync(); } /// /// Shows the notes of the lab, unless the person confirmed them as they are now. /// private async Task ShowNoticeIfNeededAsync() { var notice = BflNotice.Text; var currentHash = BflNotice.ComputeHash(notice); if (BflNotice.IsAccepted(this.SettingsManager.ConfigurationData.BeyondFactsLab.AcceptedNoticeHash, currentHash)) return; var parameters = new DialogParameters { { dialog => dialog.Notice, notice } }; var reference = await this.DialogService.ShowAsync(T("BeyondFacts Lab"), parameters, DialogOptions.BLOCKING_FULLSCREEN); var result = await reference.Result; if (result is null || result.Canceled) return; this.SettingsManager.ConfigurationData.BeyondFactsLab.AcceptedNoticeHash = currentHash; await this.SettingsManager.StoreSettings(); } /// protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default { if (triggeredEvent is Event.CONFIGURATION_CHANGED) { // An organization may hide the lab, or the person may switch its preview feature off, while the page is open: if (!BflAccess.CanOpen(this.SettingsManager)) { this.NavigationManager.NavigateTo(Routes.ASSISTANTS); return; } // A changed option, such as the form of address in the options of the lab, shows right away: this.StateHasChanged(); } await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data); } /// protected override void DisposeResources() { this.MaterialService.MaterialsChanged -= this.OnMaterialsChangedAsync; this.WorkStateGenerator.WorkStateChanged -= this.OnWorkStateChangedAsync; base.DisposeResources(); } /// /// Shows a preparation as changed once its material changed: material is work on the preparation. /// /// The ID of the preparation whose material changed. private Task OnMaterialsChangedAsync(Guid projectId) { if (this.projects.All(project => project.ProjectId != projectId)) return Task.CompletedTask; return this.InvokeAsync(async () => { if (await this.Store.LoadAsync(projectId) is not { } project) return; this.ReplaceProject(project); this.StateHasChanged(); }); } /// /// Remembers whether the person opened or closed the area which tells how to work with the lab. /// /// Whether the area is open now. private Task IntroductionExpandedChanged(bool isExpanded) { this.introductionExpanded = isExpanded; return Task.CompletedTask; } private async Task OpenSettingsDialogAsync() => await this.DialogService.ShowAsync(null, new DialogParameters(), DialogOptions.FULLSCREEN); }