fix: shadow markdown (#1871)

问题:[Bug]: 当模型回复的html代码没有正确在代码框中时,html代码内容会影响到UI界面 #1767
原因:解析html之后css会污染应用样式
解决:将markdown完全放入shadow dom中,shadow dom天然隔绝样式,即可解决

* feat: Conditionally hide thinking loader for paused messages

* feat: Implement Shadow DOM for Markdown rendering

* feat: Add StyleProvider to Shadow DOM Markdown rendering

* fix: Refactor Markdown rendering with inline ShadowDOM component

Modify ReactMarkdown component to use style component for ShadowDOM rendering instead of wrapping component, simplifying the rendering approach

---------

Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
This commit is contained in:
MyPrototypeWhat 2025-03-03 18:23:18 +08:00 committed by GitHub
parent a12d10f4f7
commit 13b465fe73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 69 additions and 4 deletions

View File

@ -0,0 +1,63 @@
import { StyleProvider } from '@ant-design/cssinjs'
import React, { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { StyleSheetManager } from 'styled-components'
interface Props {
children: React.ReactNode
}
const ShadowDOMRenderer: React.FC<Props> = ({ children }) => {
const hostRef = useRef<HTMLDivElement>(null)
const [shadowRoot, setShadowRoot] = React.useState<ShadowRoot | null>(null)
useEffect(() => {
const host = hostRef.current
if (!host) return
// 创建 shadow root
const shadow = host.shadowRoot || host.attachShadow({ mode: 'open' })
// 获取原始样式表
const markdownStyleSheet = Array.from(document.styleSheets).find((sheet) => {
try {
return Array.from(sheet.cssRules).some((rule: CSSRule) => {
return rule.cssText?.includes('.markdown')
})
} catch {
return false
}
})
if (markdownStyleSheet) {
const style = document.createElement('style')
const cssRules = Array.from(markdownStyleSheet.cssRules)
.map((rule) => rule.cssText)
.join('\n')
style.textContent = cssRules
shadow.appendChild(style)
}
setShadowRoot(shadow)
}, [])
if (!shadowRoot) {
return <div ref={hostRef} />
}
return (
<div ref={hostRef}>
{createPortal(
<StyleSheetManager target={shadowRoot}>
<StyleProvider container={shadowRoot} layer>
{children}
</StyleProvider>
</StyleSheetManager>,
shadowRoot
)}
</div>
)
}
export default ShadowDOMRenderer

View File

@ -1,13 +1,14 @@
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import 'katex/dist/contrib/copy-tex' import 'katex/dist/contrib/copy-tex'
import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { Message } from '@renderer/types' import type { Message } from '@renderer/types'
import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats' import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useMemo } from 'react' import { type FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReactMarkdown, { Components } from 'react-markdown' import ReactMarkdown, { type Components } from 'react-markdown'
import rehypeKatex from 'rehype-katex' import rehypeKatex from 'rehype-katex'
// @ts-ignore next-line // @ts-ignore next-line
import rehypeMathjax from 'rehype-mathjax' import rehypeMathjax from 'rehype-mathjax'
@ -52,11 +53,12 @@ const Markdown: FC<Props> = ({ message }) => {
return ( return (
<ReactMarkdown <ReactMarkdown
className="markdown"
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}
remarkPlugins={[remarkMath, remarkGfm]} remarkPlugins={[remarkMath, remarkGfm]}
className="markdown"
components={ components={
{ {
style: MarkdownShadowDOMRenderer,
a: Link, a: Link,
code: CodeBlock, code: CodeBlock,
img: ImagePreview img: ImagePreview