diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs index 6ea7ad26..ababf700 100644 --- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs +++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs @@ -220,11 +220,31 @@ public partial class AttachDocuments : MSGComponentBase /// Attaches what the user dropped on the zone of this component. /// /// The dropped paths, in the order the runtime delivered them. - private async Task PathsDropped(List paths) + private async Task PathsDropped(List paths) => await this.AttachDroppedPathsAsync(paths); + + /// + /// Attaches dropped paths and reports which files it made of them. + /// + /// + /// This is what the attachment dialogs call while they are open: a drop lands in the dialog the + /// user is looking at, yet only this component knows how to turn a path into an attachment. The + /// answer is what lets those dialogs show the result, see PathsDropped for our own zone. + /// + /// The dropped paths, in the order the runtime delivered them. + /// The files this call attached, in the order they were dropped. + private async Task> AttachDroppedPathsAsync(List paths) { - await this.AddFileBatchAsync(paths); + var attached = await this.AddFileBatchAsync(paths); await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths); await this.OnChange(this.DocumentPaths); + + // + // A dialog reaches us through a delegate rather than through an event callback, so nothing + // renders this component afterwards. Without this, the number on the badge would stay at its + // old value until something else happens to render us. + // + this.StateHasChanged(); + return attached; } private async Task AddFilesManually() @@ -307,8 +327,18 @@ public partial class AttachDocuments : MSGComponentBase this.OwnerChat.PendingMediaTranscripts.RemoveAll(attachment => !retainedPaths.Contains(attachment.FilePath)); } - private async Task AddFileBatchAsync(IEnumerable paths) + /// + /// Validates the given paths and attaches every file which passes. + /// + /// The paths to attach, in the order they arrived. + /// + /// The files this call attached, in the order they arrived. Media files are never among them: + /// they go to the transcription service and become attachments only once their transcript is + /// ready, which is long after this call has returned. + /// + private async Task> AddFileBatchAsync(IEnumerable paths) { + var attached = new List(); var pathList = paths.ToList(); if (this.AllowedFileTypes is { Length: > 0 }) { @@ -356,18 +386,25 @@ public partial class AttachDocuments : MSGComponentBase if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider)) continue; - this.DocumentPaths.Add(FileAttachment.FromPath(path)); + // + // This counts as attached even when the set already held the file: the user just + // dropped it, and whoever asked us wants to hear about the file they aimed at, not + // about whether it happened to be new to us. + // + var attachment = FileAttachment.FromPath(path); + this.DocumentPaths.Add(attachment); + attached.Add(attachment); } if (mediaPaths.Count is 0) - return; + return attached; if (string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)) { await this.MessageBus.SendWarning(new( Icons.Material.Filled.VoiceChat, this.T("Media files require a configured transcription provider. Configure one in the transcription settings."))); - return; + return attached; } var names = string.Join('\n', mediaPaths.Select(path => $"- {Markdown.EscapeInlineText(Path.GetFileName(path))}")); @@ -391,7 +428,7 @@ public partial class AttachDocuments : MSGComponentBase var dialogResult = await dialogReference.Result; if (dialogResult is null || dialogResult.Canceled) - return; + return attached; if (this.OwnerChat is null) this.OwnerChat = await this.EnsureOwnerChatAsync(mediaPaths[0]); @@ -408,6 +445,7 @@ public partial class AttachDocuments : MSGComponentBase } this.MediaTranscriptionService.TryStartAttachmentBatch(mediaPaths, this.EffectiveMediaImportTarget, this.OwnerChat); + return attached; } private static bool IsTranscribableMedia(string path) => FileTypes.IsAllowedPath(path, FileTypes.AUDIO) || FileTypes.IsAllowedPath(path, FileTypes.VIDEO);