From 68866ecb8aaaa2b856a8b5e3e7d6b52b50443edf Mon Sep 17 00:00:00 2001 From: Nils Kruthoff Date: Tue, 4 Aug 2026 15:13:21 +0200 Subject: [PATCH] extracting the raw JSON from the AI response and make sure its not a math or md block --- .../Chat/ContentBlockComponent.razor.cs | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 0dcb910c..ee173170 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -17,6 +17,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable private const string HTML_SELF_CLOSING_TAG = "/>"; private const string CODE_FENCE_MARKER_BACKTICK = "```"; private const string CODE_FENCE_MARKER_TILDE = "~~~"; + private const string CHART_CODE_FENCE_LANGUAGE = "aistudio-chart"; private const string MATH_BLOCK_MARKER_DOLLAR = "$$"; private const string MATH_BLOCK_MARKER_BRACKET_OPEN = """\["""; private const string MATH_BLOCK_MARKER_BRACKET_CLOSE = """\]"""; @@ -336,6 +337,20 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable } var trimmedLine = TrimWhitespace(normalizedSpan[lineStart..lineEnd]); + if (activeMathBlockFenceType is MathBlockFenceType.NONE + && activeCodeFenceMarker == '\0' + && TryGetChartFenceMarker(trimmedLine, out var chartFenceMarker) + && TryFindClosingCodeFence(normalizedSpan, nextLineStart, chartFenceMarker, out var chartContentEnd, out var afterChartFence)) + { + AddMarkdownSegment(markdownSegmentStart, lineStart); + var (start, end) = TrimLineBreaks(normalizedSpan, nextLineStart, chartContentEnd); + var chartJson = normalized.Substring(start, end - start); + segments.Add(new(MarkdownRenderSegmentType.CHART, start, end - start, ChartBlockParser.Parse(chartJson))); + markdownSegmentStart = afterChartFence; + lineStart = afterChartFence; + continue; + } + if (activeMathBlockFenceType is MathBlockFenceType.NONE && TryUpdateCodeFenceState(trimmedLine, ref activeCodeFenceMarker)) { lineStart = nextLineStart; @@ -463,6 +478,48 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable return true; } + private static bool TryGetChartFenceMarker(ReadOnlySpan trimmedLine, out char fenceMarker) + { + fenceMarker = '\0'; + if (trimmedLine.SequenceEqual($"{CODE_FENCE_MARKER_BACKTICK}{CHART_CODE_FENCE_LANGUAGE}".AsSpan())) + fenceMarker = '`'; + else if (trimmedLine.SequenceEqual($"{CODE_FENCE_MARKER_TILDE}{CHART_CODE_FENCE_LANGUAGE}".AsSpan())) + fenceMarker = '~'; + + return fenceMarker != '\0'; + } + + private static bool TryFindClosingCodeFence(ReadOnlySpan text, int searchStart, char fenceMarker, out int contentEnd, out int afterFence) + { + contentEnd = 0; + afterFence = 0; + var closingFence = fenceMarker == '`' ? CODE_FENCE_MARKER_BACKTICK : CODE_FENCE_MARKER_TILDE; + + for (var lineStart = searchStart; lineStart < text.Length;) + { + var lineEnd = lineStart; + while (lineEnd < text.Length && text[lineEnd] is not '\r' and not '\n') + lineEnd++; + + var nextLineStart = lineEnd; + if (nextLineStart < text.Length && text[nextLineStart] == '\r') + nextLineStart++; + if (nextLineStart < text.Length && text[nextLineStart] == '\n') + nextLineStart++; + + if (TrimWhitespace(text[lineStart..lineEnd]).SequenceEqual(closingFence.AsSpan())) + { + contentEnd = lineStart; + afterFence = nextLineStart; + return true; + } + + lineStart = nextLineStart; + } + + return false; + } + private static ReadOnlySpan TrimWhitespace(ReadOnlySpan text) { var start = 0; @@ -492,6 +549,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable { MARKDOWN, MATH_BLOCK, + CHART, } private enum MathBlockFenceType @@ -506,7 +564,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable public static readonly MarkdownRenderPlan EMPTY = new(string.Empty, []); } - private sealed class MarkdownRenderSegment(MarkdownRenderSegmentType type, int start, int length) + private sealed class MarkdownRenderSegment(MarkdownRenderSegmentType type, int start, int length, ChartBlockParseResult? chartResult = null) { private string? cachedContent; @@ -516,6 +574,8 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable public int Length { get; } = length; + public ChartBlockParseResult? ChartResult { get; } = chartResult; + public int RenderKey { get; } = HashCode.Combine(type, start, length); public string GetContent(string source)