@@ -3,6 +3,7 @@ import mermaid from "mermaid"
33import { useDebounceEffect } from "@src/utils/useDebounceEffect"
44import styled from "styled-components"
55import { vscode } from "@src/utils/vscode"
6+ import { useAppTranslation } from "@src/i18n/TranslationContext"
67
78const MERMAID_THEME = {
89 background : "#1e1e1e" , // VS Code dark theme background
@@ -81,10 +82,15 @@ interface MermaidBlockProps {
8182export default function MermaidBlock ( { code } : MermaidBlockProps ) {
8283 const containerRef = useRef < HTMLDivElement > ( null )
8384 const [ isLoading , setIsLoading ] = useState ( false )
85+ const [ error , setError ] = useState < string | null > ( null )
86+ const [ isErrorExpanded , setIsErrorExpanded ] = useState ( false )
87+ const [ showCopySuccess , setShowCopySuccess ] = useState ( false )
88+ const { t } = useAppTranslation ( )
8489
8590 // 1) Whenever `code` changes, mark that we need to re-render a new chart
8691 useEffect ( ( ) => {
8792 setIsLoading ( true )
93+ setError ( null )
8894 } , [ code ] )
8995
9096 // 2) Debounce the actual parse/render
@@ -93,12 +99,10 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
9399 if ( containerRef . current ) {
94100 containerRef . current . innerHTML = ""
95101 }
102+
96103 mermaid
97- . parse ( code , { suppressErrors : true } )
98- . then ( ( isValid ) => {
99- if ( ! isValid ) {
100- throw new Error ( "Invalid or incomplete Mermaid code" )
101- }
104+ . parse ( code )
105+ . then ( ( ) => {
102106 const id = `mermaid-${ Math . random ( ) . toString ( 36 ) . substring ( 2 ) } `
103107 return mermaid . render ( id , code )
104108 } )
@@ -109,7 +113,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
109113 } )
110114 . catch ( ( err ) => {
111115 console . warn ( "Mermaid parse/render failed:" , err )
112- containerRef . current ! . innerHTML = code . replace ( / < / g , "<" ) . replace ( / > / g , "> ")
116+ setError ( err . message || "Failed to render Mermaid diagram ")
113117 } )
114118 . finally ( ( ) => {
115119 setIsLoading ( false )
@@ -139,12 +143,90 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
139143 }
140144 }
141145
146+ /**
147+ * Copy the mermaid code to clipboard for easier fixing
148+ */
149+ const handleCopyCode = ( ) => {
150+ navigator . clipboard
151+ . writeText ( code )
152+ . then ( ( ) => {
153+ setShowCopySuccess ( true )
154+ setTimeout ( ( ) => {
155+ setShowCopySuccess ( false )
156+ } , 1000 )
157+ } )
158+ . catch ( ( err ) => {
159+ console . error ( "Failed to copy code:" , err )
160+ } )
161+ }
162+
142163 return (
143164 < MermaidBlockContainer >
144- { isLoading && < LoadingMessage > Generating mermaid diagram...</ LoadingMessage > }
145-
146- { /* The container for the final <svg> or raw code. */ }
147- < SvgContainer onClick = { handleClick } ref = { containerRef } $isLoading = { isLoading } />
165+ { isLoading && < LoadingMessage > { t ( "common:mermaid.loading" ) } </ LoadingMessage > }
166+
167+ { error ? (
168+ < div style = { { marginTop : "0px" , overflow : "hidden" , marginBottom : "8px" } } >
169+ < div
170+ style = { {
171+ borderBottom : isErrorExpanded ? "1px solid var(--vscode-editorGroup-border)" : "none" ,
172+ fontWeight : "normal" ,
173+ fontSize : "var(--vscode-font-size)" ,
174+ color : "var(--vscode-editor-foreground)" ,
175+ display : "flex" ,
176+ alignItems : "center" ,
177+ justifyContent : "space-between" ,
178+ cursor : "pointer" ,
179+ } }
180+ onClick = { ( ) => setIsErrorExpanded ( ! isErrorExpanded ) } >
181+ < div
182+ style = { {
183+ display : "flex" ,
184+ alignItems : "center" ,
185+ gap : "10px" ,
186+ flexGrow : 1 ,
187+ } } >
188+ < span
189+ className = "codicon codicon-warning"
190+ style = { {
191+ color : "var(--vscode-editorWarning-foreground)" ,
192+ opacity : 0.8 ,
193+ fontSize : 16 ,
194+ marginBottom : "-1.5px" ,
195+ } } > </ span >
196+ < span style = { { fontWeight : "bold" } } > { t ( "common:mermaid.render_error" ) } </ span >
197+ </ div >
198+ < div style = { { display : "flex" , alignItems : "center" } } >
199+ < CopyButton
200+ onClick = { ( e ) => {
201+ e . stopPropagation ( )
202+ handleCopyCode ( )
203+ } } >
204+ < span
205+ className = { `codicon codicon-${ showCopySuccess ? "check" : "copy" } ` }
206+ title = { showCopySuccess ? t ( "common:mermaid.copy_success" ) : undefined } > </ span >
207+ </ CopyButton >
208+ < span className = { `codicon codicon-chevron-${ isErrorExpanded ? "up" : "down" } ` } > </ span >
209+ </ div >
210+ </ div >
211+ { isErrorExpanded && (
212+ < div
213+ style = { {
214+ padding : "8px" ,
215+ backgroundColor : "var(--vscode-editor-background)" ,
216+ borderTop : "none" ,
217+ } } >
218+ < div style = { { marginBottom : "8px" , color : "var(--vscode-descriptionForeground)" } } >
219+ { error }
220+ </ div >
221+ < CodeBlock >
222+ < code > { code } </ code >
223+ </ CodeBlock >
224+ </ div >
225+ ) }
226+ </ div >
227+ ) : (
228+ < SvgContainer onClick = { handleClick } ref = { containerRef } $isLoading = { isLoading } />
229+ ) }
148230 </ MermaidBlockContainer >
149231 )
150232}
@@ -212,6 +294,39 @@ const LoadingMessage = styled.div`
212294 font-size: 0.9em;
213295`
214296
297+ const CodeBlock = styled . pre `
298+ background-color: var(--vscode-editor-background);
299+ border: 1px solid var(--vscode-editor-lineHighlightBorder);
300+ border-radius: 3px;
301+ padding: 8px;
302+ overflow: auto;
303+ max-height: 200px;
304+
305+ code {
306+ font-family: var(--vscode-editor-font-family);
307+ font-size: var(--vscode-editor-font-size);
308+ white-space: pre-wrap;
309+ word-break: break-all;
310+ }
311+ `
312+
313+ const CopyButton = styled . button `
314+ padding: 3px;
315+ height: 24px;
316+ margin-right: 4px;
317+ color: var(--vscode-editor-foreground);
318+ display: flex;
319+ align-items: center;
320+ justify-content: center;
321+ background: transparent;
322+ border: none;
323+ cursor: pointer;
324+
325+ &:hover {
326+ opacity: 0.8;
327+ }
328+ `
329+
215330interface SvgContainerProps {
216331 $isLoading : boolean
217332}
0 commit comments