Просмотр исходного кода

Handle Mermaid validation errors (#3112)

* Handle Mermaid validation errors

* PR feedback

* More PR feedback
Matt Rubens 8 месяцев назад
Родитель
Сommit
da9b857376

+ 92 - 10
webview-ui/src/components/common/MermaidBlock.tsx

@@ -3,6 +3,9 @@ import mermaid from "mermaid"
 import { useDebounceEffect } from "@src/utils/useDebounceEffect"
 import { useDebounceEffect } from "@src/utils/useDebounceEffect"
 import styled from "styled-components"
 import styled from "styled-components"
 import { vscode } from "@src/utils/vscode"
 import { vscode } from "@src/utils/vscode"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useCopyToClipboard } from "@src/utils/clipboard"
+import CodeBlock from "./CodeBlock"
 
 
 const MERMAID_THEME = {
 const MERMAID_THEME = {
 	background: "#1e1e1e", // VS Code dark theme background
 	background: "#1e1e1e", // VS Code dark theme background
@@ -81,10 +84,15 @@ interface MermaidBlockProps {
 export default function MermaidBlock({ code }: MermaidBlockProps) {
 export default function MermaidBlock({ code }: MermaidBlockProps) {
 	const containerRef = useRef<HTMLDivElement>(null)
 	const containerRef = useRef<HTMLDivElement>(null)
 	const [isLoading, setIsLoading] = useState(false)
 	const [isLoading, setIsLoading] = useState(false)
+	const [error, setError] = useState<string | null>(null)
+	const [isErrorExpanded, setIsErrorExpanded] = useState(false)
+	const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
+	const { t } = useAppTranslation()
 
 
 	// 1) Whenever `code` changes, mark that we need to re-render a new chart
 	// 1) Whenever `code` changes, mark that we need to re-render a new chart
 	useEffect(() => {
 	useEffect(() => {
 		setIsLoading(true)
 		setIsLoading(true)
+		setError(null)
 	}, [code])
 	}, [code])
 
 
 	// 2) Debounce the actual parse/render
 	// 2) Debounce the actual parse/render
@@ -93,12 +101,10 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
 			if (containerRef.current) {
 			if (containerRef.current) {
 				containerRef.current.innerHTML = ""
 				containerRef.current.innerHTML = ""
 			}
 			}
+
 			mermaid
 			mermaid
-				.parse(code, { suppressErrors: true })
-				.then((isValid) => {
-					if (!isValid) {
-						throw new Error("Invalid or incomplete Mermaid code")
-					}
+				.parse(code)
+				.then(() => {
 					const id = `mermaid-${Math.random().toString(36).substring(2)}`
 					const id = `mermaid-${Math.random().toString(36).substring(2)}`
 					return mermaid.render(id, code)
 					return mermaid.render(id, code)
 				})
 				})
@@ -109,7 +115,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
 				})
 				})
 				.catch((err) => {
 				.catch((err) => {
 					console.warn("Mermaid parse/render failed:", err)
 					console.warn("Mermaid parse/render failed:", err)
-					containerRef.current!.innerHTML = code.replace(/</g, "&lt;").replace(/>/g, "&gt;")
+					setError(err.message || "Failed to render Mermaid diagram")
 				})
 				})
 				.finally(() => {
 				.finally(() => {
 					setIsLoading(false)
 					setIsLoading(false)
@@ -139,12 +145,71 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
 		}
 		}
 	}
 	}
 
 
+	// Copy functionality handled directly through the copyWithFeedback utility
+
 	return (
 	return (
 		<MermaidBlockContainer>
 		<MermaidBlockContainer>
-			{isLoading && <LoadingMessage>Generating mermaid diagram...</LoadingMessage>}
-
-			{/* The container for the final <svg> or raw code. */}
-			<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
+			{isLoading && <LoadingMessage>{t("common:mermaid.loading")}</LoadingMessage>}
+
+			{error ? (
+				<div style={{ marginTop: "0px", overflow: "hidden", marginBottom: "8px" }}>
+					<div
+						style={{
+							borderBottom: isErrorExpanded ? "1px solid var(--vscode-editorGroup-border)" : "none",
+							fontWeight: "normal",
+							fontSize: "var(--vscode-font-size)",
+							color: "var(--vscode-editor-foreground)",
+							display: "flex",
+							alignItems: "center",
+							justifyContent: "space-between",
+							cursor: "pointer",
+						}}
+						onClick={() => setIsErrorExpanded(!isErrorExpanded)}>
+						<div
+							style={{
+								display: "flex",
+								alignItems: "center",
+								gap: "10px",
+								flexGrow: 1,
+							}}>
+							<span
+								className="codicon codicon-warning"
+								style={{
+									color: "var(--vscode-editorWarning-foreground)",
+									opacity: 0.8,
+									fontSize: 16,
+									marginBottom: "-1.5px",
+								}}></span>
+							<span style={{ fontWeight: "bold" }}>{t("common:mermaid.render_error")}</span>
+						</div>
+						<div style={{ display: "flex", alignItems: "center" }}>
+							<CopyButton
+								onClick={(e) => {
+									e.stopPropagation()
+									copyWithFeedback(code, e)
+								}}>
+								<span className={`codicon codicon-${showCopyFeedback ? "check" : "copy"}`}></span>
+							</CopyButton>
+							<span className={`codicon codicon-chevron-${isErrorExpanded ? "up" : "down"}`}></span>
+						</div>
+					</div>
+					{isErrorExpanded && (
+						<div
+							style={{
+								padding: "8px",
+								backgroundColor: "var(--vscode-editor-background)",
+								borderTop: "none",
+							}}>
+							<div style={{ marginBottom: "8px", color: "var(--vscode-descriptionForeground)" }}>
+								{error}
+							</div>
+							<CodeBlock language="mermaid" source={code} />
+						</div>
+					)}
+				</div>
+			) : (
+				<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
+			)}
 		</MermaidBlockContainer>
 		</MermaidBlockContainer>
 	)
 	)
 }
 }
