mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-10 23:47:21 +00:00
224 lines
8.3 KiB
C#
224 lines
8.3 KiB
C#
using System.Globalization;
|
|
|
|
using AIStudio.Components;
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
namespace AIStudio.Assistants.BeyondFactsLab;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
public partial class BflWorkStatePanel : MSGComponentBase
|
|
{
|
|
[Inject]
|
|
private BeyondFactsLabStore Store { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private BflWorkStateGenerator Generator { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private ILogger<BflWorkStatePanel> Logger { get; init; } = null!;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the step whose work state the component shows.
|
|
/// </summary>
|
|
[Parameter, EditorRequired]
|
|
public BflStepThreadLocation Location { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[Parameter, EditorRequired]
|
|
public IReadOnlyDictionary<BflStep, BflWorkState> WorkStates { get; set; } = new Dictionary<BflStep, BflWorkState>();
|
|
|
|
/// <summary>
|
|
/// Gets or sets the forms of the preparation, by their steps; a step which is missing has a form this version
|
|
/// cannot open.
|
|
/// </summary>
|
|
[Parameter, EditorRequired]
|
|
public IReadOnlyDictionary<BflStep, BflStepFormData> Forms { get; set; } = new Dictionary<BflStep, BflStepFormData>();
|
|
|
|
/// <summary>
|
|
/// Gets or sets the callback raised after the person took over a suggestion into the form of the step, which the
|
|
/// parent then stores.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback FormChanged { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the callback raised after the work state was stored, with the preparation and its new time of
|
|
/// change.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<BflProject> WorkStateStored { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the provider which writes the work state.
|
|
/// </summary>
|
|
[Parameter, EditorRequired]
|
|
public AIStudio.Settings.Provider Provider { get; set; } = Settings.Provider.NONE;
|
|
|
|
/// <summary>
|
|
/// Gets or sets why the provider cannot write, or null when it can.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? ProviderIssue { get; set; }
|
|
|
|
private static CultureInfo Culture => Tools.PluginSystem.I18N.I.Culture;
|
|
|
|
/// <summary>
|
|
/// Whether the component stores the work state right now.
|
|
/// </summary>
|
|
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<BflWorkStateSection> 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;
|
|
|
|
/// <summary>
|
|
/// Gets the suggestions for fields which are still empty, in the order the form shows the fields.
|
|
/// </summary>
|
|
private IReadOnlyList<KeyValuePair<BflFieldDefinition, string>> 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<BflFieldDefinition, string>(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<string, string> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a draft with every section of the step, filled with the texts given.
|
|
/// </summary>
|
|
/// <param name="sections">The texts, by the keys of their sections.</param>
|
|
private Dictionary<string, string> DraftOf(IReadOnlyDictionary<string, string> 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;
|
|
}
|
|
}
|
|
} |