mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 08:18:00 +00:00
Rework of the file export (#894)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
f14c69b938
commit
c66a61713d
33 files changed
+1212
-266
No files matched your search
@@ -1,77 +1,54 @@
|
||||
using System.Diagnostics;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class PandocExport
|
||||
{
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocExport));
|
||||
|
||||
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport));
|
||||
|
||||
public static async Task<bool> ToMicrosoftWord(RustService rustService, IDialogService dialogService, string dialogTitle, IContent markdownContent)
|
||||
{
|
||||
var response = await rustService.SaveFile(dialogTitle, [FileTypes.MS_WORD]);
|
||||
if (response.UserCancelled)
|
||||
{
|
||||
LOGGER.LogInformation("User cancelled the save dialog.");
|
||||
return false;
|
||||
}
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocExport));
|
||||
|
||||
LOGGER.LogInformation($"The user chose the path '{response.SaveFilePath}' for the Microsoft Word export.");
|
||||
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport));
|
||||
|
||||
/// <summary>
|
||||
/// Converts the given Markdown text into a document at the given path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This says nothing to the user: it reports what happened and lets the caller decide. A batch
|
||||
/// run over hundreds of documents would otherwise bury the user under notifications. Pandoc
|
||||
/// must be available, which PandocAvailabilityService.EnsureAvailabilityAsync takes care of.
|
||||
/// </remarks>
|
||||
/// <param name="rustService">The Rust service, used to build the Pandoc call.</param>
|
||||
/// <param name="markdownText">The Markdown text to convert.</param>
|
||||
/// <param name="targetFilePath">Where to write the document.</param>
|
||||
/// <param name="format">The format to write. Must be a format which uses Pandoc.</param>
|
||||
/// <param name="token">The token to cancel the conversion.</param>
|
||||
/// <returns>True, when the document was written.</returns>
|
||||
public static async Task<bool> ConvertAsync(RustService rustService, string markdownText, string targetFilePath, FileExportFormat format, CancellationToken token = default)
|
||||
{
|
||||
if (!format.UsesPandoc())
|
||||
throw new ArgumentOutOfRangeException(nameof(format), format, "Pandoc cannot write this format.");
|
||||
|
||||
var tempMarkdownFilePath = string.Empty;
|
||||
try
|
||||
{
|
||||
var tempMarkdownFile = Guid.NewGuid().ToString();
|
||||
tempMarkdownFilePath = Path.Combine(Path.GetTempPath(), tempMarkdownFile);
|
||||
|
||||
// Extract text content from chat:
|
||||
var markdownText = markdownContent switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
ContentImage _ => "Image export to Microsoft Word not yet possible",
|
||||
|
||||
_ => "Unknown content type. Cannot export to Word."
|
||||
};
|
||||
// Write text content to a temporary file. Pandoc expects UTF-8 without a byte order
|
||||
// mark; a mark would end up as a stray character at the start of the document:
|
||||
await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText, new UTF8Encoding(false), token);
|
||||
|
||||
// Write text content to a temporary file:
|
||||
await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText);
|
||||
|
||||
// Ensure that Pandoc is installed and ready:
|
||||
var pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: false);
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<PandocDialog>
|
||||
{
|
||||
{ x => x.ShowInitialResultInSnackbar, false },
|
||||
};
|
||||
|
||||
var dialogReference = await dialogService.ShowAsync<PandocDialog>(TB("Pandoc Installation"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
await dialogReference.Result;
|
||||
|
||||
pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: true);
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
LOGGER.LogError("Pandoc is not available after installation attempt.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Pandoc is required for Microsoft Word export.")));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Call Pandoc to create the Word file:
|
||||
// Call Pandoc to create the document:
|
||||
var pandoc = await PandocProcessBuilder
|
||||
.Create()
|
||||
.UseStandaloneMode()
|
||||
.WithInputFormat("gfm+emoji+tex_math_dollars")
|
||||
.WithOutputFormat("docx")
|
||||
.WithOutputFile(response.SaveFilePath)
|
||||
.WithOutputFormat(format.ToPandocOutputFormat())
|
||||
.WithOutputFile(targetFilePath)
|
||||
.WithInputFile(tempMarkdownFilePath)
|
||||
.BuildAsync(rustService);
|
||||
|
||||
@@ -83,30 +60,26 @@ public static class PandocExport
|
||||
}
|
||||
|
||||
// Read output streams asynchronously while the process runs (prevents deadlock):
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync(token);
|
||||
var errorTask = process.StandardError.ReadToEndAsync(token);
|
||||
|
||||
// Wait for the process to exit AND for streams to be fully read:
|
||||
await process.WaitForExitAsync();
|
||||
await process.WaitForExitAsync(token);
|
||||
await outputTask;
|
||||
var error = await errorTask;
|
||||
|
||||
if (process.ExitCode is not 0)
|
||||
{
|
||||
LOGGER.LogError("Pandoc failed with exit code {ProcessExitCode}: '{ErrorText}'", process.ExitCode, error);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during Microsoft Word export")));
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.LogInformation("Pandoc conversion successful.");
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Microsoft Word export successful")));
|
||||
|
||||
LOGGER.LogInformation("Pandoc conversion to {ExportFormat} successful.", format);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOGGER.LogError(ex, "Error during Word export.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during Microsoft Word export")));
|
||||
LOGGER.LogError(ex, "Error during {ExportFormat} conversion.", format);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
@@ -120,9 +93,59 @@ public static class PandocExport
|
||||
}
|
||||
catch
|
||||
{
|
||||
LOGGER.LogWarning($"Was not able to delete temporary file: '{tempMarkdownFilePath}'");
|
||||
LOGGER.LogWarning("Was not able to delete the temporary file '{TempFilePath}'.", tempMarkdownFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the given content to a document using Pandoc and lets the user save it.
|
||||
/// </summary>
|
||||
/// <param name="rustService">The Rust service, used for the save dialog and for Pandoc.</param>
|
||||
/// <param name="pandocAvailability">Makes sure Pandoc is there and offers its installation.</param>
|
||||
/// <param name="dialogTitle">The title of the save dialog. The caller knows what the user is
|
||||
/// looking at, a chat message or the result of an assistant, so the caller names it.</param>
|
||||
/// <param name="format">The format to write. Must be a format which uses Pandoc.</param>
|
||||
/// <param name="markdownContent">The content to export.</param>
|
||||
/// <returns>True, when the document was written.</returns>
|
||||
public static async Task<bool> ToDocument(RustService rustService, PandocAvailabilityService pandocAvailability, string dialogTitle, FileExportFormat format, IContent markdownContent)
|
||||
{
|
||||
if (!format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter)
|
||||
throw new ArgumentOutOfRangeException(nameof(format), format, "Pandoc cannot write this format.");
|
||||
|
||||
//
|
||||
// We read the text before we ask for a path: when there is nothing to convert, the user
|
||||
// should learn that right away instead of picking a file first and getting an error afterwards.
|
||||
//
|
||||
if (!markdownContent.TryGetMarkdownText(out var markdownText))
|
||||
{
|
||||
LOGGER.LogWarning("Cannot export the content as {ExportFormat}, because it carries no text.", format);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Only text messages can be exported.")));
|
||||
return false;
|
||||
}
|
||||
|
||||
var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName());
|
||||
if (response.UserCancelled)
|
||||
{
|
||||
LOGGER.LogInformation("User cancelled the save dialog.");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.LogInformation("The user chose the path '{SaveFilePath}' for the {ExportFormat} export.", response.SaveFilePath, format);
|
||||
|
||||
// The service reports a missing Pandoc to the user itself, so we only act on the outcome:
|
||||
var pandocState = await pandocAvailability.EnsureAvailabilityAsync(showSuccessMessage: false, showDialog: true);
|
||||
if (!pandocState.IsAvailable)
|
||||
return false;
|
||||
|
||||
if (!await ConvertAsync(rustService, markdownText, response.SaveFilePath, format))
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("The export failed.")));
|
||||
return false;
|
||||
}
|
||||
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("The export succeeded.")));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user