Added init math rendering

This commit is contained in:
Thorsten Sommer 2026-03-21 18:50:28 +01:00
parent a2bd67eda3
commit 1f05bc3197
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
5 changed files with 163 additions and 2 deletions

View File

@ -96,7 +96,18 @@
}
else
{
<MudMarkdown Value="@NormalizeMarkdownForRendering(textContent.Text)" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
var renderSegments = GetMarkdownRenderSegments(textContent.Text);
foreach (var segment in renderSegments)
{
if (segment.Type is MarkdownRenderSegmentType.MARKDOWN)
{
<MudMarkdown Value="@segment.Content" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
}
else
{
<MathJaxBlock Value="@segment.Content" Class="mb-5" />
}
}
@if (textContent.Sources.Count > 0)
{
<MudMarkdown Value="@textContent.Sources.ToMarkdown()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />

View File

@ -2,6 +2,7 @@ using AIStudio.Components;
using AIStudio.Dialogs;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using System.Text;
namespace AIStudio.Chat;
@ -194,6 +195,72 @@ public partial class ContentBlockComponent : MSGComponentBase
CodeBlock = { Theme = this.CodeColorPalette },
};
private static IReadOnlyList<MarkdownRenderSegment> GetMarkdownRenderSegments(string text)
{
var normalized = NormalizeMarkdownForRendering(text);
if (string.IsNullOrWhiteSpace(normalized))
return [];
var normalizedWithUnixLineEndings = normalized.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n');
var lines = normalizedWithUnixLineEndings.Split('\n');
var markdownBuilder = new StringBuilder();
var mathBuilder = new StringBuilder();
var segments = new List<MarkdownRenderSegment>();
string? activeCodeFenceMarker = null;
var inMathBlock = false;
foreach (var line in lines)
{
var trimmedLine = line.Trim();
if (!inMathBlock && TryUpdateCodeFenceState(trimmedLine, ref activeCodeFenceMarker))
{
AppendLine(markdownBuilder, line);
continue;
}
if (activeCodeFenceMarker is not null)
{
AppendLine(markdownBuilder, line);
continue;
}
if (trimmedLine == "$$")
{
if (inMathBlock)
{
segments.Add(new(MarkdownRenderSegmentType.MATH_BLOCK, mathBuilder.ToString().Trim('\r', '\n')));
mathBuilder.Clear();
inMathBlock = false;
}
else
{
FlushMarkdownSegment();
inMathBlock = true;
}
continue;
}
AppendLine(inMathBlock ? mathBuilder : markdownBuilder, line);
}
if (inMathBlock)
return [new(MarkdownRenderSegmentType.MARKDOWN, normalized)];
FlushMarkdownSegment();
return segments.Count > 0 ? segments : [new(MarkdownRenderSegmentType.MARKDOWN, normalized)];
void FlushMarkdownSegment()
{
if (markdownBuilder.Length == 0)
return;
segments.Add(new(MarkdownRenderSegmentType.MARKDOWN, markdownBuilder.ToString()));
markdownBuilder.Clear();
}
}
private static string NormalizeMarkdownForRendering(string text)
{
var cleaned = text.RemoveThinkTags().Trim();
@ -222,6 +289,39 @@ public partial class ContentBlockComponent : MSGComponentBase
return content.Contains("</", StringComparison.Ordinal) || content.Contains("/>", StringComparison.Ordinal);
}
private static bool TryUpdateCodeFenceState(string trimmedLine, ref string? activeCodeFenceMarker)
{
string? fenceMarker = null;
if (trimmedLine.StartsWith("```", StringComparison.Ordinal))
fenceMarker = "```";
else if (trimmedLine.StartsWith("~~~", StringComparison.Ordinal))
fenceMarker = "~~~";
if (fenceMarker is null)
return false;
activeCodeFenceMarker = activeCodeFenceMarker is null
? fenceMarker
: activeCodeFenceMarker == fenceMarker
? null
: activeCodeFenceMarker;
return true;
}
private static void AppendLine(StringBuilder builder, string line)
{
builder.AppendLine(line);
}
private enum MarkdownRenderSegmentType
{
MARKDOWN,
MATH_BLOCK,
}
private readonly record struct MarkdownRenderSegment(MarkdownRenderSegmentType Type, string Content);
private async Task RemoveBlock()
{
if (this.RemoveBlockFunc is null)

View File

@ -0,0 +1,5 @@
@namespace AIStudio.Chat
<div class="@this.RootClass" style="white-space: pre-wrap;">
@this.MathText
</div>

View File

@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Chat;
public partial class MathJaxBlock
{
private const string MATH_JAX_SCRIPT_ID = "mudblazor-markdown-mathjax";
[Parameter]
public string Value { get; init; } = string.Empty;
[Parameter]
public string Class { get; init; } = string.Empty;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
private string RootClass => string.IsNullOrWhiteSpace(this.Class)
? "chat-mathjax-block"
: $"chat-mathjax-block {this.Class}";
private string MathText => $"$${Environment.NewLine}{this.Value}{Environment.NewLine}$$";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await this.JsRuntime.InvokeVoidAsync("appendMathJaxScript", MATH_JAX_SCRIPT_ID);
await this.JsRuntime.InvokeVoidAsync("refreshMathJaxScript");
await base.OnAfterRenderAsync(firstRender);
}
}

View File

@ -151,3 +151,18 @@
top: 0em !important;
left: 2.2em !important;
}
.chat-mathjax-block {
text-align: left;
}
.chat-mathjax-block mjx-container[display="true"] {
text-align: left !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.chat-mathjax-block mjx-container[display="true"] mjx-math {
margin-left: 0 !important;
margin-right: 0 !important;
}