From 38fe814ff141e1e5eaaa033db17bfb8ba4987909 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 6 Jul 2026 16:52:48 +0200 Subject: [PATCH] Added drag-and-drop support for loading file content --- .../AssistantGrammarSpelling.razor | 1 + .../Assistants/I18N/allTexts.lua | 6 + .../Components/ReadFileContent.razor | 32 ++- .../Components/ReadFileContent.razor.cs | 185 ++++++++++++++++-- .../plugin.lua | 6 + .../plugin.lua | 6 + 6 files changed, 208 insertions(+), 28 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor index 5d116797..d98e8645 100644 --- a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor +++ b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor @@ -1,6 +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 8d758b16..ccbdb8e3 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1405,6 +1405,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::PROGRAMMINGLANGUAGESEXTENSIONS::T342 -- Please provide a text as input. You might copy the desired text from a document or a website. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T137304886"] = "Please provide a text as input. You might copy the desired text from a document or a website." +-- Load text from file +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T2210807298"] = "Load text from file" + -- Proofread UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T2325568297"] = "Proofread" @@ -2797,6 +2800,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T900237532"] = "Provid -- Failed to load file content UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T1989554334"] = "Failed to load file content" +-- Drop one file here to load its content. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T2274562398"] = "Drop one file here to load its content." + -- Use file content as input UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T3499386973"] = "Use file content as input" diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor b/app/MindWork AI Studio/Components/ReadFileContent.razor index 302224de..27f979b0 100644 --- a/app/MindWork AI Studio/Components/ReadFileContent.razor +++ b/app/MindWork AI Studio/Components/ReadFileContent.razor @@ -1,11 +1,23 @@ @inherits MSGComponentBase - - @if (string.IsNullOrWhiteSpace(this.Text)) - { - @T("Use file content as input") - } - else - { - @this.Text - } - + +@if (this.EnableDragDrop) +{ +
+ + + + @this.ButtonText + + + @T("Drop one file here to load its content.") + + + +
+} +else +{ + + @this.ButtonText + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs index d3248937..25b864d0 100644 --- a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs +++ b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs @@ -1,3 +1,5 @@ +using AIStudio.Tools; +using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using AIStudio.Tools.Validation; @@ -18,6 +20,21 @@ public partial class ReadFileContent : MSGComponentBase [Parameter] public bool Disabled { get; set; } + + [Parameter] + 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. + /// + [Parameter] + public bool CatchAllDocuments { get; set; } [Inject] private RustService RustService { get; init; } = null!; @@ -30,12 +47,104 @@ public partial class ReadFileContent : MSGComponentBase [Inject] private PandocAvailabilityService PandocAvailabilityService { 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; + + #region Overrides of MSGComponentBase + + protected override async Task OnInitializedAsync() + { + 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); + } + + await base.OnInitializedAsync(); + } + + protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + { + if (!this.EnableDragDrop) + return; + + 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) + { + 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.Disabled) return; + if (!await this.EnsurePandocAvailability()) + return; + + var selectedFile = await this.RustService.SelectFile(T("Select file to read its content")); + if (selectedFile.UserCancelled) + { + this.Logger.LogInformation("User cancelled the file selection"); + return; + } + + await this.LoadFileIfValid(selectedFile.SelectedFilePath); + } + + private async Task EnsurePandocAvailability() + { // Ensure that Pandoc is installed and ready: var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync( showSuccessMessage: false, @@ -45,38 +154,78 @@ public partial class ReadFileContent : MSGComponentBase if (!pandocState.IsAvailable) { this.Logger.LogWarning("The user cancelled the Pandoc installation or Pandoc is not available. Aborting file selection."); - return; + return false; } - var selectedFile = await this.RustService.SelectFile(T("Select file to read its content")); - if (selectedFile.UserCancelled) - { - this.Logger.LogInformation("User cancelled the file selection"); + return true; + } + + private async Task LoadFirstValidFile(List paths) + { + if (!await this.EnsurePandocAvailability()) return; + + foreach (var path in paths) + { + if (await this.LoadFileIfValid(path)) + return; + } + } + + private async Task LoadFileIfValid(string filePath) + { + if(!File.Exists(filePath)) + { + this.Logger.LogWarning("Selected file does not exist: '{FilePath}'", filePath); + return false; } - if(!File.Exists(selectedFile.SelectedFilePath)) + if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.DIRECTLY_LOADING_CONTENT, filePath)) { - this.Logger.LogWarning("Selected file does not exist: '{FilePath}'", selectedFile.SelectedFilePath); - return; - } - - if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.DIRECTLY_LOADING_CONTENT, selectedFile.SelectedFilePath)) - { - this.Logger.LogWarning("User attempted to load unsupported file: {FilePath}", selectedFile.SelectedFilePath); - return; + this.Logger.LogWarning("User attempted to load unsupported file: {FilePath}", filePath); + return false; } try { - var fileContent = await UserFile.LoadFileData(selectedFile.SelectedFilePath, this.RustService, this.DialogService); + var fileContent = await UserFile.LoadFileData(filePath, this.RustService, this.DialogService); await this.FileContentChanged.InvokeAsync(fileContent); - this.Logger.LogInformation("Successfully loaded file content: {FilePath}", selectedFile.SelectedFilePath); + this.Logger.LogInformation("Successfully loaded file content: {FilePath}", filePath); + return true; } catch (Exception ex) { - this.Logger.LogError(ex, "Failed to load file content: {FilePath}", selectedFile.SelectedFilePath); + this.Logger.LogError(ex, "Failed to load file content: {FilePath}", filePath); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, T("Failed to load file content"))); + return false; } } -} + + private bool CanCatchDroppedFile() => this.numDropAreasAboveThis is 0 && (this.isComponentHovered || this.CatchAllDocuments); + + 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.Disabled || 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.Disabled) + return; + + this.Logger.LogDebug("Read file content component is no longer hovered."); + this.isComponentHovered = false; + this.ClearDragClass(); + this.StateHasChanged(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 964ae0e0..53d1ee1d 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1407,6 +1407,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::PROGRAMMINGLANGUAGESEXTENSIONS::T342 -- Please provide a text as input. You might copy the desired text from a document or a website. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T137304886"] = "Bitte geben Sie einen Text ein. Sie können den gewünschten Text aus einem Dokument oder einer Website kopieren." +-- Load text from file +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T2210807298"] = "Text aus Datei laden" + -- Proofread UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T2325568297"] = "Korrekturlesen" @@ -2799,6 +2802,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T900237532"] = "Anbiet -- Failed to load file content UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T1989554334"] = "Laden des Dateiinhalts fehlgeschlagen" +-- Drop one file here to load its content. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T2274562398"] = "Datei hier ablegen, um ihren Inhalt zu laden." + -- Use file content as input UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T3499386973"] = "Dokumenteninhalt als Eingabe verwenden" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 487351ef..9ebffead 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1407,6 +1407,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::PROGRAMMINGLANGUAGESEXTENSIONS::T342 -- Please provide a text as input. You might copy the desired text from a document or a website. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T137304886"] = "Please provide a text as input. You might copy the desired text from a document or a website." +-- Load text from file +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T2210807298"] = "Load text from file" + -- Proofread UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::GRAMMARSPELLING::ASSISTANTGRAMMARSPELLING::T2325568297"] = "Proofread" @@ -2799,6 +2802,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T900237532"] = "Provid -- Failed to load file content UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T1989554334"] = "Failed to load file content" +-- Drop one file here to load its content. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T2274562398"] = "Drop one file here to load its content." + -- Use file content as input UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::READFILECONTENT::T3499386973"] = "Use file content as input"