Fix standalone HTML and LaTeX exports

This commit is contained in:
Peer Hogeterp 2026-09-22 15:11:45 +02:00
parent c95cc5bacc
commit a1d8169284
3 changed files with 71 additions and 3 deletions

View File

@ -732,7 +732,13 @@ public partial class ContentBlockComponent : MSGComponentBase
// here which would fall out of sync with the one in FileExportFormatExtensions. // here which would fall out of sync with the one in FileExportFormatExtensions.
// //
if (format.UsesPandoc()) if (format.UsesPandoc())
await PandocExport.ToDocument(this.RustService, this.PandocAvailability, this.EffectiveExportTitle, format, this.Content); {
if (this.Content.TryGetMarkdownText(out var originalMarkdown) &&
PlainFileExport.TryExtractStandaloneSource(originalMarkdown, format, out var source))
await PlainFileExport.ToSourceFile(this.RustService, this.EffectiveExportTitle, format, source);
else
await PandocExport.ToDocument(this.RustService, this.PandocAvailability, this.EffectiveExportTitle, format, this.Content);
}
else if (this.Content.TryGetExportMarkdown(out var markdown)) else if (this.Content.TryGetExportMarkdown(out var markdown))
await PlainFileExport.ToFile(this.RustService, this.EffectiveExportTitle, format, markdown); await PlainFileExport.ToFile(this.RustService, this.EffectiveExportTitle, format, markdown);
} }
@ -852,4 +858,4 @@ public partial class ContentBlockComponent : MSGComponentBase
await this.DisposeMathContainerIfNeededAsync(); await this.DisposeMathContainerIfNeededAsync();
} }
} }

View File

@ -1,6 +1,7 @@
using System.Text; using System.Text;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Markdig.Extensions.Tables; using Markdig.Extensions.Tables;
@ -15,6 +16,42 @@ public static class PlainFileExport
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PlainFileExport).Namespace, nameof(PlainFileExport)); private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PlainFileExport).Namespace, nameof(PlainFileExport));
/// <summary>
/// Reads a complete HTML or LaTeX source file out of a message which consists only of the
/// matching fenced code block.
/// </summary>
/// <param name="markdown">The Markdown text the model wrote.</param>
/// <param name="format">The source format to extract.</param>
/// <param name="source">The contents inside the fence, or an empty string when the message is
/// not a standalone source file.</param>
/// <returns>True, when the message is a complete source file for the selected format.</returns>
internal static bool TryExtractStandaloneSource(string markdown, FileExportFormat format, out string source)
{
source = string.Empty;
var languages = format switch
{
FileExportFormat.HTML => new[] { "html" },
FileExportFormat.LATEX => new[] { "latex", "tex" },
_ => [],
};
if (languages.Length is 0 || string.IsNullOrWhiteSpace(markdown))
return false;
var document = Markdig.Markdown.Parse(markdown, Markdown.SAFE_MARKDOWN_PIPELINE);
if (document.Count is not 1 || document[0] is not FencedCodeBlock block || block.ClosingFencedCharCount is 0)
return false;
var language = block.Info?.Trim();
if (!languages.Any(candidate => string.Equals(candidate, language, StringComparison.OrdinalIgnoreCase)))
return false;
source = block.Lines.ToString();
return true;
}
/// <summary> /// <summary>
/// Reads every table a message holds, in the order they appear in it. /// Reads every table a message holds, in the order they appear in it.
/// </summary> /// </summary>
@ -183,6 +220,30 @@ public static class PlainFileExport
if (format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter) if (format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter)
throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format itself."); throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format itself.");
return await WriteFile(rustService, dialogTitle, format, fileContent, fileTypeFilter, fileName);
}
/// <summary>
/// Writes HTML or LaTeX source authored by the model without converting it.
/// </summary>
/// <param name="rustService">The Rust service, used for the save dialog.</param>
/// <param name="dialogTitle">The title of the save dialog.</param>
/// <param name="format">The matching HTML or LaTeX format.</param>
/// <param name="source">The source text inside the model's code fence.</param>
/// <returns>True, when the file was written.</returns>
internal static async Task<bool> ToSourceFile(RustService rustService, string dialogTitle, FileExportFormat format, string source)
{
if (format is not (FileExportFormat.HTML or FileExportFormat.LATEX) || format.ToFileTypeFilter() is not { } fileTypeFilter)
throw new ArgumentOutOfRangeException(nameof(format), format, "AI Studio cannot write this format as an authored source file.");
return await WriteFile(rustService, dialogTitle, format, source, fileTypeFilter);
}
/// <summary>
/// Lets the user choose a path and writes text with the encoding of its format.
/// </summary>
private static async Task<bool> WriteFile(RustService rustService, string dialogTitle, FileExportFormat format, string fileContent, FileTypeFilter fileTypeFilter, string? fileName = null)
{
var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName(fileName)); var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName(fileName));
if (response.UserCancelled) if (response.UserCancelled)
{ {
@ -206,4 +267,4 @@ public static class PlainFileExport
return false; return false;
} }
} }
} }

View File

@ -64,6 +64,7 @@
- Fixed an assistant refusing to work after you switched on the cleanup of web content without having chosen a model first. Its button did nothing at all, and only closing the assistant and starting over helped. The cleanup now follows the model of the assistant you are working in, the moment you pick one. - Fixed an assistant refusing to work after you switched on the cleanup of web content without having chosen a model first. Its button did nothing at all, and only closing the assistant and starting over helped. The cleanup now follows the model of the assistant you are working in, the moment you pick one.
- Fixed the Visual Briefing assistant (in preview) not scrolling, which put everything below the window edge out of reach and made the assistant unusable. The briefing preview is now shown at its intended size inside its frame, and switching between the desktop, tablet, and mobile view changes its width as it should. - Fixed the Visual Briefing assistant (in preview) not scrolling, which put everything below the window edge out of reach and made the assistant unusable. The briefing preview is now shown at its intended size inside its frame, and switching between the desktop, tablet, and mobile view changes its width as it should.
- Fixed exported answers losing their sources. When an answer is based on web pages a tool read or on documents of your own, the exported file now lists those sources in every format AI Studio writes. - Fixed exported answers losing their sources. When an answer is based on web pages a tool read or on documents of your own, the exported file now lists those sources in every format AI Studio writes.
- Fixed exporting an answer made only of one complete HTML or LaTeX code block. AI Studio now saves the source directly, without Markdown fences or a Pandoc conversion.
- Fixed the copy button leaving the sources behind. Copy an answer, and its sources come along. - Fixed the copy button leaving the sources behind. Copy an answer, and its sources come along.
- Fixed a tile that opens a chat directly always demanding a workspace. Leave the workspace empty in the Assistant Builder, and the tile opens a disappearing chat instead. - Fixed a tile that opens a chat directly always demanding a workspace. Leave the workspace empty in the Assistant Builder, and the tile opens a disappearing chat instead.
- Fixed the same restriction for plugin authors: a direct-chat launcher can now open a chat without naming a workspace. The example assistant plugin shows both ways. - Fixed the same restriction for plugin authors: a direct-chat launcher can now open a chat without naming a workspace. The example assistant plugin shows both ways.