mirror of
https://github.com/vndangkhoa/Sys-Arc-Visl.git
synced 2026-04-05 01:17:57 +07:00
feat: Add ComfyUI import, static view mode, and dashboard controls
This commit is contained in:
parent
5ada3b1fcd
commit
5af1e14750
12 changed files with 1073 additions and 220 deletions
235
package-lock.json
generated
235
package-lock.json
generated
|
|
@ -34,6 +34,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@rollup/rollup-darwin-arm64": "^4.55.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.1",
|
"@testing-library/react": "^16.3.1",
|
||||||
|
|
@ -626,9 +627,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.7.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||||
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
|
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1078,9 +1079,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1235,9 +1236,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@exodus/bytes": {
|
"node_modules/@exodus/bytes": {
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.8.0.tgz",
|
||||||
"integrity": "sha512-5i+BtvujK/vM07YCGDyz4C4AyDzLmhxHMtM5HpUyPRtJPBdFPsj290ffXW+UXY21/G7GtXeHD2nRmq0T1ShyQQ==",
|
"integrity": "sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2089,15 +2090,14 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.54.0",
|
"version": "4.55.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.0.tgz",
|
||||||
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
|
"integrity": "sha512-iiSGJu03Vsi2+Zz9PRbJ18icTAte/Geh/3f5T94DGDwuCa2GBY0MwIyvgZNV6Hur5fBgEBsUUqIZ/cPC8r9B/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
]
|
]
|
||||||
|
|
@ -2369,9 +2369,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.34.45",
|
"version": "0.34.46",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.45.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.46.tgz",
|
||||||
"integrity": "sha512-qJcFVfCa5jxBFSuv7S5WYbA8XdeCPmhnaVVfX/2Y6L8WYg8sk3XY2+6W0zH+3mq1Cz+YC7Ki66HfqX6IHAwnkg==",
|
"integrity": "sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
@ -3226,20 +3226,20 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz",
|
||||||
"integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==",
|
"integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.50.1",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/type-utils": "8.50.1",
|
"@typescript-eslint/type-utils": "8.51.0",
|
||||||
"@typescript-eslint/utils": "8.50.1",
|
"@typescript-eslint/utils": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -3249,7 +3249,7 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.50.1",
|
"@typescript-eslint/parser": "^8.51.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -3265,16 +3265,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz",
|
||||||
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
|
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.50.1",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3290,14 +3290,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz",
|
||||||
"integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==",
|
"integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.50.1",
|
"@typescript-eslint/tsconfig-utils": "^8.51.0",
|
||||||
"@typescript-eslint/types": "^8.50.1",
|
"@typescript-eslint/types": "^8.51.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3312,14 +3312,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz",
|
||||||
"integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==",
|
"integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.1"
|
"@typescript-eslint/visitor-keys": "8.51.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -3330,9 +3330,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==",
|
"integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3347,17 +3347,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==",
|
"integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||||
"@typescript-eslint/utils": "8.50.1",
|
"@typescript-eslint/utils": "8.51.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -3372,9 +3372,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz",
|
||||||
"integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==",
|
"integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3386,21 +3386,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz",
|
||||||
"integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==",
|
"integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.50.1",
|
"@typescript-eslint/project-service": "8.51.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.50.1",
|
"@typescript-eslint/tsconfig-utils": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.1",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"minimatch": "^9.0.4",
|
"minimatch": "^9.0.4",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -3453,16 +3453,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==",
|
"integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.50.1",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.1"
|
"@typescript-eslint/typescript-estree": "8.51.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -3477,13 +3477,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz",
|
||||||
"integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==",
|
"integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.1",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3936,9 +3936,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001761",
|
"version": "1.0.30001762",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
||||||
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
|
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -4163,20 +4163,31 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cssstyle": {
|
"node_modules/cssstyle": {
|
||||||
"version": "5.3.5",
|
"version": "5.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.6.tgz",
|
||||||
"integrity": "sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag==",
|
"integrity": "sha512-legscpSpgSAeGEe0TNcai97DKt9Vd9AsAdOL7Uoetb52Ar/8eJm3LIa39qpv8wWzLFlNG4vVvppQM+teaMPj3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asamuzakjp/css-color": "^4.1.1",
|
"@asamuzakjp/css-color": "^4.1.1",
|
||||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.21",
|
"@csstools/css-syntax-patches-for-csstree": "^1.0.21",
|
||||||
"css-tree": "^3.1.0"
|
"css-tree": "^3.1.0",
|
||||||
|
"lru-cache": "^11.2.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cssstyle/node_modules/lru-cache": {
|
||||||
|
"version": "11.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||||
|
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
|
@ -5103,9 +5114,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esquery": {
|
"node_modules/esquery": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -6081,9 +6092,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lib0": {
|
"node_modules/lib0": {
|
||||||
"version": "0.2.116",
|
"version": "0.2.117",
|
||||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.116.tgz",
|
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
|
||||||
"integrity": "sha512-4zsosjzmt33rx5XjmFVYUAeLNh+BTeDTiwGdLt4muxiir2btsc60Nal0EvkvDRizg+pnlK1q+BtYi7M+d4eStw==",
|
"integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isomorphic.js": "^0.2.4"
|
"isomorphic.js": "^0.2.4"
|
||||||
|
|
@ -7332,6 +7343,20 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
|
||||||
|
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/roughjs": {
|
"node_modules/roughjs": {
|
||||||
"version": "4.6.6",
|
"version": "4.6.6",
|
||||||
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
|
||||||
|
|
@ -7826,9 +7851,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.3.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||||
"integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==",
|
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -7894,16 +7919,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.50.1",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz",
|
||||||
"integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==",
|
"integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
"@typescript-eslint/eslint-plugin": "8.51.0",
|
||||||
"@typescript-eslint/parser": "8.50.1",
|
"@typescript-eslint/parser": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.1",
|
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||||
"@typescript-eslint/utils": "8.50.1"
|
"@typescript-eslint/utils": "8.51.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -8214,9 +8239,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||||
"integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
|
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -8404,9 +8429,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yjs": {
|
"node_modules/yjs": {
|
||||||
"version": "13.6.28",
|
"version": "13.6.29",
|
||||||
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.28.tgz",
|
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz",
|
||||||
"integrity": "sha512-EgnDOXs8+hBVm6mq3/S89Kiwzh5JRbn7w2wXwbrMRyKy/8dOFsLvuIfC+x19ZdtaDc0tA9rQmdZzbqqNHG44wA==",
|
"integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.99"
|
"lib0": "^0.2.99"
|
||||||
|
|
@ -8434,9 +8459,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.2.1",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||||
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
|
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@rollup/rollup-darwin-arm64": "^4.55.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.1",
|
"@testing-library/react": "^16.3.1",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { nodeTypes } from './nodes/CustomNodes';
|
||||||
import { edgeTypes, EdgeDefs } from './edges/AnimatedEdge';
|
import { edgeTypes, EdgeDefs } from './edges/AnimatedEdge';
|
||||||
import {
|
import {
|
||||||
Spline, Minus, Plus, Maximize, Map, Wand2,
|
Spline, Minus, Plus, Maximize, Map, Wand2,
|
||||||
Hand, MousePointer2, Settings2, ChevronDown
|
Hand, MousePointer2, Settings2, ChevronDown, FileImage, Trash2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { getLayoutedElements } from '../lib/layoutEngine';
|
import { getLayoutedElements } from '../lib/layoutEngine';
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ export function FlowCanvas() {
|
||||||
const {
|
const {
|
||||||
nodes, edges, onNodesChange, onEdgesChange, onConnect,
|
nodes, edges, onNodesChange, onEdgesChange, onConnect,
|
||||||
setSelectedNode, edgeStyle, setEdgeStyle, theme, activeFilters,
|
setSelectedNode, edgeStyle, setEdgeStyle, theme, activeFilters,
|
||||||
setNodes, setEdges, focusMode
|
setNodes, setEdges, focusMode, viewMode, setViewMode
|
||||||
} = useFlowStore();
|
} = useFlowStore();
|
||||||
const { isMobile } = useMobileDetect();
|
const { isMobile } = useMobileDetect();
|
||||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||||
|
|
@ -110,9 +110,104 @@ export function FlowCanvas() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for Dynamic Label Filter
|
||||||
|
const label = (node.data?.label as string || '').trim();
|
||||||
|
const dynamicFilterId = `dyn-${label}`;
|
||||||
|
|
||||||
|
// Logic:
|
||||||
|
// 1. If a dynamic filter active state exists for this label (meaning the label is "known" effectively), check it.
|
||||||
|
// However, activeFilters is a list of IDs.
|
||||||
|
// Wait, we don't know if the filter 'exists' here easily without scanning all nodes or passing `dynamicFilters` prop.
|
||||||
|
// But we know if `dyn-` version is in `activeFilters`, it is explicitly ON.
|
||||||
|
// If `dyn-` version is NOT in `activeFilters`, is it implicitly OFF?
|
||||||
|
// In `InteractiveLegend`, we auto-add new dynamic labels to `activeFilters`.
|
||||||
|
// So if it IS a known dynamic label, it SHOULD be in `activeFilters` to be visible.
|
||||||
|
// But `FlowCanvas` doesn't know if it's a "known" dynamic label or just some random text.
|
||||||
|
// Heuristic: If the label is short enough to trigger a dynamic filter (length < 30),
|
||||||
|
// then we assume it is governed by the dynamic filter system.
|
||||||
|
|
||||||
|
let isVisible = false;
|
||||||
|
|
||||||
|
if (label.length < 30 && label.length > 0) {
|
||||||
|
// It is a dynamic filter candidate.
|
||||||
|
// Visibility is determined by presence in activeFilters.
|
||||||
|
// Note: InteractiveLegend adds them asynchronously. There might be a split second where it's hidden before appearing.
|
||||||
|
// To prevent flickering: maybe default to TRUE if activeFilters doesn't contain ANY dynamic filters yet? No.
|
||||||
|
// We will trust the store.
|
||||||
|
|
||||||
|
// If the user has disabled the category (e.g. 'Server'), should 'KSampler' still show?
|
||||||
|
// User request imply: "specific node that appear".
|
||||||
|
// Usually specific overrides general.
|
||||||
|
|
||||||
|
const isDynamicActive = activeFilters.includes(dynamicFilterId);
|
||||||
|
if (isDynamicActive) {
|
||||||
|
isVisible = true;
|
||||||
|
} else {
|
||||||
|
// CAUTION: If it's NOT in activeFilters, it could mean:
|
||||||
|
// A) InteractiveLegend hasn't added it yet (it's new) -> Should show?
|
||||||
|
// B) User explicitly turned it off -> Should hide.
|
||||||
|
|
||||||
|
// Problem: We can't distinguish A from B easily here.
|
||||||
|
// But we know InteractiveLegend adds them immediately on mount/update.
|
||||||
|
// A flash of invisibility is possible.
|
||||||
|
// BUT, we can fallback to Category visibility if Dynamic visibility is "off" (missing)?
|
||||||
|
// No, if I turn off "KSampler", I want it gone.
|
||||||
|
|
||||||
|
// Let's assume if the label is "Dynamic-able", strict filtering applies.
|
||||||
|
// But if activeFilters doesn't have it, it's hidden.
|
||||||
|
|
||||||
|
// Fallback check:
|
||||||
|
// Is the CATEGORY also required?
|
||||||
|
// If I have "KSampler" (Other), and I turn off "Other", "KSampler" should probably hide too?
|
||||||
|
// Composite logic: Visible if (Category is Active) AND (Dynamic is Active or Not Applicable).
|
||||||
|
|
||||||
|
// BUT, dynamic filters are added to the list.
|
||||||
|
// If I toggle "KSampler", it removes from list.
|
||||||
|
// So we must check dynamic ID.
|
||||||
|
|
||||||
|
// If we enforce BOTH, then unchecking "Other" hides everything.
|
||||||
|
// If we enforce EITHER, then unchecking "Other" keeps KSampler.
|
||||||
|
|
||||||
|
// Let's go with: Dynamic Filter OVERRIDES Category if present?
|
||||||
|
// Or Dynamic Filter is an AND condition?
|
||||||
|
// Usually: Visible = CategoryActive && (DynamicActive if exists).
|
||||||
|
|
||||||
|
// Let's try:
|
||||||
|
// If `activeFilters` has ANY `dyn-`... implied system is active.
|
||||||
|
}
|
||||||
|
|
||||||
|
// REVISED LOGIC:
|
||||||
|
// We check if `dyn-${label}` is in activeFilters.
|
||||||
|
// If we find ANY `dyn-` filters in `activeFilters` at all, we assume system is initialized.
|
||||||
|
// If so, we stick to strict checking.
|
||||||
|
|
||||||
|
const anyDynamicActive = activeFilters.some(f => f.startsWith('dyn-'));
|
||||||
|
|
||||||
|
if (!anyDynamicActive) {
|
||||||
|
// System maybe not ready or no dynamic filters active.
|
||||||
|
// Fallback to category.
|
||||||
|
isVisible = activeFilters.includes(category);
|
||||||
|
} else {
|
||||||
|
// System has dynamic filters.
|
||||||
|
// If this specific dynamic filter is present, show.
|
||||||
|
// Also check category?
|
||||||
|
// Let's prioritize Dynamic Filter visibility.
|
||||||
|
if (activeFilters.includes(dynamicFilterId)) {
|
||||||
|
isVisible = true;
|
||||||
|
} else {
|
||||||
|
// Dynamic filter is OFF.
|
||||||
|
isVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Not a dynamic label situation, standard category
|
||||||
|
isVisible = activeFilters.includes(category);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
hidden: !activeFilters.includes(category)
|
hidden: !isVisible
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [nodes, activeFilters]);
|
}, [nodes, activeFilters]);
|
||||||
|
|
@ -146,14 +241,18 @@ export function FlowCanvas() {
|
||||||
});
|
});
|
||||||
}, [edges, edgeStyle]);
|
}, [edges, edgeStyle]);
|
||||||
|
|
||||||
// Filter edges to only show connections between visible nodes
|
// Filter edges to only show connections between visible nodes AND if edges are enabled
|
||||||
const filteredEdges = useMemo(() => {
|
const filteredEdges = useMemo(() => {
|
||||||
|
// Check if edges are globally enabled via filter
|
||||||
|
const edgesEnabled = activeFilters.includes('filter-edge');
|
||||||
|
if (!edgesEnabled) return [];
|
||||||
|
|
||||||
const visibleNodeIds = new Set(filteredNodes.filter(n => !n.hidden).map(n => n.id));
|
const visibleNodeIds = new Set(filteredNodes.filter(n => !n.hidden).map(n => n.id));
|
||||||
return styledEdges.map(edge => ({
|
return styledEdges.map(edge => ({
|
||||||
...edge,
|
...edge,
|
||||||
hidden: !visibleNodeIds.has(edge.source) || !visibleNodeIds.has(edge.target)
|
hidden: !visibleNodeIds.has(edge.source) || !visibleNodeIds.has(edge.target)
|
||||||
}));
|
}));
|
||||||
}, [styledEdges, filteredNodes]);
|
}, [styledEdges, filteredNodes, activeFilters]);
|
||||||
|
|
||||||
// Node click handler - bidirectional highlighting
|
// Node click handler - bidirectional highlighting
|
||||||
const onNodeClick = useCallback((_event: React.MouseEvent, node: any) => {
|
const onNodeClick = useCallback((_event: React.MouseEvent, node: any) => {
|
||||||
|
|
@ -234,6 +333,24 @@ export function FlowCanvas() {
|
||||||
{/* Control Panel - Top Right (Unified Toolkit) - Desktop Only */}
|
{/* Control Panel - Top Right (Unified Toolkit) - Desktop Only */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Panel position="top-right" className={`!m-4 !mr-6 flex flex-col items-end gap-3 z-50 transition-all duration-300 ${focusMode ? '!mt-20' : ''}`}>
|
<Panel position="top-right" className={`!m-4 !mr-6 flex flex-col items-end gap-3 z-50 transition-all duration-300 ${focusMode ? '!mt-20' : ''}`}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Clear Dashboard Button */}
|
||||||
|
{nodes.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setNodes([]);
|
||||||
|
setEdges([]);
|
||||||
|
useFlowStore.getState().setMermaidCode('');
|
||||||
|
useFlowStore.getState().setDescription('');
|
||||||
|
useFlowStore.getState().setSourceCode('');
|
||||||
|
}}
|
||||||
|
className="h-10 w-10 flex items-center justify-center rounded-xl bg-white/90 dark:bg-surface/90 backdrop-blur-md border border-red-200 dark:border-red-900/30 text-red-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 shadow-sm transition-all"
|
||||||
|
title="Clear Dashboard"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowToolkit(!showToolkit)}
|
onClick={() => setShowToolkit(!showToolkit)}
|
||||||
|
|
@ -319,11 +436,18 @@ export function FlowCanvas() {
|
||||||
active={showMiniMap}
|
active={showMiniMap}
|
||||||
onClick={() => setShowMiniMap(!showMiniMap)}
|
onClick={() => setShowMiniMap(!showMiniMap)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<MenuButton
|
||||||
|
icon={FileImage}
|
||||||
|
label="Static View"
|
||||||
|
active={false}
|
||||||
|
onClick={() => setViewMode('static')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { ImageUpload } from './ImageUpload';
|
import { ImageUpload } from './ImageUpload';
|
||||||
import { CodeEditor } from './CodeEditor';
|
import { CodeEditor } from './CodeEditor';
|
||||||
import { Image, Code, MessageSquare, Loader2, Zap } from 'lucide-react';
|
import { ComfyImportPanel } from './editor/ComfyImportPanel'; // Import new panel
|
||||||
|
import { Image, Code, MessageSquare, Loader2, Zap, Workflow } from 'lucide-react'; // Add Workflow icon
|
||||||
import { useFlowStore } from '../store';
|
import { useFlowStore } from '../store';
|
||||||
import { interpretText } from '../lib/aiService';
|
import { interpretText } from '../lib/aiService';
|
||||||
import { parseMermaid } from '../lib/mermaidParser';
|
import { parseMermaid } from '../lib/mermaidParser';
|
||||||
import { getLayoutedElements } from '../lib/layoutEngine';
|
import { getLayoutedElements } from '../lib/layoutEngine';
|
||||||
|
|
||||||
type Tab = 'image' | 'code' | 'describe';
|
type Tab = 'image' | 'code' | 'describe' | 'comfy'; // Add 'comfy' tab type
|
||||||
|
|
||||||
export function InputPanel() {
|
export function InputPanel() {
|
||||||
const {
|
const {
|
||||||
|
|
@ -95,16 +96,17 @@ export function InputPanel() {
|
||||||
}, [description, ollamaUrl, modelName, aiMode, onlineProvider, apiKey, generationComplexity, setNodes, setEdges, setLoading, setError, setSourceCode, setMermaidCode, saveDiagram]);
|
}, [description, ollamaUrl, modelName, aiMode, onlineProvider, apiKey, generationComplexity, setNodes, setEdges, setLoading, setError, setSourceCode, setMermaidCode, saveDiagram]);
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'image' as Tab, icon: Image, label: 'Upload' },
|
{ id: 'describe' as Tab, icon: MessageSquare, label: 'Describe' }, // Moved Describe first as primary
|
||||||
{ id: 'code' as Tab, icon: Code, label: 'Code' },
|
{ id: 'code' as Tab, icon: Code, label: 'Code' },
|
||||||
{ id: 'describe' as Tab, icon: MessageSquare, label: 'Describe' },
|
{ id: 'comfy' as Tab, icon: Workflow, label: 'ComfyUI' }, // New Tab
|
||||||
|
{ id: 'image' as Tab, icon: Image, label: 'Upload' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Floating Tabs */}
|
{/* Floating Tabs */}
|
||||||
<div className="px-4 pt-6 pb-2">
|
<div className="px-4 pt-6 pb-2">
|
||||||
<div className="flex bg-slate-100 dark:bg-black/20 p-1 rounded-full border border-black/5 dark:border-white/5 mx-auto max-w-[280px]">
|
<div className="flex bg-slate-100 dark:bg-black/20 p-1 rounded-full border border-black/5 dark:border-white/5 mx-auto w-full">
|
||||||
{tabs.map(tab => {
|
{tabs.map(tab => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
const isActive = activeTab === tab.id;
|
const isActive = activeTab === tab.id;
|
||||||
|
|
@ -121,7 +123,7 @@ export function InputPanel() {
|
||||||
)}
|
)}
|
||||||
<div className="relative flex items-center gap-2 z-10">
|
<div className="relative flex items-center gap-2 z-10">
|
||||||
<Icon className="w-3.5 h-3.5" />
|
<Icon className="w-3.5 h-3.5" />
|
||||||
<span className="text-[10px] font-black uppercase tracking-wider">{tab.label}</span>
|
<span className="text-[10px] font-black uppercase tracking-wider hidden sm:inline">{tab.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
@ -129,7 +131,7 @@ export function InputPanel() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Complexity Toggle */}
|
{/* Complexity Toggle - Only for text intent flow */}
|
||||||
{(activeTab === 'image' || activeTab === 'describe') && (
|
{(activeTab === 'image' || activeTab === 'describe') && (
|
||||||
<div className="px-4 py-3">
|
<div className="px-4 py-3">
|
||||||
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 pl-1 mb-2 block">Diagram Complexity</label>
|
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 pl-1 mb-2 block">Diagram Complexity</label>
|
||||||
|
|
@ -164,13 +166,14 @@ export function InputPanel() {
|
||||||
<div className="flex-1 px-4 pb-6 overflow-y-auto hide-scrollbar">
|
<div className="flex-1 px-4 pb-6 overflow-y-auto hide-scrollbar">
|
||||||
{activeTab === 'image' && <ImageUpload />}
|
{activeTab === 'image' && <ImageUpload />}
|
||||||
{activeTab === 'code' && <CodeEditor />}
|
{activeTab === 'code' && <CodeEditor />}
|
||||||
|
{activeTab === 'comfy' && <ComfyImportPanel />}
|
||||||
|
|
||||||
{activeTab === 'describe' && (
|
{activeTab === 'describe' && (
|
||||||
<div className="h-full flex flex-col animate-slide-up">
|
<div className="h-full flex flex-col animate-slide-up">
|
||||||
<textarea
|
<textarea
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="Describe your diagram in natural language...
|
placeholder="Describe your diagram in natural language...
|
||||||
|
|
||||||
Example: Create a user registration flow with login, verification, and dashboard access"
|
Example: Create a user registration flow with login, verification, and dashboard access"
|
||||||
className="w-full flex-1 p-5 rounded-2xl bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-slate-200 text-sm resize-none outline-none focus:border-blue-500/40 focus:ring-2 focus:ring-blue-500/20 transition-all font-sans leading-relaxed placeholder:text-slate-400 dark:placeholder:text-slate-600"
|
className="w-full flex-1 p-5 rounded-2xl bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-slate-200 text-sm resize-none outline-none focus:border-blue-500/40 focus:ring-2 focus:ring-blue-500/20 transition-all font-sans leading-relaxed placeholder:text-slate-400 dark:placeholder:text-slate-600"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import { Eye, Server, Database, Smartphone, Layers, BoxSelect } from 'lucide-react';
|
import { Eye, Server, Database, Smartphone, Layers, BoxSelect, Share2 } from 'lucide-react';
|
||||||
import { useFlowStore } from '../store';
|
import { useFlowStore } from '../store';
|
||||||
|
|
||||||
const filters = [
|
const filters = [
|
||||||
|
|
@ -8,25 +8,98 @@ const filters = [
|
||||||
{ id: 'filter-db', label: 'Database', icon: Database, color: '#10b981' },
|
{ id: 'filter-db', label: 'Database', icon: Database, color: '#10b981' },
|
||||||
{ id: 'filter-group', label: 'Groups', icon: BoxSelect, color: '#94a3b8' },
|
{ id: 'filter-group', label: 'Groups', icon: BoxSelect, color: '#94a3b8' },
|
||||||
{ id: 'filter-other', label: 'Flow / Other', icon: Layers, color: '#f59e0b' },
|
{ id: 'filter-other', label: 'Flow / Other', icon: Layers, color: '#f59e0b' },
|
||||||
|
{ id: 'filter-edge', label: 'Connections', icon: Share2, color: '#64748b' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function InteractiveLegend() {
|
export default function InteractiveLegend() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { focusMode, activeFilters, toggleFilter, nodes } = useFlowStore();
|
const { focusMode, activeFilters, toggleFilter, setActiveFilters, nodes, edges } = useFlowStore();
|
||||||
|
|
||||||
|
// EFFECT: Auto-register new dynamic categories (labels) into activeFilters so they show by default
|
||||||
|
const [knownDynamicLabels, setKnownDynamicLabels] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const dynamicFilters = useMemo(() => {
|
||||||
|
if (!nodes || !Array.isArray(nodes) || nodes.length === 0) return [];
|
||||||
|
|
||||||
|
const labels = new Set<string>();
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (node.type !== 'group' && node.data?.label) {
|
||||||
|
// Heuristic: If label looks like a type (AlphaNumeric, no spaces preferably, or short)
|
||||||
|
const label = String(node.data.label).trim();
|
||||||
|
if (label.length < 30) {
|
||||||
|
labels.add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedLabels = Array.from(labels).sort();
|
||||||
|
|
||||||
|
// Generate consistent colors for labels
|
||||||
|
const stringToColor = (str: string) => {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
const c = (hash & 0x00FFFFFF).toString(16).toUpperCase();
|
||||||
|
return '#' + '00000'.substring(0, 6 - c.length) + c;
|
||||||
|
};
|
||||||
|
|
||||||
|
return sortedLabels.map(label => ({
|
||||||
|
id: `dyn-${label}`,
|
||||||
|
label: label,
|
||||||
|
icon: BoxSelect,
|
||||||
|
color: stringToColor(label),
|
||||||
|
isDynamic: true
|
||||||
|
}));
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
|
// Sync effect to auto-enable new filters
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeFilters) return;
|
||||||
|
|
||||||
|
const newLabels = dynamicFilters.filter(f => !knownDynamicLabels.has(f.id));
|
||||||
|
if (newLabels.length > 0) {
|
||||||
|
const nextKnown = new Set(knownDynamicLabels);
|
||||||
|
const idsToAdd: string[] = [];
|
||||||
|
|
||||||
|
newLabels.forEach(f => {
|
||||||
|
nextKnown.add(f.id);
|
||||||
|
// Only add to active if NOT ALREADY THERE. Default to ON.
|
||||||
|
if (!activeFilters.includes(f.id)) {
|
||||||
|
idsToAdd.push(f.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (idsToAdd.length > 0) {
|
||||||
|
// Use a timeout to avoid render-cycle issues with Zustand updates inside useEffect
|
||||||
|
setTimeout(() => {
|
||||||
|
setActiveFilters([...activeFilters, ...idsToAdd]);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setKnownDynamicLabels(nextKnown);
|
||||||
|
}
|
||||||
|
}, [dynamicFilters, activeFilters, knownDynamicLabels, setActiveFilters]);
|
||||||
|
|
||||||
if (focusMode) return null;
|
if (focusMode) return null;
|
||||||
|
|
||||||
// Calculate available categories from current nodes
|
// Calculate available categories from current nodes
|
||||||
const availableCategories = new Set(nodes.map(node => node.data.category));
|
const availableCategories = new Set((nodes || []).map(node => node.data?.category));
|
||||||
|
|
||||||
// Always include 'filter-group' if there are groups, or maybe check node type
|
// Always include 'filter-group' if there are groups
|
||||||
if (nodes.some(n => n.type === 'group')) {
|
if ((nodes || []).some(n => n.type === 'group')) {
|
||||||
availableCategories.add('filter-group');
|
availableCategories.add('filter-group');
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleFilters = filters.filter(f => availableCategories.has(f.id));
|
// Always include 'filter-edge' if there are edges
|
||||||
|
if (edges && Array.isArray(edges) && edges.length > 0) {
|
||||||
|
availableCategories.add('filter-edge');
|
||||||
|
}
|
||||||
|
|
||||||
if (visibleFilters.length === 0) return null;
|
const visibleStaticFilters = filters.filter(f => availableCategories.has(f.id));
|
||||||
|
const allVisibleFilters = [...visibleStaticFilters, ...dynamicFilters];
|
||||||
|
|
||||||
|
if (allVisibleFilters.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-20 left-4 z-50">
|
<div className="absolute bottom-20 left-4 z-50">
|
||||||
|
|
@ -44,12 +117,12 @@ export default function InteractiveLegend() {
|
||||||
|
|
||||||
{/* Panel */}
|
{/* Panel */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute bottom-12 left-0 floating-glass border titanium-border rounded-xl p-3 min-w-[140px] shadow-xl animate-fade-in">
|
<div className="absolute bottom-12 left-0 floating-glass border titanium-border rounded-xl p-3 min-w-[140px] max-h-[60vh] overflow-y-auto shadow-xl animate-fade-in hide-scrollbar">
|
||||||
<div className="text-[10px] font-bold uppercase tracking-widest text-tertiary mb-3 border-b titanium-border pb-2">
|
<div className="text-[10px] font-bold uppercase tracking-widest text-tertiary mb-3 border-b titanium-border pb-2">
|
||||||
Legend Filters
|
Legend Filters
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{visibleFilters.map(f => {
|
{allVisibleFilters.map(f => {
|
||||||
const isActive = activeFilters.includes(f.id);
|
const isActive = activeFilters.includes(f.id);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -61,7 +134,7 @@ export default function InteractiveLegend() {
|
||||||
className="w-2.5 h-2.5 rounded-full shrink-0"
|
className="w-2.5 h-2.5 rounded-full shrink-0"
|
||||||
style={{ backgroundColor: f.color }}
|
style={{ backgroundColor: f.color }}
|
||||||
/>
|
/>
|
||||||
<span className="text-[11px] font-bold tracking-tight">{f.label}</span>
|
<span className="text-[11px] font-bold tracking-tight text-left truncate">{f.label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
168
src/components/MermaidStaticViewer.tsx
Normal file
168
src/components/MermaidStaticViewer.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { useEffect, useState, useRef } from 'react';
|
||||||
|
import mermaid from 'mermaid';
|
||||||
|
import { useFlowStore } from '../store';
|
||||||
|
import { ZoomIn, ZoomOut, Maximize, AlertCircle, MousePointer2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export function MermaidStaticViewer() {
|
||||||
|
const { mermaidCode, theme, setViewMode } = useFlowStore();
|
||||||
|
const [svgContent, setSvgContent] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const renderMermaid = async () => {
|
||||||
|
if (!mermaidCode) {
|
||||||
|
setSvgContent('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Configure mermaid based on theme
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: theme === 'dark' ? 'dark' : 'default',
|
||||||
|
securityLevel: 'loose',
|
||||||
|
fontFamily: 'Instrument Sans, sans-serif',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate unique ID for the SVG
|
||||||
|
const id = `mermaid-${Date.now()}`;
|
||||||
|
const { svg } = await mermaid.render(id, mermaidCode);
|
||||||
|
|
||||||
|
setSvgContent(svg);
|
||||||
|
setError(null);
|
||||||
|
// Reset view on new render
|
||||||
|
setScale(1);
|
||||||
|
setPosition({ x: 0, y: 0 });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Mermaid Render Error", err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to render Mermaid diagram');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderMermaid();
|
||||||
|
}, [mermaidCode, theme]);
|
||||||
|
|
||||||
|
const handleWheel = (e: React.WheelEvent) => {
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||||
|
setScale(s => Math.min(Math.max(0.1, s * delta), 8));
|
||||||
|
} else {
|
||||||
|
// Pan
|
||||||
|
setPosition(p => ({ x: p.x - e.deltaX, y: p.y - e.deltaY }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: React.MouseEvent) => {
|
||||||
|
if (isDragging) {
|
||||||
|
setPosition({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitView = () => {
|
||||||
|
setScale(1);
|
||||||
|
setPosition({ x: 0, y: 0 });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!mermaidCode) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col items-center justify-center text-slate-400 dark:text-slate-500 italic relative">
|
||||||
|
<div className="mb-4">No Mermaid code to render</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('interactive')}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 bg-blue-50 dark:bg-blue-500/10 hover:bg-blue-100 dark:hover:bg-blue-500/20 rounded-lg text-blue-600 dark:text-blue-400 transition-colors font-bold text-xs"
|
||||||
|
>
|
||||||
|
<MousePointer2 className="w-4 h-4" />
|
||||||
|
Back to Interactive
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col items-center justify-center text-red-500 p-8 text-center bg-slate-50 dark:bg-slate-900 relative">
|
||||||
|
<AlertCircle className="w-10 h-10 mb-4 opacity-50" />
|
||||||
|
<h3 className="font-bold mb-2">Rendering Error</h3>
|
||||||
|
<p className="text-xs font-mono bg-red-50 dark:bg-red-900/10 p-4 rounded-xl max-w-lg overflow-auto select-text mb-6">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('interactive')}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/5 rounded-lg text-slate-600 dark:text-slate-300 transition-colors font-bold text-xs"
|
||||||
|
>
|
||||||
|
<MousePointer2 className="w-4 h-4" />
|
||||||
|
Back to Interactive
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full bg-slate-50 dark:bg-[#0B1221] relative overflow-hidden flex flex-col select-none">
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="absolute top-4 right-4 z-50 flex flex-col gap-2">
|
||||||
|
<div className="flex bg-white/90 dark:bg-slate-800/90 backdrop-blur-sm shadow-xl rounded-xl border border-slate-200 dark:border-white/10 p-1">
|
||||||
|
<button onClick={() => setScale(s => Math.min(8, s * 1.2))} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg text-slate-500 dark:text-slate-400" title="Zoom In">
|
||||||
|
<ZoomIn className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setScale(s => Math.max(0.1, s / 1.2))} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg text-slate-500 dark:text-slate-400" title="Zoom Out">
|
||||||
|
<ZoomOut className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button onClick={fitView} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg text-slate-500 dark:text-slate-400" title="Fit View">
|
||||||
|
<Maximize className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-slate-200 dark:bg-white/10 mx-1" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('interactive')}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 hover:bg-blue-50 dark:hover:bg-blue-500/10 rounded-lg text-blue-600 dark:text-blue-400 transition-colors font-bold text-xs uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
<MousePointer2 className="w-4 h-4" />
|
||||||
|
Interactive
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Viewport */}
|
||||||
|
<div
|
||||||
|
className="w-full h-full cursor-move flex items-center justify-center overflow-hidden"
|
||||||
|
onWheel={handleWheel}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseLeave={handleMouseUp}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||||||
|
transition: isDragging ? 'none' : 'transform 0.1s ease-out',
|
||||||
|
transformOrigin: 'center',
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: svgContent }}
|
||||||
|
className="mermaid-svg-container"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-4 right-4 pointer-events-none opacity-50 text-[10px] text-slate-400 font-mono">
|
||||||
|
Static View • {Math.round(scale * 100)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
src/components/editor/ComfyImportPanel.tsx
Normal file
116
src/components/editor/ComfyImportPanel.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { workflowToMermaid } from '../../lib/comfy2mermaid';
|
||||||
|
import { Upload, AlertCircle, RefreshCw } from 'lucide-react';
|
||||||
|
import { useFlowStore } from '../../store';
|
||||||
|
import { parseMermaid } from '../../lib/mermaidParser';
|
||||||
|
import { getLayoutedElements } from '../../lib/layoutEngine';
|
||||||
|
|
||||||
|
export function ComfyImportPanel() {
|
||||||
|
const {
|
||||||
|
setMermaidCode,
|
||||||
|
setSourceCode,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
setInputActiveTab,
|
||||||
|
saveDiagram
|
||||||
|
} = useFlowStore();
|
||||||
|
|
||||||
|
const [jsonInput, setJsonInput] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const text = e.target?.result as string;
|
||||||
|
setJsonInput(text);
|
||||||
|
convert(text);
|
||||||
|
} catch (err) {
|
||||||
|
setError("Failed to read file");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const convert = async (input: string) => {
|
||||||
|
setError(null);
|
||||||
|
if (!input.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workflow = JSON.parse(input);
|
||||||
|
const code = workflowToMermaid(workflow);
|
||||||
|
|
||||||
|
// Update Store
|
||||||
|
setMermaidCode(code);
|
||||||
|
setSourceCode(code);
|
||||||
|
|
||||||
|
// Process Diagram for visualization
|
||||||
|
const { nodes: parsedNodes, edges: parsedEdges } = await parseMermaid(code);
|
||||||
|
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(parsedNodes, parsedEdges);
|
||||||
|
|
||||||
|
setNodes(layoutedNodes);
|
||||||
|
setEdges(layoutedEdges);
|
||||||
|
|
||||||
|
// Auto-save
|
||||||
|
saveDiagram(`ComfyUI Import ${new Date().toLocaleTimeString()}`);
|
||||||
|
|
||||||
|
// Switch back to Code tab to see result
|
||||||
|
setInputActiveTab('code');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("Invalid JSON or conversion error: " + (err instanceof Error ? err.message : String(err)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full animate-slide-up">
|
||||||
|
<div className="p-4 mb-4 bg-slate-50 dark:bg-black/20 rounded-xl border border-dashed border-slate-300 dark:border-white/10">
|
||||||
|
<label className="flex flex-col items-center justify-center w-full h-32 cursor-pointer hover:bg-slate-100 dark:hover:bg-white/5 transition rounded-lg group">
|
||||||
|
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
|
<Upload className="w-8 h-8 mb-3 text-slate-400 group-hover:text-blue-500 transition-colors" />
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
<span className="font-semibold text-blue-600 dark:text-blue-400">Click to upload</span> or drag workflow.json
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input type="file" className="hidden" accept=".json" onChange={handleFileUpload} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col gap-2">
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 pl-1">
|
||||||
|
Or Paste JSON
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="flex-1 w-full p-4 rounded-xl bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 text-xs font-mono text-slate-600 dark:text-slate-300 resize-none outline-none focus:border-blue-500/40 focus:ring-2 focus:ring-blue-500/20 transition-all placeholder:text-slate-400"
|
||||||
|
value={jsonInput}
|
||||||
|
onChange={(e) => setJsonInput(e.target.value)}
|
||||||
|
placeholder='{ "nodes": [], "links": [] }'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-300 p-3 rounded-lg flex items-center gap-2 text-xs">
|
||||||
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => convert(jsonInput)}
|
||||||
|
disabled={!jsonInput.trim()}
|
||||||
|
className={`mt-4 w-full py-3.5 rounded-xl font-bold text-sm tracking-wide transition-all duration-300 flex items-center justify-center gap-2
|
||||||
|
${!jsonInput.trim()
|
||||||
|
? 'bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 cursor-not-allowed'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-500 text-white shadow-lg shadow-blue-600/20 active:scale-[0.98]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
Convert & Visualize
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
src/lib/comfy2mermaid.test.ts
Normal file
50
src/lib/comfy2mermaid.test.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { workflowToMermaid } from './comfy2mermaid';
|
||||||
|
|
||||||
|
describe('comfy2mermaid', () => {
|
||||||
|
it('converts a simple workflow', () => {
|
||||||
|
const workflow = {
|
||||||
|
last_node_id: 2,
|
||||||
|
last_link_id: 1,
|
||||||
|
nodes: [
|
||||||
|
{ id: 1, type: "LoadImage", title: "Load Image" },
|
||||||
|
{ id: 2, type: "PreviewImage", title: "Preview Image" }
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
[1, 1, 0, 2, 0, "IMAGE"]
|
||||||
|
],
|
||||||
|
version: 0.4
|
||||||
|
};
|
||||||
|
|
||||||
|
const mermaidCode = workflowToMermaid(workflow as any);
|
||||||
|
|
||||||
|
expect(mermaidCode).toContain('graph TD');
|
||||||
|
expect(mermaidCode).toContain('N1["Load Image"]');
|
||||||
|
expect(mermaidCode).toContain('N2["Preview Image"]');
|
||||||
|
expect(mermaidCode).toContain('N1 -- IMAGE --> N2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles groups', () => {
|
||||||
|
const workflow = {
|
||||||
|
last_node_id: 2,
|
||||||
|
last_link_id: 0,
|
||||||
|
nodes: [
|
||||||
|
{ id: 1, type: "A", pos: [100, 100], size: [50, 50] }, // Inside group
|
||||||
|
{ id: 2, type: "B", pos: [500, 500], size: [50, 50] } // Outside group
|
||||||
|
],
|
||||||
|
links: [],
|
||||||
|
groups: [
|
||||||
|
{ title: "My Group", bounding: [0, 0, 200, 200] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const mermaidCode = workflowToMermaid(workflow as any);
|
||||||
|
expect(mermaidCode).toContain('subgraph G0 ["My Group"]');
|
||||||
|
expect(mermaidCode).toContain('N1');
|
||||||
|
// Should not contain N2 inside the subgraph block, but regex is hard.
|
||||||
|
// Logic check:
|
||||||
|
// subgraph G0 ["My Group"]
|
||||||
|
// N1
|
||||||
|
// end
|
||||||
|
});
|
||||||
|
});
|
||||||
276
src/lib/comfy2mermaid.ts
Normal file
276
src/lib/comfy2mermaid.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
import mermaid from 'mermaid';
|
||||||
|
|
||||||
|
// --- Types ---
|
||||||
|
export interface ComfyNode {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
pos?: [number, number];
|
||||||
|
size?: { "0": number; "1": number } | [number, number];
|
||||||
|
flags?: Record<string, any>;
|
||||||
|
order?: number;
|
||||||
|
mode?: number;
|
||||||
|
inputs?: any[];
|
||||||
|
outputs?: any[];
|
||||||
|
properties?: Record<string, any>;
|
||||||
|
widgets_values?: any[];
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComfyLink {
|
||||||
|
// [id, origin_id, origin_slot, target_id, target_slot, type]
|
||||||
|
0: number; // link_id
|
||||||
|
1: number; // origin_id
|
||||||
|
2: number; // origin_slot
|
||||||
|
3: number; // target_id
|
||||||
|
4: number; // target_slot
|
||||||
|
5: string; // type
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComfyGroup {
|
||||||
|
title: string;
|
||||||
|
bounding: [number, number, number, number]; // [x, y, w, h]
|
||||||
|
color?: string;
|
||||||
|
font_size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComfyWorkflow {
|
||||||
|
last_node_id: number;
|
||||||
|
last_link_id: number;
|
||||||
|
nodes: ComfyNode[];
|
||||||
|
links: ComfyLink[];
|
||||||
|
groups?: ComfyGroup[];
|
||||||
|
version?: number;
|
||||||
|
extra?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MermaidConfig {
|
||||||
|
Default_Graph_Direction?: string;
|
||||||
|
Default_Connector?: string;
|
||||||
|
Default_Node_Style?: string;
|
||||||
|
Generate_ComfyUI_Subgraphs?: boolean;
|
||||||
|
Style_Definitions?: Record<string, string>;
|
||||||
|
Node_Group?: Array<{ group_name: string; nodes: string[] }>;
|
||||||
|
Default_Node_Shape?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Constants ---
|
||||||
|
const CONTRAST_THRESHOLD = 4.5;
|
||||||
|
const DEFAULT_SHAPE_SYNTAX: [string, string] = ['[', ']'];
|
||||||
|
|
||||||
|
const MERMAID_SHAPE_SYNTAX: Record<string, [string, string]> = {
|
||||||
|
rectangle: ['[', ']'],
|
||||||
|
round: ['(', ')'],
|
||||||
|
stadium: ['([', '])'],
|
||||||
|
subroutine: ['[[', ']]'],
|
||||||
|
cylinder: ['[(', ')]'],
|
||||||
|
circle: ['((', '))'],
|
||||||
|
rhombus: ['{', '}'],
|
||||||
|
diamond: ['{', '}'],
|
||||||
|
hexagon: ['{{', '}}'],
|
||||||
|
parallelogram: ['[/', '/]'],
|
||||||
|
parallelogram_alt: ['[\\', '\\]'],
|
||||||
|
trapezoid: ['[/', '\\]'],
|
||||||
|
trapezoid_alt: ['[\\', '/]'],
|
||||||
|
double_circle: ['(((', ')))'],
|
||||||
|
database: ['[(', ')]'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const LINK_LABEL_FORMATS: Record<string, string> = {
|
||||||
|
"-->": "-- {} -->", "---": "-- {} ---", "-.->": "-. {} .->", "-.-": "-. {} .-",
|
||||||
|
"==>": "== {} ==>", "===": "== {} ===", "--o": "-- {} --o", "o--": "o-- {} --",
|
||||||
|
"o--o": "o-- {} --o", "--x": "-- {} --x", "x--": "x-- {} --", "x--x": "x-- {} --x",
|
||||||
|
"<-->": "<-- {} -->", "<-.->": "<-. {} .->", "<==>": "<== {} ==>",
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
function getMermaidShapeSyntax(shapeName: string): [string, string] {
|
||||||
|
return MERMAID_SHAPE_SYNTAX[shapeName.toLowerCase()] || DEFAULT_SHAPE_SYNTAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseColor(colorString: string): [number, number, number] | null {
|
||||||
|
if (!colorString) return null;
|
||||||
|
const str = colorString.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (str.startsWith('#')) {
|
||||||
|
const hex = str.slice(1);
|
||||||
|
if (hex.length === 3) {
|
||||||
|
const r = parseInt(hex[0] + hex[0], 16);
|
||||||
|
const g = parseInt(hex[1] + hex[1], 16);
|
||||||
|
const b = parseInt(hex[2] + hex[2], 16);
|
||||||
|
if (!isNaN(r)) return [r, g, b];
|
||||||
|
} else if (hex.length === 6) {
|
||||||
|
const r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
const g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
const b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
if (!isNaN(r)) return [r, g, b];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic colors fallback
|
||||||
|
const basicColors: Record<string, [number, number, number]> = {
|
||||||
|
"white": [255, 255, 255], "black": [0, 0, 0], "red": [255, 0, 0],
|
||||||
|
"green": [0, 128, 0], "blue": [0, 0, 255], "yellow": [255, 255, 0],
|
||||||
|
"cyan": [0, 255, 255], "magenta": [255, 0, 255],
|
||||||
|
"gray": [128, 128, 128], "grey": [128, 128, 128]
|
||||||
|
};
|
||||||
|
|
||||||
|
return basicColors[str] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRelativeLuminance(rgb: [number, number, number]): number {
|
||||||
|
const [r, g, b] = rgb.map(v => {
|
||||||
|
const val = v / 255.0;
|
||||||
|
return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
|
||||||
|
});
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateContrastRatio(rgb1: [number, number, number], rgb2: [number, number, number]): number {
|
||||||
|
const lum1 = getRelativeLuminance(rgb1);
|
||||||
|
const lum2 = getRelativeLuminance(rgb2);
|
||||||
|
return (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustTextColorForBackground(styleString: string): string {
|
||||||
|
if (!styleString) return styleString;
|
||||||
|
const parts = styleString.split(',').map(p => p.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
// Check if color is already explicitly set
|
||||||
|
if (parts.some(p => p.toLowerCase().startsWith('color') || p.toLowerCase().startsWith('stroke'))) {
|
||||||
|
// simplified check, effectively what python regex did
|
||||||
|
}
|
||||||
|
|
||||||
|
let fillColorValue = null;
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.toLowerCase().startsWith('fill:')) {
|
||||||
|
fillColorValue = part.split(':', 2)[1]?.trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fillColorValue) return styleString;
|
||||||
|
const bgRgb = parseColor(fillColorValue);
|
||||||
|
if (!bgRgb) return styleString;
|
||||||
|
|
||||||
|
// Assuming default text color is white for dark mode (implied by python script)
|
||||||
|
const contrast = calculateContrastRatio(bgRgb, [255, 255, 255]);
|
||||||
|
if (contrast < CONTRAST_THRESHOLD) {
|
||||||
|
return styleString + ",color:#000";
|
||||||
|
}
|
||||||
|
return styleString;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Main Conversion Logic ---
|
||||||
|
|
||||||
|
export function workflowToMermaid(workflow: ComfyWorkflow, config: MermaidConfig = {}): string {
|
||||||
|
// Config defaults
|
||||||
|
const graphDirection = config.Default_Graph_Direction || 'LR';
|
||||||
|
const defaultConnector = config.Default_Connector || '-->';
|
||||||
|
const defaultNodeShape = config.Default_Node_Shape || 'rectangle';
|
||||||
|
|
||||||
|
// State maps
|
||||||
|
const nodeIdToDisplayLabel: Record<number, string> = {};
|
||||||
|
const nodeIdToType: Record<number, string> = {};
|
||||||
|
const nodesInGroups = new Set<number>();
|
||||||
|
|
||||||
|
// Pre-process nodes
|
||||||
|
workflow.nodes.forEach(node => {
|
||||||
|
const title = node.title || node.type || 'Unknown';
|
||||||
|
nodeIdToDisplayLabel[node.id] = title;
|
||||||
|
nodeIdToType[node.id] = node.type;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mermaidLines: string[] = [];
|
||||||
|
mermaidLines.push(`graph ${graphDirection}`);
|
||||||
|
|
||||||
|
// 1. Process Groups (Subgraphs)
|
||||||
|
if (workflow.groups) {
|
||||||
|
workflow.groups.forEach((group, index) => {
|
||||||
|
const [gx, gy, gw, gh] = group.bounding;
|
||||||
|
const groupNodes: ComfyNode[] = [];
|
||||||
|
|
||||||
|
workflow.nodes.forEach(node => {
|
||||||
|
if (!node.pos) return;
|
||||||
|
const [nx, ny] = node.pos;
|
||||||
|
// Determine node center (approximate size if missing)
|
||||||
|
const width = (node.size && (Array.isArray(node.size) ? node.size[0] : node.size["0"])) || 100;
|
||||||
|
const height = (node.size && (Array.isArray(node.size) ? node.size[1] : node.size["1"])) || 50;
|
||||||
|
const cx = nx + width / 2;
|
||||||
|
const cy = ny + height / 2;
|
||||||
|
|
||||||
|
// Check inclusion
|
||||||
|
if (cx >= gx && cx <= gx + gw && cy >= gy && cy <= gy + gh) {
|
||||||
|
groupNodes.push(node);
|
||||||
|
nodesInGroups.add(node.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (groupNodes.length > 0) {
|
||||||
|
const groupId = `G${index}`;
|
||||||
|
const sanitizedTitle = group.title.replace(/"/g, '#quot;');
|
||||||
|
|
||||||
|
mermaidLines.push(` subgraph ${groupId} ["${sanitizedTitle}"]`);
|
||||||
|
mermaidLines.push(` direction ${graphDirection}`); // Groups usually flow same direction
|
||||||
|
|
||||||
|
groupNodes.forEach(node => {
|
||||||
|
const nodeId = `N${node.id}`;
|
||||||
|
const label = nodeIdToDisplayLabel[node.id].replace(/"/g, '#quot;');
|
||||||
|
const [open, close] = getMermaidShapeSyntax(defaultNodeShape);
|
||||||
|
mermaidLines.push(` ${nodeId}${open}"${label}"${close}`);
|
||||||
|
// Force basic class
|
||||||
|
mermaidLines.push(` class ${nodeId} defaultNode`);
|
||||||
|
});
|
||||||
|
|
||||||
|
mermaidLines.push(` end`);
|
||||||
|
|
||||||
|
// Apply Group Style
|
||||||
|
if (group.color) {
|
||||||
|
let hex = group.color.toLowerCase();
|
||||||
|
mermaidLines.push(` style ${groupId} fill:${hex},stroke:${hex},color:#fff,stroke-width:2px,fill-opacity:0.6`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Process Ungrouped Nodes
|
||||||
|
workflow.nodes.forEach(node => {
|
||||||
|
if (!nodesInGroups.has(node.id)) {
|
||||||
|
const nodeId = `N${node.id}`;
|
||||||
|
const label = nodeIdToDisplayLabel[node.id].replace(/"/g, '#quot;');
|
||||||
|
const [open, close] = getMermaidShapeSyntax(defaultNodeShape);
|
||||||
|
mermaidLines.push(` ${nodeId}${open}"${label}"${close}`);
|
||||||
|
mermaidLines.push(` class ${nodeId} defaultNode`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Process Links
|
||||||
|
mermaidLines.push(" %% Connections");
|
||||||
|
workflow.links.forEach((link) => {
|
||||||
|
const startNodeId = `N${link[1]}`;
|
||||||
|
const endNodeId = `N${link[3]}`;
|
||||||
|
const connector = defaultConnector;
|
||||||
|
const type = String(link[5] || '').toUpperCase();
|
||||||
|
|
||||||
|
// Optional: filter out boring link types if needed
|
||||||
|
|
||||||
|
let linkText = ` ${startNodeId} ${connector} ${endNodeId}`;
|
||||||
|
if (type && config.Add_Link_Labels !== false) {
|
||||||
|
const format = LINK_LABEL_FORMATS[connector] || "-- {} -->";
|
||||||
|
const label = type.replace(/"/g, '#quot;');
|
||||||
|
const connectorWithLabel = format.replace('{}', label);
|
||||||
|
linkText = ` ${startNodeId} ${connectorWithLabel} ${endNodeId}`;
|
||||||
|
}
|
||||||
|
mermaidLines.push(linkText);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Global Styles
|
||||||
|
// Dark theme aesthetic: Dark blocks, white text, subtle edges
|
||||||
|
mermaidLines.push(" classDef defaultNode fill:#353535,stroke:#555,stroke-width:1px,color:#eee,rx:5,ry:5;");
|
||||||
|
mermaidLines.push(" classDef default fill:#353535,stroke:#555,stroke-width:1px,color:#eee;");
|
||||||
|
mermaidLines.push(" linkStyle default stroke:#777,stroke-width:1px,fill:none;");
|
||||||
|
|
||||||
|
return mermaidLines.join('\n');
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ReactFlowProvider } from '@xyflow/react';
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
import { InputPanel } from '../components/InputPanel';
|
import { InputPanel } from '../components/InputPanel';
|
||||||
import { FlowCanvas } from '../components/FlowCanvas';
|
import { FlowCanvas } from '../components/FlowCanvas';
|
||||||
|
import { MermaidStaticViewer } from '../components/MermaidStaticViewer';
|
||||||
import { NodeDetailsPanel } from '../components/NodeDetailsPanel';
|
import { NodeDetailsPanel } from '../components/NodeDetailsPanel';
|
||||||
import InteractiveLegend from '../components/InteractiveLegend';
|
import InteractiveLegend from '../components/InteractiveLegend';
|
||||||
import OnboardingTour from '../components/OnboardingTour';
|
import OnboardingTour from '../components/OnboardingTour';
|
||||||
|
|
@ -17,7 +18,7 @@ export function Editor() {
|
||||||
// Enable Programmatic API
|
// Enable Programmatic API
|
||||||
useDiagramAPI();
|
useDiagramAPI();
|
||||||
|
|
||||||
const { nodes, isLoading, leftPanelOpen, setLeftPanelOpen, rightPanelOpen, setRightPanelOpen, focusMode, setFocusMode, mobileEditorOpen, setMobileEditorOpen } = useFlowStore();
|
const { nodes, isLoading, leftPanelOpen, setLeftPanelOpen, rightPanelOpen, setRightPanelOpen, focusMode, setFocusMode, mobileEditorOpen, setMobileEditorOpen, viewMode } = useFlowStore();
|
||||||
const { isMobile } = useMobileDetect();
|
const { isMobile } = useMobileDetect();
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(384); // Default w-96
|
const [sidebarWidth, setSidebarWidth] = useState(384); // Default w-96
|
||||||
const isResizing = useRef(false);
|
const isResizing = useRef(false);
|
||||||
|
|
@ -130,7 +131,11 @@ export function Editor() {
|
||||||
|
|
||||||
{/* Panoramic Canvas */}
|
{/* Panoramic Canvas */}
|
||||||
<main className="flex-1 relative overflow-hidden">
|
<main className="flex-1 relative overflow-hidden">
|
||||||
|
{viewMode === 'static' ? (
|
||||||
|
<MermaidStaticViewer />
|
||||||
|
) : (
|
||||||
<FlowCanvas />
|
<FlowCanvas />
|
||||||
|
)}
|
||||||
|
|
||||||
<InteractiveLegend />
|
<InteractiveLegend />
|
||||||
<OnboardingTour />
|
<OnboardingTour />
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,10 @@ export function useFlowStore() {
|
||||||
setLoading: ui.setLoading,
|
setLoading: ui.setLoading,
|
||||||
setError: ui.setError,
|
setError: ui.setError,
|
||||||
|
|
||||||
|
// View Mode
|
||||||
|
viewMode: ui.viewMode,
|
||||||
|
setViewMode: ui.setViewMode,
|
||||||
|
|
||||||
// Combined reset
|
// Combined reset
|
||||||
reset: () => {
|
reset: () => {
|
||||||
diagram.reset();
|
diagram.reset();
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,13 @@ interface UIState {
|
||||||
setLoading: (loading: boolean) => void;
|
setLoading: (loading: boolean) => void;
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
|
|
||||||
|
// View Mode
|
||||||
|
viewMode: 'interactive' | 'static';
|
||||||
|
setViewMode: (mode: 'interactive' | 'static') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_FILTERS = ['filter-client', 'filter-server', 'filter-db', 'filter-other', 'filter-group'];
|
const DEFAULT_FILTERS = ['filter-client', 'filter-server', 'filter-db', 'filter-other', 'filter-group', 'filter-edge'];
|
||||||
|
|
||||||
export const useUIStore = create<UIState>()((set, get) => ({
|
export const useUIStore = create<UIState>()((set, get) => ({
|
||||||
// Initial state
|
// Initial state
|
||||||
|
|
@ -108,4 +112,8 @@ export const useUIStore = create<UIState>()((set, get) => ({
|
||||||
setLoading: (isLoading) => set({ isLoading }),
|
setLoading: (isLoading) => set({ isLoading }),
|
||||||
setError: (error) => set({ error }),
|
setError: (error) => set({ error }),
|
||||||
clearError: () => set({ error: null }),
|
clearError: () => set({ error: null }),
|
||||||
|
|
||||||
|
// View Mode actions
|
||||||
|
viewMode: 'interactive',
|
||||||
|
setViewMode: (viewMode) => set({ viewMode }),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue