Skip to content

Commit c0dd68e

Browse files
committed
add a button to fix mermaid syntax errors by calling the LLM
1 parent 08a0c89 commit c0dd68e

File tree

26 files changed

+747
-12
lines changed

26 files changed

+747
-12
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Prompts for Mermaid diagram-related tasks
3+
*/
4+
5+
/**
6+
* Generate a prompt for fixing invalid Mermaid diagram syntax
7+
* @param error - The error message from Mermaid parser
8+
* @param invalidCode - The invalid Mermaid code that needs fixing
9+
* @returns The formatted prompt for the AI to fix the Mermaid syntax
10+
*/
11+
export const mermaidFixPrompt = (error: string, invalidCode: string): string => {
12+
return `You are a Mermaid diagram syntax expert. Fix the following invalid Mermaid diagram syntax and return ONLY the corrected Mermaid code without any explanations or markdown formatting.
13+
14+
Error: ${error}
15+
16+
Invalid Mermaid code:
17+
\`\`\`
18+
${invalidCode}
19+
\`\`\`
20+
21+
Requirements:
22+
1. Return ONLY the corrected Mermaid syntax
23+
2. Do not include markdown code blocks or explanations
24+
3. Ensure the syntax is valid according to Mermaid specifications
25+
4. Enclose labels and edge label within double quotes even when you do not think it necessary to ensure the syntax is robust
26+
5. Do not point to multiple nodes with one edge, use multiple edges instead
27+
6. Preserve the original intent and structure as much as possible
28+
7. If the diagram type is unclear, default to a flowchart
29+
30+
Corrected Mermaid code:`
31+
}

src/core/webview/webviewMessageHandler.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
4242
import { GetModelsOptions } from "../../shared/api"
4343
import { generateSystemPrompt } from "./generateSystemPrompt"
4444
import { getCommand } from "../../utils/commands"
45+
import { mermaidFixPrompt } from "../prompts/utilities/mermaid"
4546

4647
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
4748

@@ -2019,6 +2020,34 @@ export const webviewMessageHandler = async (
20192020
}
20202021
break
20212022
}
2023+
case "fixMermaidSyntax":
2024+
if (message.text && message.requestId) {
2025+
try {
2026+
const { apiConfiguration } = await provider.getState()
2027+
2028+
const prompt = mermaidFixPrompt(message.values?.error || "Unknown syntax error", message.text)
2029+
2030+
const fixedCode = await singleCompletionHandler(apiConfiguration, prompt)
2031+
2032+
provider.postMessageToWebview({
2033+
type: "mermaidFixResponse",
2034+
requestId: message.requestId,
2035+
success: true,
2036+
fixedCode: fixedCode?.trim() || null,
2037+
})
2038+
} catch (error) {
2039+
const errorMessage = error instanceof Error ? error.message : "Failed to fix Mermaid syntax"
2040+
provider.log(`Error fixing Mermaid syntax: ${errorMessage}`)
2041+
2042+
provider.postMessageToWebview({
2043+
type: "mermaidFixResponse",
2044+
requestId: message.requestId,
2045+
success: false,
2046+
error: errorMessage,
2047+
})
2048+
}
2049+
}
2050+
break
20222051
case "focusPanelRequest": {
20232052
// Execute the focusPanel command to focus the WebView
20242053
await vscode.commands.executeCommand(getCommand("focusPanel"))

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export interface ExtensionMessage {
105105
| "shareTaskSuccess"
106106
| "codeIndexSettingsSaved"
107107
| "codeIndexSecretStatus"
108+
| "mermaidFixResponse"
108109
text?: string
109110
payload?: any // Add a generic payload for now, can refine later
110111
action?:
@@ -157,6 +158,7 @@ export interface ExtensionMessage {
157158
visibility?: ShareVisibility
158159
rulesFolderPath?: string
159160
settings?: any
161+
fixedCode?: string | null // For mermaidFixResponse
160162
}
161163

162164
export type ExtensionState = Pick<

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ export interface WebviewMessage {
172172
| "focusPanelRequest"
173173
| "profileThresholds"
174174
| "setHistoryPreviewCollapsed"
175+
| "fixMermaidSyntax"
176+
| "mermaidFixResponse"
175177
| "openExternal"
176178
| "filterMarketplaceItems"
177179
| "marketplaceButtonClicked"

webview-ui/src/components/common/MermaidBlock.tsx

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { useDebounceEffect } from "@src/utils/useDebounceEffect"
55
import { vscode } from "@src/utils/vscode"
66
import { useAppTranslation } from "@src/i18n/TranslationContext"
77
import { useCopyToClipboard } from "@src/utils/clipboard"
8+
import { MermaidSyntaxFixer } from "@src/services/mermaidSyntaxFixer"
89
import CodeBlock from "./CodeBlock"
910
import { MermaidButton } from "@/components/common/MermaidButton"
11+
import { MermaidFixButton } from "@/components/common/MermaidFixButton"
1012

1113
// Removed previous attempts at static imports for individual diagram types
1214
// as the paths were incorrect for Mermaid v11.4.1 and caused errors.
@@ -87,26 +89,50 @@ interface MermaidBlockProps {
8789
code: string
8890
}
8991

90-
export default function MermaidBlock({ code }: MermaidBlockProps) {
92+
export default function MermaidBlock({ code: originalCode }: MermaidBlockProps) {
9193
const containerRef = useRef<HTMLDivElement>(null)
9294
const [isLoading, setIsLoading] = useState(false)
9395
const [error, setError] = useState<string | null>(null)
9496
const [isErrorExpanded, setIsErrorExpanded] = useState(false)
97+
const [svgContent, setSvgContent] = useState<string>("")
98+
const [isFixing, setIsFixing] = useState(false)
99+
const [code, setCode] = useState("")
95100
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
96101
const { t } = useAppTranslation()
97102

98103
// 1) Whenever `code` changes, mark that we need to re-render a new chart
99104
useEffect(() => {
100105
setIsLoading(true)
101106
setError(null)
102-
}, [code])
107+
setCode(originalCode)
108+
setIsFixing(false)
109+
}, [originalCode])
110+
111+
const handleSyntaxFix = async () => {
112+
if (isFixing) return
113+
114+
setIsLoading(true)
115+
setIsFixing(true)
116+
const result = await MermaidSyntaxFixer.autoFixSyntax(code)
117+
if (result.fixedCode) {
118+
// Use the improved code even if not completely successful
119+
setCode(result.fixedCode)
120+
}
121+
122+
if (!result.success) {
123+
setError(result.error || t("common:mermaid.errors.fix_failed"))
124+
}
125+
126+
setIsFixing(false)
127+
setIsLoading(false)
128+
}
103129

104130
// 2) Debounce the actual parse/render
131+
// the LLM is still 'typing', and we do not want to start rendering and/or autofixing before it is fully done.
105132
useDebounceEffect(
106133
() => {
107-
if (containerRef.current) {
108-
containerRef.current.innerHTML = ""
109-
}
134+
if (isFixing) return
135+
setIsLoading(true)
110136

111137
mermaid
112138
.parse(code)
@@ -115,20 +141,20 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
115141
return mermaid.render(id, code)
116142
})
117143
.then(({ svg }) => {
118-
if (containerRef.current) {
119-
containerRef.current.innerHTML = svg
120-
}
144+
setError(null)
145+
setSvgContent(svg)
121146
})
122147
.catch((err) => {
123148
console.warn("Mermaid parse/render failed:", err)
124-
setError(err.message || "Failed to render Mermaid diagram")
149+
const errorMessage = err instanceof Error ? err.message : t("common:mermaid.render_error")
150+
setError(errorMessage)
125151
})
126152
.finally(() => {
127153
setIsLoading(false)
128154
})
129155
},
130156
500, // Delay 500ms
131-
[code], // Dependencies for scheduling
157+
[code, isFixing, originalCode, t], // Dependencies for scheduling
132158
)
133159

134160
/**
@@ -155,7 +181,11 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
155181

156182
return (
157183
<MermaidBlockContainer>
158-
{isLoading && <LoadingMessage>{t("common:mermaid.loading")}</LoadingMessage>}
184+
{isLoading && (
185+
<LoadingMessage>
186+
{isFixing ? t("common:mermaid.fixing_syntax") : t("common:mermaid.loading")}
187+
</LoadingMessage>
188+
)}
159189

160190
{error ? (
161191
<div style={{ marginTop: "0px", overflow: "hidden", marginBottom: "8px" }}>
@@ -189,6 +219,17 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
189219
<span style={{ fontWeight: "bold" }}>{t("common:mermaid.render_error")}</span>
190220
</div>
191221
<div style={{ display: "flex", alignItems: "center" }}>
222+
{!!error && (
223+
<MermaidFixButton
224+
onClick={(e) => {
225+
e.stopPropagation()
226+
handleSyntaxFix()
227+
}}
228+
disabled={isFixing}
229+
title={t("common:mermaid.fix_syntax_button")}>
230+
<span className={`codicon codicon-${isFixing ? "loading" : "wand"}`}></span>
231+
</MermaidFixButton>
232+
)}
192233
<CopyButton
193234
onClick={(e) => {
194235
e.stopPropagation()
@@ -211,12 +252,24 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
211252
{error}
212253
</div>
213254
<CodeBlock language="mermaid" source={code} />
255+
{code !== originalCode && (
256+
<div style={{ marginTop: "8px" }}>
257+
<div style={{ marginBottom: "4px", fontSize: "0.9em", fontWeight: "bold" }}>
258+
{t("common:mermaid.original_code")}
259+
</div>
260+
<CodeBlock language="mermaid" source={originalCode} />
261+
</div>
262+
)}
214263
</div>
215264
)}
216265
</div>
217266
) : (
218267
<MermaidButton containerRef={containerRef} code={code} isLoading={isLoading} svgToPng={svgToPng}>
219-
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading}></SvgContainer>
268+
<SvgContainer
269+
onClick={handleClick}
270+
$isLoading={isLoading}
271+
dangerouslySetInnerHTML={{ __html: svgContent }}
272+
/>
220273
</MermaidButton>
221274
)}
222275
</MermaidBlockContainer>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import styled from "styled-components"
2+
3+
export const MermaidFixButton = styled.button`
4+
padding: 3px;
5+
height: 24px;
6+
margin-right: 4px;
7+
color: var(--vscode-editor-foreground);
8+
display: flex;
9+
align-items: center;
10+
justify-content: center;
11+
background: transparent;
12+
border: none;
13+
cursor: pointer;
14+
15+
&:hover {
16+
opacity: 0.8;
17+
color: var(--vscode-button-foreground);
18+
background: var(--vscode-button-hoverBackground);
19+
}
20+
21+
&:disabled {
22+
opacity: 0.5;
23+
cursor: not-allowed;
24+
}
25+
26+
.codicon-loading {
27+
animation: spin 1s linear infinite;
28+
}
29+
30+
@keyframes spin {
31+
from {
32+
transform: rotate(0deg);
33+
}
34+
to {
35+
transform: rotate(360deg);
36+
}
37+
}
38+
`

webview-ui/src/i18n/locales/ca/common.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
"mermaid": {
1818
"loading": "Generant diagrama mermaid...",
1919
"render_error": "No es pot renderitzar el diagrama",
20+
"fixing_syntax": "Corregint la sintaxi de Mermaid...",
21+
"fix_syntax_button": "Corregir sintaxi amb IA",
22+
"original_code": "Codi original:",
23+
"errors": {
24+
"unknown_syntax": "Error de sintaxi desconegut",
25+
"fix_timeout": "La sol·licitud de correcció de l'IA ha esgotat el temps",
26+
"fix_failed": "La correcció de l'IA ha fallat",
27+
"fix_attempts": "No s'ha pogut corregir la sintaxi després de {{attempts}} intents. Últim error: {{error}}",
28+
"no_fix_provided": "L'IA no ha pogut proporcionar una correcció",
29+
"fix_request_failed": "La sol·licitud de correcció ha fallat"
30+
},
2031
"buttons": {
2132
"zoom": "Zoom",
2233
"zoomIn": "Ampliar",

webview-ui/src/i18n/locales/de/common.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
"mermaid": {
1818
"loading": "Mermaid-Diagramm wird generiert...",
1919
"render_error": "Diagramm kann nicht gerendert werden",
20+
"fixing_syntax": "Mermaid-Syntax wird korrigiert...",
21+
"fix_syntax_button": "Syntax mit KI korrigieren",
22+
"original_code": "Ursprünglicher Code:",
23+
"errors": {
24+
"unknown_syntax": "Unbekannter Syntaxfehler",
25+
"fix_timeout": "KI-Korrekturanfrage hat das Zeitlimit überschritten",
26+
"fix_failed": "KI-Korrektur fehlgeschlagen",
27+
"fix_attempts": "Korrektur nach {{attempts}} Versuchen fehlgeschlagen. Letzter Fehler: {{error}}",
28+
"no_fix_provided": "KI konnte keine Korrektur bereitstellen",
29+
"fix_request_failed": "Korrekturanfrage fehlgeschlagen"
30+
},
2031
"buttons": {
2132
"zoom": "Zoom",
2233
"zoomIn": "Vergrößern",

webview-ui/src/i18n/locales/en/common.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
"mermaid": {
1818
"loading": "Generating mermaid diagram...",
1919
"render_error": "Unable to Render Diagram",
20+
"fixing_syntax": "Fixing Mermaid syntax...",
21+
"fix_syntax_button": "Fix syntax with AI",
22+
"original_code": "Original code:",
23+
"errors": {
24+
"unknown_syntax": "Unknown syntax error",
25+
"fix_timeout": "LLM fix request timed out",
26+
"fix_failed": "LLM fix failed",
27+
"fix_attempts": "Failed to fix syntax after {{attempts}} attempts. Last error: {{error}}",
28+
"no_fix_provided": "LLM failed to provide a fix",
29+
"fix_request_failed": "Fix request failed"
30+
},
2031
"buttons": {
2132
"zoom": "Zoom",
2233
"zoomIn": "Zoom In",

webview-ui/src/i18n/locales/es/common.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
"mermaid": {
1818
"loading": "Generando diagrama mermaid...",
1919
"render_error": "No se puede renderizar el diagrama",
20+
"fixing_syntax": "Corrigiendo sintaxis de Mermaid...",
21+
"fix_syntax_button": "Corregir sintaxis con IA",
22+
"original_code": "Código original:",
23+
"errors": {
24+
"unknown_syntax": "Error de sintaxis desconocido",
25+
"fix_timeout": "La solicitud de corrección de IA agotó el tiempo de espera",
26+
"fix_failed": "La corrección de IA falló",
27+
"fix_attempts": "No se pudo corregir la sintaxis después de {{attempts}} intentos. Último error: {{error}}",
28+
"no_fix_provided": "La IA no pudo proporcionar una corrección",
29+
"fix_request_failed": "La solicitud de corrección falló"
30+
},
2031
"buttons": {
2132
"zoom": "Zoom",
2233
"zoomIn": "Ampliar",

0 commit comments

Comments
 (0)