Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/MindWork AI Studio/Chat/ContentBlockComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
}
}
}
Expand Down
108 changes: 104 additions & 4 deletions app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -111,6 +142,47 @@ await this.InvokeAsync(async () =>
});
}

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}";
Expand All @@ -121,6 +193,34 @@ await this.InvokeAsync(async () =>
{
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()
{
Expand Down
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Components/Changelog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
}
</MudSelect>

<MudMarkdown Value="@this.LogContent" Props="Markdown.DefaultConfig"/>
<MudMarkdown Value="@this.LogContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
1 change: 1 addition & 0 deletions app/MindWork AI Studio/Components/ChatComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@if (!block.HideFromUser)
{
<ContentBlockComponent
@key="@block"
Role="@block.Role"
Type="@block.ContentType"
Time="@block.Time"
Expand Down
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Components/ConfidenceInfo.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)">
Expand Down
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Dialogs/PandocDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Dialogs/UpdateDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
4 changes: 2 additions & 2 deletions app/MindWork AI Studio/Pages/Home.razor
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
</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")">
<Vision/>
</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>
Expand Down
4 changes: 2 additions & 2 deletions app/MindWork AI Studio/Pages/Information.razor
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@
</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>
</div>
</div>
7 changes: 7 additions & 0 deletions app/MindWork AI Studio/Tools/Markdown.cs
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down
4 changes: 3 additions & 1 deletion app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md
Original file line number Diff line number Diff line change
@@ -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.
- 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 changes: 16 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions tests/integration_tests/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading