diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index 8bd9abc5..06208e6f 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -716,7 +716,21 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore } + else if (this.loadFailureMessage is not null) + { + + @this.loadFailureMessage + + } else { diff --git a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor.cs index 4bf306f1..d652660d 100644 --- a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor.cs @@ -20,6 +20,11 @@ public partial class DocumentCheckDialog : MSGComponentBase [Parameter] public string FileContent { get; set; } = string.Empty; + + /// + /// Set when reading the file failed, so the dialog shows the reason instead of empty content. + /// + private string? loadFailureMessage; [Inject] private RustService RustService { get; init; } = null!; @@ -38,14 +43,22 @@ public partial class DocumentCheckDialog : MSGComponentBase { if (!this.Document.IsImage) { - var fileContent = await UserFile.LoadFileData(this.Document.FilePath, this.RustService, this.DialogService); - this.FileContent = fileContent; + var extraction = await UserFile.LoadFileData(this.Document.FilePath, this.RustService, this.DialogService); + this.FileContent = extraction.Content; + + // + // This dialog exists so the user can check what we hand to the AI. Showing an + // empty document when reading the file failed would answer that question wrong. + // + if (!extraction.HasUsableContent) + this.loadFailureMessage = extraction.ToUserMessage(this.Document.FileName); } } catch (Exception ex) { this.Logger.LogError(ex, "Failed to load file content from '{FilePath}'", this.Document); this.FileContent = string.Empty; + this.loadFailureMessage = FileExtractionErrorCode.INTERNAL.ToUserMessage(this.Document.FileName); } this.StateHasChanged(); diff --git a/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs b/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs index 9383bcc0..1ed0fa4a 100644 --- a/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs +++ b/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs @@ -37,6 +37,11 @@ public enum FileExtractionErrorCode // Codes reported by the app itself: // + /// + /// Reading the file needs Pandoc, which is not available. + /// + PANDOC_UNAVAILABLE, + /// /// The runtime answered with an unsuccessful HTTP status. /// diff --git a/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs b/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs index f8da220c..e1297678 100644 --- a/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs @@ -20,7 +20,19 @@ internal static class FileExtractionResultExtensions /// The extraction result. /// The name of the file, as shown to the user. /// The localized message. - internal static string ToUserMessage(this FileExtractionResult result, string fileName) => string.Format(ToUserMessageFormat(result.ErrorCode), fileName); + internal static string ToUserMessage(this FileExtractionResult result, string fileName) => result.ErrorCode.ToUserMessage(fileName); + + /// + /// Gets the localized message which explains why a file could not be read. + /// + /// + /// This overload exists for the places which know the reason before an extraction was even + /// attempted, so both ways of skipping a file tell the user the same thing. + /// + /// The stable failure code. + /// The name of the file, as shown to the user. + /// The localized message. + internal static string ToUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName); /// /// Gets the localized message for a file which was read, but lost some of its pages. @@ -45,6 +57,8 @@ internal static class FileExtractionResultExtensions FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely."), FileExtractionErrorCode.PDF_ENCRYPTED => TB("The file '{0}' is protected and could not be opened, so it was not sent."), FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("AI Studio was not able to start its PDF engine, so the file '{0}' was not sent."), + + FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Reading the file '{0}' needs Pandoc, which is not available, so the file was not sent."), FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No text could be read from the file '{0}', so it was not sent. The file might consist of scanned images without a text layer."), FileExtractionErrorCode.NO_CONTENT => TB("The file '{0}' did not provide any content and was not sent."), FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("The file type of '{0}' could not be determined, so the file was not sent."), diff --git a/app/MindWork AI Studio/Tools/UserFile.cs b/app/MindWork AI Studio/Tools/UserFile.cs index 5002cd03..6dc73ded 100644 --- a/app/MindWork AI Studio/Tools/UserFile.cs +++ b/app/MindWork AI Studio/Tools/UserFile.cs @@ -1,5 +1,6 @@ using AIStudio.Dialogs; using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using DialogOptions = AIStudio.Dialogs.DialogOptions; @@ -14,38 +15,64 @@ public static class UserFile /// /// Attempts to load the content of a file at the specified path, ensuring Pandoc is installed and available before proceeding. /// + /// + /// This is the one place which reports a failed load to the user, so callers neither have to + /// repeat that nor may they treat a failure as an empty file. + /// /// 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. - public static async Task LoadFileData(string filePath, RustService rustService, IDialogService dialogService) + /// The result of reading the file. + public static async Task LoadFileData(string filePath, RustService rustService, IDialogService dialogService) { if (string.IsNullOrEmpty(filePath)) { LOGGER.LogError("Can't load from an empty or null file path."); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("The file path is null or empty and the file therefore can not be loaded."))); + return FileExtractionResult.Failed(FileExtractionErrorCode.INVALID_REQUEST, "The file path is null or empty."); } - - // Ensure that Pandoc is installed and ready: - var pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: false); - if (!pandocState.IsAvailable) + + var fileName = Path.GetFileName(filePath); + + // + // Ensure that Pandoc is installed and ready. This is only needed for the formats we + // convert with it: PDFs and the other document types are read by the Rust runtime itself. + // + if (FileTypes.RequiresPandoc(filePath)) { - 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); + var pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: false); if (!pandocState.IsAvailable) { - 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."))); + 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."); + } } } - - var fileContent = await rustService.ReadArbitraryFileData(filePath, int.MaxValue); - return fileContent.Content; + + var result = await rustService.ReadArbitraryFileData(filePath, int.MaxValue); + if (!result.HasUsableContent) + { + LOGGER.LogError("Reading the file '{FilePath}' failed: code={ErrorCode}, message='{ErrorMessage}'.", filePath, result.ErrorCode, result.ErrorMessage); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, result.ToUserMessage(fileName))); + } + else if (result.Outcome is FileExtractionOutcome.PARTIAL) + { + LOGGER.LogWarning("Parts of the file '{FilePath}' could not be read: pages={FailedPages}.", filePath, string.Join(", ", result.FailedPages)); + await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Description, result.ToPartialUserMessage(fileName))); + } + + return result; } } \ No newline at end of file