Added an overview of all files attached to a chat (#1002)

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
j-erlerandThorsten Sommer authored and GitHub committed 2026-11-06 18:03:49 +01:00
1 parent 31862f5ecd
commit 9d1768e2f4
10 files changed
+281 -15

No files matched your search

@@ -9,7 +9,14 @@
Disabled="@this.IsZoneDisabled"
Context="isDropTarget">
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Here you can see all attached files. Files that can no longer be found (deleted, renamed, or moved) are marked with a warning icon and a strikethrough name. You can remove any attachment using the trash can icon.")
@if (this.CanRemove)
{
@T("Here you can see all attached files. Files that can no longer be found (deleted, renamed, or moved) are marked with a warning icon and a strikethrough name. You can remove any attachment using the trash can icon.")
}
else
{
@T("Here you can see all attached files. Files that can no longer be found (deleted, renamed, or moved) are marked with a warning icon and a strikethrough name.")
}
</MudJustifiedText>
@if (this.CanAttach)
@@ -31,8 +38,10 @@
@{
var currentFolder = string.Empty;
var pathsWithSeveralVersions = this.PathsWithSeveralVersions;
foreach (var fileAttachment in this.OrderedAttachments)
{
var versionLabel = this.VersionLabel(fileAttachment, pathsWithSeveralVersions);
var folderPath = Path.GetDirectoryName(fileAttachment.FilePath);
if (folderPath != currentFolder)
{
@@ -55,6 +64,12 @@
<MudText Style="white-space: nowrap;">
@fileAttachment.FileName
</MudText>
@if (versionLabel is not null)
{
<MudText Typo="Typo.body2" Class="ml-2 mud-text-secondary" Style="white-space: nowrap;">
@versionLabel
</MudText>
}
</span>
</MudTooltip>
</div>
@@ -66,11 +81,14 @@
OnClick="@(() => this.InvestigateFile(fileAttachment))"/>
</MudTooltip>
<MudTooltip Text="@T("Remove this attachment.")" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="@(() => this.DeleteAttachment(fileAttachment))"/>
</MudTooltip>
@if (this.CanRemove)
{
<MudTooltip Text="@T("Remove this attachment.")" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="@(() => this.DeleteAttachment(fileAttachment))"/>
</MudTooltip>
}
</MudToolBar>
</MudStack>
}
@@ -84,6 +102,12 @@
<MudText Style="white-space: nowrap;">
<s>@fileAttachment.FileName</s>
</MudText>
@if (versionLabel is not null)
{
<MudText Typo="Typo.body2" Class="ml-2 mud-text-secondary" Style="white-space: nowrap;">
@versionLabel
</MudText>
}
</span>
</MudTooltip>
</div>
@@ -105,11 +129,14 @@
Disabled="true"/>
}
<MudTooltip Text="@T("Remove this attachment.")" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="@(() => this.DeleteAttachment(fileAttachment))"/>
</MudTooltip>
@if (this.CanRemove)
{
<MudTooltip Text="@T("Remove this attachment.")" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="@(() => this.DeleteAttachment(fileAttachment))"/>
</MudTooltip>
}
</MudToolBar>
</MudStack>
}
@@ -47,6 +47,16 @@ public partial class ReviewAttachmentsDialog : MSGComponentBase
[Parameter]
public Func<FileAttachment, string, FileVersion, Task>? KeepReadText { get; set; }
/// <summary>
/// Whether the user may remove attachments here.
/// </summary>
/// <remarks>
/// False when this dialog gathers the attachments of several messages at once: a removal would
/// reach into a message that was already sent, and nothing here could tell which one.
/// </remarks>
[Parameter]
public bool CanRemove { get; set; } = true;
[Inject]
private IDialogService DialogService { get; set; } = null!;
@@ -83,7 +93,7 @@ public partial class ReviewAttachmentsDialog : MSGComponentBase
}
/// <summary>
/// The attachments, sorted by their folder and, within it, by their file name.
/// The attachments, sorted by their folder and, within it, by their file name, the latest version of a file first.
/// </summary>
/// <remarks>
/// The list below starts a new heading whenever the folder changes from one attachment to the
@@ -95,7 +105,37 @@ public partial class ReviewAttachmentsDialog : MSGComponentBase
/// </remarks>
private IEnumerable<FileAttachment> OrderedAttachments => this.DocumentPaths
.OrderBy(attachment => Path.GetDirectoryName(attachment.FilePath) ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ThenBy(attachment => attachment.FileName, StringComparer.OrdinalIgnoreCase);
.ThenBy(attachment => attachment.FileName, StringComparer.OrdinalIgnoreCase)
.ThenByDescending(attachment => attachment.Version?.LastWriteUtc);
/// <summary>
/// The paths which the list holds more than once, i.e., the files which were sent in several versions.
/// </summary>
/// <remarks>
/// Only the overview of all files of a chat holds a file more than once, see
/// ChatThread.CollectSentFileAttachments: the draft and a single message hold each file once, see
/// SetFileAttachmentExtensions.AttachInPlaceOfSameFile. The other uses of this dialog therefore
/// stay as they were. Asked once for each rendering, not once for each attachment.
/// </remarks>
private IReadOnlySet<string> PathsWithSeveralVersions => this.DocumentPaths
.GroupBy(attachment => attachment.FilePath)
.Where(group => group.Count() > 1)
.Select(group => group.Key)
.ToHashSet();
/// <summary>
/// Tells apart the versions of a file which the list holds more than once, by the time their file was saved.
/// </summary>
/// <param name="attachment">The attachment.</param>
/// <param name="pathsWithSeveralVersions">The paths which the list holds more than once, see PathsWithSeveralVersions.</param>
/// <returns>The label, or null for a file listed once, and for a version whose state AI Studio did not note.</returns>
private string? VersionLabel(FileAttachment attachment, IReadOnlySet<string> pathsWithSeveralVersions)
{
if (attachment.Version is not { } version || !pathsWithSeveralVersions.Contains(attachment.FilePath))
return null;
return string.Format(T("Version from {0}"), CommonTools.FormatTimestampToGeneral(version.LastWriteUtc.ToLocalTime(), I18N.I.Culture));
}
/// <summary>
/// Attaches what the user dropped onto this dialog and answers which files that became.
@@ -129,11 +169,12 @@ public partial class ReviewAttachmentsDialog : MSGComponentBase
private async Task PathsDropped(List<string> paths) => await this.AttachPathsAsync(paths);
public static async Task<HashSet<FileAttachment>> OpenDialogAsync(IDialogService dialogService, HashSet<FileAttachment> documentPaths, Func<List<string>, Task<IReadOnlyList<FileAttachment>>>? attachPaths = null, Func<bool>? isAttachingUnavailable = null, Func<FileAttachment, string, FileVersion, Task>? keepReadText = null)
public static async Task<HashSet<FileAttachment>> OpenDialogAsync(IDialogService dialogService, HashSet<FileAttachment> documentPaths, Func<List<string>, Task<IReadOnlyList<FileAttachment>>>? attachPaths = null, Func<bool>? isAttachingUnavailable = null, Func<FileAttachment, string, FileVersion, Task>? keepReadText = null, bool canRemove = true)
{
var dialogParameters = new DialogParameters<ReviewAttachmentsDialog>
{
{ x => x.DocumentPaths, documentPaths }
{ x => x.DocumentPaths, documentPaths },
{ x => x.CanRemove, canRemove },
};
if (attachPaths is not null)