diff --git a/web/app/components/base/markdown-blocks/code-block.tsx b/web/app/components/base/markdown-blocks/code-block.tsx index 9f8a6a87b..7b91cd004 100644 --- a/web/app/components/base/markdown-blocks/code-block.tsx +++ b/web/app/components/base/markdown-blocks/code-block.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import ReactEcharts from 'echarts-for-react' import SyntaxHighlighter from 'react-syntax-highlighter' import { @@ -62,6 +62,17 @@ const getCorrectCapitalizationLanguageName = (language: string) => { // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message // or use the non-minified dev environment for full errors and additional helpful warnings. +// Define ECharts event parameter types +interface EChartsEventParams { + type: string; + seriesIndex?: number; + dataIndex?: number; + name?: string; + value?: any; + currentIndex?: number; // Added for timeline events + [key: string]: any; +} + const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any) => { const { theme } = useTheme() const [isSVG, setIsSVG] = useState(true) @@ -70,6 +81,11 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any const echartsRef = useRef(null) const contentRef = useRef('') const processedRef = useRef(false) // Track if content was successfully processed + const instanceIdRef = useRef(`chart-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`) // Unique ID for logging + const isInitialRenderRef = useRef(true) // Track if this is initial render + const chartInstanceRef = useRef(null) // Direct reference to ECharts instance + const resizeTimerRef = useRef(null) // For debounce handling + const finishedEventCountRef = useRef(0) // Track finished event trigger count const match = /language-(\w+)/.exec(className || '') const language = match?.[1] const languageShowName = getCorrectCapitalizationLanguageName(language || '') @@ -85,36 +101,64 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any width: 'auto', }) as any, []) - const echartsOnEvents = useMemo(() => ({ - finished: () => { - const instance = echartsRef.current?.getEchartsInstance?.() - if (instance) - instance.resize() + // Debounce resize operations + const debouncedResize = useCallback(() => { + if (resizeTimerRef.current) + clearTimeout(resizeTimerRef.current) + + resizeTimerRef.current = setTimeout(() => { + if (chartInstanceRef.current) + chartInstanceRef.current.resize() + resizeTimerRef.current = null + }, 200) + }, []) + + // Handle ECharts instance initialization + const handleChartReady = useCallback((instance: any) => { + chartInstanceRef.current = instance + + // Force resize to ensure timeline displays correctly + setTimeout(() => { + if (chartInstanceRef.current) + chartInstanceRef.current.resize() + }, 200) + }, []) + + // Store event handlers in useMemo to avoid recreating them + const echartsEvents = useMemo(() => ({ + finished: (params: EChartsEventParams) => { + // Limit finished event frequency to avoid infinite loops + finishedEventCountRef.current++ + if (finishedEventCountRef.current > 3) { + // Stop processing after 3 times to avoid infinite loops + return + } + + if (chartInstanceRef.current) { + // Use debounced resize + debouncedResize() + } }, - }), [echartsRef]) // echartsRef is stable, so this effectively runs once. + }), [debouncedResize]) // Handle container resize for echarts useEffect(() => { - if (language !== 'echarts' || !echartsRef.current) return + if (language !== 'echarts' || !chartInstanceRef.current) return const handleResize = () => { - // This gets the echarts instance from the component - const instance = echartsRef.current?.getEchartsInstance?.() - if (instance) - instance.resize() + if (chartInstanceRef.current) + // Use debounced resize + debouncedResize() } window.addEventListener('resize', handleResize) - // Also manually trigger resize after a short delay to ensure proper sizing - const resizeTimer = setTimeout(handleResize, 200) - return () => { window.removeEventListener('resize', handleResize) - clearTimeout(resizeTimer) + if (resizeTimerRef.current) + clearTimeout(resizeTimerRef.current) } - }, [language, echartsRef.current]) - + }, [language, debouncedResize]) // Process chart data when content changes useEffect(() => { // Only process echarts content @@ -222,6 +266,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any } }, [language, children]) + // Cache rendered content to avoid unnecessary re-renders const renderCodeContent = useMemo(() => { const content = String(children).replace(/\n$/, '') switch (language) { @@ -274,6 +319,9 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any // Success state: show the chart if (chartState === 'success' && finalChartOption) { + // Reset finished event counter + finishedEventCountRef.current = 0 + return (
{ + if (e && isInitialRenderRef.current) { + echartsRef.current = e + isInitialRenderRef.current = false + } + }} option={finalChartOption} style={echartsStyle} theme={isDarkMode ? 'dark' : undefined} opts={echartsOpts} - notMerge={true} - onEvents={echartsOnEvents} + notMerge={false} + lazyUpdate={false} + onEvents={echartsEvents} + onChartReady={handleChartReady} />
@@ -363,7 +418,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any ) } - }, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, echartsOnEvents]) + }, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, handleChartReady, echartsEvents]) if (inline || !match) return {children}