mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 20:00:00 +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
5d04ef2508
commit
3df4680c7b
@ -1,42 +1,30 @@
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { EventEmitter } from '@renderer/services/EventService'
|
||||||
import { ThemeMode } from '@renderer/types'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import { loadScript, runAsyncFunction } from '@renderer/utils'
|
import { loadScript, runAsyncFunction } from '@renderer/utils'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import { useRuntime } from './useRuntime'
|
|
||||||
|
|
||||||
export const useMermaid = () => {
|
export const useMermaid = () => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { generating } = useRuntime()
|
const mermaidLoaded = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
if (!window.mermaid) {
|
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])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { EventEmitter } from '@renderer/services/EventService'
|
||||||
import { ThemeMode } from '@renderer/types'
|
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'
|
import MermaidPopup from './MermaidPopup'
|
||||||
|
|
||||||
@ -12,20 +14,44 @@ const Mermaid: React.FC<Props> = ({ chart }) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const mermaidRef = useRef<HTMLDivElement>(null)
|
const mermaidRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
const renderMermaidBase = useCallback(async () => {
|
||||||
if (mermaidRef.current && window.mermaid) {
|
if (!mermaidRef.current || !window.mermaid || isEmpty(chart)) return
|
||||||
|
|
||||||
|
try {
|
||||||
mermaidRef.current.innerHTML = chart
|
mermaidRef.current.innerHTML = chart
|
||||||
mermaidRef.current.removeAttribute('data-processed')
|
mermaidRef.current.removeAttribute('data-processed')
|
||||||
if (window.mermaid.initialize) {
|
|
||||||
window.mermaid.initialize({
|
await window.mermaid.initialize({
|
||||||
startOnLoad: true,
|
startOnLoad: true,
|
||||||
theme: theme === ThemeMode.dark ? 'dark' : 'default'
|
theme: theme === ThemeMode.dark ? 'dark' : 'default'
|
||||||
})
|
})
|
||||||
}
|
|
||||||
window.mermaid.contentLoaded()
|
await window.mermaid.run({ nodes: [mermaidRef.current] })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to render mermaid chart:', error)
|
||||||
}
|
}
|
||||||
}, [chart, theme])
|
}, [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 = () => {
|
const onPreview = () => {
|
||||||
MermaidPopup.show({ chart })
|
MermaidPopup.show({ chart })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import { TopView } from '@renderer/components/TopView'
|
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 { download } from '@renderer/utils/download'
|
||||||
import { Button, Modal, Space, Tabs } from 'antd'
|
import { Button, Modal, Space, Tabs } from 'antd'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@ -16,6 +19,7 @@ interface Props extends ShowParams {
|
|||||||
const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { theme } = useTheme()
|
||||||
const mermaidId = `mermaid-popup-${Date.now()}`
|
const mermaidId = `mermaid-popup-${Date.now()}`
|
||||||
const [activeTab, setActiveTab] = useState('preview')
|
const [activeTab, setActiveTab] = useState('preview')
|
||||||
const [scale, setScale] = useState(1)
|
const [scale, setScale] = useState(1)
|
||||||
@ -97,19 +101,21 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
|||||||
if (!element) return
|
if (!element) return
|
||||||
|
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
|
const backgroundColor = theme === ThemeMode.dark ? '#1F1F1F' : '#fff'
|
||||||
|
const svgElement = element.querySelector('svg')
|
||||||
|
|
||||||
|
if (!svgElement) return
|
||||||
|
|
||||||
if (format === 'svg') {
|
if (format === 'svg') {
|
||||||
const svgElement = element.querySelector('svg')
|
// Add background color to SVG
|
||||||
if (!svgElement) return
|
svgElement.style.backgroundColor = backgroundColor
|
||||||
|
|
||||||
const svgData = new XMLSerializer().serializeToString(svgElement)
|
const svgData = new XMLSerializer().serializeToString(svgElement)
|
||||||
const blob = new Blob([svgData], { type: 'image/svg+xml' })
|
const blob = new Blob([svgData], { type: 'image/svg+xml' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
download(url, `mermaid-diagram-${timestamp}.svg`)
|
download(url, `mermaid-diagram-${timestamp}.svg`)
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
} else if (format === 'png') {
|
} else if (format === 'png') {
|
||||||
const svgElement = element.querySelector('svg')
|
|
||||||
if (!svgElement) return
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
const img = new Image()
|
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 width = viewBox[2] || svgElement.clientWidth || svgElement.getBoundingClientRect().width
|
||||||
const height = viewBox[3] || svgElement.clientHeight || svgElement.getBoundingClientRect().height
|
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 svgData = new XMLSerializer().serializeToString(svgElement)
|
||||||
const svgBase64 = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`
|
const svgBase64 = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`
|
||||||
|
|
||||||
@ -129,6 +138,9 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
|||||||
|
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.scale(scale, scale)
|
ctx.scale(scale, scale)
|
||||||
|
// Fill background
|
||||||
|
ctx.fillStyle = backgroundColor
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
ctx.drawImage(img, 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
|
img.src = svgBase64
|
||||||
}
|
}
|
||||||
|
svgElement.style.backgroundColor = 'transparent'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Download failed:', error)
|
console.error('Download failed:', error)
|
||||||
}
|
}
|
||||||
@ -153,8 +166,30 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user