using System.Globalization; using System.Reflection; using AIStudio.Components; using AIStudio.Dialogs; using AIStudio.Tools.Metadata; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Assistants.BeyondFactsLab; /// /// Lets the person share a session of the BeyondFacts Lab with its research team: anonymized, checked in a preview /// which the person may edit, exported as a file, and sent by mail from the mail program of the person. /// /// /// Nothing of the dialog is stored. The lab suggests the names it knows each time, and the person enters their own /// name anew; the session itself stays as it is. /// public partial class BflShareDialog : MSGComponentBase { [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; [Inject] private BeyondFactsLabStore Store { get; init; } = null!; [Inject] private BflConversationService ConversationService { get; init; } = null!; [Inject] private RustService RustService { get; init; } = null!; [Inject] private AIGeneratedContentDisclosureService DisclosureService { get; init; } = null!; [Inject] private IDialogService DialogService { get; init; } = null!; [Inject] private ILogger Logger { get; init; } = null!; /// /// Gets or sets the ID of the preparation to share. /// [Parameter] public Guid ProjectId { get; set; } /// /// Gets or sets whether the lab asks right after the person finished the last step, so that it thanks them first. /// [Parameter] public bool AfterCompletion { get; set; } /// /// The version of AI Studio, which goes into the session, since the prompts of the lab change with it. /// private static readonly string APP_VERSION = Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "unknown"; /// /// The format of every shared session, so that the research team gets all of them in the same one. /// /// /// Markdown needs no Pandoc, so that everybody can share a session. The research team may ask for another format, /// but only one which AI Studio writes itself, see FileExportFormatExtensions.IsPlainText. /// private const FileExportFormat EXPORT_FORMAT = FileExportFormat.MARKDOWN; /// /// Gets the culture of the active language plugin, for numbers. /// private static CultureInfo Culture => Tools.PluginSystem.I18N.I.Culture; /// /// Gets the link which opens the mail to the research team. /// private string MailUri { get; } = BflShareMail.CreateUri(); /// /// What the session is put together from, or null when the preparation cannot be opened. /// private BflShareInput? source; /// /// Whether the dialog still reads the preparation. /// private bool isLoading = true; /// /// The name of the person, which the session should not contain. /// private string ownName = string.Empty; /// /// The names of other people, one per line, which the session should not contain. /// private string otherNames = string.Empty; /// /// Further terms, one per line, which the session should not contain. /// private string terms = string.Empty; /// /// Whether personal disclosures go into the session. /// private bool includePersonalDisclosures; /// /// The anonymized session as the person sees it in the preview, with what was replaced; null before the first /// preview. /// private BflAnonymization? preview; /// /// Whether the person changed the preview, which a new preview would discard. /// private bool previewEdited; /// /// Whether an export runs. /// private bool isExporting; /// protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); try { var forms = await this.Store.LoadFormsAsync(this.ProjectId); var workStates = await this.Store.LoadWorkStatesAsync(this.ProjectId); var results = await this.Store.LoadResultsAsync(this.ProjectId); var research = await this.Store.LoadAllResearchAsync(this.ProjectId); var materials = (await this.Store.ListMaterialsAsync(this.ProjectId)).Select(entry => entry.Material).OfType().ToList(); var conversations = new Dictionary>(); foreach (var step in BflStep.All) { if ((await this.ConversationService.GetAsync(new BflStepThreadLocation(this.ProjectId, step))).Thread is { } thread) conversations[step] = BflReflectionFolder.MessagesOf(thread); } var researchTopics = research.Where(entry => !string.IsNullOrWhiteSpace(entry.Value.Text)).Select(entry => entry.Key).ToList(); this.source = new BflShareInput(forms, workStates, results, conversations, materials, researchTopics, false, APP_VERSION, DateTimeOffset.UtcNow, Culture); this.otherNames = string.Join('\n', BflShareNames.Suggest(forms, workStates, research.GetValueOrDefault(BflResearchTopic.PARTICIPANTS))); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) { this.Logger.LogError(exception, "Could not read the preparation '{ProjectId}' of the BeyondFacts Lab to share it.", this.ProjectId); this.source = null; } finally { this.isLoading = false; } } /// /// Puts the session together and anonymizes it with the names and terms the person listed; asks first when that /// would discard changes of the person in the preview. /// private async Task CreatePreviewAsync() { if (this.source is null) return; if (this.previewEdited) { var parameters = new DialogParameters { { dialog => dialog.Message, TA("Create the preview anew? Your changes in the preview will be lost.") }, }; var reference = await this.DialogService.ShowAsync(T("Create preview anew"), parameters, DialogOptions.FULLSCREEN); var answer = await reference.Result; if (answer is null || answer.Canceled) return; } var session = BflShareModelBuilder.Build(this.source with { IncludePersonalDisclosures = this.includePersonalDisclosures, SharedAtUtc = DateTimeOffset.UtcNow }); this.preview = this.Anonymize(session); this.previewEdited = false; } /// /// Anonymizes the preview once more after the person changed it, so that a name the person typed goes as well. /// /// The preview as the person changed it. private void PreviewChanged(string? text) { if (this.preview is null) return; this.preview = this.preview.CombinedWith(this.Anonymize(text ?? string.Empty)); this.previewEdited = true; } /// /// Exports the preview in the one format the research team gets, anonymized once more with the names and terms as /// they stand now, and with the notice that AI generated it when the person or the organization switched that on. /// private async Task ExportAsync() { if (this.preview is null) return; this.preview = this.preview.CombinedWith(this.Anonymize(this.preview.Text)); var fileName = $"{T("BeyondFacts Lab")} - {T("Shared session")}"; this.isExporting = true; try { await PlainFileExport.ToFile(this.RustService, T("Share with the research team"), EXPORT_FORMAT, this.DisclosureService.AddDisclosureToMarkdown(this.preview.Text), fileName); } finally { this.isExporting = false; } } /// /// Anonymizes a text with the names and terms the person listed, the own name first. /// /// The text. private BflAnonymization Anonymize(string text) => BflAnonymizer.Anonymize(text, [this.ownName, .. LinesOf(this.otherNames)], LinesOf(this.terms)); /// /// Gets the entries of a field with one entry per line. /// /// The text of the field. private static IReadOnlyList LinesOf(string text) => text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); private void Close() => this.MudDialog.Close(DialogResult.Ok(true)); }