From 02d81ede2462acc5881a4034076c8569b48879e3 Mon Sep 17 00:00:00 2001 From: Prashant-7718 Date: Wed, 4 Feb 2026 14:24:19 +0530 Subject: [PATCH] Added Repair Diagram Function to SDK Using Backend AI and Credit Management --- .eslintrc.cjs | 2 +- .github/workflows/build.yml | 2 +- cSpell.json | 1 + packages/cli/.eslintrc.cjs | 2 +- packages/cli/package.json | 2 +- packages/sdk/package.json | 2 +- packages/sdk/src/errors.ts | 11 +++++++ packages/sdk/src/index.e2e.test.ts | 24 +++++++++++++++ packages/sdk/src/index.test.ts | 36 +++++++++++++++++++++++ packages/sdk/src/index.ts | 43 ++++++++++++++++++++++++++- packages/sdk/src/types.ts | 47 ++++++++++++++++++++++++++++++ packages/sdk/src/urls.ts | 3 ++ 12 files changed, 169 insertions(+), 6 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1f85637..b05c5c0 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -110,7 +110,7 @@ module.exports = { }, overrides: [ { - files: ['cypress/**', 'demos/**'], + files: ['cypress/**', 'demos/**', 'packages/cli/**', 'packages/sdk/**'], rules: { 'no-console': 'off', }, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f66692..5c77332 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 15 strategy: matrix: - node: ['18.18.x'] + node: ['18.18.x', '20.x', '22.x', '24.x'] pkg: ['sdk', 'cli'] os: [ubuntu-latest, windows-latest, macos-latest] runs-on: diff --git a/cSpell.json b/cSpell.json index c6cb8b9..280b7cb 100644 --- a/cSpell.json +++ b/cSpell.json @@ -4,6 +4,7 @@ "words": [ "Alois", "Cataa", + "collab", "colour", "Cookiebot", "ENONET", diff --git a/packages/cli/.eslintrc.cjs b/packages/cli/.eslintrc.cjs index d826455..864ec22 100644 --- a/packages/cli/.eslintrc.cjs +++ b/packages/cli/.eslintrc.cjs @@ -11,6 +11,6 @@ module.exports = /** @type {import("eslint").Linter.Config} */ ({ parser: '@typescript-eslint/parser', }, rules: { - "no-console": ["warn"], // TODO: fix all of these + "no-console": "off", // Console output is expected in CLI applications } }); diff --git a/packages/cli/package.json b/packages/cli/package.json index 7711939..fa86932 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -7,7 +7,7 @@ "mermaid-chart": "dist/cli.js" }, "engines": { - "node": "^18.18.0 || ^20.0.0" + "node": "^18.18.0 || ^20.0.0 || ^22.0.0 || ^24.0.0" }, "scripts": { "dev": "tsx src/cli.ts", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 4594129..638a663 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@mermaidchart/sdk", - "version": "0.2.1", + "version": "0.2.2", "description": "Access the MermaidChart services with OAuth and type safety.", "browser": "dist/bundle.iife.js", "types": "dist/index.d.ts", diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index 5556672..879e76d 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -9,3 +9,14 @@ export class OAuthError extends Error { super(message); } } + +/** + * Error thrown when AI credits limit is exceeded. + * This corresponds to HTTP 402 status code from the repair API. + */ +export class AICreditsLimitExceededError extends Error { + constructor(message: string = 'AI credits limit exceeded') { + super(message); + this.name = 'AICreditsLimitExceededError'; + } +} diff --git a/packages/sdk/src/index.e2e.test.ts b/packages/sdk/src/index.e2e.test.ts index a269881..3f567d8 100644 --- a/packages/sdk/src/index.e2e.test.ts +++ b/packages/sdk/src/index.e2e.test.ts @@ -2,6 +2,7 @@ * E2E tests */ import { MermaidChart } from './index.js'; +import { AICreditsLimitExceededError } from './errors.js'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import process from 'node:process'; @@ -203,3 +204,26 @@ describe('getDocument', () => { expect(error?.response?.status).toBe(404); }); }); + +describe('repairDiagram', () => { + it('should repair a broken diagram', async () => { + const brokenCode = 'graph TD\n A[Start] --> B{Decision}\n B -->|Yes| C[End'; + const error = 'Syntax error: missing closing bracket'; + + try { + const result = await client.repairDiagram({ + code: brokenCode, + error: error, + }); + + expect(result).toHaveProperty('result'); + expect(result).toHaveProperty('code'); + expect(['ok', 'fail']).toContain(result.result); + } catch (error) { + if (error instanceof AICreditsLimitExceededError) { + return; // Credits exceeded is acceptable + } + throw error; + } + }, 60000); // 60 seconds timeout for AI repair operations +}); diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index 0030f1d..482dfd8 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { MermaidChart } from './index.js'; +import { AICreditsLimitExceededError } from './errors.js'; import type { AuthorizationData } from './types.js'; import { OAuth2Client } from '@badgateway/oauth2-client'; @@ -85,4 +86,39 @@ describe('MermaidChart', () => { await expect(client.getAccessToken()).resolves.toBe('test-example-access_token'); }); }); + + describe('#repairDiagram', () => { + beforeEach(async () => { + await client.setAccessToken('test-access-token'); + }); + + it('should repair diagram successfully', async () => { + vi.spyOn(client, 'repairDiagram').mockResolvedValue({ + result: 'ok' as const, + code: '```mermaid\ngraph TD\n A[Start] --> B[End]\n```', + solved: true, + }); + + const result = await client.repairDiagram({ + code: 'graph TD\n A[Start] --> B{Decision}', + error: 'Syntax error', + }); + + expect(result.result).toBe('ok'); + expect(result.solved).toBe(true); + }); + + it('should throw AICreditsLimitExceededError on 402', async () => { + vi.spyOn(client, 'repairDiagram').mockRejectedValue( + new AICreditsLimitExceededError('AI credits limit exceeded'), + ); + + await expect( + client.repairDiagram({ + code: 'graph TD\n A --> B', + error: 'Syntax error', + }), + ).rejects.toThrow(AICreditsLimitExceededError); + }); + }); }); diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 60a999d..5311336 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -2,7 +2,11 @@ import { OAuth2Client, generateCodeVerifier } from '@badgateway/oauth2-client'; import type { AxiosInstance, AxiosResponse } from 'axios'; import defaultAxios from 'axios'; import { v4 as uuid } from 'uuid'; -import { OAuthError, RequiredParameterMissingError } from './errors.js'; +import { + AICreditsLimitExceededError, + OAuthError, + RequiredParameterMissingError, +} from './errors.js'; import type { AuthState, AuthorizationData, @@ -11,6 +15,8 @@ import type { MCDocument, MCProject, MCUser, + RepairDiagramRequest, + RepairDiagramResponse, } from './types.js'; import { URLS } from './urls.js'; @@ -266,4 +272,39 @@ export class MermaidChart { const raw = await this.axios.get(URLS.raw(document, theme).svg); return raw.data; } + + /** + * Repairs a broken Mermaid diagram using AI. + * + * @param request - The repair request containing diagram code and error message + * @returns The repair response with repaired code and status + * @throws {@link AICreditsLimitExceededError} if credits limit exceeded (HTTP 402) + */ + public async repairDiagram(request: RepairDiagramRequest): Promise { + try { + const response = await this.axios.post( + URLS.rest.openai.repair, + request, + ); + return response.data; + } catch (error: unknown) { + if ( + error && + typeof error === 'object' && + 'response' in error && + error.response && + typeof error.response === 'object' && + 'status' in error.response && + error.response.status === 402 + ) { + const axiosError = error as { response: { status: number; data?: unknown } }; + throw new AICreditsLimitExceededError( + typeof axiosError.response.data === 'string' + ? axiosError.response.data + : 'AI credits limit exceeded', + ); + } + throw error; + } + } } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index b755b56..895b852 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -65,3 +65,50 @@ export interface AuthorizationData { state: string; scope: string[]; } + +/** + * Request parameters for repairing a diagram. + */ +export interface RepairDiagramRequest { + /** The Mermaid diagram code that needs to be repaired */ + code: string; + /** The error message from the broken diagram */ + error: string; + /** + * The diagram UUID to associate this repair with, or + * `undefined` if it is not associated with a diagram (e.g. in the playground). + */ + diagramDocumentID?: string; + /** + * The diagram ID associated with this repair. + */ + diagramID?: string; + /** + * The user ID associated with this repair. + */ + userID?: string; +} + +/** + * Response from repairing a diagram. + * Matches OpenAIGenerationResult from collab. + */ +export interface RepairDiagramResponse { + /** + * The status of the repair: 'ok' if successful, 'fail' if not. + * `ok` indicates that a valid mermaid code block was generated. + * It may still fail to render. + * + * `fail` indicates that there were no exceptions/errors, but no valid + * mermaid code block was generated. + */ + result: 'ok' | 'fail'; + /** + * Markdown message, that may contain a valid mermaid code block + */ + code: string; + /** + * Whether the diagram repair was successful (only present for repair responses) + */ + solved?: boolean; +} diff --git a/packages/sdk/src/urls.ts b/packages/sdk/src/urls.ts index c337f0a..ffa23e6 100644 --- a/packages/sdk/src/urls.ts +++ b/packages/sdk/src/urls.ts @@ -38,6 +38,9 @@ export const URLS = { }; }, }, + openai: { + repair: `/rest-api/openai/repair`, + }, }, raw: (document: Pick, theme: 'light' | 'dark') => { const base = `/raw/${document.documentID}?version=v${document.major}.${document.minor}&theme=${theme}&format=`;