using AIStudio.Chat.Embedded; using AIStudio.Components; using AIStudio.Dialogs; using AIStudio.Dialogs.Settings; using Microsoft.AspNetCore.Components; using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Assistants.ModelComparison; /// /// The mode of the Model Comparison assistant in which the user works on real tasks with both models, one after the /// other, and decides which chat helped more. /// /// /// The page shows one of two views: the list of the comparison series or an opened series. A new series comes from /// ModelComparisonSeriesDialog. A series lives in ModelComparisonStore, so the user can continue it after days or /// weeks. /// public partial class AssistantModelComparisonTasks : MSGComponentBase { [Inject] private NavigationManager NavigationManager { get; init; } = null!; [Inject] private ModelComparisonStore ModelComparisonStore { get; init; } = null!; [Inject] private EmbeddedChatService ChatService { get; init; } = null!; [Inject] private IDialogService DialogService { get; init; } = null!; [Inject] private ILogger Logger { get; init; } = null!; /// /// All comparison series, or null while they load. /// private IReadOnlyList? allSeries; /// /// The series the user opened, or null while the list shows. /// private ModelComparisonSeries? openSeries; #region Overrides of MSGComponentBase protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); if (!ModelComparisonAccess.CanOpen(this.SettingsManager, T("Model Comparison"))) { this.NavigationManager.NavigateTo(Routes.ASSISTANTS); return; } await this.ReloadSeriesAsync(); } #endregion private async Task ReloadSeriesAsync() => this.allSeries = await this.ModelComparisonStore.ListSeriesAsync(); /// /// Describes a series in the list without naming its models: their names would end the blindness. /// private string DescribeSeries(ModelComparisonSeries series) { var culture = Tools.PluginSystem.I18N.I.Culture; var decided = series.Tasks.Count(task => task.State is ModelComparisonSeriesTaskState.DONE).CompactCount(); var started = CommonTools.FormatTimestampToGeneral(series.CreatedAtUtc.ToLocalTime().DateTime, culture); return series.IsResolved ? string.Format(culture, T("Started {0}, decided tasks: {1}, result shown"), started, decided) : string.Format(culture, T("Started {0}, decided tasks: {1}"), started, decided); } /// /// Lets the user set up a new series in its dialog, and opens the series once the dialog stored it. /// /// The series whose setup the new one takes over, or null for the preselected options. private async Task CreateSeriesAsync(ModelComparisonSeries? basedOn) { var parameters = new DialogParameters { { dialog => dialog.BasedOn, basedOn } }; var reference = await this.DialogService.ShowAsync(T("New comparison series"), parameters, DialogOptions.FULLSCREEN); var result = await reference.Result; if (result is null || result.Canceled || result.Data is not ModelComparisonSeries series) return; await this.ReloadSeriesAsync(); this.OpenSeries(series); } private void OpenSeries(ModelComparisonSeries series) => this.openSeries = series; private async Task CloseSeries() { this.openSeries = null; await this.ReloadSeriesAsync(); } /// /// Asks for a new name of a series and stores it. The series is the one the list or the open view holds, so that /// both show the new name right away. /// private async Task RenameSeriesAsync(ModelComparisonSeries series) { // The names of the other series, as stored now, so that the new name stays unique: var takenNames = (await this.ModelComparisonStore.ListSeriesAsync()).Where(entry => entry.Id != series.Id).Select(entry => entry.Name).ToList(); var parameters = new DialogParameters { { dialog => dialog.Message, TA("Enter a new name for this comparison series.") }, { dialog => dialog.InputHeaderText, T("Name of the series") }, { dialog => dialog.UserInput, series.Name }, { dialog => dialog.ConfirmText, T("Rename") }, { dialog => dialog.ConfirmColor, Color.Primary }, // The name rule rejects an empty name itself, in the same words as the dialog of a new series: { dialog => dialog.AllowEmptyInput, true }, { dialog => dialog.AdditionalValidation, candidate => ModelComparisonNames.SeriesNameIssue(candidate, takenNames) }, }; var reference = await this.DialogService.ShowAsync(T("Rename series"), parameters, DialogOptions.FULLSCREEN); var result = await reference.Result; if (result is null || result.Canceled || result.Data is not string name || ModelComparisonNames.SeriesNameIssue(name, takenNames) is not null) return; var previousName = series.Name; series.Name = name.Trim(); try { await this.ModelComparisonStore.SaveSeriesAsync(series); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { series.Name = previousName; this.Logger.LogError(exception, "Could not rename a comparison series of the Model Comparison assistant."); await this.MessageBus.SendError(new(AppIcons.SAVE_FAILED, TA("AI Studio could not rename the comparison series. Please try again."))); } if (this.openSeries is null) await this.ReloadSeriesAsync(); } /// /// Deletes a series with its chats after asking. /// private async Task DeleteSeriesAsync(ModelComparisonSeries series) { var parameters = new DialogParameters { { dialog => dialog.Message, string.Format(TA("Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone."), series.Name) }, }; var reference = await this.DialogService.ShowAsync(T("Delete series"), parameters, DialogOptions.FULLSCREEN); var result = await reference.Result; if (result is null || result.Canceled) return; // An answer which still runs would otherwise try to store its chat into the deleted series: foreach (var task in series.Tasks) { await this.ChatService.ForgetAsync(task.ChatOne.ChatId); await this.ChatService.ForgetAsync(task.ChatTwo.ChatId); } try { await this.ModelComparisonStore.DeleteSeriesAsync(series.Id); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { this.Logger.LogError(exception, "Could not delete a comparison series of the Model Comparison assistant."); await this.MessageBus.SendError(new(AppIcons.DELETE, TA("AI Studio could not delete the comparison series. Please try again."))); } await this.ReloadSeriesAsync(); } /// /// Shows what all stored comparisons say about the models, the series whose result the user saw included. /// /// /// The user may delete all series there. Afterward, the list is read anew, and an open series which is gone /// closes, which also stops its judge. /// private async Task OpenResultsDialog() { var reference = await this.DialogService.ShowAsync(T("Results of all comparisons"), DialogOptions.FULLSCREEN); await reference.Result; await this.ReloadSeriesAsync(); if (this.openSeries is { } series && this.allSeries!.All(entry => entry.Id != series.Id)) this.openSeries = null; } /// /// Goes back to the choice between the two modes. /// private void SwitchMode() => this.NavigationManager.NavigateTo(Routes.ASSISTANT_MODEL_COMPARISON); /// /// Opens the options of the assistant, which both modes share, as the mode Answer Variance does through its base class. /// private async Task OpenSettingsDialog() => await this.DialogService.ShowAsync(null, new DialogParameters(), DialogOptions.FULLSCREEN); }