@ -1,5 +1,5 @@
import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react'
import mermaid from 'mermaid'
import mermaid , { type MermaidConfig } from 'mermaid'
import { useTranslation } from 'react-i18next'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { MoonIcon , SunIcon } from '@heroicons/react/24/solid'
@ -68,14 +68,13 @@ const THEMES = {
const initMermaid = ( ) = > {
if ( typeof window !== 'undefined' && ! isMermaidInitialized ) {
try {
mermaid . initialize ( {
const config : MermaidConfig = {
startOnLoad : false ,
fontFamily : 'sans-serif' ,
securityLevel : 'loose' ,
flowchart : {
htmlLabels : true ,
useMaxWidth : true ,
diagramPadding : 10 ,
curve : 'basis' ,
nodeSpacing : 50 ,
rankSpacing : 70 ,
@ -94,10 +93,10 @@ const initMermaid = () => {
mindmap : {
useMaxWidth : true ,
padding : 10 ,
diagramPadding : 20 ,
} ,
maxTextSize : 50000 ,
} )
}
mermaid . initialize ( config )
isMermaidInitialized = true
}
catch ( error ) {
@ -113,7 +112,7 @@ const Flowchart = React.forwardRef((props: {
theme ? : 'light' | 'dark'
} , ref ) = > {
const { t } = useTranslation ( )
const [ svg Code, setSvgCode ] = useState < string | null > ( null )
const [ svg String, setSvgString ] = useState < string | null > ( null )
const [ look , setLook ] = useState < 'classic' | 'handDrawn' > ( 'classic' )
const [ isInitialized , setIsInitialized ] = useState ( false )
const [ currentTheme , setCurrentTheme ] = useState < 'light' | 'dark' > ( props . theme || 'light' )
@ -125,6 +124,7 @@ const Flowchart = React.forwardRef((props: {
const [ imagePreviewUrl , setImagePreviewUrl ] = useState ( '' )
const [ isCodeComplete , setIsCodeComplete ] = useState ( false )
const codeCompletionCheckRef = useRef < NodeJS.Timeout > ( )
const prevCodeRef = useRef < string > ( )
// Create cache key from code, style and theme
const cacheKey = useMemo ( ( ) = > {
@ -169,50 +169,18 @@ const Flowchart = React.forwardRef((props: {
* /
const handleRenderError = ( error : any ) = > {
console . error ( 'Mermaid rendering error:' , error )
const errorMsg = ( error as Error ) . message
if ( errorMsg . includes ( 'getAttribute' ) ) {
diagramCache . clear ( )
mermaid . initialize ( {
startOnLoad : false ,
securityLevel : 'loose' ,
} )
}
else {
setErrMsg ( ` Rendering chart failed, please refresh and try again ${ look === 'handDrawn' ? 'Or try using classic mode' : '' } ` )
// On any render error, assume the mermaid state is corrupted and force a re-initialization.
try {
diagramCache . clear ( ) // Clear cache to prevent using potentially corrupted SVGs
isMermaidInitialized = false // <-- THE FIX: Force re-initialization
initMermaid ( ) // Re-initialize with the default safe configuration
}
if ( look === 'handDrawn' ) {
try {
// Clear possible cache issues
diagramCache . delete ( ` ${ props . PrimitiveCode } -handDrawn- ${ currentTheme } ` )
// Reset mermaid configuration
mermaid . initialize ( {
startOnLoad : false ,
securityLevel : 'loose' ,
theme : 'default' ,
maxTextSize : 50000 ,
} )
// Try rendering with standard mode
setLook ( 'classic' )
setErrMsg ( 'Hand-drawn mode is not supported for this diagram. Switched to classic mode.' )
// Delay error clearing
setTimeout ( ( ) = > {
if ( containerRef . current ) {
// Try rendering again with standard mode, but can't call renderFlowchart directly due to circular dependency
// Instead set state to trigger re-render
setIsCodeComplete ( true ) // This will trigger useEffect re-render
}
} , 500 )
}
catch ( e ) {
console . error ( 'Reset after handDrawn error failed:' , e )
}
catch ( reinitError ) {
console . error ( 'Failed to re-initialize Mermaid after error:' , reinitError )
}
setErrMsg ( ` Rendering failed: ${ ( error as Error ) . message || 'Unknown error. Please check the console.' } ` )
setIsLoading ( false )
}
@ -223,51 +191,23 @@ const Flowchart = React.forwardRef((props: {
setIsInitialized ( true )
} , [ ] )
// Update theme when prop changes
// Update theme when prop changes, but allow internal override.
const prevThemeRef = useRef < string > ( )
useEffect ( ( ) = > {
if ( props . theme )
// Only react if the theme prop from the outside has actually changed.
if ( props . theme && props . theme !== prevThemeRef . current ) {
// When the global theme prop changes, it should act as the source of truth,
// overriding any local theme selection.
diagramCache . clear ( )
setSvgString ( null )
setCurrentTheme ( props . theme )
} , [ props . theme ] )
// Validate mermaid code and check for completeness
useEffect ( ( ) = > {
if ( codeCompletionCheckRef . current )
clearTimeout ( codeCompletionCheckRef . current )
// Reset code complete status when code changes
setIsCodeComplete ( false )
// If no code or code is extremely short, don't proceed
if ( ! props . PrimitiveCode || props . PrimitiveCode . length < 10 )
return
// Check if code already in cache - if so we know it's valid
if ( diagramCache . has ( cacheKey ) ) {
setIsCodeComplete ( true )
return
}
// Initial check using the extracted isMermaidCodeComplete function
const isComplete = isMermaidCodeComplete ( props . PrimitiveCode )
if ( isComplete ) {
setIsCodeComplete ( true )
return
// Reset look to classic for a consistent state after a global change.
setLook ( 'classic' )
}
// Update the ref to the current prop value for the next render.
prevThemeRef . current = props . theme
} , [ props . theme ] )
// Set a delay to check again in case code is still being generated
codeCompletionCheckRef . current = setTimeout ( ( ) = > {
setIsCodeComplete ( isMermaidCodeComplete ( props . PrimitiveCode ) )
} , 300 )
return ( ) = > {
if ( codeCompletionCheckRef . current )
clearTimeout ( codeCompletionCheckRef . current )
}
} , [ props . PrimitiveCode , cacheKey ] )
/ * *
* Renders flowchart based on provided code
* /
const renderFlowchart = useCallback ( async ( primitiveCode : string ) = > {
if ( ! isInitialized || ! containerRef . current ) {
setIsLoading ( false )
@ -275,15 +215,11 @@ const Flowchart = React.forwardRef((props: {
return
}
// Don't render if code is not complete yet
if ( ! isCodeComplete ) {
setIsLoading ( true )
return
}
// Return cached result if available
const cacheKey = ` ${ primitiveCode } - ${ look } - ${ currentTheme } `
if ( diagramCache . has ( cacheKey ) ) {
setSvgCode ( diagramCache . get ( cacheKey ) || null )
setErrMsg ( '' )
setSvgString ( diagramCache . get ( cacheKey ) || null )
setIsLoading ( false )
return
}
@ -294,17 +230,45 @@ const Flowchart = React.forwardRef((props: {
try {
let finalCode : string
// Check if it's a gantt chart or mindmap
const isGanttChart = primitiveCode . trim ( ) . startsWith ( 'gantt' )
const isMindMap = primitiveCode . trim ( ) . startsWith ( 'mindmap' )
if ( isGanttChart || isMindMap ) {
// For gantt charts and mindmaps, ensure each task is on its own line
// and preserve exact whitespace/format
finalCode = primitiveCode . trim ( )
const trimmedCode = primitiveCode . trim ( )
const isGantt = trimmedCode . startsWith ( 'gantt' )
const isMindMap = trimmedCode . startsWith ( 'mindmap' )
const isSequence = trimmedCode . startsWith ( 'sequenceDiagram' )
if ( isGantt || isMindMap || isSequence ) {
if ( isGantt ) {
finalCode = trimmedCode
. split ( '\n' )
. map ( ( line ) = > {
// Gantt charts have specific syntax needs.
const taskMatch = line . match ( /^\s*([^:]+?)\s*:\s*(.*)/ )
if ( ! taskMatch )
return line // Not a task line, return as is.
const taskName = taskMatch [ 1 ] . trim ( )
let paramsStr = taskMatch [ 2 ] . trim ( )
// Rule 1: Correct multiple "after" dependencies ONLY if they exist.
// This is a common mistake, e.g., "..., after task1, after task2, ..."
const afterCount = ( paramsStr . match ( /after /g ) || [ ] ) . length
if ( afterCount > 1 )
paramsStr = paramsStr . replace ( /,\s*after\s+/g , ' ' )
// Rule 2: Normalize spacing between parameters for consistency.
const finalParams = paramsStr . replace ( /\s*,\s*/g , ', ' ) . trim ( )
return ` ${ taskName } : ${ finalParams } `
} )
. join ( '\n' )
}
else {
// For mindmap and sequence charts, which are sensitive to syntax,
// pass the code through directly.
finalCode = trimmedCode
}
}
else {
// Step 1: Clean and prepare Mermaid code using the extracted prepareMermaidCode function
// This function handles flowcharts appropriately.
finalCode = prepareMermaidCode ( primitiveCode , look )
}
@ -319,13 +283,12 @@ const Flowchart = React.forwardRef((props: {
THEMES ,
)
// Step 4: Clean SVG code and convert to base64 using the extracted functions
// Step 4: Clean up SVG code
const cleanedSvg = cleanUpSvgCode ( processedSvg )
const base64Svg = await svgToBase64 ( cleanedSvg )
if ( base64Svg && typeof base64 Svg === 'string' ) {
diagramCache . set ( cacheKey , base64 Svg)
setSvg Code( base64 Svg)
if ( cleanedSvg && typeof cleaned Svg === 'string' ) {
diagramCache . set ( cacheKey , cleaned Svg)
setSvg String( cleaned Svg)
}
setIsLoading ( false )
@ -334,12 +297,9 @@ const Flowchart = React.forwardRef((props: {
// Error handling
handleRenderError ( error )
}
} , [ chartId , isInitialized , cacheKey, isCodeComplete , look, currentTheme , t ] )
} , [ chartId , isInitialized , look, currentTheme , t ] )
/ * *
* Configure mermaid based on selected style and theme
* /
const configureMermaid = useCallback ( ( ) = > {
const configureMermaid = useCallback ( ( primitiveCode : string ) = > {
if ( typeof window !== 'undefined' && isInitialized ) {
const themeVars = THEMES [ currentTheme ]
const config : any = {
@ -361,23 +321,37 @@ const Flowchart = React.forwardRef((props: {
mindmap : {
useMaxWidth : true ,
padding : 10 ,
diagramPadding : 20 ,
} ,
}
const isFlowchart = primitiveCode . trim ( ) . startsWith ( 'graph' ) || primitiveCode . trim ( ) . startsWith ( 'flowchart' )
if ( look === 'classic' ) {
config . theme = currentTheme === 'dark' ? 'dark' : 'neutral'
config . flowchart = {
htmlLabels : true ,
useMaxWidth : true ,
diagramPadding : 12 ,
nodeSpacing : 60 ,
rankSpacing : 80 ,
curve : 'linear' ,
ranker : 'tight-tree' ,
if ( isFlowchart ) {
config . flowchart = {
htmlLabels : true ,
useMaxWidth : true ,
nodeSpacing : 60 ,
rankSpacing : 80 ,
curve : 'linear' ,
ranker : 'tight-tree' ,
}
}
if ( currentTheme === 'dark' ) {
config . themeVariables = {
background : themeVars.background ,
primaryColor : themeVars.primaryColor ,
primaryBorderColor : themeVars.primaryBorderColor ,
primaryTextColor : themeVars.primaryTextColor ,
secondaryColor : themeVars.secondaryColor ,
tertiaryColor : themeVars.tertiaryColor ,
}
}
}
else {
else { // look === 'handDrawn'
config . theme = 'default'
config . themeCSS = `
. node rect { fill - opacity : 0.85 ; }
@ -389,27 +363,17 @@ const Flowchart = React.forwardRef((props: {
config . themeVariables = {
fontSize : '14px' ,
fontFamily : 'sans-serif' ,
primaryBorderColor : currentTheme === 'dark' ? THEMES.dark.connectionColor : THEMES.light.connectionColor ,
}
config . flowchart = {
htmlLabels : true ,
useMaxWidth : true ,
diagramPadding : 10 ,
nodeSpacing : 40 ,
rankSpacing : 60 ,
curve : 'basis' ,
}
config . themeVariables . primaryBorderColor = currentTheme === 'dark' ? THEMES.dark.connectionColor : THEMES.light.connectionColor
}
if ( currentTheme === 'dark' && ! config . themeVariables ) {
config . themeVariables = {
background : themeVars.background ,
primaryColor : themeVars.primaryColor ,
primaryBorderColor : themeVars.primaryBorderColor ,
primaryTextColor : themeVars.primaryTextColor ,
secondaryColor : themeVars.secondaryColor ,
tertiaryColor : themeVars.tertiaryColor ,
fontFamily : 'sans-serif' ,
if ( isFlowchart ) {
config . flowchart = {
htmlLabels : true ,
useMaxWidth : true ,
nodeSpacing : 40 ,
rankSpacing : 60 ,
curve : 'basis' ,
}
}
}
@ -425,44 +389,50 @@ const Flowchart = React.forwardRef((props: {
return false
} , [ currentTheme , isInitialized , look ] )
// Effect for theme and style configuration
// This is the main rendering effect.
// It triggers whenever the code, theme, or style changes.
useEffect ( ( ) = > {
if ( diagramCache . has ( cacheKey ) ) {
setSvgCode ( diagramCache . get ( cacheKey ) || null )
setIsLoading ( false )
if ( ! isInitialized )
return
}
if ( configureMermaid ( ) && containerRef . current && isCodeComplete )
renderFlowchart ( props . PrimitiveCode )
} , [ look , props . PrimitiveCode , renderFlowchart , isInitialized , cacheKey , currentTheme , isCodeComplete , configureMermaid ] )
// Effect for rendering with debounce
useEffect ( ( ) = > {
if ( diagramCache . has ( cacheKey ) ) {
setSvgCode ( diagramCache . get ( cacheKey ) || null )
// Don't render if code is too short
if ( ! props . PrimitiveCode || props . PrimitiveCode . length < 10 ) {
setIsLoading ( false )
setSvgString ( null )
return
}
// Use a timeout to handle streaming code and debounce rendering
if ( renderTimeoutRef . current )
clearTimeout ( renderTimeoutRef . current )
if ( isCodeComplete ) {
renderTimeoutRef . current = setTimeout ( ( ) = > {
if ( isInitialized )
renderFlowchart ( props . PrimitiveCode )
} , 300 )
}
else {
setIsLoading ( true )
}
setIsLoading ( true )
renderTimeoutRef . current = setTimeout ( ( ) = > {
// Final validation before rendering
if ( ! isMermaidCodeComplete ( props . PrimitiveCode ) ) {
setIsLoading ( false )
setErrMsg ( 'Diagram code is not complete or invalid.' )
return
}
const cacheKey = ` ${ props . PrimitiveCode } - ${ look } - ${ currentTheme } `
if ( diagramCache . has ( cacheKey ) ) {
setErrMsg ( '' )
setSvgString ( diagramCache . get ( cacheKey ) || null )
setIsLoading ( false )
return
}
if ( configureMermaid ( props . PrimitiveCode ) )
renderFlowchart ( props . PrimitiveCode )
} , 300 ) // 300ms debounce
return ( ) = > {
if ( renderTimeoutRef . current )
clearTimeout ( renderTimeoutRef . current )
}
} , [ props . PrimitiveCode , renderFlowchart , isInitialized , cacheKey , isCodeComplete ] )
} , [ props . PrimitiveCode , look, currentTheme , isInitialized , configureMermaid , renderFlowchart ] )
// Cleanup on unmount
useEffect ( ( ) = > {
@ -471,14 +441,22 @@ const Flowchart = React.forwardRef((props: {
containerRef . current . innerHTML = ''
if ( renderTimeoutRef . current )
clearTimeout ( renderTimeoutRef . current )
if ( codeCompletionCheckRef . current )
clearTimeout ( codeCompletionCheckRef . current )
}
} , [ ] )
const handlePreviewClick = async ( ) = > {
if ( svgString ) {
const base64 = await svgToBase64 ( svgString )
setImagePreviewUrl ( base64 )
}
}
const toggleTheme = ( ) = > {
setCurrentTheme ( prevTheme = > prevTheme === 'light' ? Theme.dark : Theme.light )
const newTheme = currentTheme === 'light' ? 'dark' : 'light'
// Ensure a full, clean re-render cycle, consistent with global theme change.
diagramCache . clear ( )
setSvgString ( null )
setCurrentTheme ( newTheme )
}
// Style classes for theme-dependent elements
@ -527,14 +505,26 @@ const Flowchart = React.forwardRef((props: {
< div
key = 'classic'
className = { getLookButtonClass ( 'classic' ) }
onClick = { ( ) = > setLook ( 'classic' ) }
onClick = { ( ) = > {
if ( look !== 'classic' ) {
diagramCache . clear ( )
setSvgString ( null )
setLook ( 'classic' )
}
} }
>
< div className = "msh-segmented-item-label" > { t ( 'app.mermaid.classic' ) } < / div >
< / div >
< div
key = 'handDrawn'
className = { getLookButtonClass ( 'handDrawn' ) }
onClick = { ( ) = > setLook ( 'handDrawn' ) }
onClick = { ( ) = > {
if ( look !== 'handDrawn' ) {
diagramCache . clear ( )
setSvgString ( null )
setLook ( 'handDrawn' )
}
} }
>
< div className = "msh-segmented-item-label" > { t ( 'app.mermaid.handDrawn' ) } < / div >
< / div >
@ -544,7 +534,7 @@ const Flowchart = React.forwardRef((props: {
< div ref = { containerRef } style = { { position : 'absolute' , visibility : 'hidden' , height : 0 , overflow : 'hidden' } } / >
{ isLoading && ! svg Code && (
{ isLoading && ! svg String && (
< div className = 'px-[26px] py-4' >
< LoadingAnim type = 'text' / >
{ ! isCodeComplete && (
@ -555,8 +545,8 @@ const Flowchart = React.forwardRef((props: {
< / div >
) }
{ svg Code && (
< div className = { themeClasses . mermaidDiv } style = { { objectFit : 'cover' } } onClick = { ( ) = > setImagePreviewUrl ( svgCode ) } >
{ svg String && (
< div className = { themeClasses . mermaidDiv } style = { { objectFit : 'cover' } } onClick = { handlePreviewClick } >
< div className = "absolute bottom-2 left-2 z-[100]" >
< button
onClick = { ( e ) = > {
@ -571,11 +561,9 @@ const Flowchart = React.forwardRef((props: {
< / button >
< / div >
< img
src = { svgCode }
alt = "mermaid_chart"
< div
style = { { maxWidth : '100%' } }
onError= { ( ) = > { setErrMsg ( 'Chart rendering failed, please refresh and retry' ) } }
dangerouslySetInnerHTML = { { __html : svgString } }
/ >
< / div >
) }