using System.Globalization; using AIStudio.Dialogs; using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Assistants.BeyondFactsLab; public partial class BeyondFactsLabAssistant { /// /// The preparations, the one changed last first. /// private IReadOnlyList projects = []; /// /// The preparation shown, or null when there is none. /// private BflProjectEntry? selectedProject; /// /// Whether a change of the preparations is running. /// private bool isBusy; /// /// The IDs of the preparations which lack key details. /// private HashSet projectsWithMissingDetails = []; /// /// Gets the culture of the active language plugin, for dates. /// private static CultureInfo Culture => Tools.PluginSystem.I18N.I.Culture; /// /// Reads the preparations again and chooses the one to show. /// /// The preparation to show, or null to keep the one shown so far. private async Task ReloadProjectsAsync(Guid? preferredId) { this.projects = await this.Store.ListProjectsAsync(); var withMissingDetails = new HashSet(); foreach (var project in this.projects.Where(project => project.IsAvailable)) { if (BflMissingDetails.StepsOf(await this.Store.LoadFormsAsync(project.ProjectId)).Count > 0) withMissingDetails.Add(project.ProjectId); } this.projectsWithMissingDetails = withMissingDetails; await this.ShowProjectAsync(BflProjectSelection.Choose(this.projects, preferredId ?? this.selectedProject?.ProjectId, this.Store.LastSelectedProjectId)); } /// /// Shows the preparation the person selected and remembers it for the next time. /// /// The ID of the preparation. private async Task SelectProjectAsync(Guid projectId) { if (this.selectedProject?.ProjectId == projectId) return; var project = this.projects.FirstOrDefault(candidate => candidate.ProjectId == projectId); if (project is null) return; await this.RunStoreActionAsync(async () => { await this.ShowProjectAsync(project); await this.Store.RememberSelectionAsync(projectId); }); } /// /// Asks for a name and creates a preparation. /// private async Task CreateProjectAsync() { var name = await this.AskForNameAsync(T("New preparation"), T("Name the event you want to prepare for, e.g., the title of a panel discussion."), string.Empty, T("Create")); if (name is null) return; await this.RunStoreActionAsync(async () => { var project = await this.Store.CreateAsync(name); await this.Store.RememberSelectionAsync(project.ProjectId); await this.ReloadProjectsAsync(project.ProjectId); }); } /// /// Asks for a new name and renames the preparation shown. /// private async Task RenameProjectAsync() { if (this.selectedProject?.Project is not { } project) return; var name = await this.AskForNameAsync(T("Rename preparation"), T("Enter a new name for this preparation."), project.Name, T("Rename")); if (name is null) return; await this.RunStoreActionAsync(async () => { await this.Store.RenameAsync(project.ProjectId, name); await this.ReloadProjectsAsync(project.ProjectId); }); } /// /// Deletes the preparation shown, after the person confirmed it. /// private async Task DeleteProjectAsync() { if (this.selectedProject is not { } project) return; var parameters = new DialogParameters { { dialog => dialog.Message, project.IsAvailable ? string.Format(T("Permanently delete the preparation '{0}' with all of its steps, chats, and work states?"), project.Name) : T("This preparation cannot be opened. Permanently delete it with all of its files?") }, }; var reference = await this.DialogService.ShowAsync(T("Delete preparation permanently"), parameters, DialogOptions.FULLSCREEN); var result = await reference.Result; if (result is null || result.Canceled) return; await this.RunStoreActionAsync(async () => { await this.Store.DeleteAsync(project.ProjectId); this.selectedProject = null; await this.ReloadProjectsAsync(null); }); } /// /// Marks the event of the preparation shown as having taken place, or takes that back. /// /// Whether the event has taken place. private async Task SetHappenedAsync(bool happened) { if (this.selectedProject?.Project is not { } project) return; await this.RunStoreActionAsync(async () => { await this.Store.SetHappenedAsync(project.ProjectId, happened ? DateTimeOffset.UtcNow : null); await this.ReloadProjectsAsync(project.ProjectId); }); } /// /// Asks for the name of a preparation. /// /// The title of the dialog. /// What the dialog asks for. /// The name to start with. /// The text of the button which confirms. /// The name entered, or null when the person canceled. private async Task AskForNameAsync(string title, string message, string currentName, string confirmText) { var parameters = new DialogParameters { { dialog => dialog.Message, message }, { dialog => dialog.InputHeaderText, T("Name of the preparation") }, { dialog => dialog.UserInput, currentName }, { dialog => dialog.ConfirmText, confirmText }, { dialog => dialog.ConfirmColor, Color.Primary }, { dialog => dialog.AllowEmptyInput, false }, { dialog => dialog.EmptyInputErrorMessage, T("Please enter a name.") }, }; var reference = await this.DialogService.ShowAsync(title, parameters, DialogOptions.FULLSCREEN); var result = await reference.Result; if (result is null || result.Canceled || result.Data is not string name || string.IsNullOrWhiteSpace(name)) return null; return name; } /// /// Runs a change of the preparations, and tells the person when it fails. /// /// The change. private async Task RunStoreActionAsync(Func action) { this.isBusy = true; try { await action(); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException or ArgumentException) { this.Logger.LogError(exception, "A change of the preparations of the BeyondFacts Lab failed."); await this.MessageBus.SendError(new(Icons.Material.Filled.RecordVoiceOver, T("AI Studio could not save the change of your preparations. Please try again."))); await this.ReloadAfterFailureAsync(); } finally { this.isBusy = false; } } /// /// Reads the preparations again after a change failed, so the page shows what is stored. /// private async Task ReloadAfterFailureAsync() { try { await this.ReloadProjectsAsync(null); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { this.Logger.LogError(exception, "The preparations of the BeyondFacts Lab could not be read."); } } /// /// Gets the icon of a preparation in the list. /// /// The preparation. private static string ProjectIcon(BflProjectEntry project) => project switch { { IsAvailable: false } => Icons.Material.Filled.WarningAmber, { Project.HappenedAtUtc: not null } => Icons.Material.Filled.EventAvailable, _ => Icons.Material.Filled.Event, }; /// /// Gets the name of a preparation in the list, also when it cannot be read. /// /// The preparation. private string ProjectDisplayName(BflProjectEntry project) => string.IsNullOrWhiteSpace(project.Name) ? T("Unavailable preparation") : project.Name; /// /// Gets the line below the name of a preparation in the list: when it changed, and whether its event has taken place. /// /// The preparation. private string ProjectCaption(BflProjectEntry project) { var changed = string.Format(Culture, T("Changed on {0}"), project.ModifiedAtUtc.ToLocalTime().ToString("g", Culture)); return project.Project?.HappenedAtUtc is null ? changed : $"{changed} ยท {T("The event has taken place")}"; } /// /// Gets the short reason why a preparation cannot be opened. /// /// The status of the preparation. private string ProjectStatusName(BflProjectLoadStatus status) => status switch { BflProjectLoadStatus.NEWER_VERSION => T("Requires a newer AI Studio version"), _ => T("Cannot be opened"), }; /// /// Explains why a preparation cannot be opened. /// /// The status of the preparation. private string ProjectRecoveryMessage(BflProjectLoadStatus status) => status switch { BflProjectLoadStatus.NEWER_VERSION => T("A newer version of AI Studio stored this preparation. This version cannot open it. Please update AI Studio."), _ => T("AI Studio cannot read this preparation. Its files may be damaged."), }; }