using System.Globalization; using AIStudio.Components; using Microsoft.AspNetCore.Components; namespace AIStudio.Assistants.BeyondFactsLab; /// /// The work state of one step of the BeyondFacts Lab: the draft the lab writes from the conversation, the work state /// the person confirmed, and the suggestions for the empty fields of the form. /// /// /// The component changes the work state it gets and stores it; the page keeps the work states of all steps, since the /// status of a step depends on the earlier ones. The model writes in BflWorkStateGenerator, so that the work goes on /// while the person looks elsewhere; the page renders whenever the generator says a work state changed. /// public partial class BflWorkStatePanel : MSGComponentBase { [Inject] private BeyondFactsLabStore Store { get; init; } = null!; [Inject] private BflWorkStateGenerator Generator { get; init; } = null!; [Inject] private ILogger Logger { get; init; } = null!; /// /// Gets or sets the step whose work state the component shows. /// [Parameter, EditorRequired] public BflStepThreadLocation Location { get; set; } = null!; /// /// Gets or sets the work states of the preparation, by their steps; a step which is missing has a work state this /// version cannot open. /// [Parameter, EditorRequired] public IReadOnlyDictionary WorkStates { get; set; } = new Dictionary(); /// /// Gets or sets the forms of the preparation, by their steps; a step which is missing has a form this version /// cannot open. /// [Parameter, EditorRequired] public IReadOnlyDictionary Forms { get; set; } = new Dictionary(); /// /// Gets or sets the callback raised after the person took over a suggestion into the form of the step, which the /// parent then stores. /// [Parameter] public EventCallback FormChanged { get; set; } /// /// Gets or sets the callback raised after the work state was stored, with the preparation and its new time of /// change. /// [Parameter] public EventCallback WorkStateStored { get; set; } /// /// Gets or sets the provider which writes the work state. /// [Parameter, EditorRequired] public AIStudio.Settings.Provider Provider { get; set; } = Settings.Provider.NONE; /// /// Gets or sets why the provider cannot write, or null when it can. /// [Parameter] public string? ProviderIssue { get; set; } private static CultureInfo Culture => Tools.PluginSystem.I18N.I.Culture; /// /// Whether the component stores the work state right now. /// private bool isSaving; private BflStep Step => this.Location.Step; private BflWorkState? WorkState => this.WorkStates.GetValueOrDefault(this.Step); private BflStepFormData? Form => this.Forms.GetValueOrDefault(this.Step); private IReadOnlyList Sections => BflWorkStateSections.ForStep(this.Step); private BflWorkStateStatus Status => BflStaleness.Of(this.Step, this.WorkStates, this.Forms); private bool IsGenerating => this.Generator.IsRunning(this.Location); private bool IsBusy => this.isSaving || this.IsGenerating; /// /// Gets the suggestions for fields which are still empty, in the order the form shows the fields. /// private IReadOnlyList> VisibleSuggestions { get { if (this.WorkState is not { } workState || this.Form is not { } form || workState.FieldSuggestions.Count is 0) return []; return [.. BflMarkdownSections.SuggestableFields(this.Step, form) .Where(definition => workState.FieldSuggestions.ContainsKey(definition.Id)) .Select(definition => new KeyValuePair(definition, workState.FieldSuggestions[definition.Id]))]; } } private Task GenerateAsync() => this.Generator.GenerateAsync(this.Location, this.Provider); private Task CancelAsync() => this.Generator.CancelAsync(this.Location); private async Task SetDraftAsync(string key, string? value) { if (this.WorkState is not { Draft: { } draft } workState || string.Equals(draft.GetValueOrDefault(key, string.Empty), value ?? string.Empty, StringComparison.Ordinal)) return; draft[key] = value ?? string.Empty; await this.SaveAsync(workState); } private async Task ConfirmDraftAsync() { if (this.WorkState is not { Draft: { } draft } workState) return; this.Confirm(workState, draft); await this.SaveAsync(workState); } private async Task ConfirmAgainAsync() { if (this.WorkState is not { Confirmed: { } confirmed } workState) return; this.Confirm(workState, confirmed.Sections); await this.SaveAsync(workState); } private void Confirm(BflWorkState workState, IReadOnlyDictionary sections) { // A form this version cannot open has no fingerprint; its changes then do not count, see BflStaleness: var fingerprint = this.Form is { } form ? BflStaleness.FingerprintOf(form) : string.Empty; workState.Confirm(sections, BflStaleness.UpstreamRevisionsOf(this.Step, this.WorkStates), fingerprint, DateTimeOffset.UtcNow); } private async Task DiscardDraftAsync() { if (this.WorkState is not { } workState) return; workState.Draft = null; await this.SaveAsync(workState); } private async Task EditAsync() { if (this.WorkState is not { Confirmed: { } confirmed } workState) return; workState.Draft = this.DraftOf(confirmed.Sections); await this.SaveAsync(workState); } private async Task WithdrawAsync() { if (this.WorkState is not { Confirmed: { } confirmed } workState) return; // The text stays as a draft, so that withdrawing loses nothing: workState.Draft ??= this.DraftOf(confirmed.Sections); workState.Withdraw(); await this.SaveAsync(workState); } private async Task TakeOverSuggestionAsync(BflFieldDefinition field, string value) { if (this.WorkState is not { } workState || this.Form is not { } form) return; form.Set(field.Id, value.Length > field.Kind.MaxLength ? value[..field.Kind.MaxLength] : value); await this.FormChanged.InvokeAsync(); workState.FieldSuggestions.Remove(field.Id); await this.SaveAsync(workState); } private async Task DismissSuggestionAsync(BflFieldDefinition field) { if (this.WorkState is not { } workState) return; workState.FieldSuggestions.Remove(field.Id); await this.SaveAsync(workState); } /// /// Gets a draft with every section of the step, filled with the texts given. /// /// The texts, by the keys of their sections. private Dictionary DraftOf(IReadOnlyDictionary sections) => this.Sections.ToDictionary(section => section.Key, section => sections.GetValueOrDefault(section.Key, string.Empty), StringComparer.Ordinal); private async Task SaveAsync(BflWorkState workState) { this.isSaving = true; try { var project = await this.Store.SaveWorkStateAsync(this.Location.ProjectId, this.Step, workState); await this.WorkStateStored.InvokeAsync(project); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) { this.Logger.LogError(exception, "Could not save the work state of a step in the BeyondFacts Lab."); await this.MessageBus.SendError(new(Icons.Material.Filled.SaveAs, TA("AI Studio could not save the work state. Please try again."))); } finally { this.isSaving = false; } } }