Compare commits

...

58 Commits

Author SHA1 Message Date
filtered
628b44051b nit - fix TS type 2025-01-22 08:26:22 +11:00
filtered
a7a5e3cf67 [Refactor] Task state updates to TaskRunner 2025-01-22 08:18:42 +11:00
filtered
64e218a9f3 [Refactor] Task execution into task runner class 2025-01-22 08:18:42 +11:00
filtered
0b69d3cbfe [Desktop] Startup maintenance screen (#2253)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-21 16:10:15 -05:00
bymyself
8257e848c6 Update SearchBox vue features (#2310) 2025-01-21 10:32:46 -05:00
Chenlei Hu
a07b7693b6 [chore] Update vue to 3.5 (#2308)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-20 21:21:13 -05:00
Chenlei Hu
26ddf69451 Fix validation message locale (#2309) 2025-01-20 21:11:23 -05:00
bymyself
ed6ece2099 Add forms plugin to issue report component (#2302) 2025-01-20 20:20:59 -05:00
Chenlei Hu
b42516d39c 1.8.2 (#2307) 2025-01-20 17:24:58 -05:00
Chenlei Hu
ef24efe5a3 [Desktop] Report execution complete events (#2306) 2025-01-20 17:24:15 -05:00
Chenlei Hu
34c267c755 [chore] Update primevue to 4.2.5 (#2304) 2025-01-20 16:22:24 -05:00
bymyself
8b9f0ddd1d Add Comfy Forum (forum.comfy.org) to Help menu (#2305)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-20 16:06:31 -05:00
filtered
af658b7792 [Style] Use import/export icons for colour palette (#2300) 2025-01-20 14:33:02 -05:00
filtered
9c53bbd53d [Refactor] Move type extensions out of LG (SoC) (#2303)
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-01-20 14:22:56 -05:00
filtered
f9be20fa78 [Desktop] Fix unnecessary setting update (#2301)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-20 14:00:35 -05:00
Chenlei Hu
87fc7a2c5d [Cleanup] Remove explicit prettier plugin call (#2299) 2025-01-20 11:15:16 -05:00
Chenlei Hu
1f266e826e Fix .cursorrules typo (#2298) 2025-01-20 11:12:59 -05:00
Dr.Lt.Data
911adfe9f8 refine locales/ko (#2296) 2025-01-20 11:11:16 -05:00
bymyself
654d72b4cc Add issue report dialog service (#2284)
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-01-19 20:44:11 -05:00
bymyself
a1ed67fc74 Add User Feedback buttons (#2275)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-01-19 19:41:58 -05:00
Chenlei Hu
78bc635518 1.8.1 (#2295) 2025-01-19 19:07:18 -05:00
Chenlei Hu
f49ec175e9 1.8.0 (#2293)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-19 18:57:07 -05:00
Yuki Shindo
e4c60e7e18 Prevent Enter Key from Triggering Selection During IME Composition in AutoCompletePlus (#2285) 2025-01-19 18:34:26 -05:00
filtered
37cb2cb0a5 [Desktop] Add quit command (#2286)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-01-19 14:31:52 -05:00
Hikari-Fox
141825e988 Update Russian localization: refine terminology for "node", "hook", "… (#2289)
Co-authored-by: Vladimir Pozdnyakov <pozdnyakov044@gmail.com>
2025-01-19 14:29:40 -05:00
filtered
78f43b1e06 [Desktop] Add electron types update script (#2290) 2025-01-19 14:16:27 -05:00
filtered
a6105eb8c7 [DevExperience] Tailwind rules (#2292) 2025-01-19 14:16:03 -05:00
filtered
79ed598d5d [chore] Update electron-types to 0.4.11 (#2291) 2025-01-19 14:12:17 -05:00
bymyself
816574e0ab [Refactor] improve type safety in dialog service (#2283) 2025-01-18 15:46:57 -05:00
Chenlei Hu
1a4e77a3ab [Desktop] Ctrl+w to close workflow tab (#2282) 2025-01-17 20:02:54 -05:00
Chenlei Hu
de570712df 1.7.14 (#2281) 2025-01-17 18:04:43 -05:00
Chenlei Hu
44612e8f97 [Desktop] Add privacy policy link to install view about dialog (#2280)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-17 18:04:08 -05:00
bymyself
3df911c1bf Add consent prompt view (#2268)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-17 17:39:50 -05:00
Chenlei Hu
af26b9ad6d [Desktop] Report completed generation status (#2279) 2025-01-17 17:36:46 -05:00
Chenlei Hu
d503873980 Move queueStore update from QueueSidebarTab to GraphView (#2278) 2025-01-17 17:14:22 -05:00
bymyself
842a9f74fc [BrowserTest] Fix flaky gallery test (#2150) 2025-01-17 17:11:49 -05:00
Chenlei Hu
29551a36b3 Add .cursorrules (#2277) 2025-01-17 16:35:25 -05:00
filtered
d6e5c8950c [Desktop] Loading screen (#2274)
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-01-17 11:12:03 -05:00
Chenlei Hu
ad1c1ce9c2 [chore] Update electron-types to 0.4.9 (#2276) 2025-01-17 10:59:41 -05:00
Benjamin Lu
cb9d2c6bae Caching brush settings in mask editor (#2271)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-01-17 10:38:23 -05:00
Chenlei Hu
7fd41eeaba 1.7.13 (#2270) 2025-01-16 11:51:58 -05:00
filtered
79fee6ac72 Fix collapsed node textarea causes UI inconsistency (#2267) 2025-01-16 11:50:53 -05:00
Benjamin Lu
edd58cd153 Hotfix scoped --sidebar-width uasges in maskeditor.ts (#2269)
Co-authored-by: Benjamin Lu <templu1107@proton.me>
2025-01-16 11:36:11 -05:00
Chenlei Hu
e153508955 1.7.12 (#2265) 2025-01-15 20:19:24 -05:00
Chenlei Hu
237fca0bf1 [CodeHealth] Use scoped CSS for SideToolbar (#2264) 2025-01-15 20:16:39 -05:00
Chenlei Hu
65542b885a [Style] Fix root CSS selector (#2263) 2025-01-15 19:44:46 -05:00
Chenlei Hu
f739f704af [CodeHealth] Use scoped CSS in views (#2262) 2025-01-15 19:35:01 -05:00
Chenlei Hu
37abdbe35d [Desktop] Add install screen stepper change metrics (#2261) 2025-01-15 19:27:05 -05:00
Chenlei Hu
ff445f5c95 Apply min col on logs terminal for colab (#2260) 2025-01-15 17:16:33 -05:00
Chenlei Hu
84b652a281 [CodeHealth] Convert useAutoSize to kwargs (#2259) 2025-01-15 17:06:45 -05:00
Chenlei Hu
184291d21b [Settings] Enable Comfy.Window.UnloadConfirmation by default (#2258) 2025-01-15 16:37:11 -05:00
Chenlei Hu
d7fb25a36a Don't prompt unsaved when there is no unsaved workflow on window close (#2257) 2025-01-15 16:34:05 -05:00
Comfy Org PR Bot
c039a60fcc Update locales for node definitions (#2256)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-01-15 16:23:45 -05:00
Chenlei Hu
3b6108c26e Add work dir to i18n-node-defs.yaml (#2255) 2025-01-15 16:19:13 -05:00
Chenlei Hu
49bb247526 1.7.11 (#2250) 2025-01-14 23:34:58 -05:00
bymyself
dd005f5fa5 Allow parent component to pass tags to issue report panel (#2247) 2025-01-14 23:34:11 -05:00
Chenlei Hu
bf90b458d3 [Desktop] Add clarification of migration from existing install (#2249)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-14 23:33:53 -05:00
bymyself
7e78c5b1dc Fix type error in BaseViewTemplate (#2245) 2025-01-14 20:19:35 -05:00
97 changed files with 3585 additions and 1148 deletions

43
.cursorrules Normal file
View File

@@ -0,0 +1,43 @@
// Vue 3 Composition API .cursorrules
// Vue 3 Composition API best practices
const vue3CompositionApiBestPractices = [
"Use setup() function for component logic",
"Utilize ref and reactive for reactive state",
"Implement computed properties with computed()",
"Use watch and watchEffect for side effects",
"Implement lifecycle hooks with onMounted, onUpdated, etc.",
"Utilize provide/inject for dependency injection",
]
// Folder structure
const folderStructure = `
src/
components/
constants/
hooks/
views/
stores/
services/
App.vue
main.ts
`;
// Tailwind CSS best practices
const tailwindCssBestPractices = [
"Use Tailwind CSS for styling",
"Implement responsive design with Tailwind CSS",
]
// Additional instructions
const additionalInstructions = `
1. Leverage VueUse functions for performance-enhancing styles
2. Use lodash for utility functions
3. Use TypeScript for type safety
4. Implement proper props and emits definitions
5. Utilize Vue 3's Teleport component when needed
6. Use Suspense for async components
7. Implement proper error handling
8. Follow Vue 3 style guide and naming conventions
9. Use Vite for fast development and building
`;

View File

@@ -42,6 +42,7 @@ jobs:
Automated PR to update locales for node definitions
This PR was created automatically by the frontend update workflow.
branch: update-locales-node-defs-{{ github.event.inputs.trigger_type }}-{{ github.run_id }}
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
base: main
labels: dependencies
labels: dependencies
path: ComfyUI_frontend

2
.gitignore vendored
View File

@@ -16,6 +16,8 @@ dist-ssr
.vscode/*
*.code-workspace
!.vscode/extensions.json
!.vscode/tailwind.json
!.vscode/settings.json.default
.idea
.DS_Store
*.suo

5
.vscode/settings.json.default vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"css.customData": [
".vscode/tailwind.json"
]
}

55
.vscode/tailwind.json vendored Normal file
View File

@@ -0,0 +1,55 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that youd like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}

View File

@@ -0,0 +1,37 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "CLIPTextEncode",
"pos": [20, 50],
"size": [400, 200],
"flags": { "collapsed": true },
"order": 0,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": null,
"localized_name": "clip"
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": null,
"localized_name": "CONDITIONING"
}
],
"properties": {},
"widgets_values": ["Should not be displayed."]
}
],
"links": [],
"groups": [],
"config": {},
"version": 0.4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -165,3 +165,37 @@ test.describe('Settings', () => {
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
})
})
test.describe('Feedback dialog', () => {
test('Should open from topmenu help command', async ({ comfyPage }) => {
// Open feedback dialog from top menu
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
// 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()
})
})

View File

@@ -0,0 +1,27 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('DOM Widget', () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('collapsed_multiline')
expect(comfyPage.page.locator('.comfy-multiline-input')).not.toBeVisible()
})
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {
const multilineTextAreas = comfyPage.page.locator('.comfy-multiline-input')
const firstMultiline = multilineTextAreas.first()
const lastMultiline = multilineTextAreas.last()
await expect(firstMultiline).toBeVisible()
await expect(lastMultiline).toBeVisible()
const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
for (const node of nodes) {
await node.click('collapse')
}
await expect(firstMultiline).not.toBeVisible()
await expect(lastMultiline).not.toBeVisible()
})
})

View File

@@ -194,6 +194,10 @@ export class QueueSidebarTab extends SidebarTab {
return this.root.locator('.no-results-placeholder')
}
get galleryImage() {
return this.page.locator('.galleria-image')
}
private getToggleExpandButton(isExpanded: boolean) {
const iconSelector = isExpanded ? '.pi-image' : '.pi-images'
return this.root.locator(`.toggle-expanded-button ${iconSelector}`)
@@ -256,14 +260,24 @@ export class QueueSidebarTab extends SidebarTab {
async openTaskPreview(taskIndex: number) {
const previewButton = this.getTaskPreviewButton(taskIndex)
await previewButton.hover()
await previewButton.click()
return this.getGalleryImage(taskIndex).waitFor({ state: 'visible' })
return this.galleryImage.waitFor({ state: 'visible' })
}
getGalleryImage(galleryItemIndex: number) {
// Aria labels of Galleria items are 1-based indices
const galleryLabel = `${galleryItemIndex + 1}`
return this.page.getByLabel(galleryLabel).locator('.galleria-image')
getGalleryImage(imageFilename: string) {
return this.galleryImage.and(this.page.getByAltText(imageFilename))
}
getTaskImage(imageFilename: string) {
return this.tasks.getByAltText(imageFilename)
}
/** Trigger the queue store and tasks to update */
async triggerTasksUpdate() {
await this.page.evaluate(() => {
window['app']['api'].dispatchCustomEvent('status', {
exec_info: { queue_remaining: 0 }
})
})
}
}

View File

@@ -132,11 +132,12 @@ export default class TaskHistory {
private addTask(task: HistoryTaskItem) {
setPromptId(task)
setQueueIndex(task)
this.tasks.push(task)
this.tasks.unshift(task) // Tasks are added to the front of the queue
}
clearTasks() {
clearTasks(): this {
this.tasks = []
return this
}
withTask(
@@ -155,7 +156,7 @@ export default class TaskHistory {
/** Repeats the last task in the task history a specified number of times. */
repeat(n: number): this {
for (let i = 0; i < n; i++)
this.addTask(structuredClone(this.tasks.at(-1)) as HistoryTaskItem)
this.addTask(structuredClone(this.tasks.at(0)) as HistoryTaskItem)
return this
}
}

View File

@@ -1,9 +1,6 @@
import { expect, mergeTests } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture } from './fixtures/ComfyPage'
import { webSocketFixture } from './fixtures/ws'
const test = mergeTests(comfyPageFixture, webSocketFixture)
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -948,66 +945,61 @@ test.describe.skip('Queue sidebar', () => {
})
test.describe('Gallery', () => {
const firstImage = 'example.webp'
const secondImage = 'image32x32.webp'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage
.setupHistory()
.withTask(['example.webp'])
.repeat(1)
.withTask([secondImage])
.withTask([firstImage])
.setupRoutes()
await comfyPage.menu.queueTab.open()
await comfyPage.menu.queueTab.waitForTasks()
await comfyPage.menu.queueTab.openTaskPreview(0)
})
test('displays gallery image after opening task preview', async ({
comfyPage
}) => {
await comfyPage.menu.queueTab.openTaskPreview(0)
expect(comfyPage.menu.queueTab.getGalleryImage(0)).toBeVisible()
await comfyPage.nextFrame()
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
})
test('should maintain active gallery item when new tasks are added', async ({
comfyPage,
ws
test('maintains active gallery item when new tasks are added', async ({
comfyPage
}) => {
const initialIndex = 0
await comfyPage.menu.queueTab.openTaskPreview(initialIndex)
// Add a new task while the gallery is still open
comfyPage.setupHistory().withTask(['example.webp'])
await ws.trigger({
type: 'status',
data: {
status: { exec_info: { queue_remaining: 0 } }
}
})
await comfyPage.menu.queueTab.waitForTasks()
// The index of all tasks increments when a new task is prepended
const expectIndex = initialIndex + 1
expect(comfyPage.menu.queueTab.getGalleryImage(expectIndex)).toBeVisible()
const newImage = 'image64x64.webp'
comfyPage.setupHistory().withTask([newImage])
await comfyPage.menu.queueTab.triggerTasksUpdate()
await comfyPage.page.waitForTimeout(500)
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
await newTask.waitFor({ state: 'visible' })
// The active gallery item should still be the initial image
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
})
test.describe('Gallery navigation', () => {
const paths: {
description: string
path: ('Right' | 'Left')[]
expectIndex: number
end: string
}[] = [
{ description: 'Right', path: ['Right'], expectIndex: 1 },
{ description: 'Left', path: ['Right', 'Left'], expectIndex: 0 },
{ description: 'Right wrap', path: ['Right', 'Right'], expectIndex: 0 },
{ description: 'Left wrap', path: ['Left'], expectIndex: 1 }
{ description: 'Right', path: ['Right'], end: secondImage },
{ description: 'Left', path: ['Right', 'Left'], end: firstImage },
{ description: 'Left wrap', path: ['Left'], end: secondImage },
{ description: 'Right wrap', path: ['Right', 'Right'], end: firstImage }
]
paths.forEach(({ description, path, expectIndex }) => {
paths.forEach(({ description, path, end }) => {
test(`can navigate gallery ${description}`, async ({ comfyPage }) => {
await comfyPage.menu.queueTab.openTaskPreview(0)
for (const direction of path)
await comfyPage.page.keyboard.press(`Arrow${direction}`)
expect(
comfyPage.menu.queueTab.getGalleryImage(expectIndex)
).toBeVisible()
await comfyPage.page.keyboard.press(`Arrow${direction}`, {
delay: 256
})
await comfyPage.nextFrame()
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
})
})
})

View File

@@ -82,10 +82,14 @@ test.describe('Node search box', () => {
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
const node = 'Load Checkpoint'
await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.fillAndSelectFirstNode(node)
const firstResult = comfyPage.page
.locator('li.p-autocomplete-option')
.first()
await comfyPage.searchBox.input.waitFor({ state: 'visible' })
await comfyPage.searchBox.input.fill(node)
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
// Wait for some time for the auto complete list to update.
// The auto complete list is debounced and may take some time to update.
await comfyPage.page.waitForTimeout(500)
const firstResult = comfyPage.searchBox.dropdown.locator('li').first()
await expect(firstResult).toHaveAttribute('aria-label', node)
})

View File

@@ -11,7 +11,7 @@ export default {
function formatAndEslint(fileNames) {
return [
`prettier --write ${fileNames.join(' ')} --plugin @trivago/prettier-plugin-sort-imports`,
`prettier --write ${fileNames.join(' ')}`,
`eslint --fix ${fileNames.join(' ')}`
]
}

335
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.7.10",
"version": "1.8.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.7.10",
"version": "1.8.2",
"license": "GPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.7",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@comfyorg/comfyui-electron-types": "^0.4.11",
"@comfyorg/litegraph": "^0.8.61",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
@@ -32,10 +33,10 @@
"loglevel": "^1.9.2",
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"primevue": "^4.0.5",
"primevue": "^4.2.5",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"vue": "^3.4.31",
"vue": "^3.5.13",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.3",
"zod": "^3.23.8",
@@ -1936,15 +1937,15 @@
"dev": true
},
"node_modules/@comfyorg/comfyui-electron-types": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.7.tgz",
"integrity": "sha512-APC3C4VZOo9W6h0xiAGxnsU9iNp3T8rN9w/5KmOCI0GUoKtKg5U2OaicTmnMwcDSQe5Jxflmej53GyJ1nH9oRw==",
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.11.tgz",
"integrity": "sha512-RGJeWwXjyv0Ojj7xkZKgcRxC1nFv1nh7qEWpNBiofxVgFiap9Ei79b/KJYxNE0no4BoYqRMaRg+sFtCE6yEukA==",
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.8.60",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.60.tgz",
"integrity": "sha512-LkZalBcka1xVxkL7JnkF/1EzyvspLyrSthzyN9ZumWJw7kAaZkO9omraXv2t/UiFsqwMr5M/AV5UY915Vq8cxQ==",
"version": "0.8.61",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.61.tgz",
"integrity": "sha512-7DroJ0PLgI9TFvQR//6rf0NRXRvV60hapxVX5lmKzNn4Mn2Ni/JsB2ypNLKeSU5sacNyu8QT3W5Jdpafl7lcnA==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
@@ -3961,63 +3962,129 @@
"node": ">=12"
}
},
"node_modules/@primeuix/forms": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@primeuix/forms/-/forms-0.0.2.tgz",
"integrity": "sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==",
"dependencies": {
"@primeuix/utils": "^0.3.0"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primeuix/forms/node_modules/@primeuix/utils": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primeuix/styled": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.5.tgz",
"integrity": "sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==",
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
"license": "MIT",
"dependencies": {
"@primeuix/utils": "^0.0.5"
"@primeuix/utils": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primeuix/utils": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.0.5.tgz",
"integrity": "sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==",
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
"license": "MIT",
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/core": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.5.tgz",
"integrity": "sha512-DUCslDA93eUOVW0A1I3yoZgRLI4zmI2++loZQXbUF5jaXCwKiAza14+iyUU+cWH27VSq+jQnCEP9QJtPZiJJ0w==",
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz",
"integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==",
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.0.5",
"@primeuix/utils": "^0.0.5"
"@primeuix/styled": "^0.3.2",
"@primeuix/utils": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
},
"peerDependencies": {
"vue": "^3.0.0"
"vue": "^3.3.0"
}
},
"node_modules/@primevue/forms": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.2.5.tgz",
"integrity": "sha512-5jarJQ9Qv32bOo/0tY5bqR3JZI6+YmmoUQ2mjhVSbVElQsE4FNfhT7a7JwF+xgBPMPc8KWGNA1QB248HhPNVAg==",
"dependencies": {
"@primeuix/forms": "^0.0.2",
"@primeuix/utils": "^0.3.2",
"@primevue/core": "4.2.5"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/forms/node_modules/@primeuix/styled": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
"dependencies": {
"@primeuix/utils": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/forms/node_modules/@primeuix/utils": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/forms/node_modules/@primevue/core": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz",
"integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==",
"dependencies": {
"@primeuix/styled": "^0.3.2",
"@primeuix/utils": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
},
"peerDependencies": {
"vue": "^3.3.0"
}
},
"node_modules/@primevue/icons": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.5.tgz",
"integrity": "sha512-ZxR9W1wlAE2fTtUhrHyeMx5t0jNyAgxDcHPm0cNXpX8q1XF95rSM/qb48QKXIBDBrJ/xs57BcyCNADP/VDPY4g==",
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.2.5.tgz",
"integrity": "sha512-WFbUMZhQkXf/KmwcytkjGVeJ9aGEDXjP3uweOjQZMmRdEIxFnqYYpd90wE90JE1teZn3+TVnT4ZT7ejGyEXnFQ==",
"license": "MIT",
"dependencies": {
"@primeuix/utils": "^0.0.5",
"@primevue/core": "4.0.5"
"@primeuix/utils": "^0.3.2",
"@primevue/core": "4.2.5"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/themes": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.0.5.tgz",
"integrity": "sha512-cRrAhOapOT8eFCTDwNdB/acg2ZEEkn7y6h6p188PYSjJsWnYK+D8eI1Js1ZB5HwWo4sWs3oR3Sy8bPwejnGbAw==",
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.2.5.tgz",
"integrity": "sha512-8F7yA36xYIKtNuAuyBdZZEks/bKDwlhH5WjpqGGB0FdwfAEoBYsynQ5sdqcT2Lb/NsajHmS5lc++Ttlvr1g1Lw==",
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.0.5"
"@primeuix/styled": "^0.3.2"
},
"engines": {
"node": ">=12.11.0"
@@ -5726,49 +5793,53 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.31.tgz",
"integrity": "sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.24.7",
"@vue/shared": "3.4.31",
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz",
"integrity": "sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.4.31",
"@vue/shared": "3.4.31"
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz",
"integrity": "sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.24.7",
"@vue/compiler-core": "3.4.31",
"@vue/compiler-dom": "3.4.31",
"@vue/compiler-ssr": "3.4.31",
"@vue/shared": "3.4.31",
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.10",
"postcss": "^8.4.38",
"magic-string": "^0.30.11",
"postcss": "^8.4.48",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz",
"integrity": "sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.4.31",
"@vue/shared": "3.4.31"
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-vue2": {
@@ -5812,38 +5883,6 @@
}
}
},
"node_modules/@vue/language-core/node_modules/@vue/compiler-core": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/language-core/node_modules/@vue/compiler-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/language-core/node_modules/@vue/shared": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@vue/language-core/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -5871,49 +5910,54 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
"integrity": "sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.4.31"
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.31.tgz",
"integrity": "sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.4.31",
"@vue/shared": "3.4.31"
"@vue/reactivity": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz",
"integrity": "sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.4.31",
"@vue/runtime-core": "3.4.31",
"@vue/shared": "3.4.31",
"@vue/reactivity": "3.5.13",
"@vue/runtime-core": "3.5.13",
"@vue/shared": "3.5.13",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.31.tgz",
"integrity": "sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.4.31",
"@vue/shared": "3.4.31"
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13"
},
"peerDependencies": {
"vue": "3.4.31"
"vue": "3.5.13"
}
},
"node_modules/@vue/shared": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz",
"integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA=="
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"license": "MIT"
},
"node_modules/@vue/test-utils": {
"version": "2.4.6",
@@ -7847,7 +7891,8 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/data-urls": {
"version": "3.0.2",
@@ -14427,15 +14472,16 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -15170,9 +15216,10 @@
}
},
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -15336,9 +15383,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"funding": [
{
"type": "opencollective",
@@ -15353,9 +15400,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -15528,15 +15576,15 @@
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
},
"node_modules/primevue": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.5.tgz",
"integrity": "sha512-MALszGIZ5SnEQy1XeZLBFhpMXQ1OS7D1U7H+l/JAX5U46RQ1vufo7NAiWbbV5/ADjPGw4uLplqMQxujkksNY2g==",
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.2.5.tgz",
"integrity": "sha512-7UMOIJvdFz4jQyhC76yhNdSlHtXvVpmE2JSo2ndUTBWjWJOkYyT562rQ4ayO+bMdJLtzBGqgY64I9ZfEvNd7vQ==",
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.0.5",
"@primeuix/utils": "^0.0.5",
"@primevue/core": "4.0.5",
"@primevue/icons": "4.0.5"
"@primeuix/styled": "^0.3.2",
"@primeuix/utils": "^0.3.2",
"@primevue/core": "4.2.5",
"@primevue/icons": "4.2.5"
},
"engines": {
"node": ">=12.11.0"
@@ -18816,15 +18864,16 @@
"license": "MIT"
},
"node_modules/vue": {
"version": "3.4.31",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz",
"integrity": "sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.4.31",
"@vue/compiler-sfc": "3.4.31",
"@vue/runtime-dom": "3.4.31",
"@vue/server-renderer": "3.4.31",
"@vue/shared": "3.4.31"
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/runtime-dom": "3.5.13",
"@vue/server-renderer": "3.5.13",
"@vue/shared": "3.5.13"
},
"peerDependencies": {
"typescript": "*"

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.7.10",
"version": "1.8.2",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -83,9 +83,10 @@
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.7",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
"@comfyorg/comfyui-electron-types": "^0.4.11",
"@comfyorg/litegraph": "^0.8.61",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
@@ -105,10 +106,10 @@
"loglevel": "^1.9.2",
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"primevue": "^4.0.5",
"primevue": "^4.2.5",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"vue": "^3.4.31",
"vue": "^3.5.13",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.3",
"zod": "^3.23.8",

View File

@@ -0,0 +1,43 @@
import { execSync } from 'child_process'
import { readFileSync } from 'fs'
const packageName = '@comfyorg/comfyui-electron-types'
const description = 'desktop API types'
try {
// Create a new branch
console.log('Creating new branch...')
const date = new Date()
const isoDate = date.toISOString().split('T')[0]
const timestamp = date.getTime()
const branchName = `update-electron-types-${isoDate}-${timestamp}`
execSync(`git checkout -b ${branchName} -t origin/main`, { stdio: 'inherit' })
// Update npm package to latest version
console.log(`Updating ${description}...`)
execSync(`npm install ${packageName}@latest`, {
stdio: 'inherit'
})
// Get the new version from package.json
const packageLock = JSON.parse(readFileSync('./package-lock.json', 'utf8'))
const newVersion = packageLock.packages[`node_modules/${packageName}`].version
// Stage changes
const message = `[chore] Update electron-types to ${newVersion}`
execSync('git add package.json package-lock.json', { stdio: 'inherit' })
execSync(`git commit -m "${message}"`, { stdio: 'inherit' })
// Create the PR
console.log('Creating PR...')
execSync(
`gh pr create --title "${message}" --label "dependencies" --body "Automated update of ${description} to version ${newVersion}."`,
{ stdio: 'inherit' }
)
console.log(
`✅ Successfully created PR for ${description} update to ${newVersion}`
)
} catch (error) {
console.error('❌ Error during update process:', error.message)
}

View File

@@ -18,7 +18,7 @@ import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron, showNativeMenu } from './utils/envUtil'
import { electronAPI, isElectron } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
@@ -34,7 +34,7 @@ const showContextMenu = (event: PointerEvent) => {
case target instanceof HTMLTextAreaElement:
case target instanceof HTMLInputElement && target.type === 'text':
// TODO: Context input menu explicitly for text input
showNativeMenu({ type: 'text' })
electronAPI()?.showContextMenu({ type: 'text' })
return
}
}

View File

@@ -22,7 +22,7 @@
</template>
<template #item="{ item }">
<Button
:label="item.label"
:label="String(item.label)"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
size="small"

View File

@@ -20,11 +20,16 @@ const terminalCreated = (
let offData: IDisposable
let offOutput: () => void
useAutoSize(root, true, true, () => {
// If we aren't visible, don't resize
if (!terminal.element?.offsetParent) return
useAutoSize({
root,
autoRows: true,
autoCols: true,
onResize: () => {
// If we aren't visible, don't resize
if (!terminal.element?.offsetParent) return
terminalApi.resize(terminal.cols, terminal.rows)
terminalApi.resize(terminal.cols, terminal.rows)
}
})
onMounted(async () => {

View File

@@ -29,7 +29,12 @@ const terminalCreated = (
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
root: Ref<HTMLElement>
) => {
useAutoSize(root, true, false)
// `autoCols` is false because we don't want the progress bar in the terminal
// to render incorrectly as the progress bar is rendered based on the
// server's terminal size.
// Apply a min cols of 80 for colab environments
// See https://github.com/comfyanonymous/ComfyUI/issues/6396
useAutoSize({ root, autoRows: true, autoCols: false, minCols: 80 })
const update = (entries: Array<LogEntry>, size?: TerminalSize) => {
if (size) {

View File

@@ -1,40 +0,0 @@
<template>
<div :class="['flex flex-wrap', $attrs.class]">
<div
v-for="checkbox in checkboxes"
:key="checkbox.value"
class="flex items-center gap-2"
>
<Checkbox
v-model="internalSelection"
:inputId="checkbox.value"
:value="checkbox.value"
/>
<label :for="checkbox.value" class="ml-2">{{ checkbox.label }}</label>
</div>
</div>
</template>
<script setup lang="ts">
import Checkbox from 'primevue/checkbox'
import { computed } from 'vue'
interface CheckboxItem {
label: string
value: string
}
const props = defineProps<{
checkboxes: CheckboxItem[]
modelValue: string[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
const internalSelection = computed({
get: () => props.modelValue,
set: (value: string[]) => emit('update:modelValue', value)
})
</script>

View File

@@ -18,7 +18,7 @@
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="props.error"
:disabled="!!props.error"
@click="triggerDownload"
v-if="status === null || status === 'error'"
icon="pi pi-download"
@@ -30,7 +30,7 @@
v-if="status === 'in_progress' || status === 'paused'"
>
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
-->
<ProgressBar
class="flex-1"
@@ -42,7 +42,7 @@
class="file-action-button"
size="small"
outlined
:disabled="props.error"
:disabled="!!props.error"
@click="triggerPauseDownload"
v-if="status === 'in_progress'"
icon="pi pi-pause-circle"
@@ -53,7 +53,7 @@
class="file-action-button"
size="small"
outlined
:disabled="props.error"
:disabled="!!props.error"
@click="triggerResumeDownload"
v-if="status === 'paused'"
icon="pi pi-play-circle"
@@ -64,7 +64,7 @@
class="file-action-button"
size="small"
outlined
:disabled="props.error"
:disabled="!!props.error"
@click="triggerCancelDownload"
icon="pi pi-times-circle"
severity="danger"

View File

@@ -15,7 +15,7 @@
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
:disabled="props.error"
:disabled="!!props.error"
:title="props.url"
@click="download.triggerBrowserDownload"
/>

View File

@@ -1,35 +1,17 @@
<template>
<div class="color-picker-wrapper flex items-center gap-2">
<ColorPicker v-model="modelValue">
<template #header>
<div class="flex items-center justify-between p-2">
<span>{{ props.label }}</span>
<Button
v-if="props.defaultValue"
icon="pi pi-refresh"
text
size="small"
@click="resetColor"
/>
</div>
</template>
</ColorPicker>
<ColorPicker v-model="modelValue" />
<InputText v-model="modelValue" class="w-28" :placeholder="label" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ColorPicker from 'primevue/colorpicker'
import InputText from 'primevue/inputtext'
const modelValue = defineModel<string>('modelValue')
const props = defineProps<{
defineProps<{
defaultValue?: string
label?: string
}>()
const resetColor = () => {
modelValue.value = props.defaultValue || '#000000'
}
</script>

View File

@@ -1,66 +0,0 @@
<!-- A simple read-only terminal component that displays logs. -->
<template>
<div class="p-terminal rounded-none h-full w-full">
<ScrollPanel class="h-full w-full" ref="scrollPanelRef">
<pre class="px-4 whitespace-pre-wrap">{{ log }}</pre>
</ScrollPanel>
</div>
</template>
<script setup lang="ts">
import ScrollPanel from 'primevue/scrollpanel'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const props = defineProps<{
fetchLogs: () => Promise<string>
fetchInterval: number
}>()
const log = ref<string>('')
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
/**
* Whether the user has scrolled to the bottom of the terminal.
* This is used to prevent the terminal from scrolling to the bottom
* when new logs are fetched.
*/
const scrolledToBottom = ref(false)
let intervalId: number = 0
onMounted(async () => {
const element = scrollPanelRef.value?.$el
const scrollContainer = element?.querySelector('.p-scrollpanel-content')
if (scrollContainer) {
scrollContainer.addEventListener('scroll', () => {
scrolledToBottom.value =
scrollContainer.scrollTop + scrollContainer.clientHeight ===
scrollContainer.scrollHeight
})
}
const scrollToBottom = () => {
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}
watch(log, () => {
if (scrolledToBottom.value) {
scrollToBottom()
}
})
const fetchLogs = async () => {
log.value = await props.fetchLogs()
}
await fetchLogs()
scrollToBottom()
intervalId = window.setInterval(fetchLogs, props.fetchInterval)
})
onBeforeUnmount(() => {
window.clearInterval(intervalId)
})
</script>

View File

@@ -0,0 +1,53 @@
<!--
A refresh button that disables and shows a progress spinner whilst active.
Usage:
```vue
<RefreshButton
v-model="isRefreshing"
:outlined="false"
@refresh="refresh"
/>
```
-->
<template>
<Button
class="relative p-button-icon-only"
:outlined="props.outlined"
:severity="props.severity"
:disabled="active || props.disabled"
@click="(event) => $emit('refresh', event)"
>
<span
class="p-button-icon pi pi-refresh transition-all"
:class="{ 'opacity-0': active }"
data-pc-section="icon"
></span>
<span class="p-button-label" data-pc-section="label">&nbsp;</span>
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner'
import { VueSeverity } from '@/types/primeVueTypes'
// Properties
interface Props {
outlined?: boolean
disabled?: boolean
severity?: VueSeverity
}
const props = withDefaults(defineProps<Props>(), {
outlined: true,
severity: 'secondary'
})
// Model
const active = defineModel<boolean>({ required: true })
// Emits
defineEmits(['refresh'])
</script>

View File

@@ -1,10 +1,10 @@
<template>
<div :class="props.class">
<div>
<IconField>
<Button
v-if="props.filterIcon"
v-if="filterIcon"
class="p-inputicon filter-button"
:icon="props.filterIcon"
:icon="filterIcon"
text
severity="contrast"
@click="$emit('showFilter', $event)"
@@ -12,12 +12,12 @@
<InputText
class="search-box-input w-full"
@input="handleInput"
:modelValue="props.modelValue"
:placeholder="props.placeholder"
:modelValue="modelValue"
:placeholder="placeholder"
/>
<InputIcon v-if="!props.modelValue" :class="props.icon" />
<InputIcon v-if="!modelValue" :class="icon" />
<Button
v-if="props.modelValue"
v-if="modelValue"
class="p-inputicon clear-button"
icon="pi pi-times"
text
@@ -47,40 +47,36 @@ import Button from 'primevue/button'
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
import { toRefs } from 'vue'
import type { SearchFilter } from './SearchFilterChip.vue'
import SearchFilterChip from './SearchFilterChip.vue'
const props = withDefaults(
defineProps<{
class?: string
modelValue: string
placeholder?: string
icon?: string
debounceTime?: number
filterIcon?: string
filters?: TFilter[]
}>(),
{
placeholder: 'Search...',
icon: 'pi pi-search',
debounceTime: 300
}
)
const {
modelValue,
placeholder = 'Search...',
icon = 'pi pi-search',
debounceTime = 300,
filterIcon,
filters = []
} = defineProps<{
modelValue: string
placeholder?: string
icon?: string
debounceTime?: number
filterIcon?: string
filters?: TFilter[]
}>()
const { filters } = toRefs(props)
const emit = defineEmits([
'update:modelValue',
'search',
'showFilter',
'removeFilter'
])
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'search', value: string, filters: TFilter[]): void
(e: 'showFilter', event: Event): void
(e: 'removeFilter', filter: TFilter): void
}>()
const emitSearch = debounce((value: string) => {
emit('search', value, props.filters)
}, props.debounceTime)
emit('search', value, filters)
}, debounceTime)
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement

View File

@@ -19,6 +19,7 @@
v-for="device in props.stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
>
<DeviceInfo :device="device" />
</TabPanel>

View File

@@ -8,9 +8,11 @@
selectionMode="single"
:pt="{
nodeLabel: 'tree-explorer-node-label',
nodeContent: ({ props }) => ({
onClick: (e: MouseEvent) => onNodeContentClick(e, props.node),
onContextmenu: (e: MouseEvent) => handleContextMenu(props.node, e)
nodeContent: ({ context }) => ({
onClick: (e: MouseEvent) =>
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
onContextmenu: (e: MouseEvent) =>
handleContextMenu(e, context.node as RenderedTreeExplorerNode)
}),
nodeToggleButton: () => ({
onClick: (e: MouseEvent) => {
@@ -152,7 +154,7 @@ const menuItems = computed<MenuItem[]>(() =>
}))
)
const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
menuTargetNode.value = node
emit('contextMenu', node, e)
if (menuItems.value.filter((item) => item.visible).length > 0) {

View File

@@ -13,11 +13,16 @@
import { onBeforeUnmount, onMounted } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
const settingStore = useSettingStore()
const workflowStore = useWorkflowStore()
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (settingStore.get('Comfy.Window.UnloadConfirmation')) {
if (
settingStore.get('Comfy.Window.UnloadConfirmation') &&
workflowStore.modifiedWorkflows.length > 0
) {
event.preventDefault()
return true
}

View File

@@ -28,8 +28,10 @@
</template>
<ReportIssuePanel
v-if="sendReportOpen"
:title="$t('issueReport.submitErrorReport')"
error-type="graphExecutionError"
:extra-fields="[stackTraceField]"
:tags="{ exceptionMessage: props.error.exception_message }"
/>
<div class="action-container">
<FindIssueButton
@@ -88,10 +90,10 @@ const stackTraceField = computed<ReportField>(() => {
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
data: {
getData: () => ({
nodeType: props.error.node_type,
stackTrace: props.error.traceback?.join('\n')
}
})
}
})

View File

@@ -0,0 +1,31 @@
<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

@@ -1,81 +1,137 @@
<template>
<Panel>
<template #header>
<div class="flex items-center gap-2">
<span class="font-bold">{{ $t('issueReport.submitErrorReport') }}</span>
<Form
v-slot="$form"
@submit="submit"
:resolver="zodResolver(issueReportSchema)"
>
<Panel :pt="$attrs.pt">
<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-row gap-3 mb-2">
<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"
:inputId="field.value"
:value="field.value"
v-model="selection"
/>
<label :for="field.value">{{ field.label }}</label>
</FormField>
</div>
</div>
<FormField class="mb-4" v-slot="$field" name="details">
<Textarea
v-bind="$field"
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"
>
{{ t('issueReport.validation.maxLength') }}
</Message>
</FormField>
<FormField v-slot="$field" name="contactInfo">
<InputText
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>
<div class="flex flex-row 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"
:inputId="checkbox.value"
:value="checkbox.value"
v-model="contactPrefs"
:disabled="
$form.contactInfo?.error || !$form.contactInfo?.value
"
/>
<label :for="checkbox.value">{{ checkbox.label }}</label>
</FormField>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end">
<Button
v-tooltip="$t('g.reportIssueTooltip')"
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
:severity="isButtonDisabled ? 'secondary' : 'primary'"
:icon="icon"
:disabled="isButtonDisabled"
@click="reportIssue"
/>
</div>
</template>
<div class="p-4 mt-4 border border-round surface-border shadow-1">
<CheckboxGroup
v-model="selection"
class="gap-4 mb-4"
:checkboxes="reportCheckboxes"
/>
<div class="mb-4">
<InputText
v-model="contactInfo"
class="w-full"
:placeholder="$t('issueReport.provideEmail')"
:maxlength="CONTACT_MAX_LEN"
/>
<CheckboxGroup
v-model="contactPrefs"
class="gap-3 mt-2"
:checkboxes="contactCheckboxes"
/>
</div>
<div class="mb-4">
<Textarea
v-model="details"
class="w-full"
rows="4"
:maxlength="DETAILS_MAX_LEN"
:placeholder="$t('issueReport.provideAdditionalDetails')"
:aria-label="$t('issueReport.provideAdditionalDetails')"
/>
</div>
</div>
</Panel>
</Panel>
</Form>
</template>
<script setup lang="ts">
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
import { zodResolver } from '@primevue/forms/resolvers/zod'
import type { CaptureContext, User } from '@sentry/core'
import { captureMessage } from '@sentry/core'
import cloneDeep from 'lodash/cloneDeep'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
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 CheckboxGroup from '@/components/common/CheckboxGroup.vue'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import type { DefaultField, ReportField } from '@/types/issueReportTypes'
import {
type IssueReportFormData,
type ReportField,
issueReportSchema
} from '@/types/issueReportTypes'
import type {
DefaultField,
IssueReportPanelProps
} from '@/types/issueReportTypes'
import { isElectron } from '@/utils/envUtil'
const ISSUE_NAME = 'User reported issue'
const DETAILS_MAX_LEN = 5_000
const CONTACT_MAX_LEN = 320
const props = defineProps<{
errorType: string
defaultFields?: DefaultField[]
extraFields?: ReportField[]
}>()
const props = defineProps<IssueReportPanelProps>()
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
props
@@ -84,114 +140,112 @@ const toast = useToast()
const selection = ref<string[]>([])
const contactPrefs = ref<string[]>([])
const contactInfo = ref('')
const details = ref('')
const submitting = ref(false)
const submitted = ref(false)
const followUp = computed(() => contactPrefs.value.includes('FollowUp'))
const notifyResolve = computed(() => contactPrefs.value.includes('Resolution'))
const icon = computed(() => {
if (submitting.value) return 'pi pi-spin pi-spinner'
if (submitted.value) return 'pi pi-check'
return 'pi pi-send'
})
const isFormEmpty = computed(() => !selection.value.length && !details.value)
const isButtonDisabled = computed(
() => submitted.value || submitting.value || isFormEmpty.value
)
const contactCheckboxes = [
{ label: t('issueReport.contactFollowUp'), value: 'FollowUp' },
{ label: t('issueReport.notifyResolve'), value: 'Resolution' }
{ label: t('issueReport.contactFollowUp'), value: 'followUp' },
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
]
const defaultReportCheckboxes = [
{ label: t('g.workflow'), value: 'Workflow' },
{ label: t('g.logs'), value: 'Logs' },
{ label: t('issueReport.systemStats'), value: 'SystemStats' },
{ label: t('g.settings'), value: 'Settings' }
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 reportCheckboxes = computed(() => [
...(props.extraFields?.map(({ label, value }) => ({ label, value })) ?? []),
...defaultReportCheckboxes.filter(({ value }) =>
const fields = computed(() => [
...defaultFieldsConfig.filter(({ value }) =>
defaultFields.includes(value as DefaultField)
)
),
...(props.extraFields ?? [])
])
const getUserInfo = (): User => ({ email: contactInfo.value })
const createUser = (formData: IssueReportFormData): User => ({
email: formData.contactInfo || undefined
})
const getLogs = async () =>
selection.value.includes('Logs') ? api.getLogs() : null
const createExtraData = async (formData: IssueReportFormData) => {
const result = {}
const isChecked = (fieldValue: string) => formData[fieldValue]
const getSystemStats = async () =>
selection.value.includes('SystemStats') ? api.getSystemStats() : null
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) }
}
})
)
const getSettings = async () =>
selection.value.includes('Settings') ? api.getSettings() : null
const getWorkflow = () =>
selection.value.includes('Workflow')
? cloneDeep(app.graph.asSerialisable())
: null
const createDefaultFields = async () => {
const [settings, systemStats, logs, workflow] = await Promise.all([
getSettings(),
getSystemStats(),
getLogs(),
getWorkflow()
])
return { settings, systemStats, logs, workflow }
return result
}
const createExtraFields = (): Record<string, unknown> | undefined => {
if (!props.extraFields) return undefined
return props.extraFields
.filter((field) => !field.optIn || selection.value.includes(field.value))
.reduce((acc, field) => ({ ...acc, ...cloneDeep(field.data) }), {})
}
const createFeedback = () => {
const createCaptureContext = async (
formData: IssueReportFormData
): Promise<CaptureContext> => {
return {
details: details.value,
contactPreferences: {
followUp: followUp.value,
notifyOnResolution: notifyResolve.value
}
}
}
const createCaptureContext = async (): Promise<CaptureContext> => {
return {
user: getUserInfo(),
user: createUser(formData),
level: 'error',
tags: {
errorType: props.errorType
errorType: props.errorType,
followUp: formData.contactInfo ? formData.followUp : false,
notifyOnResolution: formData.contactInfo
? formData.notifyOnResolution
: false,
isElectron: isElectron(),
...props.tags
},
extra: {
...createFeedback(),
...(await createDefaultFields()),
...createExtraFields()
details: formData.details,
...(await createExtraData(formData))
}
}
}
const reportIssue = async () => {
if (isButtonDisabled.value) return
submitting.value = true
try {
captureMessage(ISSUE_NAME, await createCaptureContext())
submitted.value = true
toast.add({
severity: 'success',
summary: t('g.reportSent'),
life: 3000
})
} finally {
submitting.value = false
const submit = async (event: FormSubmitEvent) => {
if (event.valid) {
try {
const captureContext = await createCaptureContext(event.values)
captureMessage(ISSUE_NAME, 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.message,
life: 3000
})
}
}
}
</script>

View File

@@ -1,26 +1,51 @@
// @ts-strict-ignore
import { createTestingPinia } from '@pinia/testing'
import { Form } from '@primevue/forms'
import { mount } from '@vue/test-utils'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Panel from 'primevue/panel'
import Textarea from 'primevue/textarea'
import Tooltip from 'primevue/tooltip'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { createApp } from 'vue'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
import enMesages from '@/locales/en/main.json'
import { DefaultField, ReportField } from '@/types/issueReportTypes'
import { IssueReportPanelProps } from '@/types/issueReportTypes'
import ReportIssuePanel from '../ReportIssuePanel.vue'
type ReportIssuePanelProps = {
errorType: string
defaultFields?: DefaultField[]
extraFields?: ReportField[]
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({
@@ -57,18 +82,64 @@ 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() {
this.$emit('submit', {
valid: true,
values: this.formValues
})
},
updateFieldValue(name: string, value: any) {
this.formValues[name] = value
}
}
},
FormField: {
name: 'FormField',
template:
'<div><slot :modelValue="modelValue" @update:modelValue="updateValue" /></div>',
props: ['name'],
data() {
return {
modelValue: ''
}
},
methods: {
updateValue(value) {
this.modelValue = value
let parent = this.$parent
while (parent && parent.$options.name !== 'Form') {
parent = parent.$parent
}
if (parent) {
parent.updateFieldValue(this.name, value)
}
}
}
}
}))
describe('ReportIssuePanel', () => {
beforeAll(() => {
const app = createApp({})
app.use(PrimeVue)
beforeEach(() => {
vi.clearAllMocks()
})
const mountComponent = (props: ReportIssuePanelProps, options = {}): any => {
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
return mount(ReportIssuePanel, {
global: {
plugins: [PrimeVue, createTestingPinia(), i18n],
directives: { tooltip: Tooltip },
components: { InputText, Button, Panel, Textarea, CheckboxGroup }
plugins: [PrimeVue, i18n],
directives: { tooltip: Tooltip }
},
props,
...options
@@ -78,44 +149,66 @@ describe('ReportIssuePanel', () => {
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(CheckboxGroup).length).toBe(2)
expect(wrapper.findAllComponents(Checkbox).length).toBe(6)
expect(wrapper.findComponent(InputText).exists()).toBe(true)
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
expect(wrapper.findComponent(Button).exists()).toBe(true)
})
it('updates selection when checkboxes are selected', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.setValue(['Workflow', 'Logs'])
expect(wrapper.vm.selection).toEqual(['Workflow', 'Logs'])
const wrapper = mountComponent({
errorType: 'Test Error'
})
const checkboxes = wrapper.findAllComponents(Checkbox)
for (const field of DEFAULT_FIELDS) {
const checkbox = checkboxes.find(
(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.setValue('test@example.com')
expect(wrapper.vm.contactInfo).toBe('test@example.com')
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.setValue('This is a test detail.')
expect(wrapper.vm.details).toBe('This is a test detail.')
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('updates contactPrefs when preferences are selected', async () => {
it('set contact preferences back to false if email is removed', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const preferences = wrapper.findAllComponents(CheckboxGroup).at(1)
await preferences?.setValue(['FollowUp'])
expect(wrapper.vm.contactPrefs).toEqual(['FollowUp'])
})
const input = wrapper.findComponent(InputText)
it('does not allow submission if the form is empty', async () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
await wrapper.vm.reportIssue()
expect(wrapper.vm.submitted).toBe(false)
// 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', () => {
@@ -123,83 +216,87 @@ describe('ReportIssuePanel', () => {
errorType: 'Test Error',
defaultFields: ['Settings']
})
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
expect(checkboxes?.props('checkboxes')).toEqual([
{ label: 'Settings', value: 'Settings' }
])
// Filter out the contact preferences checkboxes
const fieldCheckboxes = wrapper
.findAllComponents(Checkbox)
.filter(
(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 extraFields = [
{ label: 'Custom Field', value: 'CustomField', optIn: true, data: {} }
]
const wrapper = mountComponent({ errorType: 'Test Error', extraFields })
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
expect(checkboxes?.props('checkboxes')).toContainEqual({
label: 'Custom Field',
value: 'CustomField'
const wrapper = mountComponent({
errorType: 'Test Error',
extraFields: CUSTOM_FIELDS
})
const customCheckbox = wrapper
.findAllComponents(Checkbox)
.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)
await textarea.setValue('Report with only text but no fields selected')
await wrapper.vm.reportIssue()
// 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)
const { captureMessage } = (await import('@sentry/core')) as any
const captureContext = captureMessage.mock.calls[0][1]
expect(captureContext.extra.logs).toBeNull()
expect(captureContext.extra.systemStats).toBeNull()
expect(captureContext.extra.settings).toBeNull()
expect(captureContext.extra.workflow).toBeNull()
// 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',
expectedKey: 'Logs',
mockValue: 'mock logs'
},
{
checkbox: 'SystemStats',
apiMethod: 'getSystemStats',
expectedKey: 'systemStats',
expectedKey: 'SystemStats',
mockValue: 'mock stats'
},
{
checkbox: 'Settings',
apiMethod: 'getSettings',
expectedKey: 'settings',
expectedKey: 'Settings',
mockValue: 'mock settings'
}
])(
'submits (%s) when the (%s) checkbox is selected',
'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)
const { captureMessage } = await import('@sentry/core')
// Select the checkbox
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.vm.$emit('update:modelValue', [checkbox])
await wrapper.vm.reportIssue()
expect(api[apiMethod]).toHaveBeenCalled()
// Verify the message includes the associated data
expect(captureMessage).toHaveBeenCalledWith(
'User reported issue',
expect.objectContaining({
extra: expect.objectContaining({ [expectedKey]: mockValue })
})
)
await findAndUpdateCheckbox(wrapper, checkbox)
const context = await submitForm(wrapper)
expect(context.extra[expectedKey]).toBe(mockValue)
}
)
@@ -207,24 +304,12 @@ describe('ReportIssuePanel', () => {
const wrapper = mountComponent({ errorType: 'Test Error' })
const { app } = (await import('@/scripts/app')) as any
const { captureMessage } = await import('@sentry/core')
const mockWorkflow = { nodes: [], edges: [] }
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
// Select the "Workflow" checkbox
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
await checkboxes?.vm.$emit('update:modelValue', ['Workflow'])
await findAndUpdateCheckbox(wrapper, 'Workflow')
const context = await submitForm(wrapper)
await wrapper.vm.reportIssue()
expect(app.graph.asSerialisable).toHaveBeenCalled()
// Verify the message includes the workflow
expect(captureMessage).toHaveBeenCalledWith(
'User reported issue',
expect.objectContaining({
extra: expect.objectContaining({ workflow: mockWorkflow })
})
)
expect(context.extra.Workflow).toEqual(mockWorkflow)
})
})

View File

@@ -13,13 +13,13 @@
optionValue="id"
/>
<Button
icon="pi pi-upload"
icon="pi pi-file-export"
text
:title="$t('g.export')"
@click="colorPaletteService.exportColorPalette(activePaletteId)"
/>
<Button
icon="pi pi-download"
icon="pi pi-file-import"
text
:title="$t('g.import')"
@click="importCustomPalette"

View File

@@ -72,14 +72,6 @@
'install.settings.dataCollectionDialog.collect.userJourneyEvents'
)
}}
<span
class="pi pi-info-circle text-neutral-400"
v-tooltip="
$t(
'install.settings.dataCollectionDialog.collect.userJourneyTooltip'
)
"
/>
</li>
</ul>
@@ -116,6 +108,16 @@
}}
</li>
</ul>
<div class="mt-4">
<a
href="https://comfy.org/privacy"
target="_blank"
class="text-blue-400 hover:text-blue-300 underline"
>
{{ $t('install.settings.dataCollectionDialog.viewFullPolicy') }}
</a>
</div>
</div>
</Dialog>
</div>
@@ -128,8 +130,8 @@ import ToggleSwitch from 'primevue/toggleswitch'
import { ref } from 'vue'
const showDialog = ref(false)
const autoUpdate = defineModel('autoUpdate', { required: true })
const allowMetrics = defineModel('allowMetrics', { required: true })
const autoUpdate = defineModel<boolean>('autoUpdate', { required: true })
const allowMetrics = defineModel<boolean>('allowMetrics', { required: true })
const showMetricsInfo = () => {
showDialog.value = true

View File

@@ -159,8 +159,8 @@ const pickGpu = (value: typeof selected.value) => {
}
</script>
<style lang="postcss">
:root {
<style scoped>
.p-tag {
--p-tag-gap: 0.5rem;
}

View File

@@ -0,0 +1,40 @@
<template>
<Tag :icon :severity :value />
</template>
<script setup lang="ts">
import { PrimeIcons, type PrimeIconsOptions } from '@primevue/core/api'
import Tag, { TagProps } from 'primevue/tag'
import { ref, watch } from 'vue'
import { t } from '@/i18n'
// Properties
const props = defineProps<{
error: boolean
refreshing?: boolean
}>()
// Bindings
const icon = ref<string>(null)
const severity = ref<TagProps['severity']>(null)
const value = ref<PrimeIconsOptions[keyof PrimeIconsOptions]>(null)
const updateBindings = () => {
if (props.refreshing) {
icon.value = PrimeIcons.QUESTION
severity.value = 'info'
value.value = t('maintenance.refreshing')
} else if (props.error) {
icon.value = PrimeIcons.TIMES
severity.value = 'danger'
value.value = t('g.error')
} else {
icon.value = PrimeIcons.CHECK
severity.value = 'success'
value.value = t('maintenance.OK')
}
}
watch(props, updateBindings, { deep: true })
</script>

View File

@@ -0,0 +1,127 @@
<template>
<div
class="task-div max-w-48 min-h-52 grid relative"
:class="{ 'opacity-75': isLoading }"
>
<Card
class="max-w-48 relative h-full overflow-hidden"
:class="{ 'opacity-65': runner.state !== 'error' }"
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
>
<template #header>
<i
v-if="runner.state === 'error'"
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
style="font-size: 10rem"
/>
<img
v-if="task.headerImg"
:src="task.headerImg"
class="object-contain w-full h-full opacity-25 pt-4 px-4"
/>
</template>
<template #title>{{ task.name }}</template>
<template #content>{{ description }}</template>
<template #footer>
<div class="flex gap-4 mt-1">
<Button
:icon="task.button?.icon"
:label="task.button?.text"
class="w-full"
raised
icon-pos="right"
@click="(event) => $emit('execute', event)"
:loading="isExecuting"
/>
</div>
</template>
</Card>
<i
v-if="!isLoading && runner.state === 'OK'"
class="task-card-ok pi pi-check"
/>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Card from 'primevue/card'
import { computed } from 'vue'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
const taskStore = useMaintenanceTaskStore()
const runner = computed(() => taskStore.getRunner(props.task))
// Properties
const props = defineProps<{
task: MaintenanceTask
}>()
// Events
defineEmits<{
execute: [event: MouseEvent]
}>()
// Bindings
const description = computed(() =>
runner.value.state === 'error'
? props.task.errorDescription ?? props.task.shortDescription
: props.task.shortDescription
)
// Use a minimum run time to ensure tasks "feel" like they have run
const reactiveLoading = computed(() => runner.value.refreshing)
const reactiveExecuting = computed(() => runner.value.executing)
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
</script>
<style scoped>
.task-card-ok {
@apply text-green-500 absolute -right-4 -bottom-4 opacity-100 row-span-full col-span-full transition-opacity;
font-size: 4rem;
text-shadow: 0.25rem 0 0.5rem black;
z-index: 10;
}
.p-card {
@apply transition-opacity;
--p-card-background: var(--p-button-secondary-background);
opacity: 0.9;
&.opacity-65 {
opacity: 0.4;
}
&:hover {
opacity: 1;
}
}
:deep(.p-card-header) {
z-index: 0;
}
:deep(.p-card-body) {
z-index: 1;
flex-grow: 1;
justify-content: space-between;
}
.task-div {
> i {
pointer-events: none;
}
&:hover > i {
opacity: 0.2;
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<tr
class="border-neutral-700 border-solid border-y"
:class="{
'opacity-50': runner.resolved,
'opacity-75': isLoading && runner.resolved
}"
>
<td class="text-center w-16">
<TaskListStatusIcon :state="runner.state" :loading="isLoading" />
</td>
<td>
<p class="inline-block">{{ task.name }}</p>
<Button
class="inline-block mx-2"
type="button"
:icon="PrimeIcons.INFO_CIRCLE"
severity="secondary"
:text="true"
@click="toggle"
/>
<Popover ref="infoPopover" class="block m-1 max-w-64 min-w-32">
<span class="whitespace-pre-line">{{ task.description }}</span>
</Popover>
</td>
<td class="text-right px-4">
<Button
:icon="task.button?.icon"
:label="task.button?.text"
:severity
icon-pos="right"
@click="(event) => $emit('execute', event)"
:loading="isExecuting"
/>
</td>
</tr>
</template>
<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api'
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { VueSeverity } from '@/types/primeVueTypes'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
import TaskListStatusIcon from './TaskListStatusIcon.vue'
const taskStore = useMaintenanceTaskStore()
const runner = computed(() => taskStore.getRunner(props.task))
// Properties
const props = defineProps<{
task: MaintenanceTask
}>()
// Events
defineEmits<{
execute: [event: MouseEvent]
}>()
// Binding
const severity = computed<VueSeverity>(() =>
runner.value.state === 'error' || runner.value.state === 'warning'
? 'primary'
: 'secondary'
)
// Use a minimum run time to ensure tasks "feel" like they have run
const reactiveLoading = computed(() => runner.value.refreshing)
const reactiveExecuting = computed(() => runner.value.executing)
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
// Popover
const infoPopover = ref()
const toggle = (event: Event) => {
infoPopover.value.toggle(event)
}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<!-- Tasks -->
<section class="my-4">
<template v-if="filter.tasks.length === 0">
<!-- Empty filter -->
<Divider />
<p class="text-neutral-400 w-full text-center">
{{ $t('maintenance.allOk') }}
</p>
</template>
<template v-else>
<!-- Display: List -->
<table
v-if="displayAsList === PrimeIcons.LIST"
class="w-full border-collapse border-hidden"
>
<TaskListItem
v-for="task in filter.tasks"
:key="task.id"
:task
@execute="(event) => confirmButton(event, task)"
/>
</table>
<!-- Display: Cards -->
<template v-else>
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
<TaskCard
v-for="task in filter.tasks"
:key="task.id"
:task
@execute="(event) => confirmButton(event, task)"
/>
</div>
</template>
</template>
<ConfirmPopup />
</section>
</template>
<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api'
import { useConfirm, useToast } from 'primevue'
import ConfirmPopup from 'primevue/confirmpopup'
import Divider from 'primevue/divider'
import { t } from '@/i18n'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type {
MaintenanceFilter,
MaintenanceTask
} from '@/types/desktop/maintenanceTypes'
import TaskCard from './TaskCard.vue'
import TaskListItem from './TaskListItem.vue'
const toast = useToast()
const confirm = useConfirm()
const taskStore = useMaintenanceTaskStore()
// Properties
const props = defineProps<{
displayAsList: string
filter: MaintenanceFilter
isRefreshing: boolean
}>()
const executeTask = async (task: MaintenanceTask) => {
let message: string | undefined
try {
// Success
if ((await taskStore.execute(task)) === true) return
message = t('maintenance.error.taskFailed')
} catch (error) {
message = (error as Error)?.message
}
toast.add({
severity: 'error',
summary: t('maintenance.error.toastTitle'),
detail: message ?? t('maintenance.error.defaultDescription'),
life: 10_000
})
}
// Commands
const confirmButton = async (event: MouseEvent, task: MaintenanceTask) => {
if (!task.requireConfirm) {
await executeTask(task)
return
}
confirm.require({
target: event.currentTarget as HTMLElement,
message: task.confirmText ?? t('maintenance.confirmTitle'),
icon: 'pi pi-exclamation-circle',
rejectProps: {
label: t('g.cancel'),
severity: 'secondary',
outlined: true
},
acceptProps: {
label: task.button?.text ?? t('g.save'),
severity: task.severity ?? 'primary'
},
// TODO: Not awaited.
accept: async () => {
await executeTask(task)
}
})
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<ProgressSpinner v-if="!state || loading" class="h-8 w-8" />
<template v-else>
<i :class="cssClasses" v-tooltip.top="{ value: tooltip, showDelay: 250 }" />
</template>
</template>
<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api'
import ProgressSpinner from 'primevue/progressspinner'
import { MaybeRef, computed } from 'vue'
import { t } from '@/i18n'
import { MaintenanceTaskState } from '@/stores/maintenanceTaskStore'
// Properties
const tooltip = computed(() => {
if (props.state === 'error') {
return t('g.error')
} else if (props.state === 'OK') {
return t('maintenance.OK')
} else {
return t('maintenance.Skipped')
}
})
const cssClasses = computed(() => {
let classes: string
if (props.state === 'error') {
classes = `${PrimeIcons.EXCLAMATION_TRIANGLE} text-red-500`
} else if (props.state === 'OK') {
classes = `${PrimeIcons.CHECK} text-green-500`
} else {
classes = PrimeIcons.MINUS
}
return `text-3xl pi ${classes}`
})
// Model
const props = defineProps<{
state?: MaintenanceTaskState
loading?: MaybeRef<boolean>
}>()
</script>

View File

@@ -6,11 +6,27 @@ export default {
name: 'AutoCompletePlus',
extends: AutoComplete,
emits: ['focused-option-changed'],
data() {
return {
// Flag to determine if IME is active
isComposing: false
}
},
mounted() {
if (typeof AutoComplete.mounted === 'function') {
AutoComplete.mounted.call(this)
}
// Retrieve the actual <input> element and attach IME events
const inputEl = this.$el.querySelector('input')
if (inputEl) {
inputEl.addEventListener('compositionstart', () => {
this.isComposing = true
})
inputEl.addEventListener('compositionend', () => {
this.isComposing = false
})
}
// Add a watcher on the focusedOptionIndex property
this.$watch(
() => this.focusedOptionIndex,
@@ -19,6 +35,18 @@ export default {
this.$emit('focused-option-changed', newVal)
}
)
},
methods: {
// Override onKeyDown to block Enter when IME is active
onKeyDown(event) {
if (event.key === 'Enter' && this.isComposing) {
event.preventDefault()
event.stopPropagation()
return
}
AutoComplete.methods.onKeyDown.call(this, event)
}
}
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<teleport :to="teleportTarget">
<nav :class="'side-tool-bar-container' + (isSmall ? ' small-sidebar' : '')">
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }">
<SidebarIcon
v-for="tab in tabs"
:key="tab.id"
@@ -69,17 +69,6 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
}
</script>
<style>
:root {
--sidebar-width: 4rem;
--sidebar-icon-size: 1.5rem;
}
:root .small-sidebar {
--sidebar-width: 2.5rem;
--sidebar-icon-size: 1rem;
}
</style>
<style scoped>
.side-tool-bar-container {
display: flex;
@@ -94,6 +83,14 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
background-color: var(--comfy-menu-secondary-bg);
color: var(--fg-color);
box-shadow: var(--bar-shadow);
--sidebar-width: 4rem;
--sidebar-icon-size: 1.5rem;
}
.side-tool-bar-container.small-sidebar {
--sidebar-width: 2.5rem;
--sidebar-icon-size: 1rem;
}
.side-tool-bar-end {

View File

@@ -99,7 +99,7 @@ import type { MenuItem } from 'primevue/menuitem'
import ProgressSpinner from 'primevue/progressspinner'
import { useConfirm } from 'primevue/useconfirm'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
import { computed, ref, shallowRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
@@ -194,10 +194,6 @@ const confirmRemoveAll = (event: Event) => {
})
}
const onStatus = async () => {
await queueStore.update()
}
const menu = ref(null)
const menuTargetTask = ref<TaskItemImpl | null>(null)
const menuTargetNode = ref<ComfyNode | null>(null)
@@ -267,13 +263,4 @@ watch(allTasks, () => {
const newIndex = galleryActiveIndex.value + lengthChange
galleryActiveIndex.value = Math.max(0, newIndex)
})
onMounted(() => {
api.addEventListener('status', onStatus)
queueStore.update()
})
onUnmounted(() => {
api.removeEventListener('status', onStatus)
})
</script>

View File

@@ -19,8 +19,9 @@ export const CORE_MENU_COMMANDS = [
[
'Comfy.Help.OpenComfyUIIssues',
'Comfy.Help.OpenComfyUIDocs',
'Comfy.Help.OpenComfyOrgDiscord'
'Comfy.Help.OpenComfyOrgDiscord',
'Comfy.Help.OpenComfyUIForum'
]
],
[['Help'], ['Comfy.Help.AboutComfyUI']]
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.Feedback']]
]

View File

@@ -230,7 +230,8 @@ export const CORE_SETTINGS: SettingParams[] = [
id: 'Comfy.Window.UnloadConfirmation',
name: 'Show confirmation when closing window',
type: 'boolean',
defaultValue: false
defaultValue: true,
versionModified: '1.7.12'
},
{
id: 'Comfy.TreeExplorer.ItemPadding',

View File

@@ -0,0 +1,144 @@
import { PrimeIcons } from '@primevue/core'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
const electron = electronAPI()
const openUrl = (url: string) => {
window.open(url, '_blank')
return true
}
export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
{
id: 'basePath',
execute: async () => await electron.setBasePath(),
name: 'Base path',
shortDescription: 'Change the application base path.',
errorDescription: 'Unable to open the base path. Please select a new one.',
description:
'The base path is the default location where ComfyUI stores data. It is the location fo the python environment, and may also contain models, custom nodes, and other extensions.',
isInstallationFix: true,
button: {
icon: PrimeIcons.QUESTION,
text: 'Select'
}
},
{
id: 'git',
headerImg: '/assets/images/Git-Logo-White.svg',
execute: () => openUrl('https://git-scm.com/downloads/'),
name: 'Download git',
shortDescription: 'Open the git download page.',
description:
'Git is required to download and manage custom nodes and other extensions. This fixer simply opens the download page in your browser. You must download and install git manually.',
button: {
icon: PrimeIcons.EXTERNAL_LINK,
text: 'Download'
}
},
{
id: 'vcRedist',
execute: () => openUrl('https://aka.ms/vs/17/release/vc_redist.x64.exe'),
name: 'Download VC++ Redist',
shortDescription: 'Download the latest VC++ Redistributable runtime.',
description:
'The Visual C++ runtime libraries are required to run ComfyUI. You will need to download and install this file.',
button: {
icon: PrimeIcons.EXTERNAL_LINK,
text: 'Download'
}
},
{
id: 'reinstall',
severity: 'danger',
requireConfirm: true,
execute: async () => {
await electron.reinstall()
return true
},
name: 'Reinstall ComfyUI',
shortDescription:
'Deletes the desktop app config and load the welcome screen.',
description:
'Delete the desktop app config, restart the app, and load the installation screen.',
confirmText: 'Delete all saved config and reinstall?',
button: {
icon: PrimeIcons.EXCLAMATION_TRIANGLE,
text: 'Reinstall'
}
},
{
id: 'pythonPackages',
requireConfirm: true,
execute: async () => {
try {
await electron.uv.installRequirements()
return true
} catch (error) {
return false
}
},
name: 'Install python packages',
shortDescription:
'Installs the base python packages required to run ComfyUI.',
errorDescription:
'Python packages that are required to run ComfyUI are not installed.',
description:
'This will install the python packages required to run ComfyUI. This includes torch, torchvision, and other dependencies.',
usesTerminal: true,
isInstallationFix: true,
button: {
icon: PrimeIcons.DOWNLOAD,
text: 'Install'
}
},
{
id: 'uv',
execute: () =>
openUrl('https://docs.astral.sh/uv/getting-started/installation/'),
name: 'uv executable',
shortDescription: 'uv installs and maintains the python environment.',
description:
"This will open the download page for Astral's uv tool. uv is used to install python and manage python packages.",
button: {
icon: 'pi pi-asterisk',
text: 'Download'
}
},
{
id: 'uvCache',
severity: 'danger',
requireConfirm: true,
execute: async () => await electron.uv.clearCache(),
name: 'uv cache',
shortDescription: 'Remove the Astral uv cache of python packages.',
description:
'This will remove the uv cache directory and its contents. All downloaded python packages will need to be downloaded again.',
confirmText: 'Delete uv cache of python packages?',
isInstallationFix: true,
button: {
icon: PrimeIcons.TRASH,
text: 'Clear cache'
}
},
{
id: 'venvDirectory',
severity: 'danger',
requireConfirm: true,
execute: async () => await electron.uv.resetVenv(),
name: 'Reset virtual environment',
shortDescription:
'Remove and recreate the .venv directory. This removes all python packages.',
description:
'The python environment is where ComfyUI installs python and python packages. It is used to run the ComfyUI server.',
confirmText: 'Delete the .venv directory?',
usesTerminal: true,
isInstallationFix: true,
button: {
icon: PrimeIcons.FOLDER,
text: 'Recreate'
}
}
] as const

View File

@@ -1,6 +1,8 @@
import { t } from '@/i18n'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
;(async () => {
@@ -8,6 +10,7 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
const electronAPI = getElectronAPI()
const desktopAppVersion = await electronAPI.getElectronVersion()
const workflowStore = useWorkflowStore()
const onChangeRestartApp = (newValue: string, oldValue: string) => {
// Add a delay to allow changes to take effect before restarting.
@@ -39,18 +42,18 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
id: 'Comfy-Desktop.WindowStyle',
category: ['Comfy-Desktop', 'General', 'Window Style'],
name: 'Window Style',
tooltip: 'Choose custom option to hide the system title bar',
tooltip: "Custom: Replace the system title bar with ComfyUI's Top menu",
type: 'combo',
experimental: true,
defaultValue: 'default',
options: ['default', 'custom'],
onChange: (
newValue: 'default' | 'custom',
oldValue: 'default' | 'custom'
oldValue?: 'default' | 'custom'
) => {
electronAPI.Config.setWindowStyle(newValue)
if (!oldValue) return
onChangeRestartApp(newValue, oldValue)
electronAPI.Config.setWindowStyle(newValue)
}
}
],
@@ -112,14 +115,6 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
electronAPI.openDevTools()
}
},
{
id: 'Comfy-Desktop.OpenFeedbackPage',
label: 'Feedback',
icon: 'pi pi-envelope',
function() {
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
}
},
{
id: 'Comfy-Desktop.OpenUserGuide',
label: 'Desktop User Guide',
@@ -149,16 +144,32 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
function() {
electronAPI.restartApp()
}
},
{
id: 'Comfy-Desktop.Quit',
label: 'Quit',
icon: 'pi pi-sign-out',
async function() {
// Confirm if unsaved workflows are open
if (workflowStore.modifiedWorkflows.length > 0) {
const confirmed = await useDialogService().confirm({
message: t('desktopMenu.confirmQuit'),
title: t('desktopMenu.quit'),
type: 'default'
})
if (!confirmed) return
}
electronAPI.quit()
}
}
],
menuCommands: [
{
path: ['Help'],
commands: [
'Comfy-Desktop.OpenUserGuide',
'Comfy-Desktop.OpenFeedbackPage'
]
commands: ['Comfy-Desktop.OpenUserGuide']
},
{
path: ['Help'],
@@ -181,6 +192,16 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
}
],
keybindings: [
{
commandId: 'Workspace.CloseWorkflow',
combo: {
key: 'w',
ctrl: true
}
}
],
aboutPageBadges: [
{
label: 'ComfyUI_desktop v' + desktopAppVersion,

View File

@@ -1,7 +1,10 @@
import { debounce } from 'lodash'
import { api } from '../../scripts/api'
import { app } from '../../scripts/app'
import { ComfyApp } from '../../scripts/app'
import { $el, ComfyDialog } from '../../scripts/ui'
import { getStorageValue, setStorageValue } from '../../scripts/utils'
import { ClipspaceDialog } from './clipspace'
import { MaskEditorDialogOld } from './maskEditorOld'
@@ -262,15 +265,15 @@ var styles = `
}
#maskEditor_toolPanel {
height: 100%;
width: var(--sidebar-width);
width: 4rem;
z-index: 8888;
background: var(--comfy-menu-bg);
display: flex;
flex-direction: column;
}
.maskEditor_toolPanelContainer {
width: var(--sidebar-width);
height: var(--sidebar-width);
width: 4rem;
height: 4rem;
display: flex;
justify-content: center;
align-items: center;
@@ -331,7 +334,7 @@ var styles = `
margin-bottom: 5px;
}
#maskEditor_pointerZone {
width: calc(100% - var(--sidebar-width) - 220px);
width: calc(100% - 4rem - 220px);
height: 100%;
}
#maskEditor_uiContainer {
@@ -703,8 +706,8 @@ var styles = `
}
.maskEditor_toolPanelZoomIndicator {
width: var(--sidebar-width);
height: var(--sidebar-width);
width: 4rem;
height: 4rem;
display: flex;
flex-direction: column;
justify-content: center;
@@ -776,10 +779,37 @@ interface Offset {
}
export interface Brush {
type: BrushShape
size: number
opacity: number
hardness: number
type: BrushShape
smoothingPrecision: number
}
const saveBrushToCache = debounce(function (key: string, brush: Brush): void {
try {
const brushString = JSON.stringify(brush)
setStorageValue(key, brushString)
} catch (error) {
console.error('Failed to save brush to cache:', error)
}
}, 300)
function loadBrushFromCache(key: string): Brush | null {
try {
const brushString = getStorageValue(key)
if (brushString) {
const brush = JSON.parse(brushString) as Brush
console.log('Loaded brush from cache:', brush)
return brush
} else {
console.log('No brush found in cache.')
return null
}
} catch (error) {
console.error('Failed to load brush from cache:', error)
return null
}
}
type Callback = (data?: any) => void
@@ -1952,12 +1982,19 @@ class BrushTool {
'Comfy.MaskEditor.BrushAdjustmentSpeed'
)
this.brushSettings = {
size: 10,
opacity: 100,
hardness: 1,
type: BrushShape.Arc
const cachedBrushSettings = loadBrushFromCache('maskeditor_brush_settings')
if (cachedBrushSettings) {
this.brushSettings = cachedBrushSettings
} else {
this.brushSettings = {
type: BrushShape.Arc,
size: 10,
opacity: 0.7,
hardness: 1,
smoothingPrecision: 10
}
}
this.maskBlendMode = MaskBlendMode.Black
}
@@ -2016,6 +2053,10 @@ class BrushTool {
'brushType',
async () => this.brushSettings.type
)
this.messageBroker.createPullTopic(
'brushSmoothingPrecision',
async () => this.brushSettings.smoothingPrecision
)
this.messageBroker.createPullTopic(
'maskBlendMode',
async () => this.maskBlendMode
@@ -2143,7 +2184,7 @@ class BrushTool {
}
const distanceBetweenPoints =
(this.brushSettings.size / this.smoothingPrecision) * 6
(this.brushSettings.size / this.brushSettings.smoothingPrecision) * 6
const stepNr = Math.ceil(totalLength / distanceBetweenPoints)
let interpolatedPoints = points
@@ -2190,7 +2231,7 @@ class BrushTool {
const brush_size = await this.messageBroker.pull('brushSize')
const distance = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
const steps = Math.ceil(
distance / ((brush_size / this.smoothingPrecision) * 4)
distance / ((brush_size / this.brushSettings.smoothingPrecision) * 4)
) // Adjust for smoother lines
const interpolatedOpacity =
1 / (1 + Math.exp(-6 * (this.brushSettings.opacity - 0.5))) -
@@ -2545,23 +2586,27 @@ class BrushTool {
private setBrushSize(size: number) {
this.brushSettings.size = size
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
}
private setBrushOpacity(opacity: number) {
this.brushSettings.opacity = opacity
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
}
private setBrushHardness(hardness: number) {
this.brushSettings.hardness = hardness
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
}
private setBrushType(type: BrushShape) {
this.brushSettings.type = type
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
}
private setBrushSmoothingPrecision(precision: number) {
//console.log('precision', precision)
this.smoothingPrecision = precision
this.brushSettings.smoothingPrecision = precision
saveBrushToCache('maskeditor_brush_settings', this.brushSettings)
}
}
@@ -2825,7 +2870,6 @@ class UIManager {
const circle_shape = document.createElement('div')
circle_shape.id = 'maskEditor_sidePanelBrushShapeCircle'
circle_shape.classList.add(shapeColor)
circle_shape.style.background = 'var(--p-button-text-primary-color)'
circle_shape.addEventListener('click', () => {
this.messageBroker.publish('setBrushShape', BrushShape.Arc)
this.setBrushBorderRadius()
@@ -2836,7 +2880,6 @@ class UIManager {
const square_shape = document.createElement('div')
square_shape.id = 'maskEditor_sidePanelBrushShapeSquare'
square_shape.classList.add(shapeColor)
square_shape.style.background = ''
square_shape.addEventListener('click', () => {
this.messageBroker.publish('setBrushShape', BrushShape.Rect)
this.setBrushBorderRadius()
@@ -2844,6 +2887,16 @@ class UIManager {
circle_shape.style.background = ''
})
if (
(await this.messageBroker.pull('brushSettings')).type === BrushShape.Arc
) {
circle_shape.style.background = 'var(--p-button-text-primary-color)'
square_shape.style.background = ''
} else {
circle_shape.style.background = ''
square_shape.style.background = 'var(--p-button-text-primary-color)'
}
brush_shape_container.appendChild(circle_shape)
brush_shape_container.appendChild(square_shape)
@@ -2855,7 +2908,7 @@ class UIManager {
1,
100,
1,
10,
(await this.messageBroker.pull('brushSettings')).size,
(event, value) => {
this.messageBroker.publish('setBrushSize', parseInt(value))
this.updateBrushPreview()
@@ -2868,7 +2921,7 @@ class UIManager {
0,
1,
0.01,
0.7,
(await this.messageBroker.pull('brushSettings')).opacity,
(event, value) => {
this.messageBroker.publish('setBrushOpacity', parseFloat(value))
this.updateBrushPreview()
@@ -2881,7 +2934,7 @@ class UIManager {
0,
1,
0.01,
1,
(await this.messageBroker.pull('brushSettings')).hardness,
(event, value) => {
this.messageBroker.publish('setBrushHardness', parseFloat(value))
this.updateBrushPreview()
@@ -2894,7 +2947,7 @@ class UIManager {
1,
100,
1,
10,
(await this.messageBroker.pull('brushSettings')).smoothingPrecision,
(event, value) => {
this.messageBroker.publish(
'setBrushSmoothingPrecision',
@@ -2903,7 +2956,31 @@ class UIManager {
}
)
const resetBrushSettingsButton = document.createElement('button')
resetBrushSettingsButton.id = 'resetBrushSettingsButton'
resetBrushSettingsButton.innerText = 'Reset to Default'
resetBrushSettingsButton.addEventListener('click', () => {
this.messageBroker.publish('setBrushShape', BrushShape.Arc)
this.messageBroker.publish('setBrushSize', 10)
this.messageBroker.publish('setBrushOpacity', 0.7)
this.messageBroker.publish('setBrushHardness', 1)
this.messageBroker.publish('setBrushSmoothingPrecision', 10)
circle_shape.style.background = 'var(--p-button-text-primary-color)'
square_shape.style.background = ''
thicknesSliderObj.slider.value = '10'
opacitySliderObj.slider.value = '0.7'
hardnessSliderObj.slider.value = '1'
brushSmoothingPrecisionSliderObj.slider.value = '10'
this.setBrushBorderRadius()
this.updateBrushPreview()
})
brush_settings_container.appendChild(brush_settings_title)
brush_settings_container.appendChild(resetBrushSettingsButton)
brush_settings_container.appendChild(brush_shape_outer_container)
brush_settings_container.appendChild(thicknesSliderObj.container)
brush_settings_container.appendChild(opacitySliderObj.container)

View File

@@ -36,12 +36,21 @@ export function useTerminal(element: Ref<HTMLElement>) {
return {
terminal,
useAutoSize(
root: Ref<HTMLElement>,
autoRows: boolean = true,
autoCols: boolean = true,
useAutoSize({
root,
autoRows = true,
autoCols = true,
minCols = Number.NEGATIVE_INFINITY,
minRows = Number.NEGATIVE_INFINITY,
onResize
}: {
root: Ref<HTMLElement>
autoRows?: boolean
autoCols?: boolean
minCols?: number
minRows?: number
onResize?: () => void
) {
}) {
const ensureValidRows = (rows: number | undefined) => {
if (rows == null || isNaN(rows)) {
return root.value?.clientHeight / 20
@@ -61,8 +70,14 @@ export function useTerminal(element: Ref<HTMLElement>) {
const dims = fitAddon.proposeDimensions()
// Sometimes propose returns NaN, so we may need to estimate.
terminal.resize(
autoCols ? ensureValidCols(dims?.cols) : terminal.cols,
autoRows ? ensureValidRows(dims?.rows) : terminal.rows
Math.max(
autoCols ? ensureValidCols(dims?.cols) : terminal.cols,
minCols
),
Math.max(
autoRows ? ensureValidRows(dims?.rows) : terminal.rows,
minRows
)
)
onResize?.()
}

View File

@@ -9,6 +9,7 @@ import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
} from '@/constants/coreColorPalettes'
import { t } from '@/i18n'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
@@ -538,6 +539,32 @@ export function useCoreCommands(): ComfyCommand[] {
if (workflowStore.activeWorkflow)
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.Help.OpenComfyUIForum',
icon: 'pi pi-comments',
label: 'Open ComfyUI Forum',
menubarLabel: 'ComfyUI Forum',
versionAdded: '1.8.2',
function: () => {
window.open('https://forum.comfy.org/', '_blank')
}
}
]
}

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "Open DevTools"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "Feedback"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Desktop User Guide"
},
"Comfy-Desktop_Quit": {
"label": "Quit"
},
"Comfy-Desktop_Reinstall": {
"label": "Reinstall"
},
@@ -83,6 +83,9 @@
"Comfy_ExportWorkflowAPI": {
"label": "Export Workflow (API Format)"
},
"Comfy_Feedback": {
"label": "Give Feedback"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Fit Group To Contents"
},
@@ -107,6 +110,9 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "Open ComfyUI Docs"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Open ComfyUI Forum"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "Open ComfyUI Issues"
},

View File

@@ -70,17 +70,26 @@
"keybinding": "Keybinding",
"upload": "Upload",
"export": "Export",
"workflow": "Workflow"
"workflow": "Workflow",
"success": "Success",
"ok": "OK",
"feedback": "Feedback"
},
"issueReport": {
"submitErrorReport": "Submit Error Report (Optional)",
"provideEmail": "Give us your email (Optional)",
"provideEmail": "Give us your email (optional)",
"provideAdditionalDetails": "Provide additional details (optional)",
"stackTrace": "Stack Trace",
"systemStats": "System Stats",
"contactFollowUp": "Contact me for follow up",
"notifyResolve": "Notify me when resolved",
"helpFix": "Help Fix This"
"helpFix": "Help Fix This",
"rating": "Rating",
"feedbackTitle": "Help us improve ComfyUI by providing feedback",
"validation": {
"maxLength": "Message too long",
"invalidEmail": "Please enter a valid email address"
}
},
"color": {
"default": "Default",
@@ -151,7 +160,7 @@
"appDataLocationTooltip": "ComfyUI's app data directory. Stores:\n- Logs\n- Server configs",
"appPathLocationTooltip": "ComfyUI's app asset directory. Stores the ComfyUI code and assets",
"migrateFromExistingInstallation": "Migrate from Existing Installation",
"migrationSourcePathDescription": "If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation.",
"migrationSourcePathDescription": "If you have an existing ComfyUI installation, we can copy/link your existing user files and models to the new installation. Your existing ComfyUI installation will not be affected.",
"selectItemsToMigrate": "Select Items to Migrate",
"migrationOptional": "Migration is optional. If you don't have an existing installation, you can skip this step.",
"desktopAppSettings": "Desktop App Settings",
@@ -182,6 +191,8 @@
"settings": {
"autoUpdate": "Automatic Updates",
"allowMetrics": "Usage Metrics",
"errorUpdatingConsent": "Error Updating Consent",
"errorUpdatingConsentDetail": "Failed to update metrics consent settings",
"autoUpdateDescription": "Automatically download updates when they become available. You will be notified before updates are installed.",
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous usage metrics. No personal information or workflow content will be collected.",
"learnMoreAboutData": "Learn more about data collection",
@@ -192,19 +203,25 @@
"collect": {
"errorReports": "Error message and stack trace",
"systemInfo": "Hardware, OS type, and app version",
"userJourneyEvents": "User journey events",
"userJourneyTooltip": "User journey events are used to track the user's journey through the app installation process. The event collection ends on the first successful ComfyUI workflow run."
"userJourneyEvents": "User journey events"
},
"doNotCollect": {
"personalInformation": "Personal information",
"fileSystemInformation": "File system information",
"workflowContents": "Workflow contents",
"customNodeConfigurations": "Custom node configurations"
}
},
"viewFullPolicy": "View full policy"
}
},
"customNodes": "Custom Nodes",
"customNodesDescription": "Reinstall custom nodes from existing ComfyUI installations."
"customNodesDescription": "Reinstall custom nodes from existing ComfyUI installations.",
"helpImprove": "Please help improve ComfyUI",
"moreInfo": "For more info, please read our",
"privacyPolicy": "privacy policy",
"metricsEnabled": "Metrics Enabled",
"metricsDisabled": "Metrics Disabled",
"updateConsent": "You previously opted in to reporting crashes. We are now tracking event-based metrics to help identify bugs and improve the app. No personal identifiable information is collected."
},
"serverStart": {
"reinstall": "Reinstall",
@@ -349,8 +366,8 @@
"Open Models Folder": "Open Models Folder",
"Open Outputs Folder": "Open Outputs Folder",
"Open DevTools": "Open DevTools",
"Feedback": "Feedback",
"Desktop User Guide": "Desktop User Guide",
"Quit": "Quit",
"Reinstall": "Reinstall",
"Restart": "Restart",
"Browse Templates": "Browse Templates",
@@ -370,6 +387,7 @@
"Duplicate Current Workflow": "Duplicate Current Workflow",
"Export": "Export",
"Export (API)": "Export (API)",
"Give Feedback": "Give Feedback",
"Fit Group To Contents": "Fit Group To Contents",
"Group Selected Nodes": "Group Selected Nodes",
"Convert selected nodes to group node": "Convert selected nodes to group node",
@@ -378,6 +396,7 @@
"About ComfyUI": "About ComfyUI",
"Comfy-Org Discord": "Comfy-Org Discord",
"ComfyUI Docs": "ComfyUI Docs",
"ComfyUI Forum": "ComfyUI Forum",
"ComfyUI Issues": "ComfyUI Issues",
"Interrupt": "Interrupt",
"Load Default Workflow": "Load Default Workflow",
@@ -408,7 +427,9 @@
},
"desktopMenu": {
"reinstall": "Reinstall",
"confirmReinstall": "This will clear your extra_models_config.yaml file,\nand begin installation again.\n\nAre you sure?"
"confirmReinstall": "This will clear your extra_models_config.yaml file,\nand begin installation again.\n\nAre you sure?",
"quit": "Quit",
"confirmQuit": "There are unsaved workflows open; any unsaved changes will be lost. Ignore this and quit?"
},
"settingsCategories": {
"Comfy-Desktop": "Comfy-Desktop",
@@ -600,6 +621,7 @@
"combine": "combine",
"cond single": "cond single",
"controlnet": "controlnet",
"inpaint": "inpaint",
"scheduling": "scheduling",
"create": "create",
"mask": "mask",
@@ -619,7 +641,6 @@
"batch": "batch",
"video_models": "video_models",
"upscaling": "upscaling",
"inpaint": "inpaint",
"instructpix2pix": "instructpix2pix",
"compositing": "compositing",
"samplers": "samplers",
@@ -671,5 +692,21 @@
"UPSCALE_MODEL": "UPSCALE_MODEL",
"VAE": "VAE",
"WEBCAM": "WEBCAM"
},
"maintenance": {
"allOk": "No issues were detected.",
"status": "Status",
"detected": "Detected",
"refreshing": "Refreshing",
"None": "None",
"OK": "OK",
"Skipped": "Skipped",
"showManual": "Show maintenance tasks",
"confirmTitle": "Are you sure?",
"error": {
"toastTitle": "Task error",
"taskFailed": "Task failed to run.",
"defaultDescription": "An error occurred while running a maintenance task."
}
}
}

View File

@@ -177,7 +177,7 @@
},
"CLIPLoader": {
"display_name": "Load CLIP",
"description": "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
"description": "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
"inputs": {
"clip_name": {
"name": "clip_name"
@@ -862,6 +862,32 @@
}
}
},
"CosmosImageToVideoLatent": {
"display_name": "CosmosImageToVideoLatent",
"inputs": {
"vae": {
"name": "vae"
},
"width": {
"name": "width"
},
"height": {
"name": "height"
},
"length": {
"name": "length"
},
"batch_size": {
"name": "batch_size"
},
"start_image": {
"name": "start_image"
},
"end_image": {
"name": "end_image"
}
}
},
"CreateHookKeyframe": {
"display_name": "Create Hook Keyframe",
"inputs": {
@@ -1230,6 +1256,23 @@
}
}
},
"EmptyCosmosLatentVideo": {
"display_name": "EmptyCosmosLatentVideo",
"inputs": {
"width": {
"name": "width"
},
"height": {
"name": "height"
},
"length": {
"name": "length"
},
"batch_size": {
"name": "batch_size"
}
}
},
"EmptyHunyuanLatentVideo": {
"display_name": "EmptyHunyuanLatentVideo",
"inputs": {
@@ -4745,6 +4788,17 @@
}
}
},
"SetFirstSigma": {
"display_name": "SetFirstSigma",
"inputs": {
"sigmas": {
"name": "sigmas"
},
"sigma": {
"name": "sigma"
}
}
},
"SetHookKeyframes": {
"display_name": "Set Hook Keyframes",
"inputs": {

View File

@@ -7,7 +7,7 @@
},
"Comfy-Desktop_WindowStyle": {
"name": "Window Style",
"tooltip": "Choose custom option to hide the system title bar",
"tooltip": "Custom: Replace the system title bar with ComfyUI's Top menu",
"options": {
"default": "default",
"custom": "custom"

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "Ouvrir les outils de développement"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "Retour d'information"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Guide de l'utilisateur du bureau"
},
"Comfy-Desktop_Quit": {
"label": "Quitter"
},
"Comfy-Desktop_Reinstall": {
"label": "Réinstaller"
},
@@ -83,6 +83,9 @@
"Comfy_ExportWorkflowAPI": {
"label": "Exporter le flux de travail (format API)"
},
"Comfy_Feedback": {
"label": "Retour d'information"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Ajuster le groupe au contenu"
},
@@ -107,6 +110,9 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "Ouvrir les documents ComfyUI"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Ouvrir le forum Comfy-Org"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "Ouvrir les problèmes ComfyUI"
},

View File

@@ -43,7 +43,9 @@
"WEBCAM": "WEBCAM"
},
"desktopMenu": {
"confirmQuit": "Il y a des flux de travail non enregistrés ouverts; toutes les modifications non enregistrées seront perdues. Ignorer cela et quitter?",
"confirmReinstall": "Cela effacera votre fichier extra_models_config.yaml,\net commencera l'installation à nouveau.\n\nÊtes-vous sûr ?",
"quit": "Quitter",
"reinstall": "Réinstaller"
},
"downloadGit": {
@@ -87,6 +89,7 @@
"experimental": "BETA",
"export": "Exportation",
"extensionName": "Nom de l'extension",
"feedback": "Commentaires",
"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.",
"goToNode": "Aller au nœud",
@@ -107,6 +110,7 @@
"noTasksFound": "Aucune tâche trouvée",
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
"noWorkflowsFound": "Aucun flux de travail trouvé.",
"ok": "OK",
"openNewIssue": "Ouvrir un nouveau problème",
"overwrite": "Écraser",
"reconnected": "Reconnecté",
@@ -129,6 +133,7 @@
"searchWorkflows": "Rechercher des flux de travail",
"settings": "Paramètres",
"showReport": "Afficher le rapport",
"success": "Succès",
"systemInfo": "Informations système",
"terminal": "Terminal",
"upload": "Téléverser",
@@ -186,6 +191,7 @@
"selectGpu": "Sélectionnez le GPU",
"selectGpuDescription": "Sélectionnez le type de GPU que vous avez"
},
"helpImprove": "Veuillez aider à améliorer ComfyUI",
"installLocation": "Emplacement d'installation",
"installLocationDescription": "Sélectionnez le répertoire pour les données utilisateur de ComfyUI. Un environnement python sera installé à l'emplacement sélectionné. Veuillez vous assurer que le disque sélectionné a suffisamment d'espace (~15GB) restant.",
"installLocationTooltip": "Répertoire des données utilisateur de ComfyUI. Stocke :\n- Environnement Python\n- Modèles\n- Nœuds personnalisés\n",
@@ -197,13 +203,17 @@
"title": "Configuration manuelle",
"virtualEnvironmentPath": "Chemin de l'environnement virtuel"
},
"metricsDisabled": "Métriques désactivées",
"metricsEnabled": "Métriques activées",
"migrateFromExistingInstallation": "Migrer à partir d'une installation existante",
"migration": "Migration",
"migrationOptional": "La migration est facultative. Si vous n'avez pas d'installation existante, vous pouvez sauter cette étape.",
"migrationSourcePathDescription": "Si vous avez une installation existante de ComfyUI, nous pouvons copier/lier vos fichiers utilisateur et modèles existants à la nouvelle installation.",
"migrationSourcePathDescription": "Si vous avez une installation existante de ComfyUI, nous pouvons copier/lier vos fichiers utilisateur et modèles existants à la nouvelle installation. Votre installation existante de ComfyUI ne sera pas affectée.",
"moreInfo": "Pour plus d'informations, veuillez lire notre",
"parentMissing": "Le chemin n'existe pas - créez d'abord le répertoire contenant",
"pathExists": "Le répertoire existe déjà - veuillez vous assurer que vous avez sauvegardé toutes les données",
"pathValidationFailed": "Échec de la validation du chemin",
"privacyPolicy": "politique de confidentialité",
"selectItemsToMigrate": "Sélectionnez les éléments à migrer",
"settings": {
"allowMetrics": "Métriques d'utilisation",
@@ -214,8 +224,7 @@
"collect": {
"errorReports": "Message d'erreur et trace de la pile",
"systemInfo": "Matériel, type de système d'exploitation et version de l'application",
"userJourneyEvents": "Événements du parcours utilisateur",
"userJourneyTooltip": "Les événements du parcours utilisateur sont utilisés pour suivre le parcours de l'utilisateur lors du processus d'installation de l'application. La collecte d'événements se termine lors de la première exécution réussie du flux de travail ComfyUI."
"userJourneyEvents": "Événements du parcours utilisateur"
},
"doNotCollect": {
"customNodeConfigurations": "Configurations de nœud personnalisées",
@@ -224,23 +233,49 @@
"workflowContents": "Contenus du flux de travail"
},
"title": "À propos de la collecte de données",
"viewFullPolicy": "Voir la politique complète",
"whatWeCollect": "Ce que nous collectons :",
"whatWeDoNotCollect": "Ce que nous ne collectons pas :"
},
"errorUpdatingConsent": "Erreur de mise à jour du consentement",
"errorUpdatingConsentDetail": "Échec de la mise à jour des paramètres de consentement aux métriques",
"learnMoreAboutData": "En savoir plus sur la collecte de données"
},
"systemLocations": "Emplacements système",
"unhandledError": "Erreur inconnue"
"unhandledError": "Erreur inconnue",
"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",
"feedbackTitle": "Aidez-nous à améliorer ComfyUI en fournissant des commentaires",
"helpFix": "Aidez à résoudre cela",
"notifyResolve": "Prévenez-moi lorsque résolu",
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
"provideEmail": "Donnez-nous votre email (Facultatif)",
"rating": "Évaluation",
"stackTrace": "Trace de la pile",
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
"systemStats": "Statistiques du système"
"systemStats": "Statistiques du système",
"validation": {
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
"maxLength": "Message trop long"
}
},
"maintenance": {
"None": "Aucun",
"OK": "OK",
"Skipped": "Ignoré",
"allOk": "Aucun problème détecté.",
"confirmTitle": "Êtes-vous sûr ?",
"detected": "Détecté",
"error": {
"defaultDescription": "Une erreur s'est produite lors de l'exécution d'une tâche de maintenance.",
"taskFailed": "La tâche a échoué.",
"toastTitle": "Erreur de tâche"
},
"refreshing": "Actualisation",
"showManual": "Afficher les tâches de maintenance",
"status": "Statut"
},
"menu": {
"autoQueue": "File d'attente automatique",
@@ -278,6 +313,7 @@
"Collapse/Expand Selected Nodes": "Réduire/Étendre les nœuds sélectionnés",
"Comfy-Org Discord": "Discord de Comfy-Org",
"ComfyUI Docs": "Docs de ComfyUI",
"ComfyUI Forum": "Forum ComfyUI",
"ComfyUI Issues": "Problèmes de ComfyUI",
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
"Desktop User Guide": "Guide de l'utilisateur de bureau",
@@ -285,9 +321,9 @@
"Edit": "Éditer",
"Export": "Exporter",
"Export (API)": "Exporter (API)",
"Feedback": "Retour d'information",
"Fit Group To Contents": "Ajuster le groupe au contenu",
"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",
"Interrupt": "Interrompre",
@@ -309,6 +345,7 @@
"Previous Opened Workflow": "Flux de travail ouvert précédent",
"Queue Prompt": "Invite de file d'attente",
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
"Quit": "Quitter",
"Redo": "Refaire",
"Refresh Node Definitions": "Actualiser les définitions de nœud",
"Reinstall": "Réinstaller",

View File

@@ -113,7 +113,7 @@
}
},
"CLIPLoader": {
"description": "[Recettes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
"description": "[Recettes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
"display_name": "Charger CLIP",
"inputs": {
"clip_name": {
@@ -862,6 +862,32 @@
}
}
},
"CosmosImageToVideoLatent": {
"display_name": "CosmosImageVersVidéoLatent",
"inputs": {
"batch_size": {
"name": "taille_du_lot"
},
"end_image": {
"name": "image_de_fin"
},
"height": {
"name": "hauteur"
},
"length": {
"name": "longueur"
},
"start_image": {
"name": "image_de_départ"
},
"vae": {
"name": "vae"
},
"width": {
"name": "largeur"
}
}
},
"CreateHookKeyframe": {
"display_name": "Créer une image clé de crochet",
"inputs": {
@@ -1230,6 +1256,23 @@
}
}
},
"EmptyCosmosLatentVideo": {
"display_name": "VidéoLatenteCosmosVide",
"inputs": {
"batch_size": {
"name": "taille_du_lot"
},
"height": {
"name": "hauteur"
},
"length": {
"name": "longueur"
},
"width": {
"name": "largeur"
}
}
},
"EmptyHunyuanLatentVideo": {
"display_name": "EmptyHunyuanLatentVideo",
"inputs": {
@@ -4825,6 +4868,17 @@
}
}
},
"SetFirstSigma": {
"display_name": "DéfinirPremierSigma",
"inputs": {
"sigma": {
"name": "sigma"
},
"sigmas": {
"name": "sigmas"
}
}
},
"SetHookKeyframes": {
"display_name": "Définir les Images Clés de Crochet",
"inputs": {

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "DevToolsを開く"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "フィードバック"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "デスクトップユーザーガイド"
},
"Comfy-Desktop_Quit": {
"label": "終了"
},
"Comfy-Desktop_Reinstall": {
"label": "再インストール"
},
@@ -83,6 +83,9 @@
"Comfy_ExportWorkflowAPI": {
"label": "ワークフローをエクスポートAPI形式"
},
"Comfy_Feedback": {
"label": "フィードバック"
},
"Comfy_Graph_FitGroupToContents": {
"label": "グループを内容に合わせて調整"
},
@@ -107,6 +110,9 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "ComfyUIのドキュメントを開く"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Comfy-Orgフォーラムを開く"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "ComfyUIの問題を開く"
},

View File

@@ -43,7 +43,9 @@
"WEBCAM": "ウェブカメラ"
},
"desktopMenu": {
"confirmQuit": "保存されていないワークフローが開いています。保存されていない変更はすべて失われます。これを無視して終了しますか?",
"confirmReinstall": "これにより、extra_models_config.yamlファイルがクリアされ、再インストールが開始されます。本当によろしいですか",
"quit": "終了",
"reinstall": "再インストール"
},
"downloadGit": {
@@ -87,6 +89,7 @@
"experimental": "ベータ",
"export": "エクスポート",
"extensionName": "拡張機能名",
"feedback": "フィードバック",
"findIssues": "問題を見つける",
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択して古いUIに戻してください。",
"goToNode": "ノードに移動",
@@ -107,6 +110,7 @@
"noTasksFound": "タスクが見つかりません",
"noTasksFoundMessage": "キューにタスクがありません。",
"noWorkflowsFound": "ワークフローが見つかりません。",
"ok": "OK",
"openNewIssue": "新しい問題を開く",
"overwrite": "上書き",
"reconnected": "再接続されました",
@@ -129,6 +133,7 @@
"searchWorkflows": "ワークフローを検索",
"settings": "設定",
"showReport": "レポートを表示",
"success": "成功",
"systemInfo": "システム情報",
"terminal": "ターミナル",
"upload": "アップロード",
@@ -186,6 +191,7 @@
"selectGpu": "GPUを選択",
"selectGpuDescription": "所有しているGPUのタイプを選択してください"
},
"helpImprove": "ComfyUIの改善にご協力ください",
"installLocation": "インストール先",
"installLocationDescription": "ComfyUIのユーザーデータを保存するディレクトリを選択してください。Python環境が選択した場所にインストールされます。選択したディスクに約15GBの空き容量が必要です。",
"installLocationTooltip": "ComfyUIのユーザーデータディレクトリ。保存内容:\n- Python環境\n- モデル\n- カスタムノード\n",
@@ -197,13 +203,17 @@
"title": "マニュアル設定",
"virtualEnvironmentPath": "仮想環境のパス"
},
"metricsDisabled": "メトリクス無効",
"metricsEnabled": "メトリクス有効",
"migrateFromExistingInstallation": "既存のインストールから移行",
"migration": "移行",
"migrationOptional": "移行は任意です。既存のインストールがない場合、このステップをスキップできます。",
"migrationSourcePathDescription": "既存のComfyUIインストールがある場合、既存のユーザーファイルモデルを新しいインストールにコピー/リンクできます。",
"migrationSourcePathDescription": "既存のComfyUIインストールがある場合、既存のユーザーファイルモデルを新しいインストールにコピー/リンクすることができます。既存のComfyUIインストールは影響を受けません。",
"moreInfo": "詳細は、私たちの",
"parentMissing": "パスが存在しません - 最初に含まれるディレクトリを作成してください",
"pathExists": "ディレクトリはすでに存在します - すべてのデータをバックアップしたことを確認してください",
"pathValidationFailed": "パスの検証に失敗しました",
"privacyPolicy": "プライバシーポリシー",
"selectItemsToMigrate": "移行する項目を選択",
"settings": {
"allowMetrics": "使用状況のメトリクス",
@@ -214,8 +224,7 @@
"collect": {
"errorReports": "エラーメッセージとスタックトレース",
"systemInfo": "ハードウェア、OSタイプ、アプリバージョン",
"userJourneyEvents": "ユーザージャーニーイベント",
"userJourneyTooltip": "ユーザージャーニーイベントは、アプリのインストールプロセスを通じてユーザーの旅を追跡するために使用されます。イベントの収集は、最初の成功したComfyUIワークフローの実行で終了します。"
"userJourneyEvents": "ユーザージャーニーイベント"
},
"doNotCollect": {
"customNodeConfigurations": "カスタムノードの設定",
@@ -224,23 +233,49 @@
"workflowContents": "ワークフローの内容"
},
"title": "データ収集について",
"viewFullPolicy": "完全なポリシーを見る",
"whatWeCollect": "収集内容:",
"whatWeDoNotCollect": "収集しない内容:"
},
"errorUpdatingConsent": "同意の更新エラー",
"errorUpdatingConsentDetail": "メトリクスの同意設定の更新に失敗しました",
"learnMoreAboutData": "データ収集の詳細を見る"
},
"systemLocations": "システムの場所",
"unhandledError": "未知のエラー"
"unhandledError": "未知のエラー",
"updateConsent": "以前はクラッシュの報告に同意していました。現在、バグの特定とアプリの改善を助けるためにイベントベースのメトリクスを追跡しています。個人を特定できる情報は収集されません。"
},
"issueReport": {
"contactFollowUp": "フォローアップのために私に連絡する",
"feedbackTitle": "フィードバックを提供してComfyUIの改善にご協力ください",
"helpFix": "これを修正するのを助ける",
"notifyResolve": "解決したときに通知する",
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
"rating": "評価",
"stackTrace": "スタックトレース",
"submitErrorReport": "エラーレポートを提出する(オプション)",
"systemStats": "システム統計"
"systemStats": "システム統計",
"validation": {
"invalidEmail": "有効なメールアドレスを入力してください",
"maxLength": "メッセージが長すぎます"
}
},
"maintenance": {
"None": "なし",
"OK": "OK",
"Skipped": "スキップされました",
"allOk": "問題は検出されませんでした。",
"confirmTitle": "よろしいですか?",
"detected": "検出されました",
"error": {
"defaultDescription": "メンテナンスタスクの実行中にエラーが発生しました。",
"taskFailed": "タスクの実行に失敗しました。",
"toastTitle": "タスクエラー"
},
"refreshing": "更新中",
"showManual": "メンテナンスタスクを表示",
"status": "ステータス"
},
"menu": {
"autoQueue": "自動キュー",
@@ -278,6 +313,7 @@
"Collapse/Expand Selected Nodes": "選択したノードの折りたたみ/展開",
"Comfy-Org Discord": "Comfy-Org Discord",
"ComfyUI Docs": "ComfyUIのドキュメント",
"ComfyUI Forum": "ComfyUI フォーラム",
"ComfyUI Issues": "ComfyUIの問題",
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
"Desktop User Guide": "デスクトップユーザーガイド",
@@ -285,9 +321,9 @@
"Edit": "編集",
"Export": "エクスポート",
"Export (API)": "エクスポート (API)",
"Feedback": "フィードバック",
"Fit Group To Contents": "グループを内容に合わせる",
"Fit view to selected nodes": "選択したノードにビューを合わせる",
"Give Feedback": "フィードバックを送る",
"Group Selected Nodes": "選択したノードをグループ化",
"Help": "ヘルプ",
"Interrupt": "中断",
@@ -309,6 +345,7 @@
"Previous Opened Workflow": "前に開いたワークフロー",
"Queue Prompt": "キューのプロンプト",
"Queue Prompt (Front)": "キューのプロンプト (前面)",
"Quit": "終了",
"Redo": "やり直す",
"Refresh Node Definitions": "ノード定義を更新",
"Reinstall": "再インストール",

View File

@@ -113,7 +113,7 @@
}
},
"CLIPLoader": {
"description": "[レシピ]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
"description": "[レシピ]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
"display_name": "CLIPを読み込む",
"inputs": {
"clip_name": {
@@ -862,6 +862,32 @@
}
}
},
"CosmosImageToVideoLatent": {
"display_name": "CosmosImageToVideoLatent",
"inputs": {
"batch_size": {
"name": "バッチサイズ"
},
"end_image": {
"name": "終了画像"
},
"height": {
"name": "高さ"
},
"length": {
"name": "長さ"
},
"start_image": {
"name": "開始画像"
},
"vae": {
"name": "vae"
},
"width": {
"name": "幅"
}
}
},
"CreateHookKeyframe": {
"display_name": "フックキーフレームを作成",
"inputs": {
@@ -1230,6 +1256,23 @@
}
}
},
"EmptyCosmosLatentVideo": {
"display_name": "EmptyCosmosLatentVideo",
"inputs": {
"batch_size": {
"name": "バッチサイズ"
},
"height": {
"name": "高さ"
},
"length": {
"name": "長さ"
},
"width": {
"name": "幅"
}
}
},
"EmptyHunyuanLatentVideo": {
"display_name": "EmptyHunyuanLatentVideo",
"inputs": {
@@ -4825,6 +4868,17 @@
}
}
},
"SetFirstSigma": {
"display_name": "SetFirstSigma",
"inputs": {
"sigma": {
"name": "シグマ"
},
"sigmas": {
"name": "シグマ"
}
}
},
"SetHookKeyframes": {
"display_name": "フックキーフレームを設定",
"inputs": {

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "DevTools 열기"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "피드백"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "데스크톱 사용자 가이드"
},
"Comfy-Desktop_Quit": {
"label": "종료"
},
"Comfy-Desktop_Reinstall": {
"label": "재설치"
},
@@ -83,6 +83,9 @@
"Comfy_ExportWorkflowAPI": {
"label": "워크플로 내보내기 (API 형식)"
},
"Comfy_Feedback": {
"label": "피드백"
},
"Comfy_Graph_FitGroupToContents": {
"label": "그룹을 내용에 맞게 맞추기"
},
@@ -107,6 +110,9 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "ComfyUI 문서 열기"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Comfy-Org 포럼 열기"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "ComfyUI 문제 열기"
},

View File

@@ -43,7 +43,9 @@
"WEBCAM": "웹캠"
},
"desktopMenu": {
"confirmQuit": "저장되지 않은 워크플로가 열려 있습니다. 저장되지 않은 변경 사항은 모두 손실됩니다. 이를 무시하고 종료하시겠습니까?",
"confirmReinstall": "이 작업은 extra_models_config.yaml 파일을 지우고 설치를 다시 시작합니다. 정말로 진행하시겠습니까?",
"quit": "종료",
"reinstall": "재설치"
},
"downloadGit": {
@@ -87,6 +89,7 @@
"experimental": "베타",
"export": "내보내기",
"extensionName": "확장 이름",
"feedback": "피드백",
"findIssues": "문제 찾기",
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
"goToNode": "노드로 이동",
@@ -107,6 +110,7 @@
"noTasksFound": "작업을 찾을 수 없습니다.",
"noTasksFoundMessage": "대기열에 작업이 없습니다.",
"noWorkflowsFound": "워크플로를 찾을 수 없습니다.",
"ok": "확인",
"openNewIssue": "새 문제 열기",
"overwrite": "덮어쓰기",
"reconnected": "재연결됨",
@@ -129,11 +133,12 @@
"searchWorkflows": "워크플로 검색",
"settings": "설정",
"showReport": "보고서 보기",
"success": "성공",
"systemInfo": "시스템 정보",
"terminal": "터미널",
"upload": "업로드",
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
"workflow": "워크플로"
"workflow": "워크플로"
},
"graphCanvasMenu": {
"fitView": "보기 맞춤",
@@ -186,6 +191,7 @@
"selectGpu": "GPU 선택",
"selectGpuDescription": "소유한 GPU 유형을 선택하세요"
},
"helpImprove": "ComfyUI 개선에 도움을 주세요",
"installLocation": "설치 위치",
"installLocationDescription": "ComfyUI의 사용자 데이터 디렉토리를 선택하십시오. 선택한 위치에 Python 환경이 설치됩니다. 선택한 디스크에 충분한 공간(~15GB)이 남아 있는지 확인하십시오.",
"installLocationTooltip": "ComfyUI의 사용자 데이터 디렉토리. 저장소:\n- Python 환경\n- 모델\n- 사용자 정의 노드\n",
@@ -197,50 +203,79 @@
"title": "수동 구성",
"virtualEnvironmentPath": "가상 환경 경로"
},
"metricsDisabled": "데이터 수집 비활성화",
"metricsEnabled": "데이터 수집 활성화",
"migrateFromExistingInstallation": "기존 설치에서 마이그레이션",
"migration": "마이그레이션",
"migrationOptional": "마이그레이션은 선택 사항입니다. 기존에 설치된 것이 없다면, 이 단계를 건너뛸 수 있습니다.",
"migrationSourcePathDescription": "기존에 설치된 ComfyUI가 있는 경우, 기존 사용자 파일과 모델을 새 설치 복사하거나 링크할 수 있습니다",
"migrationSourcePathDescription": "기존에 설치된 ComfyUI가 있으면, 기존 사용자 파일과 모델을 새 설치본으로 복사하거나 링크 할 수 있습니다. 기존의 ComfyUI 설치는 영향을 받지 않습니다.",
"moreInfo": "더 많은 정보를 원하시면, 다음을 읽어주세요",
"parentMissing": "경로가 존재하지 않습니다 - 먼저 포함하는 디렉토리를 생성하세요",
"pathExists": "디렉토리가 이미 존재합니다 - 모든 데이터를 백업했는지 확인해 주세요",
"pathValidationFailed": "경로 유효성 검사 실패",
"privacyPolicy": "개인정보 보호정책",
"selectItemsToMigrate": "마이그레이션 항목 선택",
"settings": {
"allowMetrics": "사용 통계",
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다.",
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다.",
"autoUpdate": "자동 업데이트",
"autoUpdateDescription": "업데이트가 가능해지면 자동으로 다운로드하고 설치합니다. 업데이트가 설치되기 전에 항상 알림을 받습니다.",
"dataCollectionDialog": {
"collect": {
"errorReports": "오류 메시지 및 스택 추적",
"systemInfo": "하드웨어, OS 유형, 앱 버전",
"userJourneyEvents": "사용자 여정 이벤트",
"userJourneyTooltip": "사용자 여정 이벤트는 앱 설치 과정을 통한 사용자의 여정을 추적하는 데 사용됩니다. 이벤트 수집은 첫 번째 성공적인 ComfyUI 워크플로우 실행에서 종료됩니다."
"userJourneyEvents": "사용자 행동 흐름 이벤트"
},
"doNotCollect": {
"customNodeConfigurations": "사용자 정의 노드 구성",
"customNodeConfigurations": "커스텀 노드 구성",
"fileSystemInformation": "파일 시스템 정보",
"personalInformation": "개인 정보",
"workflowContents": "워크플로 내용"
"workflowContents": "워크플로 내용"
},
"title": "데이터 수집 안내",
"viewFullPolicy": "전체 정책 보기",
"whatWeCollect": "수집하는 정보:",
"whatWeDoNotCollect": "수집하지 않는 정보:"
},
"errorUpdatingConsent": "데이터 수집 동의 설정 업데이트 오류",
"errorUpdatingConsentDetail": "데이터 수집 동의 설정 업데이트에 실패했습니다",
"learnMoreAboutData": "데이터 수집에 대해 더 알아보기"
},
"systemLocations": "시스템 위치",
"unhandledError": "알 수 없는 오류"
"unhandledError": "알 수 없는 오류",
"updateConsent": "이전에 충돌 보고에 동의하셨습니다. 이제 버그를 식별하고 앱을 개선하기 위해 이벤트 기반 통계 정보의 추적을 시작합니다. 개인을 식별할 수 있는 정보는 수집되지 않습니다."
},
"issueReport": {
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
"feedbackTitle": "피드백을 제공함으로써 ComfyUI를 개선하는 데 도움을 주십시오",
"helpFix": "이 문제 해결에 도움을 주세요",
"notifyResolve": "해결되었을 때 알려주세요",
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
"provideEmail": "이메일을 알려주세요 (선택 사항)",
"rating": "평가",
"stackTrace": "스택 추적",
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
"systemStats": "시스템 통계"
"systemStats": "시스템 통계",
"validation": {
"invalidEmail": "유효한 이메일 주소를 입력해 주세요",
"maxLength": "메시지가 너무 깁니다"
}
},
"maintenance": {
"None": "없음",
"OK": "확인",
"Skipped": "건너뜀",
"allOk": "문제가 발견되지 않았습니다.",
"confirmTitle": "확실합니까?",
"detected": "감지됨",
"error": {
"defaultDescription": "유지 보수 작업을 실행하는 동안 오류가 발생했습니다.",
"taskFailed": "작업 실행에 실패했습니다.",
"toastTitle": "작업 오류"
},
"refreshing": "새로 고침 중",
"showManual": "유지 보수 작업 보기",
"status": "상태"
},
"menu": {
"autoQueue": "자동 실행 큐",
@@ -274,20 +309,21 @@
"Clear Pending Tasks": "보류 중인 작업 제거하기",
"Clear Workflow": "워크플로 지우기",
"Clipspace": "클립스페이스",
"Close Current Workflow": "현재 워크플로 닫기",
"Close Current Workflow": "현재 워크플로 닫기",
"Collapse/Expand Selected Nodes": "선택한 노드 축소/확장",
"Comfy-Org Discord": "Comfy-Org 디스코드",
"ComfyUI Docs": "ComfyUI 문서",
"ComfyUI Forum": "ComfyUI 포럼",
"ComfyUI Issues": "ComfyUI 이슈 페이지",
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
"Desktop User Guide": "데스크톱 사용자 가이드",
"Duplicate Current Workflow": "현재 워크플로 복제",
"Duplicate Current Workflow": "현재 워크플로 복제",
"Edit": "편집",
"Export": "내보내기",
"Export (API)": "내보내기 (API)",
"Feedback": "피드백",
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
"Give Feedback": "피드백 제공",
"Group Selected Nodes": "선택한 노드 그룹화",
"Help": "도움말",
"Interrupt": "중단",
@@ -309,6 +345,7 @@
"Previous Opened Workflow": "이전 열린 워크플로",
"Queue Prompt": "실행 큐에 프롬프트 추가",
"Queue Prompt (Front)": "실행 큐 맨 앞에 프롬프트 추가",
"Quit": "종료",
"Redo": "다시 실행",
"Refresh Node Definitions": "노드 정의 새로 고침",
"Reinstall": "재설치",

View File

@@ -12,7 +12,7 @@
"name": "노이즈"
},
"sigmas": {
"name": "시그마"
"name": "시그마 배열"
}
}
},
@@ -113,7 +113,7 @@
}
},
"CLIPLoader": {
"description": "[조합법]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5",
"description": "[조합법]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 / clip-g / clip-l\nstable_audio: t5\nmochi: t5\ncosmos: old t5 xxl",
"display_name": "CLIP 로드",
"inputs": {
"clip_name": {
@@ -862,6 +862,32 @@
}
}
},
"CosmosImageToVideoLatent": {
"display_name": "CosmosImageToVideoLatent",
"inputs": {
"batch_size": {
"name": "배치 크기"
},
"end_image": {
"name": "끝 이미지"
},
"height": {
"name": "높이"
},
"length": {
"name": "길이"
},
"start_image": {
"name": "시작 이미지"
},
"vae": {
"name": "vae"
},
"width": {
"name": "너비"
}
}
},
"CreateHookKeyframe": {
"display_name": "후크 키프레임 생성",
"inputs": {
@@ -1230,6 +1256,23 @@
}
}
},
"EmptyCosmosLatentVideo": {
"display_name": "EmptyCosmosLatentVideo",
"inputs": {
"batch_size": {
"name": "배치 크기"
},
"height": {
"name": "높이"
},
"length": {
"name": "길이"
},
"width": {
"name": "너비"
}
}
},
"EmptyHunyuanLatentVideo": {
"display_name": "빈 잠재 비디오 (Hunyuan)",
"inputs": {
@@ -1382,10 +1425,10 @@
}
},
"FlipSigmas": {
"display_name": "시그마 뒤집기",
"display_name": "시그마 배열 뒤집기",
"inputs": {
"sigmas": {
"name": "시그마"
"name": "시그마 배열"
}
}
},
@@ -4524,7 +4567,7 @@
"name": "샘플러"
},
"sigmas": {
"name": "시그마"
"name": "시그마 배열"
}
},
"outputs": {
@@ -4552,7 +4595,7 @@
"name": "샘플러"
},
"sigmas": {
"name": "시그마"
"name": "시그마 배열"
}
},
"outputs": {
@@ -4825,6 +4868,17 @@
}
}
},
"SetFirstSigma": {
"display_name": "SetFirstSigma",
"inputs": {
"sigma": {
"name": "시그마"
},
"sigmas": {
"name": "시그마 배열"
}
}
},
"SetHookKeyframes": {
"display_name": "후크 키프레임 설정",
"inputs": {
@@ -4929,10 +4983,10 @@
}
},
"SplitSigmas": {
"display_name": "시그마 분할 (스텝)",
"display_name": "시그마 배열 분할 (스텝)",
"inputs": {
"sigmas": {
"name": "시그마"
"name": "시그마 배열"
},
"step": {
"name": "분할 스텝"
@@ -4940,29 +4994,29 @@
},
"outputs": {
"0": {
"name": "높은 시그마"
"name": "높은 시그마 배열"
},
"1": {
"name": "낮은 시그마"
"name": "낮은 시그마 배열"
}
}
},
"SplitSigmasDenoise": {
"display_name": "시그마 분할 (노이즈 제거양)",
"display_name": "시그마 배열 분할 (노이즈 제거양)",
"inputs": {
"denoise": {
"name": "노이즈 제거양"
},
"sigmas": {
"name": "시그마"
"name": "시그마 배열"
}
},
"outputs": {
"0": {
"name": "높은 시그마"
"name": "높은 시그마 배열"
},
"1": {
"name": "낮은 시그마"
"name": "낮은 시그마 배열"
}
}
},

View File

@@ -1,31 +1,31 @@
{
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Открыть папку с пользовательскими узлами"
"label": "Открыть папку пользовательских нод"
},
"Comfy-Desktop_Folders_OpenInputsFolder": {
"label": "Открыть папку с входными данными"
"label": "Открыть папку входных данных"
},
"Comfy-Desktop_Folders_OpenLogsFolder": {
"label": "Открыть папку с логами"
"label": "Открыть папку логов"
},
"Comfy-Desktop_Folders_OpenModelConfig": {
"label": "Открыть extra_model_paths.yaml"
},
"Comfy-Desktop_Folders_OpenModelsFolder": {
"label": "Открыть папку с моделями"
"label": "Открыть папку моделей"
},
"Comfy-Desktop_Folders_OpenOutputsFolder": {
"label": "Открыть папку с результатами"
"label": "Открыть папку результатов"
},
"Comfy-Desktop_OpenDevTools": {
"label": "Открыть инструменты разработчика"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "Обратная связь"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Руководство пользователя для рабочего стола"
},
"Comfy-Desktop_Quit": {
"label": "Выйти"
},
"Comfy-Desktop_Reinstall": {
"label": "Переустановить"
},
@@ -36,7 +36,7 @@
"label": "Просмотр шаблонов"
},
"Comfy_Canvas_FitView": {
"label": "Подогнать вид к выбранным узлам"
"label": "Подогнать вид к выбранным нодам"
},
"Comfy_Canvas_ResetView": {
"label": "Сбросить вид"
@@ -48,19 +48,19 @@
"label": "Переключить блокировку холста"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "Обход/Необход выбранных узлов"
"label": "Обход/Необход выбранных нод"
},
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
"label": "Свернуть/Развернуть выбранные узлы"
"label": "Свернуть/Развернуть выбранные ноды"
},
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
"label": "Отключить/Включить звук выбранных узлов"
"label": "Отключить/Включить звук выбранных нод"
},
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "Закрепить/Открепить выбранные узлы"
"label": "Закрепить/Открепить выбранные ноды"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Закрепить/Открепить выбранные элементы"
"label": "Закрепить/Открепить выбранных нод"
},
"Comfy_Canvas_ZoomIn": {
"label": "Увеличить"
@@ -83,32 +83,38 @@
"Comfy_ExportWorkflowAPI": {
"label": "Экспорт рабочего процесса (формат API)"
},
"Comfy_Feedback": {
"label": "Обратная связь"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Подогнать группу к содержимому"
},
"Comfy_Graph_GroupSelectedNodes": {
"label": "Группировать выбранные узлы"
"label": "Группировать выбранные ноды"
},
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
"label": "Преобразовать выбранные узлы в групповой узел"
"label": "Преобразовать выбранные ноды в групповую ноду"
},
"Comfy_GroupNode_ManageGroupNodes": {
"label": "Управление групповыми узлами"
"label": "Управление групповыми нодами"
},
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
"label": "Разгруппировать выбранные групповые узлы"
"label": "Разгруппировать выбранные групповые ноды"
},
"Comfy_Help_AboutComfyUI": {
"label": "Открыть о ComfyUI"
"label": "Открыть «О ComfyUI»"
},
"Comfy_Help_OpenComfyOrgDiscord": {
"label": "Открыть Comfy-Org Discord"
},
"Comfy_Help_OpenComfyUIDocs": {
"label": "Открыть документы ComfyUI"
"label": "Открыть документацию ComfyUI"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Открыть форум Comfy-Org"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "Открыть проблемы ComfyUI"
"label": "Открыть ComfyUI Issues"
},
"Comfy_Interrupt": {
"label": "Прервать"
@@ -135,7 +141,7 @@
"label": "Повторить"
},
"Comfy_RefreshNodeDefinitions": {
"label": "Обновить определения узлов"
"label": "Обновить определения нод"
},
"Comfy_SaveWorkflow": {
"label": "Сохранить рабочий процесс"
@@ -147,7 +153,7 @@
"label": "Показать диалог настроек"
},
"Comfy_ToggleTheme": {
"label": "Переключить тему (Темная/Светлая)"
"label": "Переключить тему (Тёмная/Светлая)"
},
"Comfy_Undo": {
"label": "Отменить"
@@ -171,7 +177,7 @@
"label": "Переключить нижнюю панель терминала"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Переключить нижнюю панель журналов"
"label": "Переключить нижнюю панель логов"
},
"Workspace_ToggleFocusMode": {
"label": "Переключить режим фокуса"
@@ -181,8 +187,8 @@
"tooltip": "Библиотека моделей"
},
"Workspace_ToggleSidebarTab_node-library": {
"label": "Переключить боковую панель библиотеки узлов",
"tooltip": "Библиотека узлов"
"label": "Переключить боковую панель библиотеки нод",
"tooltip": "Библиотека нод"
},
"Workspace_ToggleSidebarTab_queue": {
"label": "Переключить боковую панель очереди",

View File

@@ -3,10 +3,10 @@
"blue": "Синий",
"custom": "Пользовательский",
"default": "По умолчанию",
"green": "Зеленый",
"green": "Зелёный",
"pink": "Розовый",
"red": "Красный",
"yellow": елтый"
"yellow": ёлтый"
},
"dataTypes": {
"AUDIO": "АУДИО",
@@ -16,17 +16,17 @@
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
"COMBO": "КОМБО",
"CONDITIONING": "КОНДИЦИОНИРОВАНИЕ",
"CONTROL_NET": "СЕТЬ УПРАВЛЕНИЯ",
"CONTROL_NET": "CONTROL_NET",
"FLOAT": "ПЛАВАЮЩИЙ",
"FLOATS": "ПЛАВАЮЩИЕ",
"GLIGEN": "GLIGEN",
"GUIDER": "РУКОВОДИТЕЛЬ",
"HOOKS": "КРЮКИ",
"HOOK_KEYFRAMES": "КЛЮЧЕВЫЕ КАДРЫ КРЮКА",
"GUIDER": "ГИД",
"HOOKS": "ХУКИ",
"HOOK_KEYFRAMES": "КЛЮЧЕВЫЕ_КАДРЫ_ХУКА",
"IMAGE": "ИЗОБРАЖЕНИЕ",
"INT": "ЦЕЛОЕ",
"LATENT": "ЛАТЕНТНЫЙ",
"LATENT_OPERATION": "ЛАТЕНТНАЯ ОПЕРАЦИЯ",
"LATENT_OPERATION": "ЛАТЕНТНАЯ_ОПЕРАЦИЯ",
"LOAD_3D": "ЗАГРУЗИТЬ_3D",
"LOAD_3D_ANIMATION": "ЗАГРУЗИТЬ_3D_АНИМАЦИЮ",
"MASK": "МАСКА",
@@ -36,23 +36,25 @@
"SAMPLER": "СЭМПЛЕР",
"SIGMAS": "СИГМЫ",
"STRING": "СТРОКА",
"STYLE_MODEL": "МОДЕЛЬ СТИЛЯ",
"TIMESTEPS_RANGE": "ДИАПАЗОН ВРЕМЕННЫХ ШАГОВ",
"UPSCALE_MODEL": "МОДЕЛЬ УВЕЛИЧЕНИЯ",
"STYLE_MODEL": "МОДЕЛЬ_СТИЛЯ",
"TIMESTEPS_RANGE": "ДИАПАЗОН_ВРЕМЕННЫХ_ШАГОВ",
"UPSCALE_MODEL": "МОДЕЛЬ_АПСКЕЙЛА",
"VAE": "VAE",
"WEBCAM": "ВЕБ-КАМЕРА"
},
"desktopMenu": {
"confirmReinstall": "Это очистит ваш файл extra_models_config.yaml и начнет установку заново. Вы уверены?",
"confirmQuit": "Открыты несохраненные рабочие процессы; все несохраненные изменения будут потеряны. Проигнорировать это и выйти?",
"confirmReinstall": "Это очистит ваш файл extra_models_config.yaml и начнёт установку заново. Вы уверены?",
"quit": "Выйти",
"reinstall": "Переустановить"
},
"downloadGit": {
"gitWebsite": "Скачать git",
"instructions": "Пожалуйста, скачайте и установите последнюю версию для вашей операционной системы. Кнопка 'Скачать git' ниже открывает страницу загрузок git-scm.com.",
"instructions": "Пожалуйста, скачайте и установите последнюю версию для вашей операционной системы. Кнопка «Скачать git» ниже открывает страницу загрузок git-scm.com.",
"message": "Не удалось найти git. Рабочая копия git необходима для нормальной работы.",
"skip": "Пропустить",
"title": "Скачать git",
"warning": "Если вы уверены, что вам не нужно устанавливать git, или произошла ошибка, вы можете нажать 'Пропустить', чтобы обойти эту проверку. Попытка запустить ComfyUI без рабочей копии git в настоящее время не поддерживается."
"warning": "Если вы уверены, что вам не нужно устанавливать git, или произошла ошибка, вы можете нажать «Пропустить», чтобы обойти эту проверку. Попытка запустить ComfyUI без рабочей копии git в настоящее время не поддерживается."
},
"electronFileDownload": {
"cancel": "Отменить загрузку",
@@ -77,7 +79,7 @@
"customize": "Настроить",
"customizeFolder": "Настроить папку",
"delete": "Удалить",
"deprecated": "УСТАРЕЛО",
"deprecated": "Устарело",
"devices": "Устройства",
"disableAll": "Отключить все",
"download": "Скачать",
@@ -86,10 +88,11 @@
"error": "Ошибка",
"experimental": "БЕТА",
"export": "Экспорт",
"extensionName": "Имя расширения",
"extensionName": "Название расширения",
"feedback": "Обратная связь",
"findIssues": "Найти проблемы",
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
"goToNode": "Перейти к узлу",
"goToNode": "Перейти к ноде",
"icon": "Иконка",
"imageFailedToLoad": "Не удалось загрузить изображение",
"import": "Импорт",
@@ -99,7 +102,7 @@
"loadAllFolders": "Загрузить все папки",
"loadWorkflow": "Загрузить рабочий процесс",
"loading": "Загрузка",
"logs": "Журналы",
"logs": "Логи",
"newFolder": "Новая папка",
"next": "Далее",
"no": "Нет",
@@ -107,6 +110,7 @@
"noTasksFound": "Задачи не найдены",
"noTasksFoundMessage": "В очереди нет задач.",
"noWorkflowsFound": "Рабочие процессы не найдены.",
"ok": "ОК",
"openNewIssue": "Открыть новую проблему",
"overwrite": "Перезаписать",
"reconnected": "Переподключено",
@@ -114,21 +118,22 @@
"refresh": "Обновить",
"reloadToApplyChanges": "Перезагрузите, чтобы применить изменения",
"rename": "Переименовать",
"reportIssue": "Отправить отчет",
"reportIssueTooltip": "Отправить отчет об ошибке в Comfy Org",
"reportSent": "Отчет отправлен",
"reportIssue": "Отправить отчёт",
"reportIssueTooltip": "Отправить отчёт об ошибке в Comfy Org",
"reportSent": "Отчёт отправлен",
"reset": "Сбросить",
"resetKeybindingsTooltip": "Сбросить сочетания клавиш к умолчанию",
"resetKeybindingsTooltip": "Сбросить сочетания клавиш по умолчанию",
"save": "Сохранить",
"searchExtensions": "Поиск расширений",
"searchFailedMessage": "Мы не смогли найти настройки, соответствующие вашему запросу. Попробуйте изменить поисковые термины.",
"searchKeybindings": "Поиск сочетаний клавиш",
"searchModels": "Поиск моделей",
"searchNodes": "Поиск узлов",
"searchNodes": "Поиск нод",
"searchSettings": "Поиск настроек",
"searchWorkflows": "Поиск рабочих процессов",
"settings": "Настройки",
"showReport": "Показать отчет",
"showReport": "Показать отчёт",
"success": "Успех",
"systemInfo": "Информация о системе",
"terminal": "Терминал",
"upload": "Загрузить",
@@ -145,8 +150,8 @@
"zoomOut": "Уменьшить"
},
"groupNode": {
"create": "Создать узел группы",
"enterName": "Введите имя"
"create": "Создать ноду группы",
"enterName": "Введите название"
},
"icon": {
"bookmark": "Закладка",
@@ -164,17 +169,17 @@
"appPathLocationTooltip": "Директория активов приложения ComfyUI. Хранит код и активы ComfyUI",
"cannotWrite": "Невозможно записать в выбранный путь",
"chooseInstallationLocation": "Выберите место установки",
"customNodes": "Пользовательские узлы",
"customNodesDescription": "Переустановите пользовательские узлы из существующих установок ComfyUI.",
"desktopAppSettings": "Настройки настольного приложения",
"desktopAppSettingsDescription": "Настройте, как ComfyUI ведет себя на вашем рабочем столе. Вы можете изменить эти настройки позже.",
"customNodes": "Пользовательские ноды",
"customNodesDescription": "Переустановите пользовательские ноды из существующих установок ComfyUI.",
"desktopAppSettings": "Настройки десктопного приложения",
"desktopAppSettingsDescription": "Настройте, как ComfyUI ведёт себя на вашем рабочем столе. Вы можете изменить эти настройки позже.",
"desktopSettings": "Настройки рабочего стола",
"failedToSelectDirectory": "Не удалось выбрать директорию",
"gpu": "GPU",
"gpuSelection": {
"cpuMode": "Режим CPU",
"cpuModeDescription": "Режим CPU предназначен только для разработчиков и редких крайних случаев.",
"cpuModeDescription2": "Если вы не абсолютно уверены, что вам это нужно, пожалуйста, проигнорируйте эту галочку и выберите ваш GPU выше.",
"cpuModeDescription": "Режим CPU предназначен только для разработчиков и крайне редких случаев.",
"cpuModeDescription2": "Если вы не полностью уверены, что вам это нужно, пожалуйста, проигнорируйте эту галочку и выберите ваш GPU выше.",
"customComfyNeedsPython": "ComfyUI не будет работать, пока python не будет настроен",
"customInstallRequirements": "Установите все требования и зависимости (например, custom torch)",
"customManualVenv": "Вручную настроить python venv",
@@ -186,10 +191,11 @@
"selectGpu": "Выберите GPU",
"selectGpuDescription": "Выберите тип GPU, который у вас есть"
},
"helpImprove": "Пожалуйста, помогите улучшить ComfyUI",
"installLocation": "Место установки",
"installLocationDescription": "Выберите директорию для пользовательских данных ComfyUI. В выбранном месте будет установлена среда Python. Пожалуйста, убедитесь, что на выбранном диске достаточно места (~15 ГБ).",
"installLocationTooltip": "Директория пользовательских данных ComfyUI. Хранит:\n- Среда Python\n- Модели\n- Пользовательские узлы\n",
"insufficientFreeSpace": "Недостаточно места - минимально необходимое свободное место",
"installLocationTooltip": "Директория пользовательских данных ComfyUI. Хранит:\n- Среда Python\n- Модели\n- Пользовательские ноды\n",
"insufficientFreeSpace": "Недостаточно места минимально необходимое свободное место",
"manualConfiguration": {
"createVenv": "Вам потребуется создать виртуальное окружение в следующем каталоге",
"requirements": "Требования",
@@ -197,13 +203,17 @@
"title": "Ручная Конфигурация",
"virtualEnvironmentPath": "Путь виртуального окружения"
},
"metricsDisabled": "Метрики отключены",
"metricsEnabled": "Метрики включены",
"migrateFromExistingInstallation": "Миграция из существующей установки",
"migration": "Миграция",
"migrationOptional": "Миграция является необязательной. Если у вас нет существующей установки, вы можете пропустить этот шаг.",
"migrationSourcePathDescription": "Если у вас есть существующая установка ComfyUI, мы можем скопировать/связать ваши существующие пользовательские файлы и модели в новую установку.",
"parentMissing": "Путь не существует - сначала создайте родительский каталог",
"pathExists": "Директория уже существует - пожалуйста, убедитесь, что вы сделали резервное копирование всех данных",
"migrationSourcePathDescription": "Если у вас уже есть установленный ComfyUI, мы можем скопировать/связать ваши существующие пользовательские файлы и модели с новой установкой. Ваша существующая установка ComfyUI не будет затронута.",
"moreInfo": "Для получения дополнительной информации, пожалуйста, прочтите нашу",
"parentMissing": "Путь не существует — сначала создайте родительский каталог",
"pathExists": "Директория уже существует — пожалуйста, убедитесь, что вы сделали резервное копирование всех данных",
"pathValidationFailed": "Не удалось проверить путь",
"privacyPolicy": "политику конфиденциальности",
"selectItemsToMigrate": "Выберите элементы для миграции",
"settings": {
"allowMetrics": "Метрики использования",
@@ -214,33 +224,58 @@
"collect": {
"errorReports": "Сообщение об ошибке и трассировка стека",
"systemInfo": "Аппаратное обеспечение, тип ОС и версия приложения",
"userJourneyEvents": "События пользовательского пути",
"userJourneyTooltip": "События пользовательского пути используются для отслеживания пути пользователя в процессе установки приложения. Сбор событий заканчивается после первого успешного запуска рабочего процесса ComfyUI."
"userJourneyEvents": "События пользовательского пути"
},
"doNotCollect": {
"customNodeConfigurations": "Пользовательские конфигурации узлов",
"customNodeConfigurations": "Пользовательские конфигурации нод",
"fileSystemInformation": "Информация о файловой системе",
"personalInformation": "Личная информация",
"workflowContents": "Содержание рабочего процесса"
},
"title": "О сборе данных",
"viewFullPolicy": "Просмотреть политику полностью",
"whatWeCollect": "Что мы собираем:",
"whatWeDoNotCollect": "Что мы не собираем:"
},
"errorUpdatingConsent": "Ошибка обновления согласия",
"errorUpdatingConsentDetail": "Не удалось обновить настройки согласия на метрики",
"learnMoreAboutData": "Узнать больше о сборе данных"
},
"systemLocations": "Системные места",
"unhandledError": "Неизвестная ошибка"
"unhandledError": "Неизвестная ошибка",
"updateConsent": "Вы ранее согласились на отчётность об ошибках. Теперь мы отслеживаем метрики событий, чтобы помочь выявить ошибки и улучшить приложение. Личная идентифицируемая информация не собирается."
},
"issueReport": {
"contactFollowUp": "Свяжитесь со мной для уточнения",
"feedbackTitle": "Помогите нам улучшить ComfyUI, оставив отзыв",
"helpFix": "Помочь исправить это",
"notifyResolve": "Уведомить меня, когда проблема будет решена",
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
"provideEmail": "Укажите вашу электронную почту (необязательно)",
"rating": "Рейтинг",
"stackTrace": "Трассировка стека",
"submitErrorReport": "Отправить отчет об ошибке (необязательно)",
"systemStats": "Статистика системы"
"submitErrorReport": "Отправить отчёт об ошибке (необязательно)",
"systemStats": "Статистика системы",
"validation": {
"invalidEmail": "Пожалуйста, введите действительный адрес электронной почты",
"maxLength": "Сообщение слишком длинное"
}
},
"maintenance": {
"None": "Нет",
"OK": "OK",
"Skipped": "Пропущено",
"allOk": "Проблем не обнаружено.",
"confirmTitle": "Вы уверены?",
"detected": "Обнаружено",
"error": {
"defaultDescription": "Произошла ошибка при выполнении задачи по обслуживанию.",
"taskFailed": "Не удалось выполнить задачу.",
"toastTitle": "Ошибка задачи"
},
"refreshing": "Обновление",
"showManual": "Показать задачи по обслуживанию",
"status": "Статус"
},
"menu": {
"autoQueue": "Автоочередь",
@@ -260,7 +295,7 @@
"queue": "Выполнить",
"queueWorkflow": "Очередь рабочего процесса (Shift для вставки спереди)",
"queueWorkflowFront": "Очередь рабочего процесса (Вставка спереди)",
"refresh": "Обновить определения узлов",
"refresh": "Обновить определения нод",
"resetView": "Сбросить вид холста",
"showMenu": "Показать меню",
"toggleBottomPanel": "Переключить нижнюю панель"
@@ -268,36 +303,37 @@
"menuLabels": {
"About ComfyUI": "О ComfyUI",
"Browse Templates": "Просмотреть шаблоны",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные узлы",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
"Canvas Toggle Lock": "Переключение блокировки холста",
"Clear Pending Tasks": "Очистить ожидающие задачи",
"Clear Workflow": "Очистить рабочий процесс",
"Clipspace": "Клиппространство",
"Close Current Workflow": "Закрыть текущий рабочий процесс",
"Collapse/Expand Selected Nodes": "Свернуть/развернуть выбранные узлы",
"Collapse/Expand Selected Nodes": "Свернуть/развернуть выбранные ноды",
"Comfy-Org Discord": "Discord Comfy-Org",
"ComfyUI Docs": "Документация ComfyUI",
"ComfyUI Forum": "Форум ComfyUI",
"ComfyUI Issues": "Проблемы ComfyUI",
"Convert selected nodes to group node": "Преобразовать выбранные узлы в групповой узел",
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
"Desktop User Guide": "Руководство пользователя для настольных ПК",
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
"Edit": "Редактировать",
"Export": "Экспортировать",
"Export (API)": "Экспорт (API)",
"Feedback": "Обратная связь",
"Fit Group To Contents": "Подогнать группу под содержимое",
"Fit view to selected nodes": "Подогнать вид под выбранные узлы",
"Group Selected Nodes": "Сгруппировать выбранные узлы",
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
"Give Feedback": "Оставить отзыв",
"Group Selected Nodes": "Сгруппировать выбранные ноды",
"Help": "Помощь",
"Interrupt": "Прервать",
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
"Manage group nodes": "Управление групповыми узлами",
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных узлов",
"Manage group nodes": "Управление групповыми нодами",
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
"New": "Новый",
"Next Opened Workflow": "Следующий открытый рабочий процесс",
"Open": "Открыть",
"Open Custom Nodes Folder": "Открыть папку пользовательских узлов",
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
"Open DevTools": "Открыть инструменты разработчика",
"Open Inputs Folder": "Открыть папку входных данных",
"Open Logs Folder": "Открыть папку журналов",
@@ -305,12 +341,13 @@
"Open Outputs Folder": "Открыть папку выходных данных",
"Open extra_model_paths_yaml": "Открыть extra_model_paths.yaml",
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные узлы",
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
"Queue Prompt": "Запрос в очереди",
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
"Quit": "Выйти",
"Redo": "Повторить",
"Refresh Node Definitions": "Обновить определения узлов",
"Refresh Node Definitions": "Обновить определения нод",
"Reinstall": "Переустановить",
"Reset View": "Сбросить вид",
"Restart": "Перезапустить",
@@ -321,14 +358,14 @@
"Toggle Focus Mode": "Переключить режим фокуса",
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
"Toggle Model Library Sidebar": "Переключение боковой панели библиотеки моделей",
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки узлов",
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки нод",
"Toggle Queue Sidebar": "Переключение боковой панели очереди",
"Toggle Search Box": "Переключить поисковую панель",
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
"Toggle Theme (Dark/Light)": "Переключение темы (Темная/Светлая)",
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
"Toggle Workflows Sidebar": "Переключение боковой панели рабочих процессов",
"Undo": "Отменить",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые узлы",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
"Workflow": "Рабочий процесс",
"Zoom In": "Увеличить",
"Zoom Out": "Уменьшить"
@@ -344,24 +381,24 @@
"attention_experiments": "эксперименты_внимания",
"audio": "аудио",
"batch": "пакет",
"clip": "клип",
"clip": "clip",
"combine": "объединить",
"compositing": "композитинг",
"compositing": "композиционирование",
"cond pair": "условие_пара",
"cond single": "условие_одиночное",
"conditioning": "условие",
"controlnet": "контрольная_сеть",
"controlnet": "controlnet",
"create": "создать",
"custom_sampling": "пользовательская_выборка",
"custom_sampling": "пользовательский_семплинг",
"deprecated": "устаревший",
"flux": "flux",
"gligen": "глиген",
"gligen": "gligen",
"guidance": "направление",
"guiders": "направляющие",
"hooks": "хуки",
"image": "изображение",
"inpaint": "восстановление",
"instructpix2pix": "инструктпикс2пикс",
"instructpix2pix": "instructpix2pix",
"latent": "латентный",
"loaders": "загрузчики",
"ltxv": "ltxv",
@@ -377,26 +414,26 @@
"preprocessors": "предобработчики",
"samplers": "семплеры",
"sampling": "выборка",
"schedulers": "планировщики",
"scheduling": "планирование",
"schedulers": "schedulers",
"scheduling": "scheduling",
"sd3": "sd3",
"sigmas": "сигмы",
"stable_cascade": "стабильная_каскадная",
"style_model": одель_стиля",
"transform": "преобразование",
"unet": "унет",
"upscale_diffusion": "увеличение_диффузии",
"upscaling": "увеличение",
"unet": "unet",
"upscale_diffusion": "диффузии_апскейла",
"upscaling": "апскейл",
"video": "видео",
"video_models": "видеомодели"
},
"nodeTemplates": {
"enterName": "Введите имя",
"enterName": "Введите название",
"saveAsTemplate": "Сохранить как шаблон"
},
"notSupported": {
"continue": "Продолжить",
"continueTooltip": "Я уверен, что мое устройство поддерживается",
"continueTooltip": "Я уверен, что моё устройство поддерживается",
"learnMore": "Узнать больше",
"message": "Поддерживаются только следующие устройства:",
"reportIssue": "Сообщить о проблеме",
@@ -427,14 +464,14 @@
"name": "Использовать классическую систему кэширования"
},
"cache-lru": {
"name": "Использовать LRU кэширование с максимальным количеством N кэшированных результатов узлов.",
"name": "Использовать LRU кэширование с максимальным количеством N кэшированных результатов нод.",
"tooltip": "Может использовать больше ОЗУ/ВРП."
},
"cpu-vae": {
"name": "Запуск VAE на CPU"
},
"cross-attention-method": {
"name": "Метод перекрестного внимания"
"name": "Метод перекрёстного внимания"
},
"cuda-device": {
"name": "Индекс устройства CUDA для использования"
@@ -453,7 +490,7 @@
"name": "Индекс устройства DirectML"
},
"disable-all-custom-nodes": {
"name": "Отключить загрузку всех пользовательских узлов."
"name": "Отключить загрузку всех пользовательских нод."
},
"disable-ipex-optimize": {
"name": "Отключить оптимизацию IPEX"
@@ -469,7 +506,7 @@
"name": "Отключить оптимизацию xFormers"
},
"dont-print-server": {
"name": "Не выводить вывод сервера в консоль."
"name": "Не показывать вывод сервера в консоль."
},
"dont-upcast-attention": {
"name": "Предотвратить повышение внимания"
@@ -516,7 +553,7 @@
},
"reserve-vram": {
"name": "Резервируемая VRAM (ГБ)",
"tooltip": "Установите количество VRAM в ГБ, которое вы хотите зарезервировать для использования вашей ОС/другими программами. По умолчанию резервируется определенное количество в зависимости от вашей ОС."
"tooltip": "Установите количество VRAM в ГБ, которое вы хотите зарезервировать для использования вашей ОС/другими программами. По умолчанию резервируется определённое количество в зависимости от вашей ОС."
},
"text-encoder-precision": {
"name": "Точность текстового кодировщика",
@@ -544,10 +581,10 @@
"openLogs": "Открыть логи",
"process": {
"error": "Не удалось запустить ComfyUI Desktop",
"initial-state": "Загрузка...",
"python-setup": "Настройка окружения Python...",
"ready": "Завершение...",
"starting-server": "Запуск сервера ComfyUI..."
"initial-state": "Загрузка",
"python-setup": "Настройка окружения Python",
"ready": "Завершение",
"starting-server": "Запуск сервера ComfyUI"
},
"reinstall": "Переустановить",
"reportIssue": "Сообщить о проблеме",
@@ -560,7 +597,7 @@
"Canvas": "Холст",
"ColorPalette": "Цветовая палитра",
"Comfy": "Comfy",
"Comfy-Desktop": "Comfy рабочий стол",
"Comfy-Desktop": "Десктопный Comfy",
"CustomColorPalettes": "Пользовательские цветовые палитры",
"DevMode": "Режим разработчика",
"EditTokenWeight": "Редактировать вес токена",
@@ -577,11 +614,11 @@
"Menu": "Меню",
"ModelLibrary": "Библиотека моделей",
"NewEditor": "Новый редактор",
"Node": "Узел",
"Node Search Box": "Поисковая строка узлов",
"Node Widget": "Виджет узла",
"NodeInputConversionSubmenus": "Подменю преобразования ввода узла",
"NodeLibrary": "Библиотека узлов",
"Node": "Нода",
"Node Search Box": "Поисковая строка нод",
"Node Widget": "Виджет ноды",
"NodeInputConversionSubmenus": "Подменю преобразования ввода ноды",
"NodeLibrary": "Библиотека нод",
"Pointer": "Указатель",
"Queue": "Очередь",
"QueueButton": "Кнопка очереди",
@@ -601,7 +638,7 @@
"logout": "Выйти",
"modelLibrary": "Библиотека моделей",
"newBlankWorkflow": "Создайте новый пустой рабочий процесс",
"nodeLibrary": "Библиотека узлов",
"nodeLibrary": "Библиотека нод",
"nodeLibraryTab": {
"sortOrder": "Порядок сортировки"
},
@@ -627,7 +664,7 @@
"confirmOverwriteTitle": "Перезаписать существующий файл?",
"deleteFailed": "Попытка удалить рабочий процесс не удалась.",
"deleteFailedTitle": "Не удалось удалить",
"deleted": "Рабочий процесс удален",
"deleted": "Рабочий процесс удалён",
"dirtyClose": "Файлы ниже были изменены. Вы хотите сохранить их перед закрытием?",
"dirtyCloseTitle": "Сохранить изменения?",
"workflowTreeType": {
@@ -649,12 +686,12 @@
},
"templateWorkflows": {
"template": {
"default": "Image Generation",
"default": "Генерация изображений",
"flux_schnell": "Flux Schnell",
"image2image": "Image to Image",
"upscale": "2 Pass Upscale"
"image2image": "Изображение в изображение",
"upscale": "2-этапный апскейл"
},
"title": "Начните работу с шаблона"
"title": "Начните с шаблона"
},
"userSelect": {
"enterUsername": "Введите имя пользователя",
@@ -668,7 +705,7 @@
"title": "Добро пожаловать в ComfyUI"
},
"workflowService": {
"enterFilename": "Введите имя файла",
"enterFilename": "Введите название файла",
"exportWorkflow": "Экспорт рабочего процесса",
"saveWorkflow": "Сохранить рабочий процесс"
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,14 +20,14 @@
"name": "Включить обрезку элементов DOM (включение может снизить производительность)"
},
"Comfy_DevMode": {
"name": "Включить параметры режима разработчика (сохранение API и т.д.)"
"name": "Включить параметры режима разработчика (сохранение API и т. д.)"
},
"Comfy_DisableFloatRounding": {
"name": "Отключить округление по умолчанию для плавающих виджетов.",
"tooltip": "(требуется перезагрузка страницы) Невозможно отключить округление, если оно установлено узлом на сервере."
},
"Comfy_DisableSliders": {
"name": "Отключить ползунки виджетов узлов"
"name": "Отключить ползунки виджетов нод"
},
"Comfy_EditAttention_Delta": {
"name": "Точность Ctrl+вверх/вниз"
@@ -49,7 +49,7 @@
"name": "Показать меню холста графа"
},
"Comfy_Graph_CtrlShiftZoom": {
"name": "Включить быстрый зум с помощью сочетания клавиш (Ctrl + Shift + Перетаскивание)"
"name": "Включить быстрый зум с помощью сочетания клавиш (Ctrl + Shift + Колёсико мыши)"
},
"Comfy_Graph_LinkMarkers": {
"name": "Маркер середины ссылки",
@@ -63,10 +63,10 @@
"name": "Скорость зума холста"
},
"Comfy_GroupSelectedNodes_Padding": {
"name": "Отступ для выбранных узлов группы"
"name": "Отступ для выбранных нод группы"
},
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "Дважды щелкните по заголовку группы, чтобы редактировать"
"name": "Дважды щёлкните по заголовку группы, чтобы редактировать"
},
"Comfy_LinkRelease_Action": {
"name": "Действие при отпускании ссылки (без модификатора)",
@@ -98,11 +98,11 @@
},
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
"name": "Множитель скорости регулировки кисти",
"tooltip": "Управляет тем, как быстро изменяются размер и жесткость кисти при регулировке. Более высокие значения означают более быстрые изменения."
"tooltip": "Управляет тем, как быстро изменяются размер и жёсткость кисти при регулировке. Более высокие значения означают более быстрые изменения."
},
"Comfy_MaskEditor_UseDominantAxis": {
"name": "Закрепить регулировку кисти по доминирующей оси",
"tooltip": "При включении регулировки кисти будет влиять только на размер ИЛИ жесткость в зависимости от того, в каком направлении вы двигаетесь больше"
"tooltip": "При включении регулировки кисти будет влиять только на размер или жёсткость в зависимости от того, в каком направлении вы двигаетесь больше"
},
"Comfy_MaskEditor_UseNewEditor": {
"name": "Использовать новый редактор масок",
@@ -113,29 +113,29 @@
"tooltip": "Если true, все папки будут загружены, как только вы откроете библиотеку моделей (это может вызвать задержки при загрузке). Если false, корневые папки моделей будут загружены только после нажатия на них."
},
"Comfy_ModelLibrary_NameFormat": {
"name": "Какое имя отображать в древовидном представлении библиотеки моделей",
"name": "Какое название отображать в древовидном представлении библиотеки моделей",
"options": {
"filename": "имя файла",
"filename": "название файла",
"title": "название"
},
"tooltip": "Выберите \"имя файла\", чтобы отобразить упрощенный вид сырого имени файла (без директории или расширения \".safetensors\") в списке моделей. Выберите \"название\", чтобы отобразить настраиваемое название метаданных модели."
"tooltip": "Выберите \"название файла\", чтобы отобразить упрощённый вид сырого названия файла (без директории или расширения \".safetensors\") в списке моделей. Выберите \"название\", чтобы отобразить настраиваемое название метаданных модели."
},
"Comfy_NodeBadge_NodeIdBadgeMode": {
"name": "Режим значка ID узла",
"name": "Режим значка ID ноды",
"options": {
"None": "Нет",
"Show all": "Показать все"
}
},
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
"name": "Режим значка жизненного цикла узла",
"name": "Режим значка жизненного цикла ноды",
"options": {
"None": "Нет",
"Show all": "Показать все"
}
},
"Comfy_NodeBadge_NodeSourceBadgeMode": {
"name": "Режим значка источника узла",
"name": "Режим значка источника ноды",
"options": {
"Hide built-in": "Скрыть встроенные",
"None": "Нет",
@@ -143,63 +143,63 @@
}
},
"Comfy_NodeInputConversionSubmenus": {
"name": "В контекстном меню узла разместите элементы, которые конвертируют между вводом/виджетом в подменю."
"name": "В контекстном меню ноды разместите элементы, которые конвертируют между вводом/виджетом в подменю."
},
"Comfy_NodeSearchBoxImpl": {
"name": "Реализация поискового поля узлов",
"name": "Реализация поискового поля нод",
"options": {
"default": "по умолчанию",
"litegraph (legacy)": "litegraph (устаревший)"
}
},
"Comfy_NodeSearchBoxImpl_NodePreview": {
"name": "Предварительный просмотр узла",
"name": "Предварительный просмотр ноды",
"tooltip": "Применяется только к стандартной реализации"
},
"Comfy_NodeSearchBoxImpl_ShowCategory": {
"name": "Показать категорию узла в результатах поиска",
"name": "Показать категорию ноды в результатах поиска",
"tooltip": "Применяется только к стандартной реализации"
},
"Comfy_NodeSearchBoxImpl_ShowIdName": {
"name": "Показать имя ID узла в результатах поиска",
"name": "Показать название ID ноды в результатах поиска",
"tooltip": "Применяется только к стандартной реализации"
},
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
"name": "Показать частоту узла в результатах поиска",
"name": "Показать частоту ноды в результатах поиска",
"tooltip": "Применяется только к стандартной реализации"
},
"Comfy_NodeSuggestions_number": {
"name": "Количество предложений узлов",
"name": "Количество предложенных нод",
"tooltip": "Только для поля поиска litegraph/контекстного меню"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Автоматически привязывать ссылку к слоту узла",
"tooltip": "При перетаскивании ссылки над узлом ссылка автоматически привязывается к подходящему входному слоту узла"
"name": "Автоматически привязывать ссылку к слоту ноды",
"tooltip": "При перетаскивании ссылки над нодой ссылка автоматически привязывается к подходящему входному слоту ноды"
},
"Comfy_Node_BypassAllLinksOnDelete": {
"name": "Сохранить все ссылки при удалении узлов",
"tooltip": "При удалении узла попытаться переподключить все его входные и выходные ссылки (обходя удаленный узел)"
"name": "Сохранить все ссылки при удалении нод",
"tooltip": "При удалении ноды попытаться переподключить все её входные и выходные ссылки (обходя удалённую ноду)"
},
"Comfy_Node_DoubleClickTitleToEdit": {
"name": "Дважды щелкните по заголовку узла, чтобы редактировать"
"name": "Дважды щёлкните по заголовку ноды, чтобы редактировать"
},
"Comfy_Node_MiddleClickRerouteNode": {
"name": "Средний щелчок создает новый узел перенаправления"
"name": "Средний щелчок создаёт новую ноду перенаправления"
},
"Comfy_Node_Opacity": {
"name": "Непрозрачность узла"
"name": "Непрозрачность ноды"
},
"Comfy_Node_ShowDeprecated": {
"name": "Показать устаревшие узлы в поиске",
"tooltip": "Устаревшие узлы по умолчанию скрыты в интерфейсе, но остаются функциональными в существующих рабочих процессах, которые их используют."
"name": "Показать устаревшие ноды в поиске",
"tooltip": "Устаревшие ноды по умолчанию скрыты в интерфейсе, но остаются функциональными в существующих рабочих процессах, которые их используют."
},
"Comfy_Node_ShowExperimental": {
"name": "Показать экспериментальные узлы в поиске",
"tooltip": "Экспериментальные узлы помечены как таковые в интерфейсе и могут подвергаться значительным изменениям или удалению в будущих версиях. Используйте с осторожностью в производственных рабочих процессах"
"name": "Показать экспериментальные ноды в поиске",
"tooltip": "Экспериментальные ноды помечены как таковые в интерфейсе и могут подвергаться значительным изменениям или удалению в будущих версиях. Используйте с осторожностью в производственных рабочих процессах"
},
"Comfy_Node_SnapHighlightsNode": {
"name": "Подсветка узла при привязке",
"tooltip": "При перетаскивании ссылки над узлом с подходящим входным слотом, узел подсвечивается"
"name": "Подсветка ноды при привязке",
"tooltip": "При перетаскивании ссылки над нодой с подходящим входным слотом, нода подсвечивается"
},
"Comfy_Pointer_ClickBufferTime": {
"name": "Задержка дрейфа щелчка указателя",
@@ -218,7 +218,7 @@
"tooltip": "При отображении предварительного просмотра в виджете изображения, преобразуйте его в легковесное изображение, например, webp, jpeg, webp;50 и т.д."
},
"Comfy_PromptFilename": {
"name": "Запрос имени файла при сохранении рабочего процесса"
"name": "Запрос названия файла при сохранении рабочего процесса"
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "Ограничение количества партий",
@@ -230,7 +230,7 @@
},
"Comfy_RerouteBeta": {
"name": "Участвовать в бета-тестировании перенаправления",
"tooltip": "Включает новые нативные перенаправления.\n\nПеренаправления можно добавлять, удерживая alt и перетаскивая от линии ссылки или в меню ссылки.\n\nОтключение этой опции не разрушительно - перенаправления скрыты."
"tooltip": "Включает новые нативные перенаправления.\n\nПеренаправления можно добавлять, удерживая alt и перетаскивая от линии ссылки или в меню ссылки.\n\nОтключение этой опции не разрушительно перенаправления скрыты."
},
"Comfy_Sidebar_Location": {
"name": "Расположение боковой панели",
@@ -248,7 +248,7 @@
},
"Comfy_SnapToGrid_GridSize": {
"name": "Размер сетки привязки",
"tooltip": "При перетаскивании и изменении размера узлов, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
"tooltip": "При перетаскивании и изменении размера нод, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
},
"Comfy_TextareaWidget_FontSize": {
"name": "Размер шрифта виджета текстовой области"
@@ -268,8 +268,8 @@
}
},
"Comfy_Validation_NodeDefs": {
"name": "Проверка определений узлов (медленно)",
"tooltip": "Рекомендуется для разработчиков узлов. Это проверит все определения узлов при запуске."
"name": "Проверка определений нод (медленно)",
"tooltip": "Рекомендуется для разработчиков нод. Это проверит все определения нод при запуске."
},
"Comfy_Validation_Workflows": {
"name": "Проверка рабочих процессов"
@@ -292,10 +292,10 @@
"name": "Показать предупреждение об отсутствующих моделях"
},
"Comfy_Workflow_ShowMissingNodesWarning": {
"name": "Показать предупреждение об отсутствующих узлах"
"name": "Показать предупреждение об отсутствующих нодах"
},
"Comfy_Workflow_SortNodeIdOnSave": {
"name": "Сортировать ID узлов при сохранении рабочего процесса"
"name": "Сортировать ID нод при сохранении рабочего процесса"
},
"Comfy_Workflow_WorkflowTabsPosition": {
"name": "Положение открытых рабочих процессов",
@@ -307,7 +307,7 @@
},
"LiteGraph_Canvas_MaximumFps": {
"name": "Максимум FPS",
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счет плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счёт плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
},
"pysssss_SnapToGrid": {
"name": "Всегда привязываться к сетке"

View File

@@ -20,12 +20,12 @@
"Comfy-Desktop_OpenDevTools": {
"label": "打开开发者工具"
},
"Comfy-Desktop_OpenFeedbackPage": {
"label": "反馈"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "桌面用户指南"
},
"Comfy-Desktop_Quit": {
"label": "退出"
},
"Comfy-Desktop_Reinstall": {
"label": "重新安装"
},
@@ -83,6 +83,9 @@
"Comfy_ExportWorkflowAPI": {
"label": "导出工作流API格式"
},
"Comfy_Feedback": {
"label": "反馈"
},
"Comfy_Graph_FitGroupToContents": {
"label": "适应节点框到内容"
},
@@ -107,6 +110,9 @@
"Comfy_Help_OpenComfyUIDocs": {
"label": "打开ComfyUI文档"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "打开 Comfy-Org 论坛"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "打开ComfyUI问题"
},

View File

@@ -43,7 +43,9 @@
"WEBCAM": "摄像头"
},
"desktopMenu": {
"confirmQuit": "有未保存的工作流程开启;任何未保存的更改都将丢失。忽略此警告并退出?",
"confirmReinstall": "这将清除您的 extra_models_config.yaml 文件,并重新开始安装。您确定吗?",
"quit": "退出",
"reinstall": "重新安装"
},
"downloadGit": {
@@ -87,6 +89,7 @@
"experimental": "测试版",
"export": "导出",
"extensionName": "扩展名称",
"feedback": "反馈",
"findIssues": "查找问题",
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
"goToNode": "转到节点",
@@ -107,6 +110,7 @@
"noTasksFound": "未找到任务",
"noTasksFoundMessage": "队列中没有任务。",
"noWorkflowsFound": "未找到工作流。",
"ok": "确定",
"openNewIssue": "打开新问题",
"overwrite": "覆盖",
"reconnected": "已重新连接",
@@ -129,6 +133,7 @@
"searchWorkflows": "搜索工作流",
"settings": "设置",
"showReport": "显示报告",
"success": "成功",
"systemInfo": "系统信息",
"terminal": "终端",
"upload": "上传",
@@ -186,6 +191,7 @@
"selectGpu": "选择 GPU",
"selectGpuDescription": "选择你拥有的 GPU 类型"
},
"helpImprove": "请帮助我们改进ComfyUI",
"installLocation": "安装位置",
"installLocationDescription": "选择 ComfyUI 用户数据的存放目录。将安装一个 Python 环境到所选位置。请确保所选磁盘有足够的空间(约 15GB。",
"installLocationTooltip": "ComfyUI 的用户数据目录。存储:\n- Python 环境\n- 模型\n- 自定义节点\n",
@@ -197,13 +203,17 @@
"title": "手动配置",
"virtualEnvironmentPath": "虚拟环境路径"
},
"metricsDisabled": "禁用度量",
"metricsEnabled": "启用度量",
"migrateFromExistingInstallation": "从现有安装迁移",
"migration": "迁移",
"migrationOptional": "迁移是可选的。如果您之前没有安装过 ComfyUI可以跳过此步骤。",
"migrationSourcePathDescription": "如果您有可用的 ComfyUI我们可以将您的现有用户文件和模型复制/链接到新安装。",
"migrationSourcePathDescription": "如果您已有现有的ComfyUI安装,我们可以复制/链接您现有用户文件和模型到新安装。您现有的ComfyUI安装将不会受到影响。",
"moreInfo": "有关更多信息,请阅读我们的",
"parentMissing": "路径不存在 - 请先创建包含该路径的目录",
"pathExists": "目录已存在 - 请确保您已备份全部数据",
"pathValidationFailed": "路径验证失败",
"privacyPolicy": "隐私政策",
"selectItemsToMigrate": "选择要迁移的项目",
"settings": {
"allowMetrics": "使用情况指标",
@@ -214,8 +224,7 @@
"collect": {
"errorReports": "错误报告和堆栈跟踪",
"systemInfo": "硬件,操作系统类型和应用版本",
"userJourneyEvents": "用户旅程事件",
"userJourneyTooltip": "用户旅程事件用于跟踪用户通过应用安装过程的旅程。事件收集在第一次成功运行ComfyUI工作流后结束。"
"userJourneyEvents": "用户旅程事件"
},
"doNotCollect": {
"customNodeConfigurations": "自定义节点配置",
@@ -224,23 +233,49 @@
"workflowContents": "工作流内容"
},
"title": "关于数据收集",
"viewFullPolicy": "查看完整政策",
"whatWeCollect": "我们收集的内容:",
"whatWeDoNotCollect": "我们不收集的内容:"
},
"errorUpdatingConsent": "更新同意错误",
"errorUpdatingConsentDetail": "无法更新度量同意设置",
"learnMoreAboutData": "了解更多关于数据收集的信息"
},
"systemLocations": "系统位置",
"unhandledError": "未知错误"
"unhandledError": "未知错误",
"updateConsent": "您之前选择了报告崩溃。我们现在正在跟踪基于事件的度量,以帮助识别错误并改进应用程序。我们不收集任何个人可识别信息。"
},
"issueReport": {
"contactFollowUp": "跟进联系我",
"feedbackTitle": "通过提供反馈帮助我们改进ComfyUI",
"helpFix": "帮助修复这个",
"notifyResolve": "解决时通知我",
"provideAdditionalDetails": "提供额外的详细信息(可选)",
"provideEmail": "提供您的电子邮件(可选)",
"rating": "评分",
"stackTrace": "堆栈跟踪",
"submitErrorReport": "提交错误报告(可选)",
"systemStats": "系统状态"
"systemStats": "系统状态",
"validation": {
"invalidEmail": "请输入有效的电子邮件地址",
"maxLength": "消息过长"
}
},
"maintenance": {
"None": "无",
"OK": "确定",
"Skipped": "跳过",
"allOk": "未检测到任何问题。",
"confirmTitle": "你确定吗?",
"detected": "检测到",
"error": {
"defaultDescription": "运行维护任务时发生错误。",
"taskFailed": "任务运行失败。",
"toastTitle": "任务错误"
},
"refreshing": "刷新中",
"showManual": "显示维护任务",
"status": "状态"
},
"menu": {
"autoQueue": "自动执行",
@@ -278,6 +313,7 @@
"Collapse/Expand Selected Nodes": "折叠/展开选定节点",
"Comfy-Org Discord": "Comfy-Org Discord",
"ComfyUI Docs": "ComfyUI 文档",
"ComfyUI Forum": "ComfyUI 论坛",
"ComfyUI Issues": "ComfyUI 问题",
"Convert selected nodes to group node": "将选中节点转换为组节点",
"Desktop User Guide": "桌面端用户指南",
@@ -285,9 +321,9 @@
"Edit": "编辑",
"Export": "导出",
"Export (API)": "导出 (API)",
"Feedback": "反馈",
"Fit Group To Contents": "适应组内容",
"Fit view to selected nodes": "适应视图到选中节点",
"Give Feedback": "提供反馈",
"Group Selected Nodes": "将选中节点转换为组节点",
"Help": "帮助",
"Interrupt": "中断",
@@ -309,6 +345,7 @@
"Previous Opened Workflow": "上一个打开的工作流",
"Queue Prompt": "执行提示词",
"Queue Prompt (Front)": "执行提示词 (优先执行)",
"Quit": "退出",
"Redo": "重做",
"Refresh Node Definitions": "刷新节点定义",
"Reinstall": "重新安装",

View File

@@ -113,7 +113,7 @@
}
},
"CLIPLoader": {
"description": "[配方]\n\nStable Diffusionclip-l\nStable Cascadeclip-g\nSD3t5 / clip-g / clip-l\nStable Audiot5\nMochit5",
"description": "[配方]\n\nStable Diffusionclip-l\nStable Cascadeclip-g\nSD3t5 / clip-g / clip-l\nStable Audiot5\nMochit5\ncosmosold t5 xxl",
"display_name": "加载CLIP",
"inputs": {
"clip_name": {
@@ -862,6 +862,32 @@
}
}
},
"CosmosImageToVideoLatent": {
"display_name": "Cosmos图像到视频潜在",
"inputs": {
"batch_size": {
"name": "批量大小"
},
"end_image": {
"name": "结束图像"
},
"height": {
"name": "高度"
},
"length": {
"name": "长度"
},
"start_image": {
"name": "开始图像"
},
"vae": {
"name": "vae"
},
"width": {
"name": "宽度"
}
}
},
"CreateHookKeyframe": {
"display_name": "创建约束关键帧",
"inputs": {
@@ -1230,6 +1256,23 @@
}
}
},
"EmptyCosmosLatentVideo": {
"display_name": "空的Cosmos潜在视频",
"inputs": {
"batch_size": {
"name": "批量大小"
},
"height": {
"name": "高度"
},
"length": {
"name": "长度"
},
"width": {
"name": "宽度"
}
}
},
"EmptyHunyuanLatentVideo": {
"display_name": "空Latent视频混元",
"inputs": {
@@ -4825,6 +4868,17 @@
}
}
},
"SetFirstSigma": {
"display_name": "设置第一个Sigma",
"inputs": {
"sigma": {
"name": "sigma"
},
"sigmas": {
"name": "sigmas"
}
}
},
"SetHookKeyframes": {
"display_name": "设置约束关键帧",
"inputs": {

View File

@@ -92,6 +92,24 @@ const router = createRouter({
name: 'ManualConfigurationView',
component: () => import('@/views/ManualConfigurationView.vue'),
beforeEnter: guardElectronAccess
},
{
path: '/metrics-consent',
name: 'MetricsConsentView',
component: () => import('@/views/MetricsConsentView.vue'),
beforeEnter: guardElectronAccess
},
{
path: 'desktop-start',
name: 'DesktopStartView',
component: () => import('@/views/DesktopStartView.vue'),
beforeEnter: guardElectronAccess
},
{
path: 'maintenance',
name: 'MaintenanceView',
component: () => import('@/views/MaintenanceView.vue'),
beforeEnter: guardElectronAccess
}
]
}

View File

@@ -942,7 +942,7 @@ export class ComfyApp {
api.addEventListener('execution_error', ({ detail }) => {
this.lastExecutionError = detail
useDialogService().showExecutionErrorDialog(detail)
useDialogService().showExecutionErrorDialog({ error: detail })
this.canvas.draw(true, true)
})

View File

@@ -272,7 +272,7 @@ LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
w.element.hidden = actualHidden
w.element.style.display = actualHidden ? 'none' : null
if (actualHidden && !wasHidden) {
w.options.onHide?.(w)
w.options.onHide?.(w as DOMWidget<HTMLElement, object>)
}
}
}
@@ -417,6 +417,12 @@ LGraphNode.prototype.addDOMWidget = function <
element.dataset.collapsed = this.flags?.collapsed ? 'true' : 'false'
}
const { onConfigure } = this
this.onConfigure = function () {
onConfigure?.apply(this, arguments)
element.dataset.collapsed = this.flags?.collapsed ? 'true' : 'false'
}
const onRemoved = this.onRemoved
this.onRemoved = function () {
element.remove()

View File

@@ -1,5 +1,6 @@
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
@@ -8,8 +9,6 @@ import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
import { t } from '@/i18n'
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
import type { ExecutionErrorWsMessage } from '@/types/apiTypes'
import type { MissingNodeType } from '@/types/comfy'
export type ConfirmationDialogType =
| 'default'
@@ -20,10 +19,9 @@ export type ConfirmationDialogType =
export const useDialogService = () => {
const dialogStore = useDialogStore()
function showLoadWorkflowWarning(props: {
missingNodeTypes: MissingNodeType[]
[key: string]: any
}) {
function showLoadWorkflowWarning(
props: InstanceType<typeof LoadWorkflowWarning>['$props']
) {
dialogStore.showDialog({
key: 'global-load-workflow-warning',
component: LoadWorkflowWarning,
@@ -31,11 +29,9 @@ export const useDialogService = () => {
})
}
function showMissingModelsWarning(props: {
missingModels: any[]
paths: Record<string, string[]>
[key: string]: any
}) {
function showMissingModelsWarning(
props: InstanceType<typeof MissingModelsWarning>['$props']
) {
dialogStore.showDialog({
key: 'global-missing-models-warning',
component: MissingModelsWarning,
@@ -67,21 +63,34 @@ export const useDialogService = () => {
})
}
function showExecutionErrorDialog(error: ExecutionErrorWsMessage) {
function showExecutionErrorDialog(
props: InstanceType<typeof ExecutionErrorDialogContent>['$props']
) {
dialogStore.showDialog({
key: 'global-execution-error',
component: ExecutionErrorDialogContent,
props: {
error
}
props
})
}
function showTemplateWorkflowsDialog() {
function showTemplateWorkflowsDialog(
props: InstanceType<typeof TemplateWorkflowsContent>['$props'] = {}
) {
dialogStore.showDialog({
key: 'global-template-workflows',
title: t('templateWorkflows.title'),
component: TemplateWorkflowsContent
component: TemplateWorkflowsContent,
props
})
}
function showIssueReportDialog(
props: InstanceType<typeof IssueReportDialogContent>['$props']
) {
dialogStore.showDialog({
key: 'global-issue-report',
component: IssueReportDialogContent,
props
})
}
@@ -162,6 +171,7 @@ export const useDialogService = () => {
showAboutDialog,
showExecutionErrorDialog,
showTemplateWorkflowsDialog,
showIssueReportDialog,
prompt,
confirm
}

View File

@@ -367,7 +367,6 @@ export const useLitegraphService = () => {
const w = node.widgets[node.widgets.length - 1]
shiftY = w.last_y
if (w.computeSize) {
// @ts-expect-error requires 1 param
shiftY += w.computeSize()[1] + 4
// @ts-expect-error computedHeight only exists for DOMWidget
} else if (w.computedHeight) {

View File

@@ -0,0 +1,175 @@
import type { InstallValidation } from '@comfyorg/comfyui-electron-types'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { DESKTOP_MAINTENANCE_TASKS } from '@/constants/desktopMaintenanceTasks'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
/** State of a maintenance task, managed by the maintenance task store. */
export type MaintenanceTaskState = 'warning' | 'error' | 'OK' | 'skipped'
// Type not exported by API
type ValidationState = InstallValidation['basePath']
// Add index to API type
type IndexedUpdate = InstallValidation & Record<string, ValidationState>
/** State of a maintenance task, managed by the maintenance task store. */
export class MaintenanceTaskRunner {
constructor(readonly task: MaintenanceTask) {}
private _state?: MaintenanceTaskState
/** The current state of the task. Setter also controls {@link resolved} as a side-effect. */
get state() {
return this._state
}
/** Updates the task state and {@link resolved} status. */
setState(value: MaintenanceTaskState) {
// Mark resolved
if (this._state === 'error' && value === 'OK') this.resolved = true
// Mark unresolved (if previously resolved)
if (value === 'error') this.resolved &&= false
this._state = value
}
/** `true` if the task has been resolved (was `error`, now `OK`). This is a side-effect of the {@link state} setter. */
resolved?: boolean
/** Whether the task state is currently being refreshed. */
refreshing?: boolean
/** Whether the task is currently running. */
executing?: boolean
/** The error message that occurred when the task failed. */
error?: string
update(update: IndexedUpdate) {
const state = update[this.task.id]
this.refreshing = state === undefined
if (state) this.setState(state)
}
finaliseUpdate(update: IndexedUpdate) {
this.refreshing = false
this.setState(update[this.task.id] ?? 'skipped')
}
/** Wraps the execution of a maintenance task, updating state and rethrowing errors. */
async execute(task: MaintenanceTask) {
try {
this.executing = true
const success = await task.execute()
if (!success) return false
this.error = undefined
return true
} catch (error) {
this.error = (error as Error)?.message
throw error
} finally {
this.executing = false
}
}
}
/**
* User-initiated maintenance tasks. Currently only used by the desktop app maintenance view.
*
* Includes running state, task list, and execution / refresh logic.
* @returns The maintenance task store
*/
export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
/** Refresh should run for at least this long, even if it completes much faster. Ensures refresh feels like it is doing something. */
const electron = electronAPI()
// Reactive state
const isRefreshing = ref(false)
const isRunningTerminalCommand = computed(() =>
tasks.value
.filter((task) => task.usesTerminal)
.some((task) => getRunner(task)?.executing)
)
const isRunningInstallationFix = computed(() =>
tasks.value
.filter((task) => task.isInstallationFix)
.some((task) => getRunner(task)?.executing)
)
// Task list
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
const taskStates = ref(
new Map<MaintenanceTask['id'], MaintenanceTaskRunner>(
DESKTOP_MAINTENANCE_TASKS.map((x) => [x.id, new MaintenanceTaskRunner(x)])
)
)
/** True if any tasks are in an error state. */
const anyErrors = computed(() =>
tasks.value.some((task) => getRunner(task).state === 'error')
)
/**
* Returns the matching state object for a task.
* @param task Task to get the matching state object for
* @returns The state object for this task
*/
const getRunner = (task: MaintenanceTask) => taskStates.value.get(task.id)!
/**
* Updates the task list with the latest validation state.
* @param validationUpdate Update details passed in by electron
*/
const processUpdate = (validationUpdate: InstallValidation) => {
const update = validationUpdate as IndexedUpdate
isRefreshing.value = true
// Update each task state
for (const task of tasks.value) {
getRunner(task).update(update)
}
// Final update
if (!update.inProgress && isRefreshing.value) {
isRefreshing.value = false
for (const task of tasks.value) {
getRunner(task).finaliseUpdate(update)
}
}
}
/** Clears the resolved status of tasks (when changing filters) */
const clearResolved = () => {
for (const task of tasks.value) {
getRunner(task).resolved &&= false
}
}
/** @todo Refreshes Electron tasks only. */
const refreshDesktopTasks = async () => {
isRefreshing.value = true
console.log('Refreshing desktop tasks')
await electron.Validation.validateInstallation(processUpdate)
}
const execute = async (task: MaintenanceTask) => {
return getRunner(task).execute(task)
}
return {
tasks,
isRefreshing,
isRunningTerminalCommand,
isRunningInstallationFix,
execute,
getRunner,
processUpdate,
clearResolved,
/** True if any tasks are in an error state. */
anyErrors,
refreshDesktopTasks
}
})

0
src/types/desktop/index.d.ts vendored Normal file
View File

View File

@@ -0,0 +1,50 @@
import type { VueSeverity } from '../primeVueTypes'
interface MaintenanceTaskButton {
/** The text to display on the button. */
text?: string
/** CSS classes used for the button icon, e.g. 'pi pi-external-link' */
icon?: string
}
/** A maintenance task, used by the maintenance page. */
export interface MaintenanceTask {
/** ID string used as i18n key */
id: string
/** The display name of the task, e.g. Git */
name: string
/** Short description of the task. */
shortDescription?: string
/** Description of the task when it is in an error state. */
errorDescription?: string
/** Description of the task when it is in a warning state. */
warningDescription?: string
/** Full description of the task when it is in an OK state. */
description?: string
/** URL to the image to show in card mode. */
headerImg?: string
/** The button to display on the task card / list item. */
button?: MaintenanceTaskButton
/** Whether to show a confirmation dialog before running the task. */
requireConfirm?: boolean
/** The text to display in the confirmation dialog. */
confirmText?: string
/** Called by onClick to run the actual task. */
execute: (args?: unknown[]) => boolean | Promise<boolean>
/** Show the button with `severity="danger"` */
severity?: VueSeverity
/** Whether this task should display the terminal window when run. */
usesTerminal?: boolean
/** If `true`, successful completion of this task will refresh install validation and automatically continue if successful. */
isInstallationFix?: boolean
}
/** The filter options for the maintenance task list. */
export interface MaintenanceFilter {
/** CSS classes used for the filter button icon, e.g. 'pi pi-cross' */
icon: string
/** The text to display on the filter button. */
value: string
/** The tasks to display when this filter is selected. */
tasks: ReadonlyArray<MaintenanceTask>
}

View File

@@ -1,3 +1,5 @@
import { z } from 'zod'
export type DefaultField = 'Workflow' | 'Logs' | 'SystemStats' | 'Settings'
export interface ReportField {
@@ -14,7 +16,7 @@ export interface ReportField {
/**
* The data associated with this field, sent as part of the report.
*/
data: Record<string, unknown>
getData: () => unknown
/**
* Indicates whether the field requires explicit opt-in from the user
@@ -22,3 +24,42 @@ export interface ReportField {
*/
optIn: boolean
}
export interface IssueReportPanelProps {
/**
* The type of error being reported. This is used to categorize the error.
*/
errorType: string
/**
* Which of the default fields to include in the report.
*/
defaultFields?: DefaultField[]
/**
* Additional fields to include in the report.
*/
extraFields?: ReportField[]
/**
* Tags that will be added to the report. Tags are used to further categorize the error.
*/
tags?: Record<string, string>
/**
* The title displayed in the dialog.
*/
title?: string
}
const checkboxField = z.boolean().optional()
export const issueReportSchema = z
.object({
contactInfo: z.string().email().max(320).optional().or(z.literal('')),
details: z.string().max(5_000).optional()
})
.catchall(checkboxField)
.refine((data) => Object.values(data).some((value) => value), {
path: ['details']
})
export type IssueReportFormData = z.infer<typeof issueReportSchema>

View File

@@ -1,10 +1,24 @@
import '@comfyorg/litegraph'
import type { LLink } from '@comfyorg/litegraph'
import type { DOMWidget } from '@/scripts/domWidget'
import type { ComfyNodeDef } from '@/types/apiTypes'
import type { NodeId } from './comfyWorkflow'
/** ComfyUI extensions of litegraph */
declare module '@comfyorg/litegraph/dist/types/widgets' {
interface IWidgetOptions {
/** Currently used by DOM widgets only. Declaring here reduces complexity. */
onHide?: (widget: DOMWidget) => void
}
interface IBaseWidget {
onRemove?: () => void
beforeQueued?: () => unknown
}
}
/**
* ComfyUI extensions of litegraph
*/

View File

@@ -0,0 +1,10 @@
/** Button, Tag, etc severity type is 'string' instead of this list. */
export type VueSeverity =
| 'primary'
| 'secondary'
| 'success'
| 'info'
| 'warn'
| 'help'
| 'danger'
| 'contrast'

View File

@@ -11,6 +11,6 @@ export function electronAPI() {
return (window as any).electronAPI as ElectronAPI
}
export function showNativeMenu(options?: ElectronContextMenuOptions) {
electronAPI()?.showContextMenu(options)
export function showNativeMenu(event: MouseEvent) {
electronAPI()?.showContextMenu(event as ElectronContextMenuOptions)
}

29
src/utils/refUtil.ts Normal file
View File

@@ -0,0 +1,29 @@
import { useTimeout } from '@vueuse/core'
import { type Ref, computed, ref, watch } from 'vue'
/**
* Vue boolean ref (writable computed) with one difference: when set to `true` it stays that way for at least {@link minDuration}.
* If set to `false` before {@link minDuration} has passed, it uses a timer to delay the change.
* @param value The default value to set on this ref
* @param minDuration The minimum time that this ref must be `true` for
* @returns A custom boolean vue ref with a minimum activation time
*/
export function useMinLoadingDurationRef(
value: Ref<boolean>,
minDuration = 250
) {
const current = ref(value.value)
const { ready, start } = useTimeout(minDuration, {
controls: true,
immediate: false
})
watch(value, (newValue) => {
if (newValue && !current.value) start()
current.value = newValue
})
return computed(() => current.value || !ready.value)
}

View File

@@ -0,0 +1,13 @@
<template>
<BaseViewTemplate dark>
<div class="max-w-screen-sm w-screen p-8">
<ProgressBar mode="indeterminate" />
</div>
</BaseViewTemplate>
</template>
<script setup lang="ts">
import ProgressBar from 'primevue/progressbar'
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
</script>

View File

@@ -55,6 +55,7 @@ const toast = useToast()
const settingStore = useSettingStore()
const executionStore = useExecutionStore()
const colorPaletteStore = useColorPaletteStore()
const queueStore = useQueueStore()
watch(
() => colorPaletteStore.completedActivePalette,
@@ -76,6 +77,32 @@ watch(
{ immediate: true }
)
if (isElectron()) {
watch(
() => queueStore.tasks,
(newTasks, oldTasks) => {
// Report tasks that previously running but are now completed (i.e. in history)
const oldRunningTaskIds = new Set(
oldTasks.filter((task) => task.isRunning).map((task) => task.promptId)
)
newTasks
.filter(
(task) => oldRunningTaskIds.has(task.promptId) && task.isHistory
)
.forEach((task) => {
electronAPI().Events.incrementUserProperty(
`execution:${task.displayStatus.toLowerCase()}`,
1
)
electronAPI().Events.trackEvent('execution', {
status: task.displayStatus.toLowerCase()
})
})
},
{ deep: true }
)
}
watchEffect(() => {
const fontSize = settingStore.get('Comfy.TextareaWidget.FontSize')
document.documentElement.style.setProperty(
@@ -110,9 +137,7 @@ watchEffect(() => {
})
watchEffect(() => {
useQueueStore().maxHistoryItems = settingStore.get(
'Comfy.Queue.MaxHistoryItems'
)
queueStore.maxHistoryItems = settingStore.get('Comfy.Queue.MaxHistoryItems')
})
const init = () => {
@@ -126,8 +151,9 @@ const init = () => {
}
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
const onStatus = (e: CustomEvent<StatusWsMessageStatus>) => {
const onStatus = async (e: CustomEvent<StatusWsMessageStatus>) => {
queuePendingTaskCountStore.update(e)
await queueStore.update()
}
const reconnectingMessage: ToastMessageOptions = {

View File

@@ -6,7 +6,7 @@
<Stepper
class="h-full p-8 2xl:p-16"
value="0"
@update:value="setHighestStep"
@update:value="handleStepChange"
>
<StepList class="select-none">
<Step value="0">
@@ -137,6 +137,14 @@ const allowMetrics = ref(true)
/** Forces each install step to be visited at least once. */
const highestStep = ref(0)
const handleStepChange = (value: string | number) => {
setHighestStep(value)
electronAPI().Events.trackEvent('install_stepper_change', {
step: value
})
}
const setHighestStep = (value: string | number) => {
const int = typeof value === 'number' ? value : parseInt(value, 10)
if (!isNaN(int) && int > highestStep.value) highestStep.value = int
@@ -167,12 +175,18 @@ onMounted(async () => {
if (!electron) return
const detectedGpu = await electron.Config.getDetectedGpu()
if (detectedGpu === 'mps' || detectedGpu === 'nvidia')
if (detectedGpu === 'mps' || detectedGpu === 'nvidia') {
device.value = detectedGpu
}
electronAPI().Events.trackEvent('install_stepper_change', {
step: '0',
gpu: detectedGpu
})
})
</script>
<style lang="postcss" scoped>
<style scoped>
:deep(.p-steppanel) {
@apply bg-transparent;
}

View File

@@ -0,0 +1,221 @@
<template>
<BaseViewTemplate dark>
<div
class="min-w-full min-h-full font-sans w-screen h-screen grid justify-around text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto overflow-y-auto"
>
<div class="max-w-screen-sm w-screen m-8 relative">
<!-- Header -->
<h1 class="backspan pi-wrench text-4xl font-bold">Maintenance</h1>
<!-- Toolbar -->
<div class="w-full flex flex-wrap gap-4 items-center">
<span class="grow">
Status: <StatusTag :refreshing="isRefreshing" :error="anyErrors" />
</span>
<div class="flex gap-4 items-center">
<SelectButton
v-model="displayAsList"
:options="[PrimeIcons.LIST, PrimeIcons.TH_LARGE]"
:allow-empty="false"
>
<template #option="opts"><i :class="opts.option" /></template>
</SelectButton>
<SelectButton
v-model="filter"
:options="filterOptions"
:allow-empty="false"
optionLabel="value"
dataKey="value"
area-labelledby="custom"
@change="clearResolved"
>
<template #option="opts">
<i :class="opts.option.icon"></i>
<span class="max-sm:hidden">{{ opts.option.value }}</span>
</template>
</SelectButton>
<RefreshButton
v-model="isRefreshing"
severity="secondary"
@refresh="refreshDesktopTasks"
/>
</div>
</div>
<!-- Tasks -->
<TaskListPanel
class="border-neutral-700 border-solid border-x-0 border-y"
:filter
:displayAsList
:isRefreshing
/>
<!-- Actions -->
<div class="flex justify-between gap-4 flex-row">
<Button
label="Console Logs"
icon="pi pi-desktop"
icon-pos="left"
severity="secondary"
@click="toggleConsoleDrawer"
/>
<Button
label="Continue"
icon="pi pi-arrow-right"
icon-pos="left"
:severity="anyErrors ? 'secondary' : 'primary'"
@click="() => completeValidation()"
:loading="isRefreshing"
/>
</div>
</div>
<Drawer
v-model:visible="terminalVisible"
header="Terminal"
position="bottom"
style="height: max(50vh, 34rem)"
>
<BaseTerminal @created="terminalCreated" />
</Drawer>
<Toast />
</div>
</BaseViewTemplate>
</template>
<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api'
import Button from 'primevue/button'
import Drawer from 'primevue/drawer'
import SelectButton from 'primevue/selectbutton'
import Toast from 'primevue/toast'
import { useToast } from 'primevue/usetoast'
import { Ref, computed, onMounted, onUnmounted, ref } from 'vue'
import { watch } from 'vue'
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
import RefreshButton from '@/components/common/RefreshButton.vue'
import StatusTag from '@/components/maintenance/StatusTag.vue'
import TaskListPanel from '@/components/maintenance/TaskListPanel.vue'
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
const electron = electronAPI()
const toast = useToast()
const taskStore = useMaintenanceTaskStore()
const { clearResolved, processUpdate, refreshDesktopTasks } = taskStore
const terminalVisible = ref(false)
// Use a minimum run time to ensure tasks "feel" like they have run
const reactiveIsRefreshing = computed(() => taskStore.isRefreshing)
/** `true` when waiting on tasks to complete. */
const isRefreshing = useMinLoadingDurationRef(reactiveIsRefreshing, 250)
/** True if any tasks are in an error state. */
const anyErrors = computed(() => taskStore.anyErrors)
/** Whether to display tasks as a list or cards. */
const displayAsList = ref(PrimeIcons.TH_LARGE)
const errorFilter = computed(() =>
taskStore.tasks.filter((x) => {
const { state, resolved } = taskStore.getRunner(x)
return state === 'error' || resolved
})
)
const filterOptions = ref([
{ icon: PrimeIcons.FILTER_FILL, value: 'All', tasks: taskStore.tasks },
{ icon: PrimeIcons.EXCLAMATION_TRIANGLE, value: 'Errors', tasks: errorFilter }
])
/** Filter binding; can be set to show all tasks, or only errors. */
const filter = ref<MaintenanceFilter>(filterOptions.value[1])
/** If valid, leave the validation window. */
const completeValidation = async (alertOnFail = true) => {
const isValid = await electron.Validation.complete()
if (alertOnFail && !isValid) {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Unable to continue - errors remain',
life: 5_000
})
}
}
const terminalCreated = (
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
root: Ref<HTMLElement>
) => {
useAutoSize({ root, autoRows: true, autoCols: true })
electron.onLogMessage((message: string) => {
terminal.write(message)
})
terminal.options.cursorBlink = false
terminal.options.cursorStyle = 'bar'
terminal.options.cursorInactiveStyle = 'bar'
terminal.options.disableStdin = true
}
const toggleConsoleDrawer = () => {
terminalVisible.value = !terminalVisible.value
}
// Show terminal when in use
watch(
() => taskStore.isRunningTerminalCommand,
(value) => {
terminalVisible.value = value
}
)
// If we're running a fix that may resolve all issues, auto-recheck and continue if everything is OK
watch(
() => taskStore.isRunningInstallationFix,
(value, oldValue) => {
if (!value && oldValue) completeValidation(false)
}
)
onMounted(async () => {
electron.Validation.onUpdate(processUpdate)
const update = await electron.Validation.getStatus()
processUpdate(update)
})
onUnmounted(() => electron.Validation.dispose())
</script>
<style scoped>
:deep(.p-tag) {
--p-tag-gap: 0.375rem;
}
.backspan::before {
@apply m-0 absolute text-muted;
font-family: 'primeicons';
top: -2rem;
right: -2rem;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
display: inline-block;
-webkit-font-smoothing: antialiased;
opacity: 0.02;
font-size: min(14rem, 90vw);
z-index: 0;
}
</style>

View File

@@ -75,8 +75,8 @@ onMounted(async () => {
})
</script>
<style>
:root {
<style scoped>
.p-tag {
--p-tag-gap: 0.5rem;
}

View File

@@ -0,0 +1,83 @@
<template>
<BaseViewTemplate dark>
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
<div
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
>
<h2 class="text-3xl font-semibold text-neutral-100">
{{ $t('install.helpImprove') }}
</h2>
<p class="text-neutral-400">
{{ $t('install.updateConsent') }}
</p>
<p class="text-neutral-400">
{{ $t('install.moreInfo') }}
<a
href="https://comfy.org/privacy"
target="_blank"
class="text-blue-400 hover:text-blue-300 underline"
>
{{ $t('install.privacyPolicy') }} </a
>.
</p>
<div class="flex items-center gap-4">
<ToggleSwitch
v-model="allowMetrics"
aria-describedby="metricsDescription"
/>
<span id="metricsDescription" class="text-neutral-100">
{{
allowMetrics
? $t('install.metricsEnabled')
: $t('install.metricsDisabled')
}}
</span>
</div>
<div class="flex pt-6 justify-end">
<Button
:label="$t('g.ok')"
icon="pi pi-check"
:loading="isUpdating"
iconPos="right"
@click="updateConsent"
/>
</div>
</div>
</div>
</BaseViewTemplate>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ToggleSwitch from 'primevue/toggleswitch'
import { useToast } from 'primevue/usetoast'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { electronAPI } from '@/utils/envUtil'
const toast = useToast()
const { t } = useI18n()
const allowMetrics = ref(true)
const router = useRouter()
const isUpdating = ref(false)
const updateConsent = async () => {
isUpdating.value = true
try {
await electronAPI().setMetricsConsent(allowMetrics.value)
} catch (error) {
toast.add({
severity: 'error',
summary: t('install.errorUpdatingConsent'),
detail: t('install.errorUpdatingConsentDetail'),
life: 3000
})
} finally {
isUpdating.value = false
}
router.push('/')
}
</script>

View File

@@ -78,7 +78,7 @@ const continueToInstall = () => {
}
</script>
<style>
<style scoped>
.sad-container {
@apply grid items-center justify-evenly;
grid-template-columns: 25rem 1fr;

View File

@@ -78,7 +78,7 @@ const terminalCreated = (
) => {
xterm = terminal
useAutoSize(root, true, true)
useAutoSize({ root, autoRows: true, autoCols: true })
electron.onLogMessage((message: string) => {
terminal.write(message)
})

View File

@@ -28,7 +28,7 @@ import { electronAPI, isElectron } from '@/utils/envUtil'
const props = withDefaults(
defineProps<{
dark: boolean
dark?: boolean
}>(),
{
dark: false

View File

@@ -57,8 +57,18 @@ const mockElectronAPI: Plugin = {
changeTheme: () => {},
Config: {
setWindowStyle: () => {},
getWindowStyle: () => Promise.resolve('default')
}
getWindowStyle: () => Promise.resolve('default'),
getDetectedGpu: () => Promise.resolve('nvidia')
},
Events: {
trackEvent: (event_name, event_data) => {
console.log('trackEvent', event_name, event_data)
},
incrementUserProperty: (property, value) => {
console.log('incrementUserProperty', property, value)
}
},
setMetricsConsent: (consent) => {}
};`
}
]