diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs
index 906c0ee1..7ffbab88 100644
--- a/app/MindWork AI Studio/Chat/ContentText.cs
+++ b/app/MindWork AI Studio/Chat/ContentText.cs
@@ -5,6 +5,7 @@ using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.RAG.RAGProcesses;
+using AIStudio.Tools.Rust;
namespace AIStudio.Chat;
@@ -282,70 +283,84 @@ public sealed class ContentText : IContent
// Only proceed if there are existing, allowed documents:
if (existingDocuments.Count > 0)
{
- // Check Pandoc availability once before processing file attachments
- var pandocState = await Pandoc.CheckAvailabilityAsync(Program.RUST_SERVICE, showMessages: true, showSuccessMessage: false);
-
- if (!pandocState.IsAvailable)
- LOGGER.LogWarning("File attachments could not be processed because Pandoc is not available.");
- else if (!pandocState.CheckWasSuccessful)
- LOGGER.LogWarning("File attachments could not be processed because the Pandoc version check failed.");
- else
+ //
+ // Pandoc is only needed for the few formats we convert with it. PDFs, text files,
+ // spreadsheets, and presentations are read by the runtime itself, so a missing
+ // Pandoc installation must not stop them.
+ //
+ var pandocIsUsable = true;
+ if (existingDocuments.Any(document => FileTypes.RequiresPandoc(document.FilePath)))
{
+ var pandocState = await Pandoc.CheckAvailabilityAsync(Program.RUST_SERVICE, showMessages: true, showSuccessMessage: false);
+ pandocIsUsable = pandocState is { IsAvailable: true, CheckWasSuccessful: true };
+
+ if (!pandocState.IsAvailable)
+ LOGGER.LogWarning("File attachments which need Pandoc could not be processed because Pandoc is not available.");
+ else if (!pandocState.CheckWasSuccessful)
+ LOGGER.LogWarning("File attachments which need Pandoc could not be processed because the Pandoc version check failed.");
+ }
+
+ //
+ // The document blocks are collected separately, so we only announce attached
+ // files when at least one of them could actually be read. Announcing files we
+ // then hand over as empty blocks makes the AI answer about an empty document.
+ //
+ var documentBlocks = new StringBuilder();
+ foreach(var document in existingDocuments)
+ {
+ if (document.IsForbidden)
+ {
+ LOGGER.LogWarning("File attachment '{FilePath}' has a forbidden file type and will be skipped.", document.FilePath);
+ continue;
+ }
+
+ if (!pandocIsUsable && FileTypes.RequiresPandoc(document.FilePath))
+ {
+ LOGGER.LogWarning("The file attachment '{FilePath}' needs Pandoc and will be skipped.", document.FilePath);
+ await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, string.Format(TB("The file '{0}' needs Pandoc to be read and was not sent."), document.FileName)));
+ continue;
+ }
+
+ var extraction = await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue);
+ if (!extraction.HasUsableContent)
+ {
+ LOGGER.LogError("Reading the file attachment '{FilePath}' failed and it will not be sent: code={ErrorCode}, message='{ErrorMessage}'.", document.FilePath, extraction.ErrorCode, extraction.ErrorMessage);
+ await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, extraction.ToUserMessage(document.FileName)));
+ continue;
+ }
+
//
- // The document blocks are collected separately, so we only announce attached
- // files when at least one of them could actually be read. Announcing files we
- // then hand over as empty blocks makes the AI answer about an empty document.
+ // The file is usable, but we lost parts of it. The user has to know which
+ // parts are missing, because the answer will be based on the rest.
//
- var documentBlocks = new StringBuilder();
- foreach(var document in existingDocuments)
+ if (extraction.Outcome is FileExtractionOutcome.PARTIAL)
{
- if (document.IsForbidden)
- {
- LOGGER.LogWarning("File attachment '{FilePath}' has a forbidden file type and will be skipped.", document.FilePath);
- continue;
- }
-
- var extraction = await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue);
- if (!extraction.HasUsableContent)
- {
- LOGGER.LogError("Reading the file attachment '{FilePath}' failed and it will not be sent: code={ErrorCode}, message='{ErrorMessage}'.", document.FilePath, extraction.ErrorCode, extraction.ErrorMessage);
- await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, extraction.ToUserMessage(document.FileName)));
- continue;
- }
-
- //
- // The file is usable, but we lost parts of it. The user has to know which
- // parts are missing, because the answer will be based on the rest.
- //
- if (extraction.Outcome is FileExtractionOutcome.PARTIAL)
- {
- LOGGER.LogWarning("Parts of the file attachment '{FilePath}' could not be read: pages={FailedPages}.", document.FilePath, string.Join(", ", extraction.FailedPages));
- await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Description, extraction.ToPartialUserMessage(document.FileName)));
- }
-
- documentBlocks.AppendLine();
- documentBlocks.AppendLine("---------------------------------------");
- documentBlocks.AppendLine($"File path: {document.FilePath}");
- documentBlocks.AppendLine("File content:");
- documentBlocks.AppendLine("````");
- documentBlocks.AppendLine(extraction.Content);
- documentBlocks.AppendLine("````");
+ LOGGER.LogWarning("Parts of the file attachment '{FilePath}' could not be read: pages={FailedPages}.", document.FilePath, string.Join(", ", extraction.FailedPages));
+ await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Description, extraction.ToPartialUserMessage(document.FileName)));
}
- if (documentBlocks.Length > 0)
- {
- sb.AppendLine();
- sb.AppendLine("The following files are attached to this message:");
- sb.Append(documentBlocks);
- }
+ documentBlocks.AppendLine();
+ documentBlocks.AppendLine("---------------------------------------");
+ documentBlocks.AppendLine($"File path: {document.FilePath}");
+ documentBlocks.AppendLine("File content:");
+ documentBlocks.AppendLine("````");
+ documentBlocks.AppendLine(extraction.Content);
+ documentBlocks.AppendLine("````");
+ }
- var numImages = normalizedAttachments.Count(x => x is { IsImage: true, Exists: true });
- if (numImages > 0)
- {
- sb.AppendLine();
- sb.AppendLine($"Additionally, there are {numImages} image file(s) attached to this message. ");
- sb.AppendLine("Please consider them as part of the message content and use them to answer accordingly.");
- }
+ if (documentBlocks.Length > 0)
+ {
+ sb.AppendLine();
+ sb.AppendLine("The following files are attached to this message:");
+ sb.Append(documentBlocks);
+ }
+
+ var numImages = normalizedAttachments.Count(x => x is { IsImage: true, Exists: true });
+ if (numImages > 0)
+ {
+ sb.AppendLine();
+ sb.AppendLine($"Additionally, there are {numImages} image file(s) attached to this message. ");
+ sb.AppendLine("Please consider them as part of the message content and use them to answer accordingly.");
}
}
}
diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
index 4b4274fd..9849d102 100644
--- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
+++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
@@ -443,23 +443,31 @@ public partial class AttachDocuments : MSGComponentBase
var mediaPaths = existingPaths.Where(IsTranscribableMedia).ToList();
var regularPaths = existingPaths.Except(mediaPaths).ToList();
- var canAddRegularFiles = true;
- if (regularPaths.Count > 0)
+ //
+ // Only the formats we convert with Pandoc depend on a Pandoc installation. Everything
+ // else, PDFs in particular, is read by the Rust runtime itself, so those files must stay
+ // attachable without Pandoc.
+ //
+ var canAddPandocFiles = true;
+ if (regularPaths.Any(FileTypes.RequiresPandoc))
{
var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync(
showSuccessMessage: false,
showDialog: true);
- canAddRegularFiles = pandocState.IsAvailable;
+ canAddPandocFiles = pandocState.IsAvailable;
}
foreach (var path in regularPaths)
{
- if (!canAddRegularFiles)
- break;
+ if (!canAddPandocFiles && FileTypes.RequiresPandoc(path))
+ {
+ this.Logger.LogWarning("The file '{Path}' needs Pandoc and was not attached.", path);
+ continue;
+ }
if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider))
continue;
-
+
this.DocumentPaths.Add(FileAttachment.FromPath(path));
}
diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs
index 038833f0..51cc0c1e 100644
--- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs
+++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs
@@ -84,6 +84,23 @@ public static class FileTypes
public static readonly FileTypeFilter EXECUTABLES = FileTypeFilter.Leaf(TB("Executable"), "exe", "app", "bin", "appimage");
public static readonly FileTypeFilter PLUGIN_ARCHIVE = FileTypeFilter.Leaf(TB("Plugin archive"), PluginArchive.PLUGIN_FILE_EXTENSION.TrimStart('.'), "zip");
+ ///
+ /// The file types AI Studio converts using Pandoc.
+ ///
+ ///
+ /// This is not a user-selectable type, it mirrors the formats the Rust runtime hands to
+ /// Pandoc. Every other document type is read by the runtime itself, so it must never depend
+ /// on a Pandoc installation. The name is not localized because it is never shown.
+ ///
+ private static readonly FileTypeFilter PANDOC_CONVERTED = FileTypeFilter.Leaf("Pandoc conversion", "docx", "odt", "html", "htm");
+
+ ///
+ /// Determines whether reading the given file needs Pandoc.
+ ///
+ /// The path of the file to check.
+ /// True, when reading the file needs Pandoc.
+ public static bool RequiresPandoc(string filePath) => IsAllowedPath(filePath, PANDOC_CONVERTED);
+
public static FileTypeFilter? AsOneFileType(params FileTypeFilter[]? types)
{
if (types == null || types.Length == 0)