Improved batch input handling

This commit is contained in:
Thorsten Sommer 2026-08-11 12:58:58 +02:00
parent 59c952e7d4
commit cd8ec4cbb9
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
5 changed files with 49 additions and 4 deletions

View File

@ -1,6 +1,7 @@
@attribute [Route(Routes.ASSISTANT_BATCH_PROCESSING)] @attribute [Route(Routes.ASSISTANT_BATCH_PROCESSING)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogBatchProcessing> @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogBatchProcessing>
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@using AIStudio.Tools.Rust
<MudText Typo="Typo.h5" Class="mb-3"> <MudText Typo="Typo.h5" Class="mb-3">
@T("Input") @T("Input")
@ -12,6 +13,13 @@
<MudTextSwitch Label="@T("Include subfolders?")" Disabled="@this.isProcessingBatch" Value="@this.includeSubdirectories" ValueChanged="@(v => this.includeSubdirectories = v)" LabelOn="@T("Yes, process files in subfolders as well")" LabelOff="@T("No, only process files in the selected folder")"/> <MudTextSwitch Label="@T("Include subfolders?")" Disabled="@this.isProcessingBatch" Value="@this.includeSubdirectories" ValueChanged="@(v => this.includeSubdirectories = v)" LabelOn="@T("Yes, process files in subfolders as well")" LabelOff="@T("No, only process files in the selected folder")"/>
@if (this.includeSubdirectories)
{
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
@T("A separate output folder is excluded from document discovery. This includes the default 'ai-results' folder, so results from an earlier run are not processed again. If the input folder itself is used for output, known batch result files are excluded instead.")
</MudJustifiedText>
}
<MudText Typo="Typo.h5" Class="mb-3 mt-6"> <MudText Typo="Typo.h5" Class="mb-3 mt-6">
@T("Instructions") @T("Instructions")
</MudText> </MudText>
@ -27,11 +35,13 @@
@if (this.promptSource is BatchProcessingPromptSource.FREE_PROMPT) @if (this.promptSource is BatchProcessingPromptSource.FREE_PROMPT)
{ {
<ReadFileContent Text="@T("Load prompt from file")" @bind-FileContent="@this.freePrompt" 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"/> <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"/>
} }
else if (this.promptSource is BatchProcessingPromptSource.FILE_IMPORT) else if (this.promptSource is BatchProcessingPromptSource.FILE_IMPORT)
{ {
<ReadFileContent Text="@T("Select the file with your instructions")" @bind-FileContent="@this.ImportedPrompt" ShowAttachedDocumentState="@true" Disabled="@this.isProcessingBatch"/> <ReadFileContent Text="@T("Select the file with your instructions")" @bind-FileContent="@this.ImportedPrompt" Filter="@([FileTypes.MARKDOWN])" ShowAttachedDocumentState="@true" Disabled="@this.isProcessingBatch"/>
@if (!string.IsNullOrWhiteSpace(this.promptFilePath)) @if (!string.IsNullOrWhiteSpace(this.promptFilePath))
{ {

View File

@ -20,6 +20,25 @@ public partial class AssistantBatchProcessing
if (string.IsNullOrWhiteSpace(patterns)) if (string.IsNullOrWhiteSpace(patterns))
return T("Please provide at least one file pattern, e.g., *.pdf. Separate multiple patterns with a semicolon."); return T("Please provide at least one file pattern, e.g., *.pdf. Separate multiple patterns with a semicolon.");
var individualPatterns = patterns.Split(';');
if (individualPatterns.Any(string.IsNullOrWhiteSpace))
return T("Please remove empty file patterns. Separate valid patterns with a single semicolon.");
foreach (var patternEntry in individualPatterns)
{
var pattern = patternEntry.Trim();
if (pattern is "." or ".."
|| pattern.EndsWith("..", StringComparison.Ordinal)
|| pattern.IndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, '/', '\\']) >= 0)
return T("Please use file name patterns without folder paths, e.g., *.pdf or report-*.docx.");
var invalidCharacters = Path.GetInvalidFileNameChars()
.Where(character => character is not '*' and not '?')
.ToArray();
if (pattern.IndexOfAny(invalidCharacters) >= 0)
return T("One of the file patterns contains an invalid character.");
}
return null; return null;
} }

View File

@ -50,6 +50,13 @@ public partial class ReadFileContent : MSGComponentBase
/// </summary> /// </summary>
[Parameter] [Parameter]
public bool CatchAllDocuments { get; set; } public bool CatchAllDocuments { get; set; }
/// <summary>
/// Optionally restricts the file types offered by the native file picker
/// and accepted by this component.
/// </summary>
[Parameter]
public FileTypeFilter[]? Filter { get; set; }
[Inject] [Inject]
private RustService RustService { get; init; } = null!; private RustService RustService { get; init; } = null!;
@ -252,7 +259,7 @@ public partial class ReadFileContent : MSGComponentBase
this.isFileDialogOpen = true; this.isFileDialogOpen = true;
try try
{ {
var selectedFile = await this.RustService.SelectFile(T("Select file to read its content")); var selectedFile = await this.RustService.SelectFile(T("Select file to read its content"), this.Filter);
if (selectedFile.UserCancelled) if (selectedFile.UserCancelled)
{ {
this.Logger.LogInformation("User cancelled the file selection"); this.Logger.LogInformation("User cancelled the file selection");
@ -310,6 +317,13 @@ public partial class ReadFileContent : MSGComponentBase
return false; return false;
} }
if (this.Filter is { Length: > 0 } && !FileTypes.IsAllowedPath(filePath, this.Filter))
{
this.Logger.LogWarning("Selected file does not match the configured file type filter: '{FilePath}'", filePath);
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, this.T("Please select a file with a supported file type.")));
return false;
}
if (FileTypes.IsAllowedPath(filePath, FileTypes.AUDIO) || FileTypes.IsAllowedPath(filePath, FileTypes.VIDEO)) if (FileTypes.IsAllowedPath(filePath, FileTypes.AUDIO) || FileTypes.IsAllowedPath(filePath, FileTypes.VIDEO))
return await this.LoadMediaTranscriptAsync(filePath); return await this.LoadMediaTranscriptAsync(filePath);

View File

@ -1,5 +1,6 @@
@using AIStudio.Assistants.BatchProcessing @using AIStudio.Assistants.BatchProcessing
@using AIStudio.Settings @using AIStudio.Settings
@using AIStudio.Tools.Rust
@inherits SettingsDialogBase @inherits SettingsDialogBase
<MudDialog> <MudDialog>
@ -26,7 +27,7 @@
} }
else if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.FILE_IMPORT) else if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.FILE_IMPORT)
{ {
<ConfigurationText OptionDescription="@T("Default Markdown instructions file")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Description" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath = value)" OptionHelp="@T("The current content of this Markdown file is loaded whenever the defaults are applied.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PromptFilePath, out var meta) && meta.IsLocked"/> <ConfigurationFile OptionDescription="@T("Default Markdown instructions file")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Description" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath = value)" FileDialogTitle="@T("Select the default Markdown instructions file")" Filter="@([FileTypes.MARKDOWN])" OptionHelp="@T("The current content of this Markdown file is loaded whenever the defaults are applied.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PromptFilePath, out var meta) && meta.IsLocked"/>
} }
else else
{ {
@ -54,4 +55,4 @@
<DialogActions> <DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton> <MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton>
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>

View File

@ -50,6 +50,7 @@ public static class FileTypes
// Document hierarchy // Document hierarchy
public static readonly FileTypeFilter PDF = FileTypeFilter.Leaf("PDF", "pdf"); public static readonly FileTypeFilter PDF = FileTypeFilter.Leaf("PDF", "pdf");
public static readonly FileTypeFilter MARKDOWN = FileTypeFilter.Leaf("Markdown", "md");
public static readonly FileTypeFilter TEXT = FileTypeFilter.Leaf(TB("Text"), "txt", "md", "rtf"); public static readonly FileTypeFilter TEXT = FileTypeFilter.Leaf(TB("Text"), "txt", "md", "rtf");
public static readonly FileTypeFilter TABULAR = FileTypeFilter.Leaf(TB("Tabular text"), "csv", "tsv"); public static readonly FileTypeFilter TABULAR = FileTypeFilter.Leaf(TB("Tabular text"), "csv", "tsv");
public static readonly FileTypeFilter MS_WORD = FileTypeFilter.Leaf("Microsoft Word", "docx"); public static readonly FileTypeFilter MS_WORD = FileTypeFilter.Leaf("Microsoft Word", "docx");