Merge branch 'main' into sno-fix-playwright-babel

This commit is contained in:
snomiao
2025-09-03 15:12:14 +00:00
132 changed files with 12397 additions and 3957 deletions

View File

@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
- name: Install pnpm

View File

@@ -121,4 +121,4 @@ jobs:
labels: Manager
delete-branch: true
add-paths: |
src/types/generatedManagerTypes.ts
src/types/generatedManagerTypes.ts

View File

@@ -59,18 +59,6 @@ test.describe('Execution error', () => {
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
await comfyPage.page.getByLabel('Help Fix This').click()
const issueReportForm = comfyPage.page.getByText(
'Submit Error Report (Optional)'
)
await expect(issueReportForm).toBeVisible()
})
})
test.describe('Missing models warning', () => {
@@ -303,37 +291,16 @@ test.describe('Settings', () => {
})
})
test.describe('Feedback dialog', () => {
test('Should open from topmenu help command', async ({ comfyPage }) => {
// Open feedback dialog from top menu
test.describe('Support', () => {
test('Should open external zendesk link', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const newPage = await pagePromise
// Verify feedback dialog content is visible
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
await expect(feedbackHeader).toBeVisible()
})
test('Should close when close button clicked', async ({ comfyPage }) => {
// Open feedback dialog
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
// Close feedback dialog
await comfyPage.page
.getByLabel('', { exact: true })
.getByLabel('Close')
.click()
await feedbackHeader.waitFor({ state: 'hidden' })
// Verify dialog is closed
await expect(feedbackHeader).not.toBeVisible()
await newPage.waitForLoadState('networkidle')
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
await newPage.close()
})
})

View File

@@ -15,8 +15,10 @@ test.describe('Load Workflow in Media', () => {
'workflow.mp4',
'workflow.mov',
'workflow.m4v',
'workflow.svg',
'workflow.avif'
'workflow.svg'
// TODO: Re-enable after fixing test asset to use core nodes only
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
// 'workflow.avif'
]
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -12,6 +12,7 @@ const config: KnipConfig = {
'playwright.config.ts',
'playwright.i18n.config.ts',
'vitest.config.ts',
'vitest.litegraph.config.ts',
'scripts/**/*.{js,ts}'
],
project: [
@@ -32,6 +33,8 @@ const config: KnipConfig = {
'coverage/**',
// i18n config
'.i18nrc.cjs',
// Vitest litegraph config
'vitest.litegraph.config.ts',
// Test setup files
'browser_tests/globalSetup.ts',
'browser_tests/globalTeardown.ts',

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.26.7",
"version": "1.26.8",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -21,6 +21,7 @@
"test:browser": "npx nx e2e",
"test:unit": "nx run test tests-ui/tests",
"test:component": "nx run test src/components/",
"test:litegraph": "vitest run --config vitest.litegraph.config.ts",
"preinstall": "npx only-allow pnpm",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
@@ -58,6 +59,7 @@
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/dompurify": "^3.0.5",
"@types/fs-extra": "^11.0.4",
"@types/jsdom": "^21.1.7",
"@types/node": "^20.14.8",
"@types/semver": "^7.7.0",
"@types/three": "^0.169.0",
@@ -80,6 +82,7 @@
"identity-obj-proxy": "^3.0.0",
"ink": "^6.2.2",
"jiti": "2.4.2",
"jsdom": "^26.1.0",
"knip": "^5.62.0",
"lint-staged": "^15.2.7",
"lucide-vue-next": "^0.540.0",

300
pnpm-lock.yaml generated
View File

@@ -219,6 +219,9 @@ importers:
'@types/fs-extra':
specifier: ^11.0.4
version: 11.0.4
'@types/jsdom':
specifier: ^21.1.7
version: 21.1.7
'@types/node':
specifier: ^20.14.8
version: 20.14.10
@@ -285,6 +288,9 @@ importers:
jiti:
specifier: 2.4.2
version: 2.4.2
jsdom:
specifier: ^26.1.0
version: 26.1.0
knip:
specifier: ^5.62.0
version: 5.62.0(@types/node@20.14.10)(typescript@5.9.2)
@@ -347,7 +353,7 @@ importers:
version: 7.7.6(rollup@4.22.4)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(terser@5.39.2)
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(terser@5.39.2)
vue-tsc:
specifier: ^2.1.10
version: 2.1.10(typescript@5.9.2)
@@ -486,6 +492,9 @@ packages:
'@anthropic-ai/sdk@0.8.1':
resolution: {integrity: sha512-59etePenCizVx1O8Qhi1T1ruE04ISfNzCnyhZNcsss1QljsLmYS83jttarMNEvGYcsUF7rwxw2lzcC3Zbxao7g==}
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
'@atlaskit/pragmatic-drag-and-drop@1.3.1':
resolution: {integrity: sha512-MptcLppK78B2eplL5fHk93kfCbZ6uCpt33YauBPrOwI5zcHYJhZGeaGEaAXoVAHnSJOdQUhy6kGVVC9qggz2Fg==}
@@ -1061,6 +1070,34 @@ packages:
'@comfyorg/comfyui-electron-types@0.4.43':
resolution: {integrity: sha512-o6WFbYn9yAkGbkOwvhPF7pbKDvN0occZ21Tfyhya8CIsIqKpTHLft0aOqo4yhSh+kTxN16FYjsfrTH5Olk4WuA==}
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
'@csstools/css-calc@2.1.4':
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-color-parser@3.1.0':
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-parser-algorithms@3.0.5':
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@emnapi/core@1.4.5':
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
@@ -2519,6 +2556,9 @@ packages:
'@types/fs-extra@11.0.4':
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
'@types/jsdom@21.1.7':
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -2576,6 +2616,9 @@ packages:
'@types/three@0.169.0':
resolution: {integrity: sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -2891,6 +2934,10 @@ packages:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
engines: {node: '>= 10.0.0'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
agentkeepalive@4.6.0:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'}
@@ -3448,9 +3495,17 @@ packages:
engines: {node: '>=4'}
hasBin: true
cssstyle@4.6.0:
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
engines: {node: '>=18'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
@@ -3476,6 +3531,9 @@ packages:
supports-color:
optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
@@ -3658,6 +3716,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -4235,6 +4297,10 @@ packages:
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
engines: {node: '>=12'}
html-encoding-sniffer@4.0.0:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
html-minifier-terser@6.1.0:
resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==}
engines: {node: '>=12'}
@@ -4247,6 +4313,10 @@ packages:
http-parser-js@0.5.10:
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
http-proxy@1.18.1:
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
engines: {node: '>=8.0.0'}
@@ -4256,6 +4326,10 @@ packages:
engines: {node: '>=12'}
hasBin: true
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
human-signals@5.0.0:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
@@ -4445,6 +4519,9 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
is-promise@2.2.2:
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
@@ -4549,6 +4626,15 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
jsdom@26.1.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
engines: {node: '>=18'}
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
jsesc@3.0.2:
resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
engines: {node: '>=6'}
@@ -4741,6 +4827,9 @@ packages:
resolution: {integrity: sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==}
engines: {node: 14 || >=16.14}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.1.0:
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
engines: {node: 20 || >=22}
@@ -5134,6 +5223,9 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
nwsapi@2.2.21:
resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==}
nx@21.4.1:
resolution: {integrity: sha512-nD8NjJGYk5wcqiATzlsLauvyrSHV2S2YmM2HBIKqTTwVP2sey07MF3wDB9U2BwxIjboahiITQ6pfqFgB79TF2A==}
hasBin: true
@@ -5265,6 +5357,9 @@ packages:
parse5@7.1.2:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -5787,6 +5882,9 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
run-applescript@7.0.0:
resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==}
engines: {node: '>=18'}
@@ -5806,6 +5904,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
@@ -6053,6 +6155,9 @@ packages:
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
synckit@0.11.3:
resolution: {integrity: sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -6125,6 +6230,13 @@ packages:
peerDependencies:
'@tiptap/core': ^2.0.3
tldts-core@6.1.86:
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
tldts@6.1.86:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true
tmp@0.2.5:
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
engines: {node: '>=14.14'}
@@ -6144,9 +6256,17 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tough-cookie@5.1.2:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
@@ -6593,6 +6713,10 @@ packages:
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
walk-up-path@4.0.0:
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
engines: {node: 20 || >=22}
@@ -6633,10 +6757,22 @@ packages:
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
engines: {node: '>=12'}
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -6715,6 +6851,13 @@ packages:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -6971,6 +7114,14 @@ snapshots:
transitivePeerDependencies:
- encoding
'@asamuzakjp/css-color@3.2.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
lru-cache: 10.4.3
'@atlaskit/pragmatic-drag-and-drop@1.3.1':
dependencies:
'@babel/runtime': 7.27.6
@@ -7736,6 +7887,26 @@ snapshots:
'@comfyorg/comfyui-electron-types@0.4.43': {}
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/color-helpers': 5.1.0
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-tokenizer@3.0.4': {}
'@emnapi/core@1.4.5':
dependencies:
'@emnapi/wasi-threads': 1.0.4
@@ -8732,7 +8903,7 @@ snapshots:
tsconfig-paths: 4.2.0
tslib: 2.8.1
vite: 5.4.19(@types/node@20.14.10)(terser@5.39.2)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(terser@5.39.2)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(terser@5.39.2)
transitivePeerDependencies:
- '@babel/traverse'
- '@swc-node/register'
@@ -9386,6 +9557,12 @@ snapshots:
'@types/jsonfile': 6.1.4
'@types/node': 20.14.10
'@types/jsdom@21.1.7':
dependencies:
'@types/node': 20.14.10
'@types/tough-cookie': 4.0.5
parse5: 7.1.2
'@types/json-schema@7.0.15': {}
'@types/jsonfile@6.1.4':
@@ -9450,6 +9627,8 @@ snapshots:
fflate: 0.8.2
meshoptimizer: 0.18.1
'@types/tough-cookie@4.0.5': {}
'@types/trusted-types@2.0.7': {}
'@types/unist@3.0.3': {}
@@ -9641,7 +9820,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(terser@5.39.2)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(terser@5.39.2)
'@vitest/utils@3.2.4':
dependencies:
@@ -9886,6 +10065,8 @@ snapshots:
address@1.2.2: {}
agent-base@7.1.4: {}
agentkeepalive@4.6.0:
dependencies:
humanize-ms: 1.2.1
@@ -10467,8 +10648,18 @@ snapshots:
cssesc@3.0.0: {}
cssstyle@4.6.0:
dependencies:
'@asamuzakjp/css-color': 3.2.0
rrweb-cssom: 0.8.0
csstype@3.1.3: {}
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
de-indent@1.0.2: {}
debounce-fn@6.0.0:
@@ -10483,6 +10674,8 @@ snapshots:
dependencies:
ms: 2.1.3
decimal.js@10.6.0: {}
decode-named-character-reference@1.2.0:
dependencies:
character-entities: 2.0.2
@@ -10646,6 +10839,8 @@ snapshots:
entities@4.5.0: {}
entities@6.0.1: {}
env-paths@2.2.1: {}
env-paths@3.0.0: {}
@@ -11347,6 +11542,10 @@ snapshots:
dependencies:
whatwg-encoding: 2.0.0
html-encoding-sniffer@4.0.0:
dependencies:
whatwg-encoding: 3.1.1
html-minifier-terser@6.1.0:
dependencies:
camel-case: 4.1.2
@@ -11367,6 +11566,13 @@ snapshots:
http-parser-js@0.5.10: {}
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
debug: 4.4.1
transitivePeerDependencies:
- supports-color
http-proxy@1.18.1:
dependencies:
eventemitter3: 4.0.7
@@ -11394,6 +11600,13 @@ snapshots:
- debug
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.1
transitivePeerDependencies:
- supports-color
human-signals@5.0.0: {}
human-signals@8.0.1: {}
@@ -11549,6 +11762,8 @@ snapshots:
is-plain-obj@4.1.0: {}
is-potential-custom-element-name@1.0.1: {}
is-promise@2.2.2: {}
is-promise@4.0.0: {}
@@ -11641,6 +11856,33 @@ snapshots:
dependencies:
argparse: 2.0.1
jsdom@26.1.0:
dependencies:
cssstyle: 4.6.0
data-urls: 5.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.21
parse5: 7.3.0
rrweb-cssom: 0.8.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 5.1.2
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
ws: 8.18.3
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
jsesc@3.0.2: {}
jsesc@3.1.0: {}
@@ -11843,6 +12085,8 @@ snapshots:
lru-cache@10.3.0: {}
lru-cache@10.4.3: {}
lru-cache@11.1.0: {}
lru-cache@5.1.1:
@@ -12404,6 +12648,8 @@ snapshots:
dependencies:
boolbase: 1.0.0
nwsapi@2.2.21: {}
nx@21.4.1:
dependencies:
'@napi-rs/wasm-runtime': 0.2.4
@@ -12606,6 +12852,10 @@ snapshots:
dependencies:
entities: 4.5.0
parse5@7.3.0:
dependencies:
entities: 6.0.1
parseurl@1.3.3: {}
pascal-case@3.1.2:
@@ -13231,6 +13481,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
rrweb-cssom@0.8.0: {}
run-applescript@7.0.0: {}
run-parallel@1.2.0:
@@ -13245,6 +13497,10 @@ snapshots:
safer-buffer@2.1.2: {}
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
scheduler@0.26.0: {}
section-matter@1.0.0:
@@ -13516,6 +13772,8 @@ snapshots:
react: 19.1.1
use-sync-external-store: 1.5.0(react@19.1.1)
symbol-tree@3.2.4: {}
synckit@0.11.3:
dependencies:
'@pkgr/core': 0.2.2
@@ -13609,6 +13867,12 @@ snapshots:
markdown-it-task-lists: 2.1.1
prosemirror-markdown: 1.13.1
tldts-core@6.1.86: {}
tldts@6.1.86:
dependencies:
tldts-core: 6.1.86
tmp@0.2.5: {}
to-regex-range@5.0.1:
@@ -13621,8 +13885,16 @@ snapshots:
totalist@3.0.1: {}
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
tr46@0.0.3: {}
tr46@5.1.1:
dependencies:
punycode: 2.3.1
tree-kill@1.2.2: {}
trough@2.2.0: {}
@@ -13972,7 +14244,7 @@ snapshots:
fsevents: 2.3.3
terser: 5.39.2
vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(terser@5.39.2):
vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(terser@5.39.2):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
@@ -14002,6 +14274,7 @@ snapshots:
'@types/node': 20.14.10
'@vitest/ui': 3.2.4(vitest@3.2.4)
happy-dom: 15.11.0
jsdom: 26.1.0
transitivePeerDependencies:
- less
- lightningcss
@@ -14106,6 +14379,10 @@ snapshots:
w3c-keyname@2.2.8: {}
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
walk-up-path@4.0.0: {}
wcwidth@1.0.1:
@@ -14136,8 +14413,19 @@ snapshots:
dependencies:
iconv-lite: 0.6.3
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
whatwg-mimetype@3.0.0: {}
whatwg-mimetype@4.0.0: {}
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
@@ -14201,6 +14489,10 @@ snapshots:
xml-name-validator@4.0.0: {}
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
y18n@5.0.8: {}
yallist@3.1.1: {}

View File

@@ -15,12 +15,14 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI, isElectron } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()
const conflictDetection = useConflictDetection()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
const handleKey = (e: KeyboardEvent) => {
workspaceStore.shiftDown = e.shiftKey
@@ -47,5 +49,9 @@ onMounted(() => {
if (isElectron()) {
document.addEventListener('contextmenu', showContextMenu)
}
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()
})
</script>

View File

@@ -16,6 +16,14 @@ const meta: Meta<typeof IconButton> = {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
onClick: { action: 'clicked' }
}
}

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" @click="onClick">
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<slot></slot>
</Button>
</template>
@@ -11,6 +11,7 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonTypeClasses,
getIconButtonSizeClasses
} from '@/types/buttonTypes'
@@ -22,6 +23,8 @@ interface IconButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'secondary',
border = false,
disabled = false,
class: className,
onClick
} = defineProps<IconButtonProps>()
@@ -29,7 +32,9 @@ const {
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} p-0`
const sizeClasses = getIconButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -28,6 +28,14 @@ const meta: Meta<typeof IconTextButton> = {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
iconPosition: {
control: { type: 'select' },
options: ['left', 'right']

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" @click="onClick">
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
<span>{{ label }}</span>
<slot v-if="iconPosition === 'right'" name="icon"></slot>
@@ -13,6 +13,7 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
@@ -26,6 +27,8 @@ interface IconTextButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
iconPosition = 'left',
label,
@@ -35,7 +38,9 @@ const {
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} !justify-start gap-2`
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -16,6 +16,14 @@ const meta: Meta<typeof TextButton> = {
options: ['sm', 'md'],
defaultValue: 'md'
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent'],

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" role="button" @click="onClick">
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<span>{{ label }}</span>
</Button>
</template>
@@ -11,6 +11,7 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
@@ -23,6 +24,8 @@ interface TextButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
label,
onClick
@@ -31,7 +34,9 @@ const {
const buttonStyle = computed(() => {
const baseClasses = getBaseButtonClasses()
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -0,0 +1,131 @@
<template>
<div
class="inline-flex items-center justify-center"
:style="{ width: size + 'px', height: size + 'px' }"
>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 14 14"
fill="none"
class="animate-spin"
:style="{ animationDuration: duration }"
>
<g clip-path="url(#clip0_776_9582)">
<!-- Top dot -->
<path
class="dot-animation"
style="animation-delay: 0s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M7 2.21053C7.61042 2.21053 8.10526 1.71568 8.10526 1.10526C8.10526 0.494843 7.61042 0 7 0C6.38958 0 5.89474 0.494843 5.89474 1.10526C5.89474 1.71568 6.38958 2.21053 7 2.21053Z"
:fill="color"
/>
<!-- Left dot -->
<path
class="dot-animation"
style="animation-delay: 0.25s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.21053 7C2.21053 7.61042 1.71568 8.10526 1.10526 8.10526C0.494843 8.10526 0 7.61042 0 7C0 6.38958 0.494843 5.89474 1.10526 5.89474C1.71568 5.89474 2.21053 6.38958 2.21053 7Z"
:fill="color"
/>
<!-- Right dot -->
<path
class="dot-animation"
style="animation-delay: 0.5s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M14 7C14 7.61042 13.5052 8.10526 12.8947 8.10526C12.2843 8.10526 11.7895 7.61042 11.7895 7C11.7895 6.38958 12.2843 5.89474 12.8947 5.89474C13.5052 5.89474 14 6.38958 14 7Z"
:fill="color"
/>
<!-- Bottom dot -->
<path
class="dot-animation"
style="animation-delay: 0.75s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.10526 12.8947C8.10526 13.5052 7.61041 14 6.99999 14C6.38957 14 5.89473 13.5052 5.89473 12.8947C5.89473 12.2843 6.38957 11.7895 6.99999 11.7895C7.61041 11.7895 8.10526 12.2843 8.10526 12.8947Z"
:fill="color"
/>
<!-- Top-left dot -->
<path
class="dot-animation"
style="animation-delay: 0.125s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.05039 3.61349C2.48203 4.04513 3.18184 4.04513 3.61347 3.61349C4.0451 3.18186 4.0451 2.48205 3.61347 2.05042C3.18184 1.61878 2.48203 1.61878 2.05039 2.05042C1.61876 2.48205 1.61876 3.18186 2.05039 3.61349Z"
:fill="color"
/>
<!-- Bottom-right dot -->
<path
class="dot-animation"
style="animation-delay: 0.625s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.9496 11.9496C11.518 12.3812 10.8182 12.3812 10.3865 11.9496C9.9549 11.5179 9.9549 10.8181 10.3865 10.3865C10.8182 9.95485 11.518 9.95485 11.9496 10.3865C12.3812 10.8181 12.3812 11.5179 11.9496 11.9496Z"
:fill="color"
/>
<!-- Bottom-left dot -->
<path
class="dot-animation"
style="animation-delay: 0.875s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.05039 11.9496C2.48203 12.3812 3.18184 12.3812 3.61347 11.9496C4.0451 11.5179 4.0451 10.8181 3.61347 10.3865C3.18184 9.95485 2.48203 9.95485 2.05039 10.3865C1.61876 10.8181 1.61876 11.5179 2.05039 11.9496Z"
:fill="color"
/>
<!-- Top-right dot -->
<path
class="dot-animation"
style="animation-delay: 0.375s"
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.9496 3.61349C11.518 4.04513 10.8182 4.04513 10.3865 3.61349C9.9549 3.18186 9.9549 2.48205 10.3865 2.05042C10.8182 1.61878 11.518 1.61878 11.9496 2.05042C12.3812 2.48205 12.3812 3.18186 11.9496 3.61349Z"
:fill="color"
/>
</g>
<defs>
<clipPath id="clip0_776_9582">
<rect width="14" height="14" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
const { size = 24, duration = '2s' } = defineProps<{
size?: number
duration?: string
}>()
const colorPaletteStore = useColorPaletteStore()
const color = computed(() =>
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
)
</script>
<style scoped>
.dot-animation {
animation: dot-pulse 1s ease-in-out infinite;
}
@keyframes dot-pulse {
0%,
80%,
100% {
opacity: 0.3;
}
40% {
opacity: 1;
}
}
</style>

View File

@@ -29,7 +29,7 @@
/>
<template v-if="item.footerComponent" #footer>
<component :is="item.footerComponent" />
<component :is="item.footerComponent" v-bind="item.footerProps" />
</template>
</Dialog>
</template>

View File

@@ -21,16 +21,9 @@
@click="showReport"
/>
<Button
v-show="!sendReportOpen"
text
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
<Button
v-if="authStore.currentUser"
v-show="!reportOpen"
text
:label="$t('issueReport.contactSupportTitle')"
:label="$t('issueReport.helpFix')"
@click="showContactSupport"
/>
</div>
@@ -41,16 +34,6 @@
</ScrollPanel>
<Divider />
</template>
<ReportIssuePanel
v-if="sendReportOpen"
:title="$t('issueReport.submitErrorReport')"
:error-type="error.reportType ?? 'unknownError'"
:extra-fields="[stackTraceField]"
:tags="{
exceptionMessage: error.exceptionMessage,
nodeType: error.nodeType ?? 'UNKNOWN'
}"
/>
<div class="flex gap-4 justify-end">
<FindIssueButton
:error-message="error.exceptionMessage"
@@ -81,18 +64,12 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { ReportField } from '@/types/issueReportTypes'
import {
type ErrorReportData,
generateErrorReport
} from '@/utils/errorReportUtil'
import ReportIssuePanel from './error/ReportIssuePanel.vue'
const authStore = useFirebaseAuthStore()
const { error } = defineProps<{
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
/**
@@ -114,10 +91,6 @@ const reportOpen = ref(false)
const showReport = () => {
reportOpen.value = true
}
const sendReportOpen = ref(false)
const showSendReport = () => {
sendReportOpen.value = true
}
const toast = useToast()
const { t } = useI18n()
const systemStatsStore = useSystemStatsStore()
@@ -126,15 +99,6 @@ const title = computed<string>(
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
)
const stackTraceField = computed<ReportField>(() => {
return {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => error.traceback
}
})
const showContactSupport = async () => {
await useCommandStore().execute('Comfy.ContactSupport')
}

View File

@@ -1,33 +0,0 @@
<template>
<div class="p-2 h-full" aria-labelledby="issue-report-title">
<Panel
:pt="{
root: 'border-none',
content: 'p-0'
}"
>
<template #header>
<header class="flex flex-col items-center w-full">
<h2 id="issue-report-title" class="text-4xl">
{{ title }}
</h2>
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
</header>
</template>
<ReportIssuePanel v-bind="panelProps" :pt="{ root: 'border-none' }" />
</Panel>
</div>
</template>
<script setup lang="ts">
import Panel from 'primevue/panel'
import ReportIssuePanel from '@/components/dialog/content/error/ReportIssuePanel.vue'
import type { IssueReportPanelProps } from '@/types/issueReportTypes'
defineProps<{
title: string
subtitle?: string
panelProps: IssueReportPanelProps
}>()
</script>

View File

@@ -31,12 +31,20 @@
</div>
</template>
</ListBox>
<div v-if="isManagerInstalled" class="flex justify-end py-3">
<div v-if="showManagerButtons" class="flex justify-end py-3">
<PackInstallButton
:disabled="isLoading || !!error || missingNodePacks.length === 0"
v-if="showInstallAllButton"
size="md"
:disabled="
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
"
:is-loading="isLoading"
:node-packs="missingNodePacks"
variant="black"
:label="$t('manager.installAllMissingNodes')"
:label="
isLoading
? $t('manager.gettingInfo')
: $t('manager.installAllMissingNodes')
"
/>
<Button label="Open Manager" size="small" outlined @click="openManager" />
</div>
@@ -46,34 +54,39 @@
import Button from 'primevue/button'
import ListBox from 'primevue/listbox'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useDialogService } from '@/services/dialogService'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useCommandStore } from '@/stores/commandStore'
import {
ManagerUIState,
useManagerStateStore
} from '@/stores/managerStateStore'
import { useToastStore } from '@/stores/toastStore'
import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes'
import PackInstallButton from './manager/button/PackInstallButton.vue'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
const aboutPanelStore = useAboutPanelStore()
// Get missing node packs from workflow with loading and error states
const { missingNodePacks, isLoading, error, missingCoreNodes } =
useMissingNodes()
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
// This allows us to conditionally show the Manager button only when the extension is available
// TODO: Remove this check when Manager functionality is fully migrated into core
const isManagerInstalled = computed(() => {
return aboutPanelStore.badges.some(
(badge) =>
badge.label.includes('ComfyUI-Manager') ||
badge.url.includes('ComfyUI-Manager')
const comfyManagerStore = useComfyManagerStore()
// Check if any of the missing packs are currently being installed
const isInstalling = computed(() => {
if (!missingNodePacks.value?.length) return false
return missingNodePacks.value.some((pack) =>
comfyManagerStore.isPackInstalling(pack.id)
)
})
@@ -98,10 +111,47 @@ const uniqueNodes = computed(() => {
})
})
const openManager = () => {
useDialogService().showManagerDialog({
initialTab: ManagerTab.Missing
})
const managerStateStore = useManagerStateStore()
// Show manager buttons unless manager is disabled
const showManagerButtons = computed(() => {
return managerStateStore.managerUIState !== ManagerUIState.DISABLED
})
// Only show Install All button for NEW_UI (new manager with v4 support)
const showInstallAllButton = computed(() => {
return managerStateStore.managerUIState === ManagerUIState.NEW_UI
})
const openManager = async () => {
const state = managerStateStore.managerUIState
switch (state) {
case ManagerUIState.DISABLED:
useDialogService().showSettingsDialog('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
} catch {
// If legacy command doesn't exist, show toast
const { t } = useI18n()
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
break
case ManagerUIState.NEW_UI:
useDialogService().showManagerDialog({
initialTab: ManagerTab.Missing
})
break
}
}
</script>

View File

@@ -30,11 +30,20 @@ const defaultMockTaskLogs = [
vi.mock('@/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn(() => ({
taskLogs: [...defaultMockTaskLogs]
taskLogs: [...defaultMockTaskLogs],
succeededTasksLogs: [...defaultMockTaskLogs],
failedTasksLogs: [...defaultMockTaskLogs],
managerQueue: { historyCount: 2 },
isLoading: false
})),
useManagerProgressDialogStore: vi.fn(() => ({
isExpanded: true,
collapse: mockCollapse
activeTabIndex: 0,
getActiveTabIndex: vi.fn(() => 0),
setActiveTabIndex: vi.fn(),
toggle: vi.fn(),
collapse: mockCollapse,
expand: vi.fn()
}))
}))

View File

@@ -18,16 +18,16 @@
'max-h-0': !isExpanded
}"
>
<div v-for="(panel, index) in taskPanels" :key="index">
<div v-for="(log, index) in focusedLogs" :key="index">
<Panel
:expanded="collapsedPanels[index] || false"
:expanded="collapsedPanels[index] === true"
toggleable
class="shadow-elevation-1 rounded-lg mt-2 dark-theme:bg-black dark-theme:border-black"
>
<template #header>
<div class="flex items-center justify-between w-full py-2">
<div class="flex flex-col text-sm font-medium leading-normal">
<span>{{ panel.taskName }}</span>
<span>{{ log.taskName }}</span>
<span class="text-muted">
{{
isInProgress(index)
@@ -52,24 +52,24 @@
</template>
<div
:ref="
index === taskPanels.length - 1
index === focusedLogs.length - 1
? (el) => (lastPanelRef = el as HTMLElement)
: undefined
"
class="overflow-y-auto h-64 rounded-lg bg-black"
:class="{
'h-64': index !== taskPanels.length - 1,
'flex-grow': index === taskPanels.length - 1
'h-64': index !== focusedLogs.length - 1,
'flex-grow': index === focusedLogs.length - 1
}"
@scroll="handleScroll"
>
<div class="h-full">
<div
v-for="(log, logIndex) in panel.logs"
v-for="(logLine, logIndex) in log.logs"
:key="logIndex"
class="text-neutral-400 dark-theme:text-muted"
>
<pre class="whitespace-pre-wrap break-words">{{ log }}</pre>
<pre class="whitespace-pre-wrap break-words">{{ logLine }}</pre>
</div>
</div>
</div>
@@ -90,14 +90,31 @@ import {
useManagerProgressDialogStore
} from '@/stores/comfyManagerStore'
const { taskLogs } = useComfyManagerStore()
const comfyManagerStore = useComfyManagerStore()
const progressDialogContent = useManagerProgressDialogStore()
const managerStore = useComfyManagerStore()
const isInProgress = (index: number) =>
index === taskPanels.value.length - 1 && managerStore.uncompletedCount > 0
const isInProgress = (index: number) => {
const log = focusedLogs.value[index]
if (!log) return false
const taskPanels = computed(() => taskLogs)
// Check if this task is in the running or pending queue
const taskQueue = comfyManagerStore.taskQueue
if (!taskQueue) return false
const allQueueTasks = [
...(taskQueue.running_queue || []),
...(taskQueue.pending_queue || [])
]
return allQueueTasks.some((task) => task.ui_id === log.taskId)
}
const focusedLogs = computed(() => {
if (progressDialogContent.getActiveTabIndex() === 0) {
return comfyManagerStore.succeededTasksLogs
}
return comfyManagerStore.failedTasksLogs
})
const isExpanded = computed(() => progressDialogContent.isExpanded)
const isCollapsed = computed(() => !isExpanded.value)
@@ -115,7 +132,7 @@ const { y: scrollY } = useScroll(sectionsContainerRef, {
const lastPanelRef = ref<HTMLElement | null>(null)
const isUserScrolling = ref(false)
const lastPanelLogs = computed(() => taskPanels.value?.at(-1)?.logs)
const lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)
const isAtBottom = (el: HTMLElement | null) => {
if (!el) return false

View File

@@ -1,338 +0,0 @@
import { Form } from '@primevue/forms'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import Checkbox from 'primevue/checkbox'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import Tooltip from 'primevue/tooltip'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMesages from '@/locales/en/main.json'
import { IssueReportPanelProps } from '@/types/issueReportTypes'
import ReportIssuePanel from './ReportIssuePanel.vue'
const DEFAULT_FIELDS = ['Workflow', 'Logs', 'Settings', 'SystemStats']
const CUSTOM_FIELDS = [
{
label: 'Custom Field',
value: 'CustomField',
optIn: true,
getData: () => 'mock data'
}
]
async function getSubmittedContext() {
const { captureMessage } = (await import('@sentry/core')) as any
return captureMessage.mock.calls[0][1]
}
async function submitForm(wrapper: any) {
await wrapper.findComponent(Form).trigger('submit')
return getSubmittedContext()
}
async function findAndUpdateCheckbox(
wrapper: any,
value: string,
checked = true
) {
const checkbox = wrapper
.findAllComponents(Checkbox)
.find((c: any) => c.props('value') === value)
if (!checkbox) throw new Error(`Checkbox with value "${value}" not found`)
await checkbox.vm.$emit('update:modelValue', checked)
return checkbox
}
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMesages
}
})
vi.mock('primevue/usetoast', () => ({
useToast: vi.fn(() => ({
add: vi.fn()
}))
}))
vi.mock('@/scripts/api', () => ({
api: {
getLogs: vi.fn().mockResolvedValue('mock logs'),
getSystemStats: vi.fn().mockResolvedValue('mock stats'),
getSettings: vi.fn().mockResolvedValue('mock settings'),
fetchApi: vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({}),
text: vi.fn().mockResolvedValue('')
}),
apiURL: vi.fn().mockReturnValue('https://test.com')
}
}))
vi.mock('@/scripts/app', () => ({
app: {
graph: {
asSerialisable: vi.fn().mockReturnValue({})
}
}
}))
vi.mock('@sentry/core', () => ({
captureMessage: vi.fn()
}))
vi.mock('@primevue/forms', () => ({
Form: {
name: 'Form',
template:
'<form @submit.prevent="onSubmit"><slot :values="formValues" /></form>',
props: ['resolver'],
data() {
return {
formValues: {}
}
},
methods: {
onSubmit() {
// @ts-expect-error fixme ts strict error
this.$emit('submit', {
valid: true,
// @ts-expect-error fixme ts strict error
values: this.formValues
})
},
updateFieldValue(name: string, value: any) {
// @ts-expect-error fixme ts strict error
this.formValues[name] = value
}
}
},
FormField: {
name: 'FormField',
template:
'<div><slot :modelValue="modelValue" @update:modelValue="updateValue" /></div>',
props: ['name'],
data() {
return {
modelValue: ''
}
},
methods: {
// @ts-expect-error fixme ts strict error
updateValue(value) {
// @ts-expect-error fixme ts strict error
this.modelValue = value
// @ts-expect-error fixme ts strict error
let parent = this.$parent
while (parent && parent.$options.name !== 'Form') {
parent = parent.$parent
}
if (parent) {
// @ts-expect-error fixme ts strict error
parent.updateFieldValue(this.name, value)
}
}
}
}
}))
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: () => ({
currentUser: {
email: 'test@example.com'
}
})
}))
describe('ReportIssuePanel', () => {
beforeEach(() => {
vi.clearAllMocks()
const pinia = createPinia()
setActivePinia(pinia)
})
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
return mount(ReportIssuePanel, {
global: {
plugins: [PrimeVue, i18n, createPinia()],
directives: { tooltip: Tooltip }
},
props,
...options
})
}
it('renders the panel with all required components', () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
expect(wrapper.find('.p-panel').exists()).toBe(true)
expect(wrapper.findAllComponents(Checkbox).length).toBe(6)
expect(wrapper.findComponent(InputText).exists()).toBe(true)
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
})
it('updates selection when checkboxes are selected', async () => {
const wrapper = mountComponent({
errorType: 'Test Error'
})
const checkboxes = wrapper.findAllComponents(Checkbox)
for (const field of DEFAULT_FIELDS) {
const checkbox = checkboxes.find(
// @ts-expect-error fixme ts strict error
(checkbox) => checkbox.props('value') === field
)
expect(checkbox).toBeDefined()
await checkbox?.vm.$emit('update:modelValue', [field])
expect(wrapper.vm.selection).toContain(field)
}
})
it('updates contactInfo when input is changed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const input = wrapper.findComponent(InputText)
await input.vm.$emit('update:modelValue', 'test@example.com')
const context = await submitForm(wrapper)
expect(context.user.email).toBe('test@example.com')
})
it('updates additional details when textarea is changed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const textarea = wrapper.findComponent(Textarea)
await textarea.vm.$emit('update:modelValue', 'This is a test detail.')
const context = await submitForm(wrapper)
expect(context.extra.details).toBe('This is a test detail.')
})
it('set contact preferences back to false if email is removed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const input = wrapper.findComponent(InputText)
// Set a valid email, enabling the contact preferences to be changed
await input.vm.$emit('update:modelValue', 'name@example.com')
// Enable both contact preferences
for (const pref of ['followUp', 'notifyOnResolution']) {
await findAndUpdateCheckbox(wrapper, pref)
}
// Change the email back to empty
await input.vm.$emit('update:modelValue', '')
const context = await submitForm(wrapper)
// Check that the contact preferences are back to false automatically
expect(context.tags.followUp).toBe(false)
expect(context.tags.notifyOnResolution).toBe(false)
})
it('renders with overridden default fields', () => {
const wrapper = mountComponent({
errorType: 'Test Error',
defaultFields: ['Settings']
})
// Filter out the contact preferences checkboxes
const fieldCheckboxes = wrapper.findAllComponents(Checkbox).filter(
// @ts-expect-error fixme ts strict error
(checkbox) =>
!['followUp', 'notifyOnResolution'].includes(checkbox.props('value'))
)
expect(fieldCheckboxes.length).toBe(1)
expect(fieldCheckboxes.at(0)?.props('value')).toBe('Settings')
})
it('renders additional fields when extraFields prop is provided', () => {
const wrapper = mountComponent({
errorType: 'Test Error',
extraFields: CUSTOM_FIELDS
})
const customCheckbox = wrapper
.findAllComponents(Checkbox)
// @ts-expect-error fixme ts strict error
.find((checkbox) => checkbox.props('value') === 'CustomField')
expect(customCheckbox).toBeDefined()
})
it('allows custom fields to be selected', async () => {
const wrapper = mountComponent({
errorType: 'Test Error',
extraFields: CUSTOM_FIELDS
})
await findAndUpdateCheckbox(wrapper, 'CustomField')
const context = await submitForm(wrapper)
expect(context.extra.CustomField).toBe('mock data')
})
it('does not submit unchecked fields', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const textarea = wrapper.findComponent(Textarea)
// Set details but don't check any field checkboxes
await textarea.vm.$emit(
'update:modelValue',
'Report with only text but no fields selected'
)
const context = await submitForm(wrapper)
// Verify none of the optional fields were included
for (const field of DEFAULT_FIELDS) {
expect(context.extra[field]).toBeUndefined()
}
})
it.each([
{
checkbox: 'Logs',
apiMethod: 'getLogs',
expectedKey: 'Logs',
mockValue: 'mock logs'
},
{
checkbox: 'SystemStats',
apiMethod: 'getSystemStats',
expectedKey: 'SystemStats',
mockValue: 'mock stats'
},
{
checkbox: 'Settings',
apiMethod: 'getSettings',
expectedKey: 'Settings',
mockValue: 'mock settings'
}
])(
'submits $checkbox data when checkbox is selected',
async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { api } = (await import('@/scripts/api')) as any
vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
await findAndUpdateCheckbox(wrapper, checkbox)
const context = await submitForm(wrapper)
expect(context.extra[expectedKey]).toBe(mockValue)
}
)
it('submits workflow when the Workflow checkbox is selected', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { app } = (await import('@/scripts/app')) as any
const mockWorkflow = { nodes: [], edges: [] }
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
await findAndUpdateCheckbox(wrapper, 'Workflow')
const context = await submitForm(wrapper)
expect(context.extra.Workflow).toEqual(mockWorkflow)
})
})

View File

@@ -1,348 +0,0 @@
<template>
<Form
v-slot="$form"
:resolver="zodResolver(issueReportSchema)"
@submit="submit"
>
<Panel :pt="$attrs.pt as any">
<template #header>
<div class="flex items-center gap-2">
<span class="font-bold">{{ title }}</span>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-4">
<Button
v-tooltip="!submitted ? $t('g.reportIssueTooltip') : undefined"
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
:severity="submitted ? 'secondary' : 'primary'"
:icon="submitted ? 'pi pi-check' : 'pi pi-send'"
:disabled="submitted"
type="submit"
/>
</div>
</template>
<div class="p-4 mt-2 border border-round surface-border shadow-1">
<div class="flex flex-col gap-6">
<FormField
v-slot="$field"
name="contactInfo"
:initial-value="authStore.currentUser?.email"
>
<div class="self-stretch inline-flex justify-start items-center">
<label for="contactInfo" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.email')
}}</label>
</div>
<InputText
id="contactInfo"
v-bind="$field"
class="w-full"
:placeholder="$t('issueReport.provideEmail')"
/>
<Message
v-if="$field?.error && $field.touched && $field.value !== ''"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.invalidEmail') }}
</Message>
</FormField>
<FormField v-slot="$field" name="helpType">
<div class="flex flex-col gap-2">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<label for="helpType" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.whatDoYouNeedHelpWith')
}}</label>
</div>
<Dropdown
v-bind="$field"
v-model="$field.value"
:options="helpTypes"
option-label="label"
option-value="value"
:placeholder="$t('issueReport.selectIssue')"
class="w-full"
/>
<Message
v-if="$field?.error"
severity="error"
size="small"
variant="simple"
>
{{ t('issueReport.validation.selectIssueType') }}
</Message>
</div>
</FormField>
<div class="flex flex-col gap-2">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<span class="pb-2 pt-0 opacity-80">{{
$t('issueReport.whatCanWeInclude')
}}</span>
</div>
<div class="flex flex-row gap-3">
<div v-for="field in fields" :key="field.value">
<FormField
v-if="field.optIn"
v-slot="$field"
:name="field.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
v-model="selection"
:input-id="field.value"
:value="field.value"
/>
<label :for="field.value">{{ field.label }}</label>
</FormField>
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<FormField v-slot="$field" name="details">
<div
class="self-stretch inline-flex justify-start items-center gap-2.5"
>
<label for="details" class="pb-2 pt-0 opacity-80">{{
$t('issueReport.describeTheProblem')
}}</label>
</div>
<Textarea
v-bind="$field"
id="details"
class="w-full"
rows="5"
:placeholder="$t('issueReport.provideAdditionalDetails')"
:aria-label="$t('issueReport.provideAdditionalDetails')"
/>
<Message
v-if="$field?.error && $field.touched"
severity="error"
size="small"
variant="simple"
>
{{
$field.value
? t('issueReport.validation.maxLength')
: t('issueReport.validation.descriptionRequired')
}}
</Message>
</FormField>
</div>
<div class="flex flex-col gap-3 mt-2">
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
<FormField
v-slot="$field"
:name="checkbox.value"
class="flex space-x-1"
>
<Checkbox
v-bind="$field"
v-model="contactPrefs"
:input-id="checkbox.value"
:value="checkbox.value"
:disabled="
$form.contactInfo?.error || !$form.contactInfo?.value
"
/>
<label :for="checkbox.value">{{ checkbox.label }}</label>
</FormField>
</div>
</div>
</div>
</div>
</Panel>
</Form>
</template>
<script setup lang="ts">
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import type { CaptureContext, User } from '@sentry/core'
import { captureMessage } from '@sentry/core'
import _ from 'es-toolkit/compat'
import { cloneDeep } from 'es-toolkit/compat'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import Dropdown from 'primevue/dropdown'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import Panel from 'primevue/panel'
import Textarea from 'primevue/textarea'
import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import {
type IssueReportFormData,
issueReportSchema
} from '@/schemas/issueReportSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import type {
DefaultField,
IssueReportPanelProps,
ReportField
} from '@/types/issueReportTypes'
import { isElectron } from '@/utils/envUtil'
import { generateUUID } from '@/utils/formatUtil'
const DEFAULT_ISSUE_NAME = 'User reported issue'
const props = defineProps<IssueReportPanelProps>()
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
props
const { t } = useI18n()
const toast = useToast()
const authStore = useFirebaseAuthStore()
const selection = ref<string[]>([])
const contactPrefs = ref<string[]>([])
const submitted = ref(false)
const contactCheckboxes = [
{ label: t('issueReport.contactFollowUp'), value: 'followUp' },
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
]
const helpTypes = [
{
label: t('issueReport.helpTypes.billingPayments'),
value: 'billingPayments'
},
{
label: t('issueReport.helpTypes.loginAccessIssues'),
value: 'loginAccessIssues'
},
{ label: t('issueReport.helpTypes.giveFeedback'), value: 'giveFeedback' },
{ label: t('issueReport.helpTypes.bugReport'), value: 'bugReport' },
{ label: t('issueReport.helpTypes.somethingElse'), value: 'somethingElse' }
]
const defaultFieldsConfig: ReportField[] = [
{
label: t('issueReport.systemStats'),
value: 'SystemStats',
getData: () => api.getSystemStats(),
optIn: true
},
{
label: t('g.workflow'),
value: 'Workflow',
getData: () => cloneDeep(app.graph.asSerialisable()),
optIn: true
},
{
label: t('g.logs'),
value: 'Logs',
getData: () => api.getLogs(),
optIn: true
},
{
label: t('g.settings'),
value: 'Settings',
getData: () => api.getSettings(),
optIn: true
}
]
const fields = computed(() => [
...defaultFieldsConfig.filter(({ value }) =>
defaultFields.includes(value as DefaultField)
),
...(props.extraFields ?? [])
])
const createUser = (formData: IssueReportFormData): User => ({
email: formData.contactInfo || undefined
})
const createExtraData = async (formData: IssueReportFormData) => {
const result: Record<string, unknown> = {}
const isChecked = (fieldValue: string) => formData[fieldValue]
await Promise.all(
fields.value
.filter((field) => !field.optIn || isChecked(field.value))
.map(async (field) => {
try {
result[field.value] = await field.getData()
} catch (error) {
console.error(`Failed to collect ${field.value}:`, error)
result[field.value] = { error: String(error) }
}
})
)
return result
}
const createCaptureContext = async (
formData: IssueReportFormData
): Promise<CaptureContext> => {
return {
user: createUser(formData),
level: 'error',
tags: {
errorType: props.errorType,
helpType: formData.helpType,
followUp: formData.contactInfo ? formData.followUp : false,
notifyOnResolution: formData.contactInfo
? formData.notifyOnResolution
: false,
isElectron: isElectron(),
..._.mapValues(props.tags, (tag) => _.trim(tag).replace(/[\n\r\t]/g, ' '))
},
extra: {
details: formData.details,
...(await createExtraData(formData))
}
}
}
const generateUniqueTicketId = (type: string) => `${type}-${generateUUID()}`
const submit = async (event: FormSubmitEvent) => {
if (event.valid) {
try {
const captureContext = await createCaptureContext(event.values)
// If it's billing or access issue, generate unique id to be used by customer service ticketing
const isValidContactInfo = event.values.contactInfo?.length
const isCustomerServiceIssue =
isValidContactInfo &&
['billingPayments', 'loginAccessIssues'].includes(
event.values.helpType || ''
)
const issueName = isCustomerServiceIssue
? `ticket-${generateUniqueTicketId(event.values.helpType || '')}`
: DEFAULT_ISSUE_NAME
captureMessage(issueName, captureContext)
submitted.value = true
toast.add({
severity: 'success',
summary: t('g.reportSent'),
life: 3000
})
} catch (error) {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error instanceof Error ? error.message : String(error),
life: 3000
})
}
}
}
</script>

View File

@@ -26,6 +26,35 @@
}"
>
<div class="px-6 flex flex-col h-full">
<!-- Conflict Warning Banner -->
<div
v-if="shouldShowManagerBanner"
class="bg-yellow-600 bg-opacity-20 border border-yellow-400 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6 relative"
>
<i class="pi pi-exclamation-triangle text-yellow-600 text-lg"></i>
<div class="flex flex-col gap-2 flex-1">
<p class="text-sm font-bold m-0">
{{ $t('manager.conflicts.warningBanner.title') }}
</p>
<p class="text-xs m-0">
{{ $t('manager.conflicts.warningBanner.message') }}
</p>
<p
class="text-sm font-bold m-0 cursor-pointer"
@click="onClickWarningLink"
>
{{ $t('manager.conflicts.warningBanner.button') }}
</p>
</div>
<button
type="button"
class="absolute top-2 right-2 w-6 h-6 border-none outline-none bg-transparent flex items-center justify-center text-yellow-600 rounded transition-colors"
:aria-label="$t('g.close')"
@click="dismissWarningBanner"
>
<i class="pi pi-times text-sm"></i>
</button>
</div>
<RegistrySearchBar
v-model:searchQuery="searchQuery"
v-model:searchMode="searchMode"
@@ -34,6 +63,7 @@
:suggestions="suggestions"
:is-missing-tab="isMissingTab"
:sort-options="sortOptions"
:is-update-available-tab="isUpdateAvailableTab"
/>
<div class="flex-1 overflow-auto">
<div
@@ -69,7 +99,9 @@
:is-selected="
selectedNodePacks.some((pack) => pack.id === item.id)
"
@click.stop="(event) => selectNodePack(item, event)"
@click.stop="
(event: MouseEvent) => selectNodePack(item, event)
"
/>
</template>
</VirtualGrid>
@@ -101,7 +133,8 @@ import {
onMounted,
onUnmounted,
ref,
watch
watch,
watchEffect
} from 'vue'
import { useI18n } from 'vue-i18n'
@@ -119,6 +152,7 @@ import { useManagerStatePersistence } from '@/composables/manager/useManagerStat
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useRegistrySearch } from '@/composables/useRegistrySearch'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
@@ -133,12 +167,13 @@ const { initialTab } = defineProps<{
const { t } = useI18n()
const comfyManagerStore = useComfyManagerStore()
const { getPackById } = useComfyRegistryStore()
const conflictAcknowledgment = useConflictAcknowledgment()
const persistedState = useManagerStatePersistence()
const initialState = persistedState.loadStoredState()
const GRID_STYLE = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(19rem, 1fr))',
gridTemplateColumns: 'repeat(auto-fill, minmax(17rem, 1fr))',
padding: '0.5rem',
gap: '1.5rem'
} as const
@@ -149,6 +184,13 @@ const {
toggle: toggleSideNav
} = useResponsiveCollapse()
// Use conflict acknowledgment state from composable
const {
shouldShowManagerBanner,
dismissWarningBanner,
dismissRedDotNotification
} = conflictAcknowledgment
const tabs = ref<TabItem[]>([
{ id: ManagerTab.All, label: t('g.all'), icon: 'pi-list' },
{ id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi-box' },
@@ -312,6 +354,13 @@ watch([isAllTab, searchResults], () => {
displayPacks.value = searchResults.value
})
const onClickWarningLink = () => {
window.open(
'https://docs.comfy.org/troubleshooting/custom-node-issues',
'_blank'
)
}
const onResultsChange = () => {
switch (selectedTab.value?.id) {
case ManagerTab.Installed:
@@ -472,6 +521,10 @@ watch([searchQuery, selectedTab], () => {
}
})
watchEffect(() => {
dismissRedDotNotification()
})
onBeforeUnmount(() => {
persistedState.persistState({
selectedTabId: selectedTab.value?.id,

View File

@@ -0,0 +1,82 @@
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import Tag from 'primevue/tag'
import Tooltip from 'primevue/tooltip'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json'
import ManagerHeader from './ManagerHeader.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMessages
}
})
describe('ManagerHeader', () => {
const createWrapper = () => {
return mount(ManagerHeader, {
global: {
plugins: [createPinia(), PrimeVue, i18n],
directives: {
tooltip: Tooltip
},
components: {
Tag
}
}
})
}
it('renders the component title', () => {
const wrapper = createWrapper()
expect(wrapper.find('h2').text()).toBe(
enMessages.manager.discoverCommunityContent
)
})
it('displays the legacy manager UI tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.exists()).toBe(true)
expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
})
it('applies info severity to the tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.classes()).toContain('p-tag-info')
})
it('displays info icon in the tag', () => {
const wrapper = createWrapper()
const icon = wrapper.find('.pi-info-circle')
expect(icon.exists()).toBe(true)
})
it('has cursor-help class on the tag', () => {
const wrapper = createWrapper()
const tag = wrapper.find('[data-pc-name="tag"]')
expect(tag.classes()).toContain('cursor-help')
})
it('has proper structure with flex container', () => {
const wrapper = createWrapper()
const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
expect(flexContainer.exists()).toBe(true)
const tag = flexContainer.find('[data-pc-name="tag"]')
expect(tag.exists()).toBe(true)
})
})

View File

@@ -4,6 +4,22 @@
<h2 class="text-lg font-normal text-left">
{{ $t('manager.discoverCommunityContent') }}
</h2>
<div class="flex justify-end ml-auto pr-4 pl-2">
<Tag
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
severity="info"
icon="pi pi-info-circle"
:value="$t('manager.legacyManagerUI')"
class="cursor-help ml-2"
:pt="{
root: { class: 'text-xs' }
}"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Tag from 'primevue/tag'
</script>

View File

@@ -0,0 +1,244 @@
<template>
<div class="w-[552px] flex flex-col">
<ContentDivider :width="1" />
<div class="px-4 py-6 w-full h-full flex flex-col gap-2">
<!-- Description -->
<div v-if="showAfterWhatsNew">
<p
class="text-sm leading-4 text-neutral-800 dark-theme:text-white m-0 mb-4"
>
{{ $t('manager.conflicts.description') }}
<br /><br />
{{ $t('manager.conflicts.info') }}
</p>
</div>
<!-- Import Failed List Wrapper -->
<div
v-if="importFailedConflicts.length > 0"
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
>
<div
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
@click="toggleImportFailedPanel"
>
<div class="flex-1 flex">
<span
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
>{{ importFailedConflicts.length }}</span
>
<span
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
>{{ $t('manager.conflicts.importFailedExtensions') }}</span
>
</div>
<div>
<Button
:icon="
importFailedExpanded
? 'pi pi-chevron-down text-xs'
: 'pi pi-chevron-right text-xs'
"
text
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
/>
</div>
</div>
<!-- Import failed list -->
<div
v-if="importFailedExpanded"
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
>
<div
v-for="(packageName, i) in importFailedConflicts"
:key="i"
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
>
<span class="text-xs text-neutral-600 dark-theme:text-neutral-300">
{{ packageName }}
</span>
<span class="pi pi-info-circle text-sm"></span>
</div>
</div>
</div>
<!-- Conflict List Wrapper -->
<div
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
>
<div
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
@click="toggleConflictsPanel"
>
<div class="flex-1 flex">
<span
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
>{{ allConflictDetails.length }}</span
>
<span
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
>{{ $t('manager.conflicts.conflicts') }}</span
>
</div>
<div>
<Button
:icon="
conflictsExpanded
? 'pi pi-chevron-down text-xs'
: 'pi pi-chevron-right text-xs'
"
text
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
/>
</div>
</div>
<!-- Conflicts list -->
<div
v-if="conflictsExpanded"
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
>
<div
v-for="(conflict, i) in allConflictDetails"
:key="i"
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
>
<span
class="text-xs text-neutral-600 dark-theme:text-neutral-300"
>{{ getConflictMessage(conflict, t) }}</span
>
<span class="pi pi-info-circle text-sm"></span>
</div>
</div>
</div>
<!-- Extension List Wrapper -->
<div
class="w-full flex flex-col bg-neutral-200 dark-theme:bg-black min-h-8 rounded-lg"
>
<div
class="w-full h-8 flex items-center justify-between gap-2 pl-4"
@click="toggleExtensionsPanel"
>
<div class="flex-1 flex">
<span
class="text-xs font-bold text-yellow-600 dark-theme:text-yellow-400 mr-2"
>{{ conflictData.length }}</span
>
<span
class="text-xs font-bold text-neutral-600 dark-theme:text-white"
>{{ $t('manager.conflicts.extensionAtRisk') }}</span
>
</div>
<div>
<Button
:icon="
extensionsExpanded
? 'pi pi-chevron-down text-xs'
: 'pi pi-chevron-right text-xs'
"
text
class="text-neutral-600 dark-theme:text-neutral-300 !bg-transparent"
/>
</div>
</div>
<!-- Extension list -->
<div
v-if="extensionsExpanded"
class="py-2 px-4 flex flex-col gap-2.5 max-h-[142px] overflow-y-auto scrollbar-hide"
>
<div
v-for="conflictResult in conflictData"
:key="conflictResult.package_id"
class="flex items-center justify-between h-6 px-4 flex-shrink-0 conflict-list-item"
>
<span class="text-xs text-neutral-600 dark-theme:text-neutral-300">
{{ conflictResult.package_name }}
</span>
<span class="pi pi-info-circle text-sm"></span>
</div>
</div>
</div>
</div>
<ContentDivider :width="1" />
</div>
</template>
<script setup lang="ts">
import { filter, flatMap, map, some } from 'es-toolkit/compat'
import Button from 'primevue/button'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import {
ConflictDetail,
ConflictDetectionResult
} from '@/types/conflictDetectionTypes'
import { getConflictMessage } from '@/utils/conflictMessageUtil'
const { showAfterWhatsNew = false, conflictedPackages } = defineProps<{
showAfterWhatsNew?: boolean
conflictedPackages?: ConflictDetectionResult[]
}>()
const { t } = useI18n()
const { conflictedPackages: globalConflictPackages } = useConflictDetection()
const conflictsExpanded = ref<boolean>(false)
const extensionsExpanded = ref<boolean>(false)
const importFailedExpanded = ref<boolean>(false)
const conflictData = computed(
() => conflictedPackages || globalConflictPackages.value
)
const allConflictDetails = computed(() => {
const allConflicts = flatMap(
conflictData.value,
(result: ConflictDetectionResult) => result.conflicts
)
return filter(
allConflicts,
(conflict: ConflictDetail) => conflict.type !== 'import_failed'
)
})
const packagesWithImportFailed = computed(() => {
return filter(conflictData.value, (result: ConflictDetectionResult) =>
some(
result.conflicts,
(conflict: ConflictDetail) => conflict.type === 'import_failed'
)
)
})
const importFailedConflicts = computed(() => {
return map(
packagesWithImportFailed.value,
(result: ConflictDetectionResult) =>
result.package_name || result.package_id
)
})
const toggleImportFailedPanel = () => {
importFailedExpanded.value = !importFailedExpanded.value
conflictsExpanded.value = false
extensionsExpanded.value = false
}
const toggleConflictsPanel = () => {
conflictsExpanded.value = !conflictsExpanded.value
extensionsExpanded.value = false
importFailedExpanded.value = false
}
const toggleExtensionsPanel = () => {
extensionsExpanded.value = !extensionsExpanded.value
conflictsExpanded.value = false
importFailedExpanded.value = false
}
</script>
<style scoped>
.conflict-list-item:hover {
background-color: rgba(0, 122, 255, 0.2);
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="flex items-center justify-between w-full px-3 py-4">
<div class="w-full flex items-center justify-between gap-2 pr-1">
<Button
:label="$t('manager.conflicts.conflictInfoTitle')"
text
severity="secondary"
size="small"
icon="pi pi-info-circle"
:pt="{
label: { class: 'text-sm' }
}"
@click="handleConflictInfoClick"
/>
<Button
v-if="props.buttonText"
:label="props.buttonText"
severity="secondary"
size="small"
@click="handleButtonClick"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useDialogStore } from '@/stores/dialogStore'
interface Props {
buttonText?: string
onButtonClick?: () => void
}
const props = withDefaults(defineProps<Props>(), {
buttonText: undefined,
onButtonClick: undefined
})
const dialogStore = useDialogStore()
const handleConflictInfoClick = () => {
window.open(
'https://docs.comfy.org/troubleshooting/custom-node-issues',
'_blank'
)
}
const handleButtonClick = () => {
// Close the conflict dialog
dialogStore.closeDialog({ key: 'global-node-conflict' })
// Execute the custom button action if provided
if (props.onButtonClick) {
props.onButtonClick()
}
}
</script>

View File

@@ -0,0 +1,12 @@
<template>
<div class="h-12 flex items-center justify-between w-full pl-6">
<div class="flex items-center gap-2">
<!-- Warning Icon -->
<i class="pi pi-exclamation-triangle text-lg"></i>
<!-- Title -->
<p class="text-base font-bold">
{{ $t('manager.conflicts.title') }}
</p>
</div>
</div>
</template>

View File

@@ -17,9 +17,10 @@
<script setup lang="ts">
import Message from 'primevue/message'
import { computed } from 'vue'
import { computed, inject } from 'vue'
import { components } from '@/types/comfyRegistryTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
type PackVersionStatus = components['schemas']['NodeVersionStatus']
type PackStatus = components['schemas']['NodeStatus']
@@ -32,10 +33,15 @@ type StatusProps = {
severity: MessageSeverity
}
const { statusType } = defineProps<{
const { statusType, hasCompatibilityIssues } = defineProps<{
statusType: Status
hasCompatibilityIssues?: boolean
}>()
// Inject import failed context from parent
const importFailedContext = inject(ImportFailedKey)
const importFailed = importFailedContext?.importFailed
const statusPropsMap: Record<Status, StatusProps> = {
NodeStatusActive: {
label: 'active',
@@ -71,10 +77,13 @@ const statusPropsMap: Record<Status, StatusProps> = {
}
}
const statusLabel = computed(
() => statusPropsMap[statusType]?.label || 'unknown'
)
const statusSeverity = computed(
() => statusPropsMap[statusType]?.severity || 'secondary'
)
const statusLabel = computed(() => {
if (importFailed?.value) return 'importFailed'
if (hasCompatibilityIssues) return 'conflicting'
return statusPropsMap[statusType]?.label || 'unknown'
})
const statusSeverity = computed(() => {
if (hasCompatibilityIssues || importFailed?.value) return 'error'
return statusPropsMap[statusType]?.severity || 'secondary'
})
</script>

View File

@@ -6,11 +6,18 @@ import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json'
import { SelectedVersion } from '@/types/comfyManagerTypes'
import PackVersionBadge from './PackVersionBadge.vue'
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
// Mock config to prevent __COMFYUI_FRONTEND_VERSION__ error
vi.mock('@/config', () => ({
default: {
app_title: 'ComfyUI',
app_version: '1.0.0'
}
}))
const mockNodePack = {
id: 'test-pack',
name: 'Test Pack',
@@ -120,7 +127,7 @@ describe('PackVersionBadge', () => {
const badge = wrapper.find('[role="button"]')
expect(badge.exists()).toBe(true)
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
expect(badge.find('span').text()).toBe('nightly')
})
it('falls back to NIGHTLY when nodePack.id is missing', () => {
@@ -134,7 +141,7 @@ describe('PackVersionBadge', () => {
const badge = wrapper.find('[role="button"]')
expect(badge.exists()).toBe(true)
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
expect(badge.find('span').text()).toBe('nightly')
})
it('toggles the popover when button is clicked', async () => {

View File

@@ -1,8 +1,8 @@
<template>
<div>
<div
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer px-2 py-1"
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700': fill }"
class="inline-flex items-center gap-1 rounded-2xl text-xs cursor-pointer py-1"
:class="{ 'bg-gray-100 dark-theme:bg-neutral-700 px-1.5': fill }"
aria-haspopup="true"
role="button"
tabindex="0"
@@ -12,17 +12,16 @@
>
<i
v-if="isUpdateAvailable"
class="pi pi-arrow-circle-up text-blue-600"
style="font-size: 8px"
class="pi pi-arrow-circle-up text-blue-600 text-xs"
/>
<span>{{ installedVersion }}</span>
<i class="pi pi-chevron-right" style="font-size: 8px" />
<i class="pi pi-chevron-right text-xxs" />
</div>
<Popover
ref="popoverRef"
:pt="{
content: { class: 'px-0' }
content: { class: 'p-0 shadow-lg' }
}"
>
<PackVersionSelectorPopover
@@ -42,8 +41,7 @@ import { computed, ref, watch } from 'vue'
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { SelectedVersion } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { isSemVer } from '@/utils/formatUtil'
const TRUNCATED_HASH_LENGTH = 7
@@ -64,11 +62,11 @@ const popoverRef = ref()
const managerStore = useComfyManagerStore()
const installedVersion = computed(() => {
if (!nodePack.id) return SelectedVersion.NIGHTLY
if (!nodePack.id) return 'nightly'
const version =
managerStore.installedPacks[nodePack.id]?.ver ??
nodePack.latest_version?.version ??
SelectedVersion.NIGHTLY
'nightly'
// If Git hash, truncate to 7 characters
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)

View File

@@ -3,18 +3,32 @@ import { createPinia } from 'pinia'
import Button from 'primevue/button'
import PrimeVue from 'primevue/config'
import Listbox from 'primevue/listbox'
import Select from 'primevue/select'
import Tooltip from 'primevue/tooltip'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
import enMessages from '@/locales/en/main.json'
import { SelectedVersion } from '@/types/comfyManagerTypes'
// SelectedVersion is now using direct strings instead of enum
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
// Default mock versions for reference
const defaultMockVersions = [
{ version: '1.0.0', createdAt: '2023-01-01' },
{
version: '1.0.0',
createdAt: '2023-01-01',
supported_os: ['windows', 'linux'],
supported_accelerators: ['CPU'],
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0',
supported_python_version: '>=3.8',
is_banned: false,
has_registry_data: true
},
{ version: '0.9.0', createdAt: '2022-12-01' },
{ version: '0.8.0', createdAt: '2022-11-01' }
]
@@ -22,13 +36,24 @@ const defaultMockVersions = [
const mockNodePack = {
id: 'test-pack',
name: 'Test Pack',
latest_version: { version: '1.0.0' },
repository: 'https://github.com/user/repo'
latest_version: {
version: '1.0.0',
supported_os: ['windows', 'linux'],
supported_accelerators: ['CPU'],
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0',
supported_python_version: '>=3.8',
is_banned: false,
has_registry_data: true
},
repository: 'https://github.com/user/repo',
has_registry_data: true
}
// Create mock functions
const mockGetPackVersions = vi.fn()
const mockInstallPack = vi.fn().mockResolvedValue(undefined)
const mockCheckNodeCompatibility = vi.fn()
// Mock the registry service
vi.mock('@/services/comfyRegistryService', () => ({
@@ -49,6 +74,13 @@ vi.mock('@/stores/comfyManagerStore', () => ({
}))
}))
// Mock the conflict detection composable
vi.mock('@/composables/useConflictDetection', () => ({
useConflictDetection: vi.fn(() => ({
checkNodeCompatibility: mockCheckNodeCompatibility
}))
}))
const waitForPromises = async () => {
await new Promise((resolve) => setTimeout(resolve, 16))
await nextTick()
@@ -59,6 +91,9 @@ describe('PackVersionSelectorPopover', () => {
vi.clearAllMocks()
mockGetPackVersions.mockReset()
mockInstallPack.mockReset().mockResolvedValue(undefined)
mockCheckNodeCompatibility
.mockReset()
.mockReturnValue({ hasConflict: false, conflicts: [] })
})
const mountComponent = ({
@@ -78,7 +113,12 @@ describe('PackVersionSelectorPopover', () => {
global: {
plugins: [PrimeVue, createPinia(), i18n],
components: {
Listbox
Listbox,
VerifiedIcon,
Select
},
directives: {
tooltip: Tooltip
}
}
})
@@ -120,14 +160,15 @@ describe('PackVersionSelectorPopover', () => {
const options = listbox.props('options')!
// Check that we have both special options and version options
expect(options.length).toBe(defaultMockVersions.length + 2) // 2 special options + version options
// Latest version (1.0.0) should be excluded from the version list to avoid duplication
expect(options.length).toBe(defaultMockVersions.length + 1) // 2 special options + version options minus 1 duplicate
// Check that special options exist
expect(options.some((o) => o.value === SelectedVersion.NIGHTLY)).toBe(true)
expect(options.some((o) => o.value === SelectedVersion.LATEST)).toBe(true)
expect(options.some((o) => o.value === 'nightly')).toBe(true)
expect(options.some((o) => o.value === 'latest')).toBe(true)
// Check that version options exist
expect(options.some((o) => o.value === '1.0.0')).toBe(true)
// Check that version options exist (excluding latest version 1.0.0)
expect(options.some((o) => o.value === '1.0.0')).toBe(false) // Should be excluded as it's the latest
expect(options.some((o) => o.value === '0.9.0')).toBe(true)
expect(options.some((o) => o.value === '0.8.0')).toBe(true)
})
@@ -304,7 +345,7 @@ describe('PackVersionSelectorPopover', () => {
await waitForPromises()
const listbox = wrapper.findComponent(Listbox)
expect(listbox.exists()).toBe(true)
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
expect(listbox.props('modelValue')).toBe('nightly')
})
it('defaults to nightly when publisher name is "Unclaimed"', async () => {
@@ -325,7 +366,343 @@ describe('PackVersionSelectorPopover', () => {
await waitForPromises()
const listbox = wrapper.findComponent(Listbox)
expect(listbox.exists()).toBe(true)
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
expect(listbox.props('modelValue')).toBe('nightly')
})
})
describe('version compatibility checking', () => {
it('shows warning icon for incompatible versions', async () => {
// Set up the mock for versions
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
// Mock compatibility check to return conflict for specific version
mockCheckNodeCompatibility.mockImplementation((versionData) => {
if (versionData.supported_os?.includes('linux')) {
return {
hasConflict: true,
conflicts: [
{
type: 'os',
current_value: 'windows',
required_value: 'linux'
}
]
}
}
return { hasConflict: false, conflicts: [] }
})
const nodePackWithCompatibility = {
...mockNodePack,
supported_os: ['linux'],
supported_accelerators: ['CUDA']
}
const wrapper = mountComponent({
props: { nodePack: nodePackWithCompatibility }
})
await waitForPromises()
// Check that compatibility checking function was called
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
// The warning icon should be shown for incompatible versions
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
expect(warningIcons.length).toBeGreaterThan(0)
})
it('shows verified icon for compatible versions', async () => {
// Set up the mock for versions
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
// Mock compatibility check to return no conflicts
mockCheckNodeCompatibility.mockReturnValue({
hasConflict: false,
conflicts: []
})
const wrapper = mountComponent()
await waitForPromises()
// Check that compatibility checking function was called
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
// The verified icon should be shown for compatible versions
// Look for the VerifiedIcon component or SVG elements
const verifiedIcons = wrapper.findAll('svg')
expect(verifiedIcons.length).toBeGreaterThan(0)
})
it('calls checkVersionCompatibility with correct version data', async () => {
// Set up the mock for versions with specific supported data
const versionsWithCompatibility = [
{
version: '1.0.0',
supported_os: ['windows', 'linux'],
supported_accelerators: ['CUDA', 'CPU'],
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0'
}
]
mockGetPackVersions.mockResolvedValueOnce(versionsWithCompatibility)
const nodePackWithCompatibility = {
...mockNodePack,
supported_os: ['windows'],
supported_accelerators: ['CPU'],
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0',
latest_version: {
version: '1.0.0',
supported_os: ['windows', 'linux'],
supported_accelerators: ['CPU'], // latest_version data takes precedence
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0',
supported_python_version: '>=3.8',
is_banned: false,
has_registry_data: true
}
}
const wrapper = mountComponent({
props: { nodePack: nodePackWithCompatibility }
})
await waitForPromises()
// Clear previous calls from component mounting/rendering
mockCheckNodeCompatibility.mockClear()
// Trigger compatibility check by accessing getVersionCompatibility
const vm = wrapper.vm as any
vm.getVersionCompatibility('1.0.0')
// Verify that checkNodeCompatibility was called with correct data
// Since 1.0.0 is the latest version, it should use latest_version data
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
supported_os: ['windows', 'linux'],
supported_accelerators: ['CPU'], // latest_version data takes precedence
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0',
supported_python_version: '>=3.8',
is_banned: false,
has_registry_data: true,
version: '1.0.0'
})
})
it('shows version conflict warnings for ComfyUI and frontend versions', async () => {
// Set up the mock for versions
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
// Mock compatibility check to return version conflicts
mockCheckNodeCompatibility.mockImplementation((versionData) => {
const conflicts = []
if (versionData.supported_comfyui_version) {
conflicts.push({
type: 'comfyui_version',
current_value: '0.5.0',
required_value: versionData.supported_comfyui_version
})
}
if (versionData.supported_comfyui_frontend_version) {
conflicts.push({
type: 'frontend_version',
current_value: '1.0.0',
required_value: versionData.supported_comfyui_frontend_version
})
}
return {
hasConflict: conflicts.length > 0,
conflicts
}
})
const nodePackWithVersionRequirements = {
...mockNodePack,
supported_comfyui_version: '>=1.0.0',
supported_comfyui_frontend_version: '>=2.0.0'
}
const wrapper = mountComponent({
props: { nodePack: nodePackWithVersionRequirements }
})
await waitForPromises()
// Check that compatibility checking function was called
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
// The warning icon should be shown for version incompatible packages
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
expect(warningIcons.length).toBeGreaterThan(0)
})
it('handles latest and nightly versions using nodePack data', async () => {
// Set up the mock for versions
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const nodePackWithCompatibility = {
...mockNodePack,
supported_os: ['windows'],
supported_accelerators: ['CPU'],
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0',
latest_version: {
...mockNodePack.latest_version,
supported_os: ['windows'], // Match nodePack data for test consistency
supported_accelerators: ['CPU'], // Match nodePack data for test consistency
supported_python_version: '>=3.8',
is_banned: false,
has_registry_data: true
}
}
const wrapper = mountComponent({
props: { nodePack: nodePackWithCompatibility }
})
await waitForPromises()
const vm = wrapper.vm as any
// Clear previous calls from component mounting/rendering
mockCheckNodeCompatibility.mockClear()
// Test latest version
vm.getVersionCompatibility('latest')
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
supported_os: ['windows'],
supported_accelerators: ['CPU'],
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0',
supported_python_version: '>=3.8',
is_banned: false,
has_registry_data: true,
version: '1.0.0'
})
// Clear for next test call
mockCheckNodeCompatibility.mockClear()
// Test nightly version
vm.getVersionCompatibility('nightly')
expect(mockCheckNodeCompatibility).toHaveBeenCalledWith({
id: 'test-pack',
name: 'Test Pack',
supported_os: ['windows'],
supported_accelerators: ['CPU'],
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0',
repository: 'https://github.com/user/repo',
has_registry_data: true,
latest_version: {
supported_os: ['windows'],
supported_accelerators: ['CPU'],
supported_python_version: '>=3.8',
is_banned: false,
has_registry_data: true,
version: '1.0.0',
supported_comfyui_version: '>=0.1.0',
supported_comfyui_frontend_version: '>=1.0.0'
}
})
})
it('shows banned package warnings', async () => {
// Set up the mock for versions
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
// Mock compatibility check to return banned conflicts
mockCheckNodeCompatibility.mockImplementation((versionData) => {
if (versionData.is_banned === true) {
return {
hasConflict: true,
conflicts: [
{
type: 'banned',
current_value: 'installed',
required_value: 'not_banned'
}
]
}
}
return { hasConflict: false, conflicts: [] }
})
const bannedNodePack = {
...mockNodePack,
is_banned: true,
latest_version: {
...mockNodePack.latest_version,
is_banned: true
}
}
const wrapper = mountComponent({
props: { nodePack: bannedNodePack }
})
await waitForPromises()
// Check that compatibility checking function was called
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
// Open the dropdown to see the options
const select = wrapper.find('.p-select')
if (!select.exists()) {
// Try alternative selector
const selectButton = wrapper.find('[aria-haspopup="listbox"]')
if (selectButton.exists()) {
await selectButton.trigger('click')
}
} else {
await select.trigger('click')
}
await wrapper.vm.$nextTick()
// The warning icon should be shown for banned packages in the dropdown options
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
expect(warningIcons.length).toBeGreaterThan(0)
})
it('shows security pending warnings', async () => {
// Set up the mock for versions
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
// Mock compatibility check to return security pending conflicts
mockCheckNodeCompatibility.mockImplementation((versionData) => {
if (versionData.has_registry_data === false) {
return {
hasConflict: true,
conflicts: [
{
type: 'pending',
current_value: 'no_registry_data',
required_value: 'registry_data_available'
}
]
}
}
return { hasConflict: false, conflicts: [] }
})
const securityPendingNodePack = {
...mockNodePack,
has_registry_data: false,
latest_version: {
...mockNodePack.latest_version,
has_registry_data: false
}
}
const wrapper = mountComponent({
props: { nodePack: securityPendingNodePack }
})
await waitForPromises()
// Check that compatibility checking function was called
expect(mockCheckNodeCompatibility).toHaveBeenCalled()
// The warning icon should be shown for security pending packages
const warningIcons = wrapper.findAll('.pi-exclamation-triangle')
expect(warningIcons.length).toBeGreaterThan(0)
})
})
})

View File

@@ -1,8 +1,10 @@
<template>
<div class="w-64 mt-2">
<span class="pl-3 text-muted text-md font-semibold opacity-70">
{{ $t('manager.selectVersion') }}
</span>
<div class="w-64 pt-1">
<div class="py-2">
<span class="pl-3 text-md font-semibold text-neutral-500">
{{ $t('manager.selectVersion') }}
</span>
</div>
<div
v-if="isLoadingVersions || isQueueing"
class="text-center text-muted py-4 flex flex-col items-center"
@@ -23,24 +25,44 @@
v-model="selectedVersion"
option-label="label"
option-value="value"
:options="versionOptions"
:options="processedVersionOptions"
:highlight-on-select="false"
class="my-3 w-full max-h-[50vh] border-none shadow-none"
class="w-full max-h-[50vh] border-none shadow-none rounded-md"
:pt="{
listContainer: { class: 'scrollbar-hide' }
}"
>
<template #option="slotProps">
<div class="flex justify-between items-center w-full p-1">
<span>{{ slotProps.option.label }}</span>
<div class="flex items-center gap-2">
<template v-if="slotProps.option.value === 'nightly'">
<div class="w-4"></div>
</template>
<template v-else>
<i
v-if="slotProps.option.hasConflict"
v-tooltip="{
value: slotProps.option.conflictMessage,
showDelay: 300
}"
class="pi pi-exclamation-triangle text-yellow-500"
/>
<VerifiedIcon v-else :size="20" class="relative right-0.5" />
</template>
<span>{{ slotProps.option.label }}</span>
</div>
<i
v-if="selectedVersion === slotProps.option.value"
v-if="slotProps.option.isSelected"
class="pi pi-check text-highlight"
/>
</div>
</template>
</Listbox>
<ContentDivider class="my-2" />
<div class="flex justify-end gap-2 p-1 px-3">
<div class="flex justify-end gap-2 py-1 px-3">
<Button
text
class="text-sm"
severity="secondary"
:label="$t('g.cancel')"
:disabled="isQueueing"
@@ -49,7 +71,7 @@
<Button
severity="secondary"
:label="$t('g.install')"
class="py-3 px-4 dark-theme:bg-unset bg-black/80 dark-theme:text-unset text-neutral-100 rounded-lg"
class="py-2.5 px-4 text-sm dark-theme:bg-unset bg-black/80 dark-theme:text-unset text-neutral-100 rounded-lg"
:disabled="isQueueing"
@click="handleSubmit"
/>
@@ -62,21 +84,42 @@ import { whenever } from '@vueuse/core'
import Button from 'primevue/button'
import Listbox from 'primevue/listbox'
import ProgressSpinner from 'primevue/progressspinner'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import {
ManagerChannel,
ManagerDatabaseSource,
SelectedVersion
} from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
import { isSemVer } from '@/utils/formatUtil'
type ManagerChannel = ManagerComponents['schemas']['ManagerChannel']
type ManagerDatabaseSource =
ManagerComponents['schemas']['ManagerDatabaseSource']
type SelectedVersion = ManagerComponents['schemas']['SelectedVersion']
// Enum values for runtime use
const SelectedVersionValues = {
LATEST: 'latest' as SelectedVersion,
NIGHTLY: 'nightly' as SelectedVersion
}
const ManagerChannelValues: Record<string, ManagerChannel> = {
DEFAULT: 'default', // ✅ Valid - will compile
DEV: 'dev' // ✅ Valid - will compile
}
const ManagerDatabaseSourceValues: Record<string, ManagerDatabaseSource> = {
CACHE: 'cache',
REMOTE: 'remote',
LOCAL: 'local'
}
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
@@ -89,22 +132,25 @@ const emit = defineEmits<{
const { t } = useI18n()
const registryService = useComfyRegistryService()
const managerStore = useComfyManagerStore()
const { checkNodeCompatibility } = useConflictDetection()
const isQueueing = ref(false)
const selectedVersion = ref<string>(SelectedVersion.LATEST)
const selectedVersion = ref<string>(SelectedVersionValues.LATEST)
onMounted(() => {
const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST
const initialVersion =
getInitialSelectedVersion() ?? SelectedVersionValues.LATEST
selectedVersion.value =
// Use NIGHTLY when version is a Git hash
isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY
isSemVer(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY
})
const getInitialSelectedVersion = () => {
if (!nodePack.id) return
// If unclaimed, set selected version to nightly
if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY
if (nodePack.publisher?.name === 'Unclaimed')
return SelectedVersionValues.NIGHTLY
// If node pack is installed, set selected version to the installed version
if (managerStore.isPackInstalled(nodePack.id))
@@ -126,6 +172,8 @@ const versionOptions = ref<
}[]
>([])
const fetchedVersions = ref<components['schemas']['NodeVersion'][]>([])
const isLoadingVersions = ref(false)
const onNodePackChange = async () => {
@@ -133,25 +181,34 @@ const onNodePackChange = async () => {
// Fetch versions from the registry
const versions = await fetchVersions()
fetchedVersions.value = versions
const latestVersionNumber = nodePack.latest_version?.version
const availableVersionOptions = versions
.map((version) => ({
value: version.version ?? '',
label: version.version ?? ''
}))
.filter((option) => option.value)
.filter((option) => option.value && option.value !== latestVersionNumber) // Exclude latest version from the list
// Add Latest option with actual version number
const latestLabel = latestVersionNumber
? `${t('manager.latestVersion')} (${latestVersionNumber})`
: t('manager.latestVersion')
// Add Latest option
const defaultVersions = [
{
value: SelectedVersion.LATEST,
label: t('manager.latestVersion')
value: SelectedVersionValues.LATEST,
label: latestLabel
}
]
// Add Nightly option if there is a non-empty `repository` field
if (nodePack.repository?.length) {
defaultVersions.push({
value: SelectedVersion.NIGHTLY,
value: SelectedVersionValues.NIGHTLY,
label: t('manager.nightlyVersion')
})
}
@@ -172,16 +229,86 @@ whenever(
const handleSubmit = async () => {
isQueueing.value = true
if (!nodePack.id) {
throw new Error('Node ID is required for installation')
}
// Convert 'latest' to actual version number for installation
const actualVersion =
selectedVersion.value === 'latest'
? nodePack.latest_version?.version ?? 'latest'
: selectedVersion.value
await managerStore.installPack.call({
id: nodePack.id,
repository: nodePack.repository ?? '',
channel: ManagerChannel.DEFAULT,
mode: ManagerDatabaseSource.CACHE,
version: selectedVersion.value,
channel: ManagerChannelValues.DEFAULT,
mode: ManagerDatabaseSourceValues.CACHE,
version: actualVersion,
selected_version: selectedVersion.value
})
isQueueing.value = false
emit('submit')
}
const getVersionData = (version: string) => {
const latestVersionNumber = nodePack.latest_version?.version
const useLatestVersionData =
version === 'latest' || version === latestVersionNumber
if (useLatestVersionData) {
const latestVersionData = nodePack.latest_version
return {
...latestVersionData
}
}
const versionData = fetchedVersions.value.find((v) => v.version === version)
if (versionData) {
return {
...versionData
}
}
// Fallback to nodePack data
return {
...nodePack
}
}
// Main function to get version compatibility info
const getVersionCompatibility = (version: string) => {
const versionData = getVersionData(version)
const compatibility = checkNodeCompatibility(versionData)
const conflictMessage = compatibility.hasConflict
? getJoinedConflictMessages(compatibility.conflicts, t)
: ''
return {
hasConflict: compatibility.hasConflict,
conflictMessage
}
}
// Helper to determine if an option is selected.
const isOptionSelected = (optionValue: string) => {
if (selectedVersion.value === optionValue) {
return true
}
if (
optionValue === 'latest' &&
selectedVersion.value === nodePack.latest_version?.version
) {
return true
}
return false
}
// Checks if an option is selected, treating 'latest' as an alias for the actual latest version number.
const processedVersionOptions = computed(() => {
return versionOptions.value.map((option) => {
const compatibility = getVersionCompatibility(option.value)
const isSelected = isOptionSelected(option.value)
return {
...option,
hasConflict: compatibility.hasConflict,
conflictMessage: compatibility.conflictMessage,
isSelected: isSelected
}
})
})
</script>

View File

@@ -1,53 +0,0 @@
<template>
<Button
outlined
class="!m-0 p-0 rounded-lg text-gray-900 dark-theme:text-gray-50"
:class="[
variant === 'black'
? 'bg-neutral-900 text-white border-neutral-900'
: 'border-neutral-700',
fullWidth ? 'w-full' : 'w-min-content'
]"
:disabled="loading"
v-bind="$attrs"
@click="onClick"
>
<span class="py-2 px-3 whitespace-nowrap">
<template v-if="loading">
{{ loadingMessage ?? $t('g.loading') }}
</template>
<template v-else>
{{ label }}
</template>
</span>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
const {
label,
loadingMessage,
fullWidth = false,
variant = 'default'
} = defineProps<{
label: string
loading?: boolean
loadingMessage?: string
fullWidth?: boolean
variant?: 'default' | 'black'
}>()
const emit = defineEmits<{
action: []
}>()
defineOptions({
inheritAttrs: false
})
const onClick = (): void => {
emit('action')
}
</script>

View File

@@ -12,9 +12,13 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import PackEnableToggle from './PackEnableToggle.vue'
// Mock debounce to execute immediately
vi.mock('es-toolkit/compat', () => ({
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
}))
vi.mock('es-toolkit/compat', async () => {
const actual = await vi.importActual('es-toolkit/compat')
return {
...actual,
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
}
})
const mockNodePack = {
id: 'test-pack',

View File

@@ -1,6 +1,26 @@
<template>
<div class="flex items-center">
<div class="flex items-center gap-2">
<div
v-if="hasConflict"
v-tooltip="{
value: $t('manager.conflicts.warningTooltip'),
showDelay: 300
}"
class="flex items-center justify-center w-6 h-6 cursor-pointer"
@click="showConflictModal(true)"
>
<i class="pi pi-exclamation-triangle text-yellow-500 text-xl"></i>
</div>
<ToggleSwitch
v-if="!canToggleDirectly"
:model-value="isEnabled"
:disabled="isLoading"
:readonly="!canToggleDirectly"
aria-label="Enable or disable pack"
@focus="handleToggleInteraction"
/>
<ToggleSwitch
v-else
:model-value="isEnabled"
:disabled="isLoading"
aria-label="Enable or disable pack"
@@ -8,57 +28,110 @@
/>
</div>
</template>
<script setup lang="ts">
import { debounce } from 'es-toolkit/compat'
import ToggleSwitch from 'primevue/toggleswitch'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import {
InstallPackParams,
ManagerChannel,
SelectedVersion
} from '@/types/comfyManagerTypes'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import type { components } from '@/types/comfyRegistryTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
const TOGGLE_DEBOUNCE_MS = 256
const { nodePack } = defineProps<{
const { nodePack, hasConflict } = defineProps<{
nodePack: components['schemas']['Node']
hasConflict?: boolean
}>()
const { t } = useI18n()
const { isPackEnabled, enablePack, disablePack, installedPacks } =
useComfyManagerStore()
const { getConflictsForPackageByID } = useConflictDetectionStore()
const { showNodeConflictDialog } = useDialogService()
const { acknowledgmentState, markConflictsAsSeen } = useConflictAcknowledgment()
const isLoading = ref(false)
const isEnabled = computed(() => isPackEnabled(nodePack.id))
const version = computed(() => {
const id = nodePack.id
if (!id) return SelectedVersion.NIGHTLY
if (!id) return 'nightly' as ManagerComponents['schemas']['SelectedVersion']
return (
installedPacks[id]?.ver ??
nodePack.latest_version?.version ??
SelectedVersion.NIGHTLY
('nightly' as ManagerComponents['schemas']['SelectedVersion'])
)
})
const handleEnable = () =>
enablePack.call({
id: nodePack.id,
version: version.value,
selected_version: version.value,
repository: nodePack.repository ?? '',
channel: ManagerChannel.DEFAULT,
mode: 'default' as InstallPackParams['mode']
})
const packageConflict = computed(() =>
getConflictsForPackageByID(nodePack.id || '')
)
const canToggleDirectly = computed(() => {
return !(
hasConflict &&
!acknowledgmentState.value.modal_dismissed &&
packageConflict.value
)
})
const handleDisable = () =>
disablePack({
const showConflictModal = (skipModalDismissed: boolean) => {
let modal_dismissed = acknowledgmentState.value.modal_dismissed
if (skipModalDismissed) modal_dismissed = false
if (packageConflict.value && !modal_dismissed) {
showNodeConflictDialog({
conflictedPackages: [packageConflict.value],
buttonText: !isEnabled.value
? t('manager.conflicts.enableAnyway')
: t('manager.conflicts.understood'),
onButtonClick: async () => {
if (!isEnabled.value) {
await handleEnable()
}
},
dialogComponentProps: {
onClose: () => {
markConflictsAsSeen()
}
}
})
}
}
const handleEnable = () => {
if (!nodePack.id) {
throw new Error('Node ID is required for enabling')
}
return enablePack.call({
id: nodePack.id,
version: version.value
version:
version.value ??
('latest' as ManagerComponents['schemas']['SelectedVersion']),
selected_version:
version.value ??
('latest' as ManagerComponents['schemas']['SelectedVersion']),
repository: nodePack.repository ?? '',
channel: 'default' as ManagerComponents['schemas']['ManagerChannel'],
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
skip_post_install: false
})
}
const handleDisable = () => {
if (!nodePack.id) {
throw new Error('Node ID is required for disabling')
}
return disablePack({
id: nodePack.id,
version:
version.value ??
('latest' as ManagerComponents['schemas']['SelectedVersion'])
})
}
const handleToggle = async (enable: boolean) => {
if (isLoading.value) return
@@ -67,10 +140,22 @@ const handleToggle = async (enable: boolean) => {
if (enable) {
await handleEnable()
} else {
handleDisable()
await handleDisable()
}
isLoading.value = false
}
const onToggle = debounce(handleToggle, TOGGLE_DEBOUNCE_MS, { trailing: true })
const onToggle = debounce(
(enable: boolean) => {
void handleToggle(enable)
},
TOGGLE_DEBOUNCE_MS,
{ trailing: true }
)
const handleToggleInteraction = async (event: Event) => {
if (!canToggleDirectly.value) {
event.preventDefault()
showConflictModal(false)
}
}
</script>

View File

@@ -1,59 +1,87 @@
<template>
<PackActionButton
<IconTextButton
v-bind="$attrs"
:label="
label ??
(nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install'))
"
:severity="variant === 'black' ? undefined : 'secondary'"
:variant="variant"
:loading="isInstalling"
:loading-message="$t('g.installing')"
@action="installAllPacks"
@click="onClick"
/>
type="transparent"
:label="computedLabel"
:border="true"
:size="size"
:disabled="isLoading || isInstalling"
@click="installAllPacks"
>
<template #icon>
<i
v-if="hasConflict && !isInstalling && !isLoading"
class="pi pi-exclamation-triangle text-yellow-500"
/>
<DotSpinner
v-else-if="isLoading || isInstalling"
duration="1s"
:size="size === 'sm' ? 12 : 16"
/>
</template>
</IconTextButton>
</template>
<script setup lang="ts">
import { inject, ref } from 'vue'
import { computed } from 'vue'
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { t } from '@/i18n'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import {
IsInstallingKey,
ManagerChannel,
ManagerDatabaseSource,
SelectedVersion
} from '@/types/comfyManagerTypes'
import { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import {
type ConflictDetail,
ConflictDetectionResult
} from '@/types/conflictDetectionTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
type NodePack = components['schemas']['Node']
const { nodePacks, variant, label } = defineProps<{
const {
nodePacks,
isLoading = false,
label = 'Install',
size = 'sm',
hasConflict,
conflictInfo
} = defineProps<{
nodePacks: NodePack[]
variant?: 'default' | 'black'
isLoading?: boolean
label?: string
size?: ButtonSize
hasConflict?: boolean
conflictInfo?: ConflictDetail[]
}>()
const isInstalling = inject(IsInstallingKey, ref(false))
const onClick = (): void => {
isInstalling.value = true
}
const managerStore = useComfyManagerStore()
const { showNodeConflictDialog } = useDialogService()
// Check if any of the packs are currently being installed
const isInstalling = computed(() => {
if (!nodePacks?.length) return false
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
})
const createPayload = (installItem: NodePack) => {
if (!installItem.id) {
throw new Error('Node ID is required for installation')
}
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
const versionToInstall = isUnclaimedPack
? SelectedVersion.NIGHTLY
: installItem.latest_version?.version ?? SelectedVersion.LATEST
? ('nightly' as ManagerComponents['schemas']['SelectedVersion'])
: installItem.latest_version?.version ??
('latest' as ManagerComponents['schemas']['SelectedVersion'])
return {
id: installItem.id,
repository: installItem.repository ?? '',
channel: ManagerChannel.DEV,
mode: ManagerDatabaseSource.CACHE,
channel: 'dev' as ManagerComponents['schemas']['ManagerChannel'],
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
selected_version: versionToInstall,
version: versionToInstall
}
@@ -65,14 +93,54 @@ const installPack = (item: NodePack) =>
const installAllPacks = async () => {
if (!nodePacks?.length) return
isInstalling.value = true
if (hasConflict && conflictInfo) {
// Check each package individually for conflicts
const { checkNodeCompatibility } = useConflictDetection()
const conflictedPackages: ConflictDetectionResult[] = nodePacks
.map((pack) => {
const compatibilityCheck = checkNodeCompatibility(pack)
return {
package_id: pack.id || '',
package_name: pack.name || '',
has_conflict: compatibilityCheck.hasConflict,
conflicts: compatibilityCheck.conflicts,
is_compatible: !compatibilityCheck.hasConflict
}
})
.filter((result) => result.has_conflict) // Only show packages with conflicts
showNodeConflictDialog({
conflictedPackages,
buttonText: t('manager.conflicts.installAnyway'),
onButtonClick: async () => {
// Proceed with installation of uninstalled packages
const uninstalledPacks = nodePacks.filter(
(pack) => !managerStore.isPackInstalled(pack.id)
)
if (!uninstalledPacks.length) return
await performInstallation(uninstalledPacks)
}
})
return
}
// No conflicts or conflicts acknowledged - proceed with installation
const uninstalledPacks = nodePacks.filter(
(pack) => !managerStore.isPackInstalled(pack.id)
)
if (!uninstalledPacks.length) return
await performInstallation(uninstalledPacks)
}
await Promise.all(uninstalledPacks.map(installPack))
const performInstallation = async (packs: NodePack[]) => {
await Promise.all(packs.map(installPack))
managerStore.installPack.clear()
}
const computedLabel = computed(() =>
isInstalling.value
? t('g.installing')
: label ??
(nodePacks.length > 1 ? t('manager.installSelected') : t('g.install'))
)
</script>

View File

@@ -1,35 +1,45 @@
<template>
<PackActionButton
<IconTextButton
v-bind="$attrs"
type="transparent"
:label="
nodePacks.length > 1
? $t('manager.uninstallSelected')
: $t('manager.uninstall')
"
severity="danger"
:loading-message="$t('manager.uninstalling')"
@action="uninstallItems"
:border="true"
:size="size"
class="border-red-500"
@click="uninstallItems"
/>
</template>
<script setup lang="ts">
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { ManagerPackInfo } from '@/types/comfyManagerTypes'
import { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
type NodePack = components['schemas']['Node']
const { nodePacks } = defineProps<{
const { nodePacks, size } = defineProps<{
nodePacks: NodePack[]
size?: ButtonSize
}>()
const managerStore = useComfyManagerStore()
const createPayload = (uninstallItem: NodePack): ManagerPackInfo => {
const createPayload = (
uninstallItem: NodePack
): ManagerComponents['schemas']['ManagerPackInfo'] => {
if (!uninstallItem.id) {
throw new Error('Node ID is required for uninstallation')
}
return {
id: uninstallItem.id,
version: uninstallItem.latest_version?.version
version: uninstallItem.latest_version?.version || 'unknown'
}
}

View File

@@ -0,0 +1,79 @@
<template>
<IconTextButton
v-bind="$attrs"
type="transparent"
:label="$t('manager.updateAll')"
:border="true"
size="sm"
:disabled="isUpdating"
@click="updateAllPacks"
>
<template v-if="isUpdating" #icon>
<DotSpinner duration="1s" :size="12" />
</template>
</IconTextButton>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
type NodePack = components['schemas']['Node']
const { nodePacks } = defineProps<{
nodePacks: NodePack[]
}>()
const isUpdating = ref<boolean>(false)
const managerStore = useComfyManagerStore()
const createPayload = (updateItem: NodePack) => {
return {
id: updateItem.id!,
version: updateItem.latest_version!.version!
}
}
const updatePack = async (item: NodePack) => {
if (!item.id || !item.latest_version?.version) {
console.warn('Pack missing required id or version:', item)
return
}
await managerStore.updatePack.call(createPayload(item))
}
const updateAllPacks = async () => {
if (!nodePacks?.length) {
console.warn('No packs provided for update')
return
}
isUpdating.value = true
const updatablePacks = nodePacks.filter((pack) =>
managerStore.isPackInstalled(pack.id)
)
if (!updatablePacks.length) {
console.info('No installed packs available for update')
isUpdating.value = false
return
}
console.info(`Starting update of ${updatablePacks.length} packs`)
try {
await Promise.all(updatablePacks.map(updatePack))
managerStore.updatePack.clear()
console.info('All packs updated successfully')
} catch (error) {
console.error('Pack update failed:', error)
console.error(
'Failed packs info:',
updatablePacks.map((p) => p.id)
)
} finally {
isUpdating.value = false
}
}
</script>

View File

@@ -2,20 +2,26 @@
<template v-if="nodePack">
<div class="flex flex-col h-full z-40 overflow-hidden relative">
<div class="top-0 z-10 px-6 pt-6 w-full">
<InfoPanelHeader :node-packs="[nodePack]" />
<InfoPanelHeader
:node-packs="[nodePack]"
:has-conflict="hasCompatibilityIssues"
/>
</div>
<div
ref="scrollContainer"
class="p-6 pt-2 overflow-y-auto flex-1 text-sm hidden-scrollbar"
class="p-6 pt-2 overflow-y-auto flex-1 text-sm scrollbar-hide"
>
<div class="mb-6">
<MetadataRow
v-if="isPackInstalled(nodePack.id)"
v-if="!importFailed && isPackInstalled(nodePack.id)"
:label="t('manager.filter.enabled')"
class="flex"
style="align-items: center"
>
<PackEnableToggle :node-pack="nodePack" />
<PackEnableToggle
:node-pack="nodePack"
:has-conflict="hasCompatibilityIssues"
/>
</MetadataRow>
<MetadataRow
v-for="item in infoItems"
@@ -29,6 +35,7 @@
:status-type="
nodePack.status as components['schemas']['NodeVersionStatus']
"
:has-compatibility-issues="hasCompatibilityIssues"
/>
</MetadataRow>
<MetadataRow :label="t('manager.version')">
@@ -36,7 +43,11 @@
</MetadataRow>
</div>
<div class="mb-6 overflow-hidden">
<InfoTabs :node-pack="nodePack" />
<InfoTabs
:node-pack="nodePack"
:has-compatibility-issues="hasCompatibilityIssues"
:conflict-result="conflictResult"
/>
</div>
</div>
</div>
@@ -59,9 +70,14 @@ import PackEnableToggle from '@/components/dialog/content/manager/button/PackEna
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
import InfoTabs from '@/components/dialog/content/manager/infoPanel/InfoTabs.vue'
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
interface InfoItem {
key: string
@@ -75,18 +91,55 @@ const { nodePack } = defineProps<{
const scrollContainer = ref<HTMLElement | null>(null)
const managerStore = useComfyManagerStore()
const isInstalled = computed(() => managerStore.isPackInstalled(nodePack.id))
const { isPackInstalled } = useComfyManagerStore()
const isInstalled = computed(() => isPackInstalled(nodePack.id))
const isInstalling = ref(false)
provide(IsInstallingKey, isInstalling)
whenever(isInstalled, () => {
isInstalling.value = false
})
const { isPackInstalled } = useComfyManagerStore()
const { checkNodeCompatibility } = useConflictDetection()
const { getConflictsForPackageByID } = useConflictDetectionStore()
const { t, d, n } = useI18n()
// Check compatibility once and pass to children
const conflictResult = computed((): ConflictDetectionResult | null => {
// For installed packages, use stored conflict data
if (isInstalled.value && nodePack.id) {
return getConflictsForPackageByID(nodePack.id) || null
}
// For non-installed packages, perform compatibility check
const compatibility = checkNodeCompatibility(nodePack)
if (compatibility.hasConflict) {
return {
package_id: nodePack.id || '',
package_name: nodePack.name || '',
has_conflict: true,
conflicts: compatibility.conflicts,
is_compatible: false
}
}
return null
})
const hasCompatibilityIssues = computed(() => {
return conflictResult.value?.has_conflict
})
const packageId = computed(() => nodePack.id || '')
const { importFailed, showImportFailedDialog } =
useImportFailedDetection(packageId)
provide(ImportFailedKey, {
importFailed,
showImportFailedDialog
})
const infoItems = computed<InfoItem[]>(() => [
{
key: 'publisher',
@@ -128,17 +181,3 @@ whenever(
{ immediate: true }
)
</script>
<style scoped>
.hidden-scrollbar {
/* Firefox */
scrollbar-width: none;
&::-webkit-scrollbar {
width: 1px;
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
}
</style>

View File

@@ -1,28 +1,39 @@
<template>
<div v-if="nodePacks?.length" class="flex flex-col items-center mb-6">
<div v-if="nodePacks?.length" class="flex flex-col items-center">
<slot name="thumbnail">
<PackIcon :node-pack="nodePacks[0]" width="24" height="24" />
<PackIcon :node-pack="nodePacks[0]" width="204" height="106" />
</slot>
<h2
class="text-2xl font-bold text-center mt-4 mb-2"
style="word-break: break-all"
>
<slot name="title">
{{ nodePacks[0].name }}
<span class="inline-block text-base">{{ nodePacks[0].name }}</span>
</slot>
</h2>
<div class="mt-2 mb-4 w-full max-w-xs flex justify-center">
<div
v-if="!importFailed"
class="mt-2 mb-4 w-full max-w-xs flex justify-center"
>
<slot name="install-button">
<PackUninstallButton
v-if="isAllInstalled"
v-bind="$attrs"
size="md"
:node-packs="nodePacks"
/>
<PackInstallButton v-else v-bind="$attrs" :node-packs="nodePacks" />
<PackInstallButton
v-else
v-bind="$attrs"
size="md"
:node-packs="nodePacks"
:has-conflict="hasConflict || computedHasConflict"
:conflict-info="conflictInfo"
/>
</slot>
</div>
</div>
<div v-else class="flex flex-col items-center mb-6">
<div v-else class="flex flex-col items-center">
<NoResultsPlaceholder
:message="$t('manager.status.unknown')"
:title="$t('manager.tryAgainLater')"
@@ -31,21 +42,29 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
const { nodePacks } = defineProps<{
const { nodePacks, hasConflict } = defineProps<{
nodePacks: components['schemas']['Node'][]
hasConflict?: boolean
}>()
const managerStore = useComfyManagerStore()
// Inject import failed context from parent
const importFailedContext = inject(ImportFailedKey)
const importFailed = importFailedContext?.importFailed
const isAllInstalled = ref(false)
watch(
[() => nodePacks, () => managerStore.installedPacks],
@@ -56,4 +75,23 @@ watch(
},
{ immediate: true }
)
// Add conflict detection for install button dialog
const { checkNodeCompatibility } = useConflictDetection()
// Compute conflict info for all node packs
const conflictInfo = computed<ConflictDetail[]>(() => {
if (!nodePacks?.length) return []
const allConflicts: ConflictDetail[] = []
for (const nodePack of nodePacks) {
const compatibilityCheck = checkNodeCompatibility(nodePack)
if (compatibilityCheck.conflicts) {
allConflicts.push(...compatibilityCheck.conflicts)
}
}
return allConflicts
})
const computedHasConflict = computed(() => conflictInfo.value.length > 0)
</script>

View File

@@ -6,16 +6,40 @@
<PackIconStacked :node-packs="nodePacks" />
</template>
<template #title>
{{ nodePacks.length }}
{{ $t('manager.packsSelected') }}
<div class="mt-5">
<span class="inline-block mr-2 text-blue-500 text-base">{{
nodePacks.length
}}</span>
<span class="text-base">{{ $t('manager.packsSelected') }}</span>
</div>
</template>
<template #install-button>
<PackInstallButton :full-width="true" :node-packs="nodePacks" />
<!-- Mixed: Don't show any button -->
<div v-if="isMixed" class="text-sm text-neutral-500">
{{ $t('manager.mixedSelectionMessage') }}
</div>
<!-- All installed: Show uninstall button -->
<PackUninstallButton
v-else-if="isAllInstalled"
size="md"
:node-packs="installedPacks"
/>
<!-- None installed: Show install button -->
<PackInstallButton
v-else-if="isNoneInstalled"
size="md"
:node-packs="notInstalledPacks"
:has-conflict="hasConflicts"
:conflict-info="conflictInfo"
/>
</template>
</InfoPanelHeader>
<div class="mb-6">
<MetadataRow :label="$t('g.status')">
<PackStatusMessage status-type="NodeVersionStatusActive" />
<PackStatusMessage
:status-type="overallStatus"
:has-compatibility-issues="hasConflicts"
/>
</MetadataRow>
<MetadataRow
:label="$t('manager.totalNodes')"
@@ -31,22 +55,80 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import { computed, onUnmounted } from 'vue'
import { computed, onUnmounted, provide, toRef } from 'vue'
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
import { usePacksSelection } from '@/composables/nodePack/usePacksSelection'
import { usePacksStatus } from '@/composables/nodePack/usePacksStatus'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][]
}>()
const nodePacksRef = toRef(() => nodePacks)
// Use new composables for cleaner code
const {
installedPacks,
notInstalledPacks,
isAllInstalled,
isNoneInstalled,
isMixed
} = usePacksSelection(nodePacksRef)
const { hasImportFailed, overallStatus } = usePacksStatus(nodePacksRef)
const { checkNodeCompatibility } = useConflictDetection()
const { getNodeDefs } = useComfyRegistryStore()
// Provide import failed context for PackStatusMessage
provide(ImportFailedKey, {
importFailed: hasImportFailed,
showImportFailedDialog: () => {} // No-op for multi-selection
})
// Check for conflicts in not-installed packages - keep original logic but simplified
const packageConflicts = computed(() => {
const conflictsByPackage = new Map<string, ConflictDetail[]>()
for (const pack of notInstalledPacks.value) {
const compatibilityCheck = checkNodeCompatibility(pack)
if (compatibilityCheck.hasConflict && pack.id) {
conflictsByPackage.set(pack.id, compatibilityCheck.conflicts)
}
}
return conflictsByPackage
})
// Aggregate all unique conflicts for display
const conflictInfo = computed<ConflictDetail[]>(() => {
const conflictMap = new Map<string, ConflictDetail>()
packageConflicts.value.forEach((conflicts) => {
conflicts.forEach((conflict) => {
const key = `${conflict.type}-${conflict.current_value}-${conflict.required_value}`
if (!conflictMap.has(key)) {
conflictMap.set(key, conflict)
}
})
})
return Array.from(conflictMap.values())
})
const hasConflicts = computed(() => conflictInfo.value.length > 0)
const getPackNodes = async (pack: components['schemas']['Node']) => {
if (!pack.latest_version?.version) return []
const nodeDefs = await getNodeDefs.call({

View File

@@ -1,15 +1,31 @@
<template>
<div class="overflow-hidden">
<Tabs :value="activeTab">
<TabList>
<Tab value="description">
<TabList class="overflow-x-auto scrollbar-hide">
<Tab v-if="hasCompatibilityIssues" value="warning" class="p-2 mr-6">
<div class="flex items-center gap-1">
<span></span>
{{ importFailed ? $t('g.error') : $t('g.warning') }}
</div>
</Tab>
<Tab value="description" class="p-2 mr-6">
{{ $t('g.description') }}
</Tab>
<Tab value="nodes">
<Tab value="nodes" class="p-2">
{{ $t('g.nodes') }}
</Tab>
</TabList>
<TabPanels class="overflow-auto">
<TabPanels class="overflow-auto py-4 px-2">
<TabPanel
v-if="hasCompatibilityIssues"
value="warning"
class="bg-transparent"
>
<WarningTabPanel
:node-pack="nodePack"
:conflict-result="conflictResult"
/>
</TabPanel>
<TabPanel value="description">
<DescriptionTabPanel :node-pack="nodePack" />
</TabPanel>
@@ -27,16 +43,25 @@ import TabList from 'primevue/tablist'
import TabPanel from 'primevue/tabpanel'
import TabPanels from 'primevue/tabpanels'
import Tabs from 'primevue/tabs'
import { computed, ref } from 'vue'
import { computed, inject, ref, watchEffect } from 'vue'
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
import WarningTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/WarningTabPanel.vue'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
const { nodePack } = defineProps<{
const { nodePack, hasCompatibilityIssues, conflictResult } = defineProps<{
nodePack: components['schemas']['Node']
hasCompatibilityIssues?: boolean
conflictResult?: ConflictDetectionResult | null
}>()
// Inject import failed context from parent
const importFailedContext = inject(ImportFailedKey)
const importFailed = importFailedContext?.importFailed
const nodeNames = computed(() => {
// @ts-expect-error comfy_nodes is an Algolia-specific field
const { comfy_nodes } = nodePack
@@ -44,4 +69,17 @@ const nodeNames = computed(() => {
})
const activeTab = ref('description')
// Watch for compatibility issues and automatically switch to warning tab
watchEffect(
() => {
if (hasCompatibilityIssues) {
activeTab.value = 'warning'
} else if (activeTab.value === 'warning') {
// If currently on warning tab but no issues, switch to description
activeTab.value = 'description'
}
},
{ flush: 'post' }
)
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col gap-4 text-sm">
<div v-for="(section, index) in sections" :key="index" class="mb-4">
<div class="mb-1">
<div class="mb-3">
{{ section.title }}
</div>
<div class="text-muted break-words">

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex py-1.5 text-xs">
<div class="w-1/3 truncate pr-2 text-muted">{{ label }}:</div>
<div class="w-1/3 truncate pr-2 text-muted">{{ label }}</div>
<div class="w-2/3">
<slot>{{ value }}</slot>
</div>

View File

@@ -1,5 +1,5 @@
<template>
<div class="mt-4 overflow-hidden">
<div class="overflow-hidden">
<InfoTextSection
v-if="nodePack?.description"
:sections="descriptionSections"

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col gap-4 mt-4 text-sm">
<div class="flex flex-col gap-4 text-sm">
<template v-if="mappedNodeDefs?.length">
<div
v-for="nodeDef in mappedNodeDefs"

View File

@@ -0,0 +1,43 @@
<template>
<div class="flex flex-col gap-3">
<button
v-if="importFailedInfo"
class="cursor-pointer outline-none border-none inline-flex items-center justify-end bg-transparent gap-1"
@click="showImportFailedDialog"
>
<i class="pi pi-code text-base"></i>
<span class="dark-theme:text-white text-sm">{{
t('serverStart.openLogs')
}}</span>
</button>
<div
v-for="(conflict, index) in conflictResult?.conflicts || []"
:key="index"
class="p-3 bg-yellow-800/20 rounded-md"
>
<div class="flex justify-between items-center">
<div class="text-sm break-words flex-1">
{{ getConflictMessage(conflict, $t) }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useImportFailedDetection } from '@/composables/useImportFailedDetection'
import { t } from '@/i18n'
import { components } from '@/types/comfyRegistryTypes'
import { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { getConflictMessage } from '@/utils/conflictMessageUtil'
const { nodePack, conflictResult } = defineProps<{
nodePack: components['schemas']['Node']
conflictResult: ConflictDetectionResult | null | undefined
}>()
const packageId = computed(() => nodePack?.id || '')
const { importFailedInfo, showImportFailedDialog } =
useImportFailedDetection(packageId)
</script>

View File

@@ -21,73 +21,58 @@
<PackBanner :node-pack="nodePack" />
</template>
<template #content>
<template v-if="isInstalling">
<div
class="self-stretch inline-flex flex-col justify-center items-center gap-2 h-full"
>
<ProgressSpinner />
<div
class="self-stretch text-center justify-start text-sm font-medium leading-none"
<div class="pt-4 px-4 pb-3 w-full h-full">
<div class="flex flex-col gap-y-1 w-full h-full">
<span
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
>
{{ $t('g.installing') }}...
</div>
</div>
</template>
<template v-else>
<div class="pt-4 px-4 pb-3 w-full h-full">
<div class="flex flex-col gap-y-1 w-full h-full">
<span
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
>
{{ nodePack.name }}
</span>
<p
v-if="nodePack.description"
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
>
{{ nodePack.description }}
</p>
<div class="flex flex-col gap-y-2">
<div class="flex-1 flex items-center gap-2">
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
{{ nodesCount }} {{ $t('g.nodes') }}
</div>
<PackVersionBadge
:node-pack="nodePack"
:is-selected="isSelected"
:fill="false"
/>
<div
v-if="formattedLatestVersionDate"
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
>
{{ formattedLatestVersionDate }}
</div>
{{ nodePack.name }}
</span>
<p
v-if="nodePack.description"
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
>
{{ nodePack.description }}
</p>
<div class="flex flex-col gap-y-2">
<div class="flex-1 flex items-center gap-2">
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
{{ nodesCount }} {{ $t('g.nodes') }}
</div>
<div class="flex">
<span
v-if="publisherName"
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
>
{{ publisherName }}
</span>
<PackVersionBadge
:node-pack="nodePack"
:is-selected="isSelected"
:fill="false"
:class="isInstalling ? 'pointer-events-none' : ''"
/>
<div
v-if="formattedLatestVersionDate"
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
>
{{ formattedLatestVersionDate }}
</div>
</div>
<div class="flex">
<span
v-if="publisherName"
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
>
{{ publisherName }}
</span>
</div>
</div>
</div>
</template>
</div>
</template>
<template #footer>
<PackCardFooter :node-pack="nodePack" />
<PackCardFooter :node-pack="nodePack" :is-installing="isInstalling" />
</template>
</Card>
</template>
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import Card from 'primevue/card'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, provide, ref } from 'vue'
import { computed, provide } from 'vue'
import { useI18n } from 'vue-i18n'
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
@@ -114,18 +99,17 @@ const isLightTheme = computed(
() => colorPaletteStore.completedActivePalette.light_theme
)
const isInstalling = ref(false)
provide(IsInstallingKey, isInstalling)
const { isPackInstalled, isPackEnabled, isPackInstalling } =
useComfyManagerStore()
const { isPackInstalled, isPackEnabled } = useComfyManagerStore()
const isInstalling = computed(() => isPackInstalling(nodePack?.id))
provide(IsInstallingKey, isInstalling)
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
const isDisabled = computed(
() => isInstalled.value && !isPackEnabled(nodePack?.id)
)
whenever(isInstalled, () => (isInstalling.value = false))
const nodesCount = computed(() =>
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
)

View File

@@ -6,19 +6,28 @@
<i class="pi pi-download text-muted"></i>
<span>{{ formattedDownloads }}</span>
</div>
<PackInstallButton v-if="!isInstalled" :node-packs="[nodePack]" />
<PackInstallButton
v-if="!isInstalled"
:node-packs="[nodePack]"
:is-installing="isInstalling"
:has-conflict="hasConflicts"
:conflict-info="conflictInfo"
/>
<PackEnableToggle v-else :node-pack="nodePack" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
@@ -26,10 +35,23 @@ const { nodePack } = defineProps<{
const { isPackInstalled } = useComfyManagerStore()
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
const isInstalling = inject(IsInstallingKey)
const { n } = useI18n()
const formattedDownloads = computed(() =>
nodePack.downloads ? n(nodePack.downloads) : ''
)
// Add conflict detection for the card button
const { checkNodeCompatibility } = useConflictDetection()
// Check for conflicts with this specific node pack
const conflictInfo = computed<ConflictDetail[]>(() => {
if (!nodePack) return []
const compatibilityCheck = checkNodeCompatibility(nodePack)
return compatibilityCheck.conflicts || []
})
const hasConflicts = computed(() => conflictInfo.value.length > 0)
</script>

View File

@@ -1,11 +1,37 @@
<template>
<img
:src="isImageError ? DEFAULT_ICON : imgSrc"
:alt="nodePack.name + ' icon'"
class="object-contain rounded-lg max-h-72 max-w-72"
:style="{ width: cssWidth, height: cssHeight }"
@error="isImageError = true"
/>
<div class="w-full max-w-[204] aspect-[2/1] rounded-lg overflow-hidden">
<!-- default banner show -->
<div v-if="showDefaultBanner" class="w-full h-full">
<img
:src="DEFAULT_BANNER"
alt="default banner"
class="w-full h-full object-cover"
/>
</div>
<!-- banner_url or icon show -->
<div v-else class="relative w-full h-full">
<!-- blur background -->
<div
v-if="imgSrc"
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{
backgroundImage: `url(${imgSrc})`,
filter: 'blur(10px)'
}"
></div>
<!-- image -->
<img
:src="isImageError ? DEFAULT_BANNER : imgSrc"
:alt="nodePack.name + ' banner'"
:class="
isImageError
? 'relative w-full h-full object-cover z-10'
: 'relative w-full h-full object-contain z-10'
"
@error="isImageError = true"
/>
</div>
</div>
</template>
<script setup lang="ts">
@@ -13,29 +39,14 @@ import { computed, ref } from 'vue'
import { components } from '@/types/comfyRegistryTypes'
const DEFAULT_ICON = '/assets/images/fallback-gradient-avatar.svg'
const DEFAULT_BANNER = '/assets/images/fallback-gradient-avatar.svg'
const {
nodePack,
width = '4.5rem',
height = '4.5rem'
} = defineProps<{
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
width?: string
height?: string
}>()
const isImageError = ref(false)
const shouldShowFallback = computed(
() => !nodePack.icon || nodePack.icon.trim() === '' || isImageError.value
)
const imgSrc = computed(() =>
shouldShowFallback.value ? DEFAULT_ICON : nodePack.icon
)
const convertToCssValue = (value: string | number) =>
typeof value === 'number' ? `${value}rem` : value
const cssWidth = computed(() => convertToCssValue(width))
const cssHeight = computed(() => convertToCssValue(height))
const showDefaultBanner = computed(() => !nodePack.banner_url && !nodePack.icon)
const imgSrc = computed(() => nodePack.banner_url || nodePack.icon)
</script>

View File

@@ -1,25 +1,19 @@
<template>
<div class="relative w-24 h-24">
<div class="relative w-[224px] h-[104px] shadow-xl">
<div
v-for="(pack, index) in nodePacks.slice(0, maxVisible)"
:key="pack.id"
class="absolute"
class="absolute w-[210px] h-[90px]"
:style="{
bottom: `${index * offset}px`,
right: `${index * offset}px`,
zIndex: maxVisible - index
}"
>
<div class="border rounded-lg p-0.5">
<PackIcon :node-pack="pack" width="4.5rem" height="4.5rem" />
<div class="border rounded-lg shadow-lg p-0.5">
<PackIcon :node-pack="pack" />
</div>
</div>
<div
v-if="nodePacks.length > maxVisible"
class="absolute -top-2 -right-2 bg-primary rounded-full w-7 h-7 flex items-center justify-center text-xs font-bold shadow-md z-10"
>
+{{ nodePacks.length - maxVisible }}
</div>
</div>
</template>

View File

@@ -28,11 +28,14 @@
</div>
<PackInstallButton
v-if="isMissingTab && missingNodePacks.length > 0"
variant="black"
:disabled="isLoading || !!error"
:node-packs="missingNodePacks"
:label="$t('manager.installAllMissingNodes')"
/>
<PackUpdateButton
v-if="isUpdateAvailableTab && hasUpdateAvailable"
:node-packs="updateAvailableNodePacks"
/>
</div>
<div class="flex mt-3 text-sm">
<div class="flex gap-6 ml-1">
@@ -65,8 +68,10 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import PackUpdateButton from '@/components/dialog/content/manager/button/PackUpdateButton.vue'
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
import {
type SearchOption,
SortableAlgoliaField
@@ -83,6 +88,7 @@ const { searchResults, sortOptions } = defineProps<{
suggestions?: QuerySuggestion[]
sortOptions?: SortableField[]
isMissingTab?: boolean
isUpdateAvailableTab?: boolean
}>()
const searchQuery = defineModel<string>('searchQuery')
@@ -96,6 +102,10 @@ const { t } = useI18n()
// Get missing node packs from workflow with loading and error states
const { missingNodePacks, isLoading, error } = useMissingNodes()
// Use the composable to get update available nodes
const { hasUpdateAvailable, updateAvailableNodePacks } =
useUpdateAvailableNodes()
const hasResults = computed(
() => searchQuery.value?.trim() && searchResults?.length
)

View File

@@ -112,12 +112,12 @@ import Divider from 'primevue/divider'
import Skeleton from 'primevue/skeleton'
import TabPanel from 'primevue/tabpanel'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import UserCredit from '@/components/common/UserCredit.vue'
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'
@@ -128,10 +128,10 @@ interface CreditHistoryItemData {
isPositive: boolean
}
const { t } = useI18n()
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
@@ -160,15 +160,8 @@ const handleCreditsHistoryClick = async () => {
await authActions.accessBillingPortal()
}
const handleMessageSupport = () => {
dialogService.showIssueReportDialog({
title: t('issueReport.contactSupportTitle'),
subtitle: t('issueReport.contactSupportDescription'),
panelProps: {
errorType: 'BillingSupport',
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
}
})
const handleMessageSupport = async () => {
await commandStore.execute('Comfy.ContactSupport')
}
const handleFaqClick = () => {

View File

@@ -1,49 +1,53 @@
<template>
<div
class="w-full px-6 py-4 shadow-lg flex items-center justify-between"
class="w-full px-6 py-2 shadow-lg flex items-center justify-between"
:class="{
'rounded-t-none': progressDialogContent.isExpanded,
'rounded-lg': !progressDialogContent.isExpanded
}"
>
<div class="justify-center text-sm font-bold leading-none">
<div class="flex items-center text-base leading-none">
<div class="flex items-center">
<template v-if="isInProgress">
<i class="pi pi-spin pi-spinner mr-2 text-3xl" />
<DotSpinner duration="1s" class="mr-2" />
<span>{{ currentTaskName }}</span>
</template>
<template v-else-if="isRestartCompleted">
<span class="mr-2">🎉</span>
<span>{{ currentTaskName }}</span>
</template>
<template v-else>
<i class="pi pi-check-circle mr-2 text-green-500" />
<span class="leading-none">{{
$t('manager.restartToApplyChanges')
}}</span>
<span class="mr-2"></span>
<span>{{ $t('manager.restartToApplyChanges') }}</span>
</template>
</div>
</div>
<div class="flex items-center gap-4">
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
{{ comfyManagerStore.uncompletedCount }} {{ $t('g.progressCountOf') }}
{{ comfyManagerStore.taskLogs.length }}
<span v-if="isInProgress" class="text-sm text-neutral-700">
{{ completedTasksCount }} {{ $t('g.progressCountOf') }}
{{ totalTasksCount }}
</span>
<div class="flex items-center">
<Button
v-if="!isInProgress"
v-if="!isInProgress && !isRestartCompleted"
rounded
outlined
class="px-4 py-2 rounded-md mr-4"
class="mr-4 rounded-md border-2 px-3 text-neutral-600 border-neutral-900 hover:bg-neutral-100 !dark-theme:bg-transparent dark-theme:text-white dark-theme:border-white dark-theme:hover:bg-neutral-800"
@click="handleRestart"
>
{{ $t('g.restart') }}
{{ $t('manager.applyChanges') }}
</Button>
<Button
v-else-if="!isRestartCompleted"
:icon="
progressDialogContent.isExpanded
? 'pi pi-chevron-up'
: 'pi pi-chevron-right'
: 'pi pi-chevron-down'
"
text
rounded
size="small"
class="font-bold"
severity="secondary"
:aria-label="progressDialogContent.isExpanded ? 'Collapse' : 'Expand'"
@click.stop="progressDialogContent.toggle"
@@ -53,6 +57,7 @@
text
rounded
size="small"
class="font-bold"
severity="secondary"
aria-label="Close"
@click.stop="closeDialog"
@@ -65,9 +70,11 @@
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import Button from 'primevue/button'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { api } from '@/scripts/api'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useWorkflowService } from '@/services/workflowService'
@@ -77,41 +84,107 @@ import {
} from '@/stores/comfyManagerStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useSettingStore } from '@/stores/settingStore'
const { t } = useI18n()
const dialogStore = useDialogStore()
const progressDialogContent = useManagerProgressDialogStore()
const comfyManagerStore = useComfyManagerStore()
const settingStore = useSettingStore()
const { performConflictDetection } = useConflictDetection()
const isInProgress = computed(() => comfyManagerStore.uncompletedCount > 0)
// State management for restart process
const isRestarting = ref<boolean>(false)
const isRestartCompleted = ref<boolean>(false)
const isInProgress = computed(
() => comfyManagerStore.isProcessingTasks || isRestarting.value
)
const completedTasksCount = computed(() => {
return (
comfyManagerStore.succeededTasksIds.length +
comfyManagerStore.failedTasksIds.length
)
})
const totalTasksCount = computed(() => {
const completedTasks = Object.keys(comfyManagerStore.taskHistory).length
const taskQueue = comfyManagerStore.taskQueue
const queuedTasks = taskQueue
? (taskQueue.running_queue?.length || 0) +
(taskQueue.pending_queue?.length || 0)
: 0
return completedTasks + queuedTasks
})
const closeDialog = () => {
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
}
const fallbackTaskName = t('g.installing')
const fallbackTaskName = t('manager.installingDependencies')
const currentTaskName = computed(() => {
if (isRestarting.value) {
return t('manager.restartingBackend')
}
if (isRestartCompleted.value) {
return t('manager.extensionsSuccessfullyInstalled')
}
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
const task = comfyManagerStore.taskLogs.at(-1)
return task?.taskName ?? fallbackTaskName
})
const handleRestart = async () => {
const onReconnect = async () => {
// Refresh manager state
// Store original toast setting value
const originalToastSetting = settingStore.get(
'Comfy.Toast.DisableReconnectingToast'
)
comfyManagerStore.clearLogs()
comfyManagerStore.setStale()
try {
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
// Refresh node definitions
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
isRestarting.value = true
// Reload workflow
await useWorkflowService().reloadCurrentWorkflow()
const onReconnect = async () => {
try {
comfyManagerStore.setStale()
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
await useWorkflowService().reloadCurrentWorkflow()
// Run conflict detection after restart completion
await performConflictDetection()
} finally {
await settingStore.set(
'Comfy.Toast.DisableReconnectingToast',
originalToastSetting
)
isRestarting.value = false
isRestartCompleted.value = true
setTimeout(() => {
closeDialog()
comfyManagerStore.resetTaskState()
}, 3000)
}
}
useEventListener(api, 'reconnected', onReconnect, { once: true })
await useComfyManagerService().rebootComfyUI()
} catch (error) {
// If restart fails, restore settings and reset state
await settingStore.set(
'Comfy.Toast.DisableReconnectingToast',
originalToastSetting
)
isRestarting.value = false
isRestartCompleted.value = false
closeDialog() // Close dialog on error
throw error
}
useEventListener(api, 'reconnected', onReconnect, { once: true })
await useComfyManagerService().rebootComfyUI()
closeDialog()
}
</script>

View File

@@ -18,16 +18,27 @@
<script setup lang="ts">
import TabMenu from 'primevue/tabmenu'
import { ref } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
import {
useComfyManagerStore,
useManagerProgressDialogStore
} from '@/stores/comfyManagerStore'
const progressDialogContent = useManagerProgressDialogStore()
const activeTabIndex = ref(0)
const comfyManagerStore = useComfyManagerStore()
const activeTabIndex = computed({
get: () => progressDialogContent.getActiveTabIndex(),
set: (value) => progressDialogContent.setActiveTabIndex(value)
})
const { t } = useI18n()
const tabs = [
const tabs = computed(() => [
{ label: t('manager.installationQueue') },
{ label: t('manager.failed', { count: 0 }) }
]
{
label: t('manager.failed', {
count: comfyManagerStore.failedTasksIds.length
})
}
])
</script>

View File

@@ -14,7 +14,17 @@
@mouseenter="onMenuItemHover(menuItem.key, $event)"
@mouseleave="onMenuItemLeave(menuItem.key)"
>
<i :class="menuItem.icon" class="help-menu-icon" />
<div class="help-menu-icon-container">
<div class="help-menu-icon">
<component
:is="menuItem.icon"
v-if="typeof menuItem.icon === 'object'"
:size="16"
/>
<i v-else :class="menuItem.icon" />
</div>
<div v-if="menuItem.showRedDot" class="menu-red-dot" />
</div>
<span class="menu-label">{{ menuItem.label }}</span>
<i v-if="menuItem.key === 'more'" class="pi pi-chevron-right" />
</button>
@@ -120,9 +130,19 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { type CSSProperties, computed, nextTick, onMounted, ref } from 'vue'
import {
type CSSProperties,
type Component,
computed,
nextTick,
onMounted,
ref
} from 'vue'
import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useDialogService } from '@/services/dialogService'
import { type ReleaseNote } from '@/services/releaseService'
import { useCommandStore } from '@/stores/commandStore'
import { useReleaseStore } from '@/stores/releaseStore'
@@ -133,12 +153,13 @@ import { formatVersionAnchor } from '@/utils/formatUtil'
// Types
interface MenuItem {
key: string
icon?: string
icon?: string | Component
label?: string
action?: () => void
visible?: boolean
type?: 'item' | 'divider'
items?: MenuItem[]
showRedDot?: boolean
}
// Constants
@@ -170,6 +191,7 @@ const { t, locale } = useI18n()
const releaseStore = useReleaseStore()
const commandStore = useCommandStore()
const settingStore = useSettingStore()
const dialogService = useDialogService()
// Emits
const emit = defineEmits<{
@@ -188,6 +210,10 @@ const showVersionUpdates = computed(() =>
settingStore.get('Comfy.Notification.ShowVersionUpdates')
)
// Use conflict acknowledgment state from composable
const { shouldShowRedDot: shouldShowManagerRedDot } =
useConflictAcknowledgment()
const moreItems = computed<MenuItem[]>(() => {
const allMoreItems: MenuItem[] = [
{
@@ -277,7 +303,18 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-question-circle',
label: t('helpCenter.helpFeedback'),
action: () => {
void commandStore.execute('Comfy.Feedback')
void commandStore.execute('Comfy.ContactSupport')
emit('close')
}
},
{
key: 'manager',
type: 'item',
icon: PuzzleIcon,
label: t('helpCenter.managerExtension'),
showRedDot: shouldShowManagerRedDot.value,
action: () => {
dialogService.showManagerDialog()
emit('close')
}
},
@@ -516,6 +553,13 @@ onMounted(async () => {
box-shadow: none;
}
.help-menu-icon-container {
position: relative;
margin-right: 0.75rem;
width: 16px;
flex-shrink: 0;
}
.help-menu-icon {
margin-right: 0.75rem;
font-size: 1rem;
@@ -523,9 +567,26 @@ onMounted(async () => {
width: 16px;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.help-menu-icon svg {
color: var(--p-text-muted-color);
}
.menu-red-dot {
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: #ff3b30;
border-radius: 50%;
border: 1.5px solid var(--p-content-background);
z-index: 1;
}
.menu-label {
flex: 1;
}

View File

@@ -75,6 +75,11 @@ import { formatVersionAnchor } from '@/utils/formatUtil'
const { locale, t } = useI18n()
const releaseStore = useReleaseStore()
// Define emits
const emit = defineEmits<{
'whats-new-dismissed': []
}>()
// Local state for dismissed status
const isDismissed = ref(false)
@@ -126,6 +131,7 @@ const show = () => {
const hide = () => {
isDismissed.value = true
emit('whats-new-dismissed')
}
const closePopup = async () => {

View File

@@ -0,0 +1,41 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 16 16"
fill="none"
:class="iconClass"
>
<g clip-path="url(#clip0_1099_16244)">
<path
d="M4.99992 3.00016C4.99992 2.07969 5.74611 1.3335 6.66658 1.3335C7.58706 1.3335 8.33325 2.07969 8.33325 3.00016V4.00016H8.99992C9.9318 4.00016 10.3977 4.00016 10.7653 4.1524C11.2553 4.35539 11.6447 4.74474 11.8477 5.2348C11.9999 5.60234 11.9999 6.06828 11.9999 7.00016H12.9999C13.9204 7.00016 14.6666 7.74635 14.6666 8.66683C14.6666 9.5873 13.9204 10.3335 12.9999 10.3335H11.9999V11.4668C11.9999 12.5869 11.9999 13.147 11.7819 13.5748C11.5902 13.9511 11.2842 14.2571 10.9079 14.4488C10.4801 14.6668 9.92002 14.6668 8.79992 14.6668H8.33325V13.5002C8.33325 12.6717 7.66168 12.0002 6.83325 12.0002C6.00482 12.0002 5.33325 12.6717 5.33325 13.5002V14.6668H4.53325C3.41315 14.6668 2.85309 14.6668 2.42527 14.4488C2.04895 14.2571 1.74299 13.9511 1.55124 13.5748C1.33325 13.147 1.33325 12.5869 1.33325 11.4668V10.3335H2.33325C3.25373 10.3335 3.99992 9.5873 3.99992 8.66683C3.99992 7.74635 3.25373 7.00016 2.33325 7.00016H1.33325C1.33325 6.06828 1.33325 5.60234 1.48549 5.2348C1.68848 4.74474 2.07783 4.35539 2.56789 4.1524C2.93543 4.00016 3.40137 4.00016 4.33325 4.00016H4.99992V3.00016Z"
:stroke="color"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_1099_16244">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
size?: number | string
color?: string
class?: string
}
const {
size = 16,
color = 'currentColor',
class: className
} = defineProps<Props>()
const iconClass = computed(() => className || '')
</script>

View File

@@ -0,0 +1,27 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 16 16"
fill="none"
:class="iconClass"
>
<path
d="M8.00049 1.3335C8.73661 1.33367 9.33332 1.93038 9.3335 2.6665V2.83447C9.82278 2.96041 10.2851 3.15405 10.7095 3.40479L10.8286 3.28564C11.3493 2.76525 12.1937 2.76519 12.7144 3.28564C13.235 3.80626 13.2348 4.65067 12.7144 5.17139L12.5952 5.29053C12.846 5.71486 13.0396 6.17725 13.1655 6.6665H13.3335C14.0699 6.6665 14.6665 7.26411 14.6665 8.00049C14.6663 8.73672 14.0698 9.3335 13.3335 9.3335H13.1655C13.0396 9.82284 12.846 10.2851 12.5952 10.7095L12.7144 10.8286C13.235 11.3493 13.235 12.1937 12.7144 12.7144C12.1937 13.235 11.3493 13.235 10.8286 12.7144L10.7095 12.5952C10.2851 12.846 9.82284 13.0396 9.3335 13.1655V13.3335C9.3335 14.0698 8.73672 14.6663 8.00049 14.6665C7.26411 14.6665 6.6665 14.0699 6.6665 13.3335V13.1655C6.17725 13.0396 5.71486 12.846 5.29053 12.5952L5.17139 12.7144C4.65067 13.2348 3.80626 13.235 3.28564 12.7144C2.76519 12.1937 2.76525 11.3493 3.28564 10.8286L3.40479 10.7095C3.15405 10.2851 2.96041 9.82278 2.83447 9.3335H2.6665C1.93038 9.33332 1.33367 8.73661 1.3335 8.00049C1.3335 7.26422 1.93027 6.66668 2.6665 6.6665H2.83447C2.96043 6.17722 3.15403 5.71488 3.40479 5.29053L3.28564 5.17139C2.76536 4.65065 2.76508 3.80621 3.28564 3.28564C3.80621 2.76508 4.65065 2.76536 5.17139 3.28564L5.29053 3.40479C5.71488 3.15403 6.17722 2.96043 6.6665 2.83447V2.6665C6.66668 1.93027 7.26422 1.3335 8.00049 1.3335ZM7.3335 8.00049L6.00049 6.6665L4.6665 8.00049L7.3335 10.6665L11.3335 6.6665L10.0005 5.3335L7.3335 8.00049Z"
:fill="color"
/>
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
size?: number | string
color?: string
class?: string
}
const { size = 16, color = '#60A5FA', class: className } = defineProps<Props>()
const iconClass = computed(() => className || '')
</script>

View File

@@ -42,6 +42,7 @@
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
}"
@whats-new-dismissed="handleWhatsNewDismissed"
/>
</Teleport>
@@ -63,6 +64,9 @@ import { computed, onMounted } from 'vue'
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
import ReleaseNotificationToast from '@/components/helpcenter/ReleaseNotificationToast.vue'
import WhatsNewPopup from '@/components/helpcenter/WhatsNewPopup.vue'
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useDialogService } from '@/services/dialogService'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useReleaseStore } from '@/stores/releaseStore'
import { useSettingStore } from '@/stores/settingStore'
@@ -72,8 +76,22 @@ import SidebarIcon from './SidebarIcon.vue'
const settingStore = useSettingStore()
const releaseStore = useReleaseStore()
const helpCenterStore = useHelpCenterStore()
const { shouldShowRedDot } = storeToRefs(releaseStore)
const { isVisible: isHelpCenterVisible } = storeToRefs(helpCenterStore)
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
const conflictDetection = useConflictDetection()
const { showNodeConflictDialog } = useDialogService()
// Use conflict acknowledgment state from composable - call only once
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
useConflictAcknowledgment()
// Use either release red dot or conflict red dot
const shouldShowRedDot = computed(() => {
const releaseRedDot = showReleaseRedDot
return releaseRedDot || shouldShowConflictRedDot.value
})
const sidebarLocation = computed(() =>
settingStore.get('Comfy.Sidebar.Location')
@@ -89,6 +107,36 @@ const closeHelpCenter = () => {
helpCenterStore.hide()
}
/**
* Handle What's New popup dismissal
* Check if conflict modal should be shown after ComfyUI update
*/
const handleWhatsNewDismissed = async () => {
try {
// Check if conflict modal should be shown after update
const shouldShow =
await conflictDetection.shouldShowConflictModalAfterUpdate()
if (shouldShow) {
showConflictModal()
}
} catch (error) {
console.error('[HelpCenter] Error checking conflict modal:', error)
}
}
/**
* Show the node conflict dialog with current conflict data
*/
const showConflictModal = () => {
showNodeConflictDialog({
showAfterWhatsNew: true,
dialogComponentProps: {
onClose: () => {
markConflictsAsSeen()
}
}
})
}
// Initialize release store on mount
onMounted(async () => {
// Initialize release store to fetch releases for toast and popup

View File

@@ -107,9 +107,12 @@ import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import { useDialogService } from '@/services/dialogService'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import {
ManagerUIState,
useManagerStateStore
} from '@/stores/managerStateStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useSettingStore } from '@/stores/settingStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
@@ -121,7 +124,6 @@ const colorPaletteStore = useColorPaletteStore()
const menuItemsStore = useMenuItemStore()
const commandStore = useCommandStore()
const dialogStore = useDialogStore()
const aboutPanelStore = useAboutPanelStore()
const settingStore = useSettingStore()
const { t } = useI18n()
@@ -157,23 +159,28 @@ const showSettings = (defaultPanel?: string) => {
})
}
// Temporary duplicated from LoadWorkflowWarning.vue
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
// This allows us to conditionally show the Manager button only when the extension is available
// TODO: Remove this check when Manager functionality is fully migrated into core
const isManagerInstalled = computed(() => {
return aboutPanelStore.badges.some(
(badge) =>
badge.label.includes('ComfyUI-Manager') ||
badge.url.includes('ComfyUI-Manager')
)
})
const managerStateStore = useManagerStateStore()
const showManageExtensions = () => {
if (isManagerInstalled.value) {
useDialogService().showManagerDialog()
} else {
showSettings('extension')
const showManageExtensions = async () => {
const state = managerStateStore.managerUIState
switch (state) {
case ManagerUIState.DISABLED:
showSettings('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility')
} catch {
// If legacy command doesn't exist, fall back to extensions panel
showSettings('extension')
}
break
case ManagerUIState.NEW_UI:
useDialogService().showManagerDialog()
break
}
}

View File

@@ -1,5 +1,5 @@
import { whenever } from '@vueuse/core'
import { computed, onUnmounted } from 'vue'
import { computed, onUnmounted, ref } from 'vue'
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
@@ -9,6 +9,10 @@ import type { components } from '@/types/comfyRegistryTypes'
export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
const comfyManagerStore = useComfyManagerStore()
// Flag to prevent duplicate fetches during initialization
const isInitializing = ref(false)
const lastFetchedIds = ref<string>('')
const installedPackIds = computed(() =>
Array.from(comfyManagerStore.installedPacksIds)
)
@@ -20,24 +24,59 @@ export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
packs.filter((pack) => comfyManagerStore.isPackInstalled(pack.id))
const startFetchInstalled = async () => {
await comfyManagerStore.refreshInstalledList()
await startFetch()
// Prevent duplicate calls during initialization
if (isInitializing.value) {
return
}
isInitializing.value = true
try {
if (comfyManagerStore.installedPacksIds.size === 0) {
await comfyManagerStore.refreshInstalledList()
}
await startFetch()
} finally {
isInitializing.value = false
}
}
// When installedPackIds changes, we need to update the nodePacks
whenever(installedPackIds, async () => {
await startFetch()
// But only if the IDs actually changed (not just array reference)
whenever(installedPackIds, async (newIds) => {
const newIdsStr = newIds.sort().join(',')
if (newIdsStr !== lastFetchedIds.value && !isInitializing.value) {
lastFetchedIds.value = newIdsStr
await startFetch()
}
})
onUnmounted(() => {
cleanup()
})
// Create a computed property that provides installed pack info with versions
const installedPacksWithVersions = computed(() => {
const result: Array<{ id: string; version: string }> = []
for (const pack of Object.values(comfyManagerStore.installedPacks)) {
const id = pack.cnr_id || pack.aux_id
if (id) {
result.push({
id,
version: pack.ver ?? ''
})
}
}
return result
})
return {
error,
isLoading,
isReady,
installedPacks: nodePacks,
installedPacksWithVersions,
startFetchInstalled,
filterInstalledPack
}

View File

@@ -0,0 +1,51 @@
import { type Ref, computed } from 'vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
type NodePack = components['schemas']['Node']
export type SelectionState = 'all-installed' | 'none-installed' | 'mixed'
/**
* Composable for managing multi-package selection states
* Handles installation status tracking and selection state determination
*/
export function usePacksSelection(nodePacks: Ref<NodePack[]>) {
const managerStore = useComfyManagerStore()
const installedPacks = computed(() =>
nodePacks.value.filter((pack) => managerStore.isPackInstalled(pack.id))
)
const notInstalledPacks = computed(() =>
nodePacks.value.filter((pack) => !managerStore.isPackInstalled(pack.id))
)
const isAllInstalled = computed(
() => installedPacks.value.length === nodePacks.value.length
)
const isNoneInstalled = computed(
() => notInstalledPacks.value.length === nodePacks.value.length
)
const isMixed = computed(
() => installedPacks.value.length > 0 && notInstalledPacks.value.length > 0
)
const selectionState = computed<SelectionState>(() => {
if (isAllInstalled.value) return 'all-installed'
if (isNoneInstalled.value) return 'none-installed'
return 'mixed'
})
return {
installedPacks,
notInstalledPacks,
isAllInstalled,
isNoneInstalled,
isMixed,
selectionState
}
}

View File

@@ -0,0 +1,63 @@
import { type Ref, computed } from 'vue'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import type { components } from '@/types/comfyRegistryTypes'
type NodePack = components['schemas']['Node']
type NodeStatus = components['schemas']['NodeStatus']
type NodeVersionStatus = components['schemas']['NodeVersionStatus']
const STATUS_PRIORITY = [
'NodeStatusBanned',
'NodeVersionStatusBanned',
'NodeStatusDeleted',
'NodeVersionStatusDeleted',
'NodeVersionStatusFlagged',
'NodeVersionStatusPending',
'NodeStatusActive',
'NodeVersionStatusActive'
] as const
/**
* Composable for managing package status with priority
* Handles import failures and determines the most important status
*/
export function usePacksStatus(nodePacks: Ref<NodePack[]>) {
const conflictDetectionStore = useConflictDetectionStore()
const hasImportFailed = computed(() => {
return nodePacks.value.some((pack) => {
if (!pack.id) return false
const conflicts = conflictDetectionStore.getConflictsForPackageByID(
pack.id
)
return (
conflicts?.conflicts?.some((c) => c.type === 'import_failed') || false
)
})
})
const overallStatus = computed<NodeStatus | NodeVersionStatus>(() => {
// Check for import failed first (highest priority for installed packages)
if (hasImportFailed.value) {
// Import failed doesn't have a specific status enum, so we return active
// but the PackStatusMessage will handle it via hasImportFailed prop
return 'NodeVersionStatusActive' as NodeVersionStatus
}
// Find the highest priority status from all packages
for (const priorityStatus of STATUS_PRIORITY) {
if (nodePacks.value.some((pack) => pack.status === priorityStatus)) {
return priorityStatus as NodeStatus | NodeVersionStatus
}
}
// Default to active if no specific status found
return 'NodeVersionStatusActive' as NodeVersionStatus
})
return {
hasImportFailed,
overallStatus
}
}

View File

@@ -0,0 +1,65 @@
import { computed, onMounted } from 'vue'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
/**
* Composable to find NodePacks that have updates available
* Uses the same filtering approach as ManagerDialogContent.vue
* Automatically fetches installed pack data when initialized
*/
export const useUpdateAvailableNodes = () => {
const comfyManagerStore = useComfyManagerStore()
const { installedPacks, isLoading, error, startFetchInstalled } =
useInstalledPacks()
// Check if a pack has updates available (same logic as usePackUpdateStatus)
const isOutdatedPack = (pack: components['schemas']['Node']) => {
const isInstalled = comfyManagerStore.isPackInstalled(pack?.id)
if (!isInstalled) return false
const installedVersion = comfyManagerStore.getInstalledPackVersion(
pack.id ?? ''
)
const latestVersion = pack.latest_version?.version
const isNightlyPack = !!installedVersion && !isSemVer(installedVersion)
if (isNightlyPack || !latestVersion) {
return false
}
return compareVersions(latestVersion, installedVersion) > 0
}
// Same filtering logic as ManagerDialogContent.vue
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
packs.filter(isOutdatedPack)
// Filter only outdated packs from installed packs
const updateAvailableNodePacks = computed(() => {
if (!installedPacks.value.length) return []
return filterOutdatedPacks(installedPacks.value)
})
// Check if there are any outdated packs
const hasUpdateAvailable = computed(() => {
return updateAvailableNodePacks.value.length > 0
})
// Automatically fetch installed pack data when composable is used
onMounted(async () => {
if (!installedPacks.value.length && !isLoading.value) {
await startFetchInstalled()
}
})
return {
updateAvailableNodePacks,
hasUpdateAvailable,
isLoading,
error
}
}

View File

@@ -7,7 +7,7 @@ import { app } from '@/scripts/app'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { SelectedVersion, UseNodePacksOptions } from '@/types/comfyManagerTypes'
import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
@@ -66,8 +66,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
return {
id: CORE_NODES_PACK_NAME,
version:
systemStatsStore.systemStats?.system?.comfyui_version ??
SelectedVersion.NIGHTLY
systemStatsStore.systemStats?.system?.comfyui_version ?? 'nightly'
}
}
@@ -77,7 +76,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
if (pack) {
return {
id: pack.id,
version: pack.latest_version?.version ?? SelectedVersion.NIGHTLY
version: pack.latest_version?.version ?? 'nightly'
}
}

View File

@@ -0,0 +1,101 @@
import { useStorage } from '@vueuse/core'
import { computed } from 'vue'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
/**
* LocalStorage keys for conflict acknowledgment tracking
*/
const STORAGE_KEYS = {
CONFLICT_MODAL_DISMISSED: 'Comfy.ConflictModalDismissed',
CONFLICT_RED_DOT_DISMISSED: 'Comfy.ConflictRedDotDismissed',
CONFLICT_WARNING_BANNER_DISMISSED: 'Comfy.ConflictWarningBannerDismissed'
} as const
/**
* Interface for conflict acknowledgment state
*/
interface ConflictAcknowledgmentState {
modal_dismissed: boolean
red_dot_dismissed: boolean
warning_banner_dismissed: boolean
}
// Shared state - initialized once and reused across all composable calls
const modalDismissed = useStorage(STORAGE_KEYS.CONFLICT_MODAL_DISMISSED, false)
const redDotDismissed = useStorage(
STORAGE_KEYS.CONFLICT_RED_DOT_DISMISSED,
false
)
const warningBannerDismissed = useStorage(
STORAGE_KEYS.CONFLICT_WARNING_BANNER_DISMISSED,
false
)
/**
* Composable for managing conflict acknowledgment state in localStorage
*
* This handles:
* - Tracking whether conflict modal has been dismissed
* - Tracking whether red dot notification has been cleared
* - Managing per-package conflict acknowledgments
* - Detecting ComfyUI version changes to reset acknowledgment state
*/
export function useConflictAcknowledgment() {
const conflictDetectionStore = useConflictDetectionStore()
// Create computed state object for backward compatibility
const state = computed<ConflictAcknowledgmentState>(() => ({
modal_dismissed: modalDismissed.value,
red_dot_dismissed: redDotDismissed.value,
warning_banner_dismissed: warningBannerDismissed.value
}))
/**
* Mark red dot notification as dismissed
*/
function dismissRedDotNotification(): void {
redDotDismissed.value = true
}
/**
* Mark manager warning banner as dismissed
*/
function dismissWarningBanner(): void {
warningBannerDismissed.value = true
redDotDismissed.value = true
}
/**
* Mark conflicts as seen (unified function for help center and manager)
*/
function markConflictsAsSeen(): void {
redDotDismissed.value = true
modalDismissed.value = true
warningBannerDismissed.value = true
}
const hasConflicts = computed(() => conflictDetectionStore.hasConflicts)
const shouldShowConflictModal = computed(() => !modalDismissed.value)
const shouldShowRedDot = computed(() => {
if (!hasConflicts.value) return false
if (redDotDismissed.value) return false
return true
})
const shouldShowManagerBanner = computed(() => {
return hasConflicts.value && !warningBannerDismissed.value
})
return {
// State
acknowledgmentState: state,
shouldShowConflictModal,
shouldShowRedDot,
shouldShowManagerBanner,
// Methods
dismissRedDotNotification,
dismissWarningBanner,
markConflictsAsSeen
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,10 +20,15 @@ import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService'
import type { ComfyCommand } from '@/stores/commandStore'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import {
ManagerUIState,
useManagerStateStore
} from '@/stores/managerStateStore'
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
@@ -33,6 +38,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
import {
getAllNonIoNodesInSubgraph,
getExecutionIdsForSelectedNodes
@@ -679,36 +685,13 @@ export function useCoreCommands(): ComfyCommand[] {
await workflowService.closeWorkflow(workflowStore.activeWorkflow)
}
},
{
id: 'Comfy.Feedback',
icon: 'pi pi-megaphone',
label: 'Give Feedback',
versionAdded: '1.8.2',
function: () => {
dialogService.showIssueReportDialog({
title: t('g.feedback'),
subtitle: t('issueReport.feedbackTitle'),
panelProps: {
errorType: 'Feedback',
defaultFields: ['SystemStats', 'Settings']
}
})
}
},
{
id: 'Comfy.ContactSupport',
icon: 'pi pi-question',
label: 'Contact Support',
versionAdded: '1.17.8',
function: () => {
dialogService.showIssueReportDialog({
title: t('issueReport.contactSupportTitle'),
subtitle: t('issueReport.contactSupportDescription'),
panelProps: {
errorType: 'ContactSupport',
defaultFields: ['Workflow', 'Logs', 'SystemStats', 'Settings']
}
})
window.open('https://support.comfy.org/', '_blank')
}
},
{
@@ -732,12 +715,107 @@ export function useCoreCommands(): ComfyCommand[] {
}
},
{
id: 'Comfy.Manager.CustomNodesManager',
id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
icon: 'pi pi-puzzle',
label: 'Toggle the Custom Nodes Manager',
label: 'Custom Nodes Manager',
versionAdded: '1.12.10',
function: async () => {
const managerState = useManagerStateStore().managerUIState
switch (managerState) {
case ManagerUIState.DISABLED:
dialogService.showSettingsDialog('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await useCommandStore().execute(
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
)
} catch (error) {
console.error('error', error)
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
dialogService.showManagerDialog()
}
break
case ManagerUIState.NEW_UI:
dialogService.showManagerDialog()
break
}
}
},
{
id: 'Comfy.Manager.ShowUpdateAvailablePacks',
icon: 'pi pi-sync',
label: 'Check for Custom Node Updates',
versionAdded: '1.17.0',
function: () => {
dialogService.toggleManagerDialog()
const managerStore = useManagerStateStore()
const state = managerStore.managerUIState
switch (state) {
case ManagerUIState.DISABLED:
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.notAvailable'),
life: 3000
})
break
case ManagerUIState.LEGACY_UI:
useCommandStore()
.execute('Comfy.Manager.Menu.ToggleVisibility')
.catch(() => {
// If legacy command doesn't exist, fall back to extensions panel
dialogService.showSettingsDialog('extension')
})
break
case ManagerUIState.NEW_UI:
dialogService.showManagerDialog()
break
}
}
},
{
id: 'Comfy.Manager.ShowMissingPacks',
icon: 'pi pi-exclamation-circle',
label: 'Install Missing Custom Nodes',
versionAdded: '1.17.0',
function: async () => {
const managerStore = useManagerStateStore()
const state = managerStore.managerUIState
switch (state) {
case ManagerUIState.DISABLED:
// When manager is disabled, open the extensions panel in settings
dialogService.showSettingsDialog('extension')
break
case ManagerUIState.LEGACY_UI:
try {
await useCommandStore().execute(
'Comfy.Manager.Menu.ToggleVisibility'
)
} catch {
// If legacy command doesn't exist, fall back to extensions panel
dialogService.showSettingsDialog('extension')
}
break
case ManagerUIState.NEW_UI:
dialogService.showManagerDialog({
initialTab: ManagerTab.Missing
})
break
}
}
},
{
@@ -816,6 +894,7 @@ export function useCoreCommands(): ComfyCommand[] {
})
return
}
const { node } = res
canvas.select(node)
canvasStore.updateSelectedItems()
@@ -901,6 +980,84 @@ export function useCoreCommands(): ComfyCommand[] {
const modelSelectorDialog = useModelSelectorDialog()
modelSelectorDialog.show()
}
},
{
id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',
icon: 'pi pi-bars',
label: 'Custom Nodes (Legacy)',
versionAdded: '1.16.4',
function: async () => {
try {
await useCommandStore().execute(
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
)
} catch (error) {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
}
},
{
id: 'Comfy.Manager.ShowLegacyManagerMenu',
icon: 'mdi mdi-puzzle',
label: 'Manager Menu (Legacy)',
versionAdded: '1.16.4',
function: async () => {
try {
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
} catch (error) {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
}
},
{
id: 'Comfy.Memory.UnloadModels',
icon: 'mdi mdi-vacuum-outline',
label: 'Unload Models',
versionAdded: '1.16.4',
function: async () => {
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('g.commandProhibited', {
command: 'Comfy.Memory.UnloadModels'
}),
life: 3000
})
return
}
await api.freeMemory({ freeExecutionCache: false })
}
},
{
id: 'Comfy.Memory.UnloadModelsAndExecutionCache',
icon: 'mdi mdi-vacuum-outline',
label: 'Unload Models and Execution Cache',
versionAdded: '1.16.4',
function: async () => {
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('g.commandProhibited', {
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
}),
life: 3000
})
return
}
await api.freeMemory({ freeExecutionCache: true })
}
}
]

View File

@@ -0,0 +1,40 @@
import { computed, reactive, readonly } from 'vue'
import { api } from '@/scripts/api'
/**
* Known server feature flags (top-level, not extensions)
*/
export enum ServerFeatureFlag {
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
MAX_UPLOAD_SIZE = 'max_upload_size',
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4'
}
/**
* Composable for reactive access to feature flags
*/
export function useFeatureFlags() {
// Create reactive state that tracks server feature flags
const flags = reactive({
get supportsPreviewMetadata() {
return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
},
get maxUploadSize() {
return api.getServerFeature(ServerFeatureFlag.MAX_UPLOAD_SIZE)
},
get supportsManagerV4() {
return api.getServerFeature(ServerFeatureFlag.MANAGER_SUPPORTS_V4)
}
})
// Create a reactive computed for any feature flag
const featureFlag = <T = unknown>(featurePath: string, defaultValue?: T) => {
return computed(() => api.getServerFeature(featurePath, defaultValue))
}
return {
flags: readonly(flags),
featureFlag
}
}

View File

@@ -0,0 +1,85 @@
import { type ComputedRef, computed, unref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
/**
* Extracting import failed conflicts from conflict list
*/
function extractImportFailedConflicts(conflicts?: ConflictDetail[] | null) {
if (!conflicts) return null
const importFailedConflicts = conflicts.filter(
(item): item is ConflictDetail => item.type === 'import_failed'
)
return importFailedConflicts.length > 0 ? importFailedConflicts : null
}
/**
* Creating import failed dialog
*/
function createImportFailedDialog() {
const { t } = useI18n()
const { showErrorDialog } = useDialogService()
return (importFailedInfo: ConflictDetail[] | null) => {
if (importFailedInfo) {
const errorMessage =
importFailedInfo
.map((conflict) => conflict.required_value)
.filter(Boolean)
.join('\n') || t('manager.importFailedGenericError')
const error = new Error(errorMessage)
showErrorDialog(error, {
title: t('manager.failedToInstall'),
reportType: 'importFailedError'
})
}
}
}
/**
* Composable for detecting and handling import failed conflicts
* @param packageId - Package ID string or computed ref
* @returns Object with import failed detection and dialog handler
*/
export function useImportFailedDetection(
packageId?: string | ComputedRef<string> | null
) {
const { isPackInstalled } = useComfyManagerStore()
const { getConflictsForPackageByID } = useConflictDetectionStore()
const isInstalled = computed(() =>
packageId ? isPackInstalled(unref(packageId)) : false
)
const conflicts = computed(() => {
const currentPackageId = unref(packageId)
if (!currentPackageId || !isInstalled.value) return null
return getConflictsForPackageByID(currentPackageId) || null
})
const importFailedInfo = computed(() => {
return extractImportFailedConflicts(conflicts.value?.conflicts)
})
const importFailed = computed(() => {
return importFailedInfo.value !== null
})
const showImportFailedDialog = createImportFailedDialog()
return {
importFailedInfo,
importFailed,
showImportFailedDialog: () =>
showImportFailedDialog(importFailedInfo.value),
isInstalled
}
}

View File

@@ -1,101 +1,167 @@
import { useEventListener, whenever } from '@vueuse/core'
import { computed, readonly, ref } from 'vue'
import { useEventListener } from '@vueuse/core'
import { pickBy } from 'es-toolkit/compat'
import { Ref, computed, ref } from 'vue'
import { api } from '@/scripts/api'
import { ManagerWsQueueStatus } from '@/types/comfyManagerTypes'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { components } from '@/types/generatedManagerTypes'
type QueuedTask<T> = {
task: () => Promise<T>
onComplete?: () => void
}
type ManagerTaskHistory = Record<
string,
components['schemas']['TaskHistoryItem']
>
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
type QueueTaskItem = components['schemas']['QueueTaskItem']
type HistoryTaskItem = components['schemas']['TaskHistoryItem']
type Task = QueueTaskItem | HistoryTaskItem
const MANAGER_WS_MSG_TYPE = 'cm-queue-status'
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
export const useManagerQueue = () => {
const clientQueueItems = ref<QueuedTask<unknown>[]>([])
const clientQueueLength = computed(() => clientQueueItems.value.length)
const onCompletedQueue = ref<((() => void) | undefined)[]>([])
const onCompleteWaitingCount = ref(0)
const uncompletedCount = computed(
() => clientQueueLength.value + onCompleteWaitingCount.value
export const useManagerQueue = (
taskHistory: Ref<ManagerTaskHistory>,
taskQueue: Ref<ManagerTaskQueue>,
installedPacks: Ref<Record<string, any>>
) => {
const { showManagerProgressDialog } = useDialogService()
// Task queue state (read-only from server)
const maxHistoryItems = ref(64)
const isLoading = ref(false)
const isProcessing = ref(false)
// Computed values
const currentQueueLength = computed(
() =>
taskQueue.value.running_queue.length +
taskQueue.value.pending_queue.length
)
const serverQueueStatus = ref<ManagerWsQueueStatus>(ManagerWsQueueStatus.DONE)
const isServerIdle = computed(
() => serverQueueStatus.value === ManagerWsQueueStatus.DONE
)
/**
* Update the processing state based on the current queue length.
* If the queue is empty, or all tasks in the queue are associated
* with different clients, then this client is not processing any tasks.
*/
const updateProcessingState = (): void => {
isProcessing.value = currentQueueLength.value > 0
}
const allTasksDone = computed(
() => isServerIdle.value && clientQueueLength.value === 0
)
const nextTaskReady = computed(
() => isServerIdle.value && clientQueueLength.value > 0
)
const allTasksDone = computed(() => currentQueueLength.value === 0)
const historyCount = computed(() => Object.keys(taskHistory.value).length)
const cleanupListener = useEventListener(
api,
MANAGER_WS_MSG_TYPE,
(event: CustomEvent<{ status: ManagerWsQueueStatus }>) => {
if (event?.type === MANAGER_WS_MSG_TYPE && event.detail?.status) {
serverQueueStatus.value = event.detail.status
/**
* Check if a task is associated with this client.
* Task can be from running queue, pending queue, or history.
* @param task - The task to check
* @returns True if the task belongs to this client
*/
const isTaskFromThisClient = (task: Task): boolean =>
task.client_id === app.api.clientId
/**
* Check if a history task is associated with this client.
* @param task - The history task to check
* @returns True if the task belongs to this client
*/
const isHistoryTaskFromThisClient = (task: HistoryTaskItem): boolean =>
task.client_id === app.api.clientId
/**
* Filter queue tasks by client id.
* Ensures that only tasks associated with this client are processed and
* added to client state.
* @param tasks - Array of queue tasks to filter
* @returns Filtered array containing only tasks from this client
*/
const filterQueueByClientId = (tasks: QueueTaskItem[]): QueueTaskItem[] =>
tasks.filter(isTaskFromThisClient)
/**
* Filter history tasks by client id using pickBy for optimal performance.
* Returns a new object containing only tasks associated with this client.
* @param history - The history object to filter
* @returns Filtered history object containing only tasks from this client
*/
const filterHistoryByClientId = (history: ManagerTaskHistory) =>
pickBy(history, isHistoryTaskFromThisClient)
/**
* Update task queue and history state with filtered data from server.
* Ensures only tasks from this client are stored in local state.
* @param state - The task state message from the server
*/
const updateTaskState = (state: ManagerTaskQueue) => {
taskQueue.value.running_queue = filterQueueByClientId(state.running_queue)
taskQueue.value.pending_queue = filterQueueByClientId(state.pending_queue)
taskHistory.value = filterHistoryByClientId(state.history)
if (state.installed_packs) {
installedPacks.value = state.installed_packs
}
updateProcessingState()
}
// WebSocket event listener for task done
const cleanupTaskDoneListener = useEventListener(
app.api,
MANAGER_WS_TASK_DONE_NAME,
(event: CustomEvent<ManagerWsTaskDoneMsg>) => {
if (event?.type === MANAGER_WS_TASK_DONE_NAME && event.detail?.state) {
updateTaskState(event.detail.state)
// If no more tasks are running/pending, hide the progress dialog
if (allTasksDone.value) {
setTimeout(() => {
if (allTasksDone.value) {
showManagerProgressDialog()
}
}, 1000) // Small delay to let users see completion
}
}
}
)
const startNextTask = () => {
const nextTask = clientQueueItems.value.shift()
if (!nextTask) return
// WebSocket event listener for task started
const cleanupTaskStartedListener = useEventListener(
app.api,
MANAGER_WS_TASK_STARTED_NAME,
(event: CustomEvent<ManagerWsTaskStartedMsg>) => {
if (event?.type === MANAGER_WS_TASK_STARTED_NAME && event.detail?.state) {
updateTaskState(event.detail.state)
const { task, onComplete } = nextTask
if (onComplete) {
// Set the task's onComplete to be executed the next time the server is idle
onCompletedQueue.value.push(onComplete)
onCompleteWaitingCount.value++
// Show progress dialog when a task starts
showManagerProgressDialog()
}
}
)
task().catch((e) => {
const message = `Error enqueuing task for ComfyUI Manager: ${e}`
console.error(message)
})
}
const enqueueTask = <T>(task: QueuedTask<T>): void => {
clientQueueItems.value.push(task)
}
const clearQueue = () => {
clientQueueItems.value = []
onCompletedQueue.value = []
onCompleteWaitingCount.value = 0
}
/**
* Cleanup function to remove event listeners and reset state
*/
const cleanup = () => {
clearQueue()
cleanupListener()
}
cleanupTaskDoneListener()
cleanupTaskStartedListener()
whenever(nextTaskReady, startNextTask)
whenever(isServerIdle, () => {
if (onCompletedQueue.value?.length) {
while (
onCompleteWaitingCount.value > 0 &&
onCompletedQueue.value.length > 0
) {
const onComplete = onCompletedQueue.value.shift()
onComplete?.()
onCompleteWaitingCount.value--
}
}
})
// Reset state
isProcessing.value = false
isLoading.value = false
}
return {
allTasksDone,
statusMessage: readonly(serverQueueStatus),
queueLength: clientQueueLength,
uncompletedCount,
// State
isLoading,
isProcessing,
maxHistoryItems,
enqueueTask,
clearQueue,
// Computed
allTasksDone,
historyCount,
currentQueueLength,
// Actions
updateTaskState,
cleanup
}
}

View File

@@ -3,22 +3,33 @@ import { onUnmounted, ref } from 'vue'
import { LogsWsMessage } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { components } from '@/types/generatedManagerTypes'
const LOGS_MESSAGE_TYPE = 'logs'
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
interface UseServerLogsOptions {
ui_id?: string
immediate?: boolean
messageFilter?: (message: string) => boolean
}
export const useServerLogs = (options: UseServerLogsOptions = {}) => {
const {
ui_id,
immediate = false,
messageFilter = (msg: string) => Boolean(msg.trim())
} = options
const logs = ref<string[]>([])
let stop: ReturnType<typeof useEventListener> | null = null
const isTaskStarted = ref(!ui_id) // If no ui_id, capture all logs immediately
let stopLogs: ReturnType<typeof useEventListener> | null = null
let stopTaskDone: ReturnType<typeof useEventListener> | null = null
let stopTaskStarted: ReturnType<typeof useEventListener> | null = null
const isValidLogEvent = (event: CustomEvent<LogsWsMessage>) =>
event?.type === LOGS_MESSAGE_TYPE && event.detail?.entries?.length > 0
@@ -27,19 +38,54 @@ export const useServerLogs = (options: UseServerLogsOptions = {}) => {
event.detail.entries.map((e) => e.m).filter(messageFilter)
const handleLogMessage = (event: CustomEvent<LogsWsMessage>) => {
// Only capture logs if this task has started
if (!isTaskStarted.value) return
if (isValidLogEvent(event)) {
logs.value.push(...parseLogMessage(event))
const messages = parseLogMessage(event)
if (messages.length > 0) {
logs.value.push(...messages)
}
}
}
const handleTaskStarted = (event: CustomEvent<ManagerWsTaskStartedMsg>) => {
if (ui_id && event?.detail?.ui_id === ui_id) {
isTaskStarted.value = true
}
}
const handleTaskDone = (event: CustomEvent<ManagerWsTaskDoneMsg>) => {
if (ui_id && event?.detail?.ui_id === ui_id) {
isTaskStarted.value = false
}
}
const start = async () => {
await api.subscribeLogs(true)
stop = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
stopLogs = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
if (ui_id) {
stopTaskStarted = useEventListener(
api,
MANAGER_WS_TASK_STARTED_NAME,
handleTaskStarted
)
stopTaskDone = useEventListener(
api,
MANAGER_WS_TASK_DONE_NAME,
handleTaskDone
)
}
}
const stopListening = async () => {
stop?.()
stop = null
stopLogs?.()
stopTaskStarted?.()
stopTaskDone?.()
stopLogs = null
stopTaskStarted = null
stopTaskDone = null
await api.subscribeLogs(false)
}

View File

@@ -1,3 +1,4 @@
{
"supports_preview_metadata": true
"supports_preview_metadata": true,
"supports_manager_v4_ui": true
}

View File

@@ -12,8 +12,17 @@ export const CORE_MENU_COMMANDS = [
]
],
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
[['Edit'], ['Comfy.ClearWorkflow']],
[['Edit'], ['Comfy.OpenClipspace']],
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
[
['Edit'],
[
'Comfy.RefreshNodeDefinitions',
'Comfy.Memory.UnloadModels',
'Comfy.Memory.UnloadModelsAndExecutionCache'
]
],
[
['Help'],
[
@@ -23,8 +32,5 @@ export const CORE_MENU_COMMANDS = [
'Comfy.Help.OpenComfyUIForum'
]
],
[
['Help'],
['Comfy.Help.AboutComfyUI', 'Comfy.Feedback', 'Comfy.ContactSupport']
]
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.ContactSupport']]
]

View File

@@ -13,6 +13,13 @@ import type { SettingParams } from '@/types/settingTypes'
* when they are no longer needed.
*/
export const CORE_SETTINGS: SettingParams[] = [
{
id: 'Comfy.Memory.AllowManualUnload',
name: 'Allow manual unload of models and execution cache via user command',
type: 'hidden',
defaultValue: true,
versionAdded: '1.18.0'
},
{
id: 'Comfy.Validation.Workflows',
name: 'Validate workflows',

View File

@@ -551,37 +551,7 @@
"updateConsent": "لقد وافقت سابقًا على الإبلاغ عن الأعطال. نحن الآن نتتبع إحصائيات مبنية على الأحداث للمساعدة في تحديد الأخطاء وتحسين التطبيق. لا يتم جمع معلومات شخصية قابلة للتعريف."
},
"issueReport": {
"contactFollowUp": "اتصل بي للمتابعة",
"contactSupportDescription": "يرجى ملء النموذج أدناه مع تقريرك",
"contactSupportTitle": "الاتصال بالدعم",
"describeTheProblem": "صف المشكلة",
"email": "البريد الإلكتروني",
"feedbackTitle": "ساعدنا في تحسين ComfyUI من خلال تقديم الملاحظات",
"helpFix": "المساعدة في الإصلاح",
"helpTypes": {
"billingPayments": "الفوترة / المدفوعات",
"bugReport": "تقرير خطأ",
"giveFeedback": "إرسال ملاحظات",
"loginAccessIssues": "مشكلة في تسجيل الدخول / الوصول",
"somethingElse": "أمر آخر"
},
"notifyResolve": "أعلمني عند الحل",
"provideAdditionalDetails": "أضف تفاصيل إضافية",
"provideEmail": "زودنا ببريدك الإلكتروني (اختياري)",
"rating": "التقييم",
"selectIssue": "اختر المشكلة",
"stackTrace": "أثر التكديس",
"submitErrorReport": "إرسال تقرير الخطأ (اختياري)",
"systemStats": "إحصائيات النظام",
"validation": {
"descriptionRequired": "الوصف مطلوب",
"helpTypeRequired": "نوع المساعدة مطلوب",
"invalidEmail": "يرجى إدخال بريد إلكتروني صالح",
"maxLength": "الرسالة طويلة جداً",
"selectIssueType": "يرجى اختيار نوع المشكلة"
},
"whatCanWeInclude": "حدد ما يجب تضمينه في التقرير",
"whatDoYouNeedHelpWith": "بماذا تحتاج المساعدة؟"
"helpFix": "المساعدة في الإصلاح"
},
"load3d": {
"applyingTexture": "جارٍ تطبيق الخامة...",

View File

@@ -167,8 +167,20 @@
"Comfy_LoadDefaultWorkflow": {
"label": "Load Default Workflow"
},
"Comfy_Manager_CustomNodesManager": {
"label": "Toggle the Custom Nodes Manager"
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "Custom Nodes Manager"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "Custom Nodes (Legacy)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "Manager Menu (Legacy)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "Install Missing Custom Nodes"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Check for Custom Node Updates"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Toggle the Custom Nodes Manager Progress Bar"
@@ -182,6 +194,12 @@
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Open Mask Editor for Selected Node"
},
"Comfy_Memory_UnloadModels": {
"label": "Unload Models"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "Unload Models and Execution Cache"
},
"Comfy_NewBlankWorkflow": {
"label": "New Blank Workflow"
},

View File

@@ -109,6 +109,7 @@
"resultsCount": "Found {count} Results",
"status": "Status",
"description": "Description",
"warning": "Warning",
"name": "Name",
"category": "Category",
"sort": "Sort",
@@ -122,9 +123,9 @@
"inProgress": "In progress",
"completed": "Completed",
"interrupted": "Interrupted",
"enabling": "Enabling",
"disabling": "Disabling",
"updating": "Updating",
"enabling": "Enabling {id}",
"disabling": "Disabling {id}",
"updating": "Updating {id}",
"migrate": "Migrate",
"updateAvailable": "Update Available",
"login": "Login",
@@ -143,6 +144,7 @@
"itemsSelected": "{selectedCount} items selected",
"progressCountOf": "of",
"keybindingAlreadyExists": "Keybinding already exists on",
"commandProhibited": "Command {command} is prohibited. Contact an administrator for more information.",
"startRecording": "Start Recording",
"stopRecording": "Stop Recording",
"micPermissionDenied": "Microphone permission denied",
@@ -153,7 +155,13 @@
},
"manager": {
"title": "Custom Nodes Manager",
"failed": "Failed ({count})",
"legacyMenuNotAvailable": "Legacy manager menu is not available, defaulting to the new manager menu.",
"legacyManagerUI": "Use Legacy UI",
"legacyManagerUIDescription": "To use the legacy Manager UI, start ComfyUI with --enable-manager-legacy-ui",
"failed": "Failed",
"failedToInstall": "Failed to Install",
"installError": "Install Error",
"importFailedGenericError": "Package failed to import. Check the console for more details.",
"noNodesFound": "No nodes found",
"noNodesFoundDescription": "The pack's nodes either could not be parsed, or the pack is a frontend extension only and doesn't have any nodes.",
"installationQueue": "Installation Queue",
@@ -161,15 +169,23 @@
"dependencies": "Dependencies",
"inWorkflow": "In Workflow",
"infoPanelEmpty": "Click an item to see the info",
"applyChanges": "Apply Changes",
"restartToApplyChanges": "To apply changes, please restart ComfyUI",
"clickToFinishSetup": "Click",
"toFinishSetup": "to finish setup",
"restartingBackend": "Restarting backend to apply changes...",
"extensionsSuccessfullyInstalled": "Extension(s) successfully installed and are ready to use!",
"installingDependencies": "Installing dependencies...",
"loadingVersions": "Loading versions...",
"selectVersion": "Select Version",
"downloads": "Downloads",
"repository": "Repository",
"uninstall": "Uninstall",
"uninstalling": "Uninstalling",
"uninstalling": "Uninstalling {id}",
"update": "Update",
"uninstallSelected": "Uninstall Selected",
"updateSelected": "Update Selected",
"updateAll": "Update All",
"updatingAllPacks": "Updating all packages",
"license": "License",
"nightlyVersion": "Nightly",
@@ -181,6 +197,7 @@
"noResultsFound": "No results found matching your search.",
"tryDifferentSearch": "Please try a different search query.",
"tryAgainLater": "Please try again later.",
"gettingInfo": "Getting info...",
"nodePack": "Node Pack",
"searchPlaceholder": "Search",
"version": "Version",
@@ -188,14 +205,18 @@
"noDescription": "No description available",
"installSelected": "Install Selected",
"installAllMissingNodes": "Install All Missing Nodes",
"packsSelected": "Packs Selected",
"packsSelected": "packs selected",
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
"notAvailable": "Not Available",
"status": {
"active": "Active",
"pending": "Pending",
"flagged": "Flagged",
"deleted": "Deleted",
"banned": "Banned",
"unknown": "Unknown"
"unknown": "Unknown",
"conflicting": "Conflicting",
"importFailed": "Install Error"
},
"sort": {
"downloads": "Most Popular",
@@ -207,40 +228,38 @@
"nodePack": "Node Pack",
"enabled": "Enabled",
"disabled": "Disabled"
},
"conflicts": {
"title": "Node Pack Issues Detected!",
"description": "We've detected conflicts between some of your extensions and the new version of ComfyUI. By updating you risk breaking workflows that rely on those extensions.",
"info": "If you continue with the update, the conflicting extensions will be disabled automatically. You can review and manage them anytime in the ComfyUI Manager.",
"extensionAtRisk": "Extension at Risk",
"conflicts": "Conflicts",
"importFailedExtensions": "Import Failed Extensions",
"conflictInfoTitle": "Why is this happening?",
"installAnyway": "Install Anyway",
"enableAnyway": "Enable Anyway",
"understood": "Understood",
"warningBanner": {
"title": "Some extensions are disabled due to incompatibility with your current setup",
"message": "These extensions require versions of system packages that differ from your current setup. Installing them may override core dependencies and affect other extensions or workflows.",
"button": "Learn More..."
},
"conflictMessages": {
"comfyui_version": "ComfyUI version mismatch (current: {current}, required: {required})",
"frontend_version": "Frontend version mismatch (current: {current}, required: {required})",
"os": "Operating system not supported (current: {current}, required: {required})",
"accelerator": "GPU/Accelerator not supported (available: {current}, required: {required})",
"generic": "Compatibility issue (current: {current}, required: {required})",
"banned": "This package is banned for security reasons",
"pending": "Security verification pending - compatibility cannot be verified",
"import_failed": "Import Failed"
},
"warningTooltip": "This package may have compatibility issues with your current environment"
}
},
"issueReport": {
"submitErrorReport": "Submit Error Report (Optional)",
"provideEmail": "Give us your email (optional)",
"provideAdditionalDetails": "Provide additional details",
"stackTrace": "Stack Trace",
"systemStats": "System Stats",
"contactFollowUp": "Contact me for follow up",
"notifyResolve": "Notify me when resolved",
"helpFix": "Help Fix This",
"rating": "Rating",
"feedbackTitle": "Help us improve ComfyUI by providing feedback",
"contactSupportTitle": "Contact Support",
"contactSupportDescription": "Please fill in the form below with your report",
"selectIssue": "Select the issue",
"whatDoYouNeedHelpWith": "What do you need help with?",
"whatCanWeInclude": "Specify what to include in the report",
"describeTheProblem": "Describe the problem",
"email": "Email",
"helpTypes": {
"billingPayments": "Billing / Payments",
"loginAccessIssues": "Login / Access Issues",
"giveFeedback": "Give Feedback",
"bugReport": "Bug Report",
"somethingElse": "Something Else"
},
"validation": {
"maxLength": "Message too long",
"invalidEmail": "Please enter a valid email address",
"selectIssueType": "Please select an issue type",
"descriptionRequired": "Description is required",
"helpTypeRequired": "Help type is required"
}
"helpFix": "Help Fix This"
},
"color": {
"noColor": "No Color",
@@ -435,6 +454,7 @@
"restart": "Restart"
},
"sideToolbar": {
"themeToggle": "Toggle Theme",
"helpCenter": "Help Center",
"logout": "Logout",
"queue": "Queue",
@@ -506,6 +526,7 @@
"docs": "Docs",
"github": "Github",
"helpFeedback": "Help & Feedback",
"managerExtension": "Manager Extension",
"more": "More...",
"whatsNew": "What's New?",
"clickToLearnMore": "Click to learn more →",
@@ -900,7 +921,8 @@
"zoomOptions": "Zoom Options",
"focusMode": "Focus Mode",
"hideLinks": "Hide Links",
"showLinks": "Show Links"
"showLinks": "Show Links",
"toggleLinkVisibility": "Toggle Link Visibility"
},
"zoomControls": {
"label": "Zoom Controls",
@@ -954,9 +976,29 @@
"Mask Opacity": "Mask Opacity",
"Image Layer": "Image Layer"
},
"commands": {
"runWorkflow": "Run workflow",
"runWorkflowFront": "Run workflow (Queue at front)",
"run": "Run",
"execute": "Execute",
"interrupt": "Cancel current run",
"refresh": "Refresh node definitions",
"clipspace": "Open Clipspace",
"resetView": "Reset canvas view",
"clear": "Clear workflow",
"toggleBottomPanel": "Toggle Bottom Panel",
"theme": "Theme",
"dark": "Dark",
"light": "Light",
"manageExtensions": "Manage Extensions",
"settings": "Settings",
"help": "Help",
"queue": "Queue Panel"
},
"menuLabels": {
"File": "File",
"Workflow": "Workflow",
"Edit": "Edit",
"Manager": "Manager",
"Help": "Help",
"Check for Updates": "Check for Updates",
"Open Custom Nodes Folder": "Open Custom Nodes Folder",
@@ -973,6 +1015,7 @@
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
"Browse Templates": "Browse Templates",
"Delete Selected Items": "Delete Selected Items",
"Fit view to selected nodes": "Fit view to selected nodes",
"Zoom to fit": "Zoom to fit",
"Lock Canvas": "Lock Canvas",
"Move Selected Nodes Down": "Move Selected Nodes Down",
@@ -981,9 +1024,8 @@
"Move Selected Nodes Up": "Move Selected Nodes Up",
"Reset View": "Reset View",
"Resize Selected Nodes": "Resize Selected Nodes",
"Node Links": "Node Links",
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
"Canvas Toggle Lock": "Canvas Toggle Lock",
"Minimap": "Minimap",
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
@@ -1014,11 +1056,16 @@
"ComfyUI Issues": "ComfyUI Issues",
"Interrupt": "Interrupt",
"Load Default Workflow": "Load Default Workflow",
"Toggle the Custom Nodes Manager": "Toggle the Custom Nodes Manager",
"Custom Nodes Manager": "Custom Nodes Manager",
"Custom Nodes (Legacy)": "Custom Nodes (Legacy)",
"Manager Menu (Legacy)": "Manager Menu (Legacy)",
"Install Missing": "Install Missing",
"Install Missing Custom Nodes": "Install Missing Custom Nodes",
"Check for Custom Node Updates": "Check for Custom Node Updates",
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
"Decrease Brush Size in MaskEditor": "Decrease Brush Size in MaskEditor",
"Increase Brush Size in MaskEditor": "Increase Brush Size in MaskEditor",
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
"Unload Models": "Unload Models",
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
"New": "New",
"Clipspace": "Clipspace",
"Manager": "Manager",
@@ -1041,17 +1088,14 @@
"Next Opened Workflow": "Next Opened Workflow",
"Previous Opened Workflow": "Previous Opened Workflow",
"Toggle Search Box": "Toggle Search Box",
"Bottom Panel": "Bottom Panel",
"Show Keybindings Dialog": "Show Keybindings Dialog",
"Toggle Bottom Panel": "Toggle Bottom Panel",
"Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel",
"Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel",
"Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel",
"Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel",
"Focus Mode": "Focus Mode",
"Model Library": "Model Library",
"Node Library": "Node Library",
"Queue Panel": "Queue Panel",
"Workflows": "Workflows"
"Toggle Focus Mode": "Toggle Focus Mode",
"Toggle Model Library Sidebar": "Toggle Model Library Sidebar",
"Toggle Node Library Sidebar": "Toggle Node Library Sidebar",
"Toggle Queue Sidebar": "Toggle Queue Sidebar",
"Toggle Workflows Sidebar": "Toggle Workflows Sidebar"
},
"desktopMenu": {
"reinstall": "Reinstall",
@@ -1643,6 +1687,7 @@
"minLength": "Must be at least {length} characters",
"maxLength": "Must be no more than {length} characters",
"prefix": "Must start with {prefix}",
"descriptionRequired": "Description is required",
"length": "Must be {length} characters",
"password": {
"requirements": "Password requirements",

View File

@@ -170,8 +170,20 @@
"Comfy_LoadDefaultWorkflow": {
"label": "Cargar flujo de trabajo predeterminado"
},
"Comfy_Manager_CustomNodesManager": {
"label": "Administrador de nodos personalizados"
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "Nodos personalizados (Beta)"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "Nodos personalizados (heredados)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "Menú del administrador (heredado)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "Instalar faltantes"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Buscar actualizaciones"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Alternar diálogo de progreso del administrador"
@@ -185,6 +197,12 @@
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Abrir editor de máscara para el nodo seleccionado"
},
"Comfy_Memory_UnloadModels": {
"label": "Descargar modelos"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "Descargar modelos y caché de ejecución"
},
"Comfy_NewBlankWorkflow": {
"label": "Nuevo flujo de trabajo en blanco"
},

View File

@@ -287,6 +287,7 @@
"color": "Color",
"comingSoon": "Próximamente",
"command": "Comando",
"commandProhibited": "El comando {command} está prohibido. Contacta a un administrador para más información.",
"community": "Comunidad",
"completed": "Completado",
"confirm": "Confirmar",
@@ -309,7 +310,6 @@
"disabling": "Deshabilitando",
"dismiss": "Descartar",
"download": "Descargar",
"duplicate": "Duplicar",
"edit": "Editar",
"empty": "Vacío",
"enableAll": "Habilitar todo",
@@ -322,6 +322,7 @@
"feedback": "Retroalimentación",
"filter": "Filtrar",
"findIssues": "Encontrar problemas",
"firstTimeUIMessage": "Esta es la primera vez que usas la nueva interfaz. Elige \"Menú > Usar nuevo menú > Desactivado\" para restaurar la antigua interfaz.",
"frontendNewer": "La versión del frontend {frontendVersion} puede no ser compatible con la versión del backend {backendVersion}.",
"frontendOutdated": "La versión del frontend {frontendVersion} está desactualizada. El backend requiere la versión {requiredVersion} o superior.",
"goToNode": "Ir al nodo",
@@ -419,17 +420,12 @@
},
"graphCanvasMenu": {
"fitView": "Ajustar vista",
"focusMode": "Modo de enfoque",
"hand": "Mano",
"hideLinks": "Ocultar enlaces",
"panMode": "Modo de desplazamiento",
"resetView": "Restablecer vista",
"select": "Seleccionar",
"selectMode": "Modo de selección",
"showLinks": "Mostrar enlaces",
"toggleLinkVisibility": "Alternar visibilidad de enlace",
"toggleMinimap": "Alternar minimapa",
"zoomIn": "Acercar",
"zoomOptions": "Opciones de zoom",
"zoomOut": "Alejar"
},
"groupNode": {
@@ -551,46 +547,12 @@
"updateConsent": "Anteriormente optaste por reportar fallos. Ahora estamos rastreando métricas basadas en eventos para ayudar a identificar errores y mejorar la aplicación. No se recoge ninguna información personal identificable."
},
"issueReport": {
"contactFollowUp": "Contáctame para seguimiento",
"contactSupportDescription": "Por favor, complete el siguiente formulario con su reporte",
"contactSupportTitle": "Contactar Soporte",
"describeTheProblem": "Describa el problema",
"email": "Correo electrónico",
"feedbackTitle": "Ayúdanos a mejorar ComfyUI proporcionando comentarios",
"helpFix": "Ayuda a Solucionar Esto",
"helpTypes": {
"billingPayments": "Facturación / Pagos",
"bugReport": "Reporte de error",
"giveFeedback": "Enviar comentarios",
"loginAccessIssues": "Problemas de inicio de sesión / acceso",
"somethingElse": "Otro"
},
"notifyResolve": "Notifícame cuando se resuelva",
"provideAdditionalDetails": "Proporciona detalles adicionales (opcional)",
"provideEmail": "Danos tu correo electrónico (opcional)",
"rating": "Calificación",
"selectIssue": "Seleccione el problema",
"stackTrace": "Rastreo de Pila",
"submitErrorReport": "Enviar Reporte de Error (Opcional)",
"systemStats": "Estadísticas del Sistema",
"validation": {
"descriptionRequired": "Se requiere una descripción",
"helpTypeRequired": "Se requiere el tipo de ayuda",
"invalidEmail": "Por favor ingresa una dirección de correo electrónico válida",
"maxLength": "Mensaje demasiado largo",
"selectIssueType": "Por favor, seleccione un tipo de problema"
},
"whatCanWeInclude": "Especifique qué incluir en el reporte",
"whatDoYouNeedHelpWith": "¿Con qué necesita ayuda?"
"helpFix": "Ayuda a Solucionar Esto"
},
"load3d": {
"applyingTexture": "Aplicando textura...",
"backgroundColor": "Color de fondo",
"camera": "Cámara",
"cameraType": {
"orthographic": "Ortográfica",
"perspective": "Perspectiva"
},
"clearRecording": "Borrar grabación",
"edgeThreshold": "Umbral de borde",
"export": "Exportar",
@@ -611,7 +573,6 @@
"wireframe": "Malla"
},
"model": "Modelo",
"openIn3DViewer": "Abrir en el visor 3D",
"previewOutput": "Vista previa de salida",
"removeBackgroundImage": "Eliminar imagen de fondo",
"resizeNodeMatchOutput": "Redimensionar nodo para coincidir con la salida",
@@ -622,22 +583,8 @@
"switchCamera": "Cambiar cámara",
"switchingMaterialMode": "Cambiando modo de material...",
"upDirection": "Dirección hacia arriba",
"upDirections": {
"original": "Original"
},
"uploadBackgroundImage": "Subir imagen de fondo",
"uploadTexture": "Subir textura",
"viewer": {
"apply": "Aplicar",
"cameraSettings": "Configuración de la cámara",
"cameraType": "Tipo de cámara",
"cancel": "Cancelar",
"exportSettings": "Configuración de exportación",
"lightSettings": "Configuración de la luz",
"modelSettings": "Configuración del modelo",
"sceneSettings": "Configuración de la escena",
"title": "Visor 3D (Beta)"
}
"uploadTexture": "Subir textura"
},
"loadWorkflowWarning": {
"coreNodesFromVersion": "Requiere ComfyUI {version}:",
@@ -684,6 +631,9 @@
"installationQueue": "Cola de Instalación",
"lastUpdated": "Última Actualización",
"latestVersion": "Última",
"legacyManagerUI": "Usar UI antigua",
"legacyManagerUIDescription": "Para usar la UI antigua del Manager, inicia ComfyUI con --enable-manager-legacy-ui",
"legacyMenuNotAvailable": "El menú del administrador antiguo no está disponible en esta versión de ComfyUI. Por favor, utiliza el nuevo menú del administrador en su lugar.",
"license": "Licencia",
"loadingVersions": "Cargando versiones...",
"nightlyVersion": "Nocturna",
@@ -766,7 +716,6 @@
"manageExtensions": "Gestionar extensiones",
"onChange": "Al cambiar",
"onChangeTooltip": "El flujo de trabajo se encolará una vez que se haga un cambio",
"queue": "Panel de cola",
"refresh": "Actualizar definiciones de nodos",
"resetView": "Restablecer vista del lienzo",
"run": "Ejecutar",
@@ -782,8 +731,10 @@
"Bottom Panel": "Panel inferior",
"Browse Templates": "Explorar plantillas",
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
"Canvas Performance": "Rendimiento del lienzo",
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
"Check for Custom Node Updates": "Buscar actualizaciones de nodos personalizados",
"Check for Updates": "Buscar actualizaciones",
"Clear Pending Tasks": "Borrar tareas pendientes",
"Clear Workflow": "Borrar flujo de trabajo",
@@ -797,28 +748,27 @@
"Contact Support": "Contactar soporte",
"Convert Selection to Subgraph": "Convertir selección en subgrafo",
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
"Custom Nodes (Legacy)": "Nodos personalizados (heredado)",
"Custom Nodes Manager": "Administrador de Nodos Personalizados",
"Decrease Brush Size in MaskEditor": "Disminuir tamaño del pincel en MaskEditor",
"Delete Selected Items": "Eliminar elementos seleccionados",
"Desktop User Guide": "Guía de usuario de escritorio",
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
"Edit": "Editar",
"Exit Subgraph": "Salir de subgrafo",
"Export": "Exportar",
"Export (API)": "Exportar (API)",
"File": "Archivo",
"Fit Group To Contents": "Ajustar grupo a contenidos",
"Focus Mode": "Modo de enfoque",
"Fit view to selected nodes": "Ajustar vista a los nodos seleccionados",
"Give Feedback": "Dar retroalimentación",
"Group Selected Nodes": "Agrupar nodos seleccionados",
"Help": "Ayuda",
"Help Center": "Centro de ayuda",
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
"Install Missing Custom Nodes": "Instalar nodos personalizados faltantes",
"Interrupt": "Interrumpir",
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
"Lock Canvas": "Bloquear lienzo",
"Manage group nodes": "Gestionar nodos de grupo",
"Manager": "Administrador",
"Minimap": "Minimapa",
"Model Library": "Biblioteca de modelos",
"Manager Menu (Legacy)": "Menú de gestión (heredado)",
"Move Selected Nodes Down": "Mover nodos seleccionados hacia abajo",
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
"Move Selected Nodes Right": "Mover nodos seleccionados hacia la derecha",
@@ -826,10 +776,7 @@
"Mute/Unmute Selected Nodes": "Silenciar/Activar sonido de nodos seleccionados",
"New": "Nuevo",
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
"Node Library": "Biblioteca de nodos",
"Node Links": "Enlaces de nodos",
"Open": "Abrir",
"Open 3D Viewer (Beta) for Selected Node": "Abrir visor 3D (Beta) para el nodo seleccionado",
"Open Custom Nodes Folder": "Abrir carpeta de nodos personalizados",
"Open DevTools": "Abrir DevTools",
"Open Inputs Folder": "Abrir carpeta de entradas",
@@ -842,7 +789,6 @@
"Pin/Unpin Selected Items": "Anclar/Desanclar elementos seleccionados",
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
"Previous Opened Workflow": "Flujo de trabajo abierto anterior",
"Queue Panel": "Panel de cola",
"Queue Prompt": "Indicador de cola",
"Queue Prompt (Front)": "Indicador de cola (Frente)",
"Queue Selected Output Nodes": "Encolar nodos de salida seleccionados",
@@ -859,29 +805,24 @@
"Show Model Selector (Dev)": "Mostrar selector de modelo (Desarrollo)",
"Show Settings Dialog": "Mostrar diálogo de configuración",
"Sign Out": "Cerrar sesión",
"Toggle Essential Bottom Panel": "Alternar panel inferior esencial",
"Toggle Bottom Panel": "Alternar panel inferior",
"Toggle Focus Mode": "Alternar modo de enfoque",
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
"Toggle Model Library Sidebar": "Alternar barra lateral de la biblioteca de modelos",
"Toggle Node Library Sidebar": "Alternar barra lateral de la biblioteca de nodos",
"Toggle Queue Sidebar": "Alternar barra lateral de la cola",
"Toggle Search Box": "Alternar caja de búsqueda",
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
"Toggle View Controls Bottom Panel": "Alternar panel inferior de controles de vista",
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
"Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo",
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
"Undo": "Deshacer",
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
"Unlock Canvas": "Desbloquear lienzo",
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
"Workflows": "Flujos de trabajo",
"Unload Models": "Descargar modelos",
"Unload Models and Execution Cache": "Descargar modelos y caché de ejecución",
"Workflow": "Flujo de trabajo",
"Zoom In": "Acercar",
"Zoom Out": "Alejar",
"Zoom to fit": "Ajustar al tamaño"
},
"minimap": {
"nodeColors": "Colores de nodos",
"renderBypassState": "Mostrar estado de omisión",
"renderErrorState": "Mostrar estado de error",
"showGroups": "Mostrar marcos/grupos",
"showLinks": "Mostrar enlaces"
"Zoom Out": "Alejar"
},
"missingModelsDialog": {
"doNotAskAgain": "No mostrar esto de nuevo",
@@ -948,6 +889,7 @@
"sampling": "muestreo",
"schedulers": "programadores",
"scheduling": "programación",
"sd": "sd",
"sd3": "sd3",
"sigmas": "sigmas",
"stable_cascade": "stable_cascade",
@@ -957,6 +899,9 @@
"upscale_diffusion": "difusión_de_escalado",
"upscaling": "escalado",
"utils": "utilidades",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "video",
"video_models": "modelos_de_video"
},
@@ -1145,7 +1090,6 @@
},
"settingsCategories": {
"3D": "3D",
"3DViewer": "Visor 3D",
"API Nodes": "Nodos API",
"About": "Acerca de",
"Appearance": "Apariencia",
@@ -1197,31 +1141,10 @@
"Window": "Ventana",
"Workflow": "Flujo de Trabajo"
},
"shortcuts": {
"essentials": "Esenciales",
"keyboardShortcuts": "Atajos de teclado",
"manageShortcuts": "Gestionar atajos",
"noKeybinding": "Sin asignación de tecla",
"subcategories": {
"node": "Nodo",
"panelControls": "Controles del panel",
"queue": "Cola",
"view": "Vista",
"workflow": "Flujo de trabajo"
},
"viewControls": "Controles de vista"
},
"sideToolbar": {
"browseTemplates": "Explorar plantillas de ejemplo",
"downloads": "Descargas",
"helpCenter": "Centro de ayuda",
"labels": {
"models": "Modelos",
"nodes": "Nodos",
"queue": "Cola",
"templates": "Plantillas",
"workflows": "Flujos de trabajo"
},
"logout": "Cerrar sesión",
"modelLibrary": "Biblioteca de modelos",
"newBlankWorkflow": "Crear un nuevo flujo de trabajo en blanco",
@@ -1259,7 +1182,6 @@
},
"showFlatList": "Mostrar lista plana"
},
"templates": "Plantillas",
"workflowTab": {
"confirmDelete": "¿Estás seguro de que quieres eliminar este flujo de trabajo?",
"confirmDeleteTitle": "¿Eliminar flujo de trabajo?",
@@ -1306,8 +1228,6 @@
"Video": "Video",
"Video API": "API de Video"
},
"loadingMore": "Cargando más plantillas...",
"searchPlaceholder": "Buscar plantillas...",
"template": {
"3D": {
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
@@ -1630,7 +1550,6 @@
"failedToExportModel": "Error al exportar modelo como {format}",
"failedToFetchBalance": "No se pudo obtener el saldo: {error}",
"failedToFetchLogs": "Error al obtener los registros del servidor",
"failedToInitializeLoad3dViewer": "No se pudo inicializar el visor 3D",
"failedToInitiateCreditPurchase": "No se pudo iniciar la compra de créditos: {error}",
"failedToPurchaseCredits": "No se pudo comprar créditos: {error}",
"fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}",
@@ -1705,11 +1624,5 @@
"enterFilename": "Introduzca el nombre del archivo",
"exportWorkflow": "Exportar flujo de trabajo",
"saveWorkflow": "Guardar flujo de trabajo"
},
"zoomControls": {
"hideMinimap": "Ocultar minimapa",
"label": "Controles de zoom",
"showMinimap": "Mostrar minimapa",
"zoomToFit": "Ajustar al zoom"
}
}
}

View File

@@ -170,8 +170,20 @@
"Comfy_LoadDefaultWorkflow": {
"label": "Charger le flux de travail par défaut"
},
"Comfy_Manager_CustomNodesManager": {
"label": "Gestionnaire de Nœuds Personnalisés"
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "Nœuds personnalisés (Beta)"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "Nœuds personnalisés (hérités)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "Menu du gestionnaire (héritage)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "Installer manquants"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Vérifier les mises à jour"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Basculer la boîte de dialogue de progression"
@@ -185,6 +197,12 @@
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Ouvrir l'éditeur de masque pour le nœud sélectionné"
},
"Comfy_Memory_UnloadModels": {
"label": "Décharger les modèles"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "Décharger les modèles et le cache d'exécution"
},
"Comfy_NewBlankWorkflow": {
"label": "Nouveau flux de travail vierge"
},

View File

@@ -287,6 +287,7 @@
"color": "Couleur",
"comingSoon": "Bientôt disponible",
"command": "Commande",
"commandProhibited": "La commande {command} est interdite. Contactez un administrateur pour plus d'informations.",
"community": "Communauté",
"completed": "Terminé",
"confirm": "Confirmer",
@@ -309,7 +310,6 @@
"disabling": "Désactivation",
"dismiss": "Fermer",
"download": "Télécharger",
"duplicate": "Dupliquer",
"edit": "Modifier",
"empty": "Vide",
"enableAll": "Activer tout",
@@ -322,6 +322,7 @@
"feedback": "Commentaires",
"filter": "Filtrer",
"findIssues": "Trouver des problèmes",
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
"frontendNewer": "La version du frontend {frontendVersion} peut ne pas être compatible avec la version du backend {backendVersion}.",
"frontendOutdated": "La version du frontend {frontendVersion} est obsolète. Le backend requiert la version {requiredVersion} ou supérieure.",
"goToNode": "Aller au nœud",
@@ -419,17 +420,12 @@
},
"graphCanvasMenu": {
"fitView": "Adapter la vue",
"focusMode": "Mode focus",
"hand": "Main",
"hideLinks": "Masquer les liens",
"panMode": "Mode panoramique",
"resetView": "Réinitialiser la vue",
"select": "Sélectionner",
"selectMode": "Mode sélection",
"showLinks": "Afficher les liens",
"toggleLinkVisibility": "Basculer la visibilité des liens",
"toggleMinimap": "Afficher/Masquer la mini-carte",
"zoomIn": "Zoom avant",
"zoomOptions": "Options de zoom",
"zoomOut": "Zoom arrière"
},
"groupNode": {
@@ -551,46 +547,12 @@
"updateConsent": "Vous avez précédemment accepté de signaler les plantages. Nous suivons maintenant des métriques basées sur les événements pour aider à identifier les bugs et améliorer l'application. Aucune information personnelle identifiable n'est collectée."
},
"issueReport": {
"contactFollowUp": "Contactez-moi pour un suivi",
"contactSupportDescription": "Veuillez remplir le formulaire ci-dessous avec votre signalement",
"contactSupportTitle": "Contacter le support",
"describeTheProblem": "Décrivez le problème",
"email": "E-mail",
"feedbackTitle": "Aidez-nous à améliorer ComfyUI en fournissant des commentaires",
"helpFix": "Aidez à résoudre cela",
"helpTypes": {
"billingPayments": "Facturation / Paiements",
"bugReport": "Signaler un bug",
"giveFeedback": "Donner un avis",
"loginAccessIssues": "Problèmes de connexion / d'accès",
"somethingElse": "Autre chose"
},
"notifyResolve": "Prévenez-moi lorsque résolu",
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
"provideEmail": "Donnez-nous votre email (Facultatif)",
"rating": "Évaluation",
"selectIssue": "Sélectionnez le problème",
"stackTrace": "Trace de la pile",
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
"systemStats": "Statistiques du système",
"validation": {
"descriptionRequired": "La description est requise",
"helpTypeRequired": "Le type d'aide est requis",
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
"maxLength": "Message trop long",
"selectIssueType": "Veuillez sélectionner un type de problème"
},
"whatCanWeInclude": "Précisez ce qu'il faut inclure dans le rapport",
"whatDoYouNeedHelpWith": "Avec quoi avez-vous besoin d'aide ?"
"helpFix": "Aidez à résoudre cela"
},
"load3d": {
"applyingTexture": "Application de la texture...",
"backgroundColor": "Couleur de fond",
"camera": "Caméra",
"cameraType": {
"orthographic": "Orthographique",
"perspective": "Perspective"
},
"clearRecording": "Effacer l'enregistrement",
"edgeThreshold": "Seuil de Bordure",
"export": "Exportation",
@@ -611,7 +573,6 @@
"wireframe": "Fil de fer"
},
"model": "Modèle",
"openIn3DViewer": "Ouvrir dans la visionneuse 3D",
"previewOutput": "Aperçu de la sortie",
"removeBackgroundImage": "Supprimer l'image de fond",
"resizeNodeMatchOutput": "Redimensionner le nœud pour correspondre à la sortie",
@@ -622,22 +583,8 @@
"switchCamera": "Changer de caméra",
"switchingMaterialMode": "Changement de mode de matériau...",
"upDirection": "Direction Haut",
"upDirections": {
"original": "Original"
},
"uploadBackgroundImage": "Télécharger l'image de fond",
"uploadTexture": "Télécharger Texture",
"viewer": {
"apply": "Appliquer",
"cameraSettings": "Paramètres de la caméra",
"cameraType": "Type de caméra",
"cancel": "Annuler",
"exportSettings": "Paramètres dexportation",
"lightSettings": "Paramètres de léclairage",
"modelSettings": "Paramètres du modèle",
"sceneSettings": "Paramètres de la scène",
"title": "Visionneuse 3D (Bêta)"
}
"uploadTexture": "Télécharger Texture"
},
"loadWorkflowWarning": {
"coreNodesFromVersion": "Nécessite ComfyUI {version} :",
@@ -684,6 +631,9 @@
"installationQueue": "File d'attente d'installation",
"lastUpdated": "Dernière mise à jour",
"latestVersion": "Dernière",
"legacyManagerUI": "Utiliser l'interface utilisateur héritée",
"legacyManagerUIDescription": "Pour utiliser l'interface utilisateur de gestion héritée, démarrez ComfyUI avec --enable-manager-legacy-ui",
"legacyMenuNotAvailable": "Le menu du gestionnaire de l'ancienne version n'est pas disponible dans cette version de ComfyUI. Veuillez utiliser le nouveau menu du gestionnaire à la place.",
"license": "Licence",
"loadingVersions": "Chargement des versions...",
"nightlyVersion": "Nocturne",
@@ -766,7 +716,6 @@
"manageExtensions": "Gérer les extensions",
"onChange": "Sur modification",
"onChangeTooltip": "Le flux de travail sera mis en file d'attente une fois une modification effectuée",
"queue": "Panneau de file dattente",
"refresh": "Actualiser les définitions des nœuds",
"resetView": "Réinitialiser la vue du canevas",
"run": "Exécuter",
@@ -782,8 +731,10 @@
"Bottom Panel": "Panneau inférieur",
"Browse Templates": "Parcourir les modèles",
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
"Canvas Performance": "Performance du canevas",
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
"Canvas Toggle Minimap": "Basculer la mini-carte du canevas",
"Check for Custom Node Updates": "Vérifier les mises à jour des nœuds personnalisés",
"Check for Updates": "Vérifier les mises à jour",
"Clear Pending Tasks": "Effacer les tâches en attente",
"Clear Workflow": "Effacer le flux de travail",
@@ -797,28 +748,27 @@
"Contact Support": "Contacter le support",
"Convert Selection to Subgraph": "Convertir la sélection en sous-graphe",
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
"Custom Nodes (Legacy)": "Nœuds personnalisés (héritage)",
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
"Decrease Brush Size in MaskEditor": "Réduire la taille du pinceau dans MaskEditor",
"Delete Selected Items": "Supprimer les éléments sélectionnés",
"Desktop User Guide": "Guide de l'utilisateur de bureau",
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
"Edit": "Éditer",
"Exit Subgraph": "Quitter le sous-graphe",
"Export": "Exporter",
"Export (API)": "Exporter (API)",
"File": "Fichier",
"Fit Group To Contents": "Ajuster le groupe au contenu",
"Focus Mode": "Mode focus",
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
"Give Feedback": "Donnez votre avis",
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
"Help": "Aide",
"Help Center": "Centre daide",
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
"Install Missing Custom Nodes": "Installer les nœuds personnalisés manquants",
"Interrupt": "Interrompre",
"Load Default Workflow": "Charger le flux de travail par défaut",
"Lock Canvas": "Verrouiller le canevas",
"Manage group nodes": "Gérer les nœuds de groupe",
"Manager": "Gestionnaire",
"Minimap": "Minicarte",
"Model Library": "Bibliothèque de modèles",
"Manager Menu (Legacy)": "Menu du gestionnaire (héritage)",
"Move Selected Nodes Down": "Déplacer les nœuds sélectionnés vers le bas",
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
"Move Selected Nodes Right": "Déplacer les nœuds sélectionnés vers la droite",
@@ -826,10 +776,7 @@
"Mute/Unmute Selected Nodes": "Mettre en sourdine/Activer le son des nœuds sélectionnés",
"New": "Nouveau",
"Next Opened Workflow": "Prochain flux de travail ouvert",
"Node Library": "Bibliothèque de nœuds",
"Node Links": "Liens de nœuds",
"Open": "Ouvrir",
"Open 3D Viewer (Beta) for Selected Node": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné",
"Open Custom Nodes Folder": "Ouvrir le dossier des nœuds personnalisés",
"Open DevTools": "Ouvrir DevTools",
"Open Inputs Folder": "Ouvrir le dossier des entrées",
@@ -842,7 +789,6 @@
"Pin/Unpin Selected Items": "Épingler/Désépingler les éléments sélectionnés",
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
"Previous Opened Workflow": "Flux de travail ouvert précédent",
"Queue Panel": "Panneau de file dattente",
"Queue Prompt": "Invite de file d'attente",
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
"Queue Selected Output Nodes": "Mettre en file dattente les nœuds de sortie sélectionnés",
@@ -861,6 +807,9 @@
"Sign Out": "Se déconnecter",
"Toggle Essential Bottom Panel": "Basculer le panneau inférieur essentiel",
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
"Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles",
"Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds",
"Toggle Queue Sidebar": "Afficher/Masquer la barre latérale de la file dattente",
"Toggle Search Box": "Basculer la boîte de recherche",
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
@@ -869,19 +818,11 @@
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
"Undo": "Annuler",
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
"Unlock Canvas": "Déverrouiller le canevas",
"Unpack the selected Subgraph": "Décompresser le Subgraph sélection",
"Workflows": "Flux de travail",
"Unload Models": "Décharger les modèles",
"Unload Models and Execution Cache": "Décharger les modèles et le cache d'exécution",
"Workflow": "Flux de travail",
"Zoom In": "Zoom avant",
"Zoom Out": "Zoom arrière",
"Zoom to fit": "Ajuster à lécran"
},
"minimap": {
"nodeColors": "Couleurs des nœuds",
"renderBypassState": "Afficher létat de contournement",
"renderErrorState": "Afficher létat derreur",
"showGroups": "Afficher les cadres/groupes",
"showLinks": "Afficher les liens"
"Zoom Out": "Zoom arrière"
},
"missingModelsDialog": {
"doNotAskAgain": "Ne plus afficher ce message",
@@ -948,6 +889,7 @@
"sampling": "échantillonnage",
"schedulers": "planificateurs",
"scheduling": "planification",
"sd": "sd",
"sd3": "sd3",
"sigmas": "sigmas",
"stable_cascade": "stable_cascade",
@@ -957,6 +899,9 @@
"upscale_diffusion": "diffusion_de_mise_à_l'échelle",
"upscaling": "mise_à_l'échelle",
"utils": "utilitaires",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "vidéo",
"video_models": "modèles_vidéo"
},
@@ -1145,7 +1090,6 @@
},
"settingsCategories": {
"3D": "3D",
"3DViewer": "Visionneuse 3D",
"API Nodes": "Nœuds API",
"About": "À Propos",
"Appearance": "Apparence",
@@ -1197,31 +1141,10 @@
"Window": "Fenêtre",
"Workflow": "Flux de Travail"
},
"shortcuts": {
"essentials": "Essentiel",
"keyboardShortcuts": "Raccourcis clavier",
"manageShortcuts": "Gérer les raccourcis",
"noKeybinding": "Aucun raccourci",
"subcategories": {
"node": "Nœud",
"panelControls": "Contrôles du panneau",
"queue": "File dattente",
"view": "Vue",
"workflow": "Flux de travail"
},
"viewControls": "Contrôles daffichage"
},
"sideToolbar": {
"browseTemplates": "Parcourir les modèles d'exemple",
"downloads": "Téléchargements",
"helpCenter": "Centre d'aide",
"labels": {
"models": "Modèles",
"nodes": "Nœuds",
"queue": "File dattente",
"templates": "Modèles",
"workflows": "Flux de travail"
},
"logout": "Déconnexion",
"modelLibrary": "Bibliothèque de modèles",
"newBlankWorkflow": "Créer un nouveau flux de travail vierge",
@@ -1259,7 +1182,6 @@
},
"showFlatList": "Afficher la liste plate"
},
"templates": "Modèles",
"workflowTab": {
"confirmDelete": "Êtes-vous sûr de vouloir supprimer ce flux de travail ?",
"confirmDeleteTitle": "Supprimer le flux de travail ?",
@@ -1306,8 +1228,6 @@
"Video": "Vidéo",
"Video API": "API vidéo"
},
"loadingMore": "Chargement de plus de modèles...",
"searchPlaceholder": "Rechercher des modèles...",
"template": {
"3D": {
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
@@ -1630,7 +1550,6 @@
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
"failedToFetchBalance": "Échec de la récupération du solde : {error}",
"failedToFetchLogs": "Échec de la récupération des journaux du serveur",
"failedToInitializeLoad3dViewer": "Échec de l'initialisation du visualiseur 3D",
"failedToInitiateCreditPurchase": "Échec de l'initiation de l'achat de crédits : {error}",
"failedToPurchaseCredits": "Échec de l'achat de crédits : {error}",
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
@@ -1705,11 +1624,5 @@
"enterFilename": "Entrez le nom du fichier",
"exportWorkflow": "Exporter le flux de travail",
"saveWorkflow": "Enregistrer le flux de travail"
},
"zoomControls": {
"hideMinimap": "Masquer la mini-carte",
"label": "Contrôles de zoom",
"showMinimap": "Afficher la mini-carte",
"zoomToFit": "Ajuster à lécran"
}
}
}

View File

@@ -170,8 +170,20 @@
"Comfy_LoadDefaultWorkflow": {
"label": "デフォルトのワークフローを読み込む"
},
"Comfy_Manager_CustomNodesManager": {
"label": "カスタムノードマネージャ"
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "カスタムノード(ベータ版)"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "カスタムノード(レガシー)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "マネージャーメニュー(レガシー)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "不足しているパックをインストール"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "更新を確認"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "プログレスダイアログの切り替え"
@@ -185,6 +197,12 @@
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "選択したノードのマスクエディタを開く"
},
"Comfy_Memory_UnloadModels": {
"label": "モデルのアンロード"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "モデルと実行キャッシュのアンロード"
},
"Comfy_NewBlankWorkflow": {
"label": "新しい空のワークフロー"
},

View File

@@ -287,6 +287,7 @@
"color": "色",
"comingSoon": "近日公開",
"command": "コマンド",
"commandProhibited": "コマンド {command} は禁止されています。詳細は管理者にお問い合わせください。",
"community": "コミュニティ",
"completed": "完了",
"confirm": "確認",
@@ -309,7 +310,6 @@
"disabling": "無効化",
"dismiss": "閉じる",
"download": "ダウンロード",
"duplicate": "複製",
"edit": "編集",
"empty": "空",
"enableAll": "すべて有効にする",
@@ -322,6 +322,7 @@
"feedback": "フィードバック",
"filter": "フィルタ",
"findIssues": "問題を見つける",
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択することで古いUIに戻すことが可能です。",
"frontendNewer": "フロントエンドのバージョン {frontendVersion} はバックエンドのバージョン {backendVersion} と互換性がない可能性があります。",
"frontendOutdated": "フロントエンドのバージョン {frontendVersion} は古くなっています。バックエンドは {requiredVersion} 以上が必要です。",
"goToNode": "ノードに移動",
@@ -419,17 +420,12 @@
},
"graphCanvasMenu": {
"fitView": "ビューに合わせる",
"focusMode": "フォーカスモード",
"hand": "手のひら",
"hideLinks": "リンクを非表示",
"panMode": "パンモード",
"resetView": "ビューをリセット",
"select": "選択",
"selectMode": "選択モード",
"showLinks": "リンク表示",
"toggleLinkVisibility": "リンク表示切り替え",
"toggleMinimap": "ミニマップの切り替え",
"zoomIn": "拡大",
"zoomOptions": "ズームオプション",
"zoomOut": "縮小"
},
"groupNode": {
@@ -551,46 +547,12 @@
"updateConsent": "以前、クラッシュレポートを報告することに同意していました。現在、バグの特定とアプリの改善を助けるためにイベントベースのメトリクスを追跡しています。個人を特定できる情報は収集されません。"
},
"issueReport": {
"contactFollowUp": "フォローアップのために私に連絡する",
"contactSupportDescription": "下記のフォームにご報告内容をご記入ください",
"contactSupportTitle": "サポートに連絡",
"describeTheProblem": "問題の内容を記述してください",
"email": "メールアドレス",
"feedbackTitle": "フィードバックを提供してComfyUIの改善にご協力ください",
"helpFix": "これを修正するのを助ける",
"helpTypes": {
"billingPayments": "請求/支払い",
"bugReport": "バグ報告",
"giveFeedback": "フィードバックを送る",
"loginAccessIssues": "ログイン/アクセスの問題",
"somethingElse": "その他"
},
"notifyResolve": "解決したときに通知する",
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
"rating": "評価",
"selectIssue": "問題を選択してください",
"stackTrace": "スタックトレース",
"submitErrorReport": "エラーレポートを提出する(オプション)",
"systemStats": "システム統計",
"validation": {
"descriptionRequired": "説明は必須です",
"helpTypeRequired": "ヘルプの種類は必須です",
"invalidEmail": "有効なメールアドレスを入力してください",
"maxLength": "メッセージが長すぎます",
"selectIssueType": "問題の種類を選択してください"
},
"whatCanWeInclude": "レポートに含める内容を指定してください",
"whatDoYouNeedHelpWith": "どのようなサポートが必要ですか?"
"helpFix": "これを修正するのを助ける"
},
"load3d": {
"applyingTexture": "テクスチャを適用中...",
"backgroundColor": "背景色",
"camera": "カメラ",
"cameraType": {
"orthographic": "オルソグラフィック",
"perspective": "パースペクティブ"
},
"clearRecording": "録画をクリア",
"edgeThreshold": "エッジ閾値",
"export": "エクスポート",
@@ -611,7 +573,6 @@
"wireframe": "ワイヤーフレーム"
},
"model": "モデル",
"openIn3DViewer": "3Dビューアで開く",
"previewOutput": "出力のプレビュー",
"removeBackgroundImage": "背景画像を削除",
"resizeNodeMatchOutput": "ノードを出力に合わせてリサイズ",
@@ -622,22 +583,8 @@
"switchCamera": "カメラを切り替える",
"switchingMaterialMode": "マテリアルモードの切り替え中...",
"upDirection": "上方向",
"upDirections": {
"original": "オリジナル"
},
"uploadBackgroundImage": "背景画像をアップロード",
"uploadTexture": "テクスチャをアップロード",
"viewer": {
"apply": "適用",
"cameraSettings": "カメラ設定",
"cameraType": "カメラタイプ",
"cancel": "キャンセル",
"exportSettings": "エクスポート設定",
"lightSettings": "ライト設定",
"modelSettings": "モデル設定",
"sceneSettings": "シーン設定",
"title": "3Dビューアベータ"
}
"uploadTexture": "テクスチャをアップロード"
},
"loadWorkflowWarning": {
"coreNodesFromVersion": "ComfyUI {version} が必要です:",
@@ -684,6 +631,9 @@
"installationQueue": "インストールキュー",
"lastUpdated": "最終更新日",
"latestVersion": "最新",
"legacyManagerUI": "レガシーUIを使用する",
"legacyManagerUIDescription": "レガシーManager UIを使用するには、--enable-manager-legacy-uiを付けてComfyUIを起動してください",
"legacyMenuNotAvailable": "このバージョンのComfyUIでは、レガシーマネージャーメニューは利用できません。新しいマネージャーメニューを使用してください。",
"license": "ライセンス",
"loadingVersions": "バージョンを読み込んでいます...",
"nightlyVersion": "ナイトリー",
@@ -766,7 +716,6 @@
"manageExtensions": "拡張機能の管理",
"onChange": "変更時",
"onChangeTooltip": "変更が行われるとワークフローがキューに追加されます",
"queue": "キューパネル",
"refresh": "ノードを更新",
"resetView": "ビューをリセット",
"run": "実行する",
@@ -782,8 +731,10 @@
"Bottom Panel": "下部パネル",
"Browse Templates": "テンプレートを参照",
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
"Canvas Performance": "キャンバスパフォーマンス",
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
"Check for Custom Node Updates": "カスタムノードのアップデートを確認",
"Check for Updates": "更新を確認する",
"Clear Pending Tasks": "保留中のタスクをクリア",
"Clear Workflow": "ワークフローをクリア",
@@ -797,28 +748,27 @@
"Contact Support": "サポートに連絡",
"Convert Selection to Subgraph": "選択範囲をサブグラフに変換",
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
"Custom Nodes (Legacy)": "カスタムノード(レガシー)",
"Custom Nodes Manager": "カスタムノードマネージャ",
"Decrease Brush Size in MaskEditor": "マスクエディタでブラシサイズを小さくする",
"Delete Selected Items": "選択したアイテムを削除",
"Desktop User Guide": "デスクトップユーザーガイド",
"Duplicate Current Workflow": "現在のワークフローを複製",
"Edit": "編集",
"Exit Subgraph": "サブグラフを終了",
"Export": "エクスポート",
"Export (API)": "エクスポート (API)",
"File": "ファイル",
"Fit Group To Contents": "グループを内容に合わせる",
"Focus Mode": "フォーカスモード",
"Fit view to selected nodes": "選択したノードにビューを合わせる",
"Give Feedback": "フィードバックを送る",
"Group Selected Nodes": "選択したノードをグループ化",
"Help": "ヘルプ",
"Help Center": "ヘルプセンター",
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
"Install Missing Custom Nodes": "不足しているカスタムノードをインストール",
"Interrupt": "中断",
"Load Default Workflow": "デフォルトワークフローを読み込む",
"Lock Canvas": "キャンバスをロック",
"Manage group nodes": "グループノードを管理",
"Manager": "マネージャー",
"Minimap": "ミニマップ",
"Model Library": "モデルライブラリ",
"Manager Menu (Legacy)": "マネージャーメニュー(レガシー)",
"Move Selected Nodes Down": "選択したノードを下へ移動",
"Move Selected Nodes Left": "選択したノードを左へ移動",
"Move Selected Nodes Right": "選択したノードを右へ移動",
@@ -826,10 +776,7 @@
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
"New": "新規",
"Next Opened Workflow": "次に開いたワークフロー",
"Node Library": "ノードライブラリ",
"Node Links": "ノードリンク",
"Open": "開く",
"Open 3D Viewer (Beta) for Selected Node": "選択したードの3Dビューアーベータを開く",
"Open Custom Nodes Folder": "カスタムノードフォルダを開く",
"Open DevTools": "DevToolsを開く",
"Open Inputs Folder": "入力フォルダを開く",
@@ -842,7 +789,6 @@
"Pin/Unpin Selected Items": "選択したアイテムのピン留め/ピン留め解除",
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
"Previous Opened Workflow": "前に開いたワークフロー",
"Queue Panel": "キューパネル",
"Queue Prompt": "キューのプロンプト",
"Queue Prompt (Front)": "キューのプロンプト (前面)",
"Queue Selected Output Nodes": "選択した出力ノードをキューに追加",
@@ -864,24 +810,15 @@
"Toggle Search Box": "検索ボックスの切り替え",
"Toggle Terminal Bottom Panel": "ターミナル下部パネルの切り替え",
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
"Toggle View Controls Bottom Panel": "ビューコントロール下部パネルの切り替え",
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
"Undo": "元に戻す",
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
"Unlock Canvas": "キャンバスのロックを解除",
"Unpack the selected Subgraph": "選択したサブグラフを展開",
"Workflows": "ワークフロー",
"Unload Models": "モデルのアンロード",
"Unload Models and Execution Cache": "モデルと実行キャッシュのアンロード",
"Workflow": "ワークフロー",
"Zoom In": "ズームイン",
"Zoom Out": "ズームアウト",
"Zoom to fit": "全体表示にズーム"
},
"minimap": {
"nodeColors": "ノードの色",
"renderBypassState": "バイパス状態を表示",
"renderErrorState": "エラー状態を表示",
"showGroups": "フレーム/グループを表示",
"showLinks": "リンクを表示"
"Zoom Out": "ズームアウト"
},
"missingModelsDialog": {
"doNotAskAgain": "再度表示しない",
@@ -948,6 +885,7 @@
"sampling": "サンプリング",
"schedulers": "スケジューラー",
"scheduling": "スケジューリング",
"sd": "sd",
"sd3": "SD3",
"sigmas": "シグマ",
"stable_cascade": "安定したカスケード",
@@ -957,6 +895,9 @@
"upscale_diffusion": "アップスケール拡散",
"upscaling": "アップスケーリング",
"utils": "ユーティリティ",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "ビデオ",
"video_models": "ビデオモデル"
},
@@ -1145,7 +1086,6 @@
},
"settingsCategories": {
"3D": "3D",
"3DViewer": "3Dビューア",
"API Nodes": "APIード",
"About": "情報",
"Appearance": "外観",
@@ -1197,31 +1137,10 @@
"Window": "ウィンドウ",
"Workflow": "ワークフロー"
},
"shortcuts": {
"essentials": "基本",
"keyboardShortcuts": "キーボードショートカット",
"manageShortcuts": "ショートカットの管理",
"noKeybinding": "キー割り当てなし",
"subcategories": {
"node": "ノード",
"panelControls": "パネルコントロール",
"queue": "キュー",
"view": "ビュー",
"workflow": "ワークフロー"
},
"viewControls": "表示コントロール"
},
"sideToolbar": {
"browseTemplates": "サンプルテンプレートを表示",
"downloads": "ダウンロード",
"helpCenter": "ヘルプセンター",
"labels": {
"models": "モデル",
"nodes": "ノード",
"queue": "キュー",
"templates": "テンプレート",
"workflows": "ワークフロー"
},
"logout": "ログアウト",
"modelLibrary": "モデルライブラリ",
"newBlankWorkflow": "新しい空のワークフローを作成",
@@ -1259,7 +1178,6 @@
},
"showFlatList": "フラットリストを表示"
},
"templates": "テンプレート",
"workflowTab": {
"confirmDelete": "このワークフローを削除してもよろしいですか?",
"confirmDeleteTitle": "ワークフローを削除しますか?",
@@ -1306,8 +1224,6 @@
"Video": "ビデオ",
"Video API": "動画API"
},
"loadingMore": "さらにテンプレートを読み込み中...",
"searchPlaceholder": "テンプレートを検索...",
"template": {
"3D": {
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
@@ -1630,7 +1546,6 @@
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
"failedToFetchBalance": "残高の取得に失敗しました: {error}",
"failedToFetchLogs": "サーバーログの取得に失敗しました",
"failedToInitializeLoad3dViewer": "3Dビューアの初期化に失敗しました",
"failedToInitiateCreditPurchase": "クレジット購入の開始に失敗しました: {error}",
"failedToPurchaseCredits": "クレジットの購入に失敗しました: {error}",
"fileLoadError": "{fileName}でワークフローが見つかりません",
@@ -1705,11 +1620,5 @@
"enterFilename": "ファイル名を入力",
"exportWorkflow": "ワークフローをエクスポート",
"saveWorkflow": "ワークフローを保存"
},
"zoomControls": {
"hideMinimap": "ミニマップを非表示",
"label": "ズームコントロール",
"showMinimap": "ミニマップを表示",
"zoomToFit": "全体表示にズーム"
}
}
}

View File

@@ -170,8 +170,20 @@
"Comfy_LoadDefaultWorkflow": {
"label": "기본 워크플로 로드"
},
"Comfy_Manager_CustomNodesManager": {
"label": "사용자 정의 노드 관리자"
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "사용자 정의 노드 (베타)"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "커스텀 노드 (레거시)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "매니저 메뉴 (레거시)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "누락된 팩 설치"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "업데이트 확인"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "진행 상황 대화 상자 전환"
@@ -185,6 +197,12 @@
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "선택한 노드 마스크 편집기 열기"
},
"Comfy_Memory_UnloadModels": {
"label": "모델 언로드"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "모델 및 실행 캐시 언로드"
},
"Comfy_NewBlankWorkflow": {
"label": "새로운 빈 워크플로"
},

View File

@@ -287,6 +287,7 @@
"color": "색상",
"comingSoon": "곧 출시 예정",
"command": "명령",
"commandProhibited": "명령 {command}은 금지되었습니다. 자세한 정보는 관리자에게 문의하십시오.",
"community": "커뮤니티",
"completed": "완료됨",
"confirm": "확인",
@@ -309,7 +310,6 @@
"disabling": "비활성화 중",
"dismiss": "닫기",
"download": "다운로드",
"duplicate": "복제",
"edit": "편집",
"empty": "비어 있음",
"enableAll": "모두 활성화",
@@ -322,6 +322,7 @@
"feedback": "피드백",
"filter": "필터",
"findIssues": "문제 찾기",
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
"frontendNewer": "프론트엔드 버전 {frontendVersion}이(가) 백엔드 버전 {backendVersion}과(와) 호환되지 않을 수 있습니다.",
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래되었습니다. 백엔드는 {requiredVersion} 이상이 필요합니다.",
"goToNode": "노드로 이동",
@@ -419,17 +420,12 @@
},
"graphCanvasMenu": {
"fitView": "보기 맞춤",
"focusMode": "포커스 모드",
"hand": "손 도구",
"hideLinks": "링크 숨기기",
"panMode": "팬 모드",
"resetView": "보기 재설정",
"select": "선택",
"selectMode": "선택 모드",
"showLinks": "링크 표시",
"toggleLinkVisibility": "링크 가시성 전환",
"toggleMinimap": "미니맵 전환",
"zoomIn": "확대",
"zoomOptions": "확대/축소 옵션",
"zoomOut": "축소"
},
"groupNode": {
@@ -551,46 +547,12 @@
"updateConsent": "이전에 충돌 보고에 동의하셨습니다. 이제 버그를 식별하고 앱을 개선하기 위해 이벤트 기반 통계 정보의 추적을 시작합니다. 개인을 식별할 수 있는 정보는 수집되지 않습니다."
},
"issueReport": {
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
"contactSupportDescription": "아래 양식에 보고 내용을 작성해 주세요",
"contactSupportTitle": "지원팀에 문의하기",
"describeTheProblem": "문제를 설명해 주세요",
"email": "이메일",
"feedbackTitle": "피드백을 제공함으로써 ComfyUI를 개선하는 데 도움을 주십시오",
"helpFix": "이 문제 해결에 도움을 주세요",
"helpTypes": {
"billingPayments": "결제 / 지불",
"bugReport": "버그 신고",
"giveFeedback": "피드백 제공",
"loginAccessIssues": "로그인 / 접근 문제",
"somethingElse": "기타"
},
"notifyResolve": "해결되었을 때 알려주세요",
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
"provideEmail": "이메일을 알려주세요 (선택 사항)",
"rating": "평가",
"selectIssue": "문제를 선택하세요",
"stackTrace": "스택 추적",
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
"systemStats": "시스템 통계",
"validation": {
"descriptionRequired": "설명은 필수입니다",
"helpTypeRequired": "도움 유형은 필수입니다",
"invalidEmail": "유효한 이메일 주소를 입력해 주세요",
"maxLength": "메시지가 너무 깁니다",
"selectIssueType": "문제 유형을 선택해 주세요"
},
"whatCanWeInclude": "보고서에 포함할 내용을 지정하세요",
"whatDoYouNeedHelpWith": "어떤 도움이 필요하신가요?"
"helpFix": "이 문제 해결에 도움을 주세요"
},
"load3d": {
"applyingTexture": "텍스처 적용 중...",
"backgroundColor": "배경색",
"camera": "카메라",
"cameraType": {
"orthographic": "직교",
"perspective": "원근"
},
"clearRecording": "녹화 지우기",
"edgeThreshold": "엣지 임계값",
"export": "내보내기",
@@ -611,7 +573,6 @@
"wireframe": "와이어프레임"
},
"model": "모델",
"openIn3DViewer": "3D 뷰어에서 열기",
"previewOutput": "출력 미리보기",
"removeBackgroundImage": "배경 이미지 제거",
"resizeNodeMatchOutput": "노드 크기를 출력에 맞추기",
@@ -622,22 +583,8 @@
"switchCamera": "카메라 전환",
"switchingMaterialMode": "재질 모드 전환 중...",
"upDirection": "위 방향",
"upDirections": {
"original": "원본"
},
"uploadBackgroundImage": "배경 이미지 업로드",
"uploadTexture": "텍스처 업로드",
"viewer": {
"apply": "적용",
"cameraSettings": "카메라 설정",
"cameraType": "카메라 유형",
"cancel": "취소",
"exportSettings": "내보내기 설정",
"lightSettings": "조명 설정",
"modelSettings": "모델 설정",
"sceneSettings": "씬 설정",
"title": "3D 뷰어 (베타)"
}
"uploadTexture": "텍스처 업로드"
},
"loadWorkflowWarning": {
"coreNodesFromVersion": "ComfyUI {version} 이상 필요:",
@@ -684,6 +631,9 @@
"installationQueue": "설치 대기열",
"lastUpdated": "마지막 업데이트",
"latestVersion": "최신",
"legacyManagerUI": "레거시 UI 사용",
"legacyManagerUIDescription": "레거시 매니저 UI를 사용하려면, ComfyUI를 --enable-manager-legacy-ui로 시작하세요",
"legacyMenuNotAvailable": "이 버전의 ComfyUI에서는 레거시 매니저 메뉴를 사용할 수 없습니다. 대신 새로운 매니저 메뉴를 사용하십시오.",
"license": "라이선스",
"loadingVersions": "버전 로딩 중...",
"nightlyVersion": "최신 테스트 버전(nightly)",
@@ -766,7 +716,6 @@
"manageExtensions": "확장 프로그램 관리",
"onChange": "변경 시",
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 대기열에 추가합니다.",
"queue": "대기열 패널",
"refresh": "노드 정의 새로 고침",
"resetView": "캔버스 보기 재설정",
"run": "실행",
@@ -782,8 +731,10 @@
"Bottom Panel": "하단 패널",
"Browse Templates": "템플릿 탐색",
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
"Canvas Performance": "캔버스 성",
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
"Canvas Toggle Lock": "캔버스 토글 잠금",
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
"Check for Custom Node Updates": "커스텀 노드 업데이트 확인",
"Check for Updates": "업데이트 확인",
"Clear Pending Tasks": "보류 중인 작업 제거하기",
"Clear Workflow": "워크플로 지우기",
@@ -797,28 +748,27 @@
"Contact Support": "고객 지원 문의",
"Convert Selection to Subgraph": "선택 영역을 서브그래프로 변환",
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
"Custom Nodes (Legacy)": "커스텀 노드(레거시)",
"Custom Nodes Manager": "사용자 정의 노드 관리자",
"Decrease Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 줄이기",
"Delete Selected Items": "선택한 항목 삭제",
"Desktop User Guide": "데스크톱 사용자 가이드",
"Duplicate Current Workflow": "현재 워크플로 복제",
"Edit": "편집",
"Exit Subgraph": "서브그래프 종료",
"Export": "내보내기",
"Export (API)": "내보내기 (API)",
"File": "파일",
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
"Focus Mode": "포커스 모드",
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
"Give Feedback": "피드백 제공",
"Group Selected Nodes": "선택한 노드 그룹화",
"Help": "도움말",
"Help Center": "도움말 센터",
"Increase Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 늘리기",
"Install Missing Custom Nodes": "누락된 커스텀 노드 설치",
"Interrupt": "중단",
"Load Default Workflow": "기본 워크플로 불러오기",
"Lock Canvas": "캔버스 잠금",
"Manage group nodes": "그룹 노드 관리",
"Manager": "매니저",
"Minimap": "미니맵",
"Model Library": "모델 라이브러리",
"Manager Menu (Legacy)": "매니저 메뉴(레거시)",
"Move Selected Nodes Down": "선택한 노드 아래로 이동",
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
@@ -826,10 +776,7 @@
"Mute/Unmute Selected Nodes": "선택한 노드 활성화/비활성화",
"New": "새로 만들기",
"Next Opened Workflow": "다음 열린 워크플로",
"Node Library": "노드 라이브러리",
"Node Links": "노드 링크",
"Open": "열기",
"Open 3D Viewer (Beta) for Selected Node": "선택한 노드에 대해 3D 뷰어(베타) 열기",
"Open Custom Nodes Folder": "사용자 정의 노드 폴더 열기",
"Open DevTools": "개발자 도구 열기",
"Open Inputs Folder": "입력 폴더 열기",
@@ -842,7 +789,6 @@
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
"Previous Opened Workflow": "이전 열린 워크플로",
"Queue Panel": "대기열 패널",
"Queue Prompt": "실행 대기열에 프롬프트 추가",
"Queue Prompt (Front)": "실행 대기열 맨 앞에 프롬프트 추가",
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
@@ -859,29 +805,24 @@
"Show Model Selector (Dev)": "모델 선택기 표시 (개발자용)",
"Show Settings Dialog": "설정 대화상자 표시",
"Sign Out": "로그아웃",
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",
"Toggle Bottom Panel": "하단 패널 전환",
"Toggle Focus Mode": "포커스 모드 전환",
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
"Toggle Search Box": "검색 상자 전환",
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
"Toggle Workflows Sidebar": "워크플로우 사이드바 전환",
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
"Undo": "실행 취소",
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
"Unlock Canvas": "캔버스 잠금 해제",
"Unpack the selected Subgraph": "선택한 서브그래프 풀기",
"Workflows": "워크플로",
"Unload Models": "모델 언로드",
"Unload Models and Execution Cache": "모델 및 실행 캐시 언로드",
"Workflow": "워크플로",
"Zoom In": "확대",
"Zoom Out": "축소",
"Zoom to fit": "화면에 맞추기"
},
"minimap": {
"nodeColors": "노드 색상",
"renderBypassState": "바이패스 상태 렌더링",
"renderErrorState": "에러 상태 렌더링",
"showGroups": "프레임/그룹 표시",
"showLinks": "링크 표시"
"Zoom Out": "축소"
},
"missingModelsDialog": {
"doNotAskAgain": "다시 보지 않기",
@@ -948,6 +889,7 @@
"sampling": "샘플링",
"schedulers": "스케줄러",
"scheduling": "스케줄링",
"sd": "sd",
"sd3": "sd3",
"sigmas": "시그마",
"stable_cascade": "Stable Cascade",
@@ -957,6 +899,9 @@
"upscale_diffusion": "업스케일 확산",
"upscaling": "업스케일링",
"utils": "유틸리티",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "비디오",
"video_models": "비디오 모델"
},
@@ -1145,7 +1090,6 @@
},
"settingsCategories": {
"3D": "3D",
"3DViewer": "3D뷰어",
"API Nodes": "API 노드",
"About": "정보",
"Appearance": "모양",
@@ -1197,31 +1141,10 @@
"Window": "창",
"Workflow": "워크플로"
},
"shortcuts": {
"essentials": "필수",
"keyboardShortcuts": "키보드 단축키",
"manageShortcuts": "단축키 관리",
"noKeybinding": "단축키 없음",
"subcategories": {
"node": "노드",
"panelControls": "패널 컨트롤",
"queue": "대기열",
"view": "보기",
"workflow": "워크플로우"
},
"viewControls": "보기 컨트롤"
},
"sideToolbar": {
"browseTemplates": "예제 템플릿 탐색",
"downloads": "다운로드",
"helpCenter": "도움말 센터",
"labels": {
"models": "모델",
"nodes": "노드",
"queue": "대기열",
"templates": "템플릿",
"workflows": "워크플로우"
},
"logout": "로그아웃",
"modelLibrary": "모델 라이브러리",
"newBlankWorkflow": "새 빈 워크플로 만들기",
@@ -1259,7 +1182,6 @@
},
"showFlatList": "평면 목록 표시"
},
"templates": "템플릿",
"workflowTab": {
"confirmDelete": "정말로 이 워크플로를 삭제하시겠습니까?",
"confirmDeleteTitle": "워크플로 삭제",
@@ -1306,8 +1228,6 @@
"Video": "비디오",
"Video API": "비디오 API"
},
"loadingMore": "템플릿을 더 불러오는 중...",
"searchPlaceholder": "템플릿 검색...",
"template": {
"3D": {
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
@@ -1630,7 +1550,6 @@
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
"failedToFetchBalance": "잔액을 가져오지 못했습니다: {error}",
"failedToFetchLogs": "서버 로그를 가져오는 데 실패했습니다",
"failedToInitializeLoad3dViewer": "3D 뷰어 초기화에 실패했습니다",
"failedToInitiateCreditPurchase": "크레딧 구매를 시작하지 못했습니다: {error}",
"failedToPurchaseCredits": "크레딧 구매에 실패했습니다: {error}",
"fileLoadError": "{fileName}에서 워크플로를 찾을 수 없습니다",
@@ -1705,11 +1624,5 @@
"enterFilename": "파일 이름 입력",
"exportWorkflow": "워크플로 내보내기",
"saveWorkflow": "워크플로 저장"
},
"zoomControls": {
"hideMinimap": "미니맵 숨기기",
"label": "확대/축소 컨트롤",
"showMinimap": "미니맵 표시",
"zoomToFit": "화면에 맞게 확대"
}
}
}

View File

@@ -170,8 +170,20 @@
"Comfy_LoadDefaultWorkflow": {
"label": "Загрузить стандартный рабочий процесс"
},
"Comfy_Manager_CustomNodesManager": {
"label": "Менеджер Пользовательских Узлов"
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "Пользовательские узлы (Бета)"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "Пользовательские узлы (устаревшие)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "Меню менеджера (устаревшее)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "Установить отсутствующие"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Проверить наличие обновлений"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Переключить диалоговое окно прогресса"
@@ -185,6 +197,12 @@
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Открыть редактор масок для выбранной ноды"
},
"Comfy_Memory_UnloadModels": {
"label": "Выгрузить модели"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "Выгрузить модели и кэш выполнения"
},
"Comfy_NewBlankWorkflow": {
"label": "Новый пустой рабочий процесс"
},

View File

@@ -287,6 +287,7 @@
"color": "Цвет",
"comingSoon": "Скоро будет",
"command": "Команда",
"commandProhibited": "Команда {command} запрещена. Свяжитесь с администратором для получения дополнительной информации.",
"community": "Сообщество",
"completed": "Завершено",
"confirm": "Подтвердить",
@@ -309,7 +310,6 @@
"disabling": "Отключение",
"dismiss": "Закрыть",
"download": "Скачать",
"duplicate": "Дублировать",
"edit": "Редактировать",
"empty": "Пусто",
"enableAll": "Включить все",
@@ -322,6 +322,7 @@
"feedback": "Обратная связь",
"filter": "Фильтр",
"findIssues": "Найти проблемы",
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
"frontendNewer": "Версия интерфейса {frontendVersion} может быть несовместима с версией сервера {backendVersion}.",
"frontendOutdated": "Версия интерфейса {frontendVersion} устарела. Требуется версия не ниже {requiredVersion} для работы с сервером.",
"goToNode": "Перейти к ноде",
@@ -419,17 +420,12 @@
},
"graphCanvasMenu": {
"fitView": "Подгонять под выделенные",
"focusMode": "Режим фокуса",
"hand": "Рука",
"hideLinks": "Скрыть связи",
"panMode": "Режим панорамирования",
"resetView": "Сбросить вид",
"select": "Выбрать",
"selectMode": "Выбрать режим",
"showLinks": "Показать связи",
"toggleLinkVisibility": "Переключить видимость ссылок",
"toggleMinimap": "Показать/скрыть миникарту",
"zoomIn": "Увеличить",
"zoomOptions": "Параметры масштабирования",
"zoomOut": "Уменьшить"
},
"groupNode": {
@@ -551,46 +547,12 @@
"updateConsent": "Вы ранее согласились на отчётность об ошибках. Теперь мы отслеживаем метрики событий, чтобы помочь выявить ошибки и улучшить приложение. Личная идентифицируемая информация не собирается."
},
"issueReport": {
"contactFollowUp": "Свяжитесь со мной для уточнения",
"contactSupportDescription": "Пожалуйста, заполните форму ниже для отправки вашего отчёта",
"contactSupportTitle": "Связаться с поддержкой",
"describeTheProblem": "Опишите проблему",
"email": "Электронная почта",
"feedbackTitle": "Помогите нам улучшить ComfyUI, оставив отзыв",
"helpFix": "Помочь исправить это",
"helpTypes": {
"billingPayments": "Оплата / Платежи",
"bugReport": "Сообщить об ошибке",
"giveFeedback": "Оставить отзыв",
"loginAccessIssues": "Проблемы со входом / доступом",
"somethingElse": "Другое"
},
"notifyResolve": "Уведомить меня, когда проблема будет решена",
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
"provideEmail": "Укажите вашу электронную почту (необязательно)",
"rating": "Рейтинг",
"selectIssue": "Выберите проблему",
"stackTrace": "Трассировка стека",
"submitErrorReport": "Отправить отчёт об ошибке (необязательно)",
"systemStats": "Статистика системы",
"validation": {
"descriptionRequired": "Описание обязательно",
"helpTypeRequired": "Тип помощи обязателен",
"invalidEmail": "Пожалуйста, введите действительный адрес электронной почты",
"maxLength": "Сообщение слишком длинное",
"selectIssueType": "Пожалуйста, выберите тип проблемы"
},
"whatCanWeInclude": "Уточните, что включить в отчёт",
"whatDoYouNeedHelpWith": "С чем вам нужна помощь?"
"helpFix": "Помочь исправить это"
},
"load3d": {
"applyingTexture": "Применение текстуры...",
"backgroundColor": "Цвет фона",
"camera": "Камера",
"cameraType": {
"orthographic": "Ортографическая",
"perspective": "Перспективная"
},
"clearRecording": "Очистить запись",
"edgeThreshold": "Пороговое значение края",
"export": "Экспорт",
@@ -611,7 +573,6 @@
"wireframe": "Каркас"
},
"model": "Модель",
"openIn3DViewer": "Открыть в 3D просмотрщике",
"previewOutput": "Предварительный просмотр",
"removeBackgroundImage": "Удалить фоновое изображение",
"resizeNodeMatchOutput": "Изменить размер узла под вывод",
@@ -622,22 +583,8 @@
"switchCamera": "Переключить камеру",
"switchingMaterialMode": "Переключение режима материала...",
"upDirection": "Направление Вверх",
"upDirections": {
"original": "Оригинал"
},
"uploadBackgroundImage": "Загрузить фоновое изображение",
"uploadTexture": "Загрузить текстуру",
"viewer": {
"apply": "Применить",
"cameraSettings": "Настройки камеры",
"cameraType": "Тип камеры",
"cancel": "Отмена",
"exportSettings": "Настройки экспорта",
"lightSettings": "Настройки освещения",
"modelSettings": "Настройки модели",
"sceneSettings": "Настройки сцены",
"title": "3D Просмотрщик (Бета)"
}
"uploadTexture": "Загрузить текстуру"
},
"loadWorkflowWarning": {
"coreNodesFromVersion": "Требуется ComfyUI {version}:",
@@ -684,6 +631,9 @@
"installationQueue": "Очередь установки",
"lastUpdated": "Последнее обновление",
"latestVersion": "Последняя",
"legacyManagerUI": "Использовать устаревший UI",
"legacyManagerUIDescription": "Чтобы использовать устаревший UI менеджера, запустите ComfyUI с --enable-manager-legacy-ui",
"legacyMenuNotAvailable": "Устаревшее меню менеджера недоступно в этой версии ComfyUI. Пожалуйста, используйте новое меню менеджера.",
"license": "Лицензия",
"loadingVersions": "Загрузка версий...",
"nightlyVersion": "Ночная",
@@ -766,7 +716,6 @@
"manageExtensions": "Управление расширениями",
"onChange": "При изменении",
"onChangeTooltip": "Рабочий процесс будет поставлен в очередь после внесения изменений",
"queue": "Панель очереди",
"refresh": "Обновить определения нод",
"resetView": "Сбросить вид холста",
"run": "Запустить",
@@ -782,8 +731,10 @@
"Bottom Panel": "Нижняя панель",
"Browse Templates": "Просмотреть шаблоны",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
"Canvas Performance": "Производительность холста",
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
"Canvas Toggle Lock": "Переключение блокировки холста",
"Canvas Toggle Minimap": "Показать/скрыть миникарту на холсте",
"Check for Custom Node Updates": "Проверить обновления пользовательских узлов",
"Check for Updates": "Проверить наличие обновлений",
"Clear Pending Tasks": "Очистить ожидающие задачи",
"Clear Workflow": "Очистить рабочий процесс",
@@ -797,28 +748,27 @@
"Contact Support": "Связаться с поддержкой",
"Convert Selection to Subgraph": "Преобразовать выделенное в подграф",
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
"Custom Nodes (Legacy)": "Пользовательские узлы (устаревшие)",
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
"Decrease Brush Size in MaskEditor": "Уменьшить размер кисти в MaskEditor",
"Delete Selected Items": "Удалить выбранные элементы",
"Desktop User Guide": "Руководство пользователя для настольных ПК",
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
"Edit": "Редактировать",
"Exit Subgraph": "Выйти из подграфа",
"Export": "Экспортировать",
"Export (API)": "Экспорт (API)",
"File": "Файл",
"Fit Group To Contents": "Подогнать группу под содержимое",
"Focus Mode": "Режим фокуса",
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
"Give Feedback": "Оставить отзыв",
"Group Selected Nodes": "Сгруппировать выбранные ноды",
"Help": "Помощь",
"Help Center": "Центр поддержки",
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
"Install Missing Custom Nodes": "Установить отсутствующие пользовательские узлы",
"Interrupt": "Прервать",
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
"Lock Canvas": "Заблокировать холст",
"Manage group nodes": "Управление групповыми нодами",
"Manager": "Менеджер",
"Minimap": "Мини-карта",
"Model Library": "Библиотека моделей",
"Manager Menu (Legacy)": "Меню управления (устаревшее)",
"Move Selected Nodes Down": "Переместить выбранные узлы вниз",
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
"Move Selected Nodes Right": "Переместить выбранные узлы вправо",
@@ -826,10 +776,7 @@
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
"New": "Новый",
"Next Opened Workflow": "Следующий открытый рабочий процесс",
"Node Library": "Библиотека узлов",
"Node Links": "Связи узлов",
"Open": "Открыть",
"Open 3D Viewer (Beta) for Selected Node": "Открыть 3D-просмотрщик (бета) для выбранного узла",
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
"Open DevTools": "Открыть инструменты разработчика",
"Open Inputs Folder": "Открыть папку входных данных",
@@ -842,7 +789,6 @@
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
"Queue Panel": "Панель очереди",
"Queue Prompt": "Запрос в очереди",
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
"Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь",
@@ -869,19 +815,11 @@
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
"Undo": "Отменить",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
"Unlock Canvas": "Разблокировать холст",
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
"Workflows": "Рабочие процессы",
"Unload Models": "Выгрузить модели",
"Unload Models and Execution Cache": "Выгрузить модели и кэш выполнения",
"Workflow": "Рабочий процесс",
"Zoom In": "Увеличить",
"Zoom Out": "Уменьшить",
"Zoom to fit": "Масштабировать по размеру"
},
"minimap": {
"nodeColors": "Цвета узлов",
"renderBypassState": "Отображать состояние обхода",
"renderErrorState": "Отображать состояние ошибки",
"showGroups": "Показать фреймы/группы",
"showLinks": "Показать связи"
"Zoom Out": "Уменьшить"
},
"missingModelsDialog": {
"doNotAskAgain": "Больше не показывать это",
@@ -948,6 +886,7 @@
"sampling": "выборка",
"schedulers": "schedulers",
"scheduling": "scheduling",
"sd": "sd",
"sd3": "sd3",
"sigmas": "сигмы",
"stable_cascade": "стабильная_каскадная",
@@ -957,6 +896,9 @@
"upscale_diffusion": "диффузии_апскейла",
"upscaling": "апскейл",
"utils": "утилиты",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "видео",
"video_models": "видеомодели"
},
@@ -1145,7 +1087,6 @@
},
"settingsCategories": {
"3D": "3D",
"3DViewer": "3D-просмотрщик",
"API Nodes": "API-узлы",
"About": "О программе",
"Appearance": "Внешний вид",
@@ -1197,31 +1138,10 @@
"Window": "Окно",
"Workflow": "Рабочий процесс"
},
"shortcuts": {
"essentials": "Основные",
"keyboardShortcuts": "Горячие клавиши",
"manageShortcuts": "Управление горячими клавишами",
"noKeybinding": "Нет сочетания клавиш",
"subcategories": {
"node": "Узел",
"panelControls": "Управление панелью",
"queue": "Очередь",
"view": "Просмотр",
"workflow": "Рабочий процесс"
},
"viewControls": "Управление просмотром"
},
"sideToolbar": {
"browseTemplates": "Просмотреть примеры шаблонов",
"downloads": "Загрузки",
"helpCenter": "Центр поддержки",
"labels": {
"models": "Модели",
"nodes": "Узлы",
"queue": "Очередь",
"templates": "Шаблоны",
"workflows": "Воркфлоу"
},
"logout": "Выйти",
"modelLibrary": "Библиотека моделей",
"newBlankWorkflow": "Создайте новый пустой рабочий процесс",
@@ -1259,7 +1179,6 @@
},
"showFlatList": "Показать плоский список"
},
"templates": "Шаблоны",
"workflowTab": {
"confirmDelete": "Вы уверены, что хотите удалить этот рабочий процесс?",
"confirmDeleteTitle": "Удалить рабочий процесс?",
@@ -1306,8 +1225,6 @@
"Video": "Видео",
"Video API": "Video API"
},
"loadingMore": "Загрузка дополнительных шаблонов...",
"searchPlaceholder": "Поиск шаблонов...",
"template": {
"3D": {
"3d_hunyuan3d_image_to_model": "Hunyuan3D",
@@ -1630,7 +1547,6 @@
"failedToExportModel": "Не удалось экспортировать модель как {format}",
"failedToFetchBalance": "Не удалось получить баланс: {error}",
"failedToFetchLogs": "Не удалось получить серверные логи",
"failedToInitializeLoad3dViewer": "Не удалось инициализировать 3D просмотрщик",
"failedToInitiateCreditPurchase": "Не удалось начать покупку кредитов: {error}",
"failedToPurchaseCredits": "Не удалось купить кредиты: {error}",
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
@@ -1705,11 +1621,5 @@
"enterFilename": "Введите название файла",
"exportWorkflow": "Экспорт рабочего процесса",
"saveWorkflow": "Сохранить рабочий процесс"
},
"zoomControls": {
"hideMinimap": "Скрыть миникарту",
"label": "Элементы управления масштабом",
"showMinimap": "Показать миникарту",
"zoomToFit": "Масштабировать по размеру"
}
}
}

