From b052810db766d0ece1de0983a9971d6837aec345 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 10 Sep 2026 13:26:07 +0200 Subject: [PATCH] Make the default drop target follow the open panel --- .../DocumentAnalysisAssistant.razor | 13 +++-- .../DocumentAnalysisAssistant.razor.cs | 37 +++++++++++- .../Components/AttachDocuments.razor.cs | 51 +++++++++++++--- .../Components/PathDropZone.razor.cs | 58 ++++++++++++++++--- .../Components/ReadFileContent.razor.cs | 53 ++++++++++++++--- 5 files changed, 184 insertions(+), 28 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor index 2e1dcfd8..99738648 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor @@ -128,7 +128,9 @@ else - + @* No default target for the rule zones: while the policy definition is open, three + zones are in play, so each drop has to be aimed at the one it belongs to. *@ + @T("After the AI has processed all documents, it needs your instructions on how the result should be formatted. Would you like a structured list with keywords or a continuous text? Should the output include emojis or be written in formal business language? You can specify all these preferences in the output rules. There, you can also predefine a desired structure—for example, by using Markdown formatting to define headings, paragraphs, or bullet points.") @@ -136,7 +138,7 @@ else - + @if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings) { @@ -153,7 +155,7 @@ else - + @T("Policy Description") @@ -166,7 +168,10 @@ else @T("Documents for the analysis") - + @* The whole assistant catches drops, but only while this panel is the open one. A + collapsed panel keeps its content in the DOM with a height of zero, so without this + the invisible zone would swallow the drops meant for the policy definition. *@ + diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index be9fc83a..e1067b43 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -244,6 +244,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore + /// Whether the document selection panel is the open one. + /// + /// + /// Only one of the two panels is ever open, so this is normally the opposite of the field above + /// -- but not always: the user can collapse both. It is tracked rather than derived because it + /// decides whether the document zone is the default target of the whole assistant, and a + /// collapsed zone must not hold that role. + /// + private bool documentSelectionExpanded; private string policyName = string.Empty; private string policyDescription = string.Empty; private string policyAnalysisRules = string.Empty; @@ -333,7 +345,11 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore this.selectedPolicy = value); state.Restore(POLICY_IS_PROTECTED_STATE_KEY, value => this.policyIsProtected = value); state.Restore(POLICY_HIDE_POLICY_DEFINITION_STATE_KEY, value => this.policyHidePolicyDefinition = value); - state.Restore(POLICY_DEFINITION_EXPANDED_STATE_KEY, value => this.policyDefinitionExpanded = value); + state.Restore(POLICY_DEFINITION_EXPANDED_STATE_KEY, value => + { + this.policyDefinitionExpanded = value; + this.documentSelectionExpanded = !value; + }); state.Restore(POLICY_NAME_STATE_KEY, value => this.policyName = value); state.Restore(POLICY_DESCRIPTION_STATE_KEY, value => this.policyDescription = value); state.Restore(POLICY_ANALYSIS_RULES_STATE_KEY, value => this.policyAnalysisRules = value); @@ -359,6 +375,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore this.OwnerChat is not null @@ -124,7 +125,6 @@ public partial class AttachDocuments : MSGComponentBase { this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged; this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]); - this.ClaimDefaultZoneRole(); await base.OnInitializedAsync(); } @@ -132,6 +132,7 @@ 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(); } @@ -262,11 +263,35 @@ public partial class AttachDocuments : MSGComponentBase #endregion /// - /// Asks the area for the role of its default target, if this component wants it. + /// 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.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.CatchAllDocuments) + if (this.isDefaultZone) return; if (this.Scope is null) @@ -275,15 +300,27 @@ public partial class AttachDocuments : MSGComponentBase // 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 DropZoneScope, or the flag does not belong here. Reported once only, because the + // claim is retried on every parameter change. // - 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); + 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); - if (!this.isDefaultZone) - 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); + + // 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); } /// diff --git a/app/MindWork AI Studio/Components/PathDropZone.razor.cs b/app/MindWork AI Studio/Components/PathDropZone.razor.cs index 6b0d6b2d..5f572d46 100644 --- a/app/MindWork AI Studio/Components/PathDropZone.razor.cs +++ b/app/MindWork AI Studio/Components/PathDropZone.razor.cs @@ -64,17 +64,22 @@ public partial class PathDropZone : MSGComponentBase private string dragClass = DEFAULT_DRAG_CLASS; private bool isDefaultZone; private bool isHighlighted; + private bool hasReportedDefaultZoneProblem; #region Overrides of MSGComponentBase protected override async Task OnInitializedAsync() { this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]); - this.ClaimDefaultZoneRole(); - await base.OnInitializedAsync(); } + protected override void OnParametersSet() + { + this.UpdateDefaultZoneRole(); + base.OnParametersSet(); + } + /// /// Hands the role of the default target back to the area. /// @@ -116,11 +121,35 @@ public partial class PathDropZone : MSGComponentBase #endregion /// - /// Asks the area for the role of its default target, if this zone wants it. + /// Keeps the role of the default target in step with the CatchAllDocuments parameter. + /// + /// + /// The flag is a parameter, so it can change while this zone 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 part of the page one can actually see. + /// + private void UpdateDefaultZoneRole() + { + if (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.CatchAllDocuments) + if (this.isDefaultZone) return; if (this.Scope is null) @@ -131,13 +160,28 @@ public partial class PathDropZone : MSGComponentBase // 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. // - 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); + // + // Reported once only: the claim is retried on every parameter change, and repeating + // the message on every render would bury the log. + // + 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); + } + return; } this.isDefaultZone = this.Scope.TryBecomeDefaultZone(this); - if (!this.isDefaultZone) - 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); + + // 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 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); } /// diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs index a64fc4c5..8ecf467e 100644 --- a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs +++ b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs @@ -93,6 +93,7 @@ public partial class ReadFileContent : MSGComponentBase 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; @@ -120,6 +121,7 @@ public partial class ReadFileContent : MSGComponentBase this.loadedFileName = string.Empty; } + this.UpdateDefaultZoneRole(); base.OnParametersSet(); } @@ -127,10 +129,7 @@ public partial class ReadFileContent : MSGComponentBase { this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged; if (this.EnableDragDrop) - { this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]); - this.ClaimDefaultZoneRole(); - } await base.OnInitializedAsync(); await this.SyncCompletedMediaTextAsync(); @@ -240,11 +239,35 @@ public partial class ReadFileContent : MSGComponentBase #endregion /// - /// Asks the area for the role of its default target, if this component wants it. + /// 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.CatchAllDocuments) + if (this.isDefaultZone) return; if (this.Scope is null) @@ -253,15 +276,27 @@ public partial class ReadFileContent : MSGComponentBase // 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 DropZoneScope, or the flag does not belong here. Reported once only, because the + // claim is retried on every parameter change. // - 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); + 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); - if (!this.isDefaultZone) - 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); + + // 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); } ///