diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor
index e694ebd9..3a866034 100644
--- a/app/MindWork AI Studio/Assistants/AssistantBase.razor
+++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor
@@ -4,7 +4,7 @@
@* Every assistant is a drop area: a file dropped anywhere inside it lands on the assistant's
default zone, while a file dropped on one of its specific zones lands there. *@
-
+
@@ -188,4 +188,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor b/app/MindWork AI Studio/Components/AttachDocuments.razor
index 7f308a75..0261368b 100644
--- a/app/MindWork AI Studio/Components/AttachDocuments.razor
+++ b/app/MindWork AI Studio/Components/AttachDocuments.razor
@@ -3,8 +3,13 @@
@if (this.UseSmallForm)
{
-
+ else
+ {
+
+ }
+ }
+
@if (!this.IsUnavailable)
{
diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
index be657adf..c3895fa2 100644
--- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
+++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
@@ -45,12 +45,6 @@ public partial class AttachDocuments : MSGComponentBase
[Parameter]
public bool CatchAllDocuments { get; set; }
- ///
- /// The area this component lives in, if it lives in one at all.
- ///
- [CascadingParameter]
- private DropZoneScopeState? Scope { get; set; }
-
[Parameter]
public bool UseSmallForm { get; set; }
@@ -105,11 +99,6 @@ public partial class AttachDocuments : MSGComponentBase
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly string DROP_FILES_HERE_TEXT = TB("Drop files here to attach them.");
- private readonly string dropZoneId = $"attach-documents-{Guid.NewGuid():N}";
-
- private bool isDefaultZone;
- private bool hasReportedDefaultZoneProblem;
- private bool isDraggingOver;
private bool isFileDialogOpen;
private MediaImportOwner EffectiveImportOwner => this.OwnerChat is not null
? MediaImportOwner.ForChat(this.OwnerChat.ChatId)
@@ -124,7 +113,7 @@ public partial class AttachDocuments : MSGComponentBase
protected override async Task OnInitializedAsync()
{
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
- this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]);
+ this.ApplyFilters([], []);
await base.OnInitializedAsync();
}
@@ -132,7 +121,6 @@ public partial class AttachDocuments : MSGComponentBase
/// Rehydrates results after the component is assigned another chat or target.
protected override async Task OnParametersSetAsync()
{
- this.UpdateDefaultZoneRole();
await base.OnParametersSetAsync();
await this.SyncCompletedMediaAttachmentsAsync();
}
@@ -223,139 +211,22 @@ public partial class AttachDocuments : MSGComponentBase
protected override void DisposeResources()
{
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
-
- // Hand the role of the default target back to the area:
- if (this.isDefaultZone)
- this.Scope?.ReleaseDefaultZone(this);
-
base.DisposeResources();
}
- protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
- {
- switch (triggeredEvent)
- {
- case Event.HIGHLIGHT_DROP_ZONE when data is DropZoneHighlight highlight:
- this.ApplyHighlight(this.IsThisZone(highlight.ZoneId));
- break;
-
- case Event.PATHS_DROPPED when data is DroppedPaths dropped:
- // Whoever the drop was meant for, the drag is over and no zone stays highlighted:
- this.ApplyHighlight(false);
-
- if (!this.IsThisZone(dropped.ZoneId))
- return;
-
- if (this.IsUnavailable)
- {
- this.Logger.LogDebug("The attachment zone '{Name}' is unavailable and swallowed {Count} dropped path(s).", this.Name, dropped.Paths.Count);
- return;
- }
-
- await this.AddFileBatchAsync(dropped.Paths);
- await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
- await this.OnChange(this.DocumentPaths);
- this.StateHasChanged();
- break;
- }
- }
-
#endregion
///
- /// Keeps the role of the default target in step with the CatchAllDocuments parameter.
+ /// Attaches what the user dropped on the zone of this component.
///
- ///
- /// The flag is a parameter, so it can change while this component lives. A zone inside a
- /// collapsed panel is the case this exists for: MudBlazor leaves the content of a collapsed
- /// panel in the DOM with a height of zero, so the zone stays alive and cannot be aimed at --
- /// yet it would keep the role and swallow every drop meant for the visible part of the page.
- ///
- private void UpdateDefaultZoneRole()
+ /// The dropped paths, in the order the runtime delivered them.
+ private async Task PathsDropped(List paths)
{
- if (this.CatchAllDocuments)
- {
- this.ClaimDefaultZoneRole();
- return;
- }
-
- if (!this.isDefaultZone)
- return;
-
- this.Scope?.ReleaseDefaultZone(this);
- this.isDefaultZone = false;
+ await this.AddFileBatchAsync(paths);
+ await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
+ await this.OnChange(this.DocumentPaths);
}
- ///
- /// Asks the area for the role of its default target.
- ///
- private void ClaimDefaultZoneRole()
- {
- if (this.isDefaultZone)
- return;
-
- if (this.Scope is null)
- {
- //
- // There is nothing to claim: the surrounding page, assistant, or dialog is not a drop
- // area at all. The flag would then do nothing, and silently -- which is how a zone ends
- // up promising a behaviour it cannot deliver. So say it out loud: either the area needs
- // a DropZoneScope, or the flag does not belong here. Reported once only, because the
- // claim is retried on every parameter change.
- //
- if (!this.hasReportedDefaultZoneProblem)
- {
- this.hasReportedDefaultZoneProblem = true;
- this.Logger.LogWarning("The attachment zone '{Name}' wants to be the default target of its area, but it does not live in a drop zone scope. Dropping next to this zone will do nothing.", this.Name);
- }
-
- return;
- }
-
- this.isDefaultZone = this.Scope.TryBecomeDefaultZone(this);
-
- // Losing the role to a neighbour is a decision, not a defect -- and it can be undone later,
- // when that neighbour goes away. So this one only goes to the debug log, and only once:
- if (this.isDefaultZone || this.hasReportedDefaultZoneProblem)
- return;
-
- this.hasReportedDefaultZoneProblem = true;
- this.Logger.LogDebug("The attachment zone '{Name}' asked to be the default target of its area, which another zone already is. It now takes only the drops aimed at itself.", this.Name);
- }
-
- ///
- /// Decides whether the named zone is this one.
- ///
- ///
- /// The area counts as this zone as long as this zone is its default target. That is the whole
- /// mechanism behind dropping anywhere in the chat and still landing on the composer.
- ///
- /// The ID the hit test reported, or null when it hit nothing.
- private bool IsThisZone(string? zoneId) => zoneId is not null && (zoneId == this.dropZoneId || (this.isDefaultZone && zoneId == this.Scope?.ScopeId));
-
- ///
- /// Highlights the zone, or takes the highlight away.
- ///
- ///
- /// The comparison is not for tidiness: a throttled drag-over event arrives about ten times per
- /// second, and without it every one of them would render every zone on the page anew. In the
- /// small form the highlight also swaps the markup, so this keeps the composer from flickering.
- ///
- private void ApplyHighlight(bool shouldBeHighlighted)
- {
- var highlighted = shouldBeHighlighted && !this.IsUnavailable;
- if (highlighted == this.isDraggingOver)
- return;
-
- this.isDraggingOver = highlighted;
- this.dragClass = highlighted ? $"{DEFAULT_DRAG_CLASS} mud-border-primary border-4" : DEFAULT_DRAG_CLASS;
- this.StateHasChanged();
- }
-
- private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full";
-
- private string dragClass = DEFAULT_DRAG_CLASS;
-
private async Task AddFilesManually()
{
if (this.IsUnavailable)
diff --git a/app/MindWork AI Studio/Components/DropZoneScope.razor b/app/MindWork AI Studio/Components/DropZoneScope.razor
deleted file mode 100644
index b0f3f597..00000000
--- a/app/MindWork AI Studio/Components/DropZoneScope.razor
+++ /dev/null
@@ -1,8 +0,0 @@
-@* One element, and the content immediately inside it. Layouts hang child selectors on this
- element, for instance app.css does on .inner-scrolling-context, so an extra level here would
- break them. The cascading value renders no element of its own. *@
-
-
- @this.ChildContent
-
-
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/DropZoneScope.razor.cs b/app/MindWork AI Studio/Components/DropZoneScope.razor.cs
deleted file mode 100644
index 0ed4a75f..00000000
--- a/app/MindWork AI Studio/Components/DropZoneScope.razor.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using Microsoft.AspNetCore.Components;
-
-namespace AIStudio.Components;
-
-///
-/// Marks an area whose drops end up at its default target when they hit no specific zone.
-///
-///
-///
-/// This is how the habitual behaviour survives the move to hit testing: a file dropped anywhere in
-/// the chat, in an assistant, or in a dialog still arrives where it used to, while a file dropped
-/// on a specific zone now arrives exactly there. No code decides between the two -- the browser
-/// does, because the specific zone lies deeper in the DOM than the area around it, and the hit test
-/// resolves from the inside out.
-///
-///
-/// The component replaces an existing element rather than adding one: it takes the class and the
-/// style of the element it stands in for. Where the areas already had a wrapper, which is the case
-/// for every page and every assistant, nothing about the layout changes.
-///
-///
-public partial class DropZoneScope : ComponentBase
-{
- ///
- /// The content of the area.
- ///
- [Parameter]
- public RenderFragment? ChildContent { get; set; }
-
- ///
- /// The CSS classes of the element this scope renders.
- ///
- [Parameter]
- public string Class { get; set; } = string.Empty;
-
- ///
- /// The inline style of the element this scope renders.
- ///
- [Parameter]
- public string Style { get; set; } = string.Empty;
-
- ///
- /// The state to cascade, for hosts which want to be the default target of their own area.
- ///
- ///
- /// A host cannot read what it cascades itself, so a page which is its own drop target has to own
- /// the state instead. The plugins page is such a case: it accepts an archive anywhere on it and
- /// has no inner zone to hand the role to. Everybody else leaves this alone and lets the scope
- /// keep its own state.
- ///
- [Parameter]
- public DropZoneScopeState? State { get; set; }
-
- private readonly DropZoneScopeState ownState = new($"drop-zone-scope-{Guid.NewGuid():N}");
-
- private DropZoneScopeState EffectiveState => this.State ?? this.ownState;
-}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/PathDropZone.razor b/app/MindWork AI Studio/Components/PathDropZone.razor
index d5c917b8..e3b365cf 100644
--- a/app/MindWork AI Studio/Components/PathDropZone.razor
+++ b/app/MindWork AI Studio/Components/PathDropZone.razor
@@ -1,7 +1,28 @@
@inherits MSGComponentBase
-
-
- @this.ChildContent
-
-
\ No newline at end of file
+@if (this.IsFramed)
+{
+
+}
+else
+{
+ @* An area renders one element, with the content immediately inside it. Layouts hang child
+ selectors on that element, for instance app.css does on .inner-scrolling-context, so an extra
+ level here would break them. The cascading value renders no element of its own. *@
+
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/PathDropZone.razor.cs b/app/MindWork AI Studio/Components/PathDropZone.razor.cs
index 5f572d46..6b982000 100644
--- a/app/MindWork AI Studio/Components/PathDropZone.razor.cs
+++ b/app/MindWork AI Studio/Components/PathDropZone.razor.cs
@@ -6,22 +6,40 @@ namespace AIStudio.Components;
/// A drop zone which reports the paths of whatever was dropped on it, and nothing else.
///
///
+///
/// Dropping is a native matter in AI Studio: the Tauri runtime reports real paths, which is why
/// this zone can hand out folders just as well as files. What those paths mean is the consumer's
/// business — this component reads no content and does not care whether a path leads to a file or
/// to a folder.
+///
+///
+/// One component serves both kinds of drop target, because both are the same thing to the hit test:
+/// an element with an ID. A zone is a place one aims at, and it draws a frame around whatever it is
+/// given. An area is a page, an assistant, or a dialog: it draws nothing, it marks the space in
+/// which a drop counts at all, and the drops which hit none of its zones go to its default target.
+/// Set IsArea for the second kind.
+///
///
public partial class PathDropZone : MSGComponentBase
{
///
/// The content shown inside the zone.
///
+ ///
+ /// Its argument tells the content whether this zone is the one under the cursor right now, so it
+ /// can show where the drop would land. Everybody who does not care about that ignores it.
+ ///
[Parameter]
- public RenderFragment? ChildContent { get; set; }
+ public RenderFragment? ChildContent { get; set; }
///
/// Reports the dropped paths, in the order the runtime delivered them.
///
+ ///
+ /// An area without this callback is a marker and nothing more: it takes no drops itself, it only
+ /// gives the drops aimed between its zones a place to be counted, from where the default target
+ /// of the area picks them up.
+ ///
[Parameter]
public EventCallback> OnPathsDropped { get; set; }
@@ -32,21 +50,87 @@ public partial class PathDropZone : MSGComponentBase
/// A drop aimed at this zone arrives here in any case. What this flag decides is the fate of the
/// drops aimed anywhere else in the surrounding area which hit no zone of their own: with the
/// flag, they arrive here as well. Only one zone per area can hold that role, and if several ask
- /// for it, the first one in the markup gets it.
+ /// for it, the first one in the markup gets it. An area ignores the flag: an area which takes
+ /// drops at all is its own default target, see IsArea.
///
[Parameter]
public bool CatchAllDocuments { get; set; }
///
- /// When true, the zone ignores drops and is not highlighted.
+ /// Decides, at the moment a drop arrives, whether this zone may take it.
///
///
- /// It keeps its ID in the DOM nevertheless and therefore swallows the drops aimed at it. That is
+ /// A disabled zone keeps its ID in the DOM and therefore swallows the drops aimed at it. That is
/// what the pointer says: it rests on a switched-off field, so nothing happens. Letting the drop
- /// fall through to the area behind it would deliver the files somewhere else entirely.
+ /// fall through to the area behind it would deliver the files somewhere else entirely. This is
+ /// asked rather than passed as a value because the answer often depends on work in flight, and a
+ /// value would be as old as the last render of the consumer.
///
[Parameter]
- public bool Disabled { get; set; }
+ public Func Disabled { get; set; } = () => false;
+
+ ///
+ /// Makes this element an area instead of a zone: it marks a page, an assistant, or a dialog, and
+ /// it draws no frame of its own.
+ ///
+ ///
+ /// This is how the habitual behaviour survives the move to hit testing: a file dropped anywhere
+ /// in the chat, in an assistant, or in a dialog still arrives where it used to, while a file
+ /// dropped on a specific zone now arrives exactly there. No code decides between the two — the
+ /// browser does, because a zone lies deeper in the DOM than the area around it, and the hit test
+ /// resolves from the inside out. An area replaces the element it stands in for rather than
+ /// adding one, so it takes over its class and its style.
+ ///
+ [Parameter]
+ public bool IsArea { get; set; }
+
+ ///
+ /// Leaves out the frame this zone would otherwise draw around its content.
+ ///
+ ///
+ /// For zones whose content is the marker itself, such as a toolbar which shows a drop field
+ /// while a file hovers over it. An area never has a frame, so it does not need this flag.
+ ///
+ [Parameter]
+ public bool Frameless { get; set; }
+
+ ///
+ /// The first part of the ID this element reports to the hit test, followed by a unique suffix.
+ ///
+ ///
+ /// It names the kind of zone in the log, next to the IDs the arbiter lists when a drop reached
+ /// nobody. Which is the whole reason it is a parameter: an ID of its own tells one from another,
+ /// but only a name tells what one is looking at.
+ ///
+ [Parameter]
+ public string IdPrefix { get; set; } = "path-drop-zone";
+
+ ///
+ /// The CSS classes of the element this component renders: of the frame for a zone which has one,
+ /// and of the element itself for an area and for a frameless zone.
+ ///
+ ///
+ /// A frame keeps its border and its width in any case; the classes given here replace the
+ /// padding and the margin it would use otherwise.
+ ///
+ [Parameter]
+ public string Class { get; set; } = string.Empty;
+
+ ///
+ /// The classes a frame takes on in addition while it is the zone under the cursor.
+ ///
+ ///
+ /// Only a frame is highlighted this way. Without one there is nothing to draw on, and the
+ /// content says for itself what it looks like when it is the target, see ChildContent.
+ ///
+ [Parameter]
+ public string HighlightClass { get; set; } = "mud-border-primary border-2";
+
+ ///
+ /// The inline style of the element this component renders.
+ ///
+ [Parameter]
+ public string Style { get; set; } = string.Empty;
///
/// The area this zone lives in, if it lives in one at all.
@@ -57,20 +141,65 @@ public partial class PathDropZone : MSGComponentBase
[Inject]
private ILogger Logger { get; init; } = null!;
- private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-3 mb-3 mud-width-full";
+ private const string FRAME_CLASSES = "relative rounded-lg border-2 border-dashed mud-width-full";
+ private const string DEFAULT_SPACING_CLASSES = "pa-3 mb-3";
- private readonly string dropZoneId = $"path-drop-zone-{Guid.NewGuid():N}";
-
- private string dragClass = DEFAULT_DRAG_CLASS;
+ private DropZoneScopeState? areaState;
+ private string zoneId = string.Empty;
private bool isDefaultZone;
private bool isHighlighted;
private bool hasReportedDefaultZoneProblem;
+ private bool IsFramed => !this.IsArea && !this.Frameless;
+
+ private string FrameClass => this.isHighlighted
+ ? $"{FRAME_CLASSES} {this.SpacingClasses} {this.HighlightClass}"
+ : $"{FRAME_CLASSES} {this.SpacingClasses}";
+
+ private string SpacingClasses => string.IsNullOrWhiteSpace(this.Class) ? DEFAULT_SPACING_CLASSES : this.Class;
+
+ ///
+ /// The area this zone reports to, which for an area is itself.
+ ///
+ private DropZoneScopeState? EffectiveScope => this.areaState ?? this.Scope;
+
+ ///
+ /// Whether this element wants to be the default target of its area.
+ ///
+ ///
+ /// An area which takes drops is that target by definition, because its own element is the area
+ /// and a drop next to its zones has nothing else to hit. It claims the role nevertheless, which
+ /// is what keeps a zone inside it from taking it and delivering the same drop a second time.
+ ///
+ private bool WantsDefaultZoneRole => this.IsArea ? this.OnPathsDropped.HasDelegate : this.CatchAllDocuments;
+
+ ///
+ /// Whether a drop can arrive here at all.
+ ///
+ ///
+ /// An area which delivers to nobody is only a mark on the page, and it must not listen: the
+ /// highlight would render a whole page or assistant anew several times per drag, for a highlight
+ /// nobody asked to see.
+ ///
+ private bool CanBeTarget => !this.IsArea || this.OnPathsDropped.HasDelegate;
+
#region Overrides of MSGComponentBase
protected override async Task OnInitializedAsync()
{
- this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]);
+ //
+ // The ID is built once and never again: the hit test names this element by it, so it has to
+ // outlive every render. The prefix is a parameter, and parameters are set before this point.
+ //
+ this.zoneId = $"{this.IdPrefix}-{Guid.NewGuid():N}";
+ if (this.IsArea)
+ this.areaState = new DropZoneScopeState(this.zoneId);
+
+ if (this.CanBeTarget)
+ this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]);
+ else
+ this.ApplyFilters([], []);
+
await base.OnInitializedAsync();
}
@@ -86,7 +215,7 @@ public partial class PathDropZone : MSGComponentBase
protected override void DisposeResources()
{
if (this.isDefaultZone)
- this.Scope?.ReleaseDefaultZone(this);
+ this.EffectiveScope?.ReleaseDefaultZone(this);
base.DisposeResources();
}
@@ -106,13 +235,13 @@ public partial class PathDropZone : MSGComponentBase
if (!this.IsThisZone(dropped.ZoneId))
return;
- if (this.Disabled)
+ if (this.Disabled())
{
- this.Logger.LogDebug("The path drop zone '{ZoneId}' is disabled and swallowed {Count} dropped path(s).", this.dropZoneId, dropped.Paths.Count);
+ this.Logger.LogDebug("The drop zone '{ZoneId}' cannot take drops right now and swallowed {Count} dropped path(s).", this.zoneId, dropped.Paths.Count);
return;
}
- this.Logger.LogDebug("The path drop zone '{ZoneId}' caught {Count} path(s).", this.dropZoneId, dropped.Paths.Count);
+ this.Logger.LogDebug("The drop zone '{ZoneId}' caught {Count} path(s).", this.zoneId, dropped.Paths.Count);
await this.OnPathsDropped.InvokeAsync(dropped.Paths);
break;
}
@@ -131,7 +260,7 @@ public partial class PathDropZone : MSGComponentBase
///
private void UpdateDefaultZoneRole()
{
- if (this.CatchAllDocuments)
+ if (this.WantsDefaultZoneRole)
{
this.ClaimDefaultZoneRole();
return;
@@ -140,7 +269,7 @@ public partial class PathDropZone : MSGComponentBase
if (!this.isDefaultZone)
return;
- this.Scope?.ReleaseDefaultZone(this);
+ this.EffectiveScope?.ReleaseDefaultZone(this);
this.isDefaultZone = false;
}
@@ -152,13 +281,13 @@ public partial class PathDropZone : MSGComponentBase
if (this.isDefaultZone)
return;
- if (this.Scope is null)
+ if (this.EffectiveScope is null)
{
//
// There is nothing to claim: the surrounding page, assistant, or dialog is not a drop
// area at all. The flag would then do nothing, and silently -- which is how a zone ends
// up promising a behaviour it cannot deliver. So say it out loud: either the area needs
- // a DropZoneScope, or the flag does not belong here.
+ // a drop area of its own, or the flag does not belong here.
//
//
// Reported once only: the claim is retried on every parameter change, and repeating
@@ -167,13 +296,13 @@ public partial class PathDropZone : MSGComponentBase
if (!this.hasReportedDefaultZoneProblem)
{
this.hasReportedDefaultZoneProblem = true;
- this.Logger.LogWarning("The path drop zone '{ZoneId}' wants to be the default target of its area, but it does not live in a drop zone scope. Dropping next to this zone will do nothing.", this.dropZoneId);
+ this.Logger.LogWarning("The drop zone '{ZoneId}' wants to be the default target of its area, but it does not live in a drop area. Dropping next to this zone will do nothing.", this.zoneId);
}
return;
}
- this.isDefaultZone = this.Scope.TryBecomeDefaultZone(this);
+ this.isDefaultZone = this.EffectiveScope.TryBecomeDefaultZone(this);
// Losing the role to a neighbour is a decision, not a defect -- and it can be undone later,
// when that neighbour goes away. So this one only goes to the debug log, and only once:
@@ -181,7 +310,7 @@ public partial class PathDropZone : MSGComponentBase
return;
this.hasReportedDefaultZoneProblem = true;
- this.Logger.LogDebug("The path drop zone '{ZoneId}' asked to be the default target of its area, which another zone already is. It now takes only the drops aimed at itself.", this.dropZoneId);
+ this.Logger.LogDebug("The drop zone '{ZoneId}' asked to be the default target of its area, which another zone already is. It now takes only the drops aimed at itself.", this.zoneId);
}
///
@@ -191,8 +320,8 @@ public partial class PathDropZone : MSGComponentBase
/// The area counts as this zone as long as this zone is its default target. That is the whole
/// mechanism behind dropping anywhere in a page and still landing here.
///
- /// The ID the hit test reported, or null when it hit nothing.
- private bool IsThisZone(string? zoneId) => zoneId is not null && (zoneId == this.dropZoneId || (this.isDefaultZone && zoneId == this.Scope?.ScopeId));
+ /// The ID the hit test reported, or null when it hit nothing.
+ private bool IsThisZone(string? targetZoneId) => targetZoneId is not null && (targetZoneId == this.zoneId || (this.isDefaultZone && targetZoneId == this.EffectiveScope?.ScopeId));
///
/// Highlights the zone, or takes the highlight away.
@@ -203,12 +332,11 @@ public partial class PathDropZone : MSGComponentBase
///
private void ApplyHighlight(bool shouldBeHighlighted)
{
- var highlighted = shouldBeHighlighted && !this.Disabled;
+ var highlighted = shouldBeHighlighted && !this.Disabled();
if (highlighted == this.isHighlighted)
return;
this.isHighlighted = highlighted;
- this.dragClass = highlighted ? $"{DEFAULT_DRAG_CLASS} mud-border-primary border-2" : DEFAULT_DRAG_CLASS;
this.StateHasChanged();
}
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor b/app/MindWork AI Studio/Components/ReadFileContent.razor
index a2b7f3db..bd1ba11a 100644
--- a/app/MindWork AI Studio/Components/ReadFileContent.razor
+++ b/app/MindWork AI Studio/Components/ReadFileContent.razor
@@ -2,39 +2,40 @@
@if (this.EnableDragDrop)
{
-
+
+
+ @if (this.ShowAttachedDocumentState && this.hasLoadedFileContent)
+ {
+
+
+
+ @this.ButtonText
+
+
+
+ }
+ else
+ {
+
+ @this.ButtonText
+
+ }
+
+ @if (this.IsCurrentTargetBusy)
+ {
+
+ }
+ else
+ {
+
+ @T("Drop one file here to load its content.")
+
+ }
+
+
}
else
{
diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs
index 8ecf467e..aa666217 100644
--- a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs
+++ b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs
@@ -57,12 +57,6 @@ public partial class ReadFileContent : MSGComponentBase
[Parameter]
public bool CatchAllDocuments { get; set; }
- ///
- /// The area this component lives in, if it lives in one at all.
- ///
- [CascadingParameter]
- private DropZoneScopeState? Scope { get; set; }
-
///
/// Optionally restricts the file types offered by the native file picker
/// and accepted by this component.
@@ -85,15 +79,7 @@ public partial class ReadFileContent : MSGComponentBase
[Inject]
private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
- private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-3 mb-3 mud-width-full";
-
- private readonly string dropZoneId = $"read-file-content-{Guid.NewGuid():N}";
-
private string ButtonText => string.IsNullOrWhiteSpace(this.Text) ? T("Use file content as input") : this.Text;
- private string dragClass = DEFAULT_DRAG_CLASS;
- private bool isDefaultZone;
- private bool isHighlighted;
- private bool hasReportedDefaultZoneProblem;
private bool isFileDialogOpen;
private bool hasLoadedFileContent;
private string loadedFileName = string.Empty;
@@ -121,15 +107,13 @@ public partial class ReadFileContent : MSGComponentBase
this.loadedFileName = string.Empty;
}
- this.UpdateDefaultZoneRole();
base.OnParametersSet();
}
protected override async Task OnInitializedAsync()
{
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
- if (this.EnableDragDrop)
- this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]);
+ this.ApplyFilters([], []);
await base.OnInitializedAsync();
await this.SyncCompletedMediaTextAsync();
@@ -194,139 +178,15 @@ public partial class ReadFileContent : MSGComponentBase
this.MediaTranscriptionService.AcknowledgeDelivery(delivery);
}
- /// Unsubscribes from the singleton media service and releases the drop area.
+ /// Unsubscribes from the singleton media service.
protected override void DisposeResources()
{
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
-
- // Hand the role of the default target back to the area:
- if (this.isDefaultZone)
- this.Scope?.ReleaseDefaultZone(this);
-
base.DisposeResources();
}
- protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
- {
- if (!this.EnableDragDrop)
- return;
-
- switch (triggeredEvent)
- {
- case Event.HIGHLIGHT_DROP_ZONE when data is DropZoneHighlight highlight:
- this.ApplyHighlight(this.IsThisZone(highlight.ZoneId));
- break;
-
- case Event.PATHS_DROPPED when data is DroppedPaths dropped:
- // Whoever the drop was meant for, the drag is over and no zone stays highlighted:
- this.ApplyHighlight(false);
-
- if (!this.IsThisZone(dropped.ZoneId))
- return;
-
- if (this.IsUnavailable)
- {
- this.Logger.LogDebug("The file zone '{ZoneId}' is unavailable and swallowed {Count} dropped path(s).", this.dropZoneId, dropped.Paths.Count);
- return;
- }
-
- await this.LoadFirstValidFile(dropped.Paths);
- this.StateHasChanged();
- break;
- }
- }
-
#endregion
- ///
- /// Keeps the role of the default target in step with the CatchAllDocuments parameter.
- ///
- ///
- /// The flag is a parameter, so it can change while this component lives. A zone inside a
- /// collapsed panel is the case this exists for: MudBlazor leaves the content of a collapsed
- /// panel in the DOM with a height of zero, so the zone stays alive and cannot be aimed at --
- /// yet it would keep the role and swallow every drop meant for the visible part of the page.
- ///
- private void UpdateDefaultZoneRole()
- {
- if (this.EnableDragDrop && this.CatchAllDocuments)
- {
- this.ClaimDefaultZoneRole();
- return;
- }
-
- if (!this.isDefaultZone)
- return;
-
- this.Scope?.ReleaseDefaultZone(this);
- this.isDefaultZone = false;
- }
-
- ///
- /// Asks the area for the role of its default target.
- ///
- private void ClaimDefaultZoneRole()
- {
- if (this.isDefaultZone)
- return;
-
- if (this.Scope is null)
- {
- //
- // There is nothing to claim: the surrounding page, assistant, or dialog is not a drop
- // area at all. The flag would then do nothing, and silently -- which is how a zone ends
- // up promising a behaviour it cannot deliver. So say it out loud: either the area needs
- // a DropZoneScope, or the flag does not belong here. Reported once only, because the
- // claim is retried on every parameter change.
- //
- if (!this.hasReportedDefaultZoneProblem)
- {
- this.hasReportedDefaultZoneProblem = true;
- this.Logger.LogWarning("The file zone '{ZoneId}' wants to be the default target of its area, but it does not live in a drop zone scope. Dropping next to this zone will do nothing.", this.dropZoneId);
- }
-
- return;
- }
-
- this.isDefaultZone = this.Scope.TryBecomeDefaultZone(this);
-
- // Losing the role to a neighbour is a decision, not a defect -- and it can be undone later,
- // when that neighbour goes away. So this one only goes to the debug log, and only once:
- if (this.isDefaultZone || this.hasReportedDefaultZoneProblem)
- return;
-
- this.hasReportedDefaultZoneProblem = true;
- this.Logger.LogDebug("The file zone '{ZoneId}' asked to be the default target of its area, which another zone already is. It now takes only the drops aimed at itself.", this.dropZoneId);
- }
-
- ///
- /// Decides whether the named zone is this one.
- ///
- ///
- /// The area counts as this zone as long as this zone is its default target. That is the whole
- /// mechanism behind dropping anywhere in an assistant and still landing here.
- ///
- /// The ID the hit test reported, or null when it hit nothing.
- private bool IsThisZone(string? zoneId) => zoneId is not null && (zoneId == this.dropZoneId || (this.isDefaultZone && zoneId == this.Scope?.ScopeId));
-
- ///
- /// Highlights the zone, or takes the highlight away.
- ///
- ///
- /// The comparison is not for tidiness: a throttled drag-over event arrives about ten times per
- /// second, and without it every one of them would render every zone on the page anew.
- ///
- private void ApplyHighlight(bool shouldBeHighlighted)
- {
- var highlighted = shouldBeHighlighted && !this.IsUnavailable;
- if (highlighted == this.isHighlighted)
- return;
-
- this.isHighlighted = highlighted;
- this.dragClass = highlighted ? $"{DEFAULT_DRAG_CLASS} mud-border-primary border-2" : DEFAULT_DRAG_CLASS;
- this.StateHasChanged();
- }
-
private async Task SelectFile()
{
if (this.IsUnavailable)
diff --git a/app/MindWork AI Studio/Components/SelectDirectory.razor b/app/MindWork AI Studio/Components/SelectDirectory.razor
index 73731fe3..5025861d 100644
--- a/app/MindWork AI Studio/Components/SelectDirectory.razor
+++ b/app/MindWork AI Studio/Components/SelectDirectory.razor
@@ -2,7 +2,7 @@
@if (this.EnableDragDrop)
{
-
+
@this.Picker
@T("You can also drag & drop the folder here.")
diff --git a/app/MindWork AI Studio/Components/SelectFile.razor b/app/MindWork AI Studio/Components/SelectFile.razor
index 1450f064..b6b0eb6c 100644
--- a/app/MindWork AI Studio/Components/SelectFile.razor
+++ b/app/MindWork AI Studio/Components/SelectFile.razor
@@ -2,7 +2,7 @@
@if (this.EnableDragDrop)
{
-
+
@this.Picker
@T("You can also drag & drop the file here.")
diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
index c2b8f965..638e9b9c 100644
--- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
@@ -4,7 +4,9 @@
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
-
+ @* The name for the drop state is given although nobody uses it: the messages of this dialog
+ are listed in a table, whose rows would otherwise ask for the same name. *@
+
@T("Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements.")
@@ -203,7 +205,7 @@
}
-
+
@if (this.IsReadOnly)
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
index 4cda2ad2..ad4f7c0f 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
@@ -6,7 +6,7 @@
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
-
+
@* ReSharper disable once CSharpWarnings::CS8974 *@
-
+
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor
index 96d58c07..3a94c100 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor
@@ -6,7 +6,7 @@
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
-
+
@* ReSharper disable once CSharpWarnings::CS8974 *@
-
+
diff --git a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor
index f6facdb3..b1851241 100644
--- a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor
@@ -3,7 +3,7 @@
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
-
+
@T("See how we load your file. Review the content before we process it further.")
@@ -102,7 +102,7 @@
}
}
-
+
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
index abab1c02..c68403b7 100644
--- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
@@ -13,7 +13,7 @@
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
-
+
@@ -88,7 +88,7 @@
-
+ @T("Close")
diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor
index e6c41206..e6106c30 100644
--- a/app/MindWork AI Studio/Pages/Chat.razor
+++ b/app/MindWork AI Studio/Pages/Chat.razor
@@ -4,7 +4,7 @@
@* The chat is a drop area: a file dropped anywhere in it hangs itself on the composer, which is
what users are used to. *@
-
+
@@ -168,4 +168,4 @@
}
-
+
diff --git a/app/MindWork AI Studio/Pages/Plugins.razor b/app/MindWork AI Studio/Pages/Plugins.razor
index 818647e9..65223482 100644
--- a/app/MindWork AI Studio/Pages/Plugins.razor
+++ b/app/MindWork AI Studio/Pages/Plugins.razor
@@ -5,8 +5,13 @@
@attribute [Route(Routes.PLUGINS)]
@* The page is its own drop target: a plugin archive may be dropped anywhere on it, and there is no
- inner zone to hand that role to. Hence the page owns the scope state instead of reading it. *@
-
+ inner zone to hand that role to. An area which takes drops itself is that target by definition. *@
+
@T("Plugins")
@@ -26,7 +31,7 @@
-
+
@@ -155,4 +160,4 @@
-
+
diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs
index 5eac67b6..cddf43c7 100644
--- a/app/MindWork AI Studio/Pages/Plugins.razor.cs
+++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs
@@ -42,14 +42,6 @@ public partial class Plugins : MSGComponentBase
private bool isSharingPlugin;
- ///
- /// The drop area of this page. The page owns it rather than reading it from a cascading value,
- /// because a component cannot read what it cascades itself.
- ///
- private readonly DropZoneScopeState dropZoneScope = new($"plugins-page-{Guid.NewGuid():N}");
-
- private bool isDraggingOverPage;
-
private const string IMPORT_ICON =
@"
- private string PluginTableClass => this.isDraggingOverPage
+ /// Whether the page is the target of the drop being aimed right now.
+ private static string PluginTableClass(bool isDropTarget) => isDropTarget
? "border-dashed border rounded-lg mud-border-primary border-4"
: "border-dashed border rounded-lg";
@@ -570,54 +554,11 @@ public partial class Plugins : MSGComponentBase
case Event.CONFIGURATION_CHANGED:
await this.InvokeAsync(this.StateHasChanged);
break;
-
- case Event.HIGHLIGHT_DROP_ZONE when data is DropZoneHighlight highlight:
- this.ApplyHighlight(this.IsThisZone(highlight.ZoneId));
- break;
-
- case Event.PATHS_DROPPED when data is DroppedPaths dropped:
- // Whoever the drop was meant for, the drag is over and nothing stays highlighted:
- this.ApplyHighlight(false);
-
- if (!this.IsThisZone(dropped.ZoneId))
- return;
-
- if (!this.CanCatchDroppedFile())
- {
- LOG.LogDebug("The plugins page cannot import right now and swallowed {Count} dropped path(s).", dropped.Paths.Count);
- return;
- }
-
- await this.ImportDroppedPluginArchiveAsync(dropped.Paths);
- break;
}
}
#endregion
- ///
- /// Decides whether the named zone is this page.
- ///
- /// The ID the hit test reported, or null when it hit nothing.
- private bool IsThisZone(string? zoneId) => zoneId is not null && zoneId == this.dropZoneScope.ScopeId;
-
- ///
- /// Marks the page as the drop target, or takes that mark away.
- ///
- ///
- /// The comparison is not for tidiness: a throttled drag-over event arrives about ten times per
- /// second, and without it every one of them would render the whole plugin table anew.
- ///
- private void ApplyHighlight(bool shouldBeHighlighted)
- {
- var highlighted = shouldBeHighlighted && this.CanCatchDroppedFile();
- if (highlighted == this.isDraggingOverPage)
- return;
-
- this.isDraggingOverPage = highlighted;
- this.StateHasChanged();
- }
-
///
/// Decides whether this page may process dropped files: only when the organization allows
/// importing plugins at all and no import is running.