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) || 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.