From 7486cb660cc11acbdfe2e07a12ea822d6e43abed Mon Sep 17 00:00:00 2001 From: krut_ni Date: Tue, 28 Jul 2026 17:34:09 +0200 Subject: [PATCH 01/40] defined OpenDocument Text as an own file type not just a composite text document --- app/MindWork AI Studio/Tools/Rust/FileTypes.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs index 196075e1d..9ea92ddbf 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs @@ -46,7 +46,8 @@ public static class FileTypes public static readonly FileTypeFilter PDF = FileTypeFilter.Leaf("PDF", "pdf"); public static readonly FileTypeFilter TEXT = FileTypeFilter.Leaf(TB("Text"), "txt", "md", "rtf"); 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 OPEN_DOCUMENT_TEXT = FileTypeFilter.Leaf("OpenDocument Text", "odt"); + public static readonly FileTypeFilter WORD = FileTypeFilter.Parent("Word", OPEN_DOCUMENT_TEXT, MS_WORD); public static readonly FileTypeFilter EXCEL = FileTypeFilter.Leaf("Excel", "xls", "xlsx"); public static readonly FileTypeFilter POWER_POINT = FileTypeFilter.Leaf("PowerPoint", "ppt", "pptx", "odp"); public static readonly FileTypeFilter MAIL = FileTypeFilter.Leaf(TB("Mail"), "eml", "msg", "mbox"); From b503cb886736b493e7242cd2b5c871ca980b3f05 Mon Sep 17 00:00:00 2001 From: krut_ni Date: Tue, 28 Jul 2026 17:36:40 +0200 Subject: [PATCH 02/40] adding an enum to keep track of all export formats --- app/MindWork AI Studio/Tools/FileExportFormat.cs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/MindWork AI Studio/Tools/FileExportFormat.cs diff --git a/app/MindWork AI Studio/Tools/FileExportFormat.cs b/app/MindWork AI Studio/Tools/FileExportFormat.cs new file mode 100644 index 000000000..c3bff616d --- /dev/null +++ b/app/MindWork AI Studio/Tools/FileExportFormat.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools; + +public enum FileExportFormat +{ + MICROSOFT_WORD, + OPEN_DOCUMENT_TEXT, +} \ No newline at end of file From 9896222d396c5e0c2dfd648fb0fb26a97df0b825 Mon Sep 17 00:00:00 2001 From: krut_ni Date: Tue, 28 Jul 2026 17:37:52 +0200 Subject: [PATCH 03/40] Refactor pandoc export to general document types not just specific Microsoft Word documents --- app/MindWork AI Studio/Tools/PandocExport.cs | 36 +++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs index 139f95414..40bd32657 100644 --- a/app/MindWork AI Studio/Tools/PandocExport.cs +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -4,7 +4,6 @@ using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; - using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Tools; @@ -12,19 +11,30 @@ namespace AIStudio.Tools; public static class PandocExport { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocExport)); + private sealed record ExportTarget(string DisplayName, string PandocOutputFormat, FileTypeFilter FileType); + + private static readonly ExportTarget MICROSOFT_WORD = new("Microsoft Word (.docx)", "docx", FileTypes.MS_WORD); + private static readonly ExportTarget OPEN_DOCUMENT_TEXT = new("OpenDocument Text (.odt)", "odt", FileTypes.OPEN_DOCUMENT_TEXT); private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport)); - public static async Task ToMicrosoftWord(RustService rustService, IDialogService dialogService, string dialogTitle, IContent markdownContent) + public static async Task ToDocument(RustService rustService, IDialogService dialogService, FileExportFormat format, IContent markdownContent) { - var response = await rustService.SaveFile(dialogTitle, [FileTypes.MS_WORD]); + var exportTarget = format switch + { + FileExportFormat.MICROSOFT_WORD => MICROSOFT_WORD, + FileExportFormat.OPEN_DOCUMENT_TEXT => OPEN_DOCUMENT_TEXT, + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), + }; + + var response = await rustService.SaveFile(TB("Export chat"), [exportTarget.FileType]); if (response.UserCancelled) { LOGGER.LogInformation("User cancelled the save dialog."); return false; } - LOGGER.LogInformation($"The user chose the path '{response.SaveFilePath}' for the Microsoft Word export."); + LOGGER.LogInformation("The user chose the path '{SaveFilePath}' for the {ExportFormat} export.", response.SaveFilePath, exportTarget.DisplayName); var tempMarkdownFilePath = string.Empty; try @@ -36,9 +46,9 @@ public static async Task ToMicrosoftWord(RustService rustService, IDialogS var markdownText = markdownContent switch { ContentText text => text.Text, - ContentImage _ => "Image export to Microsoft Word not yet possible", + ContentImage _ => "Image export is not yet possible.", - _ => "Unknown content type. Cannot export to Word." + _ => "Unknown content type. Cannot export document." }; // Write text content to a temporary file: @@ -60,17 +70,17 @@ public static async Task ToMicrosoftWord(RustService rustService, IDialogS 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."))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Pandoc is required for document 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") + .WithOutputFormat(exportTarget.PandocOutputFormat) .WithOutputFile(response.SaveFilePath) .WithInputFile(tempMarkdownFilePath) .BuildAsync(rustService); @@ -94,19 +104,19 @@ public static async Task ToMicrosoftWord(RustService rustService, IDialogS 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"))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during document export"))); return false; } LOGGER.LogInformation("Pandoc conversion successful."); - await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Microsoft Word export successful"))); + await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Document export successful"))); 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} export.", exportTarget.DisplayName); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during document export"))); return false; } finally From 52b778c990f0fca4ef90e8d4b09dc42f9e296bb6 Mon Sep 17 00:00:00 2001 From: krut_ni Date: Tue, 28 Jul 2026 17:38:45 +0200 Subject: [PATCH 04/40] exchanging the old word export with the new document export --- .../Chat/ContentBlockComponent.razor.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 0dcb910c6..9021cc1d1 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -33,6 +33,8 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable " /// The role of the chat content block. @@ -546,9 +548,17 @@ private async Task RemoveBlock() await this.RemoveBlockFunc(this.Content); } - private async Task ExportToWord() + private async Task ExportDocument(FileExportFormat format) { - await PandocExport.ToMicrosoftWord(this.RustService, this.DialogService, T("Export Chat to Microsoft Word"), this.Content); + try + { + await PandocExport.ToDocument(this.RustService, this.DialogService, format, this.Content); + } + catch (ArgumentOutOfRangeException e) + { + await this.MessageBus.SendError(new(Icons.Material.Filled.Error, string.Format(this.T("Failed to export document to unknown file format '{0}'."), format))); + LOGGER.LogError($"Failed to export document, because the format ('{format}') is unknown to our Pandoc service:\n{e}"); + } } private async Task RegenerateBlock() From c99be93bc5efd1c875f5edae1b341dcc92c993d5 Mon Sep 17 00:00:00 2001 From: krut_ni Date: Tue, 28 Jul 2026 21:20:44 +0200 Subject: [PATCH 05/40] coverting the export button into a menu and wiring them up to the method --- .../Chat/ContentBlockComponent.razor | 106 ++++++++++-------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 529995498..8bb7b09f7 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -16,55 +16,65 @@ - @if (this.Content.FileAttachments.Count > 0) - { - - - - - - } - @if (this.Content.Sources.Count > 0) - { - - - - - - } - @if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null) - { - - - - } - @if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null) - { - - - - } - @if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null) - { - - - - } - @if (this.RemoveBlockFunc is not null) - { - - - - } +
+ @if (this.Content.FileAttachments.Count > 0) + { + + + + + + } + @if (this.Content.Sources.Count > 0) + { + + + + + + } + @if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null) + { + + + + } + @if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null) + { + + + + } + @if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null) + { + + + + } + @if (this.RemoveBlockFunc is not null) + { + + + + } - @if (this.Role is ChatRole.AI) - { - - - - } - + @if (this.Role is ChatRole.AI) + { + + + + @T("Microsoft Word (.docx)") + + + + @T("OpenDocument Text (.odt)") + + + + + } +
From 9622784e3c720b62a987585c97a43a0f3baeaf2e Mon Sep 17 00:00:00 2001 From: krut_ni Date: Tue, 28 Jul 2026 21:59:14 +0200 Subject: [PATCH 06/40] adding html export via pandoc like odt before --- app/MindWork AI Studio/Chat/ContentBlockComponent.razor | 5 +++++ app/MindWork AI Studio/Tools/FileExportFormat.cs | 1 + app/MindWork AI Studio/Tools/PandocExport.cs | 2 ++ app/MindWork AI Studio/Tools/Rust/FileTypes.cs | 1 + 4 files changed, 9 insertions(+) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 8bb7b09f7..a2922700d 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -71,6 +71,11 @@ @T("OpenDocument Text (.odt)") + + + @T("Hypertext (.html)") + + } diff --git a/app/MindWork AI Studio/Tools/FileExportFormat.cs b/app/MindWork AI Studio/Tools/FileExportFormat.cs index c3bff616d..b866f6a0d 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormat.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormat.cs @@ -4,4 +4,5 @@ public enum FileExportFormat { MICROSOFT_WORD, OPEN_DOCUMENT_TEXT, + HTML, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs index 40bd32657..c30482fb8 100644 --- a/app/MindWork AI Studio/Tools/PandocExport.cs +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -15,6 +15,7 @@ private sealed record ExportTarget(string DisplayName, string PandocOutputFormat private static readonly ExportTarget MICROSOFT_WORD = new("Microsoft Word (.docx)", "docx", FileTypes.MS_WORD); private static readonly ExportTarget OPEN_DOCUMENT_TEXT = new("OpenDocument Text (.odt)", "odt", FileTypes.OPEN_DOCUMENT_TEXT); + private static readonly ExportTarget HTML = new("Hypertext (.html)", "html", FileTypes.HYPERTEXT); private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport)); @@ -24,6 +25,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic { FileExportFormat.MICROSOFT_WORD => MICROSOFT_WORD, FileExportFormat.OPEN_DOCUMENT_TEXT => OPEN_DOCUMENT_TEXT, + FileExportFormat.HTML => HTML, _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), }; diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs index 9ea92ddbf..9f0977030 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs @@ -31,6 +31,7 @@ public static class FileTypes public static readonly FileTypeFilter LUA = FileTypeFilter.Leaf("Lua", "lua"); public static readonly FileTypeFilter PHP = FileTypeFilter.Leaf("PHP", "php"); public static readonly FileTypeFilter WEB = FileTypeFilter.Leaf("HTML/CSS", "html", "css"); + public static readonly FileTypeFilter HYPERTEXT = 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"); From 953c1c1dd8b839d702c6b5e7ea1097e4f2c868f6 Mon Sep 17 00:00:00 2001 From: Nils Kruthoff Date: Wed, 29 Jul 2026 13:31:00 +0200 Subject: [PATCH 07/40] refactored the ExportDocument method to support pandoc and plain file export --- .../Chat/ContentBlockComponent.razor.cs | 17 +++++- .../Tools/PlainFileExport.cs | 59 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 app/MindWork AI Studio/Tools/PlainFileExport.cs diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 9021cc1d1..f7896444d 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -552,7 +552,22 @@ private async Task ExportDocument(FileExportFormat format) { try { - await PandocExport.ToDocument(this.RustService, this.DialogService, format, this.Content); + switch (format) + { + case FileExportFormat.MARKDOWN: + await PlainFileExport.ToFile(this.RustService, format, this.Content); + break; + + case FileExportFormat.MICROSOFT_WORD: + case FileExportFormat.OPEN_DOCUMENT_TEXT: + case FileExportFormat.HTML: + await PandocExport.ToDocument(this.RustService, this.DialogService, format, this.Content); + break; + + default: + LOGGER.LogError($"No exporter is registered for {format}."); + return; + } } catch (ArgumentOutOfRangeException e) { diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs new file mode 100644 index 000000000..fce357fba --- /dev/null +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using AIStudio.Chat; +using AIStudio.Dialogs; +using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Rust; +using AIStudio.Tools.Services; +using DialogOptions = MudBlazor.DialogOptions; + +namespace AIStudio.Tools; + +public static class PlainFileExport +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PlainFileExport)); + + private sealed record ExportTarget(string DisplayName, FileTypeFilter FileType); + + private static readonly ExportTarget MARKDOWN = new("Markdown (.md)", FileTypes.MARKDOWN); + + private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PlainFileExport).Namespace, nameof(PlainFileExport)); + + public static async Task ToFile(RustService rustService, FileExportFormat format, IContent markdownContent) + { + var exportTarget = format switch + { + FileExportFormat.MARKDOWN => MARKDOWN, + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), + }; + + var response = await rustService.SaveFile(TB("Export chat"), [exportTarget.FileType]); + if (response.UserCancelled) + { + LOGGER.LogInformation("User cancelled the save dialog."); + return false; + } + + LOGGER.LogInformation($"The user chose the path '{response.SaveFilePath}' for the {exportTarget.DisplayName} export."); + + try + { + var markdownText = markdownContent switch + { + ContentText text => text.Text, + ContentImage _ => "Image export is not yet possible.", + _ => "Unknown content type. Cannot export document." + }; + + await File.WriteAllTextAsync(response.SaveFilePath, markdownText); + await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Document export successful"))); + + return true; + } + catch (Exception ex) + { + LOGGER.LogError(ex, $"Error during {exportTarget.DisplayName} export."); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during document export"))); + return false; + } + } +} From c69b30bad2be11f1ac5a101a7a4a3473dcf210ad Mon Sep 17 00:00:00 2001 From: Nils Kruthoff Date: Wed, 29 Jul 2026 13:31:32 +0200 Subject: [PATCH 08/40] included a button to export the chat content directly to a Markdown file --- app/MindWork AI Studio/Chat/ContentBlockComponent.razor | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index a2922700d..5db416976 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -71,7 +71,10 @@ @T("OpenDocument Text (.odt)") - + + @T("Markdown (.md)") + + @T("Hypertext (.html)") From c550527a5054766c6e3f1b4c80c3ba837397da64 Mon Sep 17 00:00:00 2001 From: Nils Kruthoff Date: Wed, 29 Jul 2026 13:32:19 +0200 Subject: [PATCH 09/40] added a Markdown and LaTeX file filter --- app/MindWork AI Studio/Tools/Rust/FileTypes.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs index 9f0977030..39fdf94e1 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs @@ -46,18 +46,20 @@ public static class FileTypes // Document hierarchy public static readonly FileTypeFilter PDF = FileTypeFilter.Leaf("PDF", "pdf"); public static readonly FileTypeFilter TEXT = FileTypeFilter.Leaf(TB("Text"), "txt", "md", "rtf"); + public static readonly FileTypeFilter MARKDOWN = FileTypeFilter.Leaf(TB("Markdown"), "md"); public static readonly FileTypeFilter MS_WORD = FileTypeFilter.Leaf("Microsoft Word", "docx"); public static readonly FileTypeFilter OPEN_DOCUMENT_TEXT = FileTypeFilter.Leaf("OpenDocument Text", "odt"); public static readonly FileTypeFilter WORD = FileTypeFilter.Parent("Word", OPEN_DOCUMENT_TEXT, MS_WORD); public static readonly FileTypeFilter EXCEL = FileTypeFilter.Leaf("Excel", "xls", "xlsx"); public static readonly FileTypeFilter POWER_POINT = FileTypeFilter.Leaf("PowerPoint", "ppt", "pptx", "odp"); 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"); + public static readonly FileTypeFilter LATEX_FAMILY = FileTypeFilter.Leaf("LaTeX", "tex", "bib", "sty", "cls", "log"); + public static readonly FileTypeFilter LATEX = 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"), - TEXT, OFFICE_FILES, SOURCE_CODE, LATEX); + TEXT, OFFICE_FILES, SOURCE_CODE, LATEX_FAMILY); // Media hierarchy public static readonly FileTypeFilter IMAGE = FileTypeFilter.Leaf(TB("Image"), From 83a9ca276f23805416df0246926ddc1b5d32e457 Mon Sep 17 00:00:00 2001 From: Nils Kruthoff Date: Wed, 29 Jul 2026 13:32:57 +0200 Subject: [PATCH 10/40] registered Markdown and Latex as an export format --- app/MindWork AI Studio/Tools/FileExportFormat.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/MindWork AI Studio/Tools/FileExportFormat.cs b/app/MindWork AI Studio/Tools/FileExportFormat.cs index b866f6a0d..a1768644e 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormat.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormat.cs @@ -5,4 +5,6 @@ public enum FileExportFormat MICROSOFT_WORD, OPEN_DOCUMENT_TEXT, HTML, + MARKDOWN, + LATEX, } \ No newline at end of file From d1f86ef742f0b565b9f30635c4f484c19ff54353 Mon Sep 17 00:00:00 2001 From: Nils Kruthoff Date: Wed, 29 Jul 2026 13:33:44 +0200 Subject: [PATCH 11/40] added Latex as an export target to the pandoc service --- app/MindWork AI Studio/Tools/PandocExport.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs index c30482fb8..e3ba655c0 100644 --- a/app/MindWork AI Studio/Tools/PandocExport.cs +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -16,6 +16,7 @@ private sealed record ExportTarget(string DisplayName, string PandocOutputFormat private static readonly ExportTarget MICROSOFT_WORD = new("Microsoft Word (.docx)", "docx", FileTypes.MS_WORD); private static readonly ExportTarget OPEN_DOCUMENT_TEXT = new("OpenDocument Text (.odt)", "odt", FileTypes.OPEN_DOCUMENT_TEXT); private static readonly ExportTarget HTML = new("Hypertext (.html)", "html", FileTypes.HYPERTEXT); + private static readonly ExportTarget LATEX = new("LaTeX (.tex)", "latex", FileTypes.LATEX); private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport)); @@ -26,6 +27,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic FileExportFormat.MICROSOFT_WORD => MICROSOFT_WORD, FileExportFormat.OPEN_DOCUMENT_TEXT => OPEN_DOCUMENT_TEXT, FileExportFormat.HTML => HTML, + FileExportFormat.LATEX => LATEX, _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), }; From 701c27b06e600cefc3bd8babb86bb56606aa7f19 Mon Sep 17 00:00:00 2001 From: Nils Kruthoff Date: Wed, 29 Jul 2026 14:11:07 +0200 Subject: [PATCH 12/40] included LaTeX to the export --- app/MindWork AI Studio/Chat/ContentBlockComponent.razor | 4 ++++ app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs | 1 + 2 files changed, 5 insertions(+) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 5db416976..cdf22c744 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -71,6 +71,10 @@ @T("OpenDocument Text (.odt)") + + @T("LaTeX (.tex)") + + @T("Markdown (.md)") diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index f7896444d..612e6d19c 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -561,6 +561,7 @@ private async Task ExportDocument(FileExportFormat format) case FileExportFormat.MICROSOFT_WORD: case FileExportFormat.OPEN_DOCUMENT_TEXT: case FileExportFormat.HTML: + case FileExportFormat.LATEX: await PandocExport.ToDocument(this.RustService, this.DialogService, format, this.Content); break; From d188dad4b4ecb6d493ce7fafe613b01e52df7033 Mon Sep 17 00:00:00 2001 From: krut_ni Date: Wed, 29 Jul 2026 19:46:33 +0200 Subject: [PATCH 13/40] added a RegexGenerator to find the first markdown code fence with a csv attribute --- .../Tools/PlainFileExport.cs | 62 ++++++++++++++++--- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index fce357fba..ea2d28afe 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -1,28 +1,67 @@ -using System.Diagnostics; +using System.Text.RegularExpressions; using AIStudio.Chat; -using AIStudio.Dialogs; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; -using DialogOptions = MudBlazor.DialogOptions; namespace AIStudio.Tools; -public static class PlainFileExport +public static partial class PlainFileExport { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PlainFileExport)); private sealed record ExportTarget(string DisplayName, FileTypeFilter FileType); private static readonly ExportTarget MARKDOWN = new("Markdown (.md)", FileTypes.MARKDOWN); + private static readonly ExportTarget CSV = new("CSV (.csv)", FileTypes.CSV); private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PlainFileExport).Namespace, nameof(PlainFileExport)); + + /// + /// Extracts the content of the first complete Markdown code block marked as CSV. + /// + public static bool TryExtractCsvContent(IContent content, out string csvContent) + { + if (content is ContentText text) + { + var match = CsvCodeFenceRegex().Match(text.Text); + if (match.Success) + { + csvContent = match.Groups["content"].Value; + return true; + } + } + + csvContent = string.Empty; + return false; + } + + [GeneratedRegex( + """ + # Matches an opening Markdown code fence and captures its delimiter. + ^(?`{3,}|~{3,}) + + # Matches the csv language identifier and the end of the opening fence line. + # Also allow tab-separated, pipe-separated and semicolon separated values at the code fence + [ \t]*(csv|tsv|psv|ssv)[ \t]*\r?\n + + # Captures the content of the first matching fenced code block. + (?[\s\S]*?) + + # Matches the corresponding closing fence followed by a line ending or end of input. + ^\k[ \t]*(?=\r?\n|$) + """, + RegexOptions.IgnoreCase | + RegexOptions.Multiline | + RegexOptions.IgnorePatternWhitespace)] + private static partial Regex CsvCodeFenceRegex(); public static async Task ToFile(RustService rustService, FileExportFormat format, IContent markdownContent) { var exportTarget = format switch { FileExportFormat.MARKDOWN => MARKDOWN, + FileExportFormat.CSV => CSV, _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), }; @@ -37,14 +76,19 @@ public static async Task ToFile(RustService rustService, FileExportFormat try { - var markdownText = markdownContent switch + var fileContent = format switch { - ContentText text => text.Text, - ContentImage _ => "Image export is not yet possible.", - _ => "Unknown content type. Cannot export document." + FileExportFormat.MARKDOWN => markdownContent switch + { + ContentText text => text.Text, + ContentImage _ => "Image export is not yet possible.", + _ => "Unknown content type. Cannot export document." + }, + FileExportFormat.CSV when TryExtractCsvContent(markdownContent, out var csvContent) => csvContent, + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), }; - await File.WriteAllTextAsync(response.SaveFilePath, markdownText); + await File.WriteAllTextAsync(response.SaveFilePath, fileContent); await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Document export successful"))); return true; From 8c5efe5dea21316373c01c64af46778fefb4d45c Mon Sep 17 00:00:00 2001 From: krut_ni Date: Wed, 29 Jul 2026 19:47:14 +0200 Subject: [PATCH 14/40] included csv as a file filter and in export formats --- app/MindWork AI Studio/Tools/FileExportFormat.cs | 3 ++- app/MindWork AI Studio/Tools/Rust/FileTypes.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Tools/FileExportFormat.cs b/app/MindWork AI Studio/Tools/FileExportFormat.cs index a1768644e..d06f78329 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormat.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormat.cs @@ -6,5 +6,6 @@ public enum FileExportFormat OPEN_DOCUMENT_TEXT, HTML, MARKDOWN, + CSV, LATEX, -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs index 39fdf94e1..079a8def6 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs @@ -47,6 +47,7 @@ public static class FileTypes public static readonly FileTypeFilter PDF = FileTypeFilter.Leaf("PDF", "pdf"); public static readonly FileTypeFilter TEXT = FileTypeFilter.Leaf(TB("Text"), "txt", "md", "rtf"); public static readonly FileTypeFilter MARKDOWN = FileTypeFilter.Leaf(TB("Markdown"), "md"); + public static readonly FileTypeFilter CSV = FileTypeFilter.Leaf("CSV", "csv"); public static readonly FileTypeFilter MS_WORD = FileTypeFilter.Leaf("Microsoft Word", "docx"); public static readonly FileTypeFilter OPEN_DOCUMENT_TEXT = FileTypeFilter.Leaf("OpenDocument Text", "odt"); public static readonly FileTypeFilter WORD = FileTypeFilter.Parent("Word", OPEN_DOCUMENT_TEXT, MS_WORD); From efd2bd27196149a696206c02872a4edcb74a388d Mon Sep 17 00:00:00 2001 From: krut_ni Date: Wed, 29 Jul 2026 19:48:57 +0200 Subject: [PATCH 15/40] registered the csv file export as a plain text save --- app/MindWork AI Studio/Chat/ContentBlockComponent.razor | 7 +++++++ app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs | 3 +++ 2 files changed, 10 insertions(+) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index cdf22c744..708064e08 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -74,6 +74,13 @@ @T("LaTeX (.tex)") + @if (this.HasCsv) + { + + + @T("CSV (.csv)") + + } @T("Markdown (.md)") diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 612e6d19c..99cbb0a3c 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -106,6 +106,8 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable private bool hasActiveMathContainer; private bool isDisposed; + private bool HasCsv => PlainFileExport.TryExtractCsvContent(this.Content, out _); + #region Overrides of ComponentBase protected override async Task OnInitializedAsync() @@ -555,6 +557,7 @@ private async Task ExportDocument(FileExportFormat format) switch (format) { case FileExportFormat.MARKDOWN: + case FileExportFormat.CSV: await PlainFileExport.ToFile(this.RustService, format, this.Content); break; From 61dd1ad4ebe7bc0a9749a410e2ac962e6fb049c4 Mon Sep 17 00:00:00 2001 From: krut_ni Date: Wed, 29 Jul 2026 20:00:47 +0200 Subject: [PATCH 16/40] i18n --- .../Assistants/I18N/allTexts.lua | 58 ++++++++++++++++--- .../plugin.lua | 58 ++++++++++++++++--- .../plugin.lua | 58 ++++++++++++++++--- 3 files changed, 150 insertions(+), 24 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index d0213f551..4227d05ab 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2299,6 +2299,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you -- Yes, remove the AI response and edit it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it" +-- Webpage +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1578631058"] = "Webpage" + -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it" @@ -2314,18 +2317,39 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Do you -- Removes this block UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes this block" +-- LaTeX (.tex) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2233607007"] = "LaTeX (.tex)" + +-- OpenDocument Text (.odt) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2253393351"] = "OpenDocument Text (.odt)" + -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message" +-- Markdown (.md) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2319970170"] = "Markdown (.md)" + +-- Hypertext (.html) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2504486680"] = "Hypertext (.html)" + +-- CSV (.csv) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2853636652"] = "CSV (.csv)" + -- Number of attachments UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments" +-- Microsoft Word (.docx) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3054800422"] = "Microsoft Word (.docx)" + -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet." -- Edit UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit" +-- Failed to export document to unknown file format '{0}'. +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3566938024"] = "Failed to export document to unknown file format '{0}'." + -- Regenerate UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Regenerate" @@ -2338,8 +2362,11 @@ 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" +-- LibreOffice / OpenOffice +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4258507172"] = "LibreOffice / OpenOffice" + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T72398679"] = "Export chat" -- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T3267850764"] = "The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings." @@ -8248,17 +8275,29 @@ 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." +-- Error during document export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1606201199"] = "Error during document export" -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T185447014"] = "Pandoc Installation" --- Error during Microsoft Word export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Microsoft Word export" +-- Document export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T2074749452"] = "Document export successful" + +-- Pandoc is required for document export. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T274234461"] = "Pandoc is required for document export." + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T72398679"] = "Export chat" --- Microsoft Word export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful" +-- Error during document export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1606201199"] = "Error during document export" + +-- Document export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2074749452"] = "Document export successful" + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T72398679"] = "Export chat" -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" @@ -8770,6 +8809,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Certificate -- Source like prefix UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like prefix" +-- Markdown +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4132508012"] = "Markdown" + -- Document UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document" diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 01d85b7a0..6b390ae79 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -2301,6 +2301,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Möchte -- Yes, remove the AI response and edit it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Ja, entferne die KI-Antwort und bearbeite sie." +-- Webpage +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1578631058"] = "Webseite" + -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Ja, neu generieren" @@ -2316,18 +2319,39 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Möchte -- Removes this block UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Entfernt diesen Block" +-- LaTeX (.tex) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2233607007"] = "LaTeX (.tex)" + +-- OpenDocument Text (.odt) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2253393351"] = "OpenDocument-Text (.odt)" + -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Nachricht neu erstellen" +-- Markdown (.md) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2319970170"] = "Markdown (.md)" + +-- Hypertext (.html) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2504486680"] = "Hypertext (.html)" + +-- CSV (.csv) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2853636652"] = "CSV (.csv)" + -- Number of attachments UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Anzahl der Anhänge" +-- Microsoft Word (.docx) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3054800422"] = "Microsoft Word (.docx)" + -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Der Inhaltstyp {0} kann noch nicht angezeigt werden." -- Edit UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Bearbeiten" +-- Failed to export document to unknown file format '{0}'. +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3566938024"] = "Export des Dokuments in das unbekannte Dateiformat „{0}“ fehlgeschlagen." + -- Regenerate UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Neu generieren" @@ -2340,8 +2364,11 @@ 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" +-- LibreOffice / OpenOffice +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4258507172"] = "LibreOffice / OpenOffice" + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T72398679"] = "Chat exportieren" -- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T3267850764"] = "Das ausgewählte Modell '{0}' ist bei '{1}' (Anbieter={2}) nicht mehr verfügbar. Bitte passen Sie Ihre Anbietereinstellungen an." @@ -8250,17 +8277,29 @@ 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." +-- Error during document export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1606201199"] = "Fehler beim Dokumentexport" -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T185447014"] = "Pandoc-Installation" --- Error during Microsoft Word export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Fehler beim Exportieren nach Microsoft Word" +-- Document export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T2074749452"] = "Dokumentexport erfolgreich" + +-- Pandoc is required for document export. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T274234461"] = "Pandoc wird für den Dokumentexport benötigt." + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T72398679"] = "Chat exportieren" --- Microsoft Word export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Export nach Microsoft Word erfolgreich" +-- Error during document export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1606201199"] = "Fehler beim Dokumentexport" + +-- Document export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2074749452"] = "Dokumentexport erfolgreich" + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T72398679"] = "Chat exportieren" -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" @@ -8772,6 +8811,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Zertifikatsb -- Source like prefix UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source Code ähnlicher Prefix" +-- Markdown +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4132508012"] = "Markdown" + -- Document UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index bb5e3610f..8f46921bd 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -2301,6 +2301,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you -- Yes, remove the AI response and edit it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it" +-- Webpage +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1578631058"] = "Webpage" + -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it" @@ -2316,18 +2319,39 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Do you -- Removes this block UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes this block" +-- LaTeX (.tex) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2233607007"] = "LaTeX (.tex)" + +-- OpenDocument Text (.odt) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2253393351"] = "OpenDocument Text (.odt)" + -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message" +-- Markdown (.md) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2319970170"] = "Markdown (.md)" + +-- Hypertext (.html) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2504486680"] = "Hypertext (.html)" + +-- CSV (.csv) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2853636652"] = "CSV (.csv)" + -- Number of attachments UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments" +-- Microsoft Word (.docx) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3054800422"] = "Microsoft Word (.docx)" + -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet." -- Edit UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit" +-- Failed to export document to unknown file format '{0}'. +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3566938024"] = "Failed to export document to unknown file format '{0}'." + -- Regenerate UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Regenerate" @@ -2340,8 +2364,11 @@ 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" +-- LibreOffice / OpenOffice +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4258507172"] = "LibreOffice / OpenOffice" + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T72398679"] = "Export chat" -- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T3267850764"] = "The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings." @@ -8250,17 +8277,29 @@ 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." +-- Error during document export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1606201199"] = "Error during document export" -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T185447014"] = "Pandoc Installation" --- Error during Microsoft Word export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Microsoft Word export" +-- Document export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T2074749452"] = "Document export successful" + +-- Pandoc is required for document export. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T274234461"] = "Pandoc is required for document export." + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T72398679"] = "Export chat" --- Microsoft Word export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful" +-- Error during document export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1606201199"] = "Error during document export" + +-- Document export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2074749452"] = "Document export successful" + +-- Export chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T72398679"] = "Export chat" -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" @@ -8772,6 +8811,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Certificate -- Source like prefix UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like prefix" +-- Markdown +UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4132508012"] = "Markdown" + -- Document UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document" From 52da54cd8ff49b3e3225f16cfd5c2145ff35285d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 18:55:00 +0200 Subject: [PATCH 17/40] Improved the export formats by defining them in one central place --- .../Tools/FileExportFormat.cs | 13 +- .../Tools/FileExportFormatExtensions.cs | 127 ++++++++++++++++++ app/MindWork AI Studio/Tools/PandocExport.cs | 40 +++--- .../Tools/PlainFileExport.cs | 32 +++-- 4 files changed, 170 insertions(+), 42 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs diff --git a/app/MindWork AI Studio/Tools/FileExportFormat.cs b/app/MindWork AI Studio/Tools/FileExportFormat.cs index d06f78329..9beb73c0a 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormat.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormat.cs @@ -1,11 +1,18 @@ namespace AIStudio.Tools; +/// +/// The file formats a chat message can be exported to. +/// public enum FileExportFormat { + NONE, + UNKNOWN, + MICROSOFT_WORD, OPEN_DOCUMENT_TEXT, - HTML, + LATEX, MARKDOWN, + HTML, CSV, - LATEX, -} + TSV, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs new file mode 100644 index 000000000..d77acca09 --- /dev/null +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -0,0 +1,127 @@ +using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Rust; + +namespace AIStudio.Tools; + +/// +/// 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. +/// +/// +/// 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. +/// +public static class FileExportFormatExtensions +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExportFormatExtensions).Namespace, nameof(FileExportFormatExtensions)); + + /// + /// The formats which every text message can be exported to, in the order the export menu shows them. + /// + /// + /// The tabular formats are missing on purpose: they depend on the message actually containing + /// a table, so whoever builds the menu adds the one which applies. + /// + public static readonly IReadOnlyList ALWAYS_AVAILABLE_FORMATS = + [ + FileExportFormat.MICROSOFT_WORD, + FileExportFormat.OPEN_DOCUMENT_TEXT, + FileExportFormat.LATEX, + FileExportFormat.MARKDOWN, + FileExportFormat.HTML, + ]; + + /// + /// Returns the name of the format as shown to the user. + /// + /// The format. + /// The name of the format. + 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, comma-separated (.csv)"), + FileExportFormat.TSV => TB("Table, tab-separated (.tsv)"), + + _ => TB("Unknown format"), + }; + + /// + /// Returns the icon of the format. + /// + /// The format. + /// The icon of the format. + 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, + }; + + /// + /// Returns the file extension of the format, including the leading dot. + /// + /// The format. + /// The file extension, or an empty string when the format writes no file. + 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, + }; + + /// + /// Returns the filter which the save dialog offers for the format. + /// + /// The format. + /// The filter, or null when the format cannot be written. + 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, + }; + + /// + /// Returns the name Pandoc knows the format by. + /// + /// The format. + /// The Pandoc output format, or an empty string when AI Studio writes the file itself. + 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, + }; + + /// + /// Determines whether writing the format needs Pandoc. + /// + /// The format. + /// True, when Pandoc converts the message; false, when AI Studio writes the file itself. + public static bool UsesPandoc(this FileExportFormat format) => !string.IsNullOrWhiteSpace(format.ToPandocOutputFormat()); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs index c16f2b92a..a51079c83 100644 --- a/app/MindWork AI Studio/Tools/PandocExport.cs +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -10,35 +10,31 @@ namespace AIStudio.Tools; public static class PandocExport { - private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocExport)); - private sealed record ExportTarget(string DisplayName, string PandocOutputFormat, FileTypeFilter FileType); - - private static readonly ExportTarget MICROSOFT_WORD = new("Microsoft Word (.docx)", "docx", FileTypes.MS_WORD); - private static readonly ExportTarget OPEN_DOCUMENT_TEXT = new("OpenDocument Text (.odt)", "odt", FileTypes.ODT); - private static readonly ExportTarget HTML = new("Hypertext (.html)", "html", FileTypes.HTML); - private static readonly ExportTarget LATEX = new("LaTeX (.tex)", "latex", FileTypes.TEX); - + 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)); - + + /// + /// Converts the given content to a document using Pandoc and lets the user save it. + /// + /// The Rust service, used for the save dialog and for Pandoc. + /// The dialog service, used to offer the Pandoc installation. + /// The format to write. Must be a format which uses Pandoc. + /// The content to export. + /// True, when the document was written. public static async Task ToDocument(RustService rustService, IDialogService dialogService, FileExportFormat format, IContent markdownContent) { - var exportTarget = format switch - { - FileExportFormat.MICROSOFT_WORD => MICROSOFT_WORD, - FileExportFormat.OPEN_DOCUMENT_TEXT => OPEN_DOCUMENT_TEXT, - FileExportFormat.HTML => HTML, - FileExportFormat.LATEX => LATEX, - _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), - }; - - var response = await rustService.SaveFile(TB("Export chat"), [exportTarget.FileType]); + if (!format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter) + throw new ArgumentOutOfRangeException(nameof(format), format, "Pandoc cannot write this format."); + + var response = await rustService.SaveFile(TB("Export chat"), [fileTypeFilter]); 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, exportTarget.DisplayName); + LOGGER.LogInformation("The user chose the path '{SaveFilePath}' for the {ExportFormat} export.", response.SaveFilePath, format); var tempMarkdownFilePath = string.Empty; try @@ -84,7 +80,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic .Create() .UseStandaloneMode() .WithInputFormat("gfm+emoji+tex_math_dollars") - .WithOutputFormat(exportTarget.PandocOutputFormat) + .WithOutputFormat(format.ToPandocOutputFormat()) .WithOutputFile(response.SaveFilePath) .WithInputFile(tempMarkdownFilePath) .BuildAsync(rustService); @@ -119,7 +115,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic } catch (Exception ex) { - LOGGER.LogError(ex, "Error during {ExportFormat} export.", exportTarget.DisplayName); + LOGGER.LogError(ex, "Error during {ExportFormat} export.", format); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during document export"))); return false; } diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index ea2d28afe..7a648ed8a 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -8,13 +8,8 @@ namespace AIStudio.Tools; public static partial class PlainFileExport { - private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PlainFileExport)); - - private sealed record ExportTarget(string DisplayName, FileTypeFilter FileType); - - private static readonly ExportTarget MARKDOWN = new("Markdown (.md)", FileTypes.MARKDOWN); - private static readonly ExportTarget CSV = new("CSV (.csv)", FileTypes.CSV); - + 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)); /// @@ -56,23 +51,26 @@ public static bool TryExtractCsvContent(IContent content, out string csvContent) RegexOptions.IgnorePatternWhitespace)] private static partial Regex CsvCodeFenceRegex(); + /// + /// Writes the given content to a plain text file and lets the user save it. + /// + /// The Rust service, used for the save dialog. + /// The format to write. Must be a format which does not use Pandoc. + /// The content to export. + /// True, when the file was written. public static async Task ToFile(RustService rustService, FileExportFormat format, IContent markdownContent) { - var exportTarget = format switch - { - FileExportFormat.MARKDOWN => MARKDOWN, - FileExportFormat.CSV => CSV, - _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), - }; - - var response = await rustService.SaveFile(TB("Export chat"), [exportTarget.FileType]); + 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(TB("Export chat"), [fileTypeFilter]); if (response.UserCancelled) { LOGGER.LogInformation("User cancelled the save dialog."); return false; } - LOGGER.LogInformation($"The user chose the path '{response.SaveFilePath}' for the {exportTarget.DisplayName} export."); + LOGGER.LogInformation("The user chose the path '{SaveFilePath}' for the {ExportFormat} export.", response.SaveFilePath, format); try { @@ -95,7 +93,7 @@ FileExportFormat.CSV when TryExtractCsvContent(markdownContent, out var csvConte } catch (Exception ex) { - LOGGER.LogError(ex, $"Error during {exportTarget.DisplayName} export."); + LOGGER.LogError(ex, "Error during {ExportFormat} export.", format); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during document export"))); return false; } From e63fae5c182b69956711c456e3a02d99c2d80cf9 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:04:33 +0200 Subject: [PATCH 18/40] Fixed the export writing an error sentence into the file for images --- .../Chat/ContentBlockComponent.razor | 2 +- .../Chat/ContentBlockComponent.razor.cs | 6 +++++ .../Chat/IContentExtensions.cs | 23 +++++++++++++++++++ app/MindWork AI Studio/Tools/PandocExport.cs | 20 ++++++++-------- .../Tools/PlainFileExport.cs | 20 ++++++++++------ 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 708064e08..d1fd1a5dc 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -59,7 +59,7 @@ } - @if (this.Role is ChatRole.AI) + @if (this.Role is ChatRole.AI && this.CanExport) { diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 18a971894..94a5a2fdc 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -106,6 +106,12 @@ public partial class ContentBlockComponent : MSGComponentBase private bool hasActiveMathContainer; private bool isDisposed; + /// + /// Whether this block can be exported at all. Only text carries something a document can hold; + /// an image, for example, has no representation any of our export formats could write. + /// + private bool CanExport => this.Content.TryGetMarkdownText(out _); + private bool HasCsv => PlainFileExport.TryExtractCsvContent(this.Content, out _); #region Overrides of ComponentBase diff --git a/app/MindWork AI Studio/Chat/IContentExtensions.cs b/app/MindWork AI Studio/Chat/IContentExtensions.cs index cfd3510da..4b86cd726 100644 --- a/app/MindWork AI Studio/Chat/IContentExtensions.cs +++ b/app/MindWork AI Studio/Chat/IContentExtensions.cs @@ -17,4 +17,27 @@ public static void ResetStreamingHandlers(this IContent content) content.StreamingEvent = IContent.NO_STREAMING_HANDLER; content.StreamingDone = IContent.NO_STREAMING_HANDLER; } + + /// + /// Reads this content as the Markdown text the AI produced. + /// + /// + /// 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. + /// + /// The content to read. + /// The Markdown text, or an empty string when there is none. + /// True, when this content carries Markdown text. + 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; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs index a51079c83..35f48f21b 100644 --- a/app/MindWork AI Studio/Tools/PandocExport.cs +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -27,6 +27,17 @@ public static async Task ToDocument(RustService rustService, IDialogServic 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(TB("Export chat"), [fileTypeFilter]); if (response.UserCancelled) { @@ -41,15 +52,6 @@ public static async Task ToDocument(RustService rustService, IDialogServic { 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 is not yet possible.", - - _ => "Unknown content type. Cannot export document." - }; // Write text content to a temporary file: await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText); diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index 7a648ed8a..0a0056e35 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -63,6 +63,17 @@ public static async Task ToFile(RustService rustService, FileExportFormat if (format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter) throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format itself."); + // + // We read the text before we ask for a path: when there is nothing to write, the user + // should learn that right away instead of picking a file first and getting an error afterward. + // + 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(TB("Export chat"), [fileTypeFilter]); if (response.UserCancelled) { @@ -76,12 +87,7 @@ public static async Task ToFile(RustService rustService, FileExportFormat { var fileContent = format switch { - FileExportFormat.MARKDOWN => markdownContent switch - { - ContentText text => text.Text, - ContentImage _ => "Image export is not yet possible.", - _ => "Unknown content type. Cannot export document." - }, + FileExportFormat.MARKDOWN => markdownText, FileExportFormat.CSV when TryExtractCsvContent(markdownContent, out var csvContent) => csvContent, _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), }; @@ -98,4 +104,4 @@ FileExportFormat.CSV when TryExtractCsvContent(markdownContent, out var csvConte return false; } } -} +} \ No newline at end of file From 2dbb321e70308f11521daf87f385eff51fdec9c7 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:16:41 +0200 Subject: [PATCH 19/40] Fixed tables being saved as CSV even when they were tab-separated --- .../Chat/ContentBlockComponent.razor | 6 +- .../Chat/ContentBlockComponent.razor.cs | 58 +++++++++----- .../Tools/PlainFileExport.cs | 80 ++++++++++++------- .../Tools/TabularExtract.cs | 8 ++ 4 files changed, 98 insertions(+), 54 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/TabularExtract.cs diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index d1fd1a5dc..e1616f699 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -74,11 +74,11 @@ @T("LaTeX (.tex)") - @if (this.HasCsv) + @if (this.TabularExport is { } tabularExport) { - - @T("CSV (.csv)") + + @tabularExport.Format.ToName() } diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 94a5a2fdc..1db0af593 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -101,6 +101,8 @@ public partial class ContentBlockComponent : MSGComponentBase private int lastRenderHash; private string cachedMarkdownRenderPlanInput = string.Empty; private MarkdownRenderPlan cachedMarkdownRenderPlan = MarkdownRenderPlan.EMPTY; + private string cachedTabularExportInput = string.Empty; + private TabularExtract? cachedTabularExport; private ElementReference mathContentContainer; private string lastMathRenderSignature = string.Empty; private bool hasActiveMathContainer; @@ -112,7 +114,29 @@ public partial class ContentBlockComponent : MSGComponentBase /// private bool CanExport => this.Content.TryGetMarkdownText(out _); - private bool HasCsv => PlainFileExport.TryExtractCsvContent(this.Content, out _); + /// + /// The table this block holds, if any, so that the export menu can offer it. + /// + /// + /// The result is cached the same way the Markdown render plan is: the render tree asks for + /// this on every render, and during streaming every token causes one. Without the cache, the + /// regex would run over the whole message once per token. + /// + private TabularExtract? TabularExport + { + get + { + if (!this.Content.TryGetMarkdownText(out var markdown)) + return null; + + if (ReferenceEquals(this.cachedTabularExportInput, markdown) || string.Equals(this.cachedTabularExportInput, markdown, StringComparison.Ordinal)) + return this.cachedTabularExport; + + this.cachedTabularExportInput = markdown; + this.cachedTabularExport = PlainFileExport.TryExtractTabularContent(markdown, out var extract) ? extract : null; + return this.cachedTabularExport; + } + } #region Overrides of ComponentBase @@ -557,29 +581,19 @@ private async Task ExportDocument(FileExportFormat format) { try { - switch (format) - { - case FileExportFormat.MARKDOWN: - case FileExportFormat.CSV: - await PlainFileExport.ToFile(this.RustService, format, this.Content); - break; - - case FileExportFormat.MICROSOFT_WORD: - case FileExportFormat.OPEN_DOCUMENT_TEXT: - case FileExportFormat.HTML: - case FileExportFormat.LATEX: - await PandocExport.ToDocument(this.RustService, this.DialogService, format, this.Content); - break; - - default: - LOGGER.LogError($"No exporter is registered for {format}."); - return; - } + // + // 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.DialogService, format, this.Content); + else + await PlainFileExport.ToFile(this.RustService, format, this.Content); } catch (ArgumentOutOfRangeException e) { - await this.MessageBus.SendError(new(Icons.Material.Filled.Error, string.Format(this.T("Failed to export document to unknown file format '{0}'."), format))); - LOGGER.LogError($"Failed to export document, because the format ('{format}') is unknown to our Pandoc service:\n{e}"); + 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))); + LOGGER.LogError(e, "Failed to export the content, because no exporter writes the format {ExportFormat}.", format); } } @@ -653,4 +667,4 @@ protected override async ValueTask DisposeResourcesAsync() await this.DisposeMathContainerIfNeededAsync(); } -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index 0a0056e35..928727c4a 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -13,44 +13,56 @@ public static partial class PlainFileExport private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PlainFileExport).Namespace, nameof(PlainFileExport)); /// - /// Extracts the content of the first complete Markdown code block marked as CSV. + /// Reads the first complete Markdown code block which holds tabular data. /// - public static bool TryExtractCsvContent(IContent content, out string csvContent) + /// + /// Models mark such a block with the name of the separator they used. The separator only + /// decides the file format where it has an established extension of its own: comma, semicolon, + /// and pipe separated data all belong into a .csv file, whereas tab separated data has .tsv. + /// + /// The Markdown text to read. + /// The tabular data, or the default when there is none. + /// True, when the text holds tabular data. + public static bool TryExtractTabularContent(string markdown, out TabularExtract extract) { - if (content is ContentText text) + var match = TabularCodeFenceRegex().Match(markdown); + if (!match.Success) { - var match = CsvCodeFenceRegex().Match(text.Text); - if (match.Success) - { - csvContent = match.Groups["content"].Value; - return true; - } + extract = default; + return false; } - csvContent = string.Empty; - return false; + var format = match.Groups["separator"].Value.Equals("tsv", StringComparison.OrdinalIgnoreCase) + ? FileExportFormat.TSV + : FileExportFormat.CSV; + + extract = new(match.Groups["content"].Value, format); + return true; } [GeneratedRegex( """ - # Matches an opening Markdown code fence and captures its delimiter. - ^(?`{3,}|~{3,}) + # Matches an opening Markdown code fence, which CommonMark lets you indent by up to three + # spaces, and captures both the delimiter and the character it is made of. + ^[ ]{0,3}(?(?`|~)\k{2,}) - # Matches the csv language identifier and the end of the opening fence line. - # Also allow tab-separated, pipe-separated and semicolon separated values at the code fence - [ \t]*(csv|tsv|psv|ssv)[ \t]*\r?\n + # Matches the name of the separator the model used, followed by the end of the opening + # fence line. Besides comma separated values, models also produce tab, pipe, and semicolon + # separated ones. + [ \t]*(?csv|tsv|psv|ssv)[ \t]*\r?\n # Captures the content of the first matching fenced code block. (?[\s\S]*?) - # Matches the corresponding closing fence followed by a line ending or end of input. - ^\k[ \t]*(?=\r?\n|$) + # Matches the closing fence, which CommonMark lets you write longer than the opening one, + # followed by a line ending or the end of the input. + ^[ ]{0,3}\k\k*[ \t]*(?=\r?\n|$) """, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace)] - private static partial Regex CsvCodeFenceRegex(); - + private static partial Regex TabularCodeFenceRegex(); + /// /// Writes the given content to a plain text file and lets the user save it. /// @@ -64,8 +76,9 @@ public static async Task ToFile(RustService rustService, FileExportFormat throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format itself."); // - // We read the text before we ask for a path: when there is nothing to write, the user - // should learn that right away instead of picking a file first and getting an error afterward. + // We work out what we are going to write before we ask for a path: when there is nothing + // to write, the user should learn that right away instead of picking a file first and + // getting an error afterward. // if (!markdownContent.TryGetMarkdownText(out var markdownText)) { @@ -74,6 +87,22 @@ public static async Task ToFile(RustService rustService, FileExportFormat return false; } + string fileContent; + if (format is FileExportFormat.MARKDOWN) + fileContent = markdownText; + else if (TryExtractTabularContent(markdownText, out var tabularExtract) && tabularExtract.Format == format) + fileContent = tabularExtract.Content; + else + { + // + // The message changed between showing the menu entry and clicking it: what looked like + // a table a moment ago is gone, or it is no longer the format the user asked for. + // + LOGGER.LogWarning("Cannot export the content as {ExportFormat}, because it holds no matching table.", format); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("This message no longer holds a table which could be exported."))); + return false; + } + var response = await rustService.SaveFile(TB("Export chat"), [fileTypeFilter]); if (response.UserCancelled) { @@ -85,13 +114,6 @@ public static async Task ToFile(RustService rustService, FileExportFormat try { - var fileContent = format switch - { - FileExportFormat.MARKDOWN => markdownText, - FileExportFormat.CSV when TryExtractCsvContent(markdownContent, out var csvContent) => csvContent, - _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), - }; - await File.WriteAllTextAsync(response.SaveFilePath, fileContent); await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Document export successful"))); diff --git a/app/MindWork AI Studio/Tools/TabularExtract.cs b/app/MindWork AI Studio/Tools/TabularExtract.cs new file mode 100644 index 000000000..23c2baa53 --- /dev/null +++ b/app/MindWork AI Studio/Tools/TabularExtract.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools; + +/// +/// The tabular data a message holds. +/// +/// The data itself, without the surrounding Markdown code fence. +/// The format this data gets written as. +public readonly record struct TabularExtract(string Content, FileExportFormat Format); \ No newline at end of file From 215fd3c5e99d0a612a1b2f7c9573da9626353fc1 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:22:45 +0200 Subject: [PATCH 20/40] Improved the export menu to appear only once the answer is complete --- .../Chat/ContentBlockComponent.razor.cs | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 1db0af593..881f8f199 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -101,42 +101,29 @@ public partial class ContentBlockComponent : MSGComponentBase private int lastRenderHash; private string cachedMarkdownRenderPlanInput = string.Empty; private MarkdownRenderPlan cachedMarkdownRenderPlan = MarkdownRenderPlan.EMPTY; - private string cachedTabularExportInput = string.Empty; - private TabularExtract? cachedTabularExport; private ElementReference mathContentContainer; private string lastMathRenderSignature = string.Empty; private bool hasActiveMathContainer; private bool isDisposed; /// - /// Whether this block can be exported at all. Only text carries something a document can hold; - /// an image, for example, has no representation any of our export formats could write. + /// Whether this block can be exported. /// - private bool CanExport => this.Content.TryGetMarkdownText(out _); + /// + /// We wait for the stream to finish: half an answer is nothing anybody wants in a document, + /// and waiting keeps us from searching a text which still grows with every token. Only text + /// can be exported at all; an image, for example, has no representation our formats could write. + /// + private bool CanExport => this.Content is { InitialRemoteWait: false, IsStreaming: false } && this.Content.TryGetMarkdownText(out _); /// /// The table this block holds, if any, so that the export menu can offer it. /// /// - /// The result is cached the same way the Markdown render plan is: the render tree asks for - /// this on every render, and during streaming every token causes one. Without the cache, the - /// regex would run over the whole message once per token. + /// Only asked for once the stream has finished, see CanExport, so the text this searches + /// is final and the search happens once per render of a settled block. /// - private TabularExtract? TabularExport - { - get - { - if (!this.Content.TryGetMarkdownText(out var markdown)) - return null; - - if (ReferenceEquals(this.cachedTabularExportInput, markdown) || string.Equals(this.cachedTabularExportInput, markdown, StringComparison.Ordinal)) - return this.cachedTabularExport; - - this.cachedTabularExportInput = markdown; - this.cachedTabularExport = PlainFileExport.TryExtractTabularContent(markdown, out var extract) ? extract : null; - return this.cachedTabularExport; - } - } + private TabularExtract? TabularExport => this.Content.TryGetMarkdownText(out var markdown) && PlainFileExport.TryExtractTabularContent(markdown, out var extract) ? extract : null; #region Overrides of ComponentBase From b4c87deaf84ee2dce741ecbb6faf53084b130f1d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:31:41 +0200 Subject: [PATCH 21/40] Fixed the export dialog claiming to export the chat instead of one message --- .../Assistants/AssistantBase.razor | 6 +++--- .../Chat/ContentBlockComponent.razor | 2 +- .../Chat/ContentBlockComponent.razor.cs | 16 ++++++++++++++-- .../Tools/FileExportFormatExtensions.cs | 12 ++++++++++++ app/MindWork AI Studio/Tools/PandocExport.cs | 14 ++++++++------ app/MindWork AI Studio/Tools/PlainFileExport.cs | 10 ++++++---- 6 files changed, 44 insertions(+), 16 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index b1d3ef12c..431ea5b32 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -75,9 +75,9 @@
- @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) { - + } @if(this.ShowResult && this.ShowEntireChatThread && this.ChatThread is not null) @@ -86,7 +86,7 @@ { @if (block is { HideFromUser: false, Content: not null }) { - + } } } diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index e1616f699..d43c2793a 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -61,7 +61,7 @@ @if (this.Role is ChatRole.AI && this.CanExport) { - + @T("Microsoft Word (.docx)") diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 881f8f199..84c510a1a 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -86,6 +86,17 @@ public partial class ContentBlockComponent : MSGComponentBase [Parameter] public Func RegenerateEnabled { get; set; } = () => false; + + /// + /// The title of the save dialog when this block gets exported. + /// + /// + /// A block is a chat message in the chat, but the result of an assistant in an assistant, 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. + /// + [Parameter] + public string? ExportDialogTitle { get; set; } [Inject] private IDialogService DialogService { get; init; } = null!; @@ -572,10 +583,11 @@ private async Task ExportDocument(FileExportFormat format) // 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. // + var dialogTitle = this.ExportDialogTitle ?? this.T("Export message"); if (format.UsesPandoc()) - await PandocExport.ToDocument(this.RustService, this.DialogService, format, this.Content); + await PandocExport.ToDocument(this.RustService, this.DialogService, dialogTitle, format, this.Content); else - await PlainFileExport.ToFile(this.RustService, format, this.Content); + await PlainFileExport.ToFile(this.RustService, dialogTitle, format, this.Content); } catch (ArgumentOutOfRangeException e) { diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs index d77acca09..0d24876a5 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -85,6 +85,18 @@ public static class FileExportFormatExtensions _ => string.Empty, }; + /// + /// Returns the file name the save dialog starts with. + /// + /// + /// 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 name is deliberately not translated: a file + /// name should survive being copied between systems and locales. + /// + /// The format. + /// The suggested file name, including its extension. + public static string ToSuggestedFileName(this FileExportFormat format) => $"export{format.ToFileExtension()}"; + /// /// Returns the filter which the save dialog offers for the format. /// diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs index 35f48f21b..685aaa2da 100644 --- a/app/MindWork AI Studio/Tools/PandocExport.cs +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -19,10 +19,12 @@ public static class PandocExport /// /// The Rust service, used for the save dialog and for Pandoc. /// The dialog service, used to offer the Pandoc installation. + /// 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. /// The format to write. Must be a format which uses Pandoc. /// The content to export. /// True, when the document was written. - public static async Task ToDocument(RustService rustService, IDialogService dialogService, FileExportFormat format, IContent markdownContent) + public static async Task ToDocument(RustService rustService, IDialogService dialogService, 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."); @@ -38,7 +40,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic return false; } - var response = await rustService.SaveFile(TB("Export chat"), [fileTypeFilter]); + var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName()); if (response.UserCancelled) { LOGGER.LogInformation("User cancelled the save dialog."); @@ -72,7 +74,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic 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 document export."))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Pandoc is required for this export."))); return false; } } @@ -106,19 +108,19 @@ public static async Task ToDocument(RustService rustService, IDialogServic 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 document export"))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("The export failed."))); return false; } LOGGER.LogInformation("Pandoc conversion successful."); - await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Document export successful"))); + 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("Error during document export"))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("The export failed."))); return false; } finally diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index 928727c4a..6eb7a94ad 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -67,10 +67,12 @@ public static bool TryExtractTabularContent(string markdown, out TabularExtract /// Writes the given content to a plain text file and lets the user save it. /// /// The Rust service, used for the save dialog. + /// 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. /// The format to write. Must be a format which does not use Pandoc. /// The content to export. /// True, when the file was written. - public static async Task ToFile(RustService rustService, FileExportFormat format, IContent markdownContent) + public static async Task ToFile(RustService rustService, string dialogTitle, FileExportFormat format, IContent markdownContent) { if (format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter) throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format itself."); @@ -103,7 +105,7 @@ public static async Task ToFile(RustService rustService, FileExportFormat return false; } - var response = await rustService.SaveFile(TB("Export chat"), [fileTypeFilter]); + var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName()); if (response.UserCancelled) { LOGGER.LogInformation("User cancelled the save dialog."); @@ -115,14 +117,14 @@ public static async Task ToFile(RustService rustService, FileExportFormat try { await File.WriteAllTextAsync(response.SaveFilePath, fileContent); - await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Document export successful"))); + 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("Error during document export"))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("The export failed."))); return false; } } From 27992f96c1b7396578419587f9add402de6704ac Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:34:47 +0200 Subject: [PATCH 22/40] Fixed the export dialog claiming to export the chat instead of one message --- .../Assistants/AssistantBase.razor | 4 ++-- .../Chat/ContentBlockComponent.razor | 2 +- .../Chat/ContentBlockComponent.razor.cs | 22 ++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 431ea5b32..c08e0d0b2 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -77,7 +77,7 @@ @if (this.ShowResult && !this.ShowEntireChatThread && this.ResultingContentBlock?.Content != null) { - + } @if(this.ShowResult && this.ShowEntireChatThread && this.ChatThread is not null) @@ -86,7 +86,7 @@ { @if (block is { HideFromUser: false, Content: not null }) { - + } } } diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index d43c2793a..51648dacf 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -61,7 +61,7 @@ @if (this.Role is ChatRole.AI && this.CanExport) { - + @T("Microsoft Word (.docx)") diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 84c510a1a..09557a15d 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -88,15 +88,17 @@ public partial class ContentBlockComponent : MSGComponentBase public Func RegenerateEnabled { get; set; } = () => false; /// - /// The title of the save dialog when this block gets exported. + /// What the export offers, used both as the label of the export button and as the title of + /// the save dialog. /// /// - /// A block is a chat message in the chat, but the result of an assistant in an assistant, 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. + /// 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. /// [Parameter] - public string? ExportDialogTitle { get; set; } + public string? ExportTitle { get; set; } [Inject] private IDialogService DialogService { get; init; } = null!; @@ -136,6 +138,11 @@ public partial class ContentBlockComponent : MSGComponentBase /// private TabularExtract? TabularExport => this.Content.TryGetMarkdownText(out var markdown) && PlainFileExport.TryExtractTabularContent(markdown, out var extract) ? extract : null; + /// + /// What the export offers, falling back to the chat wording when nobody named it. + /// + private string EffectiveExportTitle => this.ExportTitle ?? this.T("Export AI response"); + #region Overrides of ComponentBase protected override async Task OnInitializedAsync() @@ -583,11 +590,10 @@ private async Task ExportDocument(FileExportFormat format) // 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. // - var dialogTitle = this.ExportDialogTitle ?? this.T("Export message"); if (format.UsesPandoc()) - await PandocExport.ToDocument(this.RustService, this.DialogService, dialogTitle, format, this.Content); + await PandocExport.ToDocument(this.RustService, this.DialogService, this.EffectiveExportTitle, format, this.Content); else - await PlainFileExport.ToFile(this.RustService, dialogTitle, format, this.Content); + await PlainFileExport.ToFile(this.RustService, this.EffectiveExportTitle, format, this.Content); } catch (ArgumentOutOfRangeException e) { From 400b8f637a2ba631c4c251f2eb338fd1c9a20b74 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:37:58 +0200 Subject: [PATCH 23/40] Improved the export menu to build itself from the known formats --- .../Chat/ContentBlockComponent.razor | 40 +++++++------------ .../Tools/FileExportFormatExtensions.cs | 16 +++++--- 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 51648dacf..8e1977a59 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -16,7 +16,7 @@ -
+
@if (this.Content.FileAttachments.Count > 0) { @@ -63,37 +63,25 @@ { - - @T("Microsoft Word (.docx)") - - - - @T("OpenDocument Text (.odt)") - - - - @T("LaTeX (.tex)") - + @foreach (var documentFormat in FileExportFormatExtensions.DOCUMENT_FORMATS) + { + + } @if (this.TabularExport is { } tabularExport) { - - - @tabularExport.Format.ToName() - + + + } + + @foreach (var textFormat in FileExportFormatExtensions.TEXT_FORMATS) + { + } - - - @T("Markdown (.md)") - - - - @T("Hypertext (.html)") - - } -
+ +
diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs index 0d24876a5..952596326 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -17,17 +17,21 @@ public static class FileExportFormatExtensions private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExportFormatExtensions).Namespace, nameof(FileExportFormatExtensions)); /// - /// The formats which every text message can be exported to, in the order the export menu shows them. + /// The formats which lay the text out as a document you would hand to somebody, in the order + /// the export menu shows them. /// - /// - /// The tabular formats are missing on purpose: they depend on the message actually containing - /// a table, so whoever builds the menu adds the one which applies. - /// - public static readonly IReadOnlyList ALWAYS_AVAILABLE_FORMATS = + public static readonly IReadOnlyList DOCUMENT_FORMATS = [ FileExportFormat.MICROSOFT_WORD, FileExportFormat.OPEN_DOCUMENT_TEXT, FileExportFormat.LATEX, + ]; + + /// + /// The formats which keep the text as text, in the order the export menu shows them. + /// + public static readonly IReadOnlyList TEXT_FORMATS = + [ FileExportFormat.MARKDOWN, FileExportFormat.HTML, ]; From 91a77056f77f71c741a4553017033318026c88a5 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:44:12 +0200 Subject: [PATCH 24/40] Improved the export to follow the conventions for encoding and logging --- .../Chat/ContentBlockComponent.razor.cs | 7 +++--- .../Tools/FileExportFormatExtensions.cs | 25 ++++++++++++++++++- app/MindWork AI Studio/Tools/PandocExport.cs | 10 +++++--- .../Tools/PlainFileExport.cs | 2 +- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 09557a15d..ab73218c4 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -33,8 +33,6 @@ public partial class ContentBlockComponent : MSGComponentBase " /// The role of the chat content block. @@ -109,6 +107,9 @@ public partial class ContentBlockComponent : MSGComponentBase [Inject] private IJSRuntime JsRuntime { get; init; } = null!; + [Inject] + private ILogger Logger { get; init; } = null!; + private bool HideContent { get; set; } private bool hasRenderHash; private int lastRenderHash; @@ -598,7 +599,7 @@ private async Task ExportDocument(FileExportFormat format) catch (ArgumentOutOfRangeException e) { 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))); - LOGGER.LogError(e, "Failed to export the content, because no exporter writes the format {ExportFormat}.", format); + this.Logger.LogError(e, "Failed to export the content, because no exporter writes the format {ExportFormat}.", format); } } diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs index 952596326..99e80c246 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -1,4 +1,6 @@ -using AIStudio.Tools.PluginSystem; +using System.Text; + +using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; namespace AIStudio.Tools; @@ -16,6 +18,9 @@ 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); + /// /// The formats which lay the text out as a document you would hand to somebody, in the order /// the export menu shows them. @@ -119,6 +124,24 @@ public static class FileExportFormatExtensions _ => null, }; + /// + /// Returns the encoding the file gets written with. + /// + /// + /// 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. + /// + /// The format. + /// The encoding to write the file with. + public static Encoding ToFileEncoding(this FileExportFormat format) => format switch + { + FileExportFormat.CSV or FileExportFormat.TSV => WITH_BYTE_ORDER_MARK, + + _ => WITHOUT_BYTE_ORDER_MARK, + }; + /// /// Returns the name Pandoc knows the format by. /// diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs index 685aaa2da..d80f82228 100644 --- a/app/MindWork AI Studio/Tools/PandocExport.cs +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -1,8 +1,9 @@ using System.Diagnostics; +using System.Text; + using AIStudio.Chat; using AIStudio.Dialogs; using AIStudio.Tools.PluginSystem; -using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using DialogOptions = AIStudio.Dialogs.DialogOptions; @@ -55,8 +56,9 @@ public static async Task ToDocument(RustService rustService, IDialogServic var tempMarkdownFile = Guid.NewGuid().ToString(); tempMarkdownFilePath = Path.Combine(Path.GetTempPath(), tempMarkdownFile); - // Write text content to a temporary file: - await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText); + // 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)); // Ensure that Pandoc is installed and ready: var pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: false); @@ -134,7 +136,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic } catch { - LOGGER.LogWarning($"Was not able to delete temporary file: '{tempMarkdownFilePath}'"); + LOGGER.LogWarning("Was not able to delete the temporary file '{TempFilePath}'.", tempMarkdownFilePath); } } } diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index 6eb7a94ad..0102c2bd6 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -116,7 +116,7 @@ public static async Task ToFile(RustService rustService, string dialogTitl try { - await File.WriteAllTextAsync(response.SaveFilePath, fileContent); + await File.WriteAllTextAsync(response.SaveFilePath, fileContent, format.ToFileEncoding()); await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("The export succeeded."))); return true; From 3933c64d3048e66a9c437f2ee466934555021f03 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:49:40 +0200 Subject: [PATCH 25/40] Updated I18N --- .../Assistants/I18N/allTexts.lua | 93 ++++++++++--------- .../plugin.lua | 93 ++++++++++--------- .../plugin.lua | 93 ++++++++++--------- 3 files changed, 144 insertions(+), 135 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 3af732c5e..d1d0afbc1 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -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." @@ -3181,9 +3184,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you -- Yes, remove the AI response and edit it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it" --- Webpage -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1578631058"] = "Webpage" - -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it" @@ -3199,39 +3199,24 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Do you -- Removes this block UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes this block" --- LaTeX (.tex) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2233607007"] = "LaTeX (.tex)" - --- OpenDocument Text (.odt) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2253393351"] = "OpenDocument Text (.odt)" - -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message" --- Markdown (.md) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2319970170"] = "Markdown (.md)" - --- Hypertext (.html) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2504486680"] = "Hypertext (.html)" +-- 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." --- CSV (.csv) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2853636652"] = "CSV (.csv)" +-- 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" --- Microsoft Word (.docx) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3054800422"] = "Microsoft Word (.docx)" - -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet." -- Edit UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit" --- Failed to export document to unknown file format '{0}'. -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3566938024"] = "Failed to export document to unknown file format '{0}'." - -- Regenerate UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Regenerate" @@ -3244,12 +3229,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove -- No, keep it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it" --- LibreOffice / OpenOffice -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4258507172"] = "LibreOffice / OpenOffice" - --- Export chat -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T72398679"] = "Export chat" - -- 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." @@ -9994,6 +9973,30 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The -- policy files UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files" +-- Table, tab-separated (.tsv) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1120414862"] = "Table, tab-separated (.tsv)" + +-- Table, comma-separated (.csv) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1539587981"] = "Table, comma-separated (.csv)" + +-- 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)" + +-- Microsoft Word (.docx) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3054800422"] = "Microsoft Word (.docx)" + +-- Webpage (.html) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3651679344"] = "Webpage (.html)" + +-- 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." @@ -10096,29 +10099,32 @@ 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." --- Error during document export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1606201199"] = "Error during document 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" --- Document export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T2074749452"] = "Document export successful" +-- The export failed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1895034475"] = "The export failed." + +-- Pandoc is required for this export. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3548302476"] = "Pandoc is required for this export." --- Pandoc is required for document export. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T274234461"] = "Pandoc is required for document export." +-- Only text messages can be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Only text messages can be exported." --- Export chat -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T72398679"] = "Export chat" +-- The export succeeded. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "The export succeeded." --- Error during document export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1606201199"] = "Error during document export" +-- The export failed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "The export failed." --- Document export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2074749452"] = "Document export successful" +-- This message no longer holds a table which could be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2940651523"] = "This message no longer holds a table which could be exported." --- Export chat -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T72398679"] = "Export chat" +-- Only text messages can be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T3576815370"] = "Only text messages can be exported." -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" @@ -10645,9 +10651,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Certificate -- Source like prefix UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like prefix" --- Markdown -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4132508012"] = "Markdown" - -- Document UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document" diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index d87fca080..141bd01bc 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -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." @@ -3183,9 +3186,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Möchte -- Yes, remove the AI response and edit it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Ja, entferne die KI-Antwort und bearbeite sie." --- Webpage -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1578631058"] = "Webseite" - -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Ja, neu generieren" @@ -3201,39 +3201,24 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Möchte -- Removes this block UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Entfernt diesen Block" --- LaTeX (.tex) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2233607007"] = "LaTeX (.tex)" - --- OpenDocument Text (.odt) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2253393351"] = "OpenDocument-Text (.odt)" - -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Nachricht neu erstellen" --- Markdown (.md) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2319970170"] = "Markdown (.md)" - --- Hypertext (.html) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2504486680"] = "Hypertext (.html)" +-- 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." --- CSV (.csv) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2853636652"] = "CSV (.csv)" +-- 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" --- Microsoft Word (.docx) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3054800422"] = "Microsoft Word (.docx)" - -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Der Inhaltstyp {0} kann noch nicht angezeigt werden." -- Edit UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Bearbeiten" --- Failed to export document to unknown file format '{0}'. -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3566938024"] = "Export des Dokuments in das unbekannte Dateiformat „{0}“ fehlgeschlagen." - -- Regenerate UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Neu generieren" @@ -3246,12 +3231,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Nachric -- No, keep it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "Nein, behalten" --- LibreOffice / OpenOffice -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4258507172"] = "LibreOffice / OpenOffice" - --- Export chat -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T72398679"] = "Chat 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." @@ -9996,6 +9975,30 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "Das -- policy files UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "Richtliniendateien" +-- Table, tab-separated (.tsv) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1120414862"] = "Tabelle, mit Tabs getrennt (.tsv)" + +-- Table, comma-separated (.csv) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1539587981"] = "Tabelle, mit Kommata getrennt (.csv)" + +-- 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)" + +-- Microsoft Word (.docx) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3054800422"] = "Microsoft Word (.docx)" + +-- Webpage (.html) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3651679344"] = "Webseite (.html)" + +-- 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." @@ -10098,29 +10101,32 @@ 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." --- Error during document export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1606201199"] = "Fehler beim Dokumentexport" +-- 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" --- Document export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T2074749452"] = "Dokumentexport erfolgreich" +-- The export failed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1895034475"] = "Der Export ist fehlgeschlagen." + +-- Pandoc is required for this export. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3548302476"] = "Für diesen Export ist Pandoc erforderlich." --- Pandoc is required for document export. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T274234461"] = "Pandoc wird für den Dokumentexport benötigt." +-- Only text messages can be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Nur Textnachrichten können exportiert werden." --- Export chat -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T72398679"] = "Chat exportieren" +-- The export succeeded. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "Der Export war erfolgreich." --- Error during document export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1606201199"] = "Fehler beim Dokumentexport" +-- The export failed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "Der Export ist fehlgeschlagen." --- Document export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2074749452"] = "Dokumentexport erfolgreich" +-- This message no longer holds a table which could be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2940651523"] = "Diese Nachricht enthält keine Tabelle mehr, die exportiert werden könnte." --- Export chat -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T72398679"] = "Chat exportieren" +-- Only text messages can be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T3576815370"] = "Nur Textnachrichten können exportiert werden." -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" @@ -10647,9 +10653,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Zertifikatsb -- Source like prefix UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source Code ähnlicher Prefix" --- Markdown -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4132508012"] = "Markdown" - -- Document UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index a516c8093..37a41ab4a 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -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." @@ -3183,9 +3186,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you -- Yes, remove the AI response and edit it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it" --- Webpage -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1578631058"] = "Webpage" - -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it" @@ -3201,39 +3201,24 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Do you -- Removes this block UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes this block" --- LaTeX (.tex) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2233607007"] = "LaTeX (.tex)" - --- OpenDocument Text (.odt) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2253393351"] = "OpenDocument Text (.odt)" - -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message" --- Markdown (.md) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2319970170"] = "Markdown (.md)" - --- Hypertext (.html) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2504486680"] = "Hypertext (.html)" +-- 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." --- CSV (.csv) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2853636652"] = "CSV (.csv)" +-- 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" --- Microsoft Word (.docx) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3054800422"] = "Microsoft Word (.docx)" - -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet." -- Edit UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit" --- Failed to export document to unknown file format '{0}'. -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3566938024"] = "Failed to export document to unknown file format '{0}'." - -- Regenerate UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Regenerate" @@ -3246,12 +3231,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove -- No, keep it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it" --- LibreOffice / OpenOffice -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4258507172"] = "LibreOffice / OpenOffice" - --- Export chat -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T72398679"] = "Export chat" - -- 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." @@ -9996,6 +9975,30 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The -- policy files UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files" +-- Table, tab-separated (.tsv) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1120414862"] = "Table, tab-separated (.tsv)" + +-- Table, comma-separated (.csv) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1539587981"] = "Table, comma-separated (.csv)" + +-- 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)" + +-- Microsoft Word (.docx) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3054800422"] = "Microsoft Word (.docx)" + +-- Webpage (.html) +UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T3651679344"] = "Webpage (.html)" + +-- 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." @@ -10098,29 +10101,32 @@ 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." --- Error during document export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1606201199"] = "Error during document 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" --- Document export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T2074749452"] = "Document export successful" +-- The export failed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1895034475"] = "The export failed." + +-- Pandoc is required for this export. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3548302476"] = "Pandoc is required for this export." --- Pandoc is required for document export. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T274234461"] = "Pandoc is required for document export." +-- Only text messages can be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Only text messages can be exported." --- Export chat -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T72398679"] = "Export chat" +-- The export succeeded. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "The export succeeded." --- Error during document export -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1606201199"] = "Error during document export" +-- The export failed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "The export failed." --- Document export successful -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2074749452"] = "Document export successful" +-- This message no longer holds a table which could be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2940651523"] = "This message no longer holds a table which could be exported." --- Export chat -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T72398679"] = "Export chat" +-- Only text messages can be exported. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T3576815370"] = "Only text messages can be exported." -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" @@ -10647,9 +10653,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T3543954504"] = "Certificate -- Source like prefix UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like prefix" --- Markdown -UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4132508012"] = "Markdown" - -- Document UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document" From b7db6e131ee8a96c8706326b74afe815e552f205 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 19:52:08 +0200 Subject: [PATCH 26/40] Updated changelog --- app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index fcda497a3..a565ce674 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -9,6 +9,7 @@ - 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 a table, you can save just that table as a spreadsheet file, 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. - 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. From b330f357b47fd5f6fbc92865f1f35a446de3aee6 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 20:18:00 +0200 Subject: [PATCH 27/40] Moved the CSV writing into a tool so everybody can use it --- ...istantBatchProcessing.razor.Persistence.cs | 8 ++--- .../BatchProcessing/BatchProcessingCsv.cs | 28 ++------------- app/MindWork AI Studio/Tools/CsvWriter.cs | 35 +++++++++++++++++++ 3 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/CsvWriter.cs diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs index 23103e821..8ed9b85b2 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs @@ -106,9 +106,9 @@ private async Task WriteAggregatedResultsAsync(string resolvedOutputDirectory) 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 @@ private async Task WriteResultsTableAsync(string resolvedOutputDirectory) { 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()); } diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingCsv.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingCsv.cs index 2f7b6ba95..147a0e1e4 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingCsv.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingCsv.cs @@ -3,35 +3,13 @@ namespace AIStudio.Assistants.BatchProcessing; /// -/// 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. /// public static class BatchProcessingCsv { - public static string ToCsvRow(char separator, params string[] fields) => string.Join(separator, fields.Select(field => ToCsvField(field, separator))); - - /// - /// Quotes one CSV field according to RFC 4180. - /// - 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("\"", "\"\"")}" - """; - } - /// - /// Parses a CSV text which was written by . + /// Parses a CSV text which was written by CsvWriter.ToRow. /// /// /// We parse the file ourselves instead of splitting lines, because quoted diff --git a/app/MindWork AI Studio/Tools/CsvWriter.cs b/app/MindWork AI Studio/Tools/CsvWriter.cs new file mode 100644 index 000000000..15c1de506 --- /dev/null +++ b/app/MindWork AI Studio/Tools/CsvWriter.cs @@ -0,0 +1,35 @@ +namespace AIStudio.Tools; + +/// +/// Writes rows of character-separated values. Fields are quoted according to RFC 4180 using the +/// separator of the respective file. +/// +public static class CsvWriter +{ + /// + /// Joins the given fields into one row. + /// + /// The separator between two fields. + /// The fields of the row. + /// The row, without a line ending. + public static string ToRow(char separator, params string[] fields) => string.Join(separator, fields.Select(field => ToField(field, separator))); + + /// + /// Quotes one field according to RFC 4180. + /// + 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("\"", "\"\"")}" + """; + } +} \ No newline at end of file From a17c6b31f126baf7190778be5a77cdb0453a06c1 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 20:31:36 +0200 Subject: [PATCH 28/40] Fixed the table export not recognizing normal Markdown tables --- .../Chat/ContentBlockComponent.razor | 7 +- .../Chat/ContentBlockComponent.razor.cs | 82 ++++++- app/MindWork AI Studio/Tools/MessageTable.cs | 12 + .../Tools/PlainFileExport.cs | 217 ++++++++++++------ .../Tools/TabularExtract.cs | 8 - 5 files changed, 230 insertions(+), 96 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/MessageTable.cs delete mode 100644 app/MindWork AI Studio/Tools/TabularExtract.cs diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 8e1977a59..db530dcc9 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -67,10 +67,13 @@ { } - @if (this.TabularExport is { } tabularExport) + @if (this.MessageTables.Count > 0) { - + @foreach (var messageTable in this.MessageTables) + { + + } } @foreach (var textFormat in FileExportFormatExtensions.TEXT_FORMATS) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index ab73218c4..836a14ee6 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -115,6 +115,8 @@ public partial class ContentBlockComponent : MSGComponentBase private int lastRenderHash; private string cachedMarkdownRenderPlanInput = string.Empty; private MarkdownRenderPlan cachedMarkdownRenderPlan = MarkdownRenderPlan.EMPTY; + private string cachedMessageTablesInput = string.Empty; + private IReadOnlyList cachedMessageTables = []; private ElementReference mathContentContainer; private string lastMathRenderSignature = string.Empty; private bool hasActiveMathContainer; @@ -125,19 +127,56 @@ public partial class ContentBlockComponent : MSGComponentBase /// /// /// We wait for the stream to finish: half an answer is nothing anybody wants in a document, - /// and waiting keeps us from searching a text which still grows with every token. Only text - /// can be exported at all; an image, for example, has no representation our formats could write. + /// 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. /// private bool CanExport => this.Content is { InitialRemoteWait: false, IsStreaming: false } && this.Content.TryGetMarkdownText(out _); /// - /// The table this block holds, if any, so that the export menu can offer it. + /// The tables this block holds so that the export menu can offer each of them. /// /// - /// Only asked for once the stream has finished, see CanExport, so the text this searches - /// is final and the search happens once per render of a settled block. + /// 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. /// - private TabularExtract? TabularExport => this.Content.TryGetMarkdownText(out var markdown) && PlainFileExport.TryExtractTabularContent(markdown, out var extract) ? extract : null; + private IReadOnlyList 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); + return this.cachedMessageTables; + } + } + + /// + /// Names one table in the export menu. + /// + /// + /// 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 of the first column does that, + /// unless it is missing or two tables happen to start with the same one, and then we count them. + /// + private string ExportLabel(MessageTable table) + { + var tables = this.MessageTables; + if (tables.DistinctBy(entry => entry.Ordinal).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)); + + return captionIsTelling + ? string.Format(this.T("Table \"{0}\" ({1})"), table.Caption, table.Format.ToFileExtension()) + : string.Format(this.T("Table {0} ({1})"), table.Ordinal, table.Format.ToFileExtension()); + } /// /// What the export offers, falling back to the chat wording when nobody named it. @@ -583,6 +622,9 @@ private async Task RemoveBlock() await this.RemoveBlockFunc(this.Content); } + /// + /// Exports the entire message. + /// private async Task ExportDocument(FileExportFormat format) { try @@ -593,15 +635,35 @@ private async Task ExportDocument(FileExportFormat format) // if (format.UsesPandoc()) await PandocExport.ToDocument(this.RustService, this.DialogService, this.EffectiveExportTitle, format, this.Content); - else - await PlainFileExport.ToFile(this.RustService, 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.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(e, "Failed to export the content, because no exporter writes the format {ExportFormat}.", format); + await this.ReportUnknownExportFormat(e, format); } } + + /// + /// Exports one table out of the message, exactly as the menu offered it. + /// + private async Task ExportTable(MessageTable table) + { + try + { + await PlainFileExport.ToFile(this.RustService, this.EffectiveExportTitle, table.Format, table.Content); + } + 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() { diff --git a/app/MindWork AI Studio/Tools/MessageTable.cs b/app/MindWork AI Studio/Tools/MessageTable.cs new file mode 100644 index 000000000..7ea9a7be5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MessageTable.cs @@ -0,0 +1,12 @@ +namespace AIStudio.Tools; + +/// +/// A table found in a message, ready to be written to a file. +/// +/// 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. +/// What the table is about, taken from its first column heading. +/// The format this content is written as. +/// The finished file content. +public sealed record MessageTable(int Ordinal, string Caption, FileExportFormat Format, string Content); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index 0102c2bd6..7b6304c8d 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -1,110 +1,175 @@ -using System.Text.RegularExpressions; -using AIStudio.Chat; +using System.Text; + using AIStudio.Tools.PluginSystem; -using AIStudio.Tools.Rust; using AIStudio.Tools.Services; +using Markdig.Extensions.Tables; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + namespace AIStudio.Tools; -public static partial class PlainFileExport +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)); /// - /// Reads the first complete Markdown code block which holds tabular data. + /// Reads every table a message holds, in the order they appear in it. /// /// - /// Models mark such a block with the name of the separator they used. The separator only - /// decides the file format where it has an established extension of its own: comma, semicolon, - /// and pipe separated data all belong into a .csv file, whereas tab separated data has .tsv. + /// 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 and offer + /// both separators, because a comma collides with the decimal comma of German numbers. 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. /// - /// The Markdown text to read. - /// The tabular data, or the default when there is none. - /// True, when the text holds tabular data. - public static bool TryExtractTabularContent(string markdown, out TabularExtract extract) + /// The Markdown text of the message. + /// The tables, or an empty list when the message holds none. + public static IReadOnlyList ExtractTables(string markdown) { - var match = TabularCodeFenceRegex().Match(markdown); - if (!match.Success) + 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); + + var tables = document.Descendants() + .Select(table => (table.Line, Contents: ToContents(table))); + + var codeBlocks = document.Descendants() + .Select(block => (block.Line, Contents: ToContents(block))); + + // + // The ordinal counts the tables of the message, not the entries of the menu, so the two + // entries of one Markdown table share it. That is what lets the menu name a table even + // when another one in the same answer starts with the same heading. + // + return tables.Concat(codeBlocks) + .Where(entry => entry.Contents.Count > 0) + .OrderBy(entry => entry.Line) + .SelectMany((entry, index) => entry.Contents.Select(content => new MessageTable(index + 1, content.Caption, content.Format, content.Content))) + .ToList(); + } + + /// + /// Turns a Markdown table into one file per separator we offer. + /// + private static IReadOnlyList<(string Caption, FileExportFormat Format, string Content)> ToContents(Table table) + { + var rows = table.OfType() + .Select(row => row.OfType().Select(ToPlainText).ToArray()) + .Where(fields => fields.Length > 0) + .ToList(); + + if (rows.Count is 0) + return []; + + var caption = rows[0].FirstOrDefault() ?? string.Empty; + + return + [ + (caption, FileExportFormat.CSV, ToDelimitedText(rows, ',')), + (caption, FileExportFormat.TSV, ToDelimitedText(rows, '\t')), + ]; + } + + /// + /// Turns a fenced code block into a file, when the model marked it as tabular data. + /// + private static IReadOnlyList<(string Caption, FileExportFormat Format, string Content)> ToContents(FencedCodeBlock block) + { + var format = block.Info?.Trim() switch { - extract = default; - return false; - } + "csv" => FileExportFormat.CSV, + "tsv" => FileExportFormat.TSV, - var format = match.Groups["separator"].Value.Equals("tsv", StringComparison.OrdinalIgnoreCase) - ? FileExportFormat.TSV - : FileExportFormat.CSV; + _ => FileExportFormat.NONE, + }; - extract = new(match.Groups["content"].Value, format); - return true; + if (format is FileExportFormat.NONE) + return []; + + var content = block.Lines.ToString(); + var separator = format is FileExportFormat.TSV ? '\t' : ','; + var firstLine = content.AsSpan(); + var lineEnd = firstLine.IndexOf('\n'); + if (lineEnd >= 0) + firstLine = firstLine[..lineEnd]; + + var separatorPosition = firstLine.IndexOf(separator); + var caption = (separatorPosition >= 0 ? firstLine[..separatorPosition] : firstLine).Trim().Trim('"').ToString(); + + return [(caption, format, content)]; + } + + private static string ToDelimitedText(IEnumerable rows, char separator) + { + var text = new StringBuilder(); + foreach (var fields in rows) + text.AppendLine(CsvWriter.ToRow(separator, fields)); + + return text.ToString(); } - [GeneratedRegex( - """ - # Matches an opening Markdown code fence, which CommonMark lets you indent by up to three - # spaces, and captures both the delimiter and the character it is made of. - ^[ ]{0,3}(?(?`|~)\k{2,}) - - # Matches the name of the separator the model used, followed by the end of the opening - # fence line. Besides comma separated values, models also produce tab, pipe, and semicolon - # separated ones. - [ \t]*(?csv|tsv|psv|ssv)[ \t]*\r?\n - - # Captures the content of the first matching fenced code block. - (?[\s\S]*?) - - # Matches the closing fence, which CommonMark lets you write longer than the opening one, - # followed by a line ending or the end of the input. - ^[ ]{0,3}\k\k*[ \t]*(?=\r?\n|$) - """, - RegexOptions.IgnoreCase | - RegexOptions.Multiline | - RegexOptions.IgnorePatternWhitespace)] - private static partial Regex TabularCodeFenceRegex(); + /// + /// Reads the text of a table cell, without the Markdown which decorates it. + /// + /// + /// A spreadsheet has no use for the asterisks around a bold number: they would keep it from + /// being completely recognized as a number. So we keep what a reader would read and drop the rest. + /// + private static string ToPlainText(TableCell cell) + { + var text = new StringBuilder(); + foreach (var inline in cell.Descendants()) + 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(); + } /// - /// Writes the given content to a plain text file and lets the user save it. + /// Writes the given text to a plain text file and lets the user save it. /// /// The Rust service, used for the save dialog. /// 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. /// The format to write. Must be a format which does not use Pandoc. - /// The content to export. + /// What to write. The caller decides whether that is the entire + /// message or one table out of it. /// True, when the file was written. - public static async Task ToFile(RustService rustService, string dialogTitle, FileExportFormat format, IContent markdownContent) + public static async Task ToFile(RustService rustService, string dialogTitle, FileExportFormat format, string fileContent) { if (format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter) throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format itself."); - // - // We work out what we are going to write before we ask for a path: when there is nothing - // to write, the user should learn that right away instead of picking a file first and - // getting an error afterward. - // - 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; - } - - string fileContent; - if (format is FileExportFormat.MARKDOWN) - fileContent = markdownText; - else if (TryExtractTabularContent(markdownText, out var tabularExtract) && tabularExtract.Format == format) - fileContent = tabularExtract.Content; - else - { - // - // The message changed between showing the menu entry and clicking it: what looked like - // a table a moment ago is gone, or it is no longer the format the user asked for. - // - LOGGER.LogWarning("Cannot export the content as {ExportFormat}, because it holds no matching table.", format); - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("This message no longer holds a table which could be exported."))); - return false; - } - var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName()); if (response.UserCancelled) { @@ -118,7 +183,7 @@ public static async Task ToFile(RustService rustService, string dialogTitl { 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) diff --git a/app/MindWork AI Studio/Tools/TabularExtract.cs b/app/MindWork AI Studio/Tools/TabularExtract.cs deleted file mode 100644 index 23c2baa53..000000000 --- a/app/MindWork AI Studio/Tools/TabularExtract.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AIStudio.Tools; - -/// -/// The tabular data a message holds. -/// -/// The data itself, without the surrounding Markdown code fence. -/// The format this data gets written as. -public readonly record struct TabularExtract(string Content, FileExportFormat Format); \ No newline at end of file From 19d2c4aaeb9280b706a20df415111e40175c240c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 20:46:39 +0200 Subject: [PATCH 29/40] Updated I18N --- app/MindWork AI Studio/Assistants/I18N/allTexts.lua | 12 ++++++------ .../plugin.lua | 12 ++++++------ .../plugin.lua | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index d1d0afbc1..adc1e300e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3178,6 +3178,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?" @@ -3187,6 +3190,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, re -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it" +-- Table \"{0}\" ({1}) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1681991003"] = "Table \\\"{0}\\\" ({1})" + -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Yes, remove it" @@ -10120,12 +10126,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "The export s -- The export failed. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "The export failed." --- This message no longer holds a table which could be exported. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2940651523"] = "This message no longer holds a table which could be exported." - --- Only text messages can be exported. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T3576815370"] = "Only text messages can be exported." - -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 141bd01bc..085edd467 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -3180,6 +3180,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?" @@ -3189,6 +3192,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Ja, ent -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Ja, neu generieren" +-- Table \"{0}\" ({1}) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1681991003"] = "Tabelle „{0}“ ({1})" + -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Ja, entferne es" @@ -10122,12 +10128,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "Der Export w -- The export failed. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "Der Export ist fehlgeschlagen." --- This message no longer holds a table which could be exported. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2940651523"] = "Diese Nachricht enthält keine Tabelle mehr, die exportiert werden könnte." - --- Only text messages can be exported. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T3576815370"] = "Nur Textnachrichten können exportiert werden." - -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 37a41ab4a..ffae9b7ec 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -3180,6 +3180,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?" @@ -3189,6 +3192,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, re -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it" +-- Table \"{0}\" ({1}) +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1681991003"] = "Table \\\"{0}\\\" ({1})" + -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Yes, remove it" @@ -10122,12 +10128,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1713926719"] = "The export s -- The export failed. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T1895034475"] = "The export failed." --- This message no longer holds a table which could be exported. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T2940651523"] = "This message no longer holds a table which could be exported." - --- Only text messages can be exported. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLAINFILEEXPORT::T3576815370"] = "Only text messages can be exported." - -- Text UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::DATAMODEL::ASSISTANTCOMPONENTTYPEEXTENSIONS::T1041509726"] = "Text" From 612f7c463976f06840f9ccc7125c632d0e28f11e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 21:07:03 +0200 Subject: [PATCH 30/40] Improved the table export to use one format and to name files after their heading --- .../Chat/ContentBlockComponent.razor.cs | 27 +++++- app/MindWork AI Studio/Tools/CsvWriter.cs | 29 ++++++ .../Tools/FileExportFormatExtensions.cs | 63 ++++++++++++- .../Tools/PlainFileExport.cs | 91 ++++++++++--------- 4 files changed, 156 insertions(+), 54 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 836a14ee6..f9fe996a1 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -117,6 +117,7 @@ public partial class ContentBlockComponent : MSGComponentBase private MarkdownRenderPlan cachedMarkdownRenderPlan = MarkdownRenderPlan.EMPTY; private string cachedMessageTablesInput = string.Empty; private IReadOnlyList cachedMessageTables = []; + private char csvSeparator = ','; private ElementReference mathContentContainer; private string lastMathRenderSignature = string.Empty; private bool hasActiveMathContainer; @@ -151,7 +152,7 @@ private IReadOnlyList MessageTables return this.cachedMessageTables; this.cachedMessageTablesInput = markdown; - this.cachedMessageTables = PlainFileExport.ExtractTables(markdown); + this.cachedMessageTables = PlainFileExport.ExtractTables(markdown, this.csvSeparator); return this.cachedMessageTables; } } @@ -161,13 +162,13 @@ private IReadOnlyList MessageTables /// /// /// 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 of the first column does that, - /// unless it is missing or two tables happen to start with the same one, and then we count them. + /// 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. /// private string ExportLabel(MessageTable table) { var tables = this.MessageTables; - if (tables.DistinctBy(entry => entry.Ordinal).Count() < 2) + if (tables.Count < 2) return table.Format.ToName(); var captionIsTelling = !string.IsNullOrWhiteSpace(table.Caption) @@ -189,6 +190,22 @@ 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() @@ -651,7 +668,7 @@ private async Task ExportTable(MessageTable table) { try { - await PlainFileExport.ToFile(this.RustService, this.EffectiveExportTitle, table.Format, table.Content); + await PlainFileExport.ToFile(this.RustService, this.EffectiveExportTitle, table.Format, table.Content, table.Caption); } catch (ArgumentOutOfRangeException e) { diff --git a/app/MindWork AI Studio/Tools/CsvWriter.cs b/app/MindWork AI Studio/Tools/CsvWriter.cs index 15c1de506..fe91f8295 100644 --- a/app/MindWork AI Studio/Tools/CsvWriter.cs +++ b/app/MindWork AI Studio/Tools/CsvWriter.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace AIStudio.Tools; /// @@ -6,6 +8,33 @@ namespace AIStudio.Tools; /// public static class CsvWriter { + /// + /// The separator a spreadsheet expects from a CSV file written for the given language. + /// + /// + /// 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. + /// + /// The IETF tag of the language, for example "de-DE". + /// The separator to write with. + 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 ','; + } + } + /// /// Joins the given fields into one row. /// diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs index 99e80c246..bfa6b5838 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -53,8 +53,8 @@ public static class FileExportFormatExtensions FileExportFormat.LATEX => TB("LaTeX (.tex)"), FileExportFormat.MARKDOWN => TB("Markdown (.md)"), FileExportFormat.HTML => TB("Webpage (.html)"), - FileExportFormat.CSV => TB("Table, comma-separated (.csv)"), - FileExportFormat.TSV => TB("Table, tab-separated (.tsv)"), + FileExportFormat.CSV => TB("Table (.csv)"), + FileExportFormat.TSV => TB("Table (.tsv)"), _ => TB("Unknown format"), }; @@ -99,12 +99,65 @@ public static class FileExportFormatExtensions /// /// /// 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 name is deliberately not translated: a file - /// name should survive being copied between systems and locales. + /// 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. /// /// The format. + /// 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. /// The suggested file name, including its extension. - public static string ToSuggestedFileName(this FileExportFormat format) => $"export{format.ToFileExtension()}"; + public static string ToSuggestedFileName(this FileExportFormat format, string? name = null) + { + var fileName = ToFileNameFragment(name); + return $"{(fileName.Length is 0 ? "export" : fileName)}{format.ToFileExtension()}"; + } + + /// + /// Turns arbitrary text into something a file system accepts as a name. + /// + /// + /// 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. + /// + 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('.'); + } /// /// Returns the filter which the save dialog offers for the format. diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index 7b6304c8d..b0759e5f5 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -20,14 +20,14 @@ public static class PlainFileExport /// /// /// 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 and offer - /// both separators, because a comma collides with the decimal comma of German numbers. 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. + /// 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. /// /// The Markdown text of the message. + /// The separator to write a Markdown table with, see CsvWriter.SeparatorFor. /// The tables, or an empty list when the message holds none. - public static IReadOnlyList ExtractTables(string markdown) + public static IReadOnlyList ExtractTables(string markdown, char separator) { if (string.IsNullOrWhiteSpace(markdown)) return []; @@ -39,28 +39,40 @@ public static IReadOnlyList ExtractTables(string markdown) // 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() + .Select(heading => (heading.Line, Text: ToPlainText(heading))) + .Where(heading => !string.IsNullOrWhiteSpace(heading.Text)) + .OrderBy(heading => heading.Line) + .ToList(); + var tables = document.Descendants
() - .Select(table => (table.Line, Contents: ToContents(table))); + .Select(table => (table.Line, Content: ToContent(table, separator))); var codeBlocks = document.Descendants() - .Select(block => (block.Line, Contents: ToContents(block))); + .Select(block => (block.Line, Content: ToContent(block))); - // - // The ordinal counts the tables of the message, not the entries of the menu, so the two - // entries of one Markdown table share it. That is what lets the menu name a table even - // when another one in the same answer starts with the same heading. - // return tables.Concat(codeBlocks) - .Where(entry => entry.Contents.Count > 0) + .Where(entry => entry.Content is not null) .OrderBy(entry => entry.Line) - .SelectMany((entry, index) => entry.Contents.Select(content => new MessageTable(index + 1, content.Caption, content.Format, content.Content))) + .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; } /// - /// Turns a Markdown table into one file per separator we offer. + /// Turns a Markdown table into a file. /// - private static IReadOnlyList<(string Caption, FileExportFormat Format, string Content)> ToContents(Table table) + private static (string Fallback, FileExportFormat Format, string Text)? ToContent(Table table, char separator) { var rows = table.OfType() .Select(row => row.OfType().Select(ToPlainText).ToArray()) @@ -68,21 +80,19 @@ public static IReadOnlyList ExtractTables(string markdown) .ToList(); if (rows.Count is 0) - return []; + return null; - var caption = rows[0].FirstOrDefault() ?? string.Empty; + var text = new StringBuilder(); + foreach (var fields in rows) + text.AppendLine(CsvWriter.ToRow(separator, fields)); - return - [ - (caption, FileExportFormat.CSV, ToDelimitedText(rows, ',')), - (caption, FileExportFormat.TSV, ToDelimitedText(rows, '\t')), - ]; + return (rows[0].FirstOrDefault() ?? string.Empty, FileExportFormat.CSV, text.ToString()); } /// /// Turns a fenced code block into a file, when the model marked it as tabular data. /// - private static IReadOnlyList<(string Caption, FileExportFormat Format, string Content)> ToContents(FencedCodeBlock block) + private static (string Fallback, FileExportFormat Format, string Text)? ToContent(FencedCodeBlock block) { var format = block.Info?.Trim() switch { @@ -93,41 +103,32 @@ public static IReadOnlyList ExtractTables(string markdown) }; if (format is FileExportFormat.NONE) - return []; + return null; var content = block.Lines.ToString(); - var separator = format is FileExportFormat.TSV ? '\t' : ','; + 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(separator); - var caption = (separatorPosition >= 0 ? firstLine[..separatorPosition] : firstLine).Trim().Trim('"').ToString(); - - return [(caption, format, content)]; - } - - private static string ToDelimitedText(IEnumerable rows, char separator) - { - var text = new StringBuilder(); - foreach (var fields in rows) - text.AppendLine(CsvWriter.ToRow(separator, fields)); + var separatorPosition = firstLine.IndexOf(blockSeparator); + var fallback = (separatorPosition >= 0 ? firstLine[..separatorPosition] : firstLine).Trim().Trim('"').ToString(); - return text.ToString(); + return (fallback, format, content); } /// - /// Reads the text of a table cell, without the Markdown which decorates it. + /// Reads the text of a table cell or a heading, without the Markdown which decorates it. /// /// /// A spreadsheet has no use for the asterisks around a bold number: they would keep it from - /// being completely recognized as a number. So we keep what a reader would read and drop the rest. + /// being recognized as a number. So we keep what a reader would read and drop the rest. /// - private static string ToPlainText(TableCell cell) + private static string ToPlainText(MarkdownObject container) { var text = new StringBuilder(); - foreach (var inline in cell.Descendants()) + foreach (var inline in container.Descendants()) switch (inline) { case CodeInline code: @@ -164,13 +165,15 @@ private static string ToPlainText(TableCell cell) /// The format to write. Must be a format which does not use Pandoc. /// What to write. The caller decides whether that is the entire /// message or one table out of it. + /// What the file is about, used to suggest a name in the save dialog. + /// Null falls back to a generic name. /// True, when the file was written. - public static async Task ToFile(RustService rustService, string dialogTitle, FileExportFormat format, string fileContent) + public static async Task 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()); + var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName(fileName)); if (response.UserCancelled) { LOGGER.LogInformation("User cancelled the save dialog."); From c433bdc7a712fdf04f2238be136491dece916e00 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 21:08:51 +0200 Subject: [PATCH 31/40] Updated I18N --- app/MindWork AI Studio/Assistants/I18N/allTexts.lua | 12 ++++++------ .../plugin.lua | 12 ++++++------ .../plugin.lua | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index adc1e300e..196d2740e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -9979,12 +9979,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The -- policy files UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files" --- Table, tab-separated (.tsv) -UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1120414862"] = "Table, tab-separated (.tsv)" - --- Table, comma-separated (.csv) -UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1539587981"] = "Table, comma-separated (.csv)" - -- OpenDocument Text (.odt), e.g. LibreOffice UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1612025407"] = "OpenDocument Text (.odt), e.g. LibreOffice" @@ -9994,12 +9988,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2233607007"] = "L -- 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" diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 085edd467..dcedf31a7 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -9981,12 +9981,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "Das -- policy files UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "Richtliniendateien" --- Table, tab-separated (.tsv) -UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1120414862"] = "Tabelle, mit Tabs getrennt (.tsv)" - --- Table, comma-separated (.csv) -UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1539587981"] = "Tabelle, mit Kommata getrennt (.csv)" - -- OpenDocument Text (.odt), e.g. LibreOffice UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1612025407"] = "OpenDocument-Text (.odt), z. B. LibreOffice" @@ -9996,12 +9990,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2233607007"] = "L -- 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" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index ffae9b7ec..88b305919 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -9981,12 +9981,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The -- policy files UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files" --- Table, tab-separated (.tsv) -UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1120414862"] = "Table, tab-separated (.tsv)" - --- Table, comma-separated (.csv) -UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1539587981"] = "Table, comma-separated (.csv)" - -- OpenDocument Text (.odt), e.g. LibreOffice UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T1612025407"] = "OpenDocument Text (.odt), e.g. LibreOffice" @@ -9996,12 +9990,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T2233607007"] = "L -- 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" From e5b3bb64c818ec3d9bd4731d2defd0b6272a11d0 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 21:18:23 +0200 Subject: [PATCH 32/40] Fixed the table export ignoring the heading above a table --- app/MindWork AI Studio/Tools/PlainFileExport.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index b0759e5f5..d3e56cad2 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -127,8 +127,18 @@ private static (string Fallback, FileExportFormat Format, string Text)? ToConten /// 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() ?? [] + : container.Descendants(); + var text = new StringBuilder(); - foreach (var inline in container.Descendants()) + foreach (var inline in inlines) switch (inline) { case CodeInline code: From fc0b22b51a0d7cb5e1282f66a56635275895ac2b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 21:26:30 +0200 Subject: [PATCH 33/40] Fixed the missing translation for the table export menu --- app/MindWork AI Studio/Assistants/I18N/allTexts.lua | 3 --- app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs | 7 ++++++- .../de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua | 3 --- .../en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua | 3 --- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 196d2740e..dd0da5c88 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3190,9 +3190,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, re -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it" --- Table \"{0}\" ({1}) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1681991003"] = "Table \\\"{0}\\\" ({1})" - -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Yes, remove it" diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index f9fe996a1..2a1324b4a 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -174,8 +174,13 @@ private string ExportLabel(MessageTable table) 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 - ? string.Format(this.T("Table \"{0}\" ({1})"), table.Caption, table.Format.ToFileExtension()) + ? $"{table.Caption} ({table.Format.ToFileExtension()})" : string.Format(this.T("Table {0} ({1})"), table.Ordinal, table.Format.ToFileExtension()); } diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index dcedf31a7..b09132173 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -3192,9 +3192,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Ja, ent -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Ja, neu generieren" --- Table \"{0}\" ({1}) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1681991003"] = "Tabelle „{0}“ ({1})" - -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Ja, entferne es" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 88b305919..21a8da6d5 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -3192,9 +3192,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, re -- Yes, regenerate it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, regenerate it" --- Table \"{0}\" ({1}) -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1681991003"] = "Table \\\"{0}\\\" ({1})" - -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Yes, remove it" From f0b84942f217a9a87f5384081f362de0991100ae Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 21:46:12 +0200 Subject: [PATCH 34/40] Improved the Pandoc export so it can also convert without any dialogs --- .../Chat/ContentBlockComponent.razor.cs | 5 +- app/MindWork AI Studio/Tools/PandocExport.cs | 133 +++++++++--------- .../Services/PandocAvailabilityService.cs | 2 +- 3 files changed, 75 insertions(+), 65 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 2a1324b4a..fdb586d1e 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -110,6 +110,9 @@ public partial class ContentBlockComponent : MSGComponentBase [Inject] private ILogger Logger { get; init; } = null!; + [Inject] + private PandocAvailabilityService PandocAvailability { get; init; } = null!; + private bool HideContent { get; set; } private bool hasRenderHash; private int lastRenderHash; @@ -656,7 +659,7 @@ private async Task ExportDocument(FileExportFormat format) // here which would fall out of sync with the one in FileExportFormatExtensions. // if (format.UsesPandoc()) - await PandocExport.ToDocument(this.RustService, this.DialogService, this.EffectiveExportTitle, format, this.Content); + 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); } diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs index d80f82228..db8040178 100644 --- a/app/MindWork AI Studio/Tools/PandocExport.cs +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -1,11 +1,9 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Text; using AIStudio.Chat; -using AIStudio.Dialogs; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Services; -using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Tools; @@ -16,40 +14,24 @@ public static class PandocExport private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport)); /// - /// Converts the given content to a document using Pandoc and lets the user save it. + /// Converts the given Markdown text into a document at the given path. /// - /// The Rust service, used for the save dialog and for Pandoc. - /// The dialog service, used to offer the Pandoc installation. - /// 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. + /// + /// 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. + /// + /// The Rust service, used to build the Pandoc call. + /// The Markdown text to convert. + /// Where to write the document. /// The format to write. Must be a format which uses Pandoc. - /// The content to export. + /// The token to cancel the conversion. /// True, when the document was written. - public static async Task ToDocument(RustService rustService, IDialogService dialogService, string dialogTitle, FileExportFormat format, IContent markdownContent) + public static async Task ConvertAsync(RustService rustService, string markdownText, string targetFilePath, FileExportFormat format, CancellationToken token = default) { - if (!format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter) + if (!format.UsesPandoc()) 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); - var tempMarkdownFilePath = string.Empty; try { @@ -58,28 +40,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic // 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)); - - // Ensure that Pandoc is installed and ready: - var pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: false); - if (!pandocState.IsAvailable) - { - var dialogParameters = new DialogParameters - { - { x => x.ShowInitialResultInSnackbar, false }, - }; - - var dialogReference = await dialogService.ShowAsync(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 this export."))); - return false; - } - } + await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText, new UTF8Encoding(false), token); // Call Pandoc to create the document: var pandoc = await PandocProcessBuilder @@ -87,7 +48,7 @@ public static async Task ToDocument(RustService rustService, IDialogServic .UseStandaloneMode() .WithInputFormat("gfm+emoji+tex_math_dollars") .WithOutputFormat(format.ToPandocOutputFormat()) - .WithOutputFile(response.SaveFilePath) + .WithOutputFile(targetFilePath) .WithInputFile(tempMarkdownFilePath) .BuildAsync(rustService); @@ -99,30 +60,26 @@ public static async Task ToDocument(RustService rustService, IDialogServic } // 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("The export failed."))); return false; } - LOGGER.LogInformation("Pandoc conversion successful."); - await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("The export succeeded."))); - + LOGGER.LogInformation("Pandoc conversion to {ExportFormat} successful.", format); 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."))); + LOGGER.LogError(ex, "Error during {ExportFormat} conversion.", format); return false; } finally @@ -141,4 +98,54 @@ public static async Task ToDocument(RustService rustService, IDialogServic } } } + + /// + /// Converts the given content to a document using Pandoc and lets the user save it. + /// + /// The Rust service, used for the save dialog and for Pandoc. + /// Makes sure Pandoc is there and offers its installation. + /// 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. + /// The format to write. Must be a format which uses Pandoc. + /// The content to export. + /// True, when the document was written. + public static async Task 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; + } } diff --git a/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs b/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs index 14a269088..6ac09a20c 100644 --- a/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs +++ b/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs @@ -54,7 +54,7 @@ public async Task EnsureAvailabilityAsync(bool showSuccessMe 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."))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("AI Studio needs Pandoc for this, but it is not available."))); } } From ff6d13440946366670a928f85b1022d923f1ccf8 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 21:54:32 +0200 Subject: [PATCH 35/40] Improved the file loading to use the shared Pandoc availability service --- .../AssistantPromptOptimizer.razor.cs | 6 +++- .../Components/ReadFileContent.razor.cs | 2 +- .../Dialogs/DocumentCheckDialog.razor.cs | 10 +++---- .../Services/PandocAvailabilityService.cs | 8 +++-- app/MindWork AI Studio/Tools/UserFile.cs | 29 +++++-------------- 5 files changed, 25 insertions(+), 30 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs b/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs index 344953cbc..63238103e 100644 --- a/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs +++ b/app/MindWork AI Studio/Assistants/PromptOptimizer/AssistantPromptOptimizer.razor.cs @@ -5,6 +5,7 @@ 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 Tools.Components.PROMPT_OPTIMIZER_ASSISTANT; protected override string Title => T("Prompt Optimizer"); @@ -581,7 +585,7 @@ private async Task LoadCustomPromptGuidelineContentAsync(FileAttachment fileAtta 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 diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs index 3606ba4a3..0895e133d 100644 --- a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs +++ b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs @@ -344,7 +344,7 @@ private async Task LoadFileIfValid(string filePath) 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) diff --git a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor.cs index 6eb83a007..e6e7b77c2 100644 --- a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor.cs @@ -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 Logger { get; init; } = null!; + + [Inject] + private PandocAvailabilityService PandocAvailability { get; init; } = null!; protected override async Task OnInitializedAsync() { @@ -97,7 +97,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) 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; diff --git a/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs b/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs index 6ac09a20c..81acf88a5 100644 --- a/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs +++ b/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs @@ -27,8 +27,11 @@ public sealed class PandocAvailabilityService(RustService rustService, IDialogSe /// /// Whether to show a success message if Pandoc is available. /// Whether to show the installation dialog if Pandoc is not available. + /// 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. /// The Pandoc installation state. - public async Task EnsureAvailabilityAsync(bool showSuccessMessage = false, bool showDialog = true) + public async Task 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 async Task EnsureAvailabilityAsync(bool showSuccessMe if (!pandocState.IsAvailable) { this.Logger.LogError("Pandoc is not available after installation attempt."); - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("AI Studio needs Pandoc for this, but it is not available."))); + if (showErrorMessage) + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("AI Studio needs Pandoc for this, but it is not available."))); } } diff --git a/app/MindWork AI Studio/Tools/UserFile.cs b/app/MindWork AI Studio/Tools/UserFile.cs index 71836a69a..963ec6550 100644 --- a/app/MindWork AI Studio/Tools/UserFile.cs +++ b/app/MindWork AI Studio/Tools/UserFile.cs @@ -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 /// /// The full path to the file to be read. Must not be null or empty. /// Rust service used to read file content. - /// Dialogservice used to display the Pandoc installation dialog if needed. + /// Makes sure Pandoc is there and offers its installation. /// Cancels the extraction when the caller no longer needs the content. /// The result of reading the file. - public static async Task LoadFileData(string filePath, RustService rustService, IDialogService dialogService, CancellationToken token = default) + public static async Task LoadFileData(string filePath, RustService rustService, PandocAvailabilityService pandocAvailability, CancellationToken token = default) { if (string.IsNullOrEmpty(filePath)) { @@ -41,24 +39,13 @@ public static async Task LoadFileData(string filePath, Rus // 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 - { - { x => x.ShowInitialResultInSnackbar, false }, - }; - - var dialogReference = await dialogService.ShowAsync(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."); } } From 1f6ce4fcd9c91991957c03859b473681f2351941 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 21:57:30 +0200 Subject: [PATCH 36/40] Changed the batch output mode to be independent of the file format --- .../BatchProcessing/AssistantBatchProcessing.razor | 2 +- .../AssistantBatchProcessing.razor.Run.cs | 2 +- .../BatchProcessing/AssistantBatchProcessing.razor.cs | 4 ++-- .../BatchProcessing/BatchProcessingOutputMode.cs | 9 +++++++-- .../BatchProcessingOutputModeExtensions.cs | 2 +- app/MindWork AI Studio/Plugins/configuration/plugin.lua | 4 ++-- .../Settings/DataModel/DataBatchProcessing.cs | 2 +- 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor index 0ef4092db..921f1d6a1 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor @@ -115,7 +115,7 @@ else } -@if (this.outputMode is BatchProcessingOutputMode.MARKDOWN_FILES) +@if (this.outputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES) { @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.") diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs index 60e21b8d9..b7bbebe4f 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs @@ -211,7 +211,7 @@ private async Task ProcessOneFileAsync(BatchProcessingFileResult fileResult, str } fileResult.ResultText = aiAnswer; - if (this.outputMode is BatchProcessingOutputMode.MARKDOWN_FILES) + if (this.outputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES) { try { diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs index 1811e6ab1..11b4d2351 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs @@ -87,7 +87,7 @@ protected override async Task OnDefaultsAppliedAsync() 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 string resultColumnHeader = string.Empty; private string csvFileName = string.Empty; private BatchProcessingCsvSeparator csvSeparator = BatchProcessingCsvSeparator.SEMICOLON; @@ -160,7 +160,7 @@ private void ApplyFormDefaults() this.freePrompt = string.Empty; this.promptFilePath = string.Empty; this.selectedPolicy = null; - this.outputMode = BatchProcessingOutputMode.MARKDOWN_FILES; + this.outputMode = BatchProcessingOutputMode.INDIVIDUAL_FILES; this.resultColumnHeader = string.Empty; this.csvFileName = string.Empty; this.csvSeparator = BatchProcessingCsvSeparator.SEMICOLON; diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputMode.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputMode.cs index 021031949..d7730b2ef 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputMode.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputMode.cs @@ -6,9 +6,14 @@ namespace AIStudio.Assistants.BatchProcessing; public enum BatchProcessingOutputMode { /// - /// One Markdown result file per processed document. + /// One result file per processed document, written in the chosen file format. /// - MARKDOWN_FILES, + /// + /// 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. + /// + INDIVIDUAL_FILES, /// /// A CSV results table, where each AI answer becomes one row. The content of diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputModeExtensions.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputModeExtensions.cs index 234d0db41..3bdfe402c 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputModeExtensions.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/BatchProcessingOutputModeExtensions.cs @@ -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"), diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 5317d8303..76ee7bc03 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -492,8 +492,8 @@ 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" -- CONFIG["SETTINGS"]["DataBatchProcessing.CsvFileName"] = "batch-results.csv" -- CONFIG["SETTINGS"]["DataBatchProcessing.ResultColumnHeader"] = "Result" -- Allowed CSV separator values are: COMMA, SEMICOLON, PIPE, TAB, CUSTOM diff --git a/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs b/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs index 6400ecf3c..c421b1f9b 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs @@ -42,7 +42,7 @@ public DataBatchProcessing() : this(null) 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); public string CsvFileName { get; set; } = ManagedConfiguration.Register(configSelection, value => value.CsvFileName, string.Empty); From 3a84606a2d38f34fd413c9710cd596b5e046a3f1 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 22:02:45 +0200 Subject: [PATCH 37/40] Added a file format option for the individual batch result files --- .../BatchProcessing/AssistantBatchProcessing.razor | 11 ++++++++++- .../AssistantBatchProcessing.razor.Session.cs | 3 +++ .../BatchProcessing/AssistantBatchProcessing.razor.cs | 3 +++ .../Settings/SettingsDialogBatchProcessing.razor | 6 +++++- .../Settings/SettingsDialogBatchProcessing.razor.cs | 6 ++++++ .../Plugins/configuration/plugin.lua | 8 ++++++++ .../Settings/DataModel/DataBatchProcessing.cs | 10 ++++++++++ .../Tools/FileExportFormatExtensions.cs | 9 +++++++++ .../Tools/PluginSystem/PluginConfiguration.cs | 1 + 9 files changed, 55 insertions(+), 2 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor index 921f1d6a1..b56ec8253 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor @@ -117,8 +117,17 @@ else @if (this.outputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES) { + + @foreach (var format in FileExportFormatExtensions.ANSWER_FORMATS) + { + + @format.ToName() + + } + + - @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())) } else diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Session.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Session.cs index 301959c14..19d945ac6 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Session.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Session.cs @@ -16,6 +16,7 @@ public partial class AssistantBatchProcessing private static readonly AssistantSessionStateKey PROMPT_FILE_LOAD_ISSUE_STATE_KEY = new(nameof(promptFileLoadIssue)); private static readonly AssistantSessionStateKey SELECTED_POLICY_STATE_KEY = new(nameof(selectedPolicy)); private static readonly AssistantSessionStateKey OUTPUT_MODE_STATE_KEY = new(nameof(outputMode)); + private static readonly AssistantSessionStateKey RESULT_FILE_FORMAT_STATE_KEY = new(nameof(resultFileFormat)); private static readonly AssistantSessionStateKey RESULT_COLUMN_HEADER_STATE_KEY = new(nameof(resultColumnHeader)); private static readonly AssistantSessionStateKey CSV_FILE_NAME_STATE_KEY = new(nameof(csvFileName)); private static readonly AssistantSessionStateKey CSV_SEPARATOR_STATE_KEY = new(nameof(csvSeparator)); @@ -43,6 +44,7 @@ protected override void CaptureCustomAssistantSessionState(AssistantSessionState 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 @@ protected override void RestoreCustomAssistantSessionState(AssistantSessionState 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); diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs index 11b4d2351..f20049cb2 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs @@ -88,6 +88,7 @@ protected override async Task OnDefaultsAppliedAsync() private string promptFileLoadIssue = string.Empty; private DataDocumentAnalysisPolicy? selectedPolicy; 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; @@ -161,6 +162,7 @@ private void ApplyFormDefaults() this.promptFilePath = string.Empty; this.selectedPolicy = null; this.outputMode = BatchProcessingOutputMode.INDIVIDUAL_FILES; + this.resultFileFormat = FileExportFormat.MARKDOWN; this.resultColumnHeader = string.Empty; this.csvFileName = string.Empty; this.csvSeparator = BatchProcessingCsvSeparator.SEMICOLON; @@ -180,6 +182,7 @@ private void ApplyFormDefaults() 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; diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor index 8e1374b54..4b2830f3d 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor @@ -43,7 +43,11 @@ @T("Output") - @if (this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode is BatchProcessingOutputMode.TABLE_ONLY) + @if (this.SettingsManager.ConfigurationData.BatchProcessing.OutputMode is BatchProcessingOutputMode.INDIVIDUAL_FILES) + { + + } + else { diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor.cs index eef566c6d..763477df4 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor.cs @@ -61,6 +61,12 @@ .. Enum .Select(value => new ConfigurationSelectData(value.Name(), value)) ]; + private static IReadOnlyList> ResultFileFormatData => + [ + .. FileExportFormatExtensions.ANSWER_FORMATS + .Select(value => new ConfigurationSelectData(value.ToName(), value)) + ]; + private IReadOnlyList> CsvSeparatorData => [ .. Enum diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 76ee7bc03..dec6d4d00 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -494,6 +494,13 @@ CONFIG["SETTINGS"] = {} -- Configure the default output mode. -- 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 diff --git a/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs b/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs index c421b1f9b..7d468122b 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs @@ -44,6 +44,16 @@ public DataBatchProcessing() : this(null) public BatchProcessingOutputMode OutputMode { get; set; } = ManagedConfiguration.Register(configSelection, value => value.OutputMode, BatchProcessingOutputMode.INDIVIDUAL_FILES); + /// + /// The file format of the individual result files, one per processed document. + /// + /// + /// 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. + /// + 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); public string ResultColumnHeader { get; set; } = ManagedConfiguration.Register(configSelection, value => value.ResultColumnHeader, string.Empty); diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs index bfa6b5838..d47ef0e8f 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -41,6 +41,15 @@ public static class FileExportFormatExtensions FileExportFormat.HTML, ]; + /// + /// Every format an entire answer can be written as. + /// + /// + /// 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. + /// + public static readonly IReadOnlyList ANSWER_FORMATS = [..DOCUMENT_FORMATS, ..TEXT_FORMATS]; + /// /// Returns the name of the format as shown to the user. /// diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 775b89bc3..97b1d18e3 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -351,6 +351,7 @@ private bool TryProcessConfiguration(bool dryRun, out string message) 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); From d014fec345911481bb68c54703dda8dcbcad33e4 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 22:06:06 +0200 Subject: [PATCH 38/40] Added the chosen file format to the individual batch result files --- ...istantBatchProcessing.razor.Persistence.cs | 11 ++++--- .../AssistantBatchProcessing.razor.Run.cs | 32 +++++++++++++++++-- .../AssistantBatchProcessing.razor.cs | 6 +++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs index 8ed9b85b2..b150afb56 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs @@ -71,8 +71,8 @@ public partial class AssistantBatchProcessing /// /// 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. /// private bool CanRestoreFromPreviousRun(string relativePath, string resolvedOutputDirectory, Dictionary previousLog, Dictionary previousResults, out BatchProcessingLogEntry? logEntry) @@ -232,7 +232,7 @@ private async Task> ReadPreviousResultsAsync(string r } /// - /// 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. /// /// /// Two documents of the same run may share their name and differ only in @@ -242,13 +242,14 @@ private async Task> ReadPreviousResultsAsync(string r /// 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++; } diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs index b7bbebe4f..cf8e9dbe1 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Globalization; -using System.Text; namespace AIStudio.Assistants.BatchProcessing; @@ -14,6 +13,19 @@ private async Task StartBatchProcessingAsync() 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 @@ private void PrepareFileResults(string resolvedOutputDirectory, IReadOnlyList Date: Sun, 30 Aug 2026 22:09:05 +0200 Subject: [PATCH 39/40] Updated I18N --- .../Assistants/I18N/allTexts.lua | 40 +++++++++++-------- .../plugin.lua | 40 +++++++++++-------- .../plugin.lua | 40 +++++++++++-------- 3 files changed, 69 insertions(+), 51 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index dd0da5c88..e2036b10e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -382,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." @@ -499,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}" @@ -586,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." @@ -640,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" @@ -6871,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?" @@ -6928,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." @@ -10105,15 +10120,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't ins -- 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." --- Pandoc is required for this export. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3548302476"] = "Pandoc is required for this export." - -- Only text messages can be exported. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Only text messages can be exported." @@ -10918,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." @@ -11137,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." diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index b09132173..0767e607e 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -384,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." @@ -501,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}" @@ -588,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." @@ -642,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" @@ -6873,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?" @@ -6930,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." @@ -10107,15 +10122,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio konnte Pando -- 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." --- Pandoc is required for this export. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3548302476"] = "Für diesen Export ist Pandoc erforderlich." - -- Only text messages can be exported. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Nur Textnachrichten können exportiert werden." @@ -10920,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." @@ -11139,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." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 21a8da6d5..edad7a023 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -384,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." @@ -501,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}" @@ -588,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." @@ -642,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" @@ -6873,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?" @@ -6930,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." @@ -10107,15 +10122,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't ins -- 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." --- Pandoc is required for this export. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3548302476"] = "Pandoc is required for this export." - -- Only text messages can be exported. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3576815370"] = "Only text messages can be exported." @@ -10920,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." @@ -11139,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." From de6568c11824f04642a92d0f3aaf987508637004 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 30 Aug 2026 22:10:34 +0200 Subject: [PATCH 40/40] Updated changelog --- app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index a565ce674..c3e1223ed 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -9,7 +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 a table, you can save just that table as a spreadsheet file, 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 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.