mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 20:12:12 +00:00
Added drag-and-drop support for loading file content
This commit is contained in:
parent
0476595f2d
commit
38fe814ff1
@ -1,6 +1,7 @@
|
||||
@attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)]
|
||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogGrammarSpelling>
|
||||
|
||||
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true"/>
|
||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to check")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" />
|
||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
@inherits MSGComponentBase
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Description" OnClick="@(async () => await this.SelectFile())" Variant="Variant.Filled" Class="mb-3" Disabled="@this.Disabled">
|
||||
@if (string.IsNullOrWhiteSpace(this.Text))
|
||||
{
|
||||
@T("Use file content as input")
|
||||
}
|
||||
else
|
||||
{
|
||||
@this.Text
|
||||
}
|
||||
</MudButton>
|
||||
|
||||
@if (this.EnableDragDrop)
|
||||
{
|
||||
<div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave">
|
||||
<MudPaper Outlined="true" Class="@this.dragClass">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.None" Wrap="Wrap.Wrap">
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Description" OnClick="@(async () => await this.SelectFile())" Variant="Variant.Filled" Disabled="@this.Disabled">
|
||||
@this.ButtonText
|
||||
</MudButton>
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Drop one file here to load its content.")
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Description" OnClick="@(async () => await this.SelectFile())" Variant="Variant.Filled" Class="mb-3" Disabled="@this.Disabled">
|
||||
@this.ButtonText
|
||||
</MudButton>
|
||||
}
|
||||
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// On which layer to register the drop area. Higher layers have priority over lower layers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int Layer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
|
||||
/// </summary>
|
||||
[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<T>(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<bool> 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<string> paths)
|
||||
{
|
||||
if (!await this.EnsurePandocAvailability())
|
||||
return;
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (await this.LoadFileIfValid(path))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> 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();
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user