Allow choosing a RAG folder or file by dropping it

This commit is contained in:
Thorsten Sommer 2026-09-09 17:10:24 +02:00
parent 44be8ce14e
commit 3439b91c28
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
11 changed files with 260 additions and 40 deletions

View File

@ -7,7 +7,9 @@
@T("Input")
</MudText>
<SelectDirectory Label="@T("Folder containing your documents")" DirectoryDialogTitle="@T("Select the folder containing your documents")" @bind-Directory="@this.inputDirectory" Validation="@this.ValidateInputDirectory" Disabled="@this.isProcessingBatch"/>
@* This page carries a second drop zone for the instructions, so neither of them catches all
documents: with two targets, only the one under the mouse may take the drop. *@
<SelectDirectory Label="@T("Folder containing your documents")" DirectoryDialogTitle="@T("Select the folder containing your documents")" @bind-Directory="@this.inputDirectory" Validation="@this.ValidateInputDirectory" Disabled="@this.isProcessingBatch" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS"/>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-1">
<MudTextField T="string" @bind-Text="@this.filePatterns" Validation="@this.ValidateFilePatterns" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("File patterns")" HelperText="@T("Which files should be processed? Separate multiple patterns with a semicolon, e.g., *.pdf;*.docx")" AdornmentIcon="@Icons.Material.Filled.FilterAlt" Adornment="Adornment.Start" Variant="Variant.Outlined" Margin="Margin.Normal" Class="flex-grow-1" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
@ -44,7 +46,7 @@
@if (this.promptSource is BatchProcessingPromptSource.FREE_PROMPT)
{
<ReadFileContent Text="@T("Load prompt from file")" @bind-FileContent="@this.freePrompt" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true" Disabled="@this.isProcessingBatch"/>
<ReadFileContent Text="@T("Load prompt from file")" @bind-FileContent="@this.freePrompt" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" Disabled="@this.isProcessingBatch"/>
<MudTextField T="string" @bind-Text="@this.freePrompt" Validation="@this.ValidateFreePrompt" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("What should the AI do with each document?")" HelperText="@T("These instructions are applied to every single document of the batch run.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="5" AutoGrow="@true" MaxLines="26" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
@ -52,7 +54,7 @@
}
else if (this.promptSource is BatchProcessingPromptSource.FILE_IMPORT)
{
<ReadFileContent Text="@T("Select the file with your instructions")" @bind-FileContent="@this.ImportedPrompt" Filter="@([FileTypes.MARKDOWN])" ShowAttachedDocumentState="@true" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true" Disabled="@this.isProcessingBatch"/>
<ReadFileContent Text="@T("Select the file with your instructions")" @bind-FileContent="@this.ImportedPrompt" Filter="@([FileTypes.MARKDOWN])" ShowAttachedDocumentState="@true" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" Disabled="@this.isProcessingBatch"/>
@if (!string.IsNullOrWhiteSpace(this.promptFilePath))
{

View File

@ -345,4 +345,4 @@ else
</MudJustifiedText>
<MudTextSwitch Label="@T("Should we write the generated code to the file system?")" Disabled="@this.IsERIInputDisabled" @bind-Value="@this.writeToFilesystem" LabelOn="@T("Yes, please write or update all generated code to the file system")" LabelOff="@T("No, just show me the code")" />
<SelectDirectory Label="@T("Base directory where to write the code")" @bind-Directory="@this.baseDirectory" Disabled="@this.IsBaseDirectorySelectionDisabled" DirectoryDialogTitle="@T("Select the target directory for the ERI server")" Validation="@this.ValidateDirectory" />
<SelectDirectory Label="@T("Base directory where to write the code")" @bind-Directory="@this.baseDirectory" Disabled="@this.IsBaseDirectorySelectionDisabled" DirectoryDialogTitle="@T("Select the target directory for the ERI server")" Validation="@this.ValidateDirectory" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true" />

View File

@ -4291,12 +4291,33 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SECRETINPUTFIELD::T1273315904"] = "Hide c
-- Show content
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SECRETINPUTFIELD::T2891011873"] = "Show content"
-- The dropped folder could not be accessed. Please choose it with the folder chooser instead.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T1153417816"] = "The dropped folder could not be accessed. Please choose it with the folder chooser instead."
-- Please drop a folder, not a file.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T3289690493"] = "Please drop a folder, not a file."
-- You can also drag & drop the folder here.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T350096725"] = "You can also drag & drop the folder here."
-- Choose Directory
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T4256489763"] = "Choose Directory"
-- Please drop a file, not a folder.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T1472251601"] = "Please drop a file, not a folder."
-- You can also drag & drop the file here.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T1984243691"] = "You can also drag & drop the file here."
-- Choose File
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T4285779702"] = "Choose File"
-- Please drop a file with a supported file type.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T930441004"] = "Please drop a file with a supported file type."
-- The dropped file could not be accessed. Please choose it with the file chooser instead.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T984660028"] = "The dropped file could not be accessed. Please choose it with the file chooser instead."
-- External Assistants rated below this audit level are treated as insufficiently reviewed.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAGENTASSISTANTAUDIT::T1162151451"] = "External Assistants rated below this audit level are treated as insufficiently reviewed."

View File

@ -1,19 +1,37 @@
@inherits MSGComponentBase
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.Directory"
Label="@this.Label"
ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Folder"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.Disabled || this.isDirectoryDialogOpen)" OnClick="@this.OpenDirectoryDialog">
@T("Choose Directory")
</MudButton>
</MudStack>
@if (this.EnableDragDrop)
{
<PathDropZone Layer="@this.Layer" CatchAllDocuments="@this.CatchAllDocuments" Disabled="@this.Disabled" OnPathsDropped="@this.PathsDropped">
@this.Picker
<MudText Typo="Typo.body2">
@T("You can also drag & drop the folder here.")
</MudText>
</PathDropZone>
}
else
{
@this.Picker
}
@code {
private RenderFragment Picker =>
@<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.Directory"
Label="@this.Label"
ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Folder"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.Disabled || this.isDirectoryDialogOpen)" OnClick="@this.OpenDirectoryDialog">
@T("Choose Directory")
</MudButton>
</MudStack>;
}

View File

@ -2,6 +2,9 @@ using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
// This component has a parameter called Directory, which would shadow the file system's Directory class:
using IODirectory = System.IO.Directory;
namespace AIStudio.Components;
public partial class SelectDirectory : MSGComponentBase
@ -24,6 +27,24 @@ public partial class SelectDirectory : MSGComponentBase
[Parameter]
public Func<string, string?> Validation { get; set; } = _ => null;
/// <summary>
/// When true, the folder can also be chosen by dropping it onto this component.
/// </summary>
[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; } = DropLayers.ROOT;
/// <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]
public RustService RustService { get; set; } = null!;
@ -69,4 +90,39 @@ public partial class SelectDirectory : MSGComponentBase
this.isDirectoryDialogOpen = false;
}
}
/// <summary>
/// Takes the first dropped path which leads to a folder.
/// </summary>
/// <remarks>
/// A dropped file is rejected instead of being taken as its parent folder. Everything a folder
/// contains is processed, so guessing the parent of a mistakenly dropped file could pull in far
/// more data than the user meant to hand over.
/// </remarks>
/// <param name="paths">The dropped paths.</param>
private async Task PathsDropped(List<string> paths)
{
foreach (var path in paths)
{
if (!IODirectory.Exists(path))
continue;
this.Logger.LogInformation("The user dropped the directory '{DroppedDirectory}'.", path);
this.InternalDirectoryChanged(path);
return;
}
this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a directory.", paths.Count);
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, this.GetDropWarning(paths)));
}
private string GetDropWarning(List<string> paths)
{
// Naming the actual mistake beats a generic "that did not work". Dropping a file onto a
// folder picker is the likeliest of them:
if (paths.Any(File.Exists))
return T("Please drop a folder, not a file.");
return T("The dropped folder could not be accessed. Please choose it with the folder chooser instead.");
}
}

