mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
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:
parent
97257839de
commit
a01e6b933b
@ -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) {
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user