Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"html-react-parser": "^3.0.4",
"lodash": "^4.17.21",
"lowlight": "^3.3.0",
"mermaid": "^10.6.1",
"moment": "^2.29.3",
"notistack": "^2.0.4",
"prop-types": "^15.7.2",
Expand Down
60 changes: 55 additions & 5 deletions packages/ui/src/ui-component/markdown/CodeBlock.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { IconClipboard, IconDownload } from '@tabler/icons-react'
import { memo, useState } from 'react'
import { memo, useState, useEffect, useRef } from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import PropTypes from 'prop-types'
import { Box, IconButton, Popover, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import mermaid from 'mermaid'

const programmingLanguages = {
javascript: '.js',
Expand All @@ -29,13 +30,41 @@ const programmingLanguages = {
shell: '.sh',
sql: '.sql',
html: '.html',
css: '.css'
css: '.css',
mermaid: '.mmd'
}

export const CodeBlock = memo(({ language, chatflowid, isFullWidth, value }) => {
const theme = useTheme()
const [anchorEl, setAnchorEl] = useState(null)
const [mermaidSvg, setMermaidSvg] = useState(null)
const [mermaidError, setMermaidError] = useState(null)
const diagramIdRef = useRef(`mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`)
const openPopOver = Boolean(anchorEl)
const isMermaid = language?.toLowerCase() === 'mermaid'

useEffect(() => {
if (!isMermaid || !value) return

mermaid.initialize({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be an issue, because when streaming, value changes, this will cause multiple initialization

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped the rendering logic in a setTimeout with a 300ms delay

startOnLoad: false,
theme: theme.palette.mode === 'dark' ? 'dark' : 'default',
securityLevel: 'strict'
})

const renderMermaid = async () => {
try {
setMermaidError(null)
const { svg } = await mermaid.render(diagramIdRef.current, value)
setMermaidSvg(svg)
} catch (error) {
setMermaidError(error.message)
setMermaidSvg(null)
}
}

renderMermaid()
}, [isMermaid, value, theme.palette.mode])
Comment on lines 46 to 71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of the useEffect hook for rendering Mermaid diagrams will execute on every change to the value prop. In a chat application with streaming responses, this can lead to excessive re-renders and performance issues, as mermaid.initialize and mermaid.render would be called for each new token. This can also result in a poor user experience with flickering error messages for incomplete diagrams.

To optimize this, I recommend debouncing the rendering logic. This will ensure that the rendering only happens after the code has stopped changing for a brief period.

    useEffect(() => {
        if (!isMermaid || !value) return

        const timer = setTimeout(() => {
            mermaid.initialize({
                startOnLoad: false,
                theme: theme.palette.mode === 'dark' ? 'dark' : 'default',
                securityLevel: 'strict'
            })

            const renderMermaid = async () => {
                try {
                    setMermaidError(null)
                    const { svg } = await mermaid.render(diagramIdRef.current, value)
                    setMermaidSvg(svg)
                } catch (error) {
                    setMermaidError(error.message)
                    setMermaidSvg(null)
                }
            }

            renderMermaid()
        }, 300)

        return () => clearTimeout(timer)
    }, [isMermaid, value, theme.palette.mode])


const handleClosePopOver = () => {
setAnchorEl(null)
Expand Down Expand Up @@ -107,9 +136,30 @@ export const CodeBlock = memo(({ language, chatflowid, isFullWidth, value }) =>
</div>
</Box>

<SyntaxHighlighter language={language} style={oneDark} customStyle={{ margin: 0 }}>
{value}
</SyntaxHighlighter>
{isMermaid ? (
<Box sx={{ background: theme.palette?.common.dark, p: 2, borderBottomLeftRadius: 10, borderBottomRightRadius: 10 }}>
{mermaidError ? (
<Box>
<Typography variant='body2' color='error' sx={{ mb: 1 }}>
Mermaid rendering error: {mermaidError}
</Typography>
<SyntaxHighlighter language='text' style={oneDark} customStyle={{ margin: 0 }}>
{value}
</SyntaxHighlighter>
</Box>
) : mermaidSvg ? (
<div dangerouslySetInnerHTML={{ __html: mermaidSvg }} style={{ display: 'flex', justifyContent: 'center' }} />
) : (
<Typography variant='body2' color='text.secondary'>
Rendering diagram...
</Typography>
)}
</Box>
) : (
<SyntaxHighlighter language={language} style={oneDark} customStyle={{ margin: 0 }}>
{value}
</SyntaxHighlighter>
)}
</div>
)
})
Expand Down
Loading