Fixed chat issue with HTML code (#679)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-02-26 08:51:22 +01:00 committed by GitHub
parent 685f95245b
commit 721d5c9070
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 275 additions and 17 deletions

View File

@ -96,10 +96,10 @@
}
else
{
<MudMarkdown Value="@textContent.Text.RemoveThinkTags().Trim()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" />
<MudMarkdown Value="@NormalizeMarkdownForRendering(textContent.Text)" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
@if (textContent.Sources.Count > 0)
{
<MudMarkdown Value="@textContent.Sources.ToMarkdown()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" />
<MudMarkdown Value="@textContent.Sources.ToMarkdown()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
}
}
}

View File

@ -10,6 +10,18 @@ namespace AIStudio.Chat;
/// </summary>
public partial class ContentBlockComponent : MSGComponentBase
{
private static readonly string[] HTML_TAG_MARKERS =
[
"<!doctype",
"<html",
"<head",
"<body",
"<style",
"<script",
"<iframe",
"<svg",
];
/// <summary>
/// The role of the chat content block.
/// </summary>
@ -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();
}
/// <inheritdoc />
protected override bool ShouldRender()
{
var currentRenderHash = this.CreateRenderHash();
if (!this.hasRenderHash || currentRenderHash != this.lastRenderHash)
{
this.lastRenderHash = currentRenderHash;
this.hasRenderHash = true;
return true;
}
return false;
}
/// <summary>
/// Gets called when the content stream ended.
/// </summary>
@ -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}";
@ -122,6 +194,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()
{
if (this.RemoveBlockFunc is null)

View File

@ -6,4 +6,4 @@
}
</MudSelect>
<MudMarkdown Value="@this.LogContent" Props="Markdown.DefaultConfig"/>
<MudMarkdown Value="@this.LogContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>

View File

@ -16,6 +16,7 @@
@if (!block.HideFromUser)
{
<ContentBlockComponent
@key="@block"
Role="@block.Role"
Type="@block.ContentType"
Time="@block.Time"

View File

@ -28,7 +28,7 @@
<MudText Typo="Typo.h6">
@T("Description")
</MudText>
<MudMarkdown Value="@this.currentConfidence.Description"/>
<MudMarkdown Value="@this.currentConfidence.Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
@if (this.currentConfidence.Sources.Count > 0)
{

View File

@ -104,7 +104,7 @@
@context.ToName()
</MudTd>
<MudTd>
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description"/>
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</MudTd>
<MudTd Style="vertical-align: top;">
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)">

View File

@ -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.")">
<div style="max-height: 40vh; overflow-y: auto;">
<MudMarkdown Value="@this.FileContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling"/>
<MudMarkdown Value="@this.FileContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</div>
</MudField>
</MudTabPanel>

View File

@ -30,7 +30,7 @@
}
else if (!string.IsNullOrWhiteSpace(this.licenseText))
{
<MudMarkdown Value="@this.licenseText" Props="Markdown.DefaultConfig"/>
<MudMarkdown Value="@this.licenseText" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
}
</ExpansionPanel>

View File

@ -5,7 +5,7 @@
<MudIcon Icon="@Icons.Material.Filled.Update" Size="Size.Large" Class="mr-3"/>
@this.HeaderText
</MudText>
<MudMarkdown Value="@this.UpdateResponse.Changelog" Props="Markdown.DefaultConfig"/>
<MudMarkdown Value="@this.UpdateResponse.Changelog" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">

View File

@ -27,7 +27,7 @@
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")">
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig"/>
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")">
@ -35,7 +35,7 @@
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")">
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE"/>
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel>
</MudExpansionPanels>

View File

@ -297,7 +297,7 @@
</MudGrid>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Verified" HeaderText="License: FSL-1.1-MIT">
<MudMarkdown Value="@LICENSE" Props="Markdown.DefaultConfig"/>
<MudMarkdown Value="@LICENSE" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel>
</MudExpansionPanels>
</InnerScrolling>

View File

@ -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 =

View File

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

16
tests/README.md Normal file
View File

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

View File

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

View File

@ -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.
<style>
* { visibility: hidden !important; }
html, body { background: white !important; }
</style>
<div>TEST</div>
```
**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.
<style>
html, body { overflow: hidden !important; }
body::before {
content: "";
position: fixed;
inset: 0;
background: white;
z-index: 2147483647;
}
</style>
<div>TEST</div>
```
**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.
<img src="x" onerror="document.body.innerHTML=''; document.body.style.background='white';">
<div>TEST</div>
```
**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.
<svg xmlns="http://www.w3.org/2000/svg" onload="document.documentElement.innerHTML=''"></svg>
<div>TEST</div>
```
**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.