From 09d45046e5f46fb2300f8860a3819568d71916d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=86=8A?= <1114202297@qq.com> Date: Fri, 15 May 2026 22:29:44 +0800 Subject: [PATCH] Feat/web markdown UI updates (#14214) ### What problem does this PR solve? LLM/chat and search UIs render Markdown in several places (document preview, floating chat widget, next-search, etc.). Plugin lists and behavior were duplicated or inconsistent, and single newlines in model output were not always rendered as visible line breaks, which hurts readability for chat-style content. This PR centralizes shared **remark/rehype** configuration (including **`remark-breaks`** for newline handling) and wires the main Markdown surfaces to use it, so behavior is consistent and easier to maintain. ### Type of change - [x] Refactoring --------- Co-authored-by: Yingfeng Zhang --- web/package-lock.json | 30 +++++++++++++++++++ web/package.json | 1 + .../components/document-preview/md/index.tsx | 6 ++-- .../floating-chat-widget-markdown.tsx | 5 ++-- .../components/highlight-markdown/index.tsx | 5 ++-- web/src/components/markdown-content/index.tsx | 5 ++-- .../next-markdown-content/index.tsx | 5 ++-- web/src/constants/markdown-remark-plugins.ts | 17 +++++++++++ .../next-search/markdown-content/index.tsx | 5 ++-- 9 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 web/src/constants/markdown-remark-plugins.ts diff --git a/web/package-lock.json b/web/package-lock.json index bfb0aee4f2..38407cfbe3 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -100,6 +100,7 @@ "recharts": "^2.12.4", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", + "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "sonner": "^1.7.4", @@ -18439,6 +18440,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-newline-to-break": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", @@ -22539,6 +22554,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-breaks": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz", diff --git a/web/package.json b/web/package.json index 4e0485c6d9..6dbed295b4 100644 --- a/web/package.json +++ b/web/package.json @@ -122,6 +122,7 @@ "recharts": "^2.12.4", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", + "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "sonner": "^1.7.4", diff --git a/web/src/components/document-preview/md/index.tsx b/web/src/components/document-preview/md/index.tsx index bdc30f91bc..13f1af3c2f 100644 --- a/web/src/components/document-preview/md/index.tsx +++ b/web/src/components/document-preview/md/index.tsx @@ -1,10 +1,10 @@ import { Authorization } from '@/constants/authorization'; +import { MarkdownRemarkPluginsLite } from '@/constants/markdown-remark-plugins'; import { cn } from '@/lib/utils'; import FileError from '@/pages/document-viewer/file-error'; import { getAuthorization } from '@/utils/authorization-util'; import React, { useEffect, useState } from 'react'; import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; interface MdProps { // filePath: string; @@ -34,7 +34,9 @@ export const Md: React.FC = ({ url, className }) => { style={{ padding: 4, overflow: 'scroll' }} className={cn(className, 'markdown-body h-[calc(100vh - 200px)]')} > - {content} + + {content} + ); }; diff --git a/web/src/components/floating-chat-widget-markdown.tsx b/web/src/components/floating-chat-widget-markdown.tsx index 4237fa1758..51912d72af 100644 --- a/web/src/components/floating-chat-widget-markdown.tsx +++ b/web/src/components/floating-chat-widget-markdown.tsx @@ -36,8 +36,7 @@ import { } from 'react-syntax-highlighter/dist/esm/styles/prism'; import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; -import remarkGfm from 'remark-gfm'; -import remarkMath from 'remark-math'; +import { MarkdownRemarkPlugins } from '@/constants/markdown-remark-plugins'; import { visitParents } from 'unist-util-visit-parents'; import styles from './floating-chat-widget-markdown.module.less'; import { useIsDarkTheme } from './theme-provider'; @@ -292,7 +291,7 @@ const FloatingChatWidgetMarkdown = ({

{children}

, diff --git a/web/src/components/next-markdown-content/index.tsx b/web/src/components/next-markdown-content/index.tsx index ebedb0eed6..1bab4440a7 100644 --- a/web/src/components/next-markdown-content/index.tsx +++ b/web/src/components/next-markdown-content/index.tsx @@ -10,8 +10,7 @@ import Markdown from 'react-markdown'; import SyntaxHighlighter from 'react-syntax-highlighter'; import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; -import remarkGfm from 'remark-gfm'; -import remarkMath from 'remark-math'; +import { MarkdownRemarkPlugins } from '@/constants/markdown-remark-plugins'; import { visitParents } from 'unist-util-visit-parents'; import { useTranslation } from 'react-i18next'; @@ -355,7 +354,7 @@ function MarkdownContent({

{children}

, diff --git a/web/src/constants/markdown-remark-plugins.ts b/web/src/constants/markdown-remark-plugins.ts new file mode 100644 index 0000000000..9d797082bb --- /dev/null +++ b/web/src/constants/markdown-remark-plugins.ts @@ -0,0 +1,17 @@ +import remarkBreaks from 'remark-breaks'; +import remarkGfm from 'remark-gfm'; +import remarkMath from 'remark-math'; + +/** + * GFM + line breaks only (no TeX). For surfaces that do not wire rehype-katex + * (e.g. uploaded document preview). + */ +export const MarkdownRemarkPluginsLite = [remarkGfm, remarkBreaks]; + +/** + * Shared Markdown pipeline for assistant-style content: + * - remark-gfm: GFM tables, task lists, strikethrough, autolinks, etc. + * - remark-math: TeX ($...$ / $$...$$); pair with rehype-katex on render. + * - remark-breaks: treat single newlines as hard breaks (common in LLM chat). + */ +export const MarkdownRemarkPlugins = [remarkGfm, remarkMath, remarkBreaks]; diff --git a/web/src/pages/next-search/markdown-content/index.tsx b/web/src/pages/next-search/markdown-content/index.tsx index 132f34d5df..7149e2b8f0 100644 --- a/web/src/pages/next-search/markdown-content/index.tsx +++ b/web/src/pages/next-search/markdown-content/index.tsx @@ -8,8 +8,7 @@ import Markdown from 'react-markdown'; import SyntaxHighlighter from 'react-syntax-highlighter'; import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; -import remarkGfm from 'remark-gfm'; -import remarkMath from 'remark-math'; +import { MarkdownRemarkPlugins } from '@/constants/markdown-remark-plugins'; import { visitParents } from 'unist-util-visit-parents'; import { useTranslation } from 'react-i18next'; @@ -250,7 +249,7 @@ const MarkdownContent = ({ >

{children}

,