View File

@ -1,19 +1,37 @@
@inherits MSGComponentBase
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.File"
Label="@this.Label"
ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AttachFile"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.Disabled || this.isFileDialogOpen)" OnClick="@this.OpenFileDialog">
@T("Choose File")
</MudButton>
</MudStack>
@if (this.EnableDragDrop)
{
<PathDropZone Layer="@this.Layer" CatchAllDocuments="@this.CatchAllDocuments" Disabled="@this.Disabled" OnPathsDropped="@this.PathsDropped">
@this.Picker
<MudText Typo="Typo.body2">
@T("You can also drag & drop the file here.")
</MudText>
</PathDropZone>
}
else
{
@this.Picker
}
@code {
private RenderFragment Picker =>
@<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.File"
Label="@this.Label"
ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AttachFile"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.Disabled || this.isFileDialogOpen)" OnClick="@this.OpenFileDialog">
@T("Choose File")
</MudButton>
</MudStack>;
}

View File

@ -3,6 +3,9 @@ using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
// This component has a parameter called File, which would shadow the file system's File class:
using IOFile = System.IO.File;
namespace AIStudio.Components;
public partial class SelectFile : MSGComponentBase
@ -28,6 +31,24 @@ public partial class SelectFile : MSGComponentBase
[Parameter]
public Func<string, string?> Validation { get; set; } = _ => null;
/// <summary>
/// When true, the file can also be chosen by dropping it onto this component.
/// </summary>
[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; } = DropLayers.ROOT;
/// <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]
public RustService RustService { get; set; } = null!;
@ -73,4 +94,46 @@ public partial class SelectFile : MSGComponentBase
this.isFileDialogOpen = false;
}
}
/// <summary>
/// Takes the first dropped path which leads to a usable file.
/// </summary>
/// <remarks>
/// This component carries exactly one file, so a multi-selection cannot be honored as a whole.
/// A dropped folder is rejected instead of being read as "the first file inside it": the user
/// was asked for a file, and picking one for them would be a surprise.
/// </remarks>
/// <param name="paths">The dropped paths.</param>
private async Task PathsDropped(List<string> paths)
{
foreach (var path in paths)
{
if (!IOFile.Exists(path))
continue;
if (this.Filter is { Length: > 0 } && !FileTypes.IsAllowedPath(path, this.Filter))
continue;
this.Logger.LogInformation("The user dropped the file '{DroppedFilePath}'.", path);
this.InternalFileChanged(path);
return;
}
this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a file.", paths.Count);
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, this.GetDropWarning(paths)));
}
private string GetDropWarning(List<string> paths)
{
// Naming the actual mistake beats a generic "that did not work". Dropping a folder onto a
// file picker is the likeliest of them:
if (paths.Any(Directory.Exists))
return T("Please drop a file, not a folder.");
// The file exists, so the file type filter is what turned it down:
if (paths.Any(IOFile.Exists))
return T("Please drop a file with a supported file type.");
return T("The dropped file could not be accessed. Please choose it with the file chooser instead.");
}
}

View File

@ -51,7 +51,7 @@
}
@if (this.CanChangeSourceAndEmbedding)
{
<SelectDirectory @bind-Directory="@this.dataPath" Label="@T("Selected base directory for this data source")" DirectoryDialogTitle="@T("Select the base directory")" Validation="@this.dataSourceValidation.ValidatePath" />
<SelectDirectory @bind-Directory="@this.dataPath" Label="@T("Selected base directory for this data source")" DirectoryDialogTitle="@T("Select the base directory")" Validation="@this.dataSourceValidation.ValidatePath" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true" />
}
else
{

View File

@ -51,7 +51,7 @@
}
@if (this.CanChangeSourceAndEmbedding)
{
<SelectFile @bind-File="@this.dataFilePath" Label="@T("Selected file path for this data source")" FileDialogTitle="@T("Select the file")" Validation="@this.dataSourceValidation.ValidateFilePath" />
<SelectFile @bind-File="@this.dataFilePath" Label="@T("Selected file path for this data source")" FileDialogTitle="@T("Select the file")" Validation="@this.dataSourceValidation.ValidateFilePath" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true" />
}
else
{

View File

@ -4293,12 +4293,33 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SECRETINPUTFIELD::T1273315904"] = "Inhalt
-- Show content
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SECRETINPUTFIELD::T2891011873"] = "Inhalt anzeigen"
-- The dropped folder could not be accessed. Please choose it with the folder chooser instead.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T1153417816"] = "Auf den abgelegten Ordner konnte nicht zugegriffen werden. Bitte wählen Sie ihn stattdessen über die Ordnerauswahl aus."
-- Please drop a folder, not a file.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T3289690493"] = "Bitte legen Sie einen Ordner ab, keine Datei."
-- You can also drag & drop the folder here.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T350096725"] = "Sie können den Ordner auch hierher ziehen und ablegen."
-- Choose Directory
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T4256489763"] = "Verzeichnis auswählen"
-- Please drop a file, not a folder.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T1472251601"] = "Bitte legen Sie eine Datei ab, keinen Ordner."
-- You can also drag & drop the file here.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T1984243691"] = "Sie können die Datei auch hierher ziehen und ablegen."
-- Choose File
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T4285779702"] = "Datei auswählen"
-- Please drop a file with a supported file type.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T930441004"] = "Bitte legen Sie eine Datei mit einem unterstützten Dateityp ab."
-- The dropped file could not be accessed. Please choose it with the file chooser instead.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T984660028"] = "Auf die abgelegte Datei konnte nicht zugegriffen werden. Bitte wählen Sie sie stattdessen über die Dateiauswahl aus."
-- External Assistants rated below this audit level are treated as insufficiently reviewed.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAGENTASSISTANTAUDIT::T1162151451"] = "Externe Assistenten, die unter diesem Audit Level bewertet werden, gelten als nicht ausreichend sicher."

View File

@ -4293,12 +4293,33 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SECRETINPUTFIELD::T1273315904"] = "Hide c
-- Show content
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SECRETINPUTFIELD::T2891011873"] = "Show content"
-- The dropped folder could not be accessed. Please choose it with the folder chooser instead.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T1153417816"] = "The dropped folder could not be accessed. Please choose it with the folder chooser instead."
-- Please drop a folder, not a file.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T3289690493"] = "Please drop a folder, not a file."
-- You can also drag & drop the folder here.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T350096725"] = "You can also drag & drop the folder here."
-- Choose Directory
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTDIRECTORY::T4256489763"] = "Choose Directory"
-- Please drop a file, not a folder.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T1472251601"] = "Please drop a file, not a folder."
-- You can also drag & drop the file here.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T1984243691"] = "You can also drag & drop the file here."
-- Choose File
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T4285779702"] = "Choose File"
-- Please drop a file with a supported file type.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T930441004"] = "Please drop a file with a supported file type."
-- The dropped file could not be accessed. Please choose it with the file chooser instead.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SELECTFILE::T984660028"] = "The dropped file could not be accessed. Please choose it with the file chooser instead."
-- External Assistants rated below this audit level are treated as insufficiently reviewed.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAGENTASSISTANTAUDIT::T1162151451"] = "External Assistants rated below this audit level are treated as insufficiently reviewed."