diff --git a/webui/src/components/MarkdownTextRenderer.tsx b/webui/src/components/MarkdownTextRenderer.tsx index 5f2a23510..a8c42c978 100644 --- a/webui/src/components/MarkdownTextRenderer.tsx +++ b/webui/src/components/MarkdownTextRenderer.tsx @@ -237,11 +237,48 @@ function remarkSafeHtmlSubset() { }; } +// Recover a common model-output edge case that CommonMark leaves as literal +// text: `**结论。**如果`, with no separator after the closing delimiter. +const CJK_AFTER_STRONG = + /(? { + if (child.type !== "text" || !child.value?.includes("**")) { + normalizeCjkStrongBoundaries(child); + return [child]; + } + + const replacement: MarkdownAstNode[] = []; + let cursor = 0; + for (const match of child.value.matchAll(CJK_AFTER_STRONG)) { + const start = match.index; + if (start > cursor) replacement.push(safeText(child.value.slice(cursor, start))); + replacement.push({ + type: "strong", + children: [safeText(match[1])], + }); + cursor = start + match[0].length; + } + if (cursor === 0) return [child]; + if (cursor < child.value.length) replacement.push(safeText(child.value.slice(cursor))); + return replacement; + }); +} + +function remarkCjkStrongBoundaries() { + return (tree: MarkdownAstNode) => { + normalizeCjkStrongBoundaries(tree); + }; +} + const remarkPlugins: NonNullable = [ remarkBreaks, remarkGfm, [remarkMath, { singleDollarTextMath: false }], remarkTexMath, + remarkCjkStrongBoundaries, remarkSafeHtmlSubset, ]; const rehypePlugins: NonNullable = [rehypeKatex]; diff --git a/webui/src/tests/markdown-text-renderer.test.tsx b/webui/src/tests/markdown-text-renderer.test.tsx index 0e445b735..797a76c29 100644 --- a/webui/src/tests/markdown-text-renderer.test.tsx +++ b/webui/src/tests/markdown-text-renderer.test.tsx @@ -523,6 +523,21 @@ describe("MarkdownTextRenderer", () => { expect(screen.getByRole("link", { name: "links" })).not.toHaveAttribute("node"); }); + it("renders bold CJK text when more CJK text follows immediately", () => { + render( + + { + "**结论:目前看风险可控,没有发现常驻或可疑安装。**如果你之后不想再用,我可以帮你彻底卸载。" + } + , + ); + + expect( + screen.getByText("结论:目前看风险可控,没有发现常驻或可疑安装。").tagName, + ).toBe("STRONG"); + expect(screen.getByText(/如果你之后不想再用/)).toBeInTheDocument(); + }); + it("adds line numbers to multiline fenced code without changing inline code", () => { render(