@@ -3,6 +3,9 @@ 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"
7+ import { useCopyToClipboard } from "@src/utils/clipboard"
8+ import CodeBlock from "./CodeBlock"
69
710const MERMAID_THEME = {
811 background : "#1e1e1e" , // VS Code dark theme background
@@ -81,10 +84,15 @@ interface MermaidBlockProps {
8184export default function MermaidBlock ( { code } : MermaidBlockProps ) {
8285 const containerRef = useRef < HTMLDivElement > ( null )
8386 const [ isLoading , setIsLoading ] = useState ( false )
87+ const [ error , setError ] = useState < string | null > ( null )
88+ const [ isErrorExpanded , setIsErrorExpanded ] = useState ( false )
89+ const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard ( )
90+ const { t } = useAppTranslation ( )
8491
8592 // 1) Whenever `code` changes, mark that we need to re-render a new chart
8693 useEffect ( ( ) => {
8794 setIsLoading ( true )
95+ setError ( null )
8896 } , [ code ] )
8997
9098 // 2) Debounce the actual parse/render
@@ -93,12 +101,10 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
93101 if ( containerRef . current ) {
94102 containerRef . current . innerHTML = ""
95103 }
104+
96105 mermaid
97- . parse ( code , { suppressErrors : true } )
98- . then ( ( isValid ) => {
99- if ( ! isValid ) {
100- throw new Error ( "Invalid or incomplete Mermaid code" )
101- }
106+ . parse ( code )
107+ . then ( ( ) => {
102108 const id = `mermaid-${ Math . random ( ) . toString ( 36 ) . substring ( 2 ) } `
103109 return mermaid . render ( id , code )
104110 } )
@@ -109,7 +115,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
109115 } )
110116 . catch ( ( err ) => {
111117 console . warn ( "Mermaid parse/render failed:" , err )
112- containerRef . current ! . innerHTML = code . replace ( / < / g , "<" ) . replace ( / > / g , "> ")
118+ setError ( err . message || "Failed to render Mermaid diagram ")
113119 } )
114120 . finally ( ( ) => {
115121 setIsLoading ( false )
@@ -139,12 +145,71 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
139145 }
140146 }
141147
148+ // Copy functionality handled directly through the copyWithFeedback utility
149+
142150 return (
143151 < 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 } />
152+ { isLoading && < LoadingMessage > { t ( "common:mermaid.loading" ) } </ LoadingMessage > }
153+
154+ { error ? (
155+ < div style = { { marginTop : "0px" , overflow : "hidden" , marginBottom : "8px" } } >
156+ < div
157+ style = { {
158+ borderBottom : isErrorExpanded ? "1px solid var(--vscode-editorGroup-border)" : "none" ,
159+ fontWeight : "normal" ,
160+ fontSize : "var(--vscode-font-size)" ,
161+ color : "var(--vscode-editor-foreground)" ,
162+ display : "flex" ,
163+ alignItems : "center" ,
164+ justifyContent : "space-between" ,
165+ cursor : "pointer" ,
166+ } }
167+ onClick = { ( ) => setIsErrorExpanded ( ! isErrorExpanded ) } >
168+ < div
169+ style = { {
170+ display : "flex" ,
171+ alignItems : "center" ,
172+ gap : "10px" ,
173+ flexGrow : 1 ,
174+ } } >
175+ < span
176+ className = "codicon codicon-warning"
177+ style = { {
178+ color : "var(--vscode-editorWarning-foreground)" ,
179+ opacity : 0.8 ,
180+ fontSize : 16 ,
181+ marginBottom : "-1.5px" ,
182+ } } > </ span >
183+ < span style = { { fontWeight : "bold" } } > { t ( "common:mermaid.render_error" ) } </ span >
184+ </ div >
185+ < div style = { { display : "flex" , alignItems : "center" } } >
186+ < CopyButton
187+ onClick = { ( e ) => {
188+ e . stopPropagation ( )
189+ copyWithFeedback ( code , e )
190+ } } >
191+ < span className = { `codicon codicon-${ showCopyFeedback ? "check" : "copy" } ` } > </ span >
192+ </ CopyButton >
193+ < span className = { `codicon codicon-chevron-${ isErrorExpanded ? "up" : "down" } ` } > </ span >
194+ </ div >
195+ </ div >
196+ { isErrorExpanded && (
197+ < div
198+ style = { {
199+ padding : "8px" ,
200+ backgroundColor : "var(--vscode-editor-background)" ,
201+ borderTop : "none" ,
202+ } } >
203+ < div style = { { marginBottom : "8px" , color : "var(--vscode-descriptionForeground)" } } >
204+ { error }
205+ </ div >
206+ < CodeBlock language = "mermaid" source = { code } />
207+ </ div >
208+ ) }
209+ </ div >
210+ ) : (
211+ < SvgContainer onClick = { handleClick } ref = { containerRef } $isLoading = { isLoading } />
212+ ) }
148213 </ MermaidBlockContainer >
149214 )
150215}
@@ -212,6 +277,23 @@ const LoadingMessage = styled.div`
212277 font-size: 0.9em;
213278`
214279
280+ const CopyButton = styled . button `
281+ padding: 3px;
282+ height: 24px;
283+ margin-right: 4px;
284+ color: var(--vscode-editor-foreground);
285+ display: flex;
286+ align-items: center;
287+ justify-content: center;
288+ background: transparent;
289+ border: none;
290+ cursor: pointer;
291+
292+ &:hover {
293+ opacity: 0.8;
294+ }
295+ `
296+
215297interface SvgContainerProps {
216298 $isLoading : boolean
217299}
0 commit comments