mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add AI-powered Mermaid diagram fixer
- Add Fix Diagram button to MermaidBlock component when rendering errors occur - Create MermaidDiagramFixer service using Google Gemini AI - Implement 4-stage fixing approach: JSON generation, validation, Python code generation, and execution - Add message handlers for webview-extension communication - Add comprehensive tests for the new functionality - Update translations for new UI elements Fixes #7513
This commit is contained in:
parent
cd9e92fa9b
commit
ffcb45e559
8 changed files with 678 additions and 5 deletions
|
|
@ -2655,5 +2655,47 @@ export const webviewMessageHandler = async (
|
|||
vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth"))
|
||||
break
|
||||
}
|
||||
case "fixMermaidDiagram": {
|
||||
// Handle Mermaid diagram fixing request
|
||||
const { code, error } = message
|
||||
if (code && error) {
|
||||
try {
|
||||
// Import the MermaidDiagramFixer
|
||||
const { MermaidDiagramFixer } = await import("../../services/mermaid/MermaidDiagramFixer")
|
||||
|
||||
// Get the API configuration
|
||||
const { apiConfiguration } = await provider.getState()
|
||||
|
||||
// Create fixer instance with Gemini API key
|
||||
const fixer = new MermaidDiagramFixer({
|
||||
geminiApiKey: apiConfiguration.geminiApiKey,
|
||||
geminiModel: apiConfiguration.apiModelId,
|
||||
})
|
||||
|
||||
// Fix the diagram
|
||||
const fixedCode = await fixer.fixDiagram(code, error)
|
||||
|
||||
// Send the fixed code back to the webview
|
||||
await provider.postMessageToWebview({
|
||||
type: "mermaidDiagramFixed",
|
||||
originalCode: code,
|
||||
fixedCode: fixedCode,
|
||||
})
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
t("common:info.mermaid_diagram_fixed") || "Mermaid diagram has been fixed!",
|
||||
)
|
||||
} catch (error) {
|
||||
provider.log(
|
||||
`Error fixing Mermaid diagram: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(
|
||||
t("common:errors.mermaid.fix_failed") ||
|
||||
`Failed to fix diagram: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,11 @@
|
|||
"gemini": {
|
||||
"generate_stream": "Gemini generate context stream error: {{error}}",
|
||||
"generate_complete_prompt": "Gemini completion error: {{error}}",
|
||||
"sources": "Sources:"
|
||||
"sources": "Sources:",
|
||||
"api_key_required": "Gemini API key is required for diagram fixing"
|
||||
},
|
||||
"mermaid": {
|
||||
"fix_failed": "Failed to fix Mermaid diagram"
|
||||
},
|
||||
"cerebras": {
|
||||
"authenticationFailed": "Cerebras API authentication failed. Please check your API key is valid and not expired.",
|
||||
|
|
@ -124,7 +128,8 @@
|
|||
"image_copied_to_clipboard": "Image data URI copied to clipboard",
|
||||
"image_saved": "Image saved to {{path}}",
|
||||
"mode_exported": "Mode '{{mode}}' exported successfully",
|
||||
"mode_imported": "Mode imported successfully"
|
||||
"mode_imported": "Mode imported successfully",
|
||||
"mermaid_diagram_fixed": "Mermaid diagram has been fixed successfully!"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Yes",
|
||||
|
|
|
|||
338
src/services/mermaid/MermaidDiagramFixer.ts
Normal file
338
src/services/mermaid/MermaidDiagramFixer.ts
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
import { GoogleGenAI } from "@google/genai"
|
||||
import { safeJsonParse } from "../../shared/safeJsonParse"
|
||||
import { t } from "../../i18n"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// JSON Schema for structured Mermaid diagram representation
|
||||
const MERMAID_JSON_SCHEMA = {
|
||||
type: "object",
|
||||
properties: {
|
||||
diagramType: {
|
||||
type: "string",
|
||||
enum: ["flowchart", "sequence", "class", "state", "er", "gantt", "pie", "journey", "gitGraph", "mindmap"],
|
||||
},
|
||||
title: { type: "string" },
|
||||
nodes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
label: { type: "string" },
|
||||
shape: { type: "string" },
|
||||
style: { type: "string" },
|
||||
},
|
||||
required: ["id", "label"],
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
from: { type: "string" },
|
||||
to: { type: "string" },
|
||||
label: { type: "string" },
|
||||
type: { type: "string" },
|
||||
},
|
||||
required: ["from", "to"],
|
||||
},
|
||||
},
|
||||
participants: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
messages: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
from: { type: "string" },
|
||||
to: { type: "string" },
|
||||
message: { type: "string" },
|
||||
type: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
classes: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
attributes: { type: "array", items: { type: "string" } },
|
||||
methods: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
relationships: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
from: { type: "string" },
|
||||
to: { type: "string" },
|
||||
type: { type: "string" },
|
||||
label: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["diagramType"],
|
||||
}
|
||||
|
||||
export interface MermaidFixerOptions {
|
||||
geminiApiKey?: string
|
||||
geminiModel?: string
|
||||
}
|
||||
|
||||
export class MermaidDiagramFixer {
|
||||
private client: GoogleGenAI | null = null
|
||||
private modelName: string
|
||||
|
||||
constructor(private options: MermaidFixerOptions = {}) {
|
||||
this.modelName = options.geminiModel || "gemini-2.0-flash-exp"
|
||||
if (options.geminiApiKey) {
|
||||
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix a Mermaid diagram with syntax errors using Gemini AI
|
||||
* @param invalidCode The invalid Mermaid code
|
||||
* @param errorMessage The error message from Mermaid parser
|
||||
* @returns Fixed Mermaid code
|
||||
* @throws Error if API key is missing or fixing fails
|
||||
*/
|
||||
async fixDiagram(invalidCode: string, errorMessage: string): Promise<string> {
|
||||
if (!this.client || !this.options.geminiApiKey || this.options.geminiApiKey.trim() === "") {
|
||||
throw new Error("Gemini API key is required for diagram fixing")
|
||||
}
|
||||
|
||||
try {
|
||||
// Stage 1: Generate structured JSON representation
|
||||
const structuredJson = await this.generateStructuredJson(invalidCode, errorMessage)
|
||||
if (!structuredJson) {
|
||||
throw new Error("Failed to fix Mermaid diagram")
|
||||
}
|
||||
|
||||
// Stage 2: Validate JSON against schema
|
||||
const validationResult = this.validateJson(structuredJson)
|
||||
if (!validationResult.valid) {
|
||||
console.error("JSON validation failed:", validationResult.errors)
|
||||
throw new Error("Failed to fix Mermaid diagram")
|
||||
}
|
||||
|
||||
// Stage 3: Generate Python code to convert JSON to Mermaid
|
||||
const pythonCode = await this.generatePythonConverter(structuredJson)
|
||||
if (!pythonCode) {
|
||||
throw new Error("Failed to fix Mermaid diagram")
|
||||
}
|
||||
|
||||
// Stage 4: Execute Python code to get final Mermaid DSL
|
||||
const fixedMermaid = await this.executePythonCode(pythonCode, structuredJson)
|
||||
if (!fixedMermaid) {
|
||||
throw new Error("Failed to fix Mermaid diagram")
|
||||
}
|
||||
|
||||
// Post-process and validate the result
|
||||
const cleanedMermaid = this.postProcessMermaid(fixedMermaid)
|
||||
return cleanedMermaid
|
||||
} catch (error) {
|
||||
console.error("Error fixing Mermaid diagram:", error)
|
||||
if (error instanceof Error && error.message === "Gemini API key is required for diagram fixing") {
|
||||
throw error
|
||||
}
|
||||
throw new Error("Failed to fix Mermaid diagram")
|
||||
}
|
||||
}
|
||||
|
||||
private async generateStructuredJson(invalidCode: string, errorMessage: string): Promise<any> {
|
||||
const prompt = `
|
||||
You are a Mermaid diagram expert. Analyze this invalid Mermaid code and its error message, then generate a corrected version as a structured JSON object.
|
||||
|
||||
Invalid Mermaid Code:
|
||||
\`\`\`mermaid
|
||||
${invalidCode}
|
||||
\`\`\`
|
||||
|
||||
Error Message:
|
||||
${errorMessage}
|
||||
|
||||
Generate a JSON object that represents the corrected diagram structure. The JSON should follow this schema:
|
||||
${JSON.stringify(MERMAID_JSON_SCHEMA, null, 2)}
|
||||
|
||||
Rules:
|
||||
1. NO parentheses in any labels or text
|
||||
2. Use only alphanumeric characters, spaces, and basic punctuation in labels
|
||||
3. Ensure all node IDs are unique and valid
|
||||
4. Fix any syntax errors while preserving the original intent
|
||||
5. Return ONLY valid JSON, no markdown or explanations
|
||||
|
||||
Response:`
|
||||
|
||||
try {
|
||||
const model = this.client!.models.generateContent({
|
||||
model: this.modelName,
|
||||
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
maxOutputTokens: 2048,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await model
|
||||
const text = result.text || ""
|
||||
|
||||
// Try to parse as JSON
|
||||
const json = safeJsonParse(text, null)
|
||||
if (!json) {
|
||||
console.error("Failed to parse JSON from Gemini response:", text)
|
||||
return null
|
||||
}
|
||||
|
||||
return json
|
||||
} catch (error) {
|
||||
console.error("Error generating structured JSON:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private validateJson(json: any): { valid: boolean; errors?: string[] } {
|
||||
// Basic validation - check required fields and structure
|
||||
const errors: string[] = []
|
||||
|
||||
if (!json.diagramType) {
|
||||
errors.push("Missing required field: diagramType")
|
||||
}
|
||||
|
||||
// Check for parentheses in labels
|
||||
const checkForParentheses = (obj: any, path: string = "") => {
|
||||
if (typeof obj === "string" && (obj.includes("(") || obj.includes(")"))) {
|
||||
errors.push(`Parentheses found in ${path}: "${obj}"`)
|
||||
} else if (Array.isArray(obj)) {
|
||||
obj.forEach((item, index) => checkForParentheses(item, `${path}[${index}]`))
|
||||
} else if (obj && typeof obj === "object") {
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
checkForParentheses(value, path ? `${path}.${key}` : key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
checkForParentheses(json)
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
private async generatePythonConverter(json: any): Promise<string | null> {
|
||||
const prompt = `
|
||||
Generate a Python function that converts this JSON structure into valid Mermaid DSL syntax.
|
||||
|
||||
JSON Structure:
|
||||
${JSON.stringify(json, null, 2)}
|
||||
|
||||
Requirements:
|
||||
1. The function should be named 'json_to_mermaid'
|
||||
2. It should take the JSON object as input
|
||||
3. It should return a string containing valid Mermaid DSL
|
||||
4. Handle the specific diagram type (${json.diagramType})
|
||||
5. Ensure proper Mermaid syntax for the diagram type
|
||||
6. NO parentheses in any output
|
||||
7. Use proper escaping for special characters
|
||||
|
||||
Return ONLY the Python code, no explanations or markdown.`
|
||||
|
||||
try {
|
||||
const model = this.client!.models.generateContent({
|
||||
model: this.modelName,
|
||||
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
maxOutputTokens: 2048,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await model
|
||||
const code = result.text || ""
|
||||
|
||||
// Clean up the code if it has markdown
|
||||
const cleanCode = code
|
||||
.replace(/```python\n?/g, "")
|
||||
.replace(/```\n?/g, "")
|
||||
.trim()
|
||||
|
||||
return cleanCode
|
||||
} catch (error) {
|
||||
console.error("Error generating Python converter:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async executePythonCode(pythonCode: string, json: any): Promise<string | null> {
|
||||
// Since we can't actually execute Python in the browser/extension context,
|
||||
// we'll use Gemini's code execution capability
|
||||
const prompt = `
|
||||
Execute this Python code with the provided JSON input and return the output:
|
||||
|
||||
Python Code:
|
||||
\`\`\`python
|
||||
${pythonCode}
|
||||
|
||||
# Execute the function
|
||||
import json
|
||||
json_data = ${JSON.stringify(json)}
|
||||
result = json_to_mermaid(json_data)
|
||||
print(result)
|
||||
\`\`\`
|
||||
|
||||
Return ONLY the Mermaid DSL output, no explanations or markdown.`
|
||||
|
||||
try {
|
||||
const model = this.client!.models.generateContent({
|
||||
model: this.modelName,
|
||||
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
||||
config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 2048,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await model
|
||||
const mermaidCode = result.text || ""
|
||||
|
||||
// Clean up the output
|
||||
const cleanMermaid = mermaidCode
|
||||
.replace(/```mermaid\n?/g, "")
|
||||
.replace(/```\n?/g, "")
|
||||
.trim()
|
||||
|
||||
return cleanMermaid
|
||||
} catch (error) {
|
||||
console.error("Error executing Python code:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private postProcessMermaid(mermaidCode: string): string {
|
||||
// Remove any remaining parentheses
|
||||
let cleaned = mermaidCode.replace(/[()]/g, "")
|
||||
|
||||
// Ensure proper line endings
|
||||
cleaned = cleaned.replace(/\r\n/g, "\n")
|
||||
|
||||
// Remove any duplicate whitespace
|
||||
cleaned = cleaned.replace(/ +/g, " ")
|
||||
|
||||
// Trim each line
|
||||
cleaned = cleaned
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
.join("\n")
|
||||
|
||||
return cleaned
|
||||
}
|
||||
}
|
||||
204
src/services/mermaid/__tests__/MermaidDiagramFixer.spec.ts
Normal file
204
src/services/mermaid/__tests__/MermaidDiagramFixer.spec.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { MermaidDiagramFixer } from "../MermaidDiagramFixer"
|
||||
|
||||
// Mock the entire Google Generative AI module
|
||||
vi.mock("@google/generative-ai", () => {
|
||||
const mockGenerateContent = vi.fn()
|
||||
return {
|
||||
GoogleGenerativeAI: vi.fn().mockImplementation(() => ({
|
||||
getGenerativeModel: vi.fn().mockReturnValue({
|
||||
generateContent: mockGenerateContent,
|
||||
}),
|
||||
})),
|
||||
_mockGenerateContent: mockGenerateContent,
|
||||
}
|
||||
})
|
||||
|
||||
describe("MermaidDiagramFixer", () => {
|
||||
let fixer: MermaidDiagramFixer
|
||||
let mockApiConfig: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockApiConfig = {
|
||||
geminiApiKey: "test-api-key",
|
||||
geminiModel: "gemini-1.5-flash",
|
||||
}
|
||||
|
||||
fixer = new MermaidDiagramFixer(mockApiConfig)
|
||||
})
|
||||
|
||||
describe("fixDiagram", () => {
|
||||
it("should throw error if API key is not configured", async () => {
|
||||
const invalidConfig = { geminiModel: "gemini-1.5-flash" }
|
||||
const fixer = new MermaidDiagramFixer(invalidConfig)
|
||||
|
||||
await expect(fixer.fixDiagram("invalid diagram", "error message")).rejects.toThrow(
|
||||
"Gemini API key is required for diagram fixing",
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error if API key is empty string", async () => {
|
||||
const invalidConfig = { geminiApiKey: "", geminiModel: "gemini-1.5-flash" }
|
||||
const fixer = new MermaidDiagramFixer(invalidConfig)
|
||||
|
||||
await expect(fixer.fixDiagram("invalid diagram", "error message")).rejects.toThrow(
|
||||
"Gemini API key is required for diagram fixing",
|
||||
)
|
||||
})
|
||||
|
||||
it("should use default model if not specified", () => {
|
||||
const configWithoutModel = { geminiApiKey: "test-key" }
|
||||
const fixer = new MermaidDiagramFixer(configWithoutModel)
|
||||
|
||||
// The constructor should set a default model
|
||||
expect(fixer).toBeDefined()
|
||||
})
|
||||
|
||||
it("should handle API errors gracefully", async () => {
|
||||
// Get the mock function
|
||||
const { _mockGenerateContent } = (await import("@google/generative-ai")) as any
|
||||
_mockGenerateContent.mockRejectedValueOnce(new Error("API Error"))
|
||||
|
||||
await expect(fixer.fixDiagram("invalid diagram", "error")).rejects.toThrow("Failed to fix Mermaid diagram")
|
||||
})
|
||||
|
||||
it("should successfully process a valid response", async () => {
|
||||
// Get the mock function
|
||||
const { _mockGenerateContent } = (await import("@google/generative-ai")) as any
|
||||
|
||||
// Mock successful responses for each stage
|
||||
_mockGenerateContent
|
||||
.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () =>
|
||||
JSON.stringify({
|
||||
diagram_type: "flowchart",
|
||||
nodes: [
|
||||
{ id: "A", label: "Start" },
|
||||
{ id: "B", label: "End" },
|
||||
],
|
||||
edges: [{ from: "A", to: "B", label: "Process" }],
|
||||
}),
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () => "Valid JSON structure",
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () => `
|
||||
def convert_to_mermaid(json_data):
|
||||
result = "graph TD\\n"
|
||||
for edge in json_data['edges']:
|
||||
label = f"|{edge['label']}|" if edge.get('label') else ""
|
||||
result += f"{edge['from']} -->{label} {edge['to']}\\n"
|
||||
return result.strip()
|
||||
`,
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () => "graph TD\nA -->|Process| B",
|
||||
},
|
||||
})
|
||||
|
||||
const result = await fixer.fixDiagram("graph TD\nA -> B", "Syntax error")
|
||||
|
||||
expect(result).toBe("graph TD\nA -->|Process| B")
|
||||
expect(_mockGenerateContent).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it("should handle empty response from API", async () => {
|
||||
const { _mockGenerateContent } = (await import("@google/generative-ai")) as any
|
||||
_mockGenerateContent.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () => "",
|
||||
},
|
||||
})
|
||||
|
||||
await expect(fixer.fixDiagram("invalid", "error")).rejects.toThrow("Failed to fix Mermaid diagram")
|
||||
})
|
||||
|
||||
it("should handle invalid JSON response", async () => {
|
||||
const { _mockGenerateContent } = (await import("@google/generative-ai")) as any
|
||||
_mockGenerateContent.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () => "Not valid JSON",
|
||||
},
|
||||
})
|
||||
|
||||
await expect(fixer.fixDiagram("invalid", "error")).rejects.toThrow("Failed to fix Mermaid diagram")
|
||||
})
|
||||
|
||||
it("should handle sequence diagrams", async () => {
|
||||
const { _mockGenerateContent } = (await import("@google/generative-ai")) as any
|
||||
|
||||
_mockGenerateContent
|
||||
.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () =>
|
||||
JSON.stringify({
|
||||
diagram_type: "sequence",
|
||||
participants: ["Alice", "Bob"],
|
||||
messages: [{ from: "Alice", to: "Bob", message: "Hello", type: "solid" }],
|
||||
}),
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () => "Valid",
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () => `
|
||||
def convert_to_mermaid(json_data):
|
||||
result = "sequenceDiagram\\n"
|
||||
for p in json_data['participants']:
|
||||
result += f"participant {p}\\n"
|
||||
for msg in json_data['messages']:
|
||||
arrow = '->' if msg['type'] == 'solid' else '-->'
|
||||
result += f"{msg['from']}{arrow}{msg['to']}: {msg['message']}\\n"
|
||||
return result.strip()
|
||||
`,
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
response: {
|
||||
text: () => "sequenceDiagram\nparticipant Alice\nparticipant Bob\nAlice->Bob: Hello",
|
||||
},
|
||||
})
|
||||
|
||||
const result = await fixer.fixDiagram("sequenceDiagram\nAlice->Bob Hello", "Missing colon")
|
||||
|
||||
expect(result).toBe("sequenceDiagram\nparticipant Alice\nparticipant Bob\nAlice->Bob: Hello")
|
||||
})
|
||||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should handle network errors", async () => {
|
||||
const { _mockGenerateContent } = (await import("@google/generative-ai")) as any
|
||||
_mockGenerateContent.mockRejectedValueOnce(new Error("Network error"))
|
||||
|
||||
await expect(fixer.fixDiagram("invalid", "error")).rejects.toThrow("Failed to fix Mermaid diagram")
|
||||
})
|
||||
|
||||
it("should handle timeout errors", async () => {
|
||||
const { _mockGenerateContent } = (await import("@google/generative-ai")) as any
|
||||
_mockGenerateContent.mockRejectedValueOnce(new Error("Request timeout"))
|
||||
|
||||
await expect(fixer.fixDiagram("invalid", "error")).rejects.toThrow("Failed to fix Mermaid diagram")
|
||||
})
|
||||
|
||||
it("should handle rate limit errors", async () => {
|
||||
const { _mockGenerateContent } = (await import("@google/generative-ai")) as any
|
||||
_mockGenerateContent.mockRejectedValueOnce(new Error("Rate limit exceeded"))
|
||||
|
||||
await expect(fixer.fixDiagram("invalid", "error")).rejects.toThrow("Failed to fix Mermaid diagram")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -122,6 +122,7 @@ export interface ExtensionMessage {
|
|||
| "showEditMessageDialog"
|
||||
| "commands"
|
||||
| "insertTextIntoTextarea"
|
||||
| "mermaidDiagramFixed"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
action?:
|
||||
|
|
@ -196,6 +197,8 @@ export interface ExtensionMessage {
|
|||
messageTs?: number
|
||||
context?: string
|
||||
commands?: Command[]
|
||||
originalCode?: string
|
||||
fixedCode?: string
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
|
|||
|
|
@ -212,8 +212,11 @@ export interface WebviewMessage {
|
|||
| "createCommand"
|
||||
| "insertTextIntoTextarea"
|
||||
| "showMdmAuthRequiredNotification"
|
||||
| "fixMermaidDiagram"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
code?: string
|
||||
error?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
disabled?: boolean
|
||||
context?: string
|
||||
|
|
|
|||
|
|
@ -87,11 +87,13 @@ interface MermaidBlockProps {
|
|||
code: string
|
||||
}
|
||||
|
||||
export default function MermaidBlock({ code }: MermaidBlockProps) {
|
||||
export default function MermaidBlock({ code: initialCode }: MermaidBlockProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [code, setCode] = useState(initialCode)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isErrorExpanded, setIsErrorExpanded] = useState(false)
|
||||
const [isFixing, setIsFixing] = useState(false)
|
||||
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
|
|
@ -101,6 +103,23 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
|
|||
setError(null)
|
||||
}, [code])
|
||||
|
||||
// Listen for fixed diagram response from extension
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "mermaidDiagramFixed" && message.originalCode === initialCode) {
|
||||
// Update the code with the fixed version
|
||||
setCode(message.fixedCode)
|
||||
setIsFixing(false)
|
||||
setError(null)
|
||||
setIsErrorExpanded(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
}, [initialCode])
|
||||
|
||||
// 2) Debounce the actual parse/render
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
|
|
@ -153,6 +172,22 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
|
|||
|
||||
// Copy functionality handled directly through the copyWithFeedback utility
|
||||
|
||||
const handleFixDiagram = async () => {
|
||||
setIsFixing(true)
|
||||
try {
|
||||
// Send message to extension to fix the diagram
|
||||
vscode.postMessage({
|
||||
type: "fixMermaidDiagram",
|
||||
code: code,
|
||||
error: error || undefined,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error("Error fixing diagram:", err)
|
||||
} finally {
|
||||
setIsFixing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MermaidBlockContainer>
|
||||
{isLoading && <LoadingMessage>{t("common:mermaid.loading")}</LoadingMessage>}
|
||||
|
|
@ -188,7 +223,16 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
|
|||
}}></span>
|
||||
<span style={{ fontWeight: "bold" }}>{t("common:mermaid.render_error")}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<FixButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleFixDiagram()
|
||||
}}
|
||||
disabled={isFixing}>
|
||||
<span className={`codicon codicon-${isFixing ? "loading" : "wrench"}`}></span>
|
||||
<span style={{ marginLeft: "4px" }}>{t("common:mermaid.buttons.fix")}</span>
|
||||
</FixButton>
|
||||
<CopyButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -309,6 +353,39 @@ const CopyButton = styled.button`
|
|||
}
|
||||
`
|
||||
|
||||
const FixButton = styled.button<{ disabled?: boolean }>`
|
||||
padding: 4px 8px;
|
||||
height: 26px;
|
||||
margin-right: 4px;
|
||||
color: var(--vscode-button-foreground);
|
||||
background: var(--vscode-button-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
|
||||
font-size: 12px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.codicon-loading {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface SvgContainerProps {
|
||||
$isLoading: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@
|
|||
"save": "Save Image",
|
||||
"viewCode": "View Code",
|
||||
"viewDiagram": "View Diagram",
|
||||
"close": "Close"
|
||||
"close": "Close",
|
||||
"fix": "Fix Diagram"
|
||||
},
|
||||
"modal": {
|
||||
"codeTitle": "Mermaid Code"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue