Files
cherry-studio/src/renderer/src/components/Preview/MermaidPreview.tsx
T
one d05d1309ca refactor(Preview,CodeBlock): preview components and tools (#8565)
* refactor(CodeBlockView): generalize tool and preview

Generalize code tool to action tool
- CodeTool -> ActionTool
- usePreviewTools -> useImagePreview
- rename code tool classname from icon to tool-icon

Generalize preview
- move image preview components to Preview dir
- simplify file names

* refactor(useImageTools): simplify implementation, add pan

* refactor(Preview): move image tools to floating toolbar

* refactor: add enableDrag, enable zooming for SvgPreview

* test: add tests for preview components

* feat(Preview): add download buttons to dropdown

* refactor(Preview): remove setTools from preview, improve SvgPreview

* refactor: add useTemporaryValue

* test: add tests for hooks

* test: add tests for CodeToolButton and CodeToolbar

* refactor(PreviewTool): add a setting item to enable preview tools

* test: update snapshot

* refactor: extract more code tools to hooks, add tests

* refactor: extract tools from CodeEditor and CodeViewer

* test: add tests for new tool hooks

* refactor(CodeEditor): change collapsible to expanded, change wrappable to unwrapped

* refactor: migrate codePreview to codeViewer

* docs: CodeBlockView

* refactor: add custom file icons, center the reset button

* refactor: improve code quality

* refactor: improve migration by deprecating codePreview

* refactor: improve PlantUml and svgToCanvas

* fix: plantuml style

* test: fix tests

* fix: button icon

* refactor(SvgPreview): debounce rendering

* feat(PreviewTool): add a dialog tool

* fix: remove isValidPlantUML, improve plantuml rendering

* refactor: extract shadow dom renderer

* refactor: improve plantuml error messages

* test: add tests for ImageToolbar and ImageToolButton

* refactor: add ImagePreviewLayout to simplify layout and tests

* refactor: add useDebouncedRender, update docs

* chore: clean up unused props

* refactor: clean transformation before copy/download/preview

* refactor: update migrate version

* refactor: style refactoring and fixes

- show header background in split view
- fix status bar radius
- reset special view transformation on theme change
- fix wrap tool icon
- add a divider to split view
- improve split view toggling (switch back to previous view)
- revert copy tool to separate tools
- fix top border radius for special views

* refactor: move GraphvizPreview to shadow DOM

- use renderString
- keep renderSvgInShadowHost api consistent with others

* fix: tests, icons, deleted files

* refactor: use ResetIcon in ImageToolbar

* test: remove unnecessary tests

* fix: min height for special preview

* fix: update migrate
2025-08-06 20:09:49 +08:00

121 lines
3.5 KiB
TypeScript

import { nanoid } from '@reduxjs/toolkit'
import { useMermaid } from '@renderer/hooks/useMermaid'
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import { useDebouncedRender } from './hooks/useDebouncedRender'
import ImagePreviewLayout from './ImagePreviewLayout'
import { BasicPreviewHandles, BasicPreviewProps } from './types'
/** 预览 Mermaid 图表
* 使用 usePreviewRenderer hook 重构,同时保留必要的可见性检测逻辑
* FIXME: 等将来 mermaid-js 修复可见性问题后可以进一步简化
*/
const MermaidPreview = ({
children,
enableToolbar = false,
ref
}: BasicPreviewProps & { ref?: React.RefObject<BasicPreviewHandles | null> }) => {
const { mermaid, isLoading: isLoadingMermaid, error: mermaidError } = useMermaid()
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
const [isVisible, setIsVisible] = useState(true)
// 定义渲染函数
const renderMermaid = useCallback(
async (content: string, container: HTMLDivElement) => {
// 验证语法,提前抛出异常
await mermaid.parse(content)
const { svg } = await mermaid.render(diagramId, content, container)
// 避免不可见时产生 undefined 和 NaN
const fixedSvg = svg.replace(/translate\(undefined,\s*NaN\)/g, 'translate(0, 0)')
container.innerHTML = fixedSvg
},
[diagramId, mermaid]
)
// 可见性检测函数
const shouldRender = useCallback(() => {
return !isLoadingMermaid && isVisible
}, [isLoadingMermaid, isVisible])
// 使用预览渲染器 hook
const {
containerRef,
error: renderError,
isLoading: isRendering
} = useDebouncedRender(children, renderMermaid, {
debounceDelay: 300,
shouldRender
})
/**
* 监听可见性变化,用于触发重新渲染。
* 这是为了解决 `MessageGroup` 组件的 `fold` 布局中被 `display: none` 隐藏的图标无法正确渲染的问题。
* 监听时向上遍历到第一个有 `fold` className 的父节点为止(也就是目前的 `MessageWrapper`)。
* FIXME: 将来 mermaid-js 修复此问题后可以移除这里的相关逻辑。
*/
useEffect(() => {
if (!containerRef.current) return
const checkVisibility = () => {
const element = containerRef.current
if (!element) return
const currentlyVisible = element.offsetParent !== null
setIsVisible(currentlyVisible)
}
// 初始检查
checkVisibility()
const observer = new MutationObserver(() => {
checkVisibility()
})
let targetElement = containerRef.current.parentElement
while (targetElement) {
observer.observe(targetElement, {
attributes: true,
attributeFilter: ['class', 'style']
})
if (targetElement.className?.includes('fold')) {
break
}
targetElement = targetElement.parentElement
}
return () => {
observer.disconnect()
}
}, [containerRef])
// 合并加载状态和错误状态
const isLoading = isLoadingMermaid || isRendering
const error = mermaidError || renderError
return (
<ImagePreviewLayout
loading={isLoading}
error={error}
enableToolbar={enableToolbar}
ref={ref}
imageRef={containerRef}
source="mermaid">
<StyledMermaid ref={containerRef} className="mermaid special-preview" />
</ImagePreviewLayout>
)
}
const StyledMermaid = styled.div`
overflow: auto;
position: relative;
width: 100%;
height: 100%;
`
export default memo(MermaidPreview)