View File

@@ -170,8 +170,20 @@
"Comfy_LoadDefaultWorkflow": {
"label": "載入預設工作流程"
},
"Comfy_Manager_CustomNodesManager": {
"label": "切換自訂節點管理器"
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "自訂節點管理器"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "自訂節點(舊版)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "管理選單(舊版)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "安裝缺少的自訂節點"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "檢查自訂節點更新"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "切換自訂節點管理器進度條"
@@ -185,6 +197,12 @@
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "為選取的節點開啟 Mask 編輯器"
},
"Comfy_Memory_UnloadModels": {
"label": "卸載模型"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "卸載模型與執行快取"
},
"Comfy_NewBlankWorkflow": {
"label": "新增空白工作流程"
},

View File

@@ -287,6 +287,7 @@
"color": "顏色",
"comingSoon": "即將推出",
"command": "指令",
"commandProhibited": "指令 {command} 已被禁止。如需更多資訊,請聯絡管理員。",
"community": "社群",
"completed": "已完成",
"confirm": "確認",
@@ -309,7 +310,6 @@
"disabling": "停用中",
"dismiss": "關閉",
"download": "下載",
"duplicate": "複製",
"edit": "編輯",
"empty": "空",
"enableAll": "全部啟用",
@@ -322,6 +322,7 @@
"feedback": "意見回饋",
"filter": "篩選",
"findIssues": "尋找問題",
"firstTimeUIMessage": "這是您第一次使用新介面。若要返回舊介面,請前往「選單」>「使用新介面」>「關閉」。",
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
"goToNode": "前往節點",
@@ -419,17 +420,12 @@
},
"graphCanvasMenu": {
"fitView": "適合視窗",
"focusMode": "專注模式",
"hand": "拖曳",
"hideLinks": "隱藏連結",
"panMode": "平移模式",
"resetView": "重設視圖",
"select": "選取",
"selectMode": "選取模式",
"showLinks": "顯示連結",
"toggleLinkVisibility": "切換連結顯示",
"toggleMinimap": "切換小地圖",
"zoomIn": "放大",
"zoomOptions": "縮放選項",
"zoomOut": "縮小"
},
"groupNode": {
@@ -551,46 +547,12 @@
"updateConsent": "您先前已選擇回報當機。現在我們會追蹤事件型統計資料,以協助找出錯誤並改進應用程式。不會收集任何可識別個人身分的資訊。"
},
"issueReport": {
"contactFollowUp": "需要聯絡我以便後續追蹤",
"contactSupportDescription": "請填寫下列表單並提交您的報告",
"contactSupportTitle": "聯絡客服支援",
"describeTheProblem": "請描述問題",
"email": "電子郵件",
"feedbackTitle": "協助我們改進 ComfyUI請提供您的回饋",
"helpFix": "協助修復此問題",
"helpTypes": {
"billingPayments": "帳單/付款問題",
"bugReport": "錯誤回報",
"giveFeedback": "提供回饋",
"loginAccessIssues": "登入/存取問題",
"somethingElse": "其他"
},
"notifyResolve": "問題解決時通知我",
"provideAdditionalDetails": "提供更多細節",
"provideEmail": "請提供您的電子郵件(選填)",
"rating": "評分",
"selectIssue": "請選擇問題",
"stackTrace": "堆疊追蹤",
"submitErrorReport": "提交錯誤報告(選填)",
"systemStats": "系統狀態",
"validation": {
"descriptionRequired": "請填寫問題描述",
"helpTypeRequired": "請選擇協助類型",
"invalidEmail": "請輸入有效的電子郵件地址",
"maxLength": "訊息過長",
"selectIssueType": "請選擇問題類型"
},
"whatCanWeInclude": "請說明報告中要包含哪些內容",
"whatDoYouNeedHelpWith": "您需要什麼協助?"
"helpFix": "協助修復此問題"
},
"load3d": {
"applyingTexture": "正在套用材質貼圖...",
"backgroundColor": "背景顏色",
"camera": "相機",
"cameraType": {
"orthographic": "正交",
"perspective": "透視"
},
"clearRecording": "清除錄影",
"edgeThreshold": "邊緣閾值",
"export": "匯出",
@@ -611,7 +573,6 @@
"wireframe": "線框"
},
"model": "模型",
"openIn3DViewer": "在 3D 檢視器中開啟",
"previewOutput": "預覽輸出",
"removeBackgroundImage": "移除背景圖片",
"resizeNodeMatchOutput": "調整節點以符合輸出",
@@ -622,22 +583,8 @@
"switchCamera": "切換相機",
"switchingMaterialMode": "正在切換材質模式...",
"upDirection": "上方方向",
"upDirections": {
"original": "原始"
},
"uploadBackgroundImage": "上傳背景圖片",
"uploadTexture": "上傳材質貼圖",
"viewer": {
"apply": "套用",
"cameraSettings": "相機設定",
"cameraType": "相機類型",
"cancel": "取消",
"exportSettings": "匯出設定",
"lightSettings": "燈光設定",
"modelSettings": "模型設定",
"sceneSettings": "場景設定",
"title": "3D 檢視器(測試版)"
}
"uploadTexture": "上傳材質貼圖"
},
"loadWorkflowWarning": {
"coreNodesFromVersion": "需要 ComfyUI {version}",
@@ -684,6 +631,9 @@
"installationQueue": "安裝佇列",
"lastUpdated": "最後更新",
"latestVersion": "最新版本",
"legacyManagerUI": "使用舊版介面",
"legacyManagerUIDescription": "若要使用舊版管理介面,請以 --enable-manager-legacy-ui 啟動 ComfyUI",
"legacyMenuNotAvailable": "舊版管理選單不可用,已預設切換至新版管理選單。",
"license": "授權條款",
"loadingVersions": "正在載入版本...",
"nightlyVersion": "每夜建置版",
@@ -766,7 +716,6 @@
"manageExtensions": "管理擴充功能",
"onChange": "變更時",
"onChangeTooltip": "每當有變更時,工作流程會排入佇列",
"queue": "佇列面板",
"refresh": "重新整理節點定義",
"resetView": "重設畫布視圖",
"run": "執行",
@@ -782,8 +731,10 @@
"Bottom Panel": "底部面板",
"Browse Templates": "瀏覽範本",
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
"Canvas Performance": "畫布效能",
"Canvas Toggle Link Visibility": "切換連結可見性",
"Canvas Toggle Lock": "切換畫布鎖定",
"Canvas Toggle Minimap": "畫布切換小地圖",
"Check for Custom Node Updates": "檢查自訂節點更新",
"Check for Updates": "檢查更新",
"Clear Pending Tasks": "清除待處理任務",
"Clear Workflow": "清除工作流程",
@@ -797,28 +748,27 @@
"Contact Support": "聯絡支援",
"Convert Selection to Subgraph": "將選取內容轉為子圖",
"Convert selected nodes to group node": "將選取節點轉為群組節點",
"Custom Nodes (Legacy)": "自訂節點(舊版)",
"Custom Nodes Manager": "自訂節點管理員",
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
"Delete Selected Items": "刪除選取項目",
"Desktop User Guide": "桌面應用程式使用指南",
"Duplicate Current Workflow": "複製目前工作流程",
"Edit": "編輯",
"Exit Subgraph": "離開子圖",
"Export": "匯出",
"Export (API)": "匯出API",
"File": "檔案",
"Fit Group To Contents": "群組貼合內容",
"Focus Mode": "專注模式",
"Fit view to selected nodes": "視圖貼合選取節點",
"Give Feedback": "提供意見回饋",
"Group Selected Nodes": "群組選取節點",
"Help": "說明",
"Help Center": "說明中心",
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
"Install Missing Custom Nodes": "安裝缺少的自訂節點",
"Interrupt": "中斷",
"Load Default Workflow": "載入預設工作流程",
"Lock Canvas": "鎖定畫布",
"Manage group nodes": "管理群組節點",
"Manager": "管理員",
"Minimap": "縮圖地圖",
"Model Library": "模型庫",
"Manager Menu (Legacy)": "管理員選單(舊版)",
"Move Selected Nodes Down": "選取節點下移",
"Move Selected Nodes Left": "選取節點左移",
"Move Selected Nodes Right": "選取節點右移",
@@ -826,10 +776,7 @@
"Mute/Unmute Selected Nodes": "靜音/取消靜音選取節點",
"New": "新增",
"Next Opened Workflow": "下一個已開啟的工作流程",
"Node Library": "節點庫",
"Node Links": "節點連結",
"Open": "開啟",
"Open 3D Viewer (Beta) for Selected Node": "為選取的節點開啟 3D 檢視器Beta 版)",
"Open Custom Nodes Folder": "開啟自訂節點資料夾",
"Open DevTools": "開啟開發者工具",
"Open Inputs Folder": "開啟輸入資料夾",
@@ -842,7 +789,6 @@
"Pin/Unpin Selected Items": "釘選/取消釘選選取項目",
"Pin/Unpin Selected Nodes": "釘選/取消釘選選取節點",
"Previous Opened Workflow": "上一個已開啟的工作流程",
"Queue Panel": "佇列面板",
"Queue Prompt": "加入提示至佇列",
"Queue Prompt (Front)": "將提示加入佇列前端",
"Queue Selected Output Nodes": "將選取的輸出節點加入佇列",
@@ -869,19 +815,11 @@
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
"Undo": "復原",
"Ungroup selected group nodes": "取消群組選取的群組節點",
"Unlock Canvas": "解除鎖定畫布",
"Unpack the selected Subgraph": "解包所選子圖",
"Workflows": "工作流程",
"Unload Models": "卸載模型",
"Unload Models and Execution Cache": "卸載模型與執行快取",
"Workflow": "工作流程",
"Zoom In": "放大",
"Zoom Out": "縮小",
"Zoom to fit": "縮放至適合大小"
},
"minimap": {
"nodeColors": "節點顏色",
"renderBypassState": "顯示繞過狀態",
"renderErrorState": "顯示錯誤狀態",
"showGroups": "顯示框架/群組",
"showLinks": "顯示連結"
"Zoom Out": "縮小"
},
"missingModelsDialog": {
"doNotAskAgain": "不要再顯示此訊息",
@@ -948,6 +886,7 @@
"sampling": "取樣",
"schedulers": "排程器",
"scheduling": "排程",
"sd": "SD",
"sd3": "sd3",
"sigmas": "西格瑪值",
"stable_cascade": "stable_cascade",
@@ -957,6 +896,9 @@
"upscale_diffusion": "擴散放大",
"upscaling": "放大",
"utils": "工具",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "影片",
"video_models": "影片模型"
},
@@ -1145,7 +1087,6 @@
},
"settingsCategories": {
"3D": "3D",
"3DViewer": "3D 檢視器",
"API Nodes": "API 節點",
"About": "關於",
"Appearance": "外觀",
@@ -1197,31 +1138,10 @@
"Window": "視窗",
"Workflow": "工作流程"
},
"shortcuts": {
"essentials": "基本",
"keyboardShortcuts": "鍵盤快捷鍵",
"manageShortcuts": "管理快捷鍵",
"noKeybinding": "無快捷鍵",
"subcategories": {
"node": "節點",
"panelControls": "面板控制",
"queue": "佇列",
"view": "檢視",
"workflow": "工作流程"
},
"viewControls": "檢視控制"
},
"sideToolbar": {
"browseTemplates": "瀏覽範例模板",
"downloads": "下載",
"helpCenter": "說明中心",
"labels": {
"models": "模型",
"nodes": "節點",
"queue": "佇列",
"templates": "範本",
"workflows": "工作流程"
},
"logout": "登出",
"modelLibrary": "模型庫",
"newBlankWorkflow": "建立新的空白工作流程",
@@ -1259,7 +1179,6 @@
},
"showFlatList": "顯示平面清單"
},
"templates": "範本",
"workflowTab": {
"confirmDelete": "您確定要刪除這個工作流程嗎?",
"confirmDeleteTitle": "刪除工作流程?",
@@ -1306,8 +1225,6 @@
"Video": "影片",
"Video API": "影片 API"
},
"loadingMore": "正在載入更多範本...",
"searchPlaceholder": "搜尋範本...",
"template": {
"3D": {
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
@@ -1630,7 +1547,6 @@
"failedToExportModel": "無法將模型匯出為 {format}",
"failedToFetchBalance": "取得餘額失敗:{error}",
"failedToFetchLogs": "無法取得伺服器日誌",
"failedToInitializeLoad3dViewer": "初始化 3D 檢視器失敗",
"failedToInitiateCreditPurchase": "啟動點數購買失敗:{error}",
"failedToPurchaseCredits": "購買點數失敗:{error}",
"fileLoadError": "無法在 {fileName} 中找到工作流程",
@@ -1705,11 +1621,5 @@
"enterFilename": "輸入檔案名稱",
"exportWorkflow": "匯出工作流程",
"saveWorkflow": "儲存工作流程"
},
"zoomControls": {
"hideMinimap": "隱藏小地圖",
"label": "縮放控制",
"showMinimap": "顯示小地圖",
"zoomToFit": "縮放至適合大小"
}
}
}

View File

@@ -170,8 +170,20 @@
"Comfy_LoadDefaultWorkflow": {
"label": "加载默认工作流"
},
"Comfy_Manager_CustomNodesManager": {
"label": "自定义节点管理器"
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "自定义节点(测试版)"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "自訂節點(舊版)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "管理員選單(舊版)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "安装缺失的包"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "检查更新"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "切换进度对话框"
@@ -185,6 +197,12 @@
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "打开选中节点的遮罩编辑器"
},
"Comfy_Memory_UnloadModels": {
"label": "卸载模型"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "卸载模型和执行缓存"
},
"Comfy_NewBlankWorkflow": {
"label": "新建空白工作流"
},

View File

@@ -551,37 +551,7 @@
"updateConsent": "您之前选择了报告崩溃。我们现在正在跟踪基于事件的度量,以帮助识别错误并改进应用程序。我们不收集任何个人可识别信息。"
},
"issueReport": {
"contactFollowUp": "跟进联系我",
"contactSupportDescription": "请填写下方表格提交您的报告",
"contactSupportTitle": "联系支持",
"describeTheProblem": "描述问题",
"email": "电子邮箱",
"feedbackTitle": "通过提供反馈帮助我们改进ComfyUI",
"helpFix": "帮助修复这个",
"helpTypes": {
"billingPayments": "账单 / 支付",
"bugReport": "错误报告",
"giveFeedback": "提交反馈",
"loginAccessIssues": "登录 / 访问问题",
"somethingElse": "其他"
},
"notifyResolve": "解决时通知我",
"provideAdditionalDetails": "提供额外的详细信息(可选)",
"provideEmail": "提供您的电子邮件(可选)",
"rating": "评分",
"selectIssue": "选择问题",
"stackTrace": "堆栈跟踪",
"submitErrorReport": "提交错误报告(可选)",
"systemStats": "系统状态",
"validation": {
"descriptionRequired": "描述为必填项",
"helpTypeRequired": "帮助类型为必选项",
"invalidEmail": "请输入有效的电子邮件地址",
"maxLength": "消息过长",
"selectIssueType": "请选择一个问题类型"
},
"whatCanWeInclude": "请说明报告中需要包含的内容",
"whatDoYouNeedHelpWith": "您需要什么帮助?"
"helpFix": "帮助修复这个"
},
"load3d": {
"applyingTexture": "应用纹理中...",

View File

@@ -495,6 +495,7 @@ const zSettings = z.object({
'Comfy.Load3D.LightAdjustmentIncrement': z.number(),
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
'Comfy.Load3D.3DViewerEnable': z.boolean(),
'Comfy.Memory.AllowManualUnload': z.boolean(),
'pysssss.SnapToGrid': z.boolean(),
/** VHS setting is used for queue video preview support. */
'VHS.AdvancedPreviews': z.string(),

View File

@@ -1,28 +0,0 @@
import { z } from 'zod'
import { t } from '@/i18n'
const checkboxField = z.boolean().optional()
export const issueReportSchema = z
.object({
contactInfo: z.string().email().max(320).optional().or(z.literal('')),
details: z
.string()
.min(1, { message: t('validation.descriptionRequired') })
.max(5_000, { message: t('validation.maxLength', { length: 5_000 }) })
.optional(),
helpType: z.string().optional()
})
.catchall(checkboxField)
.refine((data) => Object.values(data).some((value) => value), {
path: ['details', 'helpType']
})
.refine((data) => data.helpType !== undefined && data.helpType !== '', {
message: t('issueReport.validation.helpTypeRequired'),
path: ['helpType']
})
.refine((data) => data.details !== undefined && data.details !== '', {
message: t('issueReport.validation.descriptionRequired'),
path: ['details']
})
export type IssueReportFormData = z.infer<typeof issueReportSchema>

View File

@@ -1,4 +1,5 @@
import axios from 'axios'
import { get } from 'es-toolkit/compat'
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
import type {
@@ -35,6 +36,7 @@ import type {
NodeId
} from '@/schemas/comfyWorkflowSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { useToastStore } from '@/stores/toastStore'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
@@ -1020,6 +1022,56 @@ export class ComfyApi extends EventTarget {
return (await axios.get(this.internalURL('/folder_paths'))).data
}
/* Frees memory by unloading models and optionally freeing execution cache
* @param {Object} options - The options object
* @param {boolean} options.freeExecutionCache - If true, also frees execution cache
*/
async freeMemory(options: { freeExecutionCache: boolean }) {
try {
let mode = ''
if (options.freeExecutionCache) {
mode = '{"unload_models": true, "free_memory": true}'
} else {
mode = '{"unload_models": true}'
}
const res = await this.fetchApi(`/free`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: mode
})
if (res.status === 200) {
if (options.freeExecutionCache) {
useToastStore().add({
severity: 'success',
summary: 'Models and Execution Cache have been cleared.',
life: 3000
})
} else {
useToastStore().add({
severity: 'success',
summary: 'Models have been unloaded.',
life: 3000
})
}
} else {
useToastStore().add({
severity: 'error',
summary:
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
life: 5000
})
}
} catch (error) {
useToastStore().add({
severity: 'error',
summary: 'An error occurred while trying to unload models.',
life: 5000
})
}
}
/**
* Gets the custom nodes i18n data from the server.
*
@@ -1031,21 +1083,21 @@ export class ComfyApi extends EventTarget {
/**
* Checks if the server supports a specific feature.
* @param featureName The name of the feature to check
* @param featureName The name of the feature to check (supports dot notation for nested values)
* @returns true if the feature is supported, false otherwise
*/
serverSupportsFeature(featureName: string): boolean {
return this.serverFeatureFlags[featureName] === true
return get(this.serverFeatureFlags, featureName) === true
}
/**
* Gets a server feature flag value.
* @param featureName The name of the feature to get
* @param featureName The name of the feature to get (supports dot notation for nested values)
* @param defaultValue The default value if the feature is not found
* @returns The feature value or default
*/
getServerFeature<T = unknown>(featureName: string, defaultValue?: T): T {
return (this.serverFeatureFlags[featureName] ?? defaultValue) as T
return get(this.serverFeatureFlags, featureName, defaultValue) as T
}
/**

View File

@@ -1,17 +1,18 @@
import axios, { AxiosError, AxiosResponse } from 'axios'
import { v4 as uuidv4 } from 'uuid'
import { ref } from 'vue'
import { api } from '@/scripts/api'
import {
type InstallPackParams,
type InstalledPacksResponse,
type ManagerPackInfo,
type ManagerQueueStatus,
SelectedVersion,
type UpdateAllPacksParams
} from '@/types/comfyManagerTypes'
import { components } from '@/types/generatedManagerTypes'
import { isAbortError } from '@/utils/typeGuardUtil'
type ManagerQueueStatus = components['schemas']['QueueStatus']
type InstallPackParams = components['schemas']['InstallPackParams']
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
type ManagerTaskHistory = components['schemas']['HistoryResponse']
type QueueTaskItem = components['schemas']['QueueTaskItem']
const GENERIC_SECURITY_ERR_MSG =
'Forbidden: A security error has occurred. Please check the terminal logs'
@@ -22,21 +23,19 @@ enum ManagerRoute {
START_QUEUE = 'manager/queue/start',
RESET_QUEUE = 'manager/queue/reset',
QUEUE_STATUS = 'manager/queue/status',
INSTALL = 'manager/queue/install',
UPDATE = 'manager/queue/update',
UPDATE_ALL = 'manager/queue/update_all',
UNINSTALL = 'manager/queue/uninstall',
DISABLE = 'manager/queue/disable',
FIX_NODE = 'manager/queue/fix',
LIST_INSTALLED = 'customnode/installed',
GET_NODES = 'customnode/getmappings',
GET_PACKS = 'customnode/getlist',
IMPORT_FAIL_INFO = 'customnode/import_fail_info',
REBOOT = 'manager/reboot'
IMPORT_FAIL_INFO_BULK = 'customnode/import_fail_info_bulk',
REBOOT = 'manager/reboot',
IS_LEGACY_MANAGER_UI = 'manager/is_legacy_manager_ui',
TASK_HISTORY = 'manager/queue/history',
QUEUE_TASK = 'manager/queue/task'
}
const managerApiClient = axios.create({
baseURL: api.apiURL(''),
baseURL: api.apiURL('/v2/'),
headers: {
'Content-Type': 'application/json'
}
@@ -49,7 +48,6 @@ const managerApiClient = axios.create({
export const useComfyManagerService = () => {
const isLoading = ref(false)
const error = ref<string | null>(null)
const didStartQueue = ref(false)
const handleRequestError = (
err: unknown,
@@ -110,28 +108,21 @@ export const useComfyManagerService = () => {
201: 'Created: ComfyUI-Manager job queue is already running'
}
didStartQueue.value = true
return executeRequest<null>(
() => managerApiClient.get(ManagerRoute.START_QUEUE, { signal }),
{ errorContext, routeSpecificErrors }
)
}
const getQueueStatus = async (signal?: AbortSignal) => {
const getQueueStatus = async (client_id?: string, signal?: AbortSignal) => {
const errorContext = 'Getting ComfyUI-Manager queue status'
return executeRequest<ManagerQueueStatus>(
() => managerApiClient.get(ManagerRoute.QUEUE_STATUS, { signal }),
{ errorContext }
)
}
const resetQueue = async (signal?: AbortSignal) => {
const errorContext = 'Resetting ComfyUI-Manager queue'
return executeRequest<null>(
() => managerApiClient.get(ManagerRoute.RESET_QUEUE, { signal }),
() =>
managerApiClient.get(ManagerRoute.QUEUE_STATUS, {
params: client_id ? { client_id } : undefined,
signal
}),
{ errorContext }
)
}
@@ -154,73 +145,81 @@ export const useComfyManagerService = () => {
)
}
const installPack = async (
params: InstallPackParams,
const getImportFailInfoBulk = async (
params: components['schemas']['ImportFailInfoBulkRequest'] = {},
signal?: AbortSignal
) => {
const errorContext = `Installing pack ${params.id}`
const errorContext = 'Fetching bulk import failure information'
return executeRequest<components['schemas']['ImportFailInfoBulkResponse']>(
() =>
managerApiClient.post(ManagerRoute.IMPORT_FAIL_INFO_BULK, params, {
signal
}),
{ errorContext }
)
}
const queueTask = async (
kind: QueueTaskItem['kind'],
params: QueueTaskItem['params'],
ui_id?: string,
signal?: AbortSignal
) => {
const task: QueueTaskItem = {
kind,
params,
ui_id: ui_id || uuidv4(),
client_id: api.clientId ?? api.initialClientId ?? 'unknown'
}
const errorContext = `Queueing ${task.kind} task`
const routeSpecificErrors = {
403: GENERIC_SECURITY_ERR_MSG,
404:
params.selected_version === SelectedVersion.NIGHTLY
? `Not Found: Node pack ${params.id} does not provide nightly version`
: GENERIC_SECURITY_ERR_MSG
404: `Not Found: Task could not be queued`
}
return executeRequest<null>(
() => managerApiClient.post(ManagerRoute.INSTALL, params, { signal }),
() => managerApiClient.post(ManagerRoute.QUEUE_TASK, task, { signal }),
{ errorContext, routeSpecificErrors, isQueueOperation: true }
)
}
const installPack = async (
params: InstallPackParams,
ui_id?: string,
signal?: AbortSignal
) => {
return queueTask('install', params, ui_id, signal)
}
const uninstallPack = async (
params: ManagerPackInfo,
params: components['schemas']['UninstallPackParams'],
ui_id?: string,
signal?: AbortSignal
) => {
const errorContext = `Uninstalling pack ${params.id}`
const routeSpecificErrors = {
403: GENERIC_SECURITY_ERR_MSG
}
return executeRequest<null>(
() => managerApiClient.post(ManagerRoute.UNINSTALL, params, { signal }),
{ errorContext, routeSpecificErrors, isQueueOperation: true }
)
return queueTask('uninstall', params, ui_id, signal)
}
const disablePack = async (
params: ManagerPackInfo,
params: components['schemas']['DisablePackParams'],
ui_id?: string,
signal?: AbortSignal
): Promise<null> => {
const errorContext = `Disabling pack ${params.id}`
const routeSpecificErrors = {
404: `Pack ${params.id} not found or not installed`,
409: `Pack ${params.id} is already disabled`
}
return executeRequest<null>(
() => managerApiClient.post(ManagerRoute.DISABLE, params, { signal }),
{ errorContext, routeSpecificErrors, isQueueOperation: true }
)
return queueTask('disable', params, ui_id, signal)
}
const updatePack = async (
params: ManagerPackInfo,
params: components['schemas']['UpdatePackParams'],
ui_id?: string,
signal?: AbortSignal
): Promise<null> => {
const errorContext = `Updating pack ${params.id}`
const routeSpecificErrors = {
403: GENERIC_SECURITY_ERR_MSG
}
return executeRequest<null>(
() => managerApiClient.post(ManagerRoute.UPDATE, params, { signal }),
{ errorContext, routeSpecificErrors, isQueueOperation: true }
)
return queueTask('update', params, ui_id, signal)
}
const updateAllPacks = async (
params?: UpdateAllPacksParams,
params: UpdateAllPacksParams = {},
ui_id?: string,
signal?: AbortSignal
) => {
const errorContext = 'Updating all packs'
@@ -229,8 +228,18 @@ export const useComfyManagerService = () => {
401: 'Unauthorized: ComfyUI-Manager job queue is busy'
}
const queryParams = {
mode: params.mode,
client_id: api.clientId ?? api.initialClientId ?? 'unknown',
ui_id: ui_id || uuidv4()
}
return executeRequest<null>(
() => managerApiClient.get(ManagerRoute.UPDATE_ALL, { params, signal }),
() =>
managerApiClient.get(ManagerRoute.UPDATE_ALL, {
params: queryParams,
signal
}),
{ errorContext, routeSpecificErrors, isQueueOperation: true }
)
}
@@ -247,6 +256,36 @@ export const useComfyManagerService = () => {
)
}
const isLegacyManagerUI = async (signal?: AbortSignal) => {
const errorContext = 'Checking if user set Manager to use the legacy UI'
return executeRequest<{ is_legacy_manager_ui: boolean }>(
() => managerApiClient.get(ManagerRoute.IS_LEGACY_MANAGER_UI, { signal }),
{ errorContext }
)
}
const getTaskHistory = async (
options: {
ui_id?: string
max_items?: number
client_id?: string
offset?: number
} = {},
signal?: AbortSignal
) => {
const errorContext = 'Getting ComfyUI-Manager task history'
return executeRequest<ManagerTaskHistory>(
() =>
managerApiClient.get(ManagerRoute.TASK_HISTORY, {
params: options,
signal
}),
{ errorContext }
)
}
return {
// State
isLoading,
@@ -254,12 +293,13 @@ export const useComfyManagerService = () => {
// Queue operations
startQueue,
resetQueue,
getQueueStatus,
getTaskHistory,
// Pack management
listInstalledPacks,
getImportFailInfo,
getImportFailInfoBulk,
installPack,
uninstallPack,
enablePack: installPack, // enable is done via install
@@ -268,6 +308,7 @@ export const useComfyManagerService = () => {
updateAllPacks,
// System operations
rebootComfyUI
rebootComfyUI,
isLegacyManagerUI
}
}

Some files were not shown because too many files have changed in this diff Show More