## Context Assistant messages from the LLM contain markdown formatting (bold, code blocks, lists, headings) but are currently rendered as plain text via `@message.Content`. Markdig 1.1.1 is already in the stack spec but not yet wired up. The Blazor WASM client needs a rendering pipeline that converts markdown to HTML and displays it safely inside chat bubbles. ## Goals / Non-Goals **Goals:** - Render assistant message markdown as formatted HTML (headings, bold, italic, code, lists, tables, links) - Sanitize HTML output to prevent XSS from LLM-generated content - Style rendered elements to look good inside MudBlazor chat bubbles - Maintain streaming performance — re-render as tokens arrive without flicker **Non-Goals:** - Rendering user messages as markdown (they stay plain text) - Syntax highlighting for code blocks (future enhancement) - LaTeX/math rendering - Image rendering from markdown ## Decisions ### D1: Use Markdig for markdown-to-HTML conversion Markdig is already in the stack spec. It's the standard .NET markdown library, runs in WASM, and supports GFM (GitHub Flavored Markdown) extensions out of the box. **Alternative**: Custom regex-based parsing — rejected, fragile and incomplete. ### D2: Render via `MarkupString` in Blazor Blazor's `MarkupString` struct renders raw HTML in the component tree. We wrap the Markdig HTML output in `MarkupString` to display it. This replaces `@message.Content` with `@((MarkupString)renderedHtml)` for assistant messages only. **Alternative**: Use a third-party Blazor markdown component — rejected, adds a dependency when Markdig + MarkupString achieves the same result with less coupling. ### D3: HTML sanitization via allowlist approach LLM output is untrusted. After Markdig converts markdown to HTML, we strip any tags/attributes not on an allowlist. Allowed: `p, h1-h6, strong, em, code, pre, ul, ol, li, a (href only), table, thead, tbody, tr, th, td, br, blockquote`. This prevents script injection without a heavy sanitizer dependency. **Alternative**: Use a NuGet sanitizer package (e.g., HtmlSanitizer) — viable but adds a dependency for a simple allowlist. If the allowlist proves insufficient, revisit. ### D4: Create a MarkdownService for the conversion pipeline A `MarkdownService` class encapsulates the Markdig pipeline configuration, HTML sanitization, and caching. Registered as singleton in DI. This keeps the rendering logic out of the Razor component and makes it testable. ### D5: Incremental rendering during streaming During streaming, the assistant message content grows token by token. Each time `StateHasChanged()` is called, the full content string is re-converted through Markdig. This is acceptable because: - Markdig is fast (sub-millisecond for typical message sizes) - Messages rarely exceed a few KB - No DOM diffing overhead — Blazor handles this efficiently If performance becomes an issue, we can cache the last-known-good HTML prefix and only re-render the delta. ## Risks / Trade-offs - **[XSS from LLM output]** → Mitigated by HTML sanitization allowlist. The allowlist is conservative — only structural tags, no script/style/event handlers. - **[Streaming flicker]** → Low risk. Blazor's diffing minimizes DOM updates. If observed, add debouncing to StateHasChanged calls. - **[Markdig WASM size]** → Markdig adds ~200KB to the WASM bundle. Already accepted in stack spec. - **[Incomplete markdown support]** → Markdig supports GFM by default. Edge cases (nested tables, complex HTML blocks) may render imperfectly but are rare in LLM output.