using AIStudio.Chat; using AIStudio.Components; using AIStudio.Tools.PluginSystem; using Microsoft.AspNetCore.Components; namespace AIStudio.Dialogs; public partial class ReviewAttachmentsDialog : MSGComponentBase { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ReviewAttachmentsDialog).Namespace, nameof(ReviewAttachmentsDialog)); [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; [Parameter] public HashSet DocumentPaths { get; set; } = new(); /// /// Attaches the files the user drops onto this dialog, and answers which of them it attached. /// /// /// Null when this dialog only shows attachments, which is the case for a message that was /// already sent: there is nothing left to attach to. Without this, the dialog behaves as it /// always did and swallows every drop. /// [Parameter] public Func, Task>>? AttachPaths { get; set; } /// /// Decides, at the moment a drop arrives, whether attaching is possible right now. /// /// /// Asked rather than passed as a value, because the answer changes while this dialog is open: /// dropping a media file here starts a transcription, and nothing else may be attached until /// that one is through. /// [Parameter] public Func? IsAttachingUnavailable { get; set; } [Inject] private IDialogService DialogService { get; set; } = null!; private void Close() => this.MudDialog.Close(DialogResult.Ok(this.DocumentPaths)); /// Whether this dialog takes files at all, which decides what it says and shows. private bool CanAttach => this.AttachPaths is not null; private bool IsZoneDisabled() => this.IsAttachingUnavailable?.Invoke() ?? false; /// /// Binds the drop zone only when there is something to attach to. An area which reports a /// delegate claims the role of its own default target, and claiming it without being able to /// use it would swallow drops with no reason the user could see. /// private EventCallback> DropCallback => this.AttachPaths is null ? default : EventCallback.Factory.Create>(this, this.PathsDropped); /// /// Marks the list of attachments while a file hovers over this dialog, so it is visible where /// the file would land. The frame keeps its width in both states; only its color changes, or /// the list would jump by a few pixels with every drag. /// /// Whether this dialog is the target of the drop being aimed right now. private string AttachmentListClass(bool isDropTarget) { if (!this.CanAttach) return "pa-2"; return isDropTarget && !this.IsZoneDisabled() ? "border-dashed border-2 rounded-lg pa-2 mud-border-primary" : "border-dashed border-2 rounded-lg pa-2 mud-border-lines-default"; } /// /// The attachments, sorted by their folder and, within it, by their file name. /// /// /// The list below starts a new heading whenever the folder changes from one attachment to the /// next, which names every folder exactly once -- but only as long as the attachments of a /// folder arrive together. The set behind them keeps no order of its own to guarantee that: /// removing one attachment already scrambles it, and one attached while this dialog is open /// lands at its end, giving its folder a second heading further down. Sorting here is what that /// list assumes anyway. /// private IEnumerable OrderedAttachments => this.DocumentPaths .OrderBy(attachment => Path.GetDirectoryName(attachment.FilePath) ?? string.Empty, StringComparer.OrdinalIgnoreCase) .ThenBy(attachment => attachment.FileName, StringComparer.OrdinalIgnoreCase); /// /// Attaches what the user dropped onto this dialog and answers which files that became. /// /// /// Every drop takes this way, the ones aimed at the document preview above this dialog /// included. That is why the list is refreshed here and nowhere else. /// /// The dropped paths, in the order the runtime delivered them. /// The files which were attached, in the order they were dropped. private async Task> AttachPathsAsync(List paths) { if (this.AttachPaths is null) return []; var attached = await this.AttachPaths(paths); this.StateHasChanged(); // // The list scrolls, so a newly attached file may well sit outside the visible part of it. // Saying so is cheaper than scrolling there, and the snackbar is skipped by the hit test, // so it never gets in the way of the next drop. // if (attached.Count > 0) await this.MessageBus.SendSuccess(new(Icons.Material.Filled.AttachFile, attached.Count is 1 ? string.Format(T("Attached {0}."), attached[0].FileName) : string.Format(T("Attached {0} files."), attached.Count))); return attached; } private async Task PathsDropped(List paths) => await this.AttachPathsAsync(paths); public static async Task> OpenDialogAsync(IDialogService dialogService, HashSet documentPaths, Func, Task>>? attachPaths = null, Func? isAttachingUnavailable = null) { var dialogParameters = new DialogParameters { { x => x.DocumentPaths, documentPaths } }; if (attachPaths is not null) dialogParameters.Add(x => x.AttachPaths, attachPaths); if (isAttachingUnavailable is not null) dialogParameters.Add(x => x.IsAttachingUnavailable, isAttachingUnavailable); var dialogReference = await dialogService.ShowAsync(TB("Your attached files"), dialogParameters, DialogOptions.FULLSCREEN); var dialogResult = await dialogReference.Result; if (dialogResult is null || dialogResult.Canceled) return documentPaths; if (dialogResult.Data is null) return documentPaths; return dialogResult.Data as HashSet ?? documentPaths; } private void DeleteAttachment(FileAttachment fileAttachment) { if (this.DocumentPaths.Remove(fileAttachment)) { this.StateHasChanged(); } } /// /// The user might want to check what we actually extract from his file and therefore give the LLM as an input. /// /// The file to check. private async Task InvestigateFile(FileAttachment fileAttachment) { var dialogParameters = new DialogParameters { { x => x.Document, fileAttachment }, }; // // Give the preview our own way of attaching, so a file dropped onto it lands in this list // as well. Not when we cannot attach anything ourselves: the preview would then claim every // drop and do nothing with it. // if (this.CanAttach) { dialogParameters.Add(x => x.AttachPaths, this.AttachPathsAsync); if (this.IsAttachingUnavailable is not null) dialogParameters.Add(x => x.IsAttachingUnavailable, this.IsAttachingUnavailable); } await this.DialogService.ShowAsync(T("Document Preview"), dialogParameters, DialogOptions.FULLSCREEN); } }