diff --git a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor
index 6a620049..d653cb11 100644
--- a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor
+++ b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor
@@ -3,6 +3,7 @@
+
@foreach (var contentLine in this.contentLines)
diff --git a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs
index aefdbb48..6949d517 100644
--- a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs
+++ b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs
@@ -279,6 +279,20 @@ public partial class AssistantAgenda : AssistantBaseCore
#endregion
+ ///
+ /// Takes over a content list which came from a file or from a drop.
+ ///
+ ///
+ /// Assigning the text is not enough: the two topic selections below it are derived from the
+ /// content list, so the derivation has to run again, exactly as it does when the user types.
+ ///
+ /// The loaded content list.
+ private void ContentLoadedFromFile(string content)
+ {
+ this.inputContent = content;
+ this.OnContentChanged(content);
+ }
+
private void OnContentChanged(string content)
{
var previousSelectedFoci = new HashSet();
diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor
index 97319b33..3a866034 100644
--- a/app/MindWork AI Studio/Assistants/AssistantBase.razor
+++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor
@@ -2,7 +2,9 @@
@inherits AssistantLowerBase
@typeparam TSettings
-
diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor
index 99641135..99738648 100644
--- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor
+++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor
@@ -74,7 +74,7 @@ else
@T("Documents for the analysis")
-
+
}
else
@@ -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
-
+
}
break;
@@ -169,9 +169,8 @@ else
}
diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs
index 40c8b0a2..82ffae9b 100644
--- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs
+++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs
@@ -441,6 +441,41 @@ public partial class AssistantDynamic : AssistantBaseCore
return rootComponent is null ? prompt : this.CollectUserPromptFallback(rootComponent.Children);
}
+ ///
+ /// Whether this assistant has exactly one drop zone, which is what allows that zone to be the
+ /// default target of the whole assistant.
+ ///
+ ///
+ /// With a single zone, a drop anywhere in the assistant can only mean that one, so the habitual
+ /// "just drop it somewhere" keeps working. With several, it would be a guess: the first zone in
+ /// the markup would take the files meant for its neighbour, which is the very defect that hit
+ /// testing exists to remove. So no zone gets the role and every drop has to be aimed. A plugin
+ /// cannot opt out of this, and it does not have to know about it either.
+ /// The count is walked per render rather than cached: an assistant holds a few dozen components
+ /// at most, and a stale count would be a defect nobody would look for.
+ ///
+ private bool HasSingleDropZone => this.RootComponent is not null && CountDropZones(this.RootComponent.Children) is 1;
+
+ ///
+ /// Counts the components which accept a drop, including those nested inside layout components.
+ ///
+ /// The components to look through.
+ /// The number of drop zones.
+ private static int CountDropZones(IEnumerable components)
+ {
+ var count = 0;
+ foreach (var component in components)
+ {
+ if (component.Type is AssistantComponentType.FILE_CONTENT_READER or AssistantComponentType.FILE_ATTACHMENTS)
+ count++;
+
+ if (component.Children.Count > 0)
+ count += CountDropZones(component.Children);
+ }
+
+ return count;
+ }
+
private void InitializeComponentState(IEnumerable components)
{
foreach (var component in components)
diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor
index 15a39c90..6c87a020 100644
--- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor
+++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor
@@ -345,4 +345,4 @@ else
-
+
diff --git a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor
index d98e8645..873a1126 100644
--- a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor
+++ b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor
@@ -1,7 +1,7 @@
@attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)]
@inherits AssistantBaseCore
-
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 82e9864f..7f71d85e 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -208,6 +208,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::AGENDA::ASSISTANTAGENDA::T3292480692"] =
-- Approx. duration of the coffee or tea breaks
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::AGENDA::ASSISTANTAGENDA::T3310841480"] = "Approx. duration of the coffee or tea breaks"
+-- Load the content list from file
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::AGENDA::ASSISTANTAGENDA::T3481935567"] = "Load the content list from file"
+
-- Please provide a duration for the meeting or the seminar, e.g. '2 hours', or '2 days (8 hours and 4 hours)', etc.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::AGENDA::ASSISTANTAGENDA::T3535835316"] = "Please provide a duration for the meeting or the seminar, e.g. '2 hours', or '2 days (8 hours and 4 hours)', etc."
@@ -1948,12 +1951,24 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T191133
-- Describe what the person is supposed to do in the company. This might be just short bullet points.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T1965813611"] = "Describe what the person is supposed to do in the company. This might be just short bullet points."
+-- Load the job description from file
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T2063282133"] = "Load the job description from file"
+
-- Describe what the person should bring to the table. This might be just short bullet points.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T2223185050"] = "Describe what the person should bring to the table. This might be just short bullet points."
-- Target language
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T237828418"] = "Target language"
+-- Load the qualifications from file
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T2397083402"] = "Load the qualifications from file"
+
+-- Load the mandatory information from file
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T2682260465"] = "Load the mandatory information from file"
+
+-- Load the responsibilities from file
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T2719419106"] = "Load the responsibilities from file"
+
-- Create a job posting for {0} based on the following job description:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T3001516791"] = "Create a job posting for {0} based on the following job description:"
@@ -1990,6 +2005,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T397204
-- Create a job posting based on the following job description:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::JOBPOSTING::ASSISTANTJOBPOSTINGS::T795506638"] = "Create a job posting based on the following job description:"
+-- Load your questions from file
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T1089229279"] = "Load your questions from file"
+
-- Please provide a legal document as input. You might copy the desired text from a document or a website.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T1160217683"] = "Please provide a legal document as input. You might copy the desired text from a document or a website."
@@ -2002,6 +2020,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T1887742
-- Your questions
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T1947954583"] = "Your questions"
+-- Load the legal document from file
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T3754447262"] = "Load the legal document from file"
+
-- Provide a legal document and ask a question about it. This assistant does not replace legal advice. Consult a lawyer to get professional advice. Remember that LLMs can invent answers and facts. Please do not rely on this answers.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4016275181"] = "Provide a legal document and ask a question about it. This assistant does not replace legal advice. Consult a lawyer to get professional advice. Remember that LLMs can invent answers and facts. Please do not rely on this answers."
@@ -2254,6 +2275,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER
-- Prompting Guideline
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T4250996615"] = "Prompting Guideline"
+-- Load the prompt from file
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T466548446"] = "Load the prompt from file"
+
-- Use sequential steps
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::PROMPTOPTIMIZER::ASSISTANTPROMPTOPTIMIZER::T487578804"] = "Use sequential steps"
@@ -5551,6 +5575,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The cha
-- Please enter a name for the chat template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template."
+-- Load predefined user input from file
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load predefined user input from file"
+
-- Update
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Update"
@@ -6754,6 +6781,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T914647109"] = "Sends da
-- Destination
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T994314591"] = "Destination"
+-- Load what the AI should do from file
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1254789334"] = "Load what the AI should do from file"
+
-- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally."
@@ -6805,6 +6835,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T900713019"] = "Cancel"
-- The profile name must be unique; the chosen name is already in use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T911748898"] = "The profile name must be unique; the chosen name is already in use."
+-- Load what the AI should know from file
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T924460588"] = "Load what the AI should know from file"
+
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINGGUIDELINEDIALOG::T3448155331"] = "Close"
diff --git a/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor b/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor
index d3499d3a..27063fcd 100644
--- a/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor
+++ b/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor
@@ -3,9 +3,14 @@
+@* Four zones, so no default target: every drop has to be aimed at the field it belongs to. *@
+
+
+
+
diff --git a/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor b/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor
index fb7261e7..95121656 100644
--- a/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor
+++ b/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor
@@ -6,7 +6,10 @@
}
-
+@* Two zones, so no default target: the user has to aim at the one they mean. *@
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor
index 4a738ef7..78e32699 100644
--- a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor
+++ b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor
@@ -8,7 +8,7 @@
@T("You can enter text, attach one or more documents, or use both. At least one input is required.")
-
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor b/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor
index b2c1d3b1..5c0e1268 100644
--- a/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor
+++ b/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor
@@ -1,6 +1,9 @@
@attribute [Route(Routes.ASSISTANT_PROMPT_OPTIMIZER)]
@inherits AssistantBaseCore
+@* No default target in this assistant: the prompt guide below is a zone of its own, so a drop has
+ to be aimed at the one it belongs to. *@
+
-
+
diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor
index e451ab3d..2d1d56de 100644
--- a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor
+++ b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor
@@ -8,7 +8,7 @@
@T("Attach documents")
-
+ @T("Details about the desired presentation")
diff --git a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor
index 42fde1aa..862bb856 100644
--- a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor
+++ b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor
@@ -6,7 +6,7 @@
}
-
+
diff --git a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor
index 305be9b6..525aa2ff 100644
--- a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor
+++ b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor
@@ -6,7 +6,7 @@
}
-
+
@if (this.liveTranslation)
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor
index cfcc28dc..f0e8f378 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor
@@ -121,11 +121,11 @@
@T("Source material")@T("Documents, spreadsheets, images, audio, and video are considered as source context.")
+ @* No default target on purpose: with two zones side by side, the
+ user has to aim at the one they mean. *@
@@ -136,7 +136,6 @@
@T("Visual assets")@T("PNG, JPEG, and WebP assets are analyzed and must appear visibly in the briefing.")
-
+ 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 e3e3035a..c3895fa2 100644
--- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
+++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
@@ -24,18 +24,6 @@ public partial class AttachDocuments : MSGComponentBase
[Parameter]
public string Name { get; set; } = string.Empty;
- ///
- /// On which layer to register the drop area. Higher layers have priority over lower layers.
- ///
- [Parameter]
- public int Layer { get; set; }
-
- ///
- /// When true, pause catching dropped files. Default is false.
- ///
- [Parameter]
- public bool PauseCatchingDrops { get; set; }
-
[Parameter]
public HashSet DocumentPaths { get; set; } = [];
@@ -46,8 +34,14 @@ public partial class AttachDocuments : MSGComponentBase
public Func, Task> OnChange { get; set; } = _ => Task.CompletedTask;
///
- /// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
+ /// Makes this component the default target of its area, meaning of its page, assistant, or
+ /// dialog: it then also takes the drops which land anywhere in that area without hitting a zone
+ /// of their own.
///
+ ///
+ /// Only one zone per area can hold that role, and if several ask for it, the first one in the
+ /// markup gets it.
+ ///
[Parameter]
public bool CatchAllDocuments { get; set; }
@@ -105,9 +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 uint numDropAreasAboveThis;
- private bool isComponentHovered;
- private bool isDraggingOver;
private bool isFileDialogOpen;
private MediaImportOwner EffectiveImportOwner => this.OwnerChat is not null
? MediaImportOwner.ForChat(this.OwnerChat.ChatId)
@@ -122,10 +113,8 @@ public partial class AttachDocuments : MSGComponentBase
protected override async Task OnInitializedAsync()
{
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
- this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
+ this.ApplyFilters([], []);
- // Register this drop area:
- await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
await base.OnInitializedAsync();
}
@@ -222,104 +211,21 @@ public partial class AttachDocuments : MSGComponentBase
protected override void DisposeResources()
{
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
-
- // Release the drop area. Without this, drop areas below this one would count this component
- // forever and would stop catching dropped files:
- this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, this.Layer).Observe($"{nameof(AttachDocuments)}: releasing the drop area");
-
base.DisposeResources();
}
- protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
- {
- if (this.IsUnavailable && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
- return;
-
- switch (triggeredEvent)
- {
- case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
- {
- if(data is int layer && layer > this.Layer)
- {
- this.numDropAreasAboveThis++;
- this.PauseCatchingDrops = true;
- }
-
- break;
- }
-
- case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
- {
- if(data is int layer && layer > this.Layer)
- {
- if(this.numDropAreasAboveThis > 0)
- this.numDropAreasAboveThis--;
-
- if(this.numDropAreasAboveThis is 0)
- this.PauseCatchingDrops = false;
- }
-
- break;
- }
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
- if(this.PauseCatchingDrops)
- return;
-
- if(!this.isComponentHovered && !this.CatchAllDocuments)
- {
- this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop hovered event.", this.Name);
- return;
- }
-
- this.isDraggingOver = true;
- this.SetDragClass();
- this.StateHasChanged();
- break;
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
- if(this.PauseCatchingDrops)
- return;
-
- this.isDraggingOver = false;
- this.StateHasChanged();
- break;
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
- if(this.PauseCatchingDrops)
- return;
-
- this.isDraggingOver = false;
- this.isComponentHovered = false;
- this.ClearDragClass();
- this.StateHasChanged();
- break;
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
- if(this.PauseCatchingDrops)
- return;
-
- if(!this.isComponentHovered && !this.CatchAllDocuments)
- {
- this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop dropped event.", this.Name);
- return;
- }
-
- await this.AddFileBatchAsync(paths);
- await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
- await this.OnChange(this.DocumentPaths);
- this.isDraggingOver = false;
- this.ClearDragClass();
- this.StateHasChanged();
- break;
- }
- }
-
#endregion
- 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;
+ ///
+ /// 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)
+ {
+ await this.AddFileBatchAsync(paths);
+ await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
+ await this.OnChange(this.DocumentPaths);
+ }
private async Task AddFilesManually()
{
@@ -370,32 +276,6 @@ public partial class AttachDocuments : MSGComponentBase
await this.OnChange(this.DocumentPaths);
}
- private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-4";
-
- private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
-
- private void OnMouseEnter(EventArgs _)
- {
- if(this.IsUnavailable || this.PauseCatchingDrops)
- return;
-
- this.Logger.LogDebug("Attach documents component '{Name}' is hovered.", this.Name);
- this.isComponentHovered = true;
- this.SetDragClass();
- this.StateHasChanged();
- }
-
- private void OnMouseLeave(EventArgs _)
- {
- if(this.IsUnavailable || this.PauseCatchingDrops)
- return;
-
- this.Logger.LogDebug("Attach documents component '{Name}' is no longer hovered.", this.Name);
- this.isComponentHovered = false;
- this.ClearDragClass();
- this.StateHasChanged();
- }
-
private async Task RemoveDocument(FileAttachment fileAttachment)
{
if (this.IsUnavailable)
diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor
index 740bcde3..ad7bd9e0 100644
--- a/app/MindWork AI Studio/Components/ChatComponent.razor
+++ b/app/MindWork AI Studio/Components/ChatComponent.razor
@@ -104,7 +104,7 @@
}
-
+
diff --git a/app/MindWork AI Studio/Components/DropZoneArbiter.razor b/app/MindWork AI Studio/Components/DropZoneArbiter.razor
new file mode 100644
index 00000000..5538381d
--- /dev/null
+++ b/app/MindWork AI Studio/Components/DropZoneArbiter.razor
@@ -0,0 +1,3 @@
+@inherits MSGComponentBase
+
+@* This component renders nothing on purpose. Its whole work is in DropZoneArbiter.razor.cs. *@
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/DropZoneArbiter.razor.cs b/app/MindWork AI Studio/Components/DropZoneArbiter.razor.cs
new file mode 100644
index 00000000..04ad350c
--- /dev/null
+++ b/app/MindWork AI Studio/Components/DropZoneArbiter.razor.cs
@@ -0,0 +1,199 @@
+using AIStudio.Tools.Rust;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Components;
+
+///
+/// Decides which drop zone a native drag and drop event was aimed at.
+///
+///
+///
+/// AI Studio knows no browser drag and drop. The Tauri runtime reports the native events together
+/// with the cursor position, and this component turns that position into the ID of the zone
+/// underneath. It lets the browser answer that question, because only the browser knows what the
+/// page looks like right now: which dialog is open, which zone is scrolled out of sight, which
+/// overlay is in the way.
+///
+///
+/// It renders nothing and exists once per circuit, rendered from Routes.razor beside the MudBlazor
+/// providers and thus outside the router. A component rather than a service, because a service has
+/// no reliable moment at which JS interop becomes possible; and not a part of MainLayout, because
+/// arbitration would be a foreign body in that file.
+///
+///
+/// There is deliberately no fallback for a hit test which cannot be carried out: a circuit whose
+/// browser is gone learns nothing about the page and must therefore do nothing. The app keeps
+/// disconnected circuits for a long time, see the retention settings in Program.cs, and the message
+/// bus reaches all of them. Anything which caught a drop without asking the browser would process
+/// one and the same drop once per circuit.
+///
+///
+public partial class DropZoneArbiter : MSGComponentBase
+{
+ [Inject]
+ private IJSRuntime JsRuntime { get; init; } = null!;
+
+ [Inject]
+ private ILogger Logger { get; init; } = null!;
+
+ ///
+ /// Which zone we named last, so that an unchanged highlight costs no message.
+ ///
+ private string? highlightedZoneId;
+
+ ///
+ /// True while a hit test for the highlight is on its way to the browser.
+ ///
+ private bool isHighlightHitTestRunning;
+
+ #region Overrides of MSGComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED ]);
+ await base.OnInitializedAsync();
+ }
+
+ protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
+ {
+ switch (triggeredEvent)
+ {
+ //
+ // A drag entered the window or moved inside it. Both say where the cursor is, and
+ // nothing more, so both lead to the same question: which zone lights up?
+ //
+ case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED or TauriEventType.FILE_DROP_OVER } tauriEvent:
+ await this.MoveHighlight(tauriEvent);
+ break;
+
+ case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths } tauriEvent:
+ await this.DeliverDroppedPaths(tauriEvent, paths);
+ break;
+
+ //
+ // The drag left the window, or the window lost the focus while a drag was running. Tauri
+ // reports no position for either, and there is nothing left to aim at anyway.
+ //
+ case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED or TauriEventType.WINDOW_NOT_FOCUSED }:
+ await this.NameHighlightedZone(null);
+ break;
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Highlights the zone under the cursor of a running drag.
+ ///
+ ///
+ /// A drag-over event arrives faster than one interop round trip takes, and the message bus
+ /// delivers without awaiting the receiver. A hit test which is still on its way therefore
+ /// suppresses the next one instead of queueing it: the following event catches up with the
+ /// movement anyway, and a queue would only ever fall further behind the cursor.
+ ///
+ private async Task MoveHighlight(TauriEvent tauriEvent)
+ {
+ if (this.isHighlightHitTestRunning)
+ return;
+
+ this.isHighlightHitTestRunning = true;
+ try
+ {
+ var (wasTested, zoneId) = await this.DetermineZoneUnderCursor(tauriEvent);
+ if (!wasTested)
+ return;
+
+ await this.NameHighlightedZone(zoneId);
+ }
+ finally
+ {
+ this.isHighlightHitTestRunning = false;
+ }
+ }
+
+ ///
+ /// Hands the dropped paths to the zone under the cursor, if there is one.
+ ///
+ ///
+ /// Unlike the highlight, this hit test is never suppressed: a drop happens once and must not be
+ /// lost. The highlight goes away first and in every case, because the drag is over no matter
+ /// whether the drop finds a zone.
+ ///
+ private async Task DeliverDroppedPaths(TauriEvent tauriEvent, List paths)
+ {
+ await this.NameHighlightedZone(null);
+
+ var (wasTested, zoneId) = await this.DetermineZoneUnderCursor(tauriEvent);
+ if (!wasTested)
+ return;
+
+ if (zoneId is null)
+ {
+ //
+ // Nothing under the cursor takes drops, so nothing happens -- which is the point of the
+ // whole exercise. The zones which were available are worth logging here, though: this is
+ // the one moment where the question "which one should it have been?" gets asked, and it
+ // happens once per drag rather than ten times a second.
+ //
+ if (this.Logger.IsEnabled(LogLevel.Debug))
+ {
+ var (_, availableZones) = await this.JsRuntime.TryInvokeAsync(this.CircuitState, "dropZones.list");
+ this.Logger.LogDebug("{Count} dropped path(s) reached no drop zone. Available zones: {Zones}", paths.Count, availableZones is null ? "unknown" : string.Join(", ", availableZones));
+ }
+
+ return;
+ }
+
+ this.Logger.LogDebug("{Count} path(s) were dropped on the zone '{ZoneId}'.", paths.Count, zoneId);
+ await this.SendMessage(Event.PATHS_DROPPED, new DroppedPaths(zoneId, paths));
+ }
+
+ ///
+ /// Tells the zones which one of them is under the cursor, unless they know already.
+ ///
+ /// The ID of the zone under the cursor, or null for none.
+ private async Task NameHighlightedZone(string? zoneId)
+ {
+ if (zoneId == this.highlightedZoneId)
+ return;
+
+ this.highlightedZoneId = zoneId;
+ await this.SendMessage(Event.HIGHLIGHT_DROP_ZONE, new DropZoneHighlight(zoneId));
+ }
+
+ ///
+ /// Asks the browser which drop zone lies under the cursor of a drag and drop event.
+ ///
+ ///
+ /// The two parts of the result must stay apart. Whether the browser answered at all comes first:
+ /// while a circuit is disconnected nobody answers, and acting on an answer we never got is
+ /// exactly the mistake this design exists to avoid. Only then comes what the answer was, and
+ /// there a null is a legitimate one -- the browser looked and found no zone.
+ ///
+ private async Task<(bool WasTested, string? ZoneId)> DetermineZoneUnderCursor(TauriEvent tauriEvent)
+ {
+ if (!tauriEvent.TryGetDropPosition(out var x, out var y))
+ {
+ // The runtime sends a position with every drag and drop event which has one, so this
+ // means we are talking to a runtime which does not, i.e. an older one:
+ this.Logger.LogWarning("The Tauri event {EventType} carried no cursor position, so the drop zone under it stays unknown.", tauriEvent.EventType);
+ return (false, null);
+ }
+
+ // A failed or skipped call is already logged by the extension method, which tells a
+ // disconnected circuit from a broken call. Nothing to add here, and nothing to do:
+ var hitTest = await this.JsRuntime.TryInvokeAsync(this.CircuitState, "dropZones.hitTest", x, y);
+
+ //
+ // One line per hit test, which is about ten per second while a drag lasts. That is the
+ // instrument for checking the coordinate space: the position has to follow the cursor, in
+ // the middle of the window as well as in all four corners, and on a display with a scale
+ // factor other than one. A mistake there shows up as a factor, an offset, or a mirrored y.
+ //
+ if (hitTest.WasInvoked)
+ this.Logger.LogDebug("The event {EventType} at ({X}, {Y}) hit the drop zone '{ZoneId}'.", tauriEvent.EventType, x, y, hitTest.Value ?? "");
+
+ return hitTest;
+ }
+}
\ 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 fc80731a..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 09d6b0b9..6b982000 100644
--- a/app/MindWork AI Studio/Components/PathDropZone.razor.cs
+++ b/app/MindWork AI Studio/Components/PathDropZone.razor.cs
@@ -1,5 +1,3 @@
-using AIStudio.Tools.Rust;
-
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
@@ -8,171 +6,337 @@ 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; }
///
- /// On which layer to register the drop area. Higher layers have priority over lower layers.
- ///
- [Parameter]
- public int Layer { get; set; } = DropLayers.ROOT;
-
- ///
- /// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
+ /// Makes this zone the default target of its area, meaning of its page, assistant, or dialog.
///
///
- /// Practically every zone needs this today. Hovering is detected through mouse events, and no
- /// webview delivers those while a native drag is in progress, so a zone without this flag
- /// hardly ever catches anything. The consequence is that two zones of the same layer cannot be
- /// told apart: the one carrying this flag takes every drop, including the ones meant for the
- /// other. A page may therefore hold only one zone per layer. Lifting that limit needs the
- /// cursor position, which the runtime receives from Tauri and currently discards in
- /// app_window.rs.
+ /// 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. 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.
///
///
- /// The drop area stays registered nevertheless. Releasing it during the lifetime of the
- /// component would lower the count of every zone below this one, and those zones would then
- /// catch files while this one is still on screen.
+ /// 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. 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.
+ ///
+ [CascadingParameter]
+ private DropZoneScopeState? Scope { get; set; }
[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 string dragClass = DEFAULT_DRAG_CLASS;
- private uint numDropAreasAboveThis;
- private bool isComponentHovered;
+ 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.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
- await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
+ //
+ // 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();
}
+ protected override void OnParametersSet()
+ {
+ this.UpdateDefaultZoneRole();
+ base.OnParametersSet();
+ }
+
///
- /// Releases the drop area.
+ /// Hands the role of the default target back to the area.
///
protected override void DisposeResources()
{
- // Without this, drop areas below this one would count this component forever and would
- // stop catching dropped files:
- this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, this.Layer).Observe($"{nameof(PathDropZone)}: releasing the drop area");
+ if (this.isDefaultZone)
+ this.EffectiveScope?.ReleaseDefaultZone(this);
base.DisposeResources();
}
protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
- // A disabled zone takes no files. It keeps track of the zones above it, though, because
- // those come and go while this one is disabled:
- if (this.Disabled && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
- return;
-
switch (triggeredEvent)
{
- case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
- {
- if(data is int layer && layer > this.Layer)
+ 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.Disabled())
{
- this.numDropAreasAboveThis++;
- this.ClearDragClass();
+ this.Logger.LogDebug("The drop zone '{ZoneId}' cannot take drops right now and swallowed {Count} dropped path(s).", this.zoneId, dropped.Paths.Count);
+ return;
}
- break;
- }
-
- case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
- {
- if(data is int layer && layer > this.Layer && this.numDropAreasAboveThis > 0)
- this.numDropAreasAboveThis--;
-
- break;
- }
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
- if(!this.CanCatchDroppedPath())
- return;
-
- this.SetDragClass();
- this.StateHasChanged();
- break;
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
- this.isComponentHovered = false;
- this.ClearDragClass();
- this.StateHasChanged();
- break;
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
- if(!this.CanCatchDroppedPath())
- return;
-
- this.Logger.LogDebug("The path drop zone on layer {Layer} caught {Count} path(s).", this.Layer, paths.Count);
- await this.OnPathsDropped.InvokeAsync(paths);
- this.ClearDragClass();
- this.StateHasChanged();
+ this.Logger.LogDebug("The drop zone '{ZoneId}' caught {Count} path(s).", this.zoneId, dropped.Paths.Count);
+ await this.OnPathsDropped.InvokeAsync(dropped.Paths);
break;
}
}
#endregion
- private bool CanCatchDroppedPath() => this.numDropAreasAboveThis is 0 && (this.isComponentHovered || this.CatchAllDocuments);
-
- private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-2";
-
- private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
-
- private void OnMouseEnter(EventArgs _)
+ ///
+ /// 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.Disabled || this.numDropAreasAboveThis > 0)
+ if (this.WantsDefaultZoneRole)
+ {
+ this.ClaimDefaultZoneRole();
+ return;
+ }
+
+ if (!this.isDefaultZone)
return;
- // A native drag delivers no DOM events at all, mouse events included. This fires before a
- // drag begins, while the pointer still moves freely, which makes it a hint about where the
- // user is aiming rather than a reliable signal. See the remarks on CatchAllDocuments:
- this.isComponentHovered = true;
- this.SetDragClass();
- this.StateHasChanged();
+ this.EffectiveScope?.ReleaseDefaultZone(this);
+ this.isDefaultZone = false;
}
- private void OnMouseLeave(EventArgs _)
+ ///
+ /// Asks the area for the role of its default target.
+ ///
+ private void ClaimDefaultZoneRole()
{
- if(this.Disabled)
+ if (this.isDefaultZone)
return;
- this.isComponentHovered = false;
- this.ClearDragClass();
+ 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 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
+ // the message on every render would bury the log.
+ //
+ if (!this.hasReportedDefaultZoneProblem)
+ {
+ this.hasReportedDefaultZoneProblem = true;
+ 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.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:
+ if (this.isDefaultZone || this.hasReportedDefaultZoneProblem)
+ return;
+
+ this.hasReportedDefaultZoneProblem = true;
+ 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);
+ }
+
+ ///
+ /// 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 a page and still landing here.
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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.Disabled();
+ if (highlighted == this.isHighlighted)
+ return;
+
+ this.isHighlighted = highlighted;
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 3b34fe5e..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 0895e133..aa666217 100644
--- a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs
+++ b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs
@@ -46,14 +46,14 @@ public partial class ReadFileContent : MSGComponentBase
public bool EnableDragDrop { get; set; }
///
- /// On which layer to register the drop area. Higher layers have priority over lower layers.
- ///
- [Parameter]
- public int Layer { get; set; }
-
- ///
- /// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
+ /// Makes this component the default target of its area, meaning of its page, assistant, or
+ /// dialog: it then also takes the drops which land anywhere in that area without hitting a zone
+ /// of their own.
///
+ ///
+ /// Only one zone per area can hold that role, and if several ask for it, the first one in the
+ /// markup gets it. The flag has no effect without drag and drop being enabled.
+ ///
[Parameter]
public bool CatchAllDocuments { get; set; }
@@ -79,12 +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 string ButtonText => string.IsNullOrWhiteSpace(this.Text) ? T("Use file content as input") : this.Text;
- private string dragClass = DEFAULT_DRAG_CLASS;
- private uint numDropAreasAboveThis;
- private bool isComponentHovered;
private bool isFileDialogOpen;
private bool hasLoadedFileContent;
private string loadedFileName = string.Empty;
@@ -118,11 +113,7 @@ public partial class ReadFileContent : MSGComponentBase
protected override async Task OnInitializedAsync()
{
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
- if (this.EnableDragDrop)
- {
- this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
- await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
- }
+ this.ApplyFilters([], []);
await base.OnInitializedAsync();
await this.SyncCompletedMediaTextAsync();
@@ -187,76 +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;
-
- // Release the drop area. Without this, drop areas below this one would count this component
- // forever and would stop catching dropped files:
- if (this.EnableDragDrop)
- this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, this.Layer).Observe($"{nameof(ReadFileContent)}: releasing the drop area");
-
base.DisposeResources();
}
- protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
- {
- if (!this.EnableDragDrop)
- return;
-
- if (this.IsUnavailable && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
- return;
-
- switch (triggeredEvent)
- {
- case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
- {
- if(data is int layer && layer > this.Layer)
- {
- this.numDropAreasAboveThis++;
- this.ClearDragClass();
- }
-
- break;
- }
-
- case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
- {
- if(data is int layer && layer > this.Layer && this.numDropAreasAboveThis > 0)
- this.numDropAreasAboveThis--;
-
- break;
- }
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
- if(!this.CanCatchDroppedFile())
- return;
-
- this.SetDragClass();
- this.StateHasChanged();
- break;
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
- this.isComponentHovered = false;
- this.ClearDragClass();
- this.StateHasChanged();
- break;
-
- case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
- if(!this.CanCatchDroppedFile())
- return;
-
- await this.LoadFirstValidFile(paths);
- this.ClearDragClass();
- this.StateHasChanged();
- break;
- }
- }
-
#endregion
-
+
private async Task SelectFile()
{
if (this.IsUnavailable)
@@ -417,31 +347,4 @@ public partial class ReadFileContent : MSGComponentBase
return string.Format(this.T("Attached file '{0}'."), this.loadedFileName);
}
- private bool CanCatchDroppedFile() => this.numDropAreasAboveThis is 0 && (this.isComponentHovered || this.CatchAllDocuments);
-
- private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-2";
-
- private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
-
- private void OnMouseEnter(EventArgs _)
- {
- if(this.IsUnavailable || this.numDropAreasAboveThis > 0)
- return;
-
- this.Logger.LogDebug("Read file content component is hovered.");
- this.isComponentHovered = true;
- this.SetDragClass();
- this.StateHasChanged();
- }
-
- private void OnMouseLeave(EventArgs _)
- {
- if(this.IsUnavailable)
- return;
-
- this.Logger.LogDebug("Read file content component is no longer hovered.");
- this.isComponentHovered = false;
- this.ClearDragClass();
- this.StateHasChanged();
- }
}
diff --git a/app/MindWork AI Studio/Components/SelectDirectory.razor b/app/MindWork AI Studio/Components/SelectDirectory.razor
index b078d474..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/SelectDirectory.razor.cs b/app/MindWork AI Studio/Components/SelectDirectory.razor.cs
index 883d5277..f66c5e43 100644
--- a/app/MindWork AI Studio/Components/SelectDirectory.razor.cs
+++ b/app/MindWork AI Studio/Components/SelectDirectory.razor.cs
@@ -34,13 +34,9 @@ public partial class SelectDirectory : MSGComponentBase
public bool EnableDragDrop { get; set; }
///
- /// On which layer to register the drop area. Higher layers have priority over lower layers.
- ///
- [Parameter]
- public int Layer { get; set; } = DropLayers.ROOT;
-
- ///
- /// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
+ /// Makes this component the default target of its area, meaning of its page, assistant, or
+ /// dialog: it then also takes the drops which land anywhere in that area without hitting a zone
+ /// of their own.
///
[Parameter]
public bool CatchAllDocuments { get; set; }
diff --git a/app/MindWork AI Studio/Components/SelectFile.razor b/app/MindWork AI Studio/Components/SelectFile.razor
index 4df232b8..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/Components/SelectFile.razor.cs b/app/MindWork AI Studio/Components/SelectFile.razor.cs
index 124f104f..d9845f85 100644
--- a/app/MindWork AI Studio/Components/SelectFile.razor.cs
+++ b/app/MindWork AI Studio/Components/SelectFile.razor.cs
@@ -38,13 +38,9 @@ public partial class SelectFile : MSGComponentBase
public bool EnableDragDrop { get; set; }
///
- /// On which layer to register the drop area. Higher layers have priority over lower layers.
- ///
- [Parameter]
- public int Layer { get; set; } = DropLayers.ROOT;
-
- ///
- /// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
+ /// Makes this component the default target of its area, meaning of its page, assistant, or
+ /// dialog: it then also takes the drops which land anywhere in that area without hitting a zone
+ /// of their own.
///
[Parameter]
public bool CatchAllDocuments { get; set; }
diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
index 8080114e..638e9b9c 100644
--- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
@@ -3,6 +3,10 @@
+ @* 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.")
@@ -57,7 +61,7 @@
@T("Use the default system prompt")
-
+
@T("Predefined User Input")
@@ -81,6 +85,7 @@
HelperText="@T("Tell the AI your predefined user input.")"
ReadOnly="@this.IsReadOnly"
/>
+
@T("File Attachments")
@@ -90,10 +95,8 @@
@@ -202,6 +205,7 @@
}
+
@if (this.IsReadOnly)
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
index 9679ead3..ad4f7c0f 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
@@ -5,6 +5,8 @@
+ @* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
+
@* ReSharper disable once CSharpWarnings::CS8974 *@
+
}
else
{
@@ -191,6 +193,7 @@
+
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor
index 965a51f6..3a94c100 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor
@@ -5,6 +5,8 @@
+ @* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
+
@* ReSharper disable once CSharpWarnings::CS8974 *@
+
}
else
{
@@ -191,6 +193,7 @@
+
diff --git a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor
index 403dd67e..b1851241 100644
--- a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor
@@ -2,13 +2,15 @@
+ @* 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.")
@if (this.Document is null)
{
-
+
}
else
{
@@ -100,6 +102,7 @@
}
}
+
diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor
index a711e084..b72d3135 100644
--- a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor
@@ -47,7 +47,7 @@
HelperText="@T("Tell the AI something about yourself. What is your profession? How experienced are you in this profession? Which technologies do you like?")"
ReadOnly="@this.IsReadOnly"
/>
-
+
-
+
@T("Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.")
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
index 9838bf7b..c68403b7 100644
--- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
@@ -12,6 +12,8 @@
+ @* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
+
@@ -24,12 +26,12 @@
@if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.FREE_PROMPT)
{
-
+
}
else if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.FILE_IMPORT)
{
-
+
}
else
@@ -86,6 +88,7 @@
+ @T("Close")
diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor
index a7b85d53..e6106c30 100644
--- a/app/MindWork AI Studio/Pages/Chat.razor
+++ b/app/MindWork AI Studio/Pages/Chat.razor
@@ -2,7 +2,9 @@
@using AIStudio.Settings.DataModel
@inherits MSGComponentBase
-
+@* The chat is a drop area: a file dropped anywhere in it hangs itself on the composer, which is
+ what users are used to. *@
+
@@ -166,4 +168,4 @@
}
-
+
diff --git a/app/MindWork AI Studio/Pages/Plugins.razor b/app/MindWork AI Studio/Pages/Plugins.razor
index 7bbe889e..65223482 100644
--- a/app/MindWork AI Studio/Pages/Plugins.razor
+++ b/app/MindWork AI Studio/Pages/Plugins.razor
@@ -4,7 +4,14 @@
@inherits MSGComponentBase
@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. An area which takes drops itself is that target by definition. *@
+
@T("Plugins")
@@ -24,7 +31,7 @@
-
+
@@ -153,4 +160,4 @@
-
+
diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs
index 3be2e933..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;
- ///
- /// Number of active drop areas above this page. While there is any, another component owns the
- /// dropped files and this page must not catch them.
- ///
- private uint numDropAreasAboveThis;
-
- private bool isDraggingOverPage;
-
private const string IMPORT_ICON =
@"