feat(mermaid): update Mermaid integration and improve rendering logic

- Upgraded Mermaid script to version 11.6.0.
- Refactored rendering logic to use a debounced function for improved performance.
- Added event listener for 'mermaid-loaded' to trigger rendering.
- Enhanced error handling during Mermaid chart rendering in both main and popup components.
- Removed unnecessary initialization calls and streamlined the use of theme settings.
This commit is contained in:
kangfenmao 2025-04-23 20:10:44 +08:00
parent 97257839de
commit a01e6b933b
3 changed files with 91 additions and 42 deletions

View File

@ -1,42 +1,30 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { EventEmitter } from '@renderer/services/EventService'
import { ThemeMode } from '@renderer/types'
import { loadScript, runAsyncFunction } from '@renderer/utils'
import { useEffect } from 'react'
import { useRuntime } from './useRuntime'
import { useEffect, useRef } from 'react'
export const useMermaid = () => {
const { theme } = useTheme()
const { generating } = useRuntime()
const mermaidLoaded = useRef(false)
useEffect(() => {
runAsyncFunction(async () => {
if (!window.mermaid) {
await loadScript('https://unpkg.com/mermaid@11.4.0/dist/mermaid.min.js')
await loadScript('https://unpkg.com/mermaid@11.6.0/dist/mermaid.min.js')
}
if (!mermaidLoaded.current) {
await window.mermaid.initialize({
startOnLoad: false,
theme: theme === ThemeMode.dark ? 'dark' : 'default'
})
mermaidLoaded.current = true
EventEmitter.emit('mermaid-loaded')
}
window.mermaid.initialize({
startOnLoad: true,
theme: theme === ThemeMode.dark ? 'dark' : 'default'
})
})
}, [theme])
useEffect(() => {
if (!window.mermaid || generating) return
const renderMermaid = () => {
const mermaidElements = document.querySelectorAll('.mermaid')
mermaidElements.forEach((element) => {
if (!element.querySelector('svg')) {
element.removeAttribute('data-processed')
}
})
window.mermaid.contentLoaded()
}
setTimeout(renderMermaid, 100)
}, [generating])
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey || e.metaKey) {

View File

@ -1,6 +1,8 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { EventEmitter } from '@renderer/services/EventService'
import { ThemeMode } from '@renderer/types'
import React, { useEffect, useRef } from 'react'
import { debounce, isEmpty } from 'lodash'
import React, { useCallback, useEffect, useRef } from 'react'
import MermaidPopup from './MermaidPopup'
@ -12,20 +14,44 @@ const Mermaid: React.FC<Props> = ({ chart }) => {
const { theme } = useTheme()
const mermaidRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (mermaidRef.current && window.mermaid) {
const renderMermaidBase = useCallback(async () => {
if (!mermaidRef.current || !window.mermaid || isEmpty(chart)) return
try {
mermaidRef.current.innerHTML = chart
mermaidRef.current.removeAttribute('data-processed')
if (window.mermaid.initialize) {
window.mermaid.initialize({
startOnLoad: true,
theme: theme === ThemeMode.dark ? 'dark' : 'default'
})
}
window.mermaid.contentLoaded()
await window.mermaid.initialize({
startOnLoad: true,
theme: theme === ThemeMode.dark ? 'dark' : 'default'
})
await window.mermaid.run({ nodes: [mermaidRef.current] })
} catch (error) {
console.error('Failed to render mermaid chart:', error)
}
}, [chart, theme])
const renderMermaid = useCallback(debounce(renderMermaidBase, 1000), [renderMermaidBase])
useEffect(() => {
renderMermaid()
// Make sure to cancel any pending debounced calls when unmounting
return () => renderMermaid.cancel()
}, [renderMermaid])
useEffect(() => {
setTimeout(renderMermaidBase, 0)
}, [])
useEffect(() => {
const removeListener = EventEmitter.on('mermaid-loaded', renderMermaid)
return () => {
removeListener()
renderMermaid.cancel()
}
}, [renderMermaid])
const onPreview = () => {
MermaidPopup.show({ chart })
}

View File

@ -1,4 +1,7 @@
import { TopView } from '@renderer/components/TopView'
import { useTheme } from '@renderer/context/ThemeProvider'
import { ThemeMode } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { download } from '@renderer/utils/download'
import { Button, Modal, Space, Tabs } from 'antd'
import { useEffect, useState } from 'react'
@ -16,6 +19,7 @@ interface Props extends ShowParams {
const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { theme } = useTheme()
const mermaidId = `mermaid-popup-${Date.now()}`
const [activeTab, setActiveTab] = useState('preview')
const [scale, setScale] = useState(1)
@ -97,19 +101,21 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
if (!element) return
const timestamp = Date.now()
const backgroundColor = theme === ThemeMode.dark ? '#1F1F1F' : '#fff'
const svgElement = element.querySelector('svg')
if (!svgElement) return
if (format === 'svg') {
const svgElement = element.querySelector('svg')
if (!svgElement) return
// Add background color to SVG
svgElement.style.backgroundColor = backgroundColor
const svgData = new XMLSerializer().serializeToString(svgElement)
const blob = new Blob([svgData], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
download(url, `mermaid-diagram-${timestamp}.svg`)
URL.revokeObjectURL(url)
} else if (format === 'png') {
const svgElement = element.querySelector('svg')
if (!svgElement) return
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
@ -119,6 +125,9 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
const width = viewBox[2] || svgElement.clientWidth || svgElement.getBoundingClientRect().width
const height = viewBox[3] || svgElement.clientHeight || svgElement.getBoundingClientRect().height
// Add background color to SVG before converting to image
svgElement.style.backgroundColor = backgroundColor
const svgData = new XMLSerializer().serializeToString(svgElement)
const svgBase64 = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`
@ -129,6 +138,9 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
if (ctx) {
ctx.scale(scale, scale)
// Fill background
ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, width, height)
ctx.drawImage(img, 0, 0, width, height)
}
@ -142,6 +154,7 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
}
img.src = svgBase64
}
svgElement.style.backgroundColor = 'transparent'
} catch (error) {
console.error('Download failed:', error)
}
@ -153,8 +166,30 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
}
useEffect(() => {
window?.mermaid?.contentLoaded()
}, [])
runAsyncFunction(async () => {
if (!window.mermaid) return
try {
const element = document.getElementById(mermaidId)
if (!element) return
// Clear previous content
element.innerHTML = chart
element.removeAttribute('data-processed')
await window.mermaid.initialize({
startOnLoad: false,
theme: theme === ThemeMode.dark ? 'dark' : 'default'
})
await window.mermaid.run({
nodes: [element]
})
} catch (error) {
console.error('Failed to render mermaid chart in popup:', error)
}
})
}, [activeTab, theme, mermaidId, chart])
return (
<Modal