@@ -212,6 +277,23 @@ const LoadingMessage = styled.div`
 	font-size: 0.9em;
 	font-size: 0.9em;
 `
 `
 
 
+const CopyButton = styled.button`
+	padding: 3px;
+	height: 24px;
+	margin-right: 4px;
+	color: var(--vscode-editor-foreground);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	background: transparent;
+	border: none;
+	cursor: pointer;
+
+	&:hover {
+		opacity: 0.8;
+	}
+`
+
 interface SvgContainerProps {
 interface SvgContainerProps {
 	$isLoading: boolean
 	$isLoading: boolean
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/ca/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Cerca..."
 		"search_placeholder": "Cerca..."
+	},
+	"mermaid": {
+		"loading": "Generant diagrama mermaid...",
+		"render_error": "No es pot renderitzar el diagrama"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/de/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Suchen..."
 		"search_placeholder": "Suchen..."
+	},
+	"mermaid": {
+		"loading": "Mermaid-Diagramm wird generiert...",
+		"render_error": "Diagramm kann nicht gerendert werden"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/en/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Search..."
 		"search_placeholder": "Search..."
+	},
+	"mermaid": {
+		"loading": "Generating mermaid diagram...",
+		"render_error": "Unable to Render Diagram"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/es/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Buscar..."
 		"search_placeholder": "Buscar..."
+	},
+	"mermaid": {
+		"loading": "Generando diagrama mermaid...",
+		"render_error": "No se puede renderizar el diagrama"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/fr/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Rechercher..."
 		"search_placeholder": "Rechercher..."
+	},
+	"mermaid": {
+		"loading": "Génération du diagramme mermaid...",
+		"render_error": "Impossible de rendre le diagramme"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/hi/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "खोजें..."
 		"search_placeholder": "खोजें..."
+	},
+	"mermaid": {
+		"loading": "मरमेड डायग्राम जनरेट हो रहा है...",
+		"render_error": "डायग्राम रेंडर नहीं किया जा सकता"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/it/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Cerca..."
 		"search_placeholder": "Cerca..."
+	},
+	"mermaid": {
+		"loading": "Generazione del diagramma mermaid...",
+		"render_error": "Impossibile renderizzare il diagramma"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/ja/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "検索..."
 		"search_placeholder": "検索..."
+	},
+	"mermaid": {
+		"loading": "Mermaidダイアグラムを生成中...",
+		"render_error": "ダイアグラムをレンダリングできません"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/ko/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "검색..."
 		"search_placeholder": "검색..."
+	},
+	"mermaid": {
+		"loading": "머메이드 다이어그램 생성 중...",
+		"render_error": "다이어그램을 렌더링할 수 없음"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/pl/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Szukaj..."
 		"search_placeholder": "Szukaj..."
+	},
+	"mermaid": {
+		"loading": "Generowanie diagramu mermaid...",
+		"render_error": "Nie można renderować diagramu"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/pt-BR/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Pesquisar..."
 		"search_placeholder": "Pesquisar..."
+	},
+	"mermaid": {
+		"loading": "Gerando diagrama mermaid...",
+		"render_error": "Não foi possível renderizar o diagrama"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/ru/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Поиск..."
 		"search_placeholder": "Поиск..."
+	},
+	"mermaid": {
+		"loading": "Создание диаграммы mermaid...",
+		"render_error": "Не удалось отобразить диаграмму"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/tr/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Ara..."
 		"search_placeholder": "Ara..."
+	},
+	"mermaid": {
+		"loading": "Mermaid diyagramı oluşturuluyor...",
+		"render_error": "Diyagram render edilemiyor"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/vi/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "Tìm kiếm..."
 		"search_placeholder": "Tìm kiếm..."
+	},
+	"mermaid": {
+		"loading": "Đang tạo biểu đồ mermaid...",
+		"render_error": "Không thể hiển thị biểu đồ"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/zh-CN/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "搜索..."
 		"search_placeholder": "搜索..."
+	},
+	"mermaid": {
+		"loading": "生成 Mermaid 图表中...",
+		"render_error": "无法渲染图表"
 	}
 	}
 }
 }

+ 4 - 0
webview-ui/src/i18n/locales/zh-TW/common.json

@@ -6,5 +6,9 @@
 	},
 	},
 	"ui": {
 	"ui": {
 		"search_placeholder": "搜尋..."
 		"search_placeholder": "搜尋..."
+	},
+	"mermaid": {
+		"loading": "產生 Mermaid 圖表中...",
+		"render_error": "無法渲染圖表"
 	}
 	}
 }
 }