mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 02:53:38 +00:00
Attach files dropped onto the list of attachments
This commit is contained in:
parent
c69cc05d08
commit
fbe3fdda4b
@ -275,7 +275,7 @@ public partial class AttachDocuments : MSGComponentBase
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var previousAttachments = this.DocumentPaths.ToHashSet();
|
var previousAttachments = this.DocumentPaths.ToHashSet();
|
||||||
this.DocumentPaths = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.DocumentPaths);
|
this.DocumentPaths = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.DocumentPaths, this.AttachDroppedPathsAsync, () => this.IsUnavailable);
|
||||||
foreach (var removedAttachment in previousAttachments.Except(this.DocumentPaths))
|
foreach (var removedAttachment in previousAttachments.Except(this.DocumentPaths))
|
||||||
ManagedTranscriptAttachment.TryDeleteOwnedFile(removedAttachment);
|
ManagedTranscriptAttachment.TryDeleteOwnedFile(removedAttachment);
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,26 @@
|
|||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
|
||||||
|
<PathDropZone IsArea="@true"
|
||||||
|
IdPrefix="review-attachments"
|
||||||
|
OnPathsDropped="@this.DropCallback"
|
||||||
|
Disabled="@this.IsZoneDisabled"
|
||||||
|
Context="isDropTarget">
|
||||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
<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.")
|
@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.")
|
||||||
</MudJustifiedText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
@if (this.CanAttach)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
|
@T("You can drag more files into this window to attach them right away.")
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
<MudDivider Class="mt-3 mb-3"/>
|
<MudDivider Class="mt-3 mb-3"/>
|
||||||
|
|
||||||
<div style="max-height: 50vh; overflow-y: auto; overflow-x: hidden; padding-right: 8px;">
|
<div class="@this.AttachmentListClass(isDropTarget)" style="max-height: 50vh; overflow-y: auto; overflow-x: hidden;">
|
||||||
@if (!this.DocumentPaths.Any())
|
@if (!this.DocumentPaths.Any())
|
||||||
{
|
{
|
||||||
<MudJustifiedText Typo="Typo.body1" Class="mt-3">
|
<MudJustifiedText Typo="Typo.body1" Class="mt-3">
|
||||||
@ -91,6 +104,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</PathDropZone>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled" Color="Color.Primary">
|
<MudButton OnClick="@this.Close" Variant="Variant.Filled" Color="Color.Primary">
|
||||||
|
|||||||
@ -16,18 +16,108 @@ public partial class ReviewAttachmentsDialog : MSGComponentBase
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public HashSet<FileAttachment> DocumentPaths { get; set; } = new();
|
public HashSet<FileAttachment> DocumentPaths { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches the files the user drops onto this dialog, and answers which of them it attached.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
[Parameter]
|
||||||
|
public Func<List<string>, Task<IReadOnlyList<FileAttachment>>>? AttachPaths { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decides, at the moment a drop arrives, whether attaching is possible right now.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
[Parameter]
|
||||||
|
public Func<bool>? IsAttachingUnavailable { get; set; }
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private IDialogService DialogService { get; set; } = null!;
|
private IDialogService DialogService { get; set; } = null!;
|
||||||
|
|
||||||
private void Close() => this.MudDialog.Close(DialogResult.Ok(this.DocumentPaths));
|
private void Close() => this.MudDialog.Close(DialogResult.Ok(this.DocumentPaths));
|
||||||
|
|
||||||
public static async Task<HashSet<FileAttachment>> OpenDialogAsync(IDialogService dialogService, params HashSet<FileAttachment> documentPaths)
|
/// <summary>Whether this dialog takes files at all, which decides what it says and shows.</summary>
|
||||||
|
private bool CanAttach => this.AttachPaths is not null;
|
||||||
|
|
||||||
|
private bool IsZoneDisabled() => this.IsAttachingUnavailable?.Invoke() ?? false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private EventCallback<List<string>> DropCallback => this.AttachPaths is null
|
||||||
|
? default
|
||||||
|
: EventCallback.Factory.Create<List<string>>(this, this.PathsDropped);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isDropTarget">Whether this dialog is the target of the drop being aimed right now.</param>
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches what the user dropped onto this dialog and answers which files that became.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="paths">The dropped paths, in the order the runtime delivered them.</param>
|
||||||
|
/// <returns>The files which were attached, in the order they were dropped.</returns>
|
||||||
|
private async Task<IReadOnlyList<FileAttachment>> AttachPathsAsync(List<string> 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<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)
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters<ReviewAttachmentsDialog>
|
var dialogParameters = new DialogParameters<ReviewAttachmentsDialog>
|
||||||
{
|
{
|
||||||
{ x => x.DocumentPaths, documentPaths }
|
{ 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<ReviewAttachmentsDialog>(TB("Your attached files"), dialogParameters, DialogOptions.FULLSCREEN);
|
var dialogReference = await dialogService.ShowAsync<ReviewAttachmentsDialog>(TB("Your attached files"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||||
var dialogResult = await dialogReference.Result;
|
var dialogResult = await dialogReference.Result;
|
||||||
if (dialogResult is null || dialogResult.Canceled)
|
if (dialogResult is null || dialogResult.Canceled)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user