mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-01 17:49:14 +00:00
Rework of the file export (#894)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
parent
f14c69b938
commit
c66a61713d
@ -75,9 +75,9 @@
|
||||
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
|
||||
</div>
|
||||
|
||||
@if (this.ShowResult && !this.ShowEntireChatThread && this.ResultingContentBlock is not null && this.ResultingContentBlock.Content is not null)
|
||||
@if (this.ShowResult && !this.ShowEntireChatThread && this.ResultingContentBlock?.Content != null)
|
||||
{
|
||||
<ContentBlockComponent Role="@(this.ResultingContentBlock.Role)" Type="@(this.ResultingContentBlock.ContentType)" Time="@(this.ResultingContentBlock.Time)" Content="@this.ResultingContentBlock.Content"/>
|
||||
<ContentBlockComponent Role="@(this.ResultingContentBlock.Role)" Type="@(this.ResultingContentBlock.ContentType)" Time="@(this.ResultingContentBlock.Time)" Content="@this.ResultingContentBlock.Content" ExportTitle="@TB("Export result")"/>
|
||||
}
|
||||
|
||||
@if(this.ShowResult && this.ShowEntireChatThread && this.ChatThread is not null)
|
||||
@ -86,7 +86,7 @@
|
||||
{
|
||||
@if (block is { HideFromUser: false, Content: not null })
|
||||
{
|
||||
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
|
||||
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content" ExportTitle="@TB("Export result")"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,10 +115,19 @@ else
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (this.outputMode is BatchProcessingOutputMode.MARKDOWN_FILES)
|
||||
@if (this.outputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES)
|
||||
{
|
||||
<MudSelect T="FileExportFormat" @bind-Value="@this.resultFileFormat" Disabled="@this.isProcessingBatch" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="@T("File format")" HelperText="@T("Choose the format of the result files. Everything except Markdown is converted by Pandoc, which AI Studio offers to install when it is missing.")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3">
|
||||
@foreach (var format in FileExportFormatExtensions.ANSWER_FORMATS)
|
||||
{
|
||||
<MudSelectItem Value="@format">
|
||||
@format.ToName()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("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.")
|
||||
@(string.Format(T("Each answer is stored as its own file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result{0}."), this.resultFileFormat.ToFileExtension()))
|
||||
</MudJustifiedText>
|
||||
}
|
||||
else
|
||||
|
||||
@ -71,8 +71,8 @@ public partial class AssistantBatchProcessing
|
||||
/// <summary>
|
||||
/// Checks whether a document can be restored from the previous run. Beyond
|
||||
/// the log entry, the result of the previous run must still exist: in the
|
||||
/// table mode the answer within the results table, in the Markdown mode the
|
||||
/// result file. Without the result, restoring would mark the document as
|
||||
/// table mode the answer within the results table, in the individual file
|
||||
/// mode the result file. Without the result, restoring would mark the document as
|
||||
/// done while its answer is lost, so we process it again instead.
|
||||
/// </summary>
|
||||
private bool CanRestoreFromPreviousRun(string relativePath, string resolvedOutputDirectory, Dictionary<string, BatchProcessingLogEntry> previousLog, Dictionary<string, string> previousResults, out BatchProcessingLogEntry? logEntry)
|
||||
@ -106,9 +106,9 @@ public partial class AssistantBatchProcessing
|
||||
private async Task WriteLogAsync(string resolvedOutputDirectory)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(BatchProcessingCsv.ToCsvRow(LOG_SEPARATOR, T("File"), T("Time"), T("Model"), T("Status"), T("Details")));
|
||||
sb.AppendLine(CsvWriter.ToRow(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(LOG_SEPARATOR, fileResult.RelativePath, fileResult.ProcessedAt.ToString(TIME_FORMAT, CultureInfo.InvariantCulture), fileResult.ModelName, fileResult.Status.ToString(), fileResult.Message));
|
||||
sb.AppendLine(CsvWriter.ToRow(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());
|
||||
}
|
||||
@ -120,9 +120,9 @@ public partial class AssistantBatchProcessing
|
||||
{
|
||||
var separator = this.csvSeparator.Character(this.customCsvSeparator);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(BatchProcessingCsv.ToCsvRow(separator, T("File"), this.ResultColumnHeader));
|
||||
sb.AppendLine(CsvWriter.ToRow(separator, T("File"), this.ResultColumnHeader));
|
||||
foreach (var fileResult in this.fileResults.Where(x => x.Status is BatchProcessingFileStatus.DONE))
|
||||
sb.AppendLine(BatchProcessingCsv.ToCsvRow(separator, fileResult.RelativePath, fileResult.ResultText));
|
||||
sb.AppendLine(CsvWriter.ToRow(separator, fileResult.RelativePath, fileResult.ResultText));
|
||||
|
||||
await this.WriteCsvFileAsync(Path.Join(resolvedOutputDirectory, this.ResolveResultsFileName()), sb.ToString());
|
||||
}
|
||||
@ -232,7 +232,7 @@ public partial class AssistantBatchProcessing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the name of the Markdown result file for one document.
|
||||
/// Creates the name of the result file for one document, in the chosen file format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Two documents of the same run may share their name and differ only in
|
||||
@ -242,13 +242,14 @@ public partial class AssistantBatchProcessing
|
||||
/// </remarks>
|
||||
private string CreateResultFileName(string sourceFileName)
|
||||
{
|
||||
var extension = this.resultFileFormat.ToFileExtension();
|
||||
var stem = Path.GetFileNameWithoutExtension(sourceFileName);
|
||||
var candidate = $"{stem}{RESULT_FILE_SUFFIX}";
|
||||
var candidate = $"{stem}{RESULT_FILE_SUFFIX}{extension}";
|
||||
|
||||
var counter = 2;
|
||||
while (!this.usedResultFileNames.Add(candidate))
|
||||
{
|
||||
candidate = $"{stem}_result_{counter}.md";
|
||||
candidate = $"{stem}{RESULT_FILE_SUFFIX}_{counter}{extension}";
|
||||
counter++;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Assistants.BatchProcessing;
|
||||
|
||||
@ -14,6 +13,19 @@ public partial class AssistantBatchProcessing
|
||||
|
||||
var (resolvedOutputDirectory, files) = runPreparation.Value;
|
||||
|
||||
//
|
||||
// Every format but Markdown is written by Pandoc, so it has to be there before the first
|
||||
// document. Asking per document would put the installation dialog in front of the user
|
||||
// hundreds of times, and starting without it would spend time and tokens on answers we
|
||||
// cannot write anywhere:
|
||||
//
|
||||
if (this.outputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES && this.resultFileFormat.UsesPandoc())
|
||||
{
|
||||
var pandocState = await this.PandocAvailability.EnsureAvailabilityAsync(showSuccessMessage: false, showDialog: true);
|
||||
if (!pandocState.IsAvailable)
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// When the output folder already contains a log, a previous run was
|
||||
// interrupted or produced errors. Let the user decide what to do:
|
||||
@ -63,7 +75,7 @@ public partial class AssistantBatchProcessing
|
||||
if (DateTimeOffset.TryParseExact(logEntry.Time, TIME_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var processedAt))
|
||||
fileResult.ProcessedAt = processedAt;
|
||||
|
||||
// Reserve the Markdown file name of the previous run, so that a
|
||||
// Reserve the result file name of the previous run, so that a
|
||||
// document processed now cannot overwrite that earlier result:
|
||||
if (!string.IsNullOrWhiteSpace(logEntry.Details))
|
||||
this.usedResultFileNames.Add(logEntry.Details);
|
||||
@ -211,12 +223,26 @@ public partial class AssistantBatchProcessing
|
||||
}
|
||||
|
||||
fileResult.ResultText = aiAnswer;
|
||||
if (this.outputMode is BatchProcessingOutputMode.MARKDOWN_FILES)
|
||||
if (this.outputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resultFilePath = Path.Join(resolvedOutputDirectory, this.CreateResultFileName(fileResult.FileName));
|
||||
await File.WriteAllTextAsync(resultFilePath, aiAnswer, Encoding.UTF8, CancellationToken.None);
|
||||
if (this.resultFileFormat.UsesPandoc())
|
||||
{
|
||||
//
|
||||
// Pandoc reports a failure instead of throwing, because one document which
|
||||
// cannot be converted must not end a run over hundreds of them:
|
||||
//
|
||||
if (!await PandocExport.ConvertAsync(this.RustService, aiAnswer, resultFilePath, this.resultFileFormat, token))
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("Was not able to convert the answer into the chosen file format."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
await File.WriteAllTextAsync(resultFilePath, aiAnswer, this.resultFileFormat.ToFileEncoding(), CancellationToken.None);
|
||||
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.DONE, Path.GetFileName(resultFilePath));
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@ -16,6 +16,7 @@ public partial class AssistantBatchProcessing
|
||||
private static readonly AssistantSessionStateKey<string> PROMPT_FILE_LOAD_ISSUE_STATE_KEY = new(nameof(promptFileLoadIssue));
|
||||
private static readonly AssistantSessionStateKey<DataDocumentAnalysisPolicy?> SELECTED_POLICY_STATE_KEY = new(nameof(selectedPolicy));
|
||||
private static readonly AssistantSessionStateKey<BatchProcessingOutputMode> OUTPUT_MODE_STATE_KEY = new(nameof(outputMode));
|
||||
private static readonly AssistantSessionStateKey<FileExportFormat> RESULT_FILE_FORMAT_STATE_KEY = new(nameof(resultFileFormat));
|
||||
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));
|
||||
@ -43,6 +44,7 @@ public partial class AssistantBatchProcessing
|
||||
state.Set(PROMPT_FILE_LOAD_ISSUE_STATE_KEY, this.promptFileLoadIssue);
|
||||
state.Set(SELECTED_POLICY_STATE_KEY, this.selectedPolicy);
|
||||
state.Set(OUTPUT_MODE_STATE_KEY, this.outputMode);
|
||||
state.Set(RESULT_FILE_FORMAT_STATE_KEY, this.resultFileFormat);
|
||||
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);
|
||||
@ -71,6 +73,7 @@ public partial class AssistantBatchProcessing
|
||||
state.Restore(PROMPT_FILE_LOAD_ISSUE_STATE_KEY, value => this.promptFileLoadIssue = value);
|
||||
state.Restore(SELECTED_POLICY_STATE_KEY, value => this.selectedPolicy = value);
|
||||
state.Restore(OUTPUT_MODE_STATE_KEY, value => this.outputMode = value);
|
||||
state.Restore(RESULT_FILE_FORMAT_STATE_KEY, value => this.resultFileFormat = 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);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@ -11,10 +12,13 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PandocAvailabilityService PandocAvailability { get; init; } = null!;
|
||||
|
||||
private const string DEFAULT_OUTPUT_DIRECTORY_NAME = "ai-results";
|
||||
private const string DEFAULT_RESULTS_FILENAME = "batch-results.csv";
|
||||
private const string CSV_EXTENSION = ".csv";
|
||||
private const string RESULT_FILE_SUFFIX = "_result.md";
|
||||
private const string RESULT_FILE_SUFFIX = "_result";
|
||||
private const string TRANSCRIPT_FILE_SUFFIX = ".transcript.md";
|
||||
private const string TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
private const char LOG_SEPARATOR = ';';
|
||||
@ -87,7 +91,8 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
private string promptFilePath = string.Empty;
|
||||
private string promptFileLoadIssue = string.Empty;
|
||||
private DataDocumentAnalysisPolicy? selectedPolicy;
|
||||
private BatchProcessingOutputMode outputMode = BatchProcessingOutputMode.MARKDOWN_FILES;
|
||||
private BatchProcessingOutputMode outputMode = BatchProcessingOutputMode.INDIVIDUAL_FILES;
|
||||
private FileExportFormat resultFileFormat = FileExportFormat.MARKDOWN;
|
||||
private string resultColumnHeader = string.Empty;
|
||||
private string csvFileName = string.Empty;
|
||||
private BatchProcessingCsvSeparator csvSeparator = BatchProcessingCsvSeparator.SEMICOLON;
|
||||
@ -160,7 +165,8 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
this.freePrompt = string.Empty;
|
||||
this.promptFilePath = string.Empty;
|
||||
this.selectedPolicy = null;
|
||||
this.outputMode = BatchProcessingOutputMode.MARKDOWN_FILES;
|
||||
this.outputMode = BatchProcessingOutputMode.INDIVIDUAL_FILES;
|
||||
this.resultFileFormat = FileExportFormat.MARKDOWN;
|
||||
this.resultColumnHeader = string.Empty;
|
||||
this.csvFileName = string.Empty;
|
||||
this.csvSeparator = BatchProcessingCsvSeparator.SEMICOLON;
|
||||
@ -180,6 +186,7 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
this.selectedPolicy = this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies
|
||||
.FirstOrDefault(policy => policy.Id == settings.PreselectedPolicyId);
|
||||
this.outputMode = settings.OutputMode;
|
||||
this.resultFileFormat = settings.ResultFileFormat;
|
||||
this.resultColumnHeader = settings.ResultColumnHeader;
|
||||
this.csvFileName = settings.CsvFileName;
|
||||
this.csvSeparator = settings.CsvSeparator;
|
||||
|
||||
@ -3,35 +3,13 @@ using System.Text;
|
||||
namespace AIStudio.Assistants.BatchProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Reads and writes the CSV files of the batch processing assistant. Fields
|
||||
/// are quoted according to RFC 4180 using the separator selected for the
|
||||
/// respective file.
|
||||
/// Reads the CSV files of the batch processing assistant. Writing them is the job of CsvWriter,
|
||||
/// which quotes fields according to RFC 4180 using the separator selected for the respective file.
|
||||
/// </summary>
|
||||
public static class BatchProcessingCsv
|
||||
{
|
||||
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, char separator)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return string.Empty;
|
||||
|
||||
// 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("\"", "\"\"")}"
|
||||
""";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a CSV text which was written by <see cref="ToCsvRow"/>.
|
||||
/// Parses a CSV text which was written by CsvWriter.ToRow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We parse the file ourselves instead of splitting lines, because quoted
|
||||
|
||||
@ -6,9 +6,14 @@ namespace AIStudio.Assistants.BatchProcessing;
|
||||
public enum BatchProcessingOutputMode
|
||||
{
|
||||
/// <summary>
|
||||
/// One Markdown result file per processed document.
|
||||
/// One result file per processed document, written in the chosen file format.
|
||||
/// </summary>
|
||||
MARKDOWN_FILES,
|
||||
/// <remarks>
|
||||
/// This must stay the first member. Enums are persisted under their name, and an unknown name
|
||||
/// falls back to the default value of the enum, which is the member with the value zero. That
|
||||
/// is what lets settings written before this member was renamed still land here.
|
||||
/// </remarks>
|
||||
INDIVIDUAL_FILES,
|
||||
|
||||
/// <summary>
|
||||
/// A CSV results table, where each AI answer becomes one row. The content of
|
||||
|
||||
@ -6,7 +6,7 @@ public static class BatchProcessingOutputModeExtensions
|
||||
|
||||
public static string Name(this BatchProcessingOutputMode outputMode) => outputMode switch
|
||||
{
|
||||
BatchProcessingOutputMode.MARKDOWN_FILES => TB("One Markdown file per document"),
|
||||
BatchProcessingOutputMode.INDIVIDUAL_FILES => TB("One file per document"),
|
||||
BatchProcessingOutputMode.TABLE_ONLY => TB("One CSV results table, where each answer becomes one row"),
|
||||
|
||||
_ => TB("Unknown output mode"),
|
||||
|
||||
@ -316,6 +316,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1809312323"] = "Please se
|
||||
-- The assistant failed. The message is: '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1833836281"] = "The assistant failed. The message is: '{0}'"
|
||||
|
||||
-- Export result
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1840311560"] = "Export result"
|
||||
|
||||
-- The media transcription was canceled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T241403726"] = "The media transcription was canceled."
|
||||
|
||||
@ -379,6 +382,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Failed
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1434043348"] = "Failed"
|
||||
|
||||
-- Choose the format of the result files. Everything except Markdown is converted by Pandoc, which AI Studio offers to install when it is missing.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1457759640"] = "Choose the format of the result files. Everything except Markdown is converted by Pandoc, which AI Studio offers to install when it is missing."
|
||||
|
||||
-- Please select the file which contains your instructions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1462027716"] = "Please select the file which contains your instructions."
|
||||
|
||||
@ -496,15 +502,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Was not able to read the log of the previous run. Continuing the run would process all documents again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2717277840"] = "Was not able to read the log of the previous run. Continuing the run would process all documents again."
|
||||
|
||||
-- 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."
|
||||
|
||||
-- Before the next file starts, AI Studio waits for a random number of whole seconds from this interval. The minimum is always 6 seconds and the maximum is 300 seconds (5 minutes). Restored files and the end of a run do not add another pause.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2742154256"] = "Before the next file starts, AI Studio waits for a random number of whole seconds from this interval. The minimum is always 6 seconds and the maximum is 300 seconds (5 minutes). Restored files and the end of a run do not add another pause."
|
||||
|
||||
-- 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 convert the answer into the chosen file format.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2949919602"] = "Was not able to convert the answer into the chosen file format."
|
||||
|
||||
-- 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}"
|
||||
|
||||
@ -583,9 +589,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- The configured instructions file could not be read.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T4274794480"] = "The configured instructions file could not be read."
|
||||
|
||||
-- Each answer is stored as its own file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result{0}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T428878781"] = "Each answer is stored as its own file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result{0}."
|
||||
|
||||
-- Progress
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T439787878"] = "Progress"
|
||||
|
||||
-- File format
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T450269462"] = "File format"
|
||||
|
||||
-- Enter one punctuation or symbol character.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T469253621"] = "Enter one punctuation or symbol character."
|
||||
|
||||
@ -637,15 +649,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARA
|
||||
-- Custom character
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T719177757"] = "Custom character"
|
||||
|
||||
-- One file per document
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T1430232553"] = "One file per document"
|
||||
|
||||
-- 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"
|
||||
|
||||
-- Unknown output mode
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T2013180377"] = "Unknown output mode"
|
||||
|
||||
-- One Markdown file per document
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T2420177746"] = "One Markdown file per document"
|
||||
|
||||
-- Use a free prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGPROMPTSOURCEEXTENSIONS::T1144335"] = "Use a free prompt"
|
||||
|
||||
@ -3175,6 +3187,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "AI"
|
||||
-- Edit Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Edit Message"
|
||||
|
||||
-- Table {0} ({1})
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1340759627"] = "Table {0} ({1})"
|
||||
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?"
|
||||
|
||||
@ -3199,6 +3214,12 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes
|
||||
-- Regenerate Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message"
|
||||
|
||||
-- Failed to export this message, because the file format '{0}' is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2544592344"] = "Failed to export this message, because the file format '{0}' is unknown."
|
||||
|
||||
-- Export AI response
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2822776450"] = "Export AI response"
|
||||
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments"
|
||||
|
||||
@ -3220,9 +3241,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove
|
||||
-- No, keep it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it"
|
||||
|
||||
-- Export Chat to Microsoft Word
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word"
|
||||
|
||||
-- The file '{0}' is currently not available and was not sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T1432544573"] = "The file '{0}' is currently not available and was not sent."
|
||||
|
||||
@ -6862,6 +6880,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T26
|
||||
-- Default column separator
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2745158463"] = "Default column separator"
|
||||
|
||||
-- Choose the format of new result files. Everything except Markdown is converted by Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2760965660"] = "Choose the format of new result files. Everything except Markdown is converted by Pandoc."
|
||||
|
||||
-- Preselect batch processing options?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2849251744"] = "Preselect batch processing options?"
|
||||
|
||||
@ -6919,6 +6940,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T39
|
||||
-- Output
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T4000727844"] = "Output"
|
||||
|
||||
-- Default file format
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T4046754119"] = "Default file format"
|
||||
|
||||
-- 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."
|
||||
|
||||
@ -9967,6 +9991,30 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The
|
||||
-- policy files
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files"
|
||||
|
||||
-- OpenDocument Text (.odt), e.g. LibreOffice
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1612025407"] = "OpenDocument Text (.odt), e.g. LibreOffice"
|
||||
|
||||
-- LaTeX (.tex)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2233607007"] = "LaTeX (.tex)"
|
||||
|
||||
-- Markdown (.md)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2319970170"] = "Markdown (.md)"
|
||||
|
||||
-- Table (.tsv)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T293798559"] = "Table (.tsv)"
|
||||
|
||||
-- Microsoft Word (.docx)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3054800422"] = "Microsoft Word (.docx)"
|
||||
|
||||
-- Webpage (.html)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3651679344"] = "Webpage (.html)"
|
||||
|
||||
-- Table (.csv)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T530872684"] = "Table (.csv)"
|
||||
|
||||
-- Unknown format
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T677355172"] = "Unknown format"
|
||||
|
||||
-- The file type of '{0}' could not be determined, so the file was not sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T1459702734"] = "The file type of '{0}' could not be determined, so the file was not sent."
|
||||
|
||||
@ -10069,17 +10117,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't fin
|
||||
-- AI Studio couldn't install Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc."
|
||||
|
||||
-- Pandoc is required for Microsoft Word export.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
|
||||
-- The export succeeded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1713926719"] = "The export succeeded."
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T185447014"] = "Pandoc Installation"
|
||||
-- The export failed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1895034475"] = "The export failed."
|
||||
|
||||
-- Error during Microsoft Word export
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Microsoft Word export"
|
||||
-- Only text messages can be exported.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Only text messages can be exported."
|
||||
|
||||
-- Microsoft Word export successful
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful"
|
||||
-- The export succeeded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "The export succeeded."
|
||||
|
||||
-- The export failed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "The export failed."
|
||||
|
||||
-- Text
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text"
|
||||
@ -10876,8 +10927,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T63285243
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T185447014"] = "Pandoc Installation"
|
||||
|
||||
-- Pandoc may be required for importing files.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
||||
-- AI Studio needs Pandoc for this, but it is not available.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2610026134"] = "AI Studio needs Pandoc for this, but it is not available."
|
||||
|
||||
-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1138181282"] = "This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins."
|
||||
@ -11095,9 +11146,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources pro
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI"
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc Installation"
|
||||
|
||||
-- The file path is null or empty and the file therefore can not be loaded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T932243993"] = "The file path is null or empty and the file therefore can not be loaded."
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ using AIStudio.Chat;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Tools.AssistantSessions;
|
||||
using AIStudio.Tools.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
#if !DEBUG
|
||||
@ -28,6 +29,9 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialog
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PandocAvailabilityService PandocAvailability { get; init; } = null!;
|
||||
|
||||
protected override Tools.Components Component => Tools.Components.PROMPT_OPTIMIZER_ASSISTANT;
|
||||
|
||||
protected override string Title => T("Prompt Optimizer");
|
||||
@ -581,7 +585,7 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialog
|
||||
this.isLoadingCustomPromptGuide = true;
|
||||
|
||||
// A failure was already reported by UserFile.LoadFileData, so we only keep the content:
|
||||
var extraction = await UserFile.LoadFileData(fileAttachment.FilePath, this.RustService, this.DialogService);
|
||||
var extraction = await UserFile.LoadFileData(fileAttachment.FilePath, this.RustService, this.PandocAvailability);
|
||||
this.customPromptingGuidelineContent = extraction.HasUsableContent ? extraction.Content : string.Empty;
|
||||
}
|
||||
catch
|
||||
|
||||
@ -16,55 +16,75 @@
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
@if (this.Content.FileAttachments.Count > 0)
|
||||
{
|
||||
<MudTooltip Text="@T("Number of attachments")" Placement="Placement.Bottom">
|
||||
<MudBadge Content="@this.Content.FileAttachments.Count" Color="Color.Primary" Overlap="true" BadgeClass="sources-card-header">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AttachFile"
|
||||
OnClick="@this.OpenAttachmentsDialog"/>
|
||||
</MudBadge>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.Content.Sources.Count > 0)
|
||||
{
|
||||
<MudTooltip Text="@T("Number of sources")" Placement="Placement.Bottom">
|
||||
<MudBadge Content="@this.Content.Sources.Count" Color="Color.Primary" Overlap="true" BadgeClass="sources-card-header">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Link"/>
|
||||
</MudBadge>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Regenerate")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.RemoveBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Removes this block")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<div class="d-flex align-center">
|
||||
@if (this.Content.FileAttachments.Count > 0)
|
||||
{
|
||||
<MudTooltip Text="@T("Number of attachments")" Placement="Placement.Bottom">
|
||||
<MudBadge Content="@this.Content.FileAttachments.Count" Color="Color.Primary" Overlap="true" BadgeClass="sources-card-header">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AttachFile"
|
||||
OnClick="@this.OpenAttachmentsDialog"/>
|
||||
</MudBadge>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.Content.Sources.Count > 0)
|
||||
{
|
||||
<MudTooltip Text="@T("Number of sources")" Placement="Placement.Bottom">
|
||||
<MudBadge Content="@this.Content.Sources.Count" Color="Color.Primary" Overlap="true" BadgeClass="sources-card-header">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Link"/>
|
||||
</MudBadge>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Regenerate")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.RemoveBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Removes this block")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.Role is ChatRole.AI)
|
||||
{
|
||||
<MudTooltip Text="@T("Export Chat to Microsoft Word")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@this.ExportToWord"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudCopyClipboardButton Content="@this.Content" Type="@this.Type" Size="Size.Medium"/>
|
||||
@if (this.Role is ChatRole.AI && this.CanExport)
|
||||
{
|
||||
<MudTooltip Text="@this.EffectiveExportTitle" Placement="Placement.Bottom">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Save">
|
||||
@foreach (var documentFormat in FileExportFormatExtensions.DOCUMENT_FORMATS)
|
||||
{
|
||||
<MudMenuItem OnClick="@(() => this.ExportDocument(documentFormat))" Icon="@documentFormat.ToIcon()" Label="@documentFormat.ToName()"/>
|
||||
}
|
||||
@if (this.MessageTables.Count > 0)
|
||||
{
|
||||
<MudDivider/>
|
||||
@foreach (var messageTable in this.MessageTables)
|
||||
{
|
||||
<MudMenuItem OnClick="@(() => this.ExportTable(messageTable))" Icon="@messageTable.Format.ToIcon()" Label="@this.ExportLabel(messageTable)"/>
|
||||
}
|
||||
}
|
||||
<MudDivider/>
|
||||
@foreach (var textFormat in FileExportFormatExtensions.TEXT_FORMATS)
|
||||
{
|
||||
<MudMenuItem OnClick="@(() => this.ExportDocument(textFormat))" Icon="@textFormat.ToIcon()" Label="@textFormat.ToName()"/>
|
||||
}
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudCopyClipboardButton Content="@this.Content" Type="@this.Type" Size="Size.Medium"/>
|
||||
</div>
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
|
||||
@ -84,6 +84,19 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
[Parameter]
|
||||
public Func<bool> RegenerateEnabled { get; set; } = () => false;
|
||||
|
||||
/// <summary>
|
||||
/// What the export offers, used both as the label of the export button and as the title of
|
||||
/// the save dialog.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only AI blocks can be exported, so this always names something the AI produced. In the chat
|
||||
/// that is its response, whereas in an assistant it is the result, and there the user sees no
|
||||
/// chat at all. Whoever renders this block knows which of the two it is. Null falls back to
|
||||
/// the chat wording.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public string? ExportTitle { get; set; }
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
@ -94,22 +107,113 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<ContentBlockComponent> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PandocAvailabilityService PandocAvailability { get; init; } = null!;
|
||||
|
||||
private bool HideContent { get; set; }
|
||||
private bool hasRenderHash;
|
||||
private int lastRenderHash;
|
||||
private string cachedMarkdownRenderPlanInput = string.Empty;
|
||||
private MarkdownRenderPlan cachedMarkdownRenderPlan = MarkdownRenderPlan.EMPTY;
|
||||
private string cachedMessageTablesInput = string.Empty;
|
||||
private IReadOnlyList<MessageTable> cachedMessageTables = [];
|
||||
private char csvSeparator = ',';
|
||||
private ElementReference mathContentContainer;
|
||||
private string lastMathRenderSignature = string.Empty;
|
||||
private bool hasActiveMathContainer;
|
||||
private bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this block can be exported.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We wait for the stream to finish: half an answer is nothing anybody wants in a document,
|
||||
/// and waiting keeps us from searching for a text which still grows with every token. Only text
|
||||
/// can be completely exported; an image, for example, has no representation our formats could write.
|
||||
/// </remarks>
|
||||
private bool CanExport => this.Content is { InitialRemoteWait: false, IsStreaming: false } && this.Content.TryGetMarkdownText(out _);
|
||||
|
||||
/// <summary>
|
||||
/// The tables this block holds so that the export menu can offer each of them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Cached the same way the Markdown render plan is: reading the tables means parsing the whole
|
||||
/// message, and a block re-renders for reasons which have nothing to do with its text, such as
|
||||
/// switching the theme, which would parse every message of a long chat again.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<MessageTable> MessageTables
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!this.Content.TryGetMarkdownText(out var markdown))
|
||||
return [];
|
||||
|
||||
if (ReferenceEquals(this.cachedMessageTablesInput, markdown) || string.Equals(this.cachedMessageTablesInput, markdown, StringComparison.Ordinal))
|
||||
return this.cachedMessageTables;
|
||||
|
||||
this.cachedMessageTablesInput = markdown;
|
||||
this.cachedMessageTables = PlainFileExport.ExtractTables(markdown, this.csvSeparator);
|
||||
return this.cachedMessageTables;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Names one table in the export menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// With a single table the format alone says everything. As soon as an answer holds more than
|
||||
/// one, the user has to be able to tell them apart: the heading above a table does that, unless
|
||||
/// it is missing or two tables share one, and then we count them.
|
||||
/// </remarks>
|
||||
private string ExportLabel(MessageTable table)
|
||||
{
|
||||
var tables = this.MessageTables;
|
||||
if (tables.Count < 2)
|
||||
return table.Format.ToName();
|
||||
|
||||
var captionIsTelling = !string.IsNullOrWhiteSpace(table.Caption)
|
||||
&& tables.Where(entry => entry.Ordinal != table.Ordinal).All(entry => !string.Equals(entry.Caption, table.Caption, StringComparison.Ordinal));
|
||||
|
||||
//
|
||||
// The caption is the heading the model wrote, so it already carries the language of the
|
||||
// answer and needs no translation of ours. Only the fallback, where we have to count the
|
||||
// tables ourselves, is our own wording.
|
||||
//
|
||||
return captionIsTelling
|
||||
? $"{table.Caption} ({table.Format.ToFileExtension()})"
|
||||
: string.Format(this.T("Table {0} ({1})"), table.Ordinal, table.Format.ToFileExtension());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What the export offers, falling back to the chat wording when nobody named it.
|
||||
/// </summary>
|
||||
private string EffectiveExportTitle => this.ExportTitle ?? this.T("Export AI response");
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.RegisterStreamingEvents();
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
//
|
||||
// Which separator a CSV needs depends on the language, and asking for the language means
|
||||
// waiting for the settings. The first render therefore uses the comma we start with; once
|
||||
// we know better, we ask for another render. Nobody can have opened the export menu in
|
||||
// between, so no file is ever written with the wrong separator.
|
||||
//
|
||||
var languagePlugin = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
var separator = CsvWriter.SeparatorFor(languagePlugin.IETFTag);
|
||||
if (separator == this.csvSeparator)
|
||||
return;
|
||||
|
||||
this.csvSeparator = separator;
|
||||
this.cachedMessageTablesInput = string.Empty;
|
||||
this.cachedMessageTables = [];
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
@ -543,9 +647,47 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
await this.RemoveBlockFunc(this.Content);
|
||||
}
|
||||
|
||||
private async Task ExportToWord()
|
||||
/// <summary>
|
||||
/// Exports the entire message.
|
||||
/// </summary>
|
||||
private async Task ExportDocument(FileExportFormat format)
|
||||
{
|
||||
await PandocExport.ToMicrosoftWord(this.RustService, this.DialogService, T("Export Chat to Microsoft Word"), this.Content);
|
||||
try
|
||||
{
|
||||
//
|
||||
// The format itself knows who writes it, so we do not have to keep a list of formats
|
||||
// here which would fall out of sync with the one in FileExportFormatExtensions.
|
||||
//
|
||||
if (format.UsesPandoc())
|
||||
await PandocExport.ToDocument(this.RustService, this.PandocAvailability, this.EffectiveExportTitle, format, this.Content);
|
||||
else if (this.Content.TryGetMarkdownText(out var markdown))
|
||||
await PlainFileExport.ToFile(this.RustService, this.EffectiveExportTitle, format, markdown);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
await this.ReportUnknownExportFormat(e, format);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports one table out of the message, exactly as the menu offered it.
|
||||
/// </summary>
|
||||
private async Task ExportTable(MessageTable table)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PlainFileExport.ToFile(this.RustService, this.EffectiveExportTitle, table.Format, table.Content, table.Caption);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
await this.ReportUnknownExportFormat(e, table.Format);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReportUnknownExportFormat(ArgumentOutOfRangeException exception, FileExportFormat format)
|
||||
{
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.Error, string.Format(this.T("Failed to export this message, because the file format '{0}' is unknown."), format)));
|
||||
this.Logger.LogError(exception, "Failed to export the content, because no exporter writes the format {ExportFormat}.", format);
|
||||
}
|
||||
|
||||
private async Task RegenerateBlock()
|
||||
@ -618,4 +760,4 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
await this.DisposeMathContainerIfNeededAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,4 +17,27 @@ public static class IContentExtensions
|
||||
content.StreamingEvent = IContent.NO_STREAMING_HANDLER;
|
||||
content.StreamingDone = IContent.NO_STREAMING_HANDLER;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads this content as the Markdown text the AI produced.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only text content carries Markdown. Everything else, an image for example, has no text
|
||||
/// representation at all, which is why this reports failure instead of returning a placeholder:
|
||||
/// a caller which writes files must not put an excuse into the file it writes.
|
||||
/// </remarks>
|
||||
/// <param name="content">The content to read.</param>
|
||||
/// <param name="markdown">The Markdown text, or an empty string when there is none.</param>
|
||||
/// <returns>True, when this content carries Markdown text.</returns>
|
||||
public static bool TryGetMarkdownText(this IContent content, out string markdown)
|
||||
{
|
||||
if (content is ContentText text)
|
||||
{
|
||||
markdown = text.Text;
|
||||
return true;
|
||||
}
|
||||
|
||||
markdown = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -344,7 +344,7 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
|
||||
try
|
||||
{
|
||||
var extraction = await UserFile.LoadFileData(filePath, this.RustService, this.DialogService);
|
||||
var extraction = await UserFile.LoadFileData(filePath, this.RustService, this.PandocAvailabilityService);
|
||||
|
||||
// The failure was already reported by UserFile.LoadFileData, so we only stop here:
|
||||
if (!extraction.HasUsableContent)
|
||||
|
||||
@ -64,12 +64,12 @@ public partial class DocumentCheckDialog : MSGComponentBase
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
|
||||
[Inject]
|
||||
private ILogger<DocumentCheckDialog> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PandocAvailabilityService PandocAvailability { get; init; } = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@ -97,7 +97,7 @@ public partial class DocumentCheckDialog : MSGComponentBase
|
||||
|
||||
try
|
||||
{
|
||||
var extraction = await UserFile.LoadFileData(this.Document.FilePath, this.RustService, this.DialogService, this.extractionCancellation.Token);
|
||||
var extraction = await UserFile.LoadFileData(this.Document.FilePath, this.RustService, this.PandocAvailability, this.extractionCancellation.Token);
|
||||
if (this.isDisposed)
|
||||
return;
|
||||
|
||||
|
||||
@ -43,7 +43,11 @@
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-3 mt-6">@T("Output")</MudText>
|
||||
<ConfigurationSelect OptionDescription="@T("Default output mode")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode)" Data="@this.OutputModeData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.OutputMode, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode is BatchProcessingOutputMode.TABLE_ONLY)
|
||||
@if (this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES)
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="@T("Default file format")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.ResultFileFormat)" Data="@ResultFileFormatData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.ResultFileFormat = value)" OptionHelp="@T("Choose the format of new result files. Everything except Markdown is converted by Pandoc.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ResultFileFormat, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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"/>
|
||||
|
||||
@ -61,6 +61,12 @@ public partial class SettingsDialogBatchProcessing : SettingsDialogBase
|
||||
.Select(value => new ConfigurationSelectData<BatchProcessingOutputMode>(value.Name(), value))
|
||||
];
|
||||
|
||||
private static IReadOnlyList<ConfigurationSelectData<FileExportFormat>> ResultFileFormatData =>
|
||||
[
|
||||
.. FileExportFormatExtensions.ANSWER_FORMATS
|
||||
.Select(value => new ConfigurationSelectData<FileExportFormat>(value.ToName(), value))
|
||||
];
|
||||
|
||||
private IReadOnlyList<ConfigurationSelectData<BatchProcessingCsvSeparator>> CsvSeparatorData =>
|
||||
[
|
||||
.. Enum
|
||||
|
||||
@ -492,8 +492,15 @@ CONFIG["SETTINGS"] = {}
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.PreselectedPolicyId"] = ""
|
||||
--
|
||||
-- Configure the default output mode.
|
||||
-- Allowed values are: MARKDOWN_FILES, TABLE_ONLY
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.OutputMode"] = "MARKDOWN_FILES"
|
||||
-- Allowed values are: INDIVIDUAL_FILES, TABLE_ONLY
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.OutputMode"] = "INDIVIDUAL_FILES"
|
||||
--
|
||||
-- Configure the file format of the individual result files. Used only when the output
|
||||
-- mode is INDIVIDUAL_FILES. Everything except MARKDOWN is converted by Pandoc, which
|
||||
-- AI Studio installs on demand.
|
||||
-- Allowed values are: MICROSOFT_WORD, OPEN_DOCUMENT_TEXT, LATEX, MARKDOWN, HTML
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.ResultFileFormat"] = "MARKDOWN"
|
||||
--
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.CsvFileName"] = "batch-results.csv"
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.ResultColumnHeader"] = "Result"
|
||||
-- Allowed CSV separator values are: COMMA, SEMICOLON, PIPE, TAB, CUSTOM
|
||||
@ -523,6 +530,7 @@ CONFIG["SETTINGS"] = {}
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.PromptFilePath.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.PreselectedPolicyId.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.OutputMode.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.ResultFileFormat.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.CsvFileName.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.ResultColumnHeader.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataBatchProcessing.CsvSeparator.AllowUserOverride"] = true
|
||||
|
||||
@ -318,6 +318,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1809312323"] = "Bitte wä
|
||||
-- The assistant failed. The message is: '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1833836281"] = "Der Assistent ist fehlgeschlagen. Die Meldung lautet: „{0}“"
|
||||
|
||||
-- Export result
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1840311560"] = "Ergebnis exportieren"
|
||||
|
||||
-- The media transcription was canceled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T241403726"] = "Die Transkription des Mediums wurde abgebrochen."
|
||||
|
||||
@ -381,6 +384,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Failed
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1434043348"] = "Fehlgeschlagen"
|
||||
|
||||
-- Choose the format of the result files. Everything except Markdown is converted by Pandoc, which AI Studio offers to install when it is missing.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1457759640"] = "Wählen Sie das Format der Ergebnisdateien. Alle Formate außer Markdown werden von Pandoc konvertiert, dessen Installation AI Studio anbietet, falls es nicht vorhanden ist."
|
||||
|
||||
-- Please select the file which contains your instructions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1462027716"] = "Bitte wählen Sie die Datei aus, die Ihre Anweisungen enthält."
|
||||
|
||||
@ -498,15 +504,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Was not able to read the log of the previous run. Continuing the run would process all documents again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2717277840"] = "Die Log-Datei des vorherigen Laufs konnte nicht gelesen werden. Beim Fortsetzen würden alle Dokumente erneut verarbeitet."
|
||||
|
||||
-- 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"] = "Jede Antwort wird als eigene Ergebnisdatei (.md) gespeichert. Diese Dateien werden nach dem Eingangsdokument benannt, die Antwort zu report.pdf wird also als report_result.md gespeichert."
|
||||
|
||||
-- Before the next file starts, AI Studio waits for a random number of whole seconds from this interval. The minimum is always 6 seconds and the maximum is 300 seconds (5 minutes). Restored files and the end of a run do not add another pause.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2742154256"] = "Bevor die nächste Datei gestartet wird, wartet AI Studio eine zufällige Anzahl ganzer Sekunden aus diesem Intervall. Das Minimum beträgt immer 6 Sekunden, das Maximum 300 Sekunden (5 Minuten). Wiederhergestellte Dateien und das Ende eines Durchlaufs führen nicht zu einer weiteren Pause."
|
||||
|
||||
-- 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"] = "Bitte geben Sie genau ein Satz- oder Sonderzeichen ein. Buchstaben, Zahlen, Leerzeichen, Anführungszeichen und Zeilenumbrüche können nicht als CSV-Trennzeichen verwendet werden."
|
||||
|
||||
-- Was not able to convert the answer into the chosen file format.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2949919602"] = "Die Antwort konnte nicht in das ausgewählte Dateiformat konvertiert werden."
|
||||
|
||||
-- Was not able to write the result file: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Die Ergebnisdatei konnte nicht geschrieben werden: {0}"
|
||||
|
||||
@ -585,9 +591,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- The configured instructions file could not be read.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T4274794480"] = "Die konfigurierte Anweisungsdatei konnte nicht gelesen werden."
|
||||
|
||||
-- Each answer is stored as its own file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result{0}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T428878781"] = "Jede Antwort wird in einer eigenen Datei gespeichert. Diese Dateien werden nach dem Dokument benannt, z. B. wird die Antwort für report.pdf als report_result{0} gespeichert."
|
||||
|
||||
-- Progress
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T439787878"] = "Fortschritt"
|
||||
|
||||
-- File format
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T450269462"] = "Dateiformat"
|
||||
|
||||
-- Enter one punctuation or symbol character.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T469253621"] = "Geben Sie ein Satz- oder Sonderzeichen ein."
|
||||
|
||||
@ -639,15 +651,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARA
|
||||
-- Custom character
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T719177757"] = "Benutzerdefiniertes Zeichen"
|
||||
|
||||
-- One file per document
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T1430232553"] = "Eine Datei pro Dokument"
|
||||
|
||||
-- One CSV results table, where each answer becomes one row
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T1515293131"] = "Eine Ergebnistabelle (.csv), in der jede Antwort zu einer Zeile wird"
|
||||
|
||||
-- Unknown output mode
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T2013180377"] = "Unbekannter Ausgabemodus"
|
||||
|
||||
-- One Markdown file per document
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T2420177746"] = "Eine Ergebnisdatei (.md) pro Dokument"
|
||||
|
||||
-- Use a free prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGPROMPTSOURCEEXTENSIONS::T1144335"] = "Freien Prompt verwenden"
|
||||
|
||||
@ -3177,6 +3189,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "KI"
|
||||
-- Edit Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Nachricht bearbeiten"
|
||||
|
||||
-- Table {0} ({1})
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1340759627"] = "Tabelle {0} ({1})"
|
||||
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Möchten Sie diese Nachricht wirklich löschen?"
|
||||
|
||||
@ -3201,6 +3216,12 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Entfern
|
||||
-- Regenerate Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Nachricht neu erstellen"
|
||||
|
||||
-- Failed to export this message, because the file format '{0}' is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2544592344"] = "Diese Nachricht konnte nicht exportiert werden, da das Dateiformat „{0}“ unbekannt ist."
|
||||
|
||||
-- Export AI response
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2822776450"] = "KI-Antwort exportieren"
|
||||
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Anzahl der Anhänge"
|
||||
|
||||
@ -3222,9 +3243,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Nachric
|
||||
-- No, keep it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "Nein, behalten"
|
||||
|
||||
-- Export Chat to Microsoft Word
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Chat in Microsoft Word exportieren"
|
||||
|
||||
-- The file '{0}' is currently not available and was not sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T1432544573"] = "Die Datei „{0}“ ist derzeit nicht verfügbar und wurde nicht gesendet."
|
||||
|
||||
@ -6864,6 +6882,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T26
|
||||
-- Default column separator
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2745158463"] = "Standard-Spaltentrennzeichen"
|
||||
|
||||
-- Choose the format of new result files. Everything except Markdown is converted by Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2760965660"] = "Wählen Sie das Format neuer Ergebnisdateien. Alles außer Markdown wird von Pandoc konvertiert."
|
||||
|
||||
-- Preselect batch processing options?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2849251744"] = "Optionen für die Stapelverarbeitung vorauswählen?"
|
||||
|
||||
@ -6921,6 +6942,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T39
|
||||
-- Output
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T4000727844"] = "Ausgabe"
|
||||
|
||||
-- Default file format
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T4046754119"] = "Standarddateiformat"
|
||||
|
||||
-- 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"] = "Das konfigurierte Standardregelwerk existiert nicht mehr. Wählen Sie eine anderes Regelwerk aus, bevor Sie einen regelwerkbasierten Stapellauf starten."
|
||||
|
||||
@ -9969,6 +9993,30 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "Das
|
||||
-- policy files
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "Richtliniendateien"
|
||||
|
||||
-- OpenDocument Text (.odt), e.g. LibreOffice
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1612025407"] = "OpenDocument-Text (.odt), z. B. LibreOffice"
|
||||
|
||||
-- LaTeX (.tex)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2233607007"] = "LaTeX (.tex)"
|
||||
|
||||
-- Markdown (.md)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2319970170"] = "Markdown (.md)"
|
||||
|
||||
-- Table (.tsv)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T293798559"] = "Tabelle (.tsv)"
|
||||
|
||||
-- Microsoft Word (.docx)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3054800422"] = "Microsoft Word (.docx)"
|
||||
|
||||
-- Webpage (.html)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3651679344"] = "Webseite (.html)"
|
||||
|
||||
-- Table (.csv)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T530872684"] = "Tabelle (.csv)"
|
||||
|
||||
-- Unknown format
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T677355172"] = "Unbekanntes Format"
|
||||
|
||||
-- The file type of '{0}' could not be determined, so the file was not sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T1459702734"] = "Der Dateityp von „{0}“ konnte nicht bestimmt werden. Daher wurde die Datei nicht gesendet."
|
||||
|
||||
@ -10071,17 +10119,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio konnte die n
|
||||
-- AI Studio couldn't install Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio konnte Pandoc nicht installieren."
|
||||
|
||||
-- Pandoc is required for Microsoft Word export.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc wird für den Export nach Microsoft Word benötigt."
|
||||
-- The export succeeded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1713926719"] = "Der Export war erfolgreich."
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T185447014"] = "Pandoc-Installation"
|
||||
-- The export failed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1895034475"] = "Der Export ist fehlgeschlagen."
|
||||
|
||||
-- Error during Microsoft Word export
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Fehler beim Exportieren nach Microsoft Word"
|
||||
-- Only text messages can be exported.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Nur Textnachrichten können exportiert werden."
|
||||
|
||||
-- Microsoft Word export successful
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Export nach Microsoft Word erfolgreich"
|
||||
-- The export succeeded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "Der Export war erfolgreich."
|
||||
|
||||
-- The export failed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "Der Export ist fehlgeschlagen."
|
||||
|
||||
-- Text
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text"
|
||||
@ -10878,8 +10929,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T63285243
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T185447014"] = "Pandoc-Installation"
|
||||
|
||||
-- Pandoc may be required for importing files.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Zum Importieren von Dateien kann Pandoc erforderlich sein."
|
||||
-- AI Studio needs Pandoc for this, but it is not available.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2610026134"] = "AI Studio benötigt dafür Pandoc, aber es ist nicht verfügbar."
|
||||
|
||||
-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1138181282"] = "Dieses Plugin-Archiv gibt an, von einem Konfigurationsserver verwaltet zu werden. Nur die IT-Abteilung Ihrer Organisation kann solche Plugins bereitstellen."
|
||||
@ -11097,9 +11148,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Von den Dat
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Von der KI bereitgestellte Quellen"
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc-Installation"
|
||||
|
||||
-- The file path is null or empty and the file therefore can not be loaded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T932243993"] = "Der Dateipfad ist leer, daher kann die Datei nicht geladen werden."
|
||||
|
||||
|
||||
@ -318,6 +318,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1809312323"] = "Please se
|
||||
-- The assistant failed. The message is: '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1833836281"] = "The assistant failed. The message is: '{0}'"
|
||||
|
||||
-- Export result
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T1840311560"] = "Export result"
|
||||
|
||||
-- The media transcription was canceled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T241403726"] = "The media transcription was canceled."
|
||||
|
||||
@ -381,6 +384,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Failed
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1434043348"] = "Failed"
|
||||
|
||||
-- Choose the format of the result files. Everything except Markdown is converted by Pandoc, which AI Studio offers to install when it is missing.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1457759640"] = "Choose the format of the result files. Everything except Markdown is converted by Pandoc, which AI Studio offers to install when it is missing."
|
||||
|
||||
-- Please select the file which contains your instructions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1462027716"] = "Please select the file which contains your instructions."
|
||||
|
||||
@ -498,15 +504,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Was not able to read the log of the previous run. Continuing the run would process all documents again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2717277840"] = "Was not able to read the log of the previous run. Continuing the run would process all documents again."
|
||||
|
||||
-- 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."
|
||||
|
||||
-- Before the next file starts, AI Studio waits for a random number of whole seconds from this interval. The minimum is always 6 seconds and the maximum is 300 seconds (5 minutes). Restored files and the end of a run do not add another pause.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2742154256"] = "Before the next file starts, AI Studio waits for a random number of whole seconds from this interval. The minimum is always 6 seconds and the maximum is 300 seconds (5 minutes). Restored files and the end of a run do not add another pause."
|
||||
|
||||
-- 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 convert the answer into the chosen file format.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2949919602"] = "Was not able to convert the answer into the chosen file format."
|
||||
|
||||
-- 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}"
|
||||
|
||||
@ -585,9 +591,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- The configured instructions file could not be read.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T4274794480"] = "The configured instructions file could not be read."
|
||||
|
||||
-- Each answer is stored as its own file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result{0}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T428878781"] = "Each answer is stored as its own file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result{0}."
|
||||
|
||||
-- Progress
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T439787878"] = "Progress"
|
||||
|
||||
-- File format
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T450269462"] = "File format"
|
||||
|
||||
-- Enter one punctuation or symbol character.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T469253621"] = "Enter one punctuation or symbol character."
|
||||
|
||||
@ -639,15 +651,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARA
|
||||
-- Custom character
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T719177757"] = "Custom character"
|
||||
|
||||
-- One file per document
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T1430232553"] = "One file per document"
|
||||
|
||||
-- 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"
|
||||
|
||||
-- Unknown output mode
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T2013180377"] = "Unknown output mode"
|
||||
|
||||
-- One Markdown file per document
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T2420177746"] = "One Markdown file per document"
|
||||
|
||||
-- Use a free prompt
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGPROMPTSOURCEEXTENSIONS::T1144335"] = "Use a free prompt"
|
||||
|
||||
@ -3177,6 +3189,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "AI"
|
||||
-- Edit Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Edit Message"
|
||||
|
||||
-- Table {0} ({1})
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1340759627"] = "Table {0} ({1})"
|
||||
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?"
|
||||
|
||||
@ -3201,6 +3216,12 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes
|
||||
-- Regenerate Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message"
|
||||
|
||||
-- Failed to export this message, because the file format '{0}' is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2544592344"] = "Failed to export this message, because the file format '{0}' is unknown."
|
||||
|
||||
-- Export AI response
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2822776450"] = "Export AI response"
|
||||
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments"
|
||||
|
||||
@ -3222,9 +3243,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove
|
||||
-- No, keep it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it"
|
||||
|
||||
-- Export Chat to Microsoft Word
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word"
|
||||
|
||||
-- The file '{0}' is currently not available and was not sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T1432544573"] = "The file '{0}' is currently not available and was not sent."
|
||||
|
||||
@ -6864,6 +6882,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T26
|
||||
-- Default column separator
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2745158463"] = "Default column separator"
|
||||
|
||||
-- Choose the format of new result files. Everything except Markdown is converted by Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2760965660"] = "Choose the format of new result files. Everything except Markdown is converted by Pandoc."
|
||||
|
||||
-- Preselect batch processing options?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2849251744"] = "Preselect batch processing options?"
|
||||
|
||||
@ -6921,6 +6942,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T39
|
||||
-- Output
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T4000727844"] = "Output"
|
||||
|
||||
-- Default file format
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T4046754119"] = "Default file format"
|
||||
|
||||
-- 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."
|
||||
|
||||
@ -9969,6 +9993,30 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The
|
||||
-- policy files
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files"
|
||||
|
||||
-- OpenDocument Text (.odt), e.g. LibreOffice
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1612025407"] = "OpenDocument Text (.odt), e.g. LibreOffice"
|
||||
|
||||
-- LaTeX (.tex)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2233607007"] = "LaTeX (.tex)"
|
||||
|
||||
-- Markdown (.md)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2319970170"] = "Markdown (.md)"
|
||||
|
||||
-- Table (.tsv)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T293798559"] = "Table (.tsv)"
|
||||
|
||||
-- Microsoft Word (.docx)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3054800422"] = "Microsoft Word (.docx)"
|
||||
|
||||
-- Webpage (.html)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3651679344"] = "Webpage (.html)"
|
||||
|
||||
-- Table (.csv)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T530872684"] = "Table (.csv)"
|
||||
|
||||
-- Unknown format
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T677355172"] = "Unknown format"
|
||||
|
||||
-- The file type of '{0}' could not be determined, so the file was not sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T1459702734"] = "The file type of '{0}' could not be determined, so the file was not sent."
|
||||
|
||||
@ -10071,17 +10119,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't fin
|
||||
-- AI Studio couldn't install Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc."
|
||||
|
||||
-- Pandoc is required for Microsoft Word export.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
|
||||
-- The export succeeded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1713926719"] = "The export succeeded."
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T185447014"] = "Pandoc Installation"
|
||||
-- The export failed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1895034475"] = "The export failed."
|
||||
|
||||
-- Error during Microsoft Word export
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Microsoft Word export"
|
||||
-- Only text messages can be exported.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Only text messages can be exported."
|
||||
|
||||
-- Microsoft Word export successful
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful"
|
||||
-- The export succeeded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "The export succeeded."
|
||||
|
||||
-- The export failed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "The export failed."
|
||||
|
||||
-- Text
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text"
|
||||
@ -10878,8 +10929,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T63285243
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T185447014"] = "Pandoc Installation"
|
||||
|
||||
-- Pandoc may be required for importing files.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
||||
-- AI Studio needs Pandoc for this, but it is not available.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2610026134"] = "AI Studio needs Pandoc for this, but it is not available."
|
||||
|
||||
-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1138181282"] = "This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins."
|
||||
@ -11097,9 +11148,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources pro
|
||||
-- Sources provided by the AI
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI"
|
||||
|
||||
-- Pandoc Installation
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc Installation"
|
||||
|
||||
-- The file path is null or empty and the file therefore can not be loaded.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T932243993"] = "The file path is null or empty and the file therefore can not be loaded."
|
||||
|
||||
|
||||
@ -42,7 +42,17 @@ public sealed class DataBatchProcessing(Expression<Func<Data, DataBatchProcessin
|
||||
|
||||
public string PreselectedPolicyId { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedPolicyId, string.Empty);
|
||||
|
||||
public BatchProcessingOutputMode OutputMode { get; set; } = ManagedConfiguration.Register(configSelection, value => value.OutputMode, BatchProcessingOutputMode.MARKDOWN_FILES);
|
||||
public BatchProcessingOutputMode OutputMode { get; set; } = ManagedConfiguration.Register(configSelection, value => value.OutputMode, BatchProcessingOutputMode.INDIVIDUAL_FILES);
|
||||
|
||||
/// <summary>
|
||||
/// The file format of the individual result files, one per processed document.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only formats which hold an entire answer, see FileExportFormatExtensions.ANSWER_FORMATS.
|
||||
/// The tabular formats belong to the output mode TABLE_ONLY, which writes one table for the
|
||||
/// whole run instead of one file per document.
|
||||
/// </remarks>
|
||||
public FileExportFormat ResultFileFormat { get; set; } = ManagedConfiguration.Register(configSelection, value => value.ResultFileFormat, FileExportFormat.MARKDOWN);
|
||||
|
||||
public string CsvFileName { get; set; } = ManagedConfiguration.Register(configSelection, value => value.CsvFileName, string.Empty);
|
||||
|
||||
|
||||
64
app/MindWork AI Studio/Tools/CsvWriter.cs
Normal file
64
app/MindWork AI Studio/Tools/CsvWriter.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Writes rows of character-separated values. Fields are quoted according to RFC 4180 using the
|
||||
/// separator of the respective file.
|
||||
/// </summary>
|
||||
public static class CsvWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// The separator a spreadsheet expects from a CSV file written for the given language.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Wherever a comma separates the decimals of a number, it cannot separate the columns of a
|
||||
/// file as well: German Excel therefore expects a semicolon and puts a comma-separated file
|
||||
/// into a single column. This is the same rule Excel itself follows when it writes a CSV, so
|
||||
/// we ask the culture rather than keeping a list of languages of our own.
|
||||
/// </remarks>
|
||||
/// <param name="ietfTag">The IETF tag of the language, for example "de-DE".</param>
|
||||
/// <returns>The separator to write with.</returns>
|
||||
public static char SeparatorFor(string ietfTag)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ietfTag))
|
||||
return ',';
|
||||
|
||||
try
|
||||
{
|
||||
var culture = CultureInfo.GetCultureInfo(ietfTag);
|
||||
return culture.NumberFormat.NumberDecimalSeparator is "," ? ';' : ',';
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
return ',';
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joins the given fields into one row.
|
||||
/// </summary>
|
||||
/// <param name="separator">The separator between two fields.</param>
|
||||
/// <param name="fields">The fields of the row.</param>
|
||||
/// <returns>The row, without a line ending.</returns>
|
||||
public static string ToRow(char separator, params string[] fields) => string.Join(separator, fields.Select(field => ToField(field, separator)));
|
||||
|
||||
/// <summary>
|
||||
/// Quotes one field according to RFC 4180.
|
||||
/// </summary>
|
||||
private static string ToField(string text, char separator)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return string.Empty;
|
||||
|
||||
// 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("\"", "\"\"")}"
|
||||
""";
|
||||
}
|
||||
}
|
||||
18
app/MindWork AI Studio/Tools/FileExportFormat.cs
Normal file
18
app/MindWork AI Studio/Tools/FileExportFormat.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// The file formats a chat message can be exported to.
|
||||
/// </summary>
|
||||
public enum FileExportFormat
|
||||
{
|
||||
NONE,
|
||||
UNKNOWN,
|
||||
|
||||
MICROSOFT_WORD,
|
||||
OPEN_DOCUMENT_TEXT,
|
||||
LATEX,
|
||||
MARKDOWN,
|
||||
HTML,
|
||||
CSV,
|
||||
TSV,
|
||||
}
|
||||
228
app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs
Normal file
228
app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs
Normal file
@ -0,0 +1,228 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Everything AI Studio needs to know about an export format: how it is named, how it is shown,
|
||||
/// which file it produces, and who writes that file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the single place where an export format is described. Adding another one means adding
|
||||
/// an enum member and one line per method here; neither the exporters nor the export menu need
|
||||
/// to know about it.
|
||||
/// </remarks>
|
||||
public static class FileExportFormatExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExportFormatExtensions).Namespace, nameof(FileExportFormatExtensions));
|
||||
|
||||
private static readonly Encoding WITH_BYTE_ORDER_MARK = new UTF8Encoding(true);
|
||||
private static readonly Encoding WITHOUT_BYTE_ORDER_MARK = new UTF8Encoding(false);
|
||||
|
||||
/// <summary>
|
||||
/// The formats which lay the text out as a document you would hand to somebody, in the order
|
||||
/// the export menu shows them.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<FileExportFormat> DOCUMENT_FORMATS =
|
||||
[
|
||||
FileExportFormat.MICROSOFT_WORD,
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT,
|
||||
FileExportFormat.LATEX,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The formats which keep the text as text, in the order the export menu shows them.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<FileExportFormat> TEXT_FORMATS =
|
||||
[
|
||||
FileExportFormat.MARKDOWN,
|
||||
FileExportFormat.HTML,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Every format an entire answer can be written as.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The tabular formats are missing on purpose: they hold one table out of an answer, never the
|
||||
/// answer itself. Whoever offers a table adds them.
|
||||
/// </remarks>
|
||||
public static readonly IReadOnlyList<FileExportFormat> ANSWER_FORMATS = [..DOCUMENT_FORMATS, ..TEXT_FORMATS];
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name of the format as shown to the user.
|
||||
/// </summary>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <returns>The name of the format.</returns>
|
||||
public static string ToName(this FileExportFormat format) => format switch
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"),
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"),
|
||||
FileExportFormat.LATEX => TB("LaTeX (.tex)"),
|
||||
FileExportFormat.MARKDOWN => TB("Markdown (.md)"),
|
||||
FileExportFormat.HTML => TB("Webpage (.html)"),
|
||||
FileExportFormat.CSV => TB("Table (.csv)"),
|
||||
FileExportFormat.TSV => TB("Table (.tsv)"),
|
||||
|
||||
_ => TB("Unknown format"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the icon of the format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <returns>The icon of the format.</returns>
|
||||
public static string ToIcon(this FileExportFormat format) => format switch
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => Icons.Custom.FileFormats.FileWord,
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => Icons.Custom.FileFormats.FileDocument,
|
||||
FileExportFormat.LATEX => Icons.Material.Filled.Functions,
|
||||
FileExportFormat.MARKDOWN => Icons.Material.Filled.TextFields,
|
||||
FileExportFormat.HTML => Icons.Material.Filled.Html,
|
||||
FileExportFormat.CSV or FileExportFormat.TSV => Icons.Material.Filled.TableChart,
|
||||
|
||||
_ => Icons.Material.Filled.Help,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file extension of the format, including the leading dot.
|
||||
/// </summary>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <returns>The file extension, or an empty string when the format writes no file.</returns>
|
||||
public static string ToFileExtension(this FileExportFormat format) => format switch
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => ".docx",
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt",
|
||||
FileExportFormat.LATEX => ".tex",
|
||||
FileExportFormat.MARKDOWN => ".md",
|
||||
FileExportFormat.HTML => ".html",
|
||||
FileExportFormat.CSV => ".csv",
|
||||
FileExportFormat.TSV => ".tsv",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file name the save dialog starts with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Without a name, the dialog opens with an empty field and the user easily ends up with a
|
||||
/// file which carries no extension at all. The fallback name is deliberately not translated:
|
||||
/// a file name should survive being copied between systems and locales.
|
||||
/// </remarks>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <param name="name">What the file is about, for example the heading above a table. Anything
|
||||
/// a file name cannot hold is removed. Null or blank falls back to a generic name.</param>
|
||||
/// <returns>The suggested file name, including its extension.</returns>
|
||||
public static string ToSuggestedFileName(this FileExportFormat format, string? name = null)
|
||||
{
|
||||
var fileName = ToFileNameFragment(name);
|
||||
return $"{(fileName.Length is 0 ? "export" : fileName)}{format.ToFileExtension()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns arbitrary text into something a file system accepts as a name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We do not ask the runtime which characters are invalid: macOS forbids almost nothing, so a
|
||||
/// name taken from there would break as soon as the file reaches a Windows share. The fixed
|
||||
/// set below is what no common file system accepts, plus the length limit which keeps the name
|
||||
/// readable in a dialog.
|
||||
/// </remarks>
|
||||
private static string ToFileNameFragment(string? name)
|
||||
{
|
||||
const int MAX_LENGTH = 60;
|
||||
const string FORBIDDEN_CHARACTERS = @"\/:*?""<>|";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return string.Empty;
|
||||
|
||||
var fragment = new StringBuilder(name.Length);
|
||||
var lastWasSpace = false;
|
||||
foreach (var character in name)
|
||||
{
|
||||
var isSpace = char.IsWhiteSpace(character) || char.IsControl(character) || FORBIDDEN_CHARACTERS.Contains(character);
|
||||
if (isSpace)
|
||||
{
|
||||
// Collapse whatever we dropped into a single space, so "Table 1: People"
|
||||
// becomes "Table 1 People" instead of "Table 1 People":
|
||||
if (fragment.Length > 0)
|
||||
lastWasSpace = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastWasSpace)
|
||||
{
|
||||
fragment.Append(' ');
|
||||
lastWasSpace = false;
|
||||
}
|
||||
|
||||
fragment.Append(character);
|
||||
if (fragment.Length >= MAX_LENGTH)
|
||||
break;
|
||||
}
|
||||
|
||||
// A trailing dot makes a file invisible on Unix and is dropped by Windows:
|
||||
return fragment.ToString().TrimEnd('.');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the filter which the save dialog offers for the format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <returns>The filter, or null when the format cannot be written.</returns>
|
||||
public static FileTypeFilter? ToFileTypeFilter(this FileExportFormat format) => format switch
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => FileTypes.MS_WORD,
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => FileTypes.ODT,
|
||||
FileExportFormat.LATEX => FileTypes.TEX,
|
||||
FileExportFormat.MARKDOWN => FileTypes.MARKDOWN,
|
||||
FileExportFormat.HTML => FileTypes.HTML,
|
||||
FileExportFormat.CSV => FileTypes.CSV,
|
||||
FileExportFormat.TSV => FileTypes.TSV,
|
||||
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the encoding the file gets written with.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Everything is UTF-8, the question is only whether the file starts with a byte order mark.
|
||||
/// Tabular files get one, because Excel otherwise reads them in the local ANSI code page and
|
||||
/// turns every umlaut into garbage. Text files get none: editors, compilers, and LaTeX have
|
||||
/// no use for it and some of them stumble over it.
|
||||
/// </remarks>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <returns>The encoding to write the file with.</returns>
|
||||
public static Encoding ToFileEncoding(this FileExportFormat format) => format switch
|
||||
{
|
||||
FileExportFormat.CSV or FileExportFormat.TSV => WITH_BYTE_ORDER_MARK,
|
||||
|
||||
_ => WITHOUT_BYTE_ORDER_MARK,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name Pandoc knows the format by.
|
||||
/// </summary>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <returns>The Pandoc output format, or an empty string when AI Studio writes the file itself.</returns>
|
||||
public static string ToPandocOutputFormat(this FileExportFormat format) => format switch
|
||||
{
|
||||
FileExportFormat.MICROSOFT_WORD => "docx",
|
||||
FileExportFormat.OPEN_DOCUMENT_TEXT => "odt",
|
||||
FileExportFormat.LATEX => "latex",
|
||||
FileExportFormat.HTML => "html",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether writing the format needs Pandoc.
|
||||
/// </summary>
|
||||
/// <param name="format">The format.</param>
|
||||
/// <returns>True, when Pandoc converts the message; false, when AI Studio writes the file itself.</returns>
|
||||
public static bool UsesPandoc(this FileExportFormat format) => !string.IsNullOrWhiteSpace(format.ToPandocOutputFormat());
|
||||
}
|
||||
12
app/MindWork AI Studio/Tools/MessageTable.cs
Normal file
12
app/MindWork AI Studio/Tools/MessageTable.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// A table found in a message, ready to be written to a file.
|
||||
/// </summary>
|
||||
/// <param name="Ordinal">Which table of the message this is, counting from one. The same table
|
||||
/// appears once per format we offer for it, so this is what tells two tables apart even when they
|
||||
/// carry the same heading.</param>
|
||||
/// <param name="Caption">What the table is about, taken from its first column heading.</param>
|
||||
/// <param name="Format">The format this content is written as.</param>
|
||||
/// <param name="Content">The finished file content.</param>
|
||||
public sealed record MessageTable(int Ordinal, string Caption, FileExportFormat Format, string Content);
|
||||
@ -1,77 +1,54 @@
|
||||
using System.Diagnostics;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class PandocExport
|
||||
{
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocExport));
|
||||
|
||||
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport));
|
||||
|
||||
public static async Task<bool> ToMicrosoftWord(RustService rustService, IDialogService dialogService, string dialogTitle, IContent markdownContent)
|
||||
{
|
||||
var response = await rustService.SaveFile(dialogTitle, [FileTypes.MS_WORD]);
|
||||
if (response.UserCancelled)
|
||||
{
|
||||
LOGGER.LogInformation("User cancelled the save dialog.");
|
||||
return false;
|
||||
}
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocExport));
|
||||
|
||||
LOGGER.LogInformation($"The user chose the path '{response.SaveFilePath}' for the Microsoft Word export.");
|
||||
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport));
|
||||
|
||||
/// <summary>
|
||||
/// Converts the given Markdown text into a document at the given path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This says nothing to the user: it reports what happened and lets the caller decide. A batch
|
||||
/// run over hundreds of documents would otherwise bury the user under notifications. Pandoc
|
||||
/// must be available, which PandocAvailabilityService.EnsureAvailabilityAsync takes care of.
|
||||
/// </remarks>
|
||||
/// <param name="rustService">The Rust service, used to build the Pandoc call.</param>
|
||||
/// <param name="markdownText">The Markdown text to convert.</param>
|
||||
/// <param name="targetFilePath">Where to write the document.</param>
|
||||
/// <param name="format">The format to write. Must be a format which uses Pandoc.</param>
|
||||
/// <param name="token">The token to cancel the conversion.</param>
|
||||
/// <returns>True, when the document was written.</returns>
|
||||
public static async Task<bool> ConvertAsync(RustService rustService, string markdownText, string targetFilePath, FileExportFormat format, CancellationToken token = default)
|
||||
{
|
||||
if (!format.UsesPandoc())
|
||||
throw new ArgumentOutOfRangeException(nameof(format), format, "Pandoc cannot write this format.");
|
||||
|
||||
var tempMarkdownFilePath = string.Empty;
|
||||
try
|
||||
{
|
||||
var tempMarkdownFile = Guid.NewGuid().ToString();
|
||||
tempMarkdownFilePath = Path.Combine(Path.GetTempPath(), tempMarkdownFile);
|
||||
|
||||
// Extract text content from chat:
|
||||
var markdownText = markdownContent switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
ContentImage _ => "Image export to Microsoft Word not yet possible",
|
||||
|
||||
_ => "Unknown content type. Cannot export to Word."
|
||||
};
|
||||
// Write text content to a temporary file. Pandoc expects UTF-8 without a byte order
|
||||
// mark; a mark would end up as a stray character at the start of the document:
|
||||
await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText, new UTF8Encoding(false), token);
|
||||
|
||||
// Write text content to a temporary file:
|
||||
await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText);
|
||||
|
||||
// Ensure that Pandoc is installed and ready:
|
||||
var pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: false);
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<PandocDialog>
|
||||
{
|
||||
{ x => x.ShowInitialResultInSnackbar, false },
|
||||
};
|
||||
|
||||
var dialogReference = await dialogService.ShowAsync<PandocDialog>(TB("Pandoc Installation"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
await dialogReference.Result;
|
||||
|
||||
pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: true);
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
LOGGER.LogError("Pandoc is not available after installation attempt.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Pandoc is required for Microsoft Word export.")));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Call Pandoc to create the Word file:
|
||||
// Call Pandoc to create the document:
|
||||
var pandoc = await PandocProcessBuilder
|
||||
.Create()
|
||||
.UseStandaloneMode()
|
||||
.WithInputFormat("gfm+emoji+tex_math_dollars")
|
||||
.WithOutputFormat("docx")
|
||||
.WithOutputFile(response.SaveFilePath)
|
||||
.WithOutputFormat(format.ToPandocOutputFormat())
|
||||
.WithOutputFile(targetFilePath)
|
||||
.WithInputFile(tempMarkdownFilePath)
|
||||
.BuildAsync(rustService);
|
||||
|
||||
@ -83,30 +60,26 @@ public static class PandocExport
|
||||
}
|
||||
|
||||
// Read output streams asynchronously while the process runs (prevents deadlock):
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync(token);
|
||||
var errorTask = process.StandardError.ReadToEndAsync(token);
|
||||
|
||||
// Wait for the process to exit AND for streams to be fully read:
|
||||
await process.WaitForExitAsync();
|
||||
await process.WaitForExitAsync(token);
|
||||
await outputTask;
|
||||
var error = await errorTask;
|
||||
|
||||
if (process.ExitCode is not 0)
|
||||
{
|
||||
LOGGER.LogError("Pandoc failed with exit code {ProcessExitCode}: '{ErrorText}'", process.ExitCode, error);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during Microsoft Word export")));
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.LogInformation("Pandoc conversion successful.");
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Microsoft Word export successful")));
|
||||
|
||||
LOGGER.LogInformation("Pandoc conversion to {ExportFormat} successful.", format);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOGGER.LogError(ex, "Error during Word export.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during Microsoft Word export")));
|
||||
LOGGER.LogError(ex, "Error during {ExportFormat} conversion.", format);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
@ -120,9 +93,59 @@ public static class PandocExport
|
||||
}
|
||||
catch
|
||||
{
|
||||
LOGGER.LogWarning($"Was not able to delete temporary file: '{tempMarkdownFilePath}'");
|
||||
LOGGER.LogWarning("Was not able to delete the temporary file '{TempFilePath}'.", tempMarkdownFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the given content to a document using Pandoc and lets the user save it.
|
||||
/// </summary>
|
||||
/// <param name="rustService">The Rust service, used for the save dialog and for Pandoc.</param>
|
||||
/// <param name="pandocAvailability">Makes sure Pandoc is there and offers its installation.</param>
|
||||
/// <param name="dialogTitle">The title of the save dialog. The caller knows what the user is
|
||||
/// looking at, a chat message or the result of an assistant, so the caller names it.</param>
|
||||
/// <param name="format">The format to write. Must be a format which uses Pandoc.</param>
|
||||
/// <param name="markdownContent">The content to export.</param>
|
||||
/// <returns>True, when the document was written.</returns>
|
||||
public static async Task<bool> ToDocument(RustService rustService, PandocAvailabilityService pandocAvailability, string dialogTitle, FileExportFormat format, IContent markdownContent)
|
||||
{
|
||||
if (!format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter)
|
||||
throw new ArgumentOutOfRangeException(nameof(format), format, "Pandoc cannot write this format.");
|
||||
|
||||
//
|
||||
// We read the text before we ask for a path: when there is nothing to convert, the user
|
||||
// should learn that right away instead of picking a file first and getting an error afterwards.
|
||||
//
|
||||
if (!markdownContent.TryGetMarkdownText(out var markdownText))
|
||||
{
|
||||
LOGGER.LogWarning("Cannot export the content as {ExportFormat}, because it carries no text.", format);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Only text messages can be exported.")));
|
||||
return false;
|
||||
}
|
||||
|
||||
var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName());
|
||||
if (response.UserCancelled)
|
||||
{
|
||||
LOGGER.LogInformation("User cancelled the save dialog.");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.LogInformation("The user chose the path '{SaveFilePath}' for the {ExportFormat} export.", response.SaveFilePath, format);
|
||||
|
||||
// The service reports a missing Pandoc to the user itself, so we only act on the outcome:
|
||||
var pandocState = await pandocAvailability.EnsureAvailabilityAsync(showSuccessMessage: false, showDialog: true);
|
||||
if (!pandocState.IsAvailable)
|
||||
return false;
|
||||
|
||||
if (!await ConvertAsync(rustService, markdownText, response.SaveFilePath, format))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("The export failed.")));
|
||||
return false;
|
||||
}
|
||||
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("The export succeeded.")));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
209
app/MindWork AI Studio/Tools/PlainFileExport.cs
Normal file
209
app/MindWork AI Studio/Tools/PlainFileExport.cs
Normal file
@ -0,0 +1,209 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class PlainFileExport
|
||||
{
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PlainFileExport));
|
||||
|
||||
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PlainFileExport).Namespace, nameof(PlainFileExport));
|
||||
|
||||
/// <summary>
|
||||
/// Reads every table a message holds, in the order they appear in it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Two kinds of tables end up in an answer. Almost always it is a Markdown table written with
|
||||
/// pipes, which is what a model produces on its own; we turn its cells into a file. Rarely a
|
||||
/// model answers with a fenced code block marked as csv or tsv, which already is the finished
|
||||
/// file: we hand that through untouched rather than taking it apart and reassembling it.
|
||||
/// </remarks>
|
||||
/// <param name="markdown">The Markdown text of the message.</param>
|
||||
/// <param name="separator">The separator to write a Markdown table with, see CsvWriter.SeparatorFor.</param>
|
||||
/// <returns>The tables, or an empty list when the message holds none.</returns>
|
||||
public static IReadOnlyList<MessageTable> ExtractTables(string markdown, char separator)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(markdown))
|
||||
return [];
|
||||
|
||||
//
|
||||
// We let Markdig do the reading. It is already part of the app, the pipeline we reuse has
|
||||
// table support switched on, and it knows every corner of the syntax that a regular
|
||||
// expression of ours would have to learn one bug at a time.
|
||||
//
|
||||
var document = Markdig.Markdown.Parse(markdown, Markdown.SAFE_MARKDOWN_PIPELINE);
|
||||
|
||||
//
|
||||
// What a table is about stands above it, not in it: models introduce their tables with a
|
||||
// heading. We remember every heading with its line so that each table can take the last
|
||||
// one before it, and fall back to its own first column heading when there is none.
|
||||
//
|
||||
var headings = document.Descendants<HeadingBlock>()
|
||||
.Select(heading => (heading.Line, Text: ToPlainText(heading)))
|
||||
.Where(heading => !string.IsNullOrWhiteSpace(heading.Text))
|
||||
.OrderBy(heading => heading.Line)
|
||||
.ToList();
|
||||
|
||||
var tables = document.Descendants<Table>()
|
||||
.Select(table => (table.Line, Content: ToContent(table, separator)));
|
||||
|
||||
var codeBlocks = document.Descendants<FencedCodeBlock>()
|
||||
.Select(block => (block.Line, Content: ToContent(block)));
|
||||
|
||||
return tables.Concat(codeBlocks)
|
||||
.Where(entry => entry.Content is not null)
|
||||
.OrderBy(entry => entry.Line)
|
||||
.Select((entry, index) => new MessageTable(
|
||||
index + 1,
|
||||
Caption: HeadingAbove(entry.Line) is { Length: > 0 } heading ? heading : entry.Content!.Value.Fallback,
|
||||
entry.Content!.Value.Format,
|
||||
entry.Content.Value.Text))
|
||||
.ToList();
|
||||
|
||||
string HeadingAbove(int line) => headings.LastOrDefault(heading => heading.Line < line).Text ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns a Markdown table into a file.
|
||||
/// </summary>
|
||||
private static (string Fallback, FileExportFormat Format, string Text)? ToContent(Table table, char separator)
|
||||
{
|
||||
var rows = table.OfType<TableRow>()
|
||||
.Select(row => row.OfType<TableCell>().Select(ToPlainText).ToArray())
|
||||
.Where(fields => fields.Length > 0)
|
||||
.ToList();
|
||||
|
||||
if (rows.Count is 0)
|
||||
return null;
|
||||
|
||||
var text = new StringBuilder();
|
||||
foreach (var fields in rows)
|
||||
text.AppendLine(CsvWriter.ToRow(separator, fields));
|
||||
|
||||
return (rows[0].FirstOrDefault() ?? string.Empty, FileExportFormat.CSV, text.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns a fenced code block into a file, when the model marked it as tabular data.
|
||||
/// </summary>
|
||||
private static (string Fallback, FileExportFormat Format, string Text)? ToContent(FencedCodeBlock block)
|
||||
{
|
||||
var format = block.Info?.Trim() switch
|
||||
{
|
||||
"csv" => FileExportFormat.CSV,
|
||||
"tsv" => FileExportFormat.TSV,
|
||||
|
||||
_ => FileExportFormat.NONE,
|
||||
};
|
||||
|
||||
if (format is FileExportFormat.NONE)
|
||||
return null;
|
||||
|
||||
var content = block.Lines.ToString();
|
||||
var blockSeparator = format is FileExportFormat.TSV ? '\t' : ',';
|
||||
var firstLine = content.AsSpan();
|
||||
var lineEnd = firstLine.IndexOf('\n');
|
||||
if (lineEnd >= 0)
|
||||
firstLine = firstLine[..lineEnd];
|
||||
|
||||
var separatorPosition = firstLine.IndexOf(blockSeparator);
|
||||
var fallback = (separatorPosition >= 0 ? firstLine[..separatorPosition] : firstLine).Trim().Trim('"').ToString();
|
||||
|
||||
return (fallback, format, content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the text of a table cell or a heading, without the Markdown which decorates it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A spreadsheet has no use for the asterisks around a bold number: they would keep it from
|
||||
/// being recognized as a number. So we keep what a reader would read and drop the rest.
|
||||
/// </remarks>
|
||||
private static string ToPlainText(MarkdownObject container)
|
||||
{
|
||||
//
|
||||
// A leaf block, a heading for example, keeps its text in an inline container of its own.
|
||||
// Asking the block itself for its descendants walks its child blocks, and a leaf block has
|
||||
// none, so we would get nothing back. A table cell is a container block and needs the
|
||||
// opposite: its text sits in the paragraphs below it.
|
||||
//
|
||||
var inlines = container is LeafBlock leafBlock
|
||||
? leafBlock.Inline?.Descendants<LeafInline>() ?? []
|
||||
: container.Descendants<LeafInline>();
|
||||
|
||||
var text = new StringBuilder();
|
||||
foreach (var inline in inlines)
|
||||
switch (inline)
|
||||
{
|
||||
case CodeInline code:
|
||||
text.Append(code.Content);
|
||||
break;
|
||||
|
||||
case LiteralInline literal:
|
||||
text.Append(literal.Content.AsSpan());
|
||||
break;
|
||||
|
||||
case HtmlEntityInline entity:
|
||||
text.Append(entity.Transcoded.AsSpan());
|
||||
break;
|
||||
|
||||
case AutolinkInline autolink:
|
||||
text.Append(autolink.Url);
|
||||
break;
|
||||
|
||||
// A cell holds one line in a file, so a line break inside it becomes a space:
|
||||
case LineBreakInline:
|
||||
text.Append(' ');
|
||||
break;
|
||||
}
|
||||
|
||||
return text.ToString().Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the given text to a plain text file and lets the user save it.
|
||||
/// </summary>
|
||||
/// <param name="rustService">The Rust service, used for the save dialog.</param>
|
||||
/// <param name="dialogTitle">The title of the save dialog. The caller knows what the user is
|
||||
/// looking at, a chat message or the result of an assistant, so the caller names it.</param>
|
||||
/// <param name="format">The format to write. Must be a format which does not use Pandoc.</param>
|
||||
/// <param name="fileContent">What to write. The caller decides whether that is the entire
|
||||
/// message or one table out of it.</param>
|
||||
/// <param name="fileName">What the file is about, used to suggest a name in the save dialog.
|
||||
/// Null falls back to a generic name.</param>
|
||||
/// <returns>True, when the file was written.</returns>
|
||||
public static async Task<bool> ToFile(RustService rustService, string dialogTitle, FileExportFormat format, string fileContent, string? fileName = null)
|
||||
{
|
||||
if (format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter)
|
||||
throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format itself.");
|
||||
|
||||
var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName(fileName));
|
||||
if (response.UserCancelled)
|
||||
{
|
||||
LOGGER.LogInformation("User cancelled the save dialog.");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.LogInformation("The user chose the path '{SaveFilePath}' for the {ExportFormat} export.", response.SaveFilePath, format);
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(response.SaveFilePath, fileContent, format.ToFileEncoding());
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("The export succeeded.")));
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOGGER.LogError(ex, "Error during {ExportFormat} export.", format);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("The export failed.")));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -351,6 +351,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.PromptFilePath, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.PreselectedPolicyId, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.OutputMode, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.ResultFileFormat, 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);
|
||||
|
||||
@ -37,6 +37,7 @@ public static class FileTypes
|
||||
/// Gets the standalone HTML filter used for visual briefing import and export.
|
||||
/// </summary>
|
||||
public static readonly FileTypeFilter VISUAL_BRIEFING_HTML = FileTypeFilter.Leaf(TB("Visual briefing"), "html");
|
||||
public static readonly FileTypeFilter HTML = FileTypeFilter.Leaf("HTML", "html");
|
||||
public static readonly FileTypeFilter APP = FileTypeFilter.Leaf("Swift/Kotlin", "swift", "kt");
|
||||
public static readonly FileTypeFilter SHELL = FileTypeFilter.Leaf("Shell", "sh", "bash", "zsh");
|
||||
public static readonly FileTypeFilter LOG = FileTypeFilter.Leaf("Log", "log");
|
||||
@ -53,8 +54,11 @@ public static class FileTypes
|
||||
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 TABULAR = FileTypeFilter.Leaf(TB("Tabular text"), "csv", "tsv");
|
||||
public static readonly FileTypeFilter CSV = FileTypeFilter.Leaf("CSV", "csv");
|
||||
public static readonly FileTypeFilter TSV = FileTypeFilter.Leaf("TSV", "tsv");
|
||||
public static readonly FileTypeFilter MS_WORD = FileTypeFilter.Leaf("Microsoft Word", "docx");
|
||||
public static readonly FileTypeFilter WORD = FileTypeFilter.Composite("Word", ["odt"], MS_WORD);
|
||||
public static readonly FileTypeFilter ODT = FileTypeFilter.Leaf("OpenDocument Text", "odt");
|
||||
public static readonly FileTypeFilter WORD = FileTypeFilter.Parent("Word", ODT, MS_WORD);
|
||||
public static readonly FileTypeFilter EXCEL = FileTypeFilter.Leaf("Excel", "xls", "xlsx");
|
||||
|
||||
// The legacy binary ".ppt" is missing on purpose: AI Studio has no reader for it, so offering
|
||||
@ -63,6 +67,10 @@ public static class FileTypes
|
||||
public static readonly FileTypeFilter MAIL = FileTypeFilter.Leaf(TB("Mail"), "eml", "msg", "mbox");
|
||||
public static readonly FileTypeFilter LATEX = FileTypeFilter.Leaf("LaTeX", "tex", "bib", "sty", "cls", "log");
|
||||
|
||||
// Only the LaTeX document itself, without the auxiliary files of the LaTeX family: this is
|
||||
// what we write when exporting, whereas the family above is what we accept when reading.
|
||||
public static readonly FileTypeFilter TEX = FileTypeFilter.Leaf("LaTeX", "tex");
|
||||
|
||||
public static readonly FileTypeFilter OFFICE_FILES = FileTypeFilter.Parent(TB("Office Files"),
|
||||
WORD, EXCEL, POWER_POINT, PDF);
|
||||
public static readonly FileTypeFilter DOCUMENT = FileTypeFilter.Parent(TB("Document"),
|
||||
|
||||
@ -27,8 +27,11 @@ public sealed class PandocAvailabilityService(RustService rustService, IDialogSe
|
||||
/// </summary>
|
||||
/// <param name="showSuccessMessage">Whether to show a success message if Pandoc is available.</param>
|
||||
/// <param name="showDialog">Whether to show the installation dialog if Pandoc is not available.</param>
|
||||
/// <param name="showErrorMessage">Whether to report a still missing Pandoc to the user. Turn
|
||||
/// this off when you can say it better yourself, for example by naming the file which cannot
|
||||
/// be read; otherwise the user reads two messages about the same thing.</param>
|
||||
/// <returns>The Pandoc installation state.</returns>
|
||||
public async Task<PandocInstallation> EnsureAvailabilityAsync(bool showSuccessMessage = false, bool showDialog = true)
|
||||
public async Task<PandocInstallation> EnsureAvailabilityAsync(bool showSuccessMessage = false, bool showDialog = true, bool showErrorMessage = true)
|
||||
{
|
||||
// Check if Pandoc is available:
|
||||
var pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showMessages: false, showSuccessMessage: showSuccessMessage);
|
||||
@ -54,7 +57,8 @@ public sealed class PandocAvailabilityService(RustService rustService, IDialogSe
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
this.Logger.LogError("Pandoc is not available after installation attempt.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Pandoc may be required for importing files.")));
|
||||
if (showErrorMessage)
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("AI Studio needs Pandoc for this, but it is not available.")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
@ -21,10 +19,10 @@ public static class UserFile
|
||||
/// </remarks>
|
||||
/// <param name="filePath">The full path to the file to be read. Must not be null or empty.</param>
|
||||
/// <param name="rustService">Rust service used to read file content.</param>
|
||||
/// <param name="dialogService">Dialogservice used to display the Pandoc installation dialog if needed.</param>
|
||||
/// <param name="pandocAvailability">Makes sure Pandoc is there and offers its installation.</param>
|
||||
/// <param name="token">Cancels the extraction when the caller no longer needs the content.</param>
|
||||
/// <returns>The result of reading the file.</returns>
|
||||
public static async Task<FileExtractionResult> LoadFileData(string filePath, RustService rustService, IDialogService dialogService, CancellationToken token = default)
|
||||
public static async Task<FileExtractionResult> LoadFileData(string filePath, RustService rustService, PandocAvailabilityService pandocAvailability, CancellationToken token = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
@ -41,24 +39,13 @@ public static class UserFile
|
||||
//
|
||||
if (FileTypes.RequiresPandoc(filePath))
|
||||
{
|
||||
var pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: false);
|
||||
// We report a missing Pandoc ourselves, because we can name the file which cannot be read:
|
||||
var pandocState = await pandocAvailability.EnsureAvailabilityAsync(showSuccessMessage: false, showDialog: true, showErrorMessage: false);
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<PandocDialog>
|
||||
{
|
||||
{ x => x.ShowInitialResultInSnackbar, false },
|
||||
};
|
||||
|
||||
var dialogReference = await dialogService.ShowAsync<PandocDialog>(TB("Pandoc Installation"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
await dialogReference.Result;
|
||||
|
||||
pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: true);
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
LOGGER.LogError("Pandoc is not available after installation attempt, so '{FilePath}' cannot be read.", filePath);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, FileExtractionErrorCode.PANDOC_UNAVAILABLE.ToUserMessage(fileName)));
|
||||
return FileExtractionResult.Failed(FileExtractionErrorCode.PANDOC_UNAVAILABLE, "Pandoc is required to read this file, but it is not available.");
|
||||
}
|
||||
LOGGER.LogError("Pandoc is not available after installation attempt, so '{FilePath}' cannot be read.", filePath);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, FileExtractionErrorCode.PANDOC_UNAVAILABLE.ToUserMessage(fileName)));
|
||||
return FileExtractionResult.Failed(FileExtractionErrorCode.PANDOC_UNAVAILABLE, "Pandoc is required to read this file, but it is not available.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
- Added speech-to-text for Helmholtz Blablador and GroqCloud, and embeddings for GWDG SAIA. These providers offer these services now, so you can select them when you dictate a message or when you set up a data source.
|
||||
- Added embeddings and speech-to-text for Hugging Face, so you can now use it to prepare your own documents for retrieval and to dictate your messages. Hugging Face offers both through a few of its inference providers only, which is why you get a shorter list to choose from there than you do for chatting.
|
||||
- Added a model list for Hugging Face. Until now you had to type the name of the model yourself and hope you got it right, down to its capitalization. AI Studio now loads the models your chosen inference provider actually offers, so you pick one from a list and cannot end up with a model that provider does not serve.
|
||||
- Added more file formats for exporting an AI answer. The export button used to offer Microsoft Word only; it is now a menu which also writes OpenDocument Text for LibreOffice, LaTeX, Markdown, and a webpage. When an answer contains tables, each of them can be saved on its own as a spreadsheet file, named after the heading above it and ready to open in Excel or LibreOffice Calc. This works in the chat and for the results of every assistant. Many thanks to Nils Kruthoff (`nilskruthoff`) for this contribution.
|
||||
- Added a choice of file format to the Batch Processing assistant. When it writes one result file per document, those files were always Markdown; you can now pick Microsoft Word, OpenDocument Text, LaTeX, or a webpage instead. For IT departments: the new setting `DataBatchProcessing.ResultFileFormat` lets you configure the format for your organization.
|
||||
- Improved the safety of plugin symbols: AI Studio now shows the symbol of a plugin in isolation, so nothing inside a symbol can reach the rest of the app.
|
||||
- Improved how much memory AI Studio needs. Working with large documents used to grow the app to several gigabytes, and on macOS that memory was never handed back. AI Studio now stays at a fraction of that and returns memory to your system. This matters most on devices with little memory, such as a Raspberry Pi.
|
||||
- Improved the preview for large documents. It now shows you the beginning of your document instead of loading all of it, so the dialog opens right away. Your complete document still goes to the AI.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user