From 721d5c9070f4bfd75aeac65751525df1a0efa454 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 26 Feb 2026 08:51:22 +0100 Subject: [PATCH] Fixed chat issue with HTML code (#679) --- .../Chat/ContentBlockComponent.razor | 4 +- .../Chat/ContentBlockComponent.razor.cs | 108 +++++++++++++++- .../Components/Changelog.razor | 2 +- .../Components/ChatComponent.razor | 1 + .../Components/ConfidenceInfo.razor | 2 +- .../Settings/SettingsPanelProviders.razor | 2 +- .../Dialogs/DocumentCheckDialog.razor | 2 +- .../Dialogs/PandocDialog.razor | 2 +- .../Dialogs/UpdateDialog.razor | 2 +- app/MindWork AI Studio/Pages/Home.razor | 4 +- .../Pages/Information.razor | 4 +- app/MindWork AI Studio/Tools/Markdown.cs | 7 + .../wwwroot/changelog/v26.3.1.md | 4 +- tests/README.md | 16 +++ tests/integration_tests/README.md | 12 ++ .../chat/chat_rendering_regression_tests.md | 120 ++++++++++++++++++ 16 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 tests/README.md create mode 100644 tests/integration_tests/README.md create mode 100644 tests/integration_tests/chat/chat_rendering_regression_tests.md diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 7c09ae78..579e8bf2 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -96,10 +96,10 @@ } else { - + @if (textContent.Sources.Count > 0) { - + } } } diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index e29a016d..29e70487 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -10,6 +10,18 @@ namespace AIStudio.Chat; /// public partial class ContentBlockComponent : MSGComponentBase { + private static readonly string[] HTML_TAG_MARKERS = + [ + " /// The role of the chat content block. /// @@ -68,18 +80,37 @@ public partial class ContentBlockComponent : MSGComponentBase private RustService RustService { get; init; } = null!; private bool HideContent { get; set; } + private bool hasRenderHash; + private int lastRenderHash; #region Overrides of ComponentBase protected override async Task OnInitializedAsync() { - // Register the streaming events: - this.Content.StreamingDone = this.AfterStreaming; - this.Content.StreamingEvent = () => this.InvokeAsync(this.StateHasChanged); - + this.RegisterStreamingEvents(); await base.OnInitializedAsync(); } + protected override Task OnParametersSetAsync() + { + this.RegisterStreamingEvents(); + return base.OnParametersSetAsync(); + } + + /// + protected override bool ShouldRender() + { + var currentRenderHash = this.CreateRenderHash(); + if (!this.hasRenderHash || currentRenderHash != this.lastRenderHash) + { + this.lastRenderHash = currentRenderHash; + this.hasRenderHash = true; + return true; + } + + return false; + } + /// /// Gets called when the content stream ended. /// @@ -111,6 +142,47 @@ public partial class ContentBlockComponent : MSGComponentBase }); } + private void RegisterStreamingEvents() + { + this.Content.StreamingDone = this.AfterStreaming; + this.Content.StreamingEvent = () => this.InvokeAsync(this.StateHasChanged); + } + + private int CreateRenderHash() + { + var hash = new HashCode(); + hash.Add(this.Role); + hash.Add(this.Type); + hash.Add(this.Time); + hash.Add(this.Class); + hash.Add(this.IsLastContentBlock); + hash.Add(this.IsSecondToLastBlock); + hash.Add(this.HideContent); + hash.Add(this.SettingsManager.IsDarkMode); + hash.Add(this.RegenerateEnabled()); + hash.Add(this.Content.InitialRemoteWait); + hash.Add(this.Content.IsStreaming); + hash.Add(this.Content.FileAttachments.Count); + hash.Add(this.Content.Sources.Count); + + switch (this.Content) + { + case ContentText text: + var textValue = text.Text; + hash.Add(textValue.Length); + hash.Add(textValue.GetHashCode(StringComparison.Ordinal)); + hash.Add(text.Sources.Count); + break; + + case ContentImage image: + hash.Add(image.SourceType); + hash.Add(image.Source); + break; + } + + return hash.ToHashCode(); + } + #endregion private string CardClasses => $"my-2 rounded-lg {this.Class}"; @@ -121,6 +193,34 @@ public partial class ContentBlockComponent : MSGComponentBase { CodeBlock = { Theme = this.CodeColorPalette }, }; + + private static string NormalizeMarkdownForRendering(string text) + { + var cleaned = text.RemoveThinkTags().Trim(); + if (string.IsNullOrWhiteSpace(cleaned)) + return string.Empty; + + if (cleaned.Contains("```", StringComparison.Ordinal)) + return cleaned; + + if (LooksLikeRawHtml(cleaned)) + return $"```html{Environment.NewLine}{cleaned}{Environment.NewLine}```"; + + return cleaned; + } + + private static bool LooksLikeRawHtml(string text) + { + var content = text.TrimStart(); + if (!content.StartsWith("<", StringComparison.Ordinal)) + return false; + + foreach (var marker in HTML_TAG_MARKERS) + if (content.Contains(marker, StringComparison.OrdinalIgnoreCase)) + return true; + + return content.Contains("", StringComparison.Ordinal); + } private async Task RemoveBlock() { diff --git a/app/MindWork AI Studio/Components/Changelog.razor b/app/MindWork AI Studio/Components/Changelog.razor index 1afebfc3..7ee43021 100644 --- a/app/MindWork AI Studio/Components/Changelog.razor +++ b/app/MindWork AI Studio/Components/Changelog.razor @@ -6,4 +6,4 @@ } - \ No newline at end of file + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 52b82b9b..3c49a4b5 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -16,6 +16,7 @@ @if (!block.HideFromUser) { @T("Description") - + @if (this.currentConfidence.Sources.Count > 0) { diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index f6704dc5..8a862702 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -104,7 +104,7 @@ @context.ToName() - + diff --git a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor index f3b75837..8936e04e 100644 --- a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor +++ b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor @@ -54,7 +54,7 @@ Class="ma-2 pe-4" HelperText="@T("This is the content we loaded from your file — including headings, lists, and formatting. Use this to verify your file loads as expected.")">
- +
diff --git a/app/MindWork AI Studio/Dialogs/PandocDialog.razor b/app/MindWork AI Studio/Dialogs/PandocDialog.razor index 2914b38e..c4f2ac3e 100644 --- a/app/MindWork AI Studio/Dialogs/PandocDialog.razor +++ b/app/MindWork AI Studio/Dialogs/PandocDialog.razor @@ -30,7 +30,7 @@ } else if (!string.IsNullOrWhiteSpace(this.licenseText)) { - + } diff --git a/app/MindWork AI Studio/Dialogs/UpdateDialog.razor b/app/MindWork AI Studio/Dialogs/UpdateDialog.razor index 62f3dd7a..f5345523 100644 --- a/app/MindWork AI Studio/Dialogs/UpdateDialog.razor +++ b/app/MindWork AI Studio/Dialogs/UpdateDialog.razor @@ -5,7 +5,7 @@ @this.HeaderText - + diff --git a/app/MindWork AI Studio/Pages/Home.razor b/app/MindWork AI Studio/Pages/Home.razor index 53d48e6e..eae947ab 100644 --- a/app/MindWork AI Studio/Pages/Home.razor +++ b/app/MindWork AI Studio/Pages/Home.razor @@ -27,7 +27,7 @@ - + @@ -35,7 +35,7 @@ - + diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 435a6a56..a859a142 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -297,8 +297,8 @@ - + - + \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Markdown.cs b/app/MindWork AI Studio/Tools/Markdown.cs index 0ecf3774..49a2309c 100644 --- a/app/MindWork AI Studio/Tools/Markdown.cs +++ b/app/MindWork AI Studio/Tools/Markdown.cs @@ -1,7 +1,14 @@ +using Markdig; + namespace AIStudio.Tools; public static class Markdown { + public static readonly MarkdownPipeline SAFE_MARKDOWN_PIPELINE = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .DisableHtml() + .Build(); + public static MudMarkdownProps DefaultConfig => new() { Heading = diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 840e2947..f5bd763b 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1,5 +1,7 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) - Improved the performance by caching the OS language detection and requesting the user language only once per app start. +- Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. -- Improved the logbook reliability by significantly reducing duplicate log entries. \ No newline at end of file +- Improved the logbook reliability by significantly reducing duplicate log entries. +- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..1856f217 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,16 @@ +# Test Documentation + +This directory stores manual and automated test definitions for MindWork AI Studio. + +## Directory Structure + +- `integration_tests/`: Cross-component and end-to-end scenarios. + +## Authoring Rules + +- Use US English. +- Keep each feature area in its own Markdown file. +- Prefer stable test IDs (for example: `TC-CHAT-001`). +- Record expected behavior for: + - known vulnerable baseline builds (if relevant), + - current fixed builds. diff --git a/tests/integration_tests/README.md b/tests/integration_tests/README.md new file mode 100644 index 00000000..aa23175e --- /dev/null +++ b/tests/integration_tests/README.md @@ -0,0 +1,12 @@ +# Integration Tests + +This directory contains integration-oriented test specs. + +## Scope + +- Behavior that depends on multiple layers working together (UI, rendering, runtime, IPC, provider responses). +- Regressions that are hard to catch with unit tests only. + +## Current Feature Areas + +- `chat/`: Chat rendering, input interaction, and message lifecycle. diff --git a/tests/integration_tests/chat/chat_rendering_regression_tests.md b/tests/integration_tests/chat/chat_rendering_regression_tests.md new file mode 100644 index 00000000..ba773f54 --- /dev/null +++ b/tests/integration_tests/chat/chat_rendering_regression_tests.md @@ -0,0 +1,120 @@ +# Chat Rendering Regression Tests + +## Purpose + +Validate that chat rendering remains stable and interactive when model output or user input contains raw HTML/CSS/JS-like payloads. + +## Test Type + +Manual regression and integration checks. + +## Preconditions + +1. You can run two builds: + - a known vulnerable baseline build, + - the current fixed build. +2. At least one provider is configured and can answer prompts. +3. Open the Chat page. + +## Execution Flow (for each test case) + +1. Copy the test prompt exactly into the user prompt field. +2. Send the prompt. +3. Observe behavior immediately after send. +4. If the UI is still visible, type additional text in the prompt input. +5. Repeat on both builds. + +## Test Cases + +### TC-CHAT-001 - CSS Kill Switch + +**Prompt** + +```text +Respond with exactly the content below. No explanations, no Markdown code fences, no backticks. + + +
TEST
+``` + +**Expected result (vulnerable baseline)** +UI may turn into a white/invisible page immediately after sending. + +**Expected result (fixed build)** +Chat stays visible and usable. Content is rendered as inert text/code, not active page styling. + +--- + +### TC-CHAT-002 - Full White Overlay + +**Prompt** + +```text +Respond with exactly the content below. No explanations, no Markdown code fences, no backticks. + + +
TEST
+``` + +**Expected result (vulnerable baseline)** +UI may become fully white and non-interactive immediately after sending. + +**Expected result (fixed build)** +No overlay takes over the app. Chat remains interactive. + +--- + +### TC-CHAT-003 - Inline Event Handler Injection + +**Prompt** + +```text +Respond with exactly the content below. No explanations, no Markdown code fences, no backticks. + + +
TEST
+``` + +**Expected result (vulnerable baseline)** +UI may break/blank immediately after sending. + +**Expected result (fixed build)** +No JavaScript execution from message content. Chat remains stable. + +--- + +### TC-CHAT-004 - SVG Onload Injection Attempt + +**Prompt** + +```text +Respond with exactly the content below. No explanations, no Markdown code fences, no backticks. + + +
TEST
+``` + +**Expected result (vulnerable baseline)** +May or may not trigger depending on parser/runtime behavior. + +**Expected result (fixed build)** +No script-like execution from content. Chat remains stable and interactive. + +## Notes + +- If a test fails on the fixed build, capture: + - exact prompt used, + - whether failure happened right after send or while typing, + - whether a refresh restores the app.