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);