Make batch CSV separators configurable

This commit is contained in:
Thorsten Sommer 2026-08-11 18:59:57 +02:00
parent e9e394ed96
commit 9e07155e3d
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
16 changed files with 281 additions and 26 deletions

View File

@ -126,12 +126,26 @@ else
<MudTextField T="string" @bind-Text="@this.csvFileName" Validation="@this.ValidateCsvFileName" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("Name of the results table (optional)")" HelperText="@T("The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'.")" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Variant="Variant.Outlined" Margin="Margin.Normal" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" @bind-Text="@this.resultColumnHeader" Disabled="@this.isProcessingBatch" Label="@T("Header of the result column (optional)")" HelperText="@T("The results table contains one row per document, starting with the file name. Here you can name the column that holds the AI answer, e.g., Summary. When left empty, we use 'Result'.")" AdornmentIcon="@Icons.Material.Filled.TableChart" Adornment="Adornment.Start" Variant="Variant.Outlined" Margin="Margin.Normal" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudSelect T="BatchProcessingCsvSeparator" @bind-Value="@this.csvSeparator" Disabled="@this.isProcessingBatch" AdornmentIcon="@Icons.Material.Filled.FormatListBulleted" Adornment="Adornment.Start" Label="@T("Column separator")" HelperText="@T("Choose which character separates the columns of the results table.")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3">
@foreach (var separator in Enum.GetValues<BatchProcessingCsvSeparator>())
{
<MudSelectItem Value="@separator">
@separator.Name()
</MudSelectItem>
}
</MudSelect>
@if (this.csvSeparator is BatchProcessingCsvSeparator.CUSTOM)
{
<MudTextField T="string" @bind-Text="@this.customCsvSeparator" Validation="@this.ValidateCustomCsvSeparator" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("Custom column separator")" HelperText="@T("Enter one punctuation or symbol character.")" AdornmentIcon="@Icons.Material.Filled.Edit" Adornment="Adornment.Start" Variant="Variant.Outlined" Margin="Margin.Normal" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
}
<SelectDirectory Label="@T("Output folder (optional)")" DirectoryDialogTitle="@T("Select the output folder")" @bind-Directory="@this.outputDirectory" Disabled="@this.isProcessingBatch"/>
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
@T("We always write a log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. Its columns are separated by a vertical bar, so you can open it with any spreadsheet application. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder.")
@T("We always write a semicolon-separated log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder.")
</MudJustifiedText>
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProviderWithBatchState" Disabled="@this.isProcessingBatch" ExplicitMinimumConfidence="@this.GetMinimumConfidenceLevel()"/>

View File

@ -106,9 +106,9 @@ public partial class AssistantBatchProcessing
private async Task WriteLogAsync(string resolvedOutputDirectory)
{
var sb = new StringBuilder();
sb.AppendLine(BatchProcessingCsv.ToCsvRow(T("File"), T("Time"), T("Model"), T("Status"), T("Details")));
sb.AppendLine(BatchProcessingCsv.ToCsvRow(LOG_SEPARATOR, T("File"), T("Time"), T("Model"), T("Status"), T("Details")));
foreach (var fileResult in this.fileResults.Where(x => x.Status is not BatchProcessingFileStatus.QUEUED and not BatchProcessingFileStatus.PROCESSING))
sb.AppendLine(BatchProcessingCsv.ToCsvRow(fileResult.RelativePath, fileResult.ProcessedAt.ToString(TIME_FORMAT, CultureInfo.InvariantCulture), fileResult.ModelName, fileResult.Status.ToString(), fileResult.Message));
sb.AppendLine(BatchProcessingCsv.ToCsvRow(LOG_SEPARATOR, fileResult.RelativePath, fileResult.ProcessedAt.ToString(TIME_FORMAT, CultureInfo.InvariantCulture), fileResult.ModelName, fileResult.Status.ToString(), fileResult.Message));
await this.WriteCsvFileAsync(Path.Join(resolvedOutputDirectory, LOG_FILENAME), sb.ToString());
}
@ -118,10 +118,11 @@ public partial class AssistantBatchProcessing
/// </summary>
private async Task WriteResultsTableAsync(string resolvedOutputDirectory)
{
var separator = this.csvSeparator.Character(this.customCsvSeparator);
var sb = new StringBuilder();
sb.AppendLine(BatchProcessingCsv.ToCsvRow(T("File"), this.ResultColumnHeader));
sb.AppendLine(BatchProcessingCsv.ToCsvRow(separator, T("File"), this.ResultColumnHeader));
foreach (var fileResult in this.fileResults.Where(x => x.Status is BatchProcessingFileStatus.DONE))
sb.AppendLine(BatchProcessingCsv.ToCsvRow(fileResult.RelativePath, fileResult.ResultText));
sb.AppendLine(BatchProcessingCsv.ToCsvRow(separator, fileResult.RelativePath, fileResult.ResultText));
await this.WriteCsvFileAsync(Path.Join(resolvedOutputDirectory, this.ResolveResultsFileName()), sb.ToString());
}
@ -175,7 +176,7 @@ public partial class AssistantBatchProcessing
try
{
var content = await File.ReadAllTextAsync(logFilePath);
var rows = BatchProcessingCsv.Parse(content);
var rows = BatchProcessingCsv.ParseWithDetectedSeparator(content, 5, LOG_SEPARATOR, '|');
// The first row is the header, which we skip:
foreach (var row in rows.Skip(1))
@ -211,7 +212,9 @@ public partial class AssistantBatchProcessing
return results;
var content = await File.ReadAllTextAsync(resultsFilePath);
foreach (var row in BatchProcessingCsv.Parse(content).Skip(1))
var configuredSeparator = this.csvSeparator.Character(this.customCsvSeparator);
var rows = BatchProcessingCsv.ParseWithDetectedSeparator(content, 2, configuredSeparator, ';', '|', ',', '\t');
foreach (var row in rows.Skip(1))
{
if (row.Count < 2 || string.IsNullOrWhiteSpace(row[0]))
continue;

View File

@ -18,6 +18,8 @@ public partial class AssistantBatchProcessing
private static readonly AssistantSessionStateKey<BatchProcessingOutputMode> OUTPUT_MODE_STATE_KEY = new(nameof(outputMode));
private static readonly AssistantSessionStateKey<string> RESULT_COLUMN_HEADER_STATE_KEY = new(nameof(resultColumnHeader));
private static readonly AssistantSessionStateKey<string> CSV_FILE_NAME_STATE_KEY = new(nameof(csvFileName));
private static readonly AssistantSessionStateKey<BatchProcessingCsvSeparator> CSV_SEPARATOR_STATE_KEY = new(nameof(csvSeparator));
private static readonly AssistantSessionStateKey<string> CUSTOM_CSV_SEPARATOR_STATE_KEY = new(nameof(customCsvSeparator));
private static readonly AssistantSessionStateKey<List<BatchProcessingFileResult>> FILE_RESULTS_STATE_KEY = new(nameof(fileResults));
private static readonly AssistantSessionStateKey<HashSet<string>> USED_RESULT_FILE_NAMES_STATE_KEY = new(nameof(usedResultFileNames));
private static readonly AssistantSessionStateKey<bool> IS_PROCESSING_BATCH_STATE_KEY = new(nameof(isProcessingBatch));
@ -40,6 +42,8 @@ public partial class AssistantBatchProcessing
state.Set(OUTPUT_MODE_STATE_KEY, this.outputMode);
state.Set(RESULT_COLUMN_HEADER_STATE_KEY, this.resultColumnHeader);
state.Set(CSV_FILE_NAME_STATE_KEY, this.csvFileName);
state.Set(CSV_SEPARATOR_STATE_KEY, this.csvSeparator);
state.Set(CUSTOM_CSV_SEPARATOR_STATE_KEY, this.customCsvSeparator);
state.SetList(FILE_RESULTS_STATE_KEY, this.fileResults.Select(CloneFileResult));
state.SetHashSet(USED_RESULT_FILE_NAMES_STATE_KEY, this.usedResultFileNames);
state.Set(IS_PROCESSING_BATCH_STATE_KEY, this.isProcessingBatch);
@ -63,6 +67,8 @@ public partial class AssistantBatchProcessing
state.Restore(OUTPUT_MODE_STATE_KEY, value => this.outputMode = value);
state.Restore(RESULT_COLUMN_HEADER_STATE_KEY, value => this.resultColumnHeader = value);
state.Restore(CSV_FILE_NAME_STATE_KEY, value => this.csvFileName = value);
state.Restore(CSV_SEPARATOR_STATE_KEY, value => this.csvSeparator = value);
state.Restore(CUSTOM_CSV_SEPARATOR_STATE_KEY, value => this.customCsvSeparator = value);
state.Restore(FILE_RESULTS_STATE_KEY, values =>
{
this.fileResults.Clear();

View File

@ -56,6 +56,18 @@ public partial class AssistantBatchProcessing
return null;
}
private string? ValidateCustomCsvSeparator(string separator)
{
if (this.outputMode is not BatchProcessingOutputMode.TABLE_ONLY
|| this.csvSeparator is not BatchProcessingCsvSeparator.CUSTOM)
return null;
if (!BatchProcessingCsvSeparatorExtensions.IsValidCustomSeparator(separator))
return T("Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators.");
return null;
}
private string? ValidateFreePrompt(string prompt)
{
if (this.promptSource is BatchProcessingPromptSource.FREE_PROMPT && string.IsNullOrWhiteSpace(prompt))

View File

@ -17,6 +17,7 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
private const string RESULT_FILE_SUFFIX = "_result.md";
private const string TRANSCRIPT_FILE_SUFFIX = ".transcript.md";
private const string TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
private const char LOG_SEPARATOR = ';';
/// <summary>
/// The name of the log file. It is fixed, so that a later batch run finds
@ -88,6 +89,8 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
private BatchProcessingOutputMode outputMode = BatchProcessingOutputMode.MARKDOWN_FILES;
private string resultColumnHeader = string.Empty;
private string csvFileName = string.Empty;
private BatchProcessingCsvSeparator csvSeparator = BatchProcessingCsvSeparator.SEMICOLON;
private string customCsvSeparator = string.Empty;
private readonly List<BatchProcessingFileResult> fileResults = [];
private readonly HashSet<string> usedResultFileNames = new(StringComparer.OrdinalIgnoreCase);
@ -156,6 +159,8 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
this.outputMode = BatchProcessingOutputMode.MARKDOWN_FILES;
this.resultColumnHeader = string.Empty;
this.csvFileName = string.Empty;
this.csvSeparator = BatchProcessingCsvSeparator.SEMICOLON;
this.customCsvSeparator = string.Empty;
return;
}
@ -171,6 +176,8 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
this.outputMode = settings.OutputMode;
this.resultColumnHeader = settings.ResultColumnHeader;
this.csvFileName = settings.CsvFileName;
this.csvSeparator = settings.CsvSeparator;
this.customCsvSeparator = settings.CustomCsvSeparator;
}
private async Task LoadConfiguredPromptFileAsync()

View File

@ -4,28 +4,30 @@ namespace AIStudio.Assistants.BatchProcessing;
/// <summary>
/// Reads and writes the CSV files of the batch processing assistant. Fields
/// are quoted according to RFC 4180, but the separator is a vertical bar, so
/// that the files open nicely in spreadsheet applications regardless of the
/// list separator of the user's locale.
/// are quoted according to RFC 4180 using the separator selected for the
/// respective file.
/// </summary>
public static class BatchProcessingCsv
{
private const char SEPARATOR = '|';
public static string ToCsvRow(params string[] fields) => string.Join(SEPARATOR, fields.Select(ToCsvField));
public static string ToCsvRow(char separator, params string[] fields) => string.Join(separator, fields.Select(field => ToCsvField(field, separator)));
/// <summary>
/// Quotes one CSV field according to RFC 4180.
/// </summary>
private static string ToCsvField(string text)
private static string ToCsvField(string text, char separator)
{
if (string.IsNullOrEmpty(text))
return string.Empty;
if (!text.Contains(SEPARATOR) && !text.Contains('"') && !text.Contains('\n') && !text.Contains('\r'))
// Quoting the complete field is important for long and multi-line AI
// answers: neither separators nor line breaks within an answer may
// create another column or row.
if (!text.Contains(separator) && !text.Contains('"') && !text.Contains('\n') && !text.Contains('\r'))
return text;
return $"\"{text.Replace("\"", "\"\"")}\"";
return $"""
"{text.Replace("\"", "\"\"")}"
""";
}
/// <summary>
@ -35,7 +37,7 @@ public static class BatchProcessingCsv
/// We parse the file ourselves instead of splitting lines, because quoted
/// fields may contain the separator and line breaks.
/// </remarks>
public static List<List<string>> Parse(string content)
private static List<List<string>> Parse(string content, char separator)
{
var rows = new List<List<string>>();
var fields = new List<string>();
@ -73,7 +75,7 @@ public static class BatchProcessingCsv
hasContent = true;
break;
case SEPARATOR:
case var _ when character == separator:
hasContent = true;
EndField();
break;
@ -113,4 +115,74 @@ public static class BatchProcessingCsv
hasContent = false;
}
}
/// <summary>
/// Detects the separator from the first CSV record and parses the complete
/// content with it. Preferred separators are used as fallbacks for files
/// whose first record does not reveal a valid separator.
/// </summary>
public static List<List<string>> ParseWithDetectedSeparator(string content, int expectedNumFields, params char[] preferredSeparators)
{
var firstRecord = ReadFirstRecord(content);
var candidates = new List<char>();
var isQuoted = false;
for (var index = 0; index < firstRecord.Length; index++)
{
var character = firstRecord[index];
if (character is '"')
{
if (isQuoted && index + 1 < firstRecord.Length && firstRecord[index + 1] is '"')
{
index++;
continue;
}
isQuoted = !isQuoted;
continue;
}
if (!isQuoted
&& character is not '\r' and not '\n'
&& (char.IsPunctuation(character) || char.IsSymbol(character) || character is '\t')
&& !candidates.Contains(character))
candidates.Add(character);
}
foreach (var separator in preferredSeparators)
{
if (!candidates.Contains(separator))
candidates.Add(separator);
}
foreach (var separator in candidates)
{
var header = Parse(firstRecord, separator);
if (header.Count is 1 && header[0].Count == expectedNumFields)
return Parse(content, separator);
}
throw new InvalidDataException("Was not able to detect the CSV separator.");
}
private static string ReadFirstRecord(string content)
{
var isQuoted = false;
for (var index = 0; index < content.Length; index++)
{
if (content[index] is '"')
{
if (isQuoted && index + 1 < content.Length && content[index + 1] is '"')
{
index++;
continue;
}
isQuoted = !isQuoted;
}
else if (content[index] is '\n' && !isQuoted)
return content[..(index + 1)];
}
return content;
}
}

View File

@ -0,0 +1,13 @@
namespace AIStudio.Assistants.BatchProcessing;
/// <summary>
/// Defines the separators available for Batch Processing result tables.
/// </summary>
public enum BatchProcessingCsvSeparator
{
COMMA,
SEMICOLON,
PIPE,
TAB,
CUSTOM,
}

View File

@ -0,0 +1,41 @@
namespace AIStudio.Assistants.BatchProcessing;
public static class BatchProcessingCsvSeparatorExtensions
{
private const char DEFAULT_SEPARATOR = ';';
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingCsvSeparatorExtensions).Namespace, nameof(BatchProcessingCsvSeparatorExtensions));
public static string Name(this BatchProcessingCsvSeparator separator) => separator switch
{
BatchProcessingCsvSeparator.COMMA => TB("Comma (,)"),
BatchProcessingCsvSeparator.SEMICOLON => TB("Semicolon (;)"),
BatchProcessingCsvSeparator.PIPE => TB("Vertical bar (|)"),
BatchProcessingCsvSeparator.TAB => TB("Tab"),
BatchProcessingCsvSeparator.CUSTOM => TB("Custom character"),
_ => TB("Unknown"),
};
public static char Character(this BatchProcessingCsvSeparator separator, string customSeparator) => separator switch
{
BatchProcessingCsvSeparator.COMMA => ',',
BatchProcessingCsvSeparator.SEMICOLON => ';',
BatchProcessingCsvSeparator.PIPE => '|',
BatchProcessingCsvSeparator.TAB => '\t',
BatchProcessingCsvSeparator.CUSTOM when IsValidCustomSeparator(customSeparator) => customSeparator[0],
_ => DEFAULT_SEPARATOR,
};
internal static bool IsValidCustomSeparator(string separator)
{
if (string.IsNullOrEmpty(separator) || separator.Length is not 1)
return false;
var character = separator[0];
return !char.IsLetterOrDigit(character)
&& !char.IsWhiteSpace(character)
&& character is not '"' and not '\r' and not '\n';
}
}

View File

@ -334,6 +334,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T83711157"] = "Copy result
-- The transcription provider returned an empty transcript.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1080540822"] = "The transcription provider returned an empty transcript."
-- We always write a semicolon-separated log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1124333059"] = "We always write a semicolon-separated log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder."
-- Name of the results table (optional)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1127380661"] = "Name of the results table (optional)"
@ -382,6 +385,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- No matching files were found in the selected folder.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1528532808"] = "No matching files were found in the selected folder."
-- Custom column separator
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1547654319"] = "Custom column separator"
-- Select the output folder
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1598970341"] = "Select the output folder"
@ -421,9 +427,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Configured instructions file: {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2215428124"] = "Configured instructions file: {0}"
-- We always write a log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. Its columns are separated by a vertical bar, so you can open it with any spreadsheet application. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T225540173"] = "We always write a log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. Its columns are separated by a vertical bar, so you can open it with any spreadsheet application. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder."
-- No usable transcription provider is configured.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2282521655"] = "No usable transcription provider is configured."
@ -466,6 +469,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The batch run was canceled.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2641642683"] = "The batch run was canceled."
-- Choose which character separates the columns of the results table.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2642486086"] = "Choose which character separates the columns of the results table."
-- The configured instructions file no longer exists.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2652734495"] = "The configured instructions file no longer exists."
@ -481,6 +487,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Each answer is stored as its own Markdown file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result.md.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2724126639"] = "Each answer is stored as its own Markdown file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result.md."
-- Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2908365499"] = "Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators."
-- Was not able to write the result file: {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Was not able to write the result file: {0}"
@ -547,12 +556,18 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Was not able to extract any text from this file.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T4175885324"] = "Was not able to extract any text from this file."
-- Column separator
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T423947932"] = "Column separator"
-- The configured instructions file could not be read.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T4274794480"] = "The configured instructions file could not be read."
-- Progress
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T439787878"] = "Progress"
-- Enter one punctuation or symbol character.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T469253621"] = "Enter one punctuation or symbol character."
-- No, only process files in the selected folder
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T49675965"] = "No, only process files in the selected folder"
@ -583,6 +598,24 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- 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.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T822136905"] = "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."
-- Comma (,)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T1676507543"] = "Comma (,)"
-- Semicolon (;)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T3267990938"] = "Semicolon (;)"
-- Unknown
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T3424652889"] = "Unknown"
-- Tab
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T4219689196"] = "Tab"
-- Vertical bar (|)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T4252399493"] = "Vertical bar (|)"
-- Custom character
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T719177757"] = "Custom character"
-- One CSV results table, where each answer becomes one row
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T1515293131"] = "One CSV results table, where each answer becomes one row"
@ -6523,6 +6556,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T17
-- Batch processing options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1893713430"] = "Batch processing options are preselected"
-- Default custom column separator
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T19367494"] = "Default custom column separator"
-- Default document analysis policy
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2015391667"] = "Default document analysis policy"
@ -6547,9 +6583,15 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T26
-- Input
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2677268763"] = "Input"
-- Default column separator
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2745158463"] = "Default column separator"
-- Preselect batch processing options?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2849251744"] = "Preselect batch processing options?"
-- Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2908365499"] = "Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators."
-- Default file patterns
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2909693903"] = "Default file patterns"
@ -6592,12 +6634,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T40
-- The configured default policy no longer exists. Select another policy before starting a policy-based batch run.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T438852523"] = "The configured default policy no longer exists. Select another policy before starting a policy-based batch run."
-- Enter one punctuation or symbol character.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T469253621"] = "Enter one punctuation or symbol character."
-- Select the default Markdown instructions file
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T470691525"] = "Select the default Markdown instructions file"
-- Assistant: Batch Processing defaults
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T481452904"] = "Assistant: Batch Processing defaults"
-- Choose which character separates the columns of new results tables.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T573962596"] = "Choose which character separates the columns of new results tables."
-- Default output mode
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T601648878"] = "Default output mode"

View File

@ -15,6 +15,7 @@
AutoGrow="@this.AutoGrow"
MaxLines="@this.GetMaxLines"
Immediate="@true"
Validation="@this.Validation"
Underline="false"
/>
}
@ -34,6 +35,7 @@ else
AutoGrow="@this.AutoGrow"
MaxLines="@this.GetMaxLines"
Immediate="@true"
Validation="@this.Validation"
Underline="false"
Class="flex-grow-1"
/>
@ -41,4 +43,4 @@ else
@this.ResetButtonText
</MudButton>
</MudStack>
}
}

View File

@ -53,6 +53,12 @@ public partial class ConfigurationText : ConfigurationBaseCore
/// </summary>
[Parameter]
public string ResetButtonText { get; set; } = string.Empty;
/// <summary>
/// Validates the configured text before it is stored.
/// </summary>
[Parameter]
public Func<string, string?>? Validation { get; set; }
private string internalText = string.Empty;
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
@ -69,10 +75,6 @@ public partial class ConfigurationText : ConfigurationBaseCore
protected override string Label => this.OptionDescription;
#endregion
#region Overrides of ConfigurationBase
protected override async Task OnInitializedAsync()
{
this.timer.Elapsed += async (_, _) => await this.InvokeAsync(async () => await this.OptionChanged(this.internalText));
@ -110,6 +112,9 @@ public partial class ConfigurationText : ConfigurationBaseCore
private async Task OptionChanged(string updatedText)
{
if (this.Validation?.Invoke(updatedText) is not null)
return;
this.TextUpdate(updatedText);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();

View File

@ -47,6 +47,11 @@
{
<ConfigurationText OptionDescription="@T("Default results table name")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Description" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.CsvFileName)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.CsvFileName = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.CsvFileName, out var meta) && meta.IsLocked"/>
<ConfigurationText OptionDescription="@T("Default result column header")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.TableChart" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.ResultColumnHeader)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.ResultColumnHeader = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ResultColumnHeader, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Default column separator")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.CsvSeparator)" Data="@this.CsvSeparatorData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.CsvSeparator = value)" OptionHelp="@T("Choose which character separates the columns of new results tables.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.CsvSeparator, out var meta) && meta.IsLocked"/>
@if (this.SettingsManager.ConfigurationData.BatchProcessing.CsvSeparator is BatchProcessingCsvSeparator.CUSTOM)
{
<ConfigurationText OptionDescription="@T("Default custom column separator")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Edit" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.CustomCsvSeparator)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.CustomCsvSeparator = value)" Validation="@this.ValidateCustomCsvSeparator" OptionHelp="@T("Enter one punctuation or symbol character.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.CustomCsvSeparator, out var meta) && meta.IsLocked"/>
}
}
<ConfigurationDirectory OptionDescription="@T("Default output folder")" Disabled="@this.DefaultsDisabled" DirectoryDialogTitle="@T("Select the default output folder")" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.OutputDirectory)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.OutputDirectory = value)" OptionHelp="@T("Leave empty to use the ai-results subfolder of the input folder.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.OutputDirectory, out var meta) && meta.IsLocked"/>

View File

@ -45,6 +45,21 @@ public partial class SettingsDialogBatchProcessing : SettingsDialogBase
.Select(value => new ConfigurationSelectData<BatchProcessingOutputMode>(value.Name(), value))
];
private IReadOnlyList<ConfigurationSelectData<BatchProcessingCsvSeparator>> CsvSeparatorData =>
[
.. Enum
.GetValues<BatchProcessingCsvSeparator>()
.Select(value => new ConfigurationSelectData<BatchProcessingCsvSeparator>(value.Name(), value))
];
private string? ValidateCustomCsvSeparator(string separator)
{
if (!BatchProcessingCsvSeparatorExtensions.IsValidCustomSeparator(separator))
return T("Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators.");
return null;
}
private IReadOnlyList<ConfigurationSelectData<string>> PolicyData
{
get

View File

@ -424,6 +424,10 @@ CONFIG["SETTINGS"] = {}
-- CONFIG["SETTINGS"]["DataBatchProcessing.OutputMode"] = "MARKDOWN_FILES"
-- CONFIG["SETTINGS"]["DataBatchProcessing.CsvFileName"] = "batch-results.csv"
-- CONFIG["SETTINGS"]["DataBatchProcessing.ResultColumnHeader"] = "Result"
-- Allowed CSV separator values are: COMMA, SEMICOLON, PIPE, TAB, CUSTOM
-- A custom separator must be exactly one punctuation or symbol character.
-- CONFIG["SETTINGS"]["DataBatchProcessing.CsvSeparator"] = "SEMICOLON"
-- CONFIG["SETTINGS"]["DataBatchProcessing.CustomCsvSeparator"] = "^"
--
-- Configure the minimum provider confidence and the default provider.
-- Allowed confidence values are: NONE, UNTRUSTED, UNKNOWN, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
@ -444,6 +448,8 @@ CONFIG["SETTINGS"] = {}
-- CONFIG["SETTINGS"]["DataBatchProcessing.OutputMode.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataBatchProcessing.CsvFileName.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataBatchProcessing.ResultColumnHeader.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataBatchProcessing.CsvSeparator.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataBatchProcessing.CustomCsvSeparator.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataBatchProcessing.MinimumProviderConfidence.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataBatchProcessing.PreselectedProvider.AllowUserOverride"] = true

View File

@ -44,6 +44,10 @@ public sealed class DataBatchProcessing(Expression<Func<Data, DataBatchProcessin
public string ResultColumnHeader { get; set; } = ManagedConfiguration.Register(configSelection, value => value.ResultColumnHeader, string.Empty);
public BatchProcessingCsvSeparator CsvSeparator { get; set; } = ManagedConfiguration.Register(configSelection, value => value.CsvSeparator, BatchProcessingCsvSeparator.SEMICOLON);
public string CustomCsvSeparator { get; set; } = ManagedConfiguration.Register(configSelection, value => value.CustomCsvSeparator, string.Empty);
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE);
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProvider, string.Empty);

View File

@ -350,6 +350,8 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.OutputMode, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.CsvFileName, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.ResultColumnHeader, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.CsvSeparator, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.CustomCsvSeparator, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);