Compare commits

...

22 Commits

Author SHA1 Message Date
AustinMroz
799e2d5569 Version bump 1.27.10 (#5969)
Patch version increment to 1.27.10

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5969-Version-bump-1-27-10-2866d73d365081b78ae2dbbb26f1d113)
by [Unito](https://www.unito.io)
2025-10-07 19:37:22 -05:00
Comfy Org PR Bot
3ab97100d2 [backport 1.27] OpenAIVideoSora2 Frontend pricing (#5967)
Backport of #5958 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5967-backport-1-27-OpenAIVideoSora2-Frontend-pricing-2866d73d365081b8a146d579824a005d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Marwan Ahmed <155799754+marawan206@users.noreply.github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2025-10-07 19:32:10 -05:00
filtered
58773f51f7 [Release] v1.27.9 (#5948)
## 🐛 Bug Fixes

- Removes broken installer terminal button (#5946)

**Full Changelog**:
https://github.com/Comfy-Org/ComfyUI_frontend/compare/v1.27.8...v1.27.9
2025-10-07 08:40:27 +11:00
filtered
feb3c078fa [desktop] Removes broken installer terminal button (#5946)
## Summary

Removes the terminal toggle button from the desktop installer that
causes broken UX when clicked during desktop install.

The button itself is merely a means to show the version of logs stored
in memory, using the in-app interface, as opposed to reading the files
from disk. The `Show Logs` button (working) takes the user directly into
the log directory.

## Changes

- **What**: Removes "Show Terminal" button from ServerStartView during
install flow
- **Breaking**: Removes show terminal button functionality

## Review Focus

The button is removed and is to be re-added at a later date (if Design
agree).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5946-desktop-Removes-broken-installer-terminal-button-2846d73d365081ac88d6c541a5d9c592)
by [Unito](https://www.unito.io)
2025-10-06 14:34:18 -07:00
AustinMroz
71a2eaa902 Version bump 1.27.8 (#5944)
Patch version increment to 1.27.8

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5944-Version-bump-1-27-8-2846d73d365081eaa942e5a2306fea86)
by [Unito](https://www.unito.io)
2025-10-06 15:30:34 -05:00
filtered
a310c3cd8e Fix images missing during desktop install (#5941)
## Summary

There's missing desktop GPU picker images caused by refactoring of
desktop front-end installation code. Was previously not an issue because
the paths were automatically adjusted by Vue, vite, or some other
tooling. Now that these are set programmatically (component props), this
"helpful" path adjustment is no longer occurring.

## Changes

- **What**: Takes away leading `/` from image paths.

## Screenshots (if applicable)

Current:
<img width="480" height="307" alt="image"
src="https://github.com/user-attachments/assets/20c0dd29-26d1-41b8-a59d-d3b0e69d998a"
/>

Proposed:
<img width="480" height="299" alt="image"
src="https://github.com/user-attachments/assets/aee87d1f-2060-4ec7-a8fc-43ebe2d650c2"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5941-Fix-images-missing-during-desktop-install-2846d73d365081c7b318c0ff31f70f2f)
by [Unito](https://www.unito.io)
2025-10-06 15:22:22 -05:00
AustinMroz
1a6ef57643 1.27.7 (#5862)
## What's Changed
### 🚀 Features
- add pricing for new api nodes (#5725)
- Rework desktop install / startup UX (#5292)

### 🐛 Bug Fixes
- Make experiment asset api setting hidden (#5861)
- Only add the listeners for DomWidget components once (#5849)
- fix invalid JSON (contains merge conflict markers) in 1.27 RC branch's
locale json (#5839)
- fix: Status indicator and close button appearing together  (#5741)
- Fix cyclic prototype errors with subgraphNodes (#5650)
- fix: don't immediately close missing nodes dialog if manager is
disabled (#5649)
- Fix SaveAs (#5644)

### 🔧 Maintenance  
- Add JSON validation CI workflow (#5838)
- Cherry-pick manager flag refactor to core/1.27 (#5646)
- Cherry-pick desktop dialogs framework to core/1.27 (#5634)

**Full Changelog**:
https://github.com/Comfy-Org/ComfyUI_frontend/compare/v1.27.5...v1.27.7

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5862-1-27-7-27e6d73d3650819aa712e682f6b88077)
by [Unito](https://www.unito.io)
2025-09-30 12:40:03 -05:00
AustinMroz
d3dc330d56 [backport 1.27] [chore] make experiment asset api setting hidden (#5861)
Backport of #5851 to to `core/1.27`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5861-backport-1-27-chore-make-experiment-asset-api-setting-hidden-27e6d73d3650819ea7c0fc0d48a393ee)
by [Unito](https://www.unito.io)

Co-authored-by: Arjan Singh <1598641+arjansingh@users.noreply.github.com>
2025-09-30 08:11:06 -07:00
Comfy Org PR Bot
cdff3f90ce [backport 1.27] fix: Only add the listeners for DomWidget components once (#5849)
Backport of #5846 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5849-backport-1-27-fix-Only-add-the-listeners-for-DomWidget-components-once-27d6d73d365081438673f970b0508032)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2025-09-29 21:20:15 -07:00
Comfy Org PR Bot
470663e25d [backport 1.27] add pricing for WanImageToImageApi node (#5830)
Backport of #5829 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5830-backport-1-27-add-pricing-for-WanImageToImageApi-node-27c6d73d36508104abbbfe78153b0d2a)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2025-09-28 17:02:59 -07:00
Comfy Org PR Bot
884a9886e0 [backport 1.27] [ci] add JSON validation CI workflow (#5838)
Backport of #5837 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5838-backport-1-27-ci-add-JSON-validation-CI-workflow-27c6d73d36508113a05fcd1a5bfc6512)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-09-28 16:20:38 -07:00
Christian Byrne
0ee3138485 fix invalid JSON (contains merge conflict markers) in 1.27 RC branch's locale json (#5839)
## Summary

Merge markers were inadvertantly merged in
https://github.com/comfy-org/ComfyuI_frontend/pull/5801, which [I
thought was good to
merge](https://github.com/Comfy-Org/ComfyUI_frontend/pull/5801#issuecomment-3340372578)
but missed this due to difficulty reviewing the thousands of lines of
translations that were in the backlog due to month+ long downtime on
translation CI.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5839-fix-invalid-JSON-contains-merge-conflict-markers-in-1-27-RC-branch-s-locale-json-27c6d73d365081439838ff4055714cf5)
by [Unito](https://www.unito.io)
2025-09-28 16:06:36 -07:00
AustinMroz
d6f0cae35c 1.27.6 (#5801)
Patch version increment to 1.27.6

Forced bump to regenerate locales

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5801-1-27-27a6d73d365081b988b9fc1e2564d193)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-09-27 10:36:02 -05:00
Christian Byrne
a91948352d fix(collect-i18n-node-defs): backport ComfyNodeDefImpl browser context fix to core/1.27 (#5796)
## Summary
Backports the ComfyNodeDefImpl browser context fix from #5775 to the
core/1.27 branch.

## Changes
- Move ComfyNodeDefImpl instantiation to browser context to avoid
Node.js compatibility issues
- Add workers: 1 to playwright i18n config for consistent execution 
- Fix floating promise by awaiting page.route() call
- Add allowDefaultProject config for playwright and script files to
resolve ESLint parsing

## Original Issue
The `collect-i18n-node-defs.ts` script was failing due to Vue components
being imported into Node.js context, causing Babel compilation errors
with TypeScript 'declare' fields.

## Solution
Uses dynamic imports to defer module loading until runtime in the
browser context, avoiding Babel compilation of problematic
TypeScript/Vue files.

Cherry-picked from 3a9365af1


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5796-fix-collect-i18n-node-defs-backport-ComfyNodeDefImpl-browser-context-fix-to-core-1-27-27a6d73d365081639625f25ecf9553fd)
by [Unito](https://www.unito.io)

Co-authored-by: github-actions <github-actions@github.com>
2025-09-26 07:40:09 -05:00
filtered
df3060b8e2 [Backport 1.27] Rework desktop install / startup UX (#5292)
## Summary

Backporting #5292 to core/1.27 branch to fix desktop installation and
startup UX issues.

## What's Changed

This is a manual backport of commit
b0f81b2245 which reworks the desktop
install and startup user experience.

### Merge Conflicts Resolved

The automatic backport failed with conflicts in:
- `src/components/install/GpuPicker.vue` - Resolved by keeping the
changes made in the PR
- `src/components/install/MirrorsConfiguration.vue` - Resolved by
keeping the file deletion from the PR
- `src/components/install/mirror/MirrorItem.vue` - Resolved by
combination merge (all changes)
- `src/views/ServerStartView.vue` - Resolved by combination merge (all
changes)

## Original PR

- PR: #5292 
- Commit: b0f81b2245
- Title: Rework desktop install / startup UX

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5756-Backport-1-27-Rework-desktop-install-startup-UX-5292-2786d73d365081668f4dc16694c51185)
by [Unito](https://www.unito.io)
2025-09-24 17:12:35 -05:00
AustinMroz
26d777deee [backport 1.27] fix: Status indicator and close button appearing together (#5738) (#5741)
Backport of #5738 to `core/1.27`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5741-backport-1-27-fix-Status-indicator-and-close-button-appearing-together-5738-2776d73d36508181a7fec09edd816ec5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2025-09-23 16:28:38 -07:00
Comfy Org PR Bot
77ca6d54a0 [backport 1.27] add pricing for new api nodes (#5725)
Backport of #5724 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5725-backport-1-27-add-pricing-for-new-api-nodes-2766d73d3650819eac1de10bc967deb2)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2025-09-22 11:44:14 -07:00
Christian Byrne
12f23dac4f [backport] Fix cyclic prototype errors with subgraphNodes (#5650)
Cherry-pick of PR #5637: Fix cyclic prototype errors with subgraphNodes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5650-Hotfix-Fix-cyclic-prototype-errors-with-subgraphNodes-2736d73d365081238853d5b445d08e7f)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2025-09-18 18:59:11 -07:00
Comfy Org PR Bot
dac73dc7ed [backport 1.27] fix: don't immediately close missing nodes dialog if manager is disabled (#5649)
Backport of #5647 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5649-backport-1-27-fix-don-t-immediately-close-missing-nodes-dialog-if-manager-is-disabled-2736d73d3650813681a7d1983f8ccf96)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-09-18 18:14:18 -07:00
Christian Byrne
a99df5d3dd [backport] Cherry-pick manager flag refactor to core/1.27 (#5646)
Cherry-pick of PR #5635: refactor: Change manager flag from
--disable-manager to --enable-manager

## Summary
- Cherry-picked commit a41b8a6d4f
- Updates frontend to align with backend changes in ComfyUI core PR
#7555
- Changed manager startup argument from `--disable-manager` (opt-out) to
`--enable-manager` (opt-in)
- Manager is now disabled by default unless explicitly enabled

## Original Changes
- Modified `useManagerState.ts` to check for `--enable-manager` flag
presence
- Inverted logic: manager is disabled when the flag is NOT present
- Updated all related tests to reflect the new opt-in behavior
- Fixed edge case where `systemStats` is null

## Testing
-  TypeScript type checking passed
-  ESLint linting passed
-  Prettier formatting passed
-  Knip found no issues
-  Cherry-pick applied cleanly with no conflicts

## Related
- Original PR: #5635
- Backend PR: https://github.com/comfyanonymous/ComfyUI/pull/7555

This hotfix ensures the frontend manager flag logic matches the backend
changes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5646-backport-Cherry-pick-manager-flag-refactor-to-core-1-27-2736d73d365081d38d8fddd6e451e156)
by [Unito](https://www.unito.io)

Co-authored-by: Jin Yi <jin12cc@gmail.com>
2025-09-18 17:20:27 -07:00
Comfy Org PR Bot
909866fd1d [backport 1.27] Fix SaveAs (#5644)
Backport of #5643 to `core/1.27`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5644-backport-1-27-Fix-SaveAs-2726d73d3650815dbaefe7090ee2ffde)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2025-09-18 17:07:16 -07:00
filtered
c39a03f5fd [Hotfix] Cherry-pick desktop dialogs framework to core/1.27 (#5634)
## Summary
Cherry-picked PR #5605 (Add desktop dialogs framework) to core/1.27
branch for hotfix release.

## Cherry-picked commits
- 09e7d1040: Add desktop dialogs framework (#5605)

## Changes
- Data-driven dialog structure in `desktopDialogs.ts`
- Dynamic dialog view component with i18n support
- Button action types: openUrl, close, cancel
- Button severity levels for styling (primary, secondary, danger, warn)
- Fallback invalid dialog for error handling
- i18n collection script updated for dialog strings

## Testing
- Typecheck passed ✓
- Cherry-pick applied cleanly without conflicts

## Impact
This adds the desktop dialog framework feature to the stable 1.27
branch, allowing desktop applications to display standardized dialogs.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5634-Hotfix-Cherry-pick-desktop-dialogs-framework-to-core-1-27-2726d73d3650811188e7f7e58766d52f)
by [Unito](https://www.unito.io)
2025-09-17 23:26:40 -07:00
89 changed files with 17936 additions and 1243 deletions

15
.github/workflows/json-validate.yaml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Validate JSON
on:
push:
branches:
- main
pull_request:
jobs:
json-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate JSON syntax
run: ./scripts/cicd/check-json.sh

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -33,7 +33,16 @@ export default defineConfig([
},
parserOptions: {
parser: tseslint.parser,
projectService: true,
projectService: {
allowDefaultProject: [
'vite.config.mts',
'vite.electron.config.mts',
'vite.types.config.mts',
'playwright.config.ts',
'playwright.i18n.config.ts',
'scripts/collect-i18n-node-defs.ts'
]
},
tsConfigRootDir: import.meta.dirname,
ecmaVersion: 2020,
sourceType: 'module',

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.27.5",
"version": "1.27.10",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -7,6 +7,7 @@ export default defineConfig({
headless: true
},
reporter: 'list',
workers: 1,
timeout: 60000,
testMatch: /collect-i18n-.*\.ts/
})

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

77
scripts/cicd/check-json.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/bin/bash
set -euo pipefail
usage() {
echo "Usage: $0 [--debug]" >&2
}
debug=0
while [ "$#" -gt 0 ]; do
case "$1" in
--debug)
debug=1
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 2
;;
esac
shift
done
# Validate JSON syntax in tracked files using jq
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not installed" >&2
exit 127
fi
EXCLUDE_PATTERNS=(
'**/tsconfig*.json'
)
if [ -n "${JSON_LINT_EXCLUDES:-}" ]; then
# shellcheck disable=SC2206
EXCLUDE_PATTERNS+=( ${JSON_LINT_EXCLUDES} )
fi
pathspecs=(-- '*.json')
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
if [[ ${pattern:0:1} == ':' ]]; then
pathspecs+=("$pattern")
else
pathspecs+=(":(glob,exclude)${pattern}")
fi
done
mapfile -t json_files < <(git ls-files "${pathspecs[@]}")
if [ "${#json_files[@]}" -eq 0 ]; then
echo 'No JSON files found.'
exit 0
fi
if [ "$debug" -eq 1 ]; then
echo 'JSON files to validate:'
printf ' %s\n' "${json_files[@]}"
fi
failed=0
for file in "${json_files[@]}"; do
if ! jq -e . "$file" >/dev/null; then
echo "Invalid JSON syntax: $file" >&2
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo 'JSON validation failed.' >&2
exit 1
fi
echo 'All JSON files are valid.'

View File

@@ -2,6 +2,7 @@ import * as fs from 'fs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
import type { FormItem, SettingParams } from '../src/platform/settings/types'
import type { ComfyCommandImpl } from '../src/stores/commandStore'
@@ -131,6 +132,23 @@ test('collect-i18n-general', async ({ comfyPage }) => {
])
)
// Desktop Dialogs
const allDesktopDialogsLocale = Object.fromEntries(
Object.values(DESKTOP_DIALOGS).map((dialog) => [
normalizeI18nKey(dialog.id),
{
title: dialog.title,
message: dialog.message,
buttons: Object.fromEntries(
dialog.buttons.map((button) => [
normalizeI18nKey(button.label),
button.label
])
)
}
])
)
fs.writeFileSync(
localePath,
JSON.stringify(
@@ -144,7 +162,8 @@ test('collect-i18n-general', async ({ comfyPage }) => {
...allSettingCategoriesLocale
},
serverConfigItems: allServerConfigsLocale,
serverConfigCategories: allServerConfigCategoriesLocale
serverConfigCategories: allServerConfigCategoriesLocale,
desktopDialogs: allDesktopDialogsLocale
},
null,
2

View File

@@ -1,9 +1,9 @@
import * as fs from 'fs'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import type { ComfyNodeDef } from '../src/schemas/nodeDefSchema'
import type { ComfyApi } from '../src/scripts/api'
import { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import { normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
@@ -11,23 +11,28 @@ const nodeDefsPath = './src/locales/en/nodeDefs.json'
test('collect-i18n-node-defs', async ({ comfyPage }) => {
// Mock view route
comfyPage.page.route('**/view**', async (route) => {
await comfyPage.page.route('**/view**', async (route) => {
await route.fulfill({
body: JSON.stringify({})
})
})
const nodeDefs: ComfyNodeDefImpl[] = (
Object.values(
await comfyPage.page.evaluate(async () => {
const api = window['app'].api as ComfyApi
return await api.getNodeDefs()
})
) as ComfyNodeDef[]
// Note: Don't mock the object_info API endpoint - let it hit the actual backend
const nodeDefs: ComfyNodeDefImpl[] = await comfyPage.page.evaluate(
async () => {
const api = window['app'].api
const rawNodeDefs = await api.getNodeDefs()
const { ComfyNodeDefImpl } = await import('../src/stores/nodeDefStore')
return (
Object.values(rawNodeDefs)
// Ignore DevTools nodes (used for internal testing)
.filter((def: ComfyNodeDef) => !def.name.startsWith('DevTools'))
.map((def: ComfyNodeDef) => new ComfyNodeDefImpl(def))
)
}
)
// Ignore DevTools nodes (used for internal testing)
.filter((def) => !def.name.startsWith('DevTools'))
.map((def) => new ComfyNodeDefImpl(def))
console.log(`Collected ${nodeDefs.length} node definitions`)

View File

@@ -66,6 +66,8 @@
--color-charcoal-700: #202121;
--color-charcoal-800: #171718;
--color-neutral-550: #636363;
--color-stone-100: #444444;
--color-stone-200: #828282;
--color-stone-300: #bbbbbb;
@@ -103,6 +105,10 @@
--color-danger-100: #c02323;
--color-danger-200: #d62952;
--color-coral-red-600: #973a40;
--color-coral-red-500: #c53f49;
--color-coral-red-400: #dd424e;
--color-bypass: #6a246a;
--color-error: #962a2a;

View File

@@ -1,5 +1,8 @@
<template>
<div ref="rootEl" class="relative overflow-hidden h-full w-full bg-black">
<div
ref="rootEl"
class="relative overflow-hidden h-full w-full bg-neutral-900"
>
<div class="p-terminal rounded-none h-full w-full p-2">
<div ref="terminalEl" class="h-full terminal-host" />
</div>
@@ -97,12 +100,13 @@ onUnmounted(() => {
</script>
<style scoped>
@reference '../../../../assets/css/style.css';
:deep(.p-terminal) .xterm {
overflow-x: auto;
@apply overflow-hidden;
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
@apply bg-neutral-900 overflow-hidden;
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div :class="wrapperClass">
<div class="grid grid-rows-2 gap-8">
<!-- Top container: Logo -->
<div class="flex items-end justify-center">
<img
src="/assets/images/comfy-brand-mark.svg"
:alt="t('g.logoAlt')"
class="w-60"
/>
</div>
<!-- Bottom container: Progress and text -->
<div class="flex flex-col items-center justify-center gap-4">
<ProgressBar
v-if="!hideProgress"
:mode="progressMode"
:value="progressPercentage ?? 0"
:show-value="false"
class="w-90 h-2 mt-8"
:pt="{ value: { class: 'bg-brand-yellow' } }"
/>
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
{{ title }}
</h1>
<p v-if="statusText" class="text-lg text-neutral-400">
{{ statusText }}
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import ProgressBar from 'primevue/progressbar'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
/** Props for the StartupDisplay component */
interface StartupDisplayProps {
/** Progress: 0-100 for determinate, undefined for indeterminate */
progressPercentage?: number
/** Main title text */
title?: string
/** Status text shown below the title */
statusText?: string
/** Hide the progress bar */
hideProgress?: boolean
/** Use full screen wrapper (default: true) */
fullScreen?: boolean
}
const {
progressPercentage,
title,
statusText,
hideProgress = false,
fullScreen = true
} = defineProps<StartupDisplayProps>()
const progressMode = computed(() =>
progressPercentage === undefined ? 'indeterminate' : 'determinate'
)
const wrapperClass = computed(() =>
fullScreen
? 'flex items-center justify-center min-h-screen'
: 'flex items-center justify-center'
)
</script>

View File

@@ -138,7 +138,7 @@ const allMissingNodesInstalled = computed(() => {
})
// Watch for completion and close dialog
watch(allMissingNodesInstalled, async (allInstalled) => {
if (allInstalled) {
if (allInstalled && showInstallAllButton.value) {
// Use nextTick to ensure state updates are complete
await nextTick()

View File

@@ -124,50 +124,43 @@ watch(
}
}
)
// Set up event listeners only after the widget is mounted and visible
const setupDOMEventListeners = () => {
if (!isDOMWidget(widget) || !widgetState.visible) return
if (widget.element.blur) {
useEventListener(document, 'mousedown', (event) => {
if (!widget.element.contains(event.target as HTMLElement)) {
widget.element.blur()
}
})
useEventListener(document, 'mousedown', (event) => {
if (!isDOMWidget(widget) || !widgetState.visible || !widget.element.blur) {
return
}
if (!widget.element.contains(event.target as HTMLElement)) {
widget.element.blur()
}
})
for (const evt of widget.options.selectOn ?? ['focus', 'click']) {
useEventListener(widget.element, evt, () => {
onMounted(() => {
if (!isDOMWidget(widget)) {
return
}
useEventListener(
widget.element,
widget.options.selectOn ?? ['focus', 'click'],
() => {
const lgCanvas = canvasStore.canvas
lgCanvas?.selectNode(widget.node)
lgCanvas?.bringToFront(widget.node)
})
}
}
// Set up event listeners when widget becomes visible
watch(
() => widgetState.visible,
(visible) => {
if (visible) {
setupDOMEventListeners()
}
},
{ immediate: true }
)
)
})
const inputSpec = widget.node.constructor.nodeData
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
// Mount DOM element when widget is or becomes visible
const mountElementIfVisible = () => {
if (widgetState.visible && isDOMWidget(widget) && widgetElement.value) {
// Only append if not already a child
if (!widgetElement.value.contains(widget.element)) {
widgetElement.value.appendChild(widget.element)
}
if (!(widgetState.visible && isDOMWidget(widget) && widgetElement.value)) {
return
}
// Only append if not already a child
if (widgetElement.value.contains(widget.element)) {
return
}
widgetElement.value.appendChild(widget.element)
}
// Check on mount - but only after next tick to ensure visibility is calculated

View File

@@ -10,14 +10,14 @@
</p>
</div>
<div class="flex flex-col bg-neutral-800 p-4 rounded-lg">
<div class="flex flex-col bg-neutral-800 p-4 rounded-lg text-sm">
<!-- Auto Update Setting -->
<div class="flex items-center gap-4">
<div class="flex-1">
<h3 class="text-lg font-medium text-neutral-100">
{{ $t('install.settings.autoUpdate') }}
</h3>
<p class="text-sm text-neutral-400 mt-1">
<p class="text-neutral-400 mt-1">
{{ $t('install.settings.autoUpdateDescription') }}
</p>
</div>
@@ -32,14 +32,10 @@
<h3 class="text-lg font-medium text-neutral-100">
{{ $t('install.settings.allowMetrics') }}
</h3>
<p class="text-sm text-neutral-400 mt-1">
<p class="text-neutral-400">
{{ $t('install.settings.allowMetricsDescription') }}
</p>
<a
href="#"
class="text-sm text-blue-400 hover:text-blue-300 mt-1 inline-block"
@click.prevent="showMetricsInfo"
>
<a href="#" @click.prevent="showMetricsInfo">
{{ $t('install.settings.learnMoreAboutData') }}
</a>
</div>
@@ -51,7 +47,9 @@
<Dialog
v-model:visible="showDialog"
modal
dismissable-mask
:header="$t('install.settings.dataCollectionDialog.title')"
class="select-none"
>
<div class="text-neutral-300">
<h4 class="font-medium mb-2">
@@ -110,11 +108,7 @@
</ul>
<div class="mt-4">
<a
href="https://comfy.org/privacy"
target="_blank"
class="text-blue-400 hover:text-blue-300 underline"
>
<a href="https://comfy.org/privacy" target="_blank">
{{ $t('install.settings.dataCollectionDialog.viewFullPolicy') }}
</a>
</div>

View File

@@ -1,130 +1,66 @@
<template>
<div class="flex flex-col gap-6 w-[600px] h-[30rem] select-none">
<!-- Installation Path Section -->
<div class="grow flex flex-col gap-4 text-neutral-300">
<h2 class="text-2xl font-semibold text-neutral-100">
{{ $t('install.gpuSelection.selectGpu') }}
</h2>
<div
class="grid grid-rows-[1fr_auto_auto_1fr] w-full max-w-3xl mx-auto h-[40rem] select-none"
>
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
{{ $t('install.gpuPicker.title') }}
</h2>
<p class="m-1 text-neutral-400">
{{ $t('install.gpuSelection.selectGpuDescription') }}:
</p>
<!-- GPU Selection buttons - takes up remaining space and centers content -->
<div class="flex-1 flex gap-8 justify-center items-center">
<!-- Apple Metal / NVIDIA -->
<HardwareOption
v-if="platform === 'darwin'"
:image-path="'assets/images/apple-mps-logo.png'"
placeholder-text="Apple Metal"
subtitle="Apple Metal"
:value="'mps'"
:selected="selected === 'mps'"
:recommended="true"
@click="pickGpu('mps')"
/>
<HardwareOption
v-else
:image-path="'assets/images/nvidia-logo-square.jpg'"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:value="'nvidia'"
:selected="selected === 'nvidia'"
:recommended="true"
@click="pickGpu('nvidia')"
/>
<!-- CPU -->
<HardwareOption
placeholder-text="CPU"
:subtitle="$t('install.gpuPicker.cpuSubtitle')"
:value="'cpu'"
:selected="selected === 'cpu'"
@click="pickGpu('cpu')"
/>
<!-- Manual Install -->
<HardwareOption
placeholder-text="Manual Install"
:subtitle="$t('install.gpuPicker.manualSubtitle')"
:value="'unsupported'"
:selected="selected === 'unsupported'"
@click="pickGpu('unsupported')"
/>
</div>
<!-- GPU Selection buttons -->
<div
class="flex gap-2 text-center transition-opacity"
:class="{ selected: selected }"
>
<!-- NVIDIA -->
<div
v-if="platform !== 'darwin'"
class="gpu-button"
:class="{ selected: selected === 'nvidia' }"
role="button"
@click="pickGpu('nvidia')"
>
<img
class="m-12"
alt="NVIDIA logo"
width="196"
height="32"
src="/assets/images/nvidia-logo.svg"
/>
</div>
<!-- MPS -->
<div
v-if="platform === 'darwin'"
class="gpu-button"
:class="{ selected: selected === 'mps' }"
role="button"
@click="pickGpu('mps')"
>
<img
class="rounded-lg hover-brighten"
alt="Apple Metal Performance Shaders Logo"
width="292"
ratio
src="/assets/images/apple-mps-logo.png"
/>
</div>
<!-- Manual configuration -->
<div
class="gpu-button"
:class="{ selected: selected === 'unsupported' }"
role="button"
@click="pickGpu('unsupported')"
>
<img
class="m-12"
alt="Manual configuration"
width="196"
src="/assets/images/manual-configuration.svg"
/>
</div>
</div>
<!-- Details on selected GPU -->
<p v-if="selected === 'nvidia'" class="m-1">
<Tag icon="pi pi-check" severity="success" :value="'CUDA'" />
{{ $t('install.gpuSelection.nvidiaDescription') }}
</p>
<p v-if="selected === 'mps'" class="m-1">
<Tag icon="pi pi-check" severity="success" :value="'MPS'" />
{{ $t('install.gpuSelection.mpsDescription') }}
</p>
<div v-if="selected === 'unsupported'" class="text-neutral-300">
<p class="m-1">
<Tag
icon="pi pi-exclamation-triangle"
severity="warn"
:value="t('icon.exclamation-triangle')"
/>
{{ $t('install.gpuSelection.customSkipsPython') }}
</p>
<ul>
<li>
<strong>
{{ $t('install.gpuSelection.customComfyNeedsPython') }}
</strong>
</li>
<li>{{ $t('install.gpuSelection.customManualVenv') }}</li>
<li>{{ $t('install.gpuSelection.customInstallRequirements') }}</li>
<li>{{ $t('install.gpuSelection.customMayNotWork') }}</li>
</ul>
</div>
<div v-if="selected === 'cpu'">
<p class="m-1">
<Tag
icon="pi pi-exclamation-triangle"
severity="warn"
:value="t('icon.exclamation-triangle')"
/>
{{ $t('install.gpuSelection.cpuModeDescription') }}
</p>
<p class="m-1">
{{ $t('install.gpuSelection.cpuModeDescription2') }}
</p>
<div class="pt-12 px-24 h-16">
<div v-show="showRecommendedBadge" class="flex items-center gap-2">
<Tag
:value="$t('install.gpuPicker.recommended')"
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
/>
<i-lucide:badge-check class="text-neutral-300 text-lg" />
</div>
</div>
<div
class="transition-opacity flex gap-3 h-0"
:class="{
'opacity-40': selected && selected !== 'cpu'
}"
>
<ToggleSwitch
v-model="cpuMode"
input-id="cpu-mode"
class="-translate-y-40"
/>
<label for="cpu-mode" class="select-none">
{{ $t('install.gpuSelection.enableCpuMode') }}
</label>
<div class="text-neutral-300 px-24">
<p v-show="descriptionText" class="leading-relaxed">
{{ descriptionText }}
</p>
</div>
</div>
</template>
@@ -132,20 +68,12 @@
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import Tag from 'primevue/tag'
import ToggleSwitch from 'primevue/toggleswitch'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import HardwareOption from '@/components/install/HardwareOption.vue'
import { st } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'
const { t } = useI18n()
const cpuMode = computed({
get: () => selected.value === 'cpu',
set: (value) => {
selected.value = value ? 'cpu' : null
}
})
const selected = defineModel<TorchDeviceType | null>('device', {
required: true
})
@@ -153,55 +81,23 @@ const selected = defineModel<TorchDeviceType | null>('device', {
const electron = electronAPI()
const platform = electron.getPlatform()
const pickGpu = (value: typeof selected.value) => {
const newValue = selected.value === value ? null : value
selected.value = newValue
const showRecommendedBadge = computed(
() => selected.value === 'mps' || selected.value === 'nvidia'
)
const descriptionKeys = {
mps: 'appleMetal',
nvidia: 'nvidia',
cpu: 'cpu',
unsupported: 'manual'
} as const
const descriptionText = computed(() => {
const key = selected.value ? descriptionKeys[selected.value] : undefined
return st(`install.gpuPicker.${key}Description`, '')
})
const pickGpu = (value: TorchDeviceType) => {
selected.value = value
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
.p-tag {
--p-tag-gap: 0.5rem;
}
.hover-brighten {
@apply transition-colors;
transition-property: filter, box-shadow;
&:hover {
filter: brightness(107%) contrast(105%);
box-shadow: 0 0 0.25rem #ffffff79;
}
}
.p-accordioncontent-content {
@apply bg-neutral-900 rounded-lg transition-colors;
}
div.selected {
.gpu-button:not(.selected) {
@apply opacity-50 hover:opacity-100;
}
}
.gpu-button {
@apply w-1/2 m-0 cursor-pointer rounded-lg flex flex-col items-center justify-around bg-neutral-800/50 hover:bg-neutral-800/75 transition-colors;
&.selected {
@apply opacity-100 bg-neutral-700/50 hover:bg-neutral-700/60;
}
}
.disabled {
@apply pointer-events-none opacity-40;
}
.p-card-header {
@apply text-center grow;
}
.p-card-body {
@apply text-center pt-0;
}
</style>

View File

@@ -0,0 +1,73 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import HardwareOption from './HardwareOption.vue'
const meta: Meta<typeof HardwareOption> = {
title: 'Desktop/Components/HardwareOption',
component: HardwareOption,
parameters: {
layout: 'centered',
backgrounds: {
default: 'dark',
values: [{ name: 'dark', value: '#1a1a1a' }]
}
},
argTypes: {
selected: { control: 'boolean' },
imagePath: { control: 'text' },
placeholderText: { control: 'text' },
subtitle: { control: 'text' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const AppleMetalSelected: Story = {
args: {
imagePath: '/assets/images/apple-mps-logo.png',
placeholderText: 'Apple Metal',
subtitle: 'Apple Metal',
value: 'mps',
selected: true
}
}
export const AppleMetalUnselected: Story = {
args: {
imagePath: '/assets/images/apple-mps-logo.png',
placeholderText: 'Apple Metal',
subtitle: 'Apple Metal',
value: 'mps',
selected: false
}
}
export const CPUOption: Story = {
args: {
placeholderText: 'CPU',
subtitle: 'Subtitle',
value: 'cpu',
selected: false
}
}
export const ManualInstall: Story = {
args: {
placeholderText: 'Manual Install',
subtitle: 'Subtitle',
value: 'unsupported',
selected: false
}
}
export const NvidiaSelected: Story = {
args: {
imagePath: '/assets/images/nvidia-logo-square.jpg',
placeholderText: 'NVIDIA',
subtitle: 'NVIDIA',
value: 'nvidia',
selected: true
}
}

View File

@@ -0,0 +1,55 @@
<template>
<div class="relative">
<!-- Recommended Badge -->
<button
:class="
cn(
'hardware-option w-[170px] h-[190px] p-5 flex flex-col items-center rounded-3xl transition-all duration-200 bg-neutral-900/70 border-4',
selected ? 'border-solid border-brand-yellow' : 'border-transparent'
)
"
@click="$emit('click')"
>
<!-- Icon/Logo Area - Rounded square container -->
<div
class="icon-container w-[110px] h-[110px] shrink-0 rounded-2xl bg-neutral-800 flex items-center justify-center overflow-hidden"
>
<img
v-if="imagePath"
:src="imagePath"
:alt="placeholderText"
class="w-full h-full object-cover"
style="object-position: 57% center"
draggable="false"
/>
<span v-else class="text-xl font-medium text-neutral-400">
{{ placeholderText }}
</span>
</div>
<!-- Text Content -->
<div v-if="subtitle" class="text-center mt-4">
<div class="text-sm text-neutral-500">{{ subtitle }}</div>
</div>
</button>
</div>
</template>
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { cn } from '@/utils/tailwindUtil'
interface Props {
imagePath?: string
placeholderText: string
subtitle?: string
value: TorchDeviceType
selected?: boolean
recommended?: boolean
}
defineProps<Props>()
defineEmits<{ click: [] }>()
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-4">
<!-- Back button -->
<Button
v-if="currentStep !== '1'"
:label="$t('g.back')"
severity="secondary"
icon="pi pi-arrow-left"
class="font-inter rounded-lg border-0 px-6 py-2 justify-self-start"
@click="$emit('previous')"
/>
<div v-else></div>
<!-- Step indicators in center -->
<StepList class="flex justify-center items-center gap-3 select-none">
<Step value="1" :pt="stepPassthrough">
{{ $t('install.gpu') }}
</Step>
<Step value="2" :disabled="disableLocationStep" :pt="stepPassthrough">
{{ $t('install.installLocation') }}
</Step>
<Step value="3" :disabled="disableSettingsStep" :pt="stepPassthrough">
{{ $t('install.desktopSettings') }}
</Step>
</StepList>
<!-- Next/Install button -->
<Button
:label="currentStep !== '3' ? $t('g.next') : $t('g.install')"
class="px-8 py-2 bg-brand-yellow hover:bg-brand-yellow/90 font-inter rounded-lg border-0 transition-colors justify-self-end"
:pt="{
label: { class: 'text-neutral-900 font-inter font-black' }
}"
:disabled="!canProceed"
@click="currentStep !== '3' ? $emit('next') : $emit('install')"
/>
</div>
</template>
<script setup lang="ts">
import type { PassThrough } from '@primevue/core'
import Button from 'primevue/button'
import Step, { type StepPassThroughOptions } from 'primevue/step'
import StepList from 'primevue/steplist'
defineProps<{
/** Current step index as string ('1', '2', '3', '4') */
currentStep: string
/** Whether the user can proceed to the next step */
canProceed: boolean
/** Whether the location step should be disabled */
disableLocationStep: boolean
/** Whether the migration step should be disabled */
disableMigrationStep: boolean
/** Whether the settings step should be disabled */
disableSettingsStep: boolean
}>()
defineEmits<{
previous: []
next: []
install: []
}>()
const stepPassthrough: PassThrough<StepPassThroughOptions> = {
root: { class: 'flex-none p-0 m-0' },
header: ({ context }) => ({
class: [
'h-2.5 p-0 m-0 border-0 rounded-full transition-all duration-300',
context.active
? 'bg-brand-yellow w-8 rounded-sm'
: 'bg-neutral-700 w-2.5',
context.disabled ? 'opacity-60 cursor-not-allowed' : ''
].join(' ')
}),
number: { class: 'hidden' },
title: { class: 'hidden' }
}
</script>

View File

@@ -0,0 +1,148 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import { ref } from 'vue'
import InstallLocationPicker from './InstallLocationPicker.vue'
const meta: Meta<typeof InstallLocationPicker> = {
title: 'Desktop/Components/InstallLocationPicker',
component: InstallLocationPicker,
parameters: {
layout: 'padded',
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
{ name: 'neutral-900', value: '#171717' },
{ name: 'neutral-950', value: '#0a0a0a' }
]
}
},
decorators: [
() => {
// Mock electron API
;(window as any).electronAPI = {
getSystemPaths: () =>
Promise.resolve({
defaultInstallPath: '/Users/username/ComfyUI'
}),
validateInstallPath: () =>
Promise.resolve({
isValid: true,
exists: false,
canWrite: true,
freeSpace: 100000000000,
requiredSpace: 10000000000,
isNonDefaultDrive: false
}),
validateComfyUISource: () =>
Promise.resolve({
isValid: true
}),
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
}
return { template: '<story />' }
}
]
}
export default meta
type Story = StoryObj<typeof meta>
// Default story with accordion expanded
export const Default: Story = {
render: (args) => ({
components: { InstallLocationPicker },
setup() {
const installPath = ref('/Users/username/ComfyUI')
const pathError = ref('')
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
return {
args,
installPath,
pathError,
migrationSourcePath,
migrationItemIds
}
},
template: `
<div class="min-h-screen bg-neutral-950 p-8">
<InstallLocationPicker
v-model:installPath="installPath"
v-model:pathError="pathError"
v-model:migrationSourcePath="migrationSourcePath"
v-model:migrationItemIds="migrationItemIds"
/>
</div>
`
})
}
// Story with different background to test transparency
export const OnNeutral900: Story = {
render: (args) => ({
components: { InstallLocationPicker },
setup() {
const installPath = ref('/Users/username/ComfyUI')
const pathError = ref('')
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
return {
args,
installPath,
pathError,
migrationSourcePath,
migrationItemIds
}
},
template: `
<div class="min-h-screen bg-neutral-900 p-8">
<InstallLocationPicker
v-model:installPath="installPath"
v-model:pathError="pathError"
v-model:migrationSourcePath="migrationSourcePath"
v-model:migrationItemIds="migrationItemIds"
/>
</div>
`
})
}
// Story with debug overlay showing background colors
export const DebugBackgrounds: Story = {
render: (args) => ({
components: { InstallLocationPicker },
setup() {
const installPath = ref('/Users/username/ComfyUI')
const pathError = ref('')
const migrationSourcePath = ref('/Users/username/ComfyUI-old')
const migrationItemIds = ref<string[]>(['models', 'custom_nodes'])
return {
args,
installPath,
pathError,
migrationSourcePath,
migrationItemIds
}
},
template: `
<div class="min-h-screen bg-neutral-950 p-8 relative">
<div class="absolute top-4 right-4 text-white text-xs space-y-2 z-50">
<div>Parent bg: neutral-950 (#0a0a0a)</div>
<div>Accordion content: bg-transparent</div>
<div>Migration options: bg-transparent + p-4 rounded-lg</div>
</div>
<InstallLocationPicker
v-model:installPath="installPath"
v-model:pathError="pathError"
v-model:migrationSourcePath="migrationSourcePath"
v-model:migrationItemIds="migrationItemIds"
/>
</div>
`
})
}

View File

@@ -1,103 +1,215 @@
<template>
<div class="flex flex-col gap-6 w-[600px]">
<div class="flex flex-col gap-8 w-full max-w-3xl mx-auto select-none">
<!-- Installation Path Section -->
<div class="flex flex-col gap-4">
<h2 class="text-2xl font-semibold text-neutral-100">
{{ $t('install.chooseInstallationLocation') }}
<div class="grow flex flex-col gap-6 text-neutral-300">
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
{{ $t('install.locationPicker.title') }}
</h2>
<p class="text-neutral-400 my-0">
{{ $t('install.installLocationDescription') }}
<p class="text-center text-neutral-400 px-12">
{{ $t('install.locationPicker.subtitle') }}
</p>
<div class="flex gap-2">
<IconField class="flex-1">
<InputText
v-model="installPath"
class="w-full"
:class="{ 'p-invalid': pathError }"
@update:model-value="validatePath"
@focus="onFocus"
/>
<InputIcon
v-tooltip.top="$t('install.installLocationTooltip')"
class="pi pi-info-circle"
/>
</IconField>
<Button icon="pi pi-folder" class="w-12" @click="browsePath" />
<!-- Path Input -->
<div class="flex gap-2 px-12">
<InputText
v-model="installPath"
:placeholder="$t('install.locationPicker.pathPlaceholder')"
class="flex-1 bg-neutral-800/50 border-neutral-700 text-neutral-200 placeholder:text-neutral-500"
:class="{ 'p-invalid': pathError }"
@update:model-value="validatePath"
@focus="onFocus"
/>
<Button
icon="pi pi-folder-open"
severity="secondary"
class="bg-neutral-700 hover:bg-neutral-600 border-0"
@click="browsePath"
/>
</div>
<Message v-if="pathError" severity="error" class="whitespace-pre-line">
{{ pathError }}
</Message>
<Message v-if="pathExists" severity="warn">
{{ $t('install.pathExists') }}
</Message>
<Message v-if="nonDefaultDrive" severity="warn">
{{ $t('install.nonDefaultDrive') }}
</Message>
</div>
<!-- System Paths Info -->
<div class="bg-neutral-800 p-4 rounded-lg">
<h3 class="text-lg font-medium mt-0 mb-3 text-neutral-100">
{{ $t('install.systemLocations') }}
</h3>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<i class="pi pi-folder text-neutral-400" />
<span class="text-neutral-400">App Data:</span>
<span class="text-neutral-200">{{ appData }}</span>
<span
v-tooltip="$t('install.appDataLocationTooltip')"
class="pi pi-info-circle"
/>
</div>
<div class="flex items-center gap-2">
<i class="pi pi-desktop text-neutral-400" />
<span class="text-neutral-400">App Path:</span>
<span class="text-neutral-200">{{ appPath }}</span>
<span
v-tooltip="$t('install.appPathLocationTooltip')"
class="pi pi-info-circle"
/>
</div>
<!-- Error Messages -->
<div v-if="pathError || pathExists || nonDefaultDrive" class="px-12">
<Message
v-if="pathError"
severity="error"
class="whitespace-pre-line w-full"
>
{{ pathError }}
</Message>
<Message v-if="pathExists" severity="warn" class="w-full">
{{ $t('install.pathExists') }}
</Message>
<Message v-if="nonDefaultDrive" severity="warn" class="w-full">
{{ $t('install.nonDefaultDrive') }}
</Message>
</div>
<!-- Collapsible Sections using PrimeVue Accordion -->
<Accordion
v-model:value="activeAccordionIndex"
:multiple="true"
class="location-picker-accordion"
:pt="{
root: 'bg-transparent border-0',
panel: {
root: 'border-0 mb-0'
},
header: {
root: 'border-0',
content:
'text-neutral-400 hover:text-neutral-300 px-4 py-2 flex items-center gap-3',
toggleicon: 'text-xs order-first mr-0'
},
content: {
root: 'bg-transparent border-0',
content: 'text-neutral-500 text-sm pl-11 pb-3 pt-0'
}
}"
>
<AccordionPanel value="0">
<AccordionHeader>
{{ $t('install.locationPicker.migrateFromExisting') }}
</AccordionHeader>
<AccordionContent>
<MigrationPicker
v-model:source-path="migrationSourcePath"
v-model:migration-item-ids="migrationItemIds"
/>
</AccordionContent>
</AccordionPanel>
<AccordionPanel value="1">
<AccordionHeader>
{{ $t('install.locationPicker.chooseDownloadServers') }}
</AccordionHeader>
<AccordionContent>
<template
v-for="([item, modelValue], index) in mirrors"
:key="item.settingId + item.mirror"
>
<Divider v-if="index > 0" class="my-8" />
<MirrorItem
v-model="modelValue.value"
:item="item"
@state-change="validationStates[index] = $event"
/>
</template>
</AccordionContent>
</AccordionPanel>
</Accordion>
</div>
</div>
</template>
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
import Accordion from 'primevue/accordion'
import AccordionContent from 'primevue/accordioncontent'
import AccordionHeader from 'primevue/accordionheader'
import AccordionPanel from 'primevue/accordionpanel'
import Button from 'primevue/button'
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import Divider from 'primevue/divider'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import { onMounted, ref } from 'vue'
import { type ModelRef, computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import MigrationPicker from '@/components/install/MigrationPicker.vue'
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
import {
PYPI_MIRROR,
PYTHON_MIRROR,
type UVMirror
} from '@/constants/uvMirrors'
import { electronAPI } from '@/utils/envUtil'
import { isInChina } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'
const { t } = useI18n()
const installPath = defineModel<string>('installPath', { required: true })
const pathError = defineModel<string>('pathError', { required: true })
const migrationSourcePath = defineModel<string>('migrationSourcePath')
const migrationItemIds = defineModel<string[]>('migrationItemIds')
const pythonMirror = defineModel<string>('pythonMirror', {
default: ''
})
const pypiMirror = defineModel<string>('pypiMirror', {
default: ''
})
const torchMirror = defineModel<string>('torchMirror', {
default: ''
})
const { device } = defineProps<{ device: TorchDeviceType | null }>()
const pathExists = ref(false)
const nonDefaultDrive = ref(false)
const appData = ref('')
const appPath = ref('')
const inputTouched = ref(false)
// Accordion state - array of active panel values
const activeAccordionIndex = ref<string[] | undefined>(undefined)
const electron = electronAPI()
// Get system paths on component mount
// Mirror configuration logic
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
switch (device) {
case 'mps':
return {
settingId,
mirror: TorchMirrorUrl.NightlyCpu,
fallbackMirror: TorchMirrorUrl.NightlyCpu
}
case 'nvidia':
return {
settingId,
mirror: TorchMirrorUrl.Cuda,
fallbackMirror: TorchMirrorUrl.Cuda
}
case 'cpu':
default:
return {
settingId,
mirror: PYPI_MIRROR.mirror,
fallbackMirror: PYPI_MIRROR.fallbackMirror
}
}
}
const userIsInChina = ref(false)
const useFallbackMirror = (mirror: UVMirror) => ({
...mirror,
mirror: mirror.fallbackMirror
})
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() =>
(
[
[PYTHON_MIRROR, pythonMirror],
[PYPI_MIRROR, pypiMirror],
[getTorchMirrorItem(device ?? 'cpu'), torchMirror]
] as [UVMirror, ModelRef<string>][]
).map(([item, modelValue]) => [
userIsInChina.value ? useFallbackMirror(item) : item,
modelValue
])
)
const validationStates = ref<ValidationState[]>(
mirrors.value.map(() => ValidationState.IDLE)
)
// Get default install path on component mount
onMounted(async () => {
const paths = await electron.getSystemPaths()
appData.value = paths.appData
appPath.value = paths.appPath
installPath.value = paths.defaultInstallPath
await validatePath(paths.defaultInstallPath)
userIsInChina.value = await isInChina()
})
const validatePath = async (path: string | undefined) => {
@@ -151,3 +263,52 @@ const onFocus = async () => {
await validatePath(installPath.value)
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
:deep(.location-picker-accordion) {
@apply px-12;
.p-accordionpanel {
@apply border-0 bg-transparent;
}
.p-accordionheader {
@apply bg-neutral-800/50 border-0 rounded-xl mt-2 hover:bg-neutral-700/50;
transition:
background-color 0.2s ease,
border-radius 0.5s ease;
}
/* When panel is expanded, adjust header border radius */
.p-accordionpanel-active {
.p-accordionheader {
@apply rounded-t-xl rounded-b-none;
}
}
.p-accordioncontent {
@apply bg-neutral-800/50 border-0 rounded-b-xl rounded-t-none;
}
.p-accordioncontent-content {
@apply bg-transparent pt-3 pr-5 pb-5 pl-5;
}
/* Override default chevron icons to use up/down */
.p-accordionheader-toggle-icon {
&::before {
content: '\e933';
}
}
.p-accordionpanel-active {
.p-accordionheader-toggle-icon {
&::before {
content: '\e902';
}
}
}
}
</style>

View File

@@ -0,0 +1,45 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import { ref } from 'vue'
import MigrationPicker from './MigrationPicker.vue'
const meta: Meta<typeof MigrationPicker> = {
title: 'Desktop/Components/MigrationPicker',
component: MigrationPicker,
parameters: {
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
{ name: 'neutral-900', value: '#171717' }
]
}
},
decorators: [
() => {
;(window as any).electronAPI = {
validateComfyUISource: () => Promise.resolve({ isValid: true }),
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
}
return { template: '<story />' }
}
]
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => ({
components: { MigrationPicker },
setup() {
const sourcePath = ref('')
const migrationItemIds = ref<string[]>([])
return { sourcePath, migrationItemIds }
},
template:
'<MigrationPicker v-model:sourcePath="sourcePath" v-model:migrationItemIds="migrationItemIds" />'
})
}

View File

@@ -2,10 +2,6 @@
<div class="flex flex-col gap-6 w-[600px]">
<!-- Source Location Section -->
<div class="flex flex-col gap-4">
<h2 class="text-2xl font-semibold text-neutral-100">
{{ $t('install.migrateFromExistingInstallation') }}
</h2>
<p class="text-neutral-400 my-0">
{{ $t('install.migrationSourcePathDescription') }}
</p>
@@ -13,7 +9,7 @@
<div class="flex gap-2">
<InputText
v-model="sourcePath"
placeholder="Select existing ComfyUI installation (optional)"
:placeholder="$t('install.locationPicker.migrationPathPlaceholder')"
class="flex-1"
:class="{ 'p-invalid': pathError }"
@update:model-value="validateSource"
@@ -27,10 +23,7 @@
</div>
<!-- Migration Options -->
<div
v-if="isValidSource"
class="flex flex-col gap-4 bg-neutral-800 p-4 rounded-lg"
>
<div v-if="isValidSource" class="flex flex-col gap-4 p-4 rounded-lg">
<h3 class="text-lg mt-0 font-medium text-neutral-100">
{{ $t('install.selectItemsToMigrate') }}
</h3>

View File

@@ -1,121 +0,0 @@
<template>
<Panel
:header="$t('install.settings.mirrorSettings')"
toggleable
:collapsed="!showMirrorInputs"
pt:root="bg-neutral-800 border-none w-[600px]"
>
<template
v-for="([item, modelValue], index) in mirrors"
:key="item.settingId + item.mirror"
>
<Divider v-if="index > 0" />
<MirrorItem
v-model="modelValue.value"
:item="item"
@state-change="validationStates[index] = $event"
/>
</template>
<template #icons>
<i
v-tooltip="validationStateTooltip"
:class="{
'pi pi-spin pi-spinner text-neutral-400':
validationState === ValidationState.LOADING,
'pi pi-check text-green-500':
validationState === ValidationState.VALID,
'pi pi-times text-red-500':
validationState === ValidationState.INVALID
}"
/>
</template>
</Panel>
</template>
<script setup lang="ts">
import {
TorchDeviceType,
TorchMirrorUrl
} from '@comfyorg/comfyui-electron-types'
import Divider from 'primevue/divider'
import Panel from 'primevue/panel'
import { ModelRef, computed, onMounted, ref } from 'vue'
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
import { t } from '@/i18n'
import { isInChina } from '@/utils/networkUtil'
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
const showMirrorInputs = ref(false)
const { device } = defineProps<{ device: TorchDeviceType | null }>()
const pythonMirror = defineModel<string>('pythonMirror', { required: true })
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
const torchMirror = defineModel<string>('torchMirror', { required: true })
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
switch (device) {
case 'mps':
return {
settingId,
mirror: TorchMirrorUrl.NightlyCpu,
fallbackMirror: TorchMirrorUrl.NightlyCpu
}
case 'nvidia':
return {
settingId,
mirror: TorchMirrorUrl.Cuda,
fallbackMirror: TorchMirrorUrl.Cuda
}
case 'cpu':
default:
return {
settingId,
mirror: PYPI_MIRROR.mirror,
fallbackMirror: PYPI_MIRROR.fallbackMirror
}
}
}
const userIsInChina = ref(false)
onMounted(async () => {
userIsInChina.value = await isInChina()
})
const useFallbackMirror = (mirror: UVMirror) => ({
...mirror,
mirror: mirror.fallbackMirror
})
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() =>
(
[
[PYTHON_MIRROR, pythonMirror],
[PYPI_MIRROR, pypiMirror],
[getTorchMirrorItem(device ?? 'cpu'), torchMirror]
] as [UVMirror, ModelRef<string>][]
).map(([item, modelValue]) => [
userIsInChina.value ? useFallbackMirror(item) : item,
modelValue
])
)
const validationStates = ref<ValidationState[]>(
mirrors.value.map(() => ValidationState.IDLE)
)
const validationState = computed(() => {
return mergeValidationStates(validationStates.value)
})
const validationStateTooltip = computed(() => {
switch (validationState.value) {
case ValidationState.INVALID:
return t('install.settings.mirrorsUnreachable')
case ValidationState.VALID:
return t('install.settings.mirrorsReachable')
default:
return t('install.settings.checkingMirrors')
}
})
</script>

View File

@@ -1,10 +1,10 @@
<template>
<div class="flex flex-col items-center gap-4">
<div class="w-full">
<h3 class="text-lg font-medium text-neutral-100">
<div class="flex flex-col gap-4 text-neutral-400 text-sm">
<div>
<h3 class="text-lg font-medium text-neutral-100 mb-3 mt-0">
{{ $t(`settings.${normalizedSettingId}.name`) }}
</h3>
<p class="text-sm text-neutral-400 mt-1">
<p class="my-1">
{{ $t(`settings.${normalizedSettingId}.tooltip`) }}
</p>
</div>
@@ -16,18 +16,61 @@
"
@state-change="validationState = $event"
/>
<div v-if="secondParagraph" class="mt-2">
<a href="#" @click.prevent="showDialog = true">
{{ $t('g.learnMore') }}
</a>
<Dialog
v-model:visible="showDialog"
modal
dismissable-mask
:header="$t(`settings.${normalizedSettingId}.urlFormatTitle`)"
class="select-none max-w-3xl"
>
<div class="text-neutral-300">
<p class="mt-1 whitespace-pre-wrap">{{ secondParagraph }}</p>
<div class="mt-2 break-all">
<span class="text-neutral-300 font-semibold">
{{ EXAMPLE_URL_FIRST_PART }}
</span>
<span>{{ EXAMPLE_URL_SECOND_PART }}</span>
</div>
<Divider />
<p>
{{ $t(`settings.${normalizedSettingId}.fileUrlDescription`) }}
</p>
<span class="text-neutral-300 font-semibold">
{{ FILE_URL_SCHEME }}
</span>
<span>
{{ EXAMPLE_FILE_URL }}
</span>
</div>
</Dialog>
</div>
</div>
</template>
<script setup lang="ts">
import Dialog from 'primevue/dialog'
import Divider from 'primevue/divider'
import { computed, onMounted, ref, watch } from 'vue'
import UrlInput from '@/components/common/UrlInput.vue'
import { UVMirror } from '@/constants/uvMirrors'
import { st } from '@/i18n'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { checkMirrorReachable } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'
const FILE_URL_SCHEME = 'file://'
const EXAMPLE_FILE_URL = '/C:/MyPythonInstallers/'
const EXAMPLE_URL_FIRST_PART =
'https://github.com/astral-sh/python-build-standalone/releases/download'
const EXAMPLE_URL_SECOND_PART =
'/20250902/cpython-3.12.11+20250902-x86_64-pc-windows-msvc-install_only.tar.gz'
const { item } = defineProps<{
item: UVMirror
}>()
@@ -38,11 +81,16 @@ const emit = defineEmits<{
const modelValue = defineModel<string>('modelValue', { required: true })
const validationState = ref<ValidationState>(ValidationState.IDLE)
const showDialog = ref(false)
const normalizedSettingId = computed(() => {
return normalizeI18nKey(item.settingId)
})
const secondParagraph = computed(() =>
st(`settings.${normalizedSettingId.value}.urlDescription`, '')
)
onMounted(() => {
modelValue.value = item.mirror
})

View File

@@ -1,7 +1,7 @@
<template>
<div
ref="workflowTabRef"
class="flex p-2 gap-2 workflow-tab"
class="flex p-2 gap-2 workflow-tab group"
v-bind="$attrs"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@@ -11,9 +11,13 @@
{{ workflowOption.workflow.filename }}
</span>
<div class="relative">
<span v-if="shouldShowStatusIndicator" class="status-indicator"></span>
<span
v-if="shouldShowStatusIndicator"
class="group-hover:hidden absolute font-bold text-2xl top-1/2 left-1/2 -translate-1/2 z-10 bg-(--comfy-menu-secondary-bg) w-4"
></span
>
<Button
class="close-button p-0 w-auto"
class="close-button p-0 w-auto invisible"
icon="pi pi-times"
text
severity="secondary"
@@ -174,18 +178,6 @@ onUnmounted(() => {
})
</script>
<style scoped>
@reference '../../assets/css/style.css';
.status-indicator {
@apply absolute font-bold;
font-size: 1.5rem;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<style>
.p-tooltip.workflow-tab-tooltip {
z-index: 1200 !important;

View File

@@ -360,14 +360,6 @@ onUpdated(() => {
@apply visible;
}
:deep(.p-togglebutton:hover) .status-indicator {
@apply hidden;
}
:deep(.p-togglebutton) .close-button {
@apply invisible;
}
:deep(.p-scrollpanel-content) {
@apply h-full;
}

View File

@@ -8,7 +8,8 @@ export function useTerminal(element: Ref<HTMLElement | undefined>) {
const fitAddon = new FitAddon()
const terminal = markRaw(
new Terminal({
convertEol: true
convertEol: true,
theme: { background: '#171717' }
})
)
terminal.loadAddon(fitAddon)

View File

@@ -169,6 +169,74 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
: `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run`
}
// ---- constants ----
const SORA_SIZES = {
BASIC: new Set(['720x1280', '1280x720']),
PRO: new Set(['1024x1792', '1792x1024'])
}
const ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO])
// ---- sora-2 pricing helpers ----
function validateSora2Selection(
modelRaw: string,
duration: number,
sizeRaw: string
): string | undefined {
const model = modelRaw?.toLowerCase() ?? ''
const size = sizeRaw?.toLowerCase() ?? ''
if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)'
if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
if (!ALL_SIZES.has(size))
return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
if (model.includes('sora-2-pro')) return undefined
if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size))
return 'sora-2 supports only 720x1280 or 1280x720'
if (!model.includes('sora-2')) return 'Unsupported model'
return undefined
}
function perSecForSora2(modelRaw: string, sizeRaw: string): number {
const model = modelRaw?.toLowerCase() ?? ''
const size = sizeRaw?.toLowerCase() ?? ''
if (model.includes('sora-2-pro')) {
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3
}
if (model.includes('sora-2')) return 0.1
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1
}
function formatRunPrice(perSec: number, duration: number) {
return `$${(perSec * duration).toFixed(2)}/Run`
}
// ---- pricing calculator ----
const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {
const getWidgetValue = (name: string) =>
String(node.widgets?.find((w) => w.name === name)?.value ?? '')
const model = getWidgetValue('model')
const size = getWidgetValue('size')
const duration = Number(
node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name))
?.value
)
if (!model || !size || !duration) return 'Set model, duration & size'
const validationError = validateSora2Selection(model, duration, size)
if (validationError) return validationError
const perSec = perSecForSora2(model, size)
return formatRunPrice(perSec, duration)
}
/**
* Static pricing data for API nodes, now supporting both strings and functions
*/
@@ -195,6 +263,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
FluxProKontextMaxNode: {
displayPrice: '$0.08/Run'
},
OpenAIVideoSora2: {
displayPrice: sora2PricingCalculator
},
IdeogramV1: {
displayPrice: (node: LGraphNode): string => {
const numImagesWidget = node.widgets?.find(
@@ -1548,6 +1619,74 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
},
ByteDanceImageReferenceNode: {
displayPrice: byteDanceVideoPricingCalculator
},
WanTextToVideoApi: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
const resolutionWidget = node.widgets?.find(
(w) => w.name === 'size'
) as IComboWidget
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
const seconds = parseFloat(String(durationWidget.value))
const resolutionStr = String(resolutionWidget.value).toLowerCase()
const resKey = resolutionStr.includes('1080')
? '1080p'
: resolutionStr.includes('720')
? '720p'
: resolutionStr.includes('480')
? '480p'
: resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? ''
const pricePerSecond: Record<string, number> = {
'480p': 0.05,
'720p': 0.1,
'1080p': 0.15
}
const pps = pricePerSecond[resKey]
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
const cost = (pps * seconds).toFixed(2)
return `$${cost}/Run`
}
},
WanImageToVideoApi: {
displayPrice: (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
const resolutionWidget = node.widgets?.find(
(w) => w.name === 'resolution'
) as IComboWidget
if (!durationWidget || !resolutionWidget) return '$0.05-0.15/second'
const seconds = parseFloat(String(durationWidget.value))
const resolution = String(resolutionWidget.value).trim().toLowerCase()
const pricePerSecond: Record<string, number> = {
'480p': 0.05,
'720p': 0.1,
'1080p': 0.15
}
const pps = pricePerSecond[resolution]
if (isNaN(seconds) || !pps) return '$0.05-0.15/second'
const cost = (pps * seconds).toFixed(2)
return `$${cost}/Run`
}
},
WanTextToImageApi: {
displayPrice: '$0.03/Run'
},
WanImageToImageApi: {
displayPrice: '$0.03/Run'
}
}
@@ -1589,6 +1728,7 @@ export const useNodePricing = () => {
MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
OpenAIVideoSora2: ['model', 'size', 'duration'],
OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images', 'turbo'],
IdeogramV2: ['num_images', 'turbo'],
@@ -1647,7 +1787,9 @@ export const useNodePricing = () => {
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution']
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
WanTextToVideoApi: ['duration', 'size'],
WanImageToVideoApi: ['duration', 'resolution']
}
return widgetMap[nodeType] || []
}

View File

@@ -42,7 +42,12 @@ export function useManagerState() {
)
// Check command line args first (highest priority)
if (systemStats.value?.system?.argv?.includes('--disable-manager')) {
// --enable-manager flag enables the manager (opposite of old --disable-manager)
const hasEnableManager =
systemStats.value?.system?.argv?.includes('--enable-manager')
// If --enable-manager is NOT present, manager is disabled
if (!hasEnableManager) {
return ManagerUIState.DISABLED
}

View File

@@ -0,0 +1,75 @@
export interface DialogAction {
readonly label: string
readonly action: 'openUrl' | 'close' | 'cancel'
readonly url?: string
readonly severity?: 'danger' | 'primary' | 'secondary' | 'warn'
readonly returnValue: string
}
interface DesktopDialog {
readonly title: string
readonly message: string
readonly buttons: DialogAction[]
}
export const DESKTOP_DIALOGS = {
/** Shown when a corrupt venv is detected. */
reinstallVenv: {
title: 'Reinstall ComfyUI (Fresh Start)?',
message: `Sorry, we can't launch ComfyUI because some installed packages aren't compatible.
Click Reinstall to restore ComfyUI and get back up and running.
Please note: if you've added custom nodes, you'll need to reinstall them after this process.`,
buttons: [
{
label: 'Learn More',
action: 'openUrl',
url: 'https://docs.comfy.org',
returnValue: 'openDocs'
},
{
label: 'Reinstall',
action: 'close',
severity: 'danger',
returnValue: 'resetVenv'
}
]
},
/** A dialog that is shown when an invalid dialog ID is provided. */
invalidDialog: {
title: 'Invalid Dialog',
message: `Invalid dialog ID was provided.`,
buttons: [
{
label: 'Close',
action: 'cancel',
returnValue: 'cancel'
}
]
}
} as const satisfies { [K: string]: DesktopDialog }
/** The ID of a desktop dialog. */
type DesktopDialogId = keyof typeof DESKTOP_DIALOGS
/**
* Checks if {@link id} is a valid dialog ID.
* @param id The string to check
* @returns `true` if the ID is a valid dialog ID, otherwise `false`
*/
function isDialogId(id: unknown): id is DesktopDialogId {
return typeof id === 'string' && id in DESKTOP_DIALOGS
}
/**
* Gets the dialog with the given ID.
* @param dialogId The ID of the dialog to get
* @returns The dialog with the given ID
*/
export function getDialog(
dialogId: string | string[]
): DesktopDialog & { id: DesktopDialogId } {
const id = isDialogId(dialogId) ? dialogId : 'invalidDialog'
return { id, ...structuredClone(DESKTOP_DIALOGS[id]) }
}

View File

@@ -4,11 +4,14 @@ import { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink, type ResolvedConnection } from '@/lib/litegraph/src/LLink'
import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError'
import type { ISubgraphInput } from '@/lib/litegraph/src/interfaces'
import {
type INodeInputSlot,
type ISlotType,
type NodeId
import type {
ISubgraphInput,
IWidgetLocator
} from '@/lib/litegraph/src/interfaces'
import type {
INodeInputSlot,
ISlotType,
NodeId
} from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot'
@@ -78,9 +81,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const existingInput = this.inputs.find((i) => i.name == name)
if (existingInput) {
const linkId = subgraphInput.linkIds[0]
const { inputNode } = subgraph.links[linkId].resolve(subgraph)
const { inputNode, input } = subgraph.links[linkId].resolve(subgraph)
const widget = inputNode?.widgets?.find?.((w) => w.name == name)
if (widget) this.#setWidget(subgraphInput, existingInput, widget)
if (widget)
this.#setWidget(subgraphInput, existingInput, widget, input?.widget)
return
}
const input = this.addInput(name, type)
@@ -185,13 +189,14 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
subgraphInput.events.addEventListener(
'input-connected',
() => {
(e) => {
if (input._widget) return
const widget = subgraphInput._widget
if (!widget) return
this.#setWidget(subgraphInput, input, widget)
const widgetLocator = e.detail.input.widget
this.#setWidget(subgraphInput, input, widget, widgetLocator)
},
{ signal }
)
@@ -301,7 +306,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const widget = resolved.inputNode.getWidgetFromSlot(resolved.input)
if (!widget) continue
this.#setWidget(subgraphInput, input, widget)
this.#setWidget(subgraphInput, input, widget, resolved.input.widget)
break
}
}
@@ -310,11 +315,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
#setWidget(
subgraphInput: Readonly<SubgraphInput>,
input: INodeInputSlot,
widget: Readonly<IBaseWidget>
widget: Readonly<IBaseWidget>,
inputWidget: IWidgetLocator | undefined
) {
// Use the first matching widget
const targetWidget = toConcreteWidget(widget, this)
const promotedWidget = targetWidget.createCopyForNode(this)
const promotedWidget = toConcreteWidget(widget, this).createCopyForNode(
this
)
Object.assign(promotedWidget, {
get name() {
@@ -372,11 +379,9 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
// NOTE: This code creates linked chains of prototypes for passing across
// multiple levels of subgraphs. As part of this, it intentionally avoids
// creating new objects. Have care when making changes.
const backingInput =
targetWidget.node.findInputSlot(widget.name, true)?.widget ?? {}
input.widget ??= { name: subgraphInput.name }
input.widget.name = subgraphInput.name
Object.setPrototypeOf(input.widget, backingInput)
if (inputWidget) Object.setPrototypeOf(input.widget, inputWidget)
input._widget = promotedWidget
}

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "تصدير سير العمل (تنسيق API)"
},
"Comfy_Feedback": {
"label": "إرسال ملاحظات"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "تحويل التحديد إلى رسم فرعي"
},
@@ -170,9 +167,6 @@
"Comfy_LoadDefaultWorkflow": {
"label": "تحميل سير العمل الافتراضي"
},
"Comfy_Manager_CustomNodesManager": {
"label": "تبديل مدير العقد المخصصة"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "تبديل شريط تقدم مدير العقد المخصصة"
},
@@ -291,4 +285,4 @@
"label": "تبديل الشريط الجانبي لسير العمل",
"tooltip": "سير العمل"
}
}
}

View File

@@ -325,7 +325,6 @@
"frontendNewer": "إصدار الواجهة الأمامية {frontendVersion} قد لا يكون متوافقاً مع الإصدار الخلفي {backendVersion}.",
"frontendOutdated": "إصدار الواجهة الأمامية {frontendVersion} قديم. يتطلب الإصدار الخلفي {requiredVersion} أو أحدث.",
"goToNode": "الانتقال إلى العقدة",
"help": "مساعدة",
"icon": "أيقونة",
"imageFailedToLoad": "فشل تحميل الصورة",
"imageUrl": "رابط الصورة",
@@ -778,7 +777,6 @@
"File": "ملف",
"Fit Group To Contents": "ملائمة المجموعة للمحتويات",
"Focus Mode": "وضع التركيز",
"Give Feedback": "تقديم ملاحظات",
"Group Selected Nodes": "تجميع العقد المحددة",
"Help": "مساعدة",
"Help Center": "مركز المساعدة",
@@ -837,7 +835,6 @@
"Toggle Terminal Bottom Panel": "تبديل لوحة الطرفية السفلية",
"Toggle Theme (Dark/Light)": "تبديل السمة (داكن/فاتح)",
"Toggle View Controls Bottom Panel": "تبديل لوحة عناصر التحكم في العرض السفلية",
"Toggle the Custom Nodes Manager": "تبديل مدير العقد المخصصة",
"Toggle the Custom Nodes Manager Progress Bar": "تبديل شريط تقدم مدير العقد المخصصة",
"Undo": "تراجع",
"Ungroup selected group nodes": "فك تجميع عقد المجموعة المحددة",
@@ -931,9 +928,6 @@
"upscale_diffusion": "انتشار التكبير",
"upscaling": "تكبير",
"utils": "أدوات مساعدة",
"v1": "الإصدار 1",
"v2": "الإصدار 2",
"v3": "الإصدار 3",
"video": "فيديو",
"video_models": "نماذج الفيديو"
},
@@ -1693,4 +1687,4 @@
"showMinimap": "إظهار الخريطة المصغرة",
"zoomToFit": "تكبير لتناسب الشاشة"
}
}
}

View File

@@ -7366,19 +7366,6 @@
}
}
},
"SaveSVG": {
"description": "يحفظ ملفات SVG على القرص.",
"display_name": "حفظ SVG",
"inputs": {
"filename_prefix": {
"name": "بادئة اسم الملف",
"tooltip": "بادئة اسم الملف للحفظ. يمكن أن تتضمن معلومات تنسيق مثل %date:yyyy-MM-dd% أو %Empty Latent Image.width% لاستخدام قيم من العقد."
},
"svg": {
"name": "ملف SVG"
}
}
},
"SaveVideo": {
"description": "يحفظ الصور المدخلة في مجلد مخرجات ComfyUI الخاص بك.",
"display_name": "حفظ الفيديو",
@@ -8657,4 +8644,4 @@
}
}
}
}
}

View File

@@ -388,10 +388,6 @@
"Topbar (2nd-row)": "شريط الأعلى (الصف الثاني)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "عتبة التكبير للرسم بجودة منخفضة",
"tooltip": "عرض أشكال بجودة منخفضة عند التكبير للخارج"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "الحد الأقصى للإطارات في الثانية",
"tooltip": "الحد الأقصى لعدد الإطارات في الثانية التي يسمح للرسم أن يعرضها. يحد من استخدام GPU على حساب السلاسة. إذا كانت 0، يتم استخدام معدل تحديث الشاشة. الافتراضي: 0"
@@ -413,4 +409,4 @@
"pysssss_SnapToGrid": {
"name": "الالتصاق بالشبكة دائمًا"
}
}
}

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "Export Workflow (API Format)"
},
"Comfy_Feedback": {
"label": "Give Feedback"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "Convert Selection to Subgraph"
},
@@ -260,6 +257,9 @@
"Comfy_User_SignOut": {
"label": "Sign Out"
},
"Experimental_ToggleVueNodes": {
"label": "Experimental: Enable Vue Nodes"
},
"Workspace_CloseWorkflow": {
"label": "Close Current Workflow"
},

View File

@@ -18,6 +18,7 @@
"calculatingDimensions": "Calculating dimensions",
"import": "Import",
"loadAllFolders": "Load All Folders",
"logoAlt": "ComfyUI Logo",
"refresh": "Refresh",
"refreshNode": "Refresh Node",
"terminal": "Terminal",
@@ -406,6 +407,27 @@
"migration": "Migration",
"desktopSettings": "Desktop Settings",
"chooseInstallationLocation": "Choose Installation Location",
"gpuPicker": {
"title": "Choose your hardware setup",
"recommended": "RECOMMENDED",
"nvidiaSubtitle": "NVIDIA CUDA",
"cpuSubtitle": "CPU Mode",
"manualSubtitle": "Manual Setup",
"appleMetalDescription": "Leverages your Mac's GPU for faster speed and a better overall experience",
"nvidiaDescription": "Use your NVIDIA GPU with CUDA acceleration for the best performance.",
"cpuDescription": "Use CPU mode for compatibility when GPU acceleration is not available",
"manualDescription": "Configure ComfyUI manually for advanced setups or unsupported hardware"
},
"locationPicker": {
"title": "Choose where to install ComfyUI",
"subtitle": "Pick a folder for ComfyUI's files. We'll also set up Python there automatically.",
"pathPlaceholder": "/Users/username/Documents/ComfyUI",
"migrationPathPlaceholder": "Select existing ComfyUI installation (optional)",
"migrateFromExisting": "Migrate from existing installation",
"migrateDescription": "Copy or link your existing models, custom nodes, and configurations from a previous ComfyUI installation.",
"chooseDownloadServers": "Choose download servers manually",
"downloadServersDescription": "Select specific mirror servers for downloading Python, PyPI packages, and PyTorch based on your location."
},
"systemLocations": "System Locations",
"failedToSelectDirectory": "Failed to select directory",
"pathValidationFailed": "Failed to validate path",
@@ -490,18 +512,26 @@
"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."
},
"desktopStart": {
"initialising": "Initialising..."
},
"serverStart": {
"title": "Starting ComfyUI",
"troubleshoot": "Troubleshoot",
"reportIssue": "Report Issue",
"openLogs": "Open Logs",
"showTerminal": "Show Terminal",
"copySelectionTooltip": "Copy selection",
"copyAllTooltip": "Copy all",
"errorMessage": "Unable to start ComfyUI Desktop",
"installation": {
"title": "Installing ComfyUI"
},
"process": {
"initial-state": "Loading...",
"python-setup": "Setting up Python Environment...",
"starting-server": "Starting ComfyUI server...",
"ready": "Finishing...",
"ready": "Loading Human Interface",
"error": "Unable to start ComfyUI Desktop"
}
},
@@ -1073,7 +1103,7 @@
"queue": "Queue Panel"
},
"menuLabels": {
"Workflow": "Workflow",
"File": "File",
"Edit": "Edit",
"View": "View",
"Help": "Help",
@@ -1092,7 +1122,6 @@
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
"Browse Templates": "Browse Templates",
"Delete Selected Items": "Delete Selected Items",
"Fit view to selected nodes": "Fit view to selected nodes",
"Zoom to fit": "Zoom to fit",
"Lock Canvas": "Lock Canvas",
"Move Selected Nodes Down": "Move Selected Nodes Down",
@@ -1101,8 +1130,9 @@
"Move Selected Nodes Up": "Move Selected Nodes Up",
"Reset View": "Reset View",
"Resize Selected Nodes": "Resize Selected Nodes",
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
"Node Links": "Node Links",
"Canvas Toggle Lock": "Canvas Toggle Lock",
"Minimap": "Minimap",
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
@@ -1118,7 +1148,6 @@
"Duplicate Current Workflow": "Duplicate Current Workflow",
"Export": "Export",
"Export (API)": "Export (API)",
"Give Feedback": "Give Feedback",
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
"Exit Subgraph": "Exit Subgraph",
"Fit Group To Contents": "Fit Group To Contents",
@@ -1137,10 +1166,11 @@
"Custom Nodes Manager": "Custom Nodes Manager",
"Custom Nodes (Legacy)": "Custom Nodes (Legacy)",
"Manager Menu (Legacy)": "Manager Menu (Legacy)",
"Install Missing": "Install Missing",
"Install Missing Custom Nodes": "Install Missing Custom Nodes",
"Check for Custom Node Updates": "Check for Custom Node Updates",
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
"Decrease Brush Size in MaskEditor": "Decrease Brush Size in MaskEditor",
"Increase Brush Size in MaskEditor": "Increase Brush Size in MaskEditor",
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
"Unload Models": "Unload Models",
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
@@ -1163,31 +1193,22 @@
"Undo": "Undo",
"Open Sign In Dialog": "Open Sign In Dialog",
"Sign Out": "Sign Out",
"Experimental: Enable Vue Nodes": "Experimental: Enable Vue Nodes",
"Close Current Workflow": "Close Current Workflow",
"Next Opened Workflow": "Next Opened Workflow",
"Previous Opened Workflow": "Previous Opened Workflow",
"Toggle Search Box": "Toggle Search Box",
"Bottom Panel": "Bottom Panel",
"Toggle Bottom Panel": "Toggle Bottom Panel",
"Show Keybindings Dialog": "Show Keybindings Dialog",
"Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel",
"Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel",
"Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel",
"Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel",
"Toggle Focus Mode": "Toggle Focus Mode",
"Focus Mode": "Focus Mode",
"Model Library": "Model Library",
"Node Library": "Node Library",
"Queue Panel": "Queue Panel",
"Workflows": "Workflows",
"Toggle Model Library Sidebar": "Toggle Model Library Sidebar",
"Toggle Node Library Sidebar": "Toggle Node Library Sidebar",
"Toggle Queue Sidebar": "Toggle Queue Sidebar",
"Toggle Workflows Sidebar": "Toggle Workflows Sidebar",
"sideToolbar_modelLibrary": "sideToolbar.modelLibrary",
"sideToolbar_nodeLibrary": "sideToolbar.nodeLibrary",
"sideToolbar_queue": "sideToolbar.queue",
"sideToolbar_workflows": "sideToolbar.workflows"
"Workflows": "Workflows"
},
"desktopMenu": {
"reinstall": "Reinstall",
@@ -1248,7 +1269,9 @@
"API Nodes": "API Nodes",
"Notification Preferences": "Notification Preferences",
"3DViewer": "3DViewer",
"Vue Nodes": "Vue Nodes"
"Vue Nodes": "Vue Nodes",
"Assets": "Assets",
"Canvas Navigation": "Canvas Navigation"
},
"serverConfigItems": {
"listen": {
@@ -1385,42 +1408,49 @@
"noise": "noise",
"sampling": "sampling",
"schedulers": "schedulers",
"audio": "audio",
"conditioning": "conditioning",
"loaders": "loaders",
"guiders": "guiders",
"api node": "api node",
"video": "video",
"ByteDance": "ByteDance",
"image": "image",
"preprocessors": "preprocessors",
"utils": "utils",
"string": "string",
"advanced": "advanced",
"guidance": "guidance",
"loaders": "loaders",
"model_merging": "model_merging",
"model_patches": "model_patches",
"chroma_radiance": "chroma_radiance",
"attention_experiments": "attention_experiments",
"conditioning": "conditioning",
"flux": "flux",
"hooks": "hooks",
"combine": "combine",
"cond single": "cond single",
"context": "context",
"controlnet": "controlnet",
"inpaint": "inpaint",
"scheduling": "scheduling",
"create": "create",
"video": "video",
"mask": "mask",
"deprecated": "deprecated",
"debug": "debug",
"model": "model",
"latent": "latent",
"audio": "audio",
"3d": "3d",
"ltxv": "ltxv",
"sd3": "sd3",
"sigmas": "sigmas",
"api node": "api node",
"BFL": "BFL",
"model_patches": "model_patches",
"unet": "unet",
"Gemini": "Gemini",
"text": "text",
"gligen": "gligen",
"video_models": "video_models",
"sd": "sd",
"Ideogram": "Ideogram",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"postprocessing": "postprocessing",
"transform": "transform",
"batch": "batch",
@@ -1430,34 +1460,44 @@
"Kling": "Kling",
"samplers": "samplers",
"operations": "operations",
"training": "training",
"lotus": "lotus",
"Luma": "Luma",
"MiniMax": "MiniMax",
"debug": "debug",
"model": "model",
"model_specific": "model_specific",
"Moonvalley Marey": "Moonvalley Marey",
"OpenAI": "OpenAI",
"cond pair": "cond pair",
"photomaker": "photomaker",
"Pika": "Pika",
"PixVerse": "PixVerse",
"utils": "utils",
"primitive": "primitive",
"qwen": "qwen",
"Recraft": "Recraft",
"edit_models": "edit_models",
"Rodin": "Rodin",
"Runway": "Runway",
"animation": "animation",
"api": "api",
"save": "save",
"upscale_diffusion": "upscale_diffusion",
"clip": "clip",
"Stability AI": "Stability AI",
"stable_cascade": "stable_cascade",
"3d_models": "3d_models",
"style_model": "style_model",
"sd": "sd",
"Veo": "Veo"
"Tripo": "Tripo",
"Veo": "Veo",
"Vidu": "Vidu",
"camera": "camera",
"Wan": "Wan"
},
"dataTypes": {
"*": "*",
"AUDIO": "AUDIO",
"AUDIO_ENCODER": "AUDIO_ENCODER",
"AUDIO_ENCODER_OUTPUT": "AUDIO_ENCODER_OUTPUT",
"AUDIO_RECORD": "AUDIO_RECORD",
"BOOLEAN": "BOOLEAN",
"CAMERA_CONTROL": "CAMERA_CONTROL",
"CLIP": "CLIP",
@@ -1468,6 +1508,7 @@
"CONTROL_NET": "CONTROL_NET",
"FLOAT": "FLOAT",
"FLOATS": "FLOATS",
"GEMINI_INPUT_FILES": "GEMINI_INPUT_FILES",
"GLIGEN": "GLIGEN",
"GUIDER": "GUIDER",
"HOOK_KEYFRAMES": "HOOK_KEYFRAMES",
@@ -1479,17 +1520,25 @@
"LOAD_3D": "LOAD_3D",
"LOAD_3D_ANIMATION": "LOAD_3D_ANIMATION",
"LOAD3D_CAMERA": "LOAD3D_CAMERA",
"LORA_MODEL": "LORA_MODEL",
"LOSS_MAP": "LOSS_MAP",
"LUMA_CONCEPTS": "LUMA_CONCEPTS",
"LUMA_REF": "LUMA_REF",
"MASK": "MASK",
"MESH": "MESH",
"MODEL": "MODEL",
"MODEL_PATCH": "MODEL_PATCH",
"MODEL_TASK_ID": "MODEL_TASK_ID",
"NOISE": "NOISE",
"OPENAI_CHAT_CONFIG": "OPENAI_CHAT_CONFIG",
"OPENAI_INPUT_FILES": "OPENAI_INPUT_FILES",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "PIXVERSE_TEMPLATE",
"RECRAFT_COLOR": "RECRAFT_COLOR",
"RECRAFT_CONTROLS": "RECRAFT_CONTROLS",
"RECRAFT_V3_STYLE": "RECRAFT_V3_STYLE",
"RETARGET_TASK_ID": "RETARGET_TASK_ID",
"RIG_TASK_ID": "RIG_TASK_ID",
"SAMPLER": "SAMPLER",
"SIGMAS": "SIGMAS",
"STRING": "STRING",
@@ -1500,6 +1549,7 @@
"VAE": "VAE",
"VIDEO": "VIDEO",
"VOXEL": "VOXEL",
"WAN_CAMERA_EMBEDDING": "WAN_CAMERA_EMBEDDING",
"WEBCAM": "WEBCAM"
},
"maintenance": {
@@ -1862,5 +1912,33 @@
"showGroups": "Show Frames/Groups",
"renderBypassState": "Render Bypass State",
"renderErrorState": "Render Error State"
},
"assetBrowser": {
"assets": "Assets",
"browseAssets": "Browse Assets",
"noAssetsFound": "No assets found",
"tryAdjustingFilters": "Try adjusting your search or filters",
"loadingModels": "Loading {type}...",
"connectionError": "Please check your connection and try again",
"noModelsInFolder": "No {type} available in this folder",
"searchAssetsPlaceholder": "Search assets...",
"allModels": "All Models",
"unknown": "Unknown",
"fileFormats": "File formats",
"baseModels": "Base models",
"sortBy": "Sort by",
"sortAZ": "A-Z",
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular"
},
"desktopDialogs": {
"": {
"title": "Invalid Dialog",
"message": "Invalid dialog ID was provided.",
"buttons": {
"Close": "Close"
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -25,15 +25,34 @@
"custom": "custom"
}
},
"Comfy_Assets_UseAssetAPI": {
"name": "Use Asset API for model library",
"tooltip": "Use new Asset API for model browsing"
},
"Comfy_Canvas_BackgroundImage": {
"name": "Canvas background image",
"tooltip": "Image URL for the canvas background. You can right-click an image in the outputs panel and select \"Set as Background\" to use it, or upload your own image using the upload button."
},
"Comfy_Canvas_LeftMouseClickBehavior": {
"name": "Left Mouse Click Behavior",
"options": {
"Panning": "Panning",
"Select": "Select"
}
},
"Comfy_Canvas_MouseWheelScroll": {
"name": "Mouse Wheel Scroll",
"options": {
"Panning": "Panning",
"Zoom in/out": "Zoom in/out"
}
},
"Comfy_Canvas_NavigationMode": {
"name": "Navigation Mode",
"options": {
"Standard (New)": "Standard (New)",
"Drag Navigation": "Drag Navigation"
"Drag Navigation": "Drag Navigation",
"Custom": "Custom"
}
},
"Comfy_Canvas_SelectionToolbox": {
@@ -343,14 +362,6 @@
"Comfy_Validation_Workflows": {
"name": "Validate workflows"
},
"Comfy_VueNodes_Enabled": {
"name": "Enable Vue node rendering",
"tooltip": "Render nodes as Vue components instead of canvas elements. Experimental feature."
},
"Comfy_VueNodes_Widgets": {
"name": "Enable Vue widgets",
"tooltip": "Render widgets as Vue components within Vue nodes."
},
"Comfy_WidgetControlMode": {
"name": "Widget control mode",
"tooltip": "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
@@ -388,6 +399,9 @@
"Comfy_Workflow_SortNodeIdOnSave": {
"name": "Sort node IDs when saving workflow"
},
"Comfy_Workflow_WarnBlueprintOverwrite": {
"name": "Require confirmation to overwrite an existing subgraph blueprint"
},
"Comfy_Workflow_WorkflowTabsPosition": {
"name": "Opened workflows position",
"options": {
@@ -396,14 +410,14 @@
"Topbar (2nd-row)": "Topbar (2nd-row)"
}
},
"LiteGraph_Canvas_MinFontSizeForLOD": {
"name": "Zoom Node Level of Detail - font size threshold",
"tooltip": "Controls when the nodes switch to low quality LOD rendering. Uses font size in pixels to determine when to switch. Set to 0 to disable. Values 1-24 set the minimum font size threshold for LOD - higher values (24px) = switch nodes to simplified rendering sooner when zooming out, lower values (1px) = maintain full node quality longer."
},
"LiteGraph_Canvas_MaximumFps": {
"name": "Maximum FPS",
"tooltip": "The maximum frames per second that the canvas is allowed to render. Caps GPU usage at the cost of smoothness. If 0, the screen refresh rate is used. Default: 0"
},
"LiteGraph_Canvas_MinFontSizeForLOD": {
"name": "Zoom Node Level of Detail - font size threshold",
"tooltip": "Controls when the nodes switch to low quality LOD rendering. Uses font size in pixels to determine when to switch. Set to 0 to disable. Values 1-24 set the minimum font size threshold for LOD - higher values (24px) = switch nodes to simplified rendering sooner when zooming out, lower values (1px) = maintain full node quality longer."
},
"LiteGraph_ContextMenu_Scaling": {
"name": "Scale node combo widget menus (lists) when zoomed in"
},

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "Exportar flujo de trabajo (formato API)"
},
"Comfy_Feedback": {
"label": "Dar retroalimentación"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "Convertir selección en subgrafo"
},
@@ -309,4 +306,4 @@
"label": "Alternar Barra Lateral de Flujos de Trabajo",
"tooltip": "Flujos de Trabajo"
}
}
}

View File

@@ -324,11 +324,9 @@
"feedback": "Retroalimentación",
"filter": "Filtrar",
"findIssues": "Encontrar problemas",
"firstTimeUIMessage": "Esta es la primera vez que usas la nueva interfaz. Elige \"Menú > Usar nuevo menú > Desactivado\" para restaurar la antigua interfaz.",
"frontendNewer": "La versión del frontend {frontendVersion} puede no ser compatible con la versión del backend {backendVersion}.",
"frontendOutdated": "La versión del frontend {frontendVersion} está desactualizada. El backend requiere la versión {requiredVersion} o superior.",
"goToNode": "Ir al nodo",
"help": "Ayuda",
"icon": "Icono",
"imageFailedToLoad": "Falló la carga de la imagen",
"imageUrl": "URL de la imagen",
@@ -733,9 +731,7 @@
"Bottom Panel": "Panel inferior",
"Browse Templates": "Explorar plantillas",
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
"Check for Custom Node Updates": "Buscar actualizaciones de nodos personalizados",
"Check for Updates": "Buscar actualizaciones",
"Clear Pending Tasks": "Borrar tareas pendientes",
@@ -760,8 +756,6 @@
"Export": "Exportar",
"Export (API)": "Exportar (API)",
"Fit Group To Contents": "Ajustar grupo a contenidos",
"Fit view to selected nodes": "Ajustar vista a los nodos seleccionados",
"Give Feedback": "Dar retroalimentación",
"Group Selected Nodes": "Agrupar nodos seleccionados",
"Help": "Ayuda",
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
@@ -808,18 +802,11 @@
"Show Settings Dialog": "Mostrar diálogo de configuración",
"Sign Out": "Cerrar sesión",
"Toggle Essential Bottom Panel": "Alternar panel inferior esencial",
"Toggle Bottom Panel": "Alternar panel inferior",
"Toggle Focus Mode": "Alternar modo de enfoque",
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
"Toggle Model Library Sidebar": "Alternar barra lateral de la biblioteca de modelos",
"Toggle Node Library Sidebar": "Alternar barra lateral de la biblioteca de nodos",
"Toggle Queue Sidebar": "Alternar barra lateral de la cola",
"Toggle Search Box": "Alternar caja de búsqueda",
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
"Toggle View Controls Bottom Panel": "Alternar panel inferior de controles de vista",
"Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo",
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
"Undo": "Deshacer",
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
@@ -828,7 +815,6 @@
"Unlock Canvas": "Desbloquear lienzo",
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
"View": "Ver",
"Workflow": "Flujo de trabajo",
"Workflows": "Flujos de trabajo",
"Zoom In": "Acercar",
"Zoom Out": "Alejar",
@@ -839,11 +825,7 @@
"renderBypassState": "Mostrar estado de omisión",
"renderErrorState": "Mostrar estado de error",
"showGroups": "Mostrar marcos/grupos",
"showLinks": "Mostrar enlaces",
"sideToolbar_modelLibrary": "sideToolbar.bibliotecaDeModelos",
"sideToolbar_nodeLibrary": "sideToolbar.bibliotecaDeNodos",
"sideToolbar_queue": "sideToolbar.cola",
"sideToolbar_workflows": "sideToolbar.flujosDeTrabajo"
"showLinks": "Mostrar enlaces"
},
"missingModelsDialog": {
"doNotAskAgain": "No mostrar esto de nuevo",
@@ -920,9 +902,6 @@
"upscale_diffusion": "difusión_de_escalado",
"upscaling": "escalado",
"utils": "utilidades",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "video",
"video_models": "modelos_de_video"
},

View File

@@ -7366,19 +7366,6 @@
}
}
},
"SaveSVG": {
"description": "Guardar archivos SVG en el disco.",
"display_name": "Guardar SVG",
"inputs": {
"filename_prefix": {
"name": "prefijo_de_archivo",
"tooltip": "El prefijo para el archivo a guardar. Esto puede incluir información de formato como %date:yyyy-MM-dd% o %Empty Latent Image.width% para incluir valores de los nodos."
},
"svg": {
"name": "svg"
}
}
},
"SaveVideo": {
"description": "Guarda las imágenes de entrada en tu directorio de salida de ComfyUI.",
"display_name": "Guardar video",
@@ -8657,4 +8644,4 @@
}
}
}
}
}

View File

@@ -343,14 +343,6 @@
"Comfy_Validation_Workflows": {
"name": "Validar flujos de trabajo"
},
"Comfy_VueNodes_Enabled": {
"name": "Habilitar renderizado de nodos Vue",
"tooltip": "Renderiza los nodos como componentes Vue en lugar de elementos canvas. Función experimental."
},
"Comfy_VueNodes_Widgets": {
"name": "Habilitar widgets de Vue",
"tooltip": "Renderiza los widgets como componentes de Vue dentro de los nodos de Vue."
},
"Comfy_WidgetControlMode": {
"name": "Modo de control del widget",
"options": {
@@ -396,10 +388,6 @@
"Topbar (2nd-row)": "Barra superior (2ª fila)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "Umbral de renderizado de baja calidad al hacer zoom",
"tooltip": "Renderiza formas de baja calidad cuando se aleja"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "FPS máximo",
"tooltip": "La cantidad máxima de cuadros por segundo que se permite renderizar en el lienzo. Limita el uso de la GPU a costa de la suavidad. Si es 0, se utiliza la tasa de refresco de la pantalla. Predeterminado: 0"
@@ -421,4 +409,4 @@
"pysssss_SnapToGrid": {
"name": "Siempre ajustar a la cuadrícula"
}
}
}

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "Exporter le flux de travail (format API)"
},
"Comfy_Feedback": {
"label": "Retour d'information"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "Convertir la sélection en sous-graphe"
},
@@ -309,4 +306,4 @@
"label": "Basculer la barre latérale des flux de travail",
"tooltip": "Flux de travail"
}
}
}

View File

@@ -324,11 +324,9 @@
"feedback": "Commentaires",
"filter": "Filtrer",
"findIssues": "Trouver des problèmes",
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
"frontendNewer": "La version du frontend {frontendVersion} peut ne pas être compatible avec la version du backend {backendVersion}.",
"frontendOutdated": "La version du frontend {frontendVersion} est obsolète. Le backend requiert la version {requiredVersion} ou supérieure.",
"goToNode": "Aller au nœud",
"help": "Aide",
"icon": "Icône",
"imageFailedToLoad": "Échec du chargement de l'image",
"imageUrl": "URL de l'image",
@@ -733,9 +731,7 @@
"Bottom Panel": "Panneau inférieur",
"Browse Templates": "Parcourir les modèles",
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
"Canvas Toggle Minimap": "Basculer la mini-carte du canevas",
"Check for Custom Node Updates": "Vérifier les mises à jour des nœuds personnalisés",
"Check for Updates": "Vérifier les mises à jour",
"Clear Pending Tasks": "Effacer les tâches en attente",
@@ -760,8 +756,6 @@
"Export": "Exporter",
"Export (API)": "Exporter (API)",
"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",
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
@@ -808,18 +802,11 @@
"Show Settings Dialog": "Afficher la boîte de dialogue des paramètres",
"Sign Out": "Se déconnecter",
"Toggle Essential Bottom Panel": "Basculer le panneau inférieur essentiel",
"Toggle Bottom Panel": "Basculer le panneau inférieur",
"Toggle Focus Mode": "Basculer le mode focus",
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
"Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles",
"Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds",
"Toggle Queue Sidebar": "Afficher/Masquer la barre latérale de la file dattente",
"Toggle Search Box": "Basculer la boîte de recherche",
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
"Toggle View Controls Bottom Panel": "Basculer le panneau inférieur des contrôles daffichage",
"Toggle Workflows Sidebar": "Afficher/Masquer la barre latérale des workflows",
"Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés",
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
"Undo": "Annuler",
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
@@ -828,7 +815,6 @@
"Unlock Canvas": "Déverrouiller le canevas",
"Unpack the selected Subgraph": "Décompresser le Subgraph sélectionné",
"View": "Afficher",
"Workflow": "Flux de travail",
"Workflows": "Flux de travail",
"Zoom In": "Zoom avant",
"Zoom Out": "Zoom arrière",
@@ -839,12 +825,7 @@
"renderBypassState": "Afficher l'état de contournement",
"renderErrorState": "Afficher l'état d'erreur",
"showGroups": "Afficher les cadres/groupes",
"showLinks": "Afficher les liens",
"Zoom Out": "Zoom arrière",
"sideToolbar_modelLibrary": "Bibliothèque de modèles",
"sideToolbar_nodeLibrary": "Bibliothèque de nœuds",
"sideToolbar_queue": "File d'attente",
"sideToolbar_workflows": "Flux de travail"
"showLinks": "Afficher les liens"
},
"missingModelsDialog": {
"doNotAskAgain": "Ne plus afficher ce message",
@@ -921,9 +902,6 @@
"upscale_diffusion": "diffusion_de_mise_à_l'échelle",
"upscaling": "mise_à_l'échelle",
"utils": "utilitaires",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "vidéo",
"video_models": "modèles_vidéo"
},

View File

@@ -7366,19 +7366,6 @@
}
}
},
"SaveSVG": {
"description": "Enregistrer les fichiers SVG sur le disque.",
"display_name": "Enregistrer SVG",
"inputs": {
"filename_prefix": {
"name": "préfixe_nom_fichier",
"tooltip": "Le préfixe pour le fichier à enregistrer. Cela peut inclure des informations de formatage telles que %date:yyyy-MM-dd% ou %Empty Latent Image.width% pour inclure des valeurs provenant des nœuds."
},
"svg": {
"name": "svg"
}
}
},
"SaveVideo": {
"description": "Enregistre les images d'entrée dans votre répertoire de sortie ComfyUI.",
"display_name": "Enregistrer la vidéo",
@@ -8657,4 +8644,4 @@
}
}
}
}
}

View File

@@ -343,14 +343,6 @@
"Comfy_Validation_Workflows": {
"name": "Valider les flux de travail"
},
"Comfy_VueNodes_Enabled": {
"name": "Activer le rendu des nœuds Vue",
"tooltip": "Rendre les nœuds comme composants Vue au lieu déléments canvas. Fonctionnalité expérimentale."
},
"Comfy_VueNodes_Widgets": {
"name": "Activer les widgets Vue",
"tooltip": "Rendre les widgets comme composants Vue à l'intérieur des nœuds Vue."
},
"Comfy_WidgetControlMode": {
"name": "Mode de contrôle du widget",
"options": {
@@ -396,10 +388,6 @@
"Topbar (2nd-row)": "Barre supérieure (2ème rangée)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "Seuil de zoom pour le rendu de faible qualité",
"tooltip": "Rendre des formes de faible qualité lorsqu'on est dézoomé"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "FPS maximum",
"tooltip": "Le nombre maximum d'images par seconde que le canevas est autorisé à rendre. Limite l'utilisation du GPU au détriment de la fluidité. Si 0, le taux de rafraîchissement de l'écran est utilisé. Par défaut : 0"
@@ -421,4 +409,4 @@
"pysssss_SnapToGrid": {
"name": "Toujours aligner sur la grille"
}
}
}

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "ワークフローをエクスポートAPI形式"
},
"Comfy_Feedback": {
"label": "フィードバック"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "選択範囲をサブグラフに変換"
},
@@ -309,4 +306,4 @@
"label": "ワークフローサイドバーの切り替え",
"tooltip": "ワークフロー"
}
}
}

View File

@@ -324,11 +324,9 @@
"feedback": "フィードバック",
"filter": "フィルタ",
"findIssues": "問題を見つける",
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択することで古いUIに戻すことが可能です。",
"frontendNewer": "フロントエンドのバージョン {frontendVersion} はバックエンドのバージョン {backendVersion} と互換性がない可能性があります。",
"frontendOutdated": "フロントエンドのバージョン {frontendVersion} は古くなっています。バックエンドは {requiredVersion} 以上が必要です。",
"goToNode": "ノードに移動",
"help": "ヘルプ",
"icon": "アイコン",
"imageFailedToLoad": "画像の読み込みに失敗しました",
"imageUrl": "画像URL",
@@ -733,9 +731,7 @@
"Bottom Panel": "下部パネル",
"Browse Templates": "テンプレートを参照",
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
"Check for Custom Node Updates": "カスタムノードのアップデートを確認",
"Check for Updates": "更新を確認する",
"Clear Pending Tasks": "保留中のタスクをクリア",
@@ -760,8 +756,6 @@
"Export": "エクスポート",
"Export (API)": "エクスポート (API)",
"Fit Group To Contents": "グループを内容に合わせる",
"Fit view to selected nodes": "選択したノードにビューを合わせる",
"Give Feedback": "フィードバックを送る",
"Group Selected Nodes": "選択したノードをグループ化",
"Help": "ヘルプ",
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
@@ -809,16 +803,9 @@
"Sign Out": "サインアウト",
"Toggle Essential Bottom Panel": "エッセンシャル下部パネルの切り替え",
"Toggle Logs Bottom Panel": "ログ下部パネルの切り替え",
"Toggle Bottom Panel": "下部パネルの切り替え",
"Toggle Focus Mode": "フォーカスモードの切り替え",
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
"Toggle Search Box": "検索ボックスの切り替え",
"Toggle Terminal Bottom Panel": "ターミナル下部パネルの切り替え",
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
"Undo": "元に戻す",
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
@@ -827,7 +814,6 @@
"Unlock Canvas": "キャンバスのロックを解除",
"Unpack the selected Subgraph": "選択したサブグラフを展開",
"View": "表示",
"Workflow": "ワークフロー",
"Workflows": "ワークフロー",
"Zoom In": "ズームイン",
"Zoom Out": "ズームアウト",
@@ -838,11 +824,7 @@
"renderBypassState": "バイパス状態を表示",
"renderErrorState": "エラー状態を表示",
"showGroups": "フレーム/グループを表示",
"showLinks": "リンクを表示",
"sideToolbar_modelLibrary": "モデルライブラリ",
"sideToolbar_nodeLibrary": "ノードライブラリ",
"sideToolbar_queue": "キュー",
"sideToolbar_workflows": "ワークフロー"
"showLinks": "リンクを表示"
},
"missingModelsDialog": {
"doNotAskAgain": "再度表示しない",
@@ -919,9 +901,6 @@
"upscale_diffusion": "アップスケール拡散",
"upscaling": "アップスケーリング",
"utils": "ユーティリティ",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "ビデオ",
"video_models": "ビデオモデル"
},

View File

@@ -7366,19 +7366,6 @@
}
}
},
"SaveSVG": {
"description": "SVGファイルをディスクに保存します。",
"display_name": "SVGを保存",
"inputs": {
"filename_prefix": {
"name": "ファイル名プレフィックス",
"tooltip": "保存するファイルのプレフィックスです。%date:yyyy-MM-dd% や %Empty Latent Image.width% など、ノードからの値を含めるフォーマット情報を指定できます。"
},
"svg": {
"name": "svg"
}
}
},
"SaveVideo": {
"description": "入力画像をComfyUIの出力ディレクトリに保存します。",
"display_name": "ビデオを保存",
@@ -8657,4 +8644,4 @@
}
}
}
}
}

View File

@@ -343,14 +343,6 @@
"Comfy_Validation_Workflows": {
"name": "ワークフローを検証"
},
"Comfy_VueNodes_Enabled": {
"name": "Vueードレンダリングを有効化",
"tooltip": "ードをキャンバス要素の代わりにVueコンポーネントとしてレンダリングします。実験的な機能です。"
},
"Comfy_VueNodes_Widgets": {
"name": "Vueウィジェットを有効化",
"tooltip": "ウィジェットをVueード内のVueコンポーネントとしてレンダリングします。"
},
"Comfy_WidgetControlMode": {
"name": "ウィジェット制御モード",
"options": {
@@ -396,10 +388,6 @@
"Topbar (2nd-row)": "トップバー2行目"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "低品質レンダリングズーム閾値",
"tooltip": "ズームアウト時に低品質の形状をレンダリングする"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "最大FPS",
"tooltip": "キャンバスがレンダリングできる最大フレーム数です。スムーズさの代わりにGPU使用量を制限します。0の場合、画面のリフレッシュレートが使用されます。デフォルト0"
@@ -421,4 +409,4 @@
"pysssss_SnapToGrid": {
"name": "常にグリッドにスナップ"
}
}
}

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "워크플로 내보내기 (API 형식)"
},
"Comfy_Feedback": {
"label": "피드백"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "선택 영역을 서브그래프로 변환"
},
@@ -309,4 +306,4 @@
"label": "워크플로 사이드바 토글",
"tooltip": "워크플로"
}
}
}

View File

@@ -324,11 +324,9 @@
"feedback": "피드백",
"filter": "필터",
"findIssues": "문제 찾기",
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
"frontendNewer": "프론트엔드 버전 {frontendVersion}이(가) 백엔드 버전 {backendVersion}과(와) 호환되지 않을 수 있습니다.",
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래된 버전입니다. 백엔드는 {requiredVersion} 이상 버전이 필요합니다.",
"goToNode": "노드로 이동",
"help": "도움말",
"icon": "아이콘",
"imageFailedToLoad": "이미지를 로드하지 못했습니다.",
"imageUrl": "이미지 URL",
@@ -733,9 +731,7 @@
"Bottom Panel": "하단 패널",
"Browse Templates": "템플릿 탐색",
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
"Canvas Toggle Lock": "캔버스 토글 잠금",
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
"Check for Custom Node Updates": "커스텀 노드 업데이트 확인",
"Check for Updates": "업데이트 확인",
"Clear Pending Tasks": "보류 중인 작업 제거하기",
@@ -760,8 +756,6 @@
"Export": "내보내기",
"Export (API)": "내보내기 (API)",
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
"Give Feedback": "피드백 제공",
"Group Selected Nodes": "선택한 노드 그룹화",
"Help": "도움말",
"Increase Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 늘리기",
@@ -808,20 +802,11 @@
"Show Settings Dialog": "설정 대화상자 표시",
"Sign Out": "로그아웃",
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",
"Toggle Bottom Panel": "하단 패널 전환",
"Toggle Focus Mode": "포커스 모드 전환",
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
"Toggle Queue Sidebar": "대기열 사이드바 전환",
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
"Toggle Search Box": "검색 상자 전환",
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
"Undo": "실행 취소",
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
@@ -830,7 +815,6 @@
"Unlock Canvas": "캔버스 잠금 해제",
"Unpack the selected Subgraph": "선택한 서브그래프 묶음 풀기",
"View": "보기",
"Workflow": "워크플로",
"Workflows": "워크플로",
"Zoom In": "확대",
"Zoom Out": "축소",
@@ -841,11 +825,7 @@
"renderBypassState": "바이패스 상태 렌더링",
"renderErrorState": "에러 상태 렌더링",
"showGroups": "프레임/그룹 표시",
"showLinks": "링크 표시",
"sideToolbar_modelLibrary": "사이드툴바.모델 라이브러리",
"sideToolbar_nodeLibrary": "사이드툴바.노드 라이브러리",
"sideToolbar_queue": "사이드툴바.대기열",
"sideToolbar_workflows": "사이드툴바.워크플로"
"showLinks": "링크 표시"
},
"missingModelsDialog": {
"doNotAskAgain": "다시 보지 않기",
@@ -922,9 +902,6 @@
"upscale_diffusion": "업스케일 확산",
"upscaling": "업스케일링",
"utils": "유틸리티",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "비디오",
"video_models": "비디오 모델"
},

View File

@@ -7366,19 +7366,6 @@
}
}
},
"SaveSVG": {
"description": "SVG 파일을 디스크에 저장합니다.",
"display_name": "SVG 저장",
"inputs": {
"filename_prefix": {
"name": "파일명 접두사",
"tooltip": "저장할 파일의 접두사입니다. %date:yyyy-MM-dd% 또는 %Empty Latent Image.width%와 같이 노드의 값을 포함하는 형식 정보를 사용할 수 있습니다."
},
"svg": {
"name": "svg"
}
}
},
"SaveVideo": {
"description": "입력 이미지를 ComfyUI 출력 디렉토리에 저장합니다.",
"display_name": "비디오 저장",
@@ -8657,4 +8644,4 @@
}
}
}
}
}

View File

@@ -343,14 +343,6 @@
"Comfy_Validation_Workflows": {
"name": "워크플로 유효성 검사"
},
"Comfy_VueNodes_Enabled": {
"name": "Vue 노드 렌더링 활성화",
"tooltip": "노드를 캔버스 요소 대신 Vue 컴포넌트로 렌더링합니다. 실험적인 기능입니다."
},
"Comfy_VueNodes_Widgets": {
"name": "Vue 위젯 활성화",
"tooltip": "Vue 노드 내에서 위젯을 Vue 컴포넌트로 렌더링합니다."
},
"Comfy_WidgetControlMode": {
"name": "위젯 제어 모드",
"options": {
@@ -396,10 +388,6 @@
"Topbar (2nd-row)": "상단바 (2번째 행)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "저품질 렌더링 줌 임계값",
"tooltip": "줌 아웃시 저품질 도형 렌더링"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "최대 FPS",
"tooltip": "캔버스가 렌더링할 수 있는 최대 프레임 수입니다. 부드럽게 동작하도록 GPU 사용률을 제한 합니다. 0이면 화면 주사율로 작동 합니다. 기본값: 0"
@@ -421,4 +409,4 @@
"pysssss_SnapToGrid": {
"name": "항상 그리드에 스냅"
}
}
}

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "Экспорт рабочего процесса (формат API)"
},
"Comfy_Feedback": {
"label": "Обратная связь"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "Преобразовать выделенное в подграф"
},
@@ -309,4 +306,4 @@
"label": "Переключить боковую панель рабочих процессов",
"tooltip": "Рабочие процессы"
}
}
}

View File

@@ -324,11 +324,9 @@
"feedback": "Обратная связь",
"filter": "Фильтр",
"findIssues": "Найти проблемы",
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
"frontendNewer": "Версия интерфейса {frontendVersion} может быть несовместима с версией сервера {backendVersion}.",
"frontendOutdated": "Версия интерфейса {frontendVersion} устарела. Требуется версия не ниже {requiredVersion} для работы с сервером.",
"goToNode": "Перейти к ноде",
"help": "Помощь",
"icon": "Иконка",
"imageFailedToLoad": "Не удалось загрузить изображение",
"imageUrl": "URL изображения",
@@ -733,9 +731,7 @@
"Bottom Panel": "Нижняя панель",
"Browse Templates": "Просмотреть шаблоны",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
"Canvas Toggle Lock": "Переключение блокировки холста",
"Canvas Toggle Minimap": "Показать/скрыть миникарту на холсте",
"Check for Custom Node Updates": "Проверить обновления пользовательских узлов",
"Check for Updates": "Проверить наличие обновлений",
"Clear Pending Tasks": "Очистить ожидающие задачи",
@@ -760,8 +756,6 @@
"Export": "Экспортировать",
"Export (API)": "Экспорт (API)",
"Fit Group To Contents": "Подогнать группу под содержимое",
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
"Give Feedback": "Оставить отзыв",
"Group Selected Nodes": "Сгруппировать выбранные ноды",
"Help": "Помощь",
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
@@ -809,17 +803,10 @@
"Sign Out": "Выйти",
"Toggle Essential Bottom Panel": "Показать/скрыть нижнюю панель основных элементов",
"Toggle Logs Bottom Panel": "Показать/скрыть нижнюю панель логов",
"Toggle Bottom Panel": "Переключить нижнюю панель",
"Toggle Focus Mode": "Переключить режим фокуса",
"Toggle Model Library Sidebar": "Показать/скрыть боковую панель библиотеки моделей",
"Toggle Node Library Sidebar": "Показать/скрыть боковую панель библиотеки узлов",
"Toggle Queue Sidebar": "Показать/скрыть боковую панель очереди",
"Toggle Search Box": "Переключить поисковую панель",
"Toggle Terminal Bottom Panel": "Показать/скрыть нижнюю панель терминала",
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
"Toggle View Controls Bottom Panel": "Показать/скрыть нижнюю панель элементов управления",
"Toggle Workflows Sidebar": "Показать/скрыть боковую панель рабочих процессов",
"Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов",
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
"Undo": "Отменить",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
@@ -828,7 +815,6 @@
"Unlock Canvas": "Разблокировать холст",
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
"View": "Вид",
"Workflow": "Рабочий процесс",
"Workflows": "Рабочие процессы",
"Zoom In": "Увеличить",
"Zoom Out": "Уменьшить",
@@ -839,11 +825,7 @@
"renderBypassState": "Отображать состояние обхода",
"renderErrorState": "Отображать состояние ошибки",
"showGroups": "Показать фреймы/группы",
"showLinks": "Показать связи",
"sideToolbar_modelLibrary": "sideToolbar.каталогМоделей",
"sideToolbar_nodeLibrary": "sideToolbar.каталогУзлов",
"sideToolbar_queue": "sideToolbar.очередь",
"sideToolbar_workflows": "sideToolbar.рабочиеПроцессы"
"showLinks": "Показать связи"
},
"missingModelsDialog": {
"doNotAskAgain": "Больше не показывать это",
@@ -920,9 +902,6 @@
"upscale_diffusion": "диффузии_апскейла",
"upscaling": "апскейл",
"utils": "утилиты",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "видео",
"video_models": "видеомодели"
},

View File

@@ -7366,19 +7366,6 @@
}
}
},
"SaveSVG": {
"description": "Сохранять файлы SVG на диск.",
"display_name": "Сохранить SVG",
"inputs": {
"filename_prefix": {
"name": "префикс_имени_файла",
"tooltip": "Префикс для сохраняемого файла. Может включать информацию о форматировании, такую как %date:yyyy-MM-dd% или %Empty Latent Image.width% для включения значений из узлов."
},
"svg": {
"name": "svg"
}
}
},
"SaveVideo": {
"description": "Сохраняет входные изображения в вашу папку вывода ComfyUI.",
"display_name": "Сохранить видео",
@@ -8657,4 +8644,4 @@
}
}
}
}
}

View File

@@ -343,14 +343,6 @@
"Comfy_Validation_Workflows": {
"name": "Проверка рабочих процессов"
},
"Comfy_VueNodes_Enabled": {
"name": "Включить рендеринг узлов через Vue",
"tooltip": "Отображать узлы как компоненты Vue вместо элементов canvas. Экспериментальная функция."
},
"Comfy_VueNodes_Widgets": {
"name": "Включить виджеты Vue",
"tooltip": "Отображать виджеты как компоненты Vue внутри узлов Vue."
},
"Comfy_WidgetControlMode": {
"name": "Режим управления виджетом",
"options": {
@@ -396,10 +388,6 @@
"Topbar (2nd-row)": "Топбар (2-й ряд)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "Порог масштабирования для рендеринга низкого качества",
"tooltip": "Рендеринг фигур низкого качества при уменьшении масштаба"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "Максимум FPS",
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счёт плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
@@ -421,4 +409,4 @@
"pysssss_SnapToGrid": {
"name": "Всегда привязываться к сетке"
}
}
}

View File

@@ -0,0 +1,309 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "Güncellemeleri Kontrol Et"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Özel Düğümler Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenInputsFolder": {
"label": "Girişler Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenLogsFolder": {
"label": "Kayıtlar Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenModelConfig": {
"label": "extra_model_paths.yaml dosyasını aç"
},
"Comfy-Desktop_Folders_OpenModelsFolder": {
"label": "Modeller Klasörünü Aç"
},
"Comfy-Desktop_Folders_OpenOutputsFolder": {
"label": ıktılar Klasörünü Aç"
},
"Comfy-Desktop_OpenDevTools": {
"label": "Geliştirici Araçlarını Aç"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Masaüstü Kullanıcı Kılavuzu"
},
"Comfy-Desktop_Quit": {
"label": ık"
},
"Comfy-Desktop_Reinstall": {
"label": "Yeniden Yükle"
},
"Comfy-Desktop_Restart": {
"label": "Yeniden Başlat"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "Seçili Düğüm için 3D Görüntüleyiciyi (Beta) Aç"
},
"Comfy_BrowseTemplates": {
"label": "Şablonlara Gözat"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Seçili Öğeleri Sil"
},
"Comfy_Canvas_FitView": {
"label": "Görünümü seçili düğümlere sığdır"
},
"Comfy_Canvas_Lock": {
"label": "Tuvali Kilitle"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "Seçili Düğümleri Aşağı Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Left": {
"label": "Seçili Düğümleri Sola Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Right": {
"label": "Seçili Düğümleri Sağa Taşı"
},
"Comfy_Canvas_MoveSelectedNodes_Up": {
"label": "Seçili Düğümleri Yukarı Taşı"
},
"Comfy_Canvas_ResetView": {
"label": "Görünümü Sıfırla"
},
"Comfy_Canvas_Resize": {
"label": "Seçili Düğümleri Yeniden Boyutlandır"
},
"Comfy_Canvas_ToggleLinkVisibility": {
"label": "Tuval Bağlantı Görünürlüğünü Aç/Kapat"
},
"Comfy_Canvas_ToggleLock": {
"label": "Tuval Kilidini Aç/Kapat"
},
"Comfy_Canvas_ToggleMinimap": {
"label": "Mini Haritayı Aç/Kapat"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "Seçili Düğümleri Atla/Geri Al"
},
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
"label": "Seçili Düğümleri Daralt/Genişlet"
},
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
"label": "Seçili Düğümleri Sessize Al/Sesi Aç"
},
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "Seçili Düğümleri Sabitle/Sabitlemeyi Kaldır"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Seçili Öğeleri Sabitle/Sabitlemeyi Kaldır"
},
"Comfy_Canvas_Unlock": {
"label": "Tuvalin Kilidini Aç"
},
"Comfy_Canvas_ZoomIn": {
"label": "Yakınlaştır"
},
"Comfy_Canvas_ZoomOut": {
"label": "Uzaklaştır"
},
"Comfy_ClearPendingTasks": {
"label": "Bekleyen Görevleri Temizle"
},
"Comfy_ClearWorkflow": {
"label": "İş Akışını Temizle"
},
"Comfy_ContactSupport": {
"label": "Destekle İletişime Geç"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Model Seçiciyi Göster (Geliştirici)"
},
"Comfy_DuplicateWorkflow": {
"label": "Mevcut İş Akışını Çoğalt"
},
"Comfy_ExportWorkflow": {
"label": "İş Akışını Dışa Aktar"
},
"Comfy_ExportWorkflowAPI": {
"label": "İş Akışını Dışa Aktar (API Formatı)"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "Seçimi Alt Grafiğe Dönüştür"
},
"Comfy_Graph_ExitSubgraph": {
"label": "Alt Grafikten Çık"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Grubu İçeriğe Sığdır"
},
"Comfy_Graph_GroupSelectedNodes": {
"label": "Seçili Düğümleri Gruplandır"
},
"Comfy_Graph_UnpackSubgraph": {
"label": "Seçili Alt Grafiği Aç"
},
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
"label": "Seçili düğümleri grup düğümüne dönüştür"
},
"Comfy_GroupNode_ManageGroupNodes": {
"label": "Grup düğümlerini yönet"
},
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
"label": "Seçili grup düğümlerinin grubunu çöz"
},
"Comfy_Help_AboutComfyUI": {
"label": "ComfyUI Hakkında'yı Aç"
},
"Comfy_Help_OpenComfyOrgDiscord": {
"label": "Comfy-Org Discord'unu Aç"
},
"Comfy_Help_OpenComfyUIDocs": {
"label": "ComfyUI Belgelerini Aç"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "ComfyUI Forumunu Aç"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "ComfyUI Sorunlarını Aç"
},
"Comfy_Interrupt": {
"label": "Kes"
},
"Comfy_LoadDefaultWorkflow": {
"label": "Varsayılan İş Akışını Yükle"
},
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
"label": "Özel Düğüm Yöneticisi"
},
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
"label": "Özel Düğümler (Eski)"
},
"Comfy_Manager_ShowLegacyManagerMenu": {
"label": "Yönetici Menüsü (Eski)"
},
"Comfy_Manager_ShowMissingPacks": {
"label": "Eksik Özel Düğümleri Yükle"
},
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Özel Düğüm Güncellemelerini Kontrol Et"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Özel Düğüm Yöneticisi İlerleme Çubuğunu Aç/Kapat"
},
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "Maske Düzenleyicide Fırça Boyutunu Azalt"
},
"Comfy_MaskEditor_BrushSize_Increase": {
"label": "Maske Düzenleyicide Fırça Boyutunu Artır"
},
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Seçili Düğüm için Maske Düzenleyiciyi Aç"
},
"Comfy_Memory_UnloadModels": {
"label": "Modelleri Boşalt"
},
"Comfy_Memory_UnloadModelsAndExecutionCache": {
"label": "Modelleri ve Yürütme Önbelleğini Boşalt"
},
"Comfy_NewBlankWorkflow": {
"label": "Yeni Boş İş Akışı"
},
"Comfy_OpenClipspace": {
"label": "Clipspace"
},
"Comfy_OpenManagerDialog": {
"label": "Yönetici"
},
"Comfy_OpenWorkflow": {
"label": "İş Akışını Aç"
},
"Comfy_PublishSubgraph": {
"label": "Alt Grafiği Yayınla"
},
"Comfy_QueuePrompt": {
"label": "İstemi Kuyruğa Al"
},
"Comfy_QueuePromptFront": {
"label": "İstemi Kuyruğa Al (Ön)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "Seçili Çıktı Düğümlerini Kuyruğa Al"
},
"Comfy_Redo": {
"label": "Yinele"
},
"Comfy_RefreshNodeDefinitions": {
"label": "Düğüm Tanımlarını Yenile"
},
"Comfy_SaveWorkflow": {
"label": "İş Akışını Kaydet"
},
"Comfy_SaveWorkflowAs": {
"label": "İş Akışını Farklı Kaydet"
},
"Comfy_ShowSettingsDialog": {
"label": "Ayarlar İletişim Kutusunu Göster"
},
"Comfy_ToggleCanvasInfo": {
"label": "Tuval Performansı"
},
"Comfy_ToggleHelpCenter": {
"label": "Yardım Merkezi"
},
"Comfy_ToggleTheme": {
"label": "Temayı Değiştir (Karanlık/Açık)"
},
"Comfy_Undo": {
"label": "Geri Al"
},
"Comfy_User_OpenSignInDialog": {
"label": "Giriş Yapma İletişim Kutusunu Aç"
},
"Comfy_User_SignOut": {
"label": ıkış Yap"
},
"Workspace_CloseWorkflow": {
"label": "Mevcut İş Akışını Kapat"
},
"Workspace_NextOpenedWorkflow": {
"label": "Sonraki Açılan İş Akışı"
},
"Workspace_PreviousOpenedWorkflow": {
"label": "Önceki Açılan İş Akışı"
},
"Workspace_SearchBox_Toggle": {
"label": "Arama Kutusunu Aç/Kapat"
},
"Workspace_ToggleBottomPanel": {
"label": "Alt Paneli Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Terminal Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Kayıtlar Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "Temel Alt Paneli Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
"label": "Görünüm Kontrolleri Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanel_Shortcuts": {
"label": "Tuş Atamaları İletişim Kutusunu Göster"
},
"Workspace_ToggleFocusMode": {
"label": "Odak Modunu Aç/Kapat"
},
"Workspace_ToggleSidebarTab_model-library": {
"label": "Model Kütüphanesi Kenar Çubuğunu Aç/Kapat",
"tooltip": "Model Kütüphanesi"
},
"Workspace_ToggleSidebarTab_node-library": {
"label": "Düğüm Kütüphanesi Kenar Çubuğunu Aç/Kapat",
"tooltip": "Düğüm Kütüphanesi"
},
"Workspace_ToggleSidebarTab_queue": {
"label": "Kuyruk Kenar Çubuğunu Aç/Kapat",
"tooltip": "Kuyruk"
},
"Workspace_ToggleSidebarTab_workflows": {
"label": "İş Akışları Kenar Çubuğunu Aç/Kapat",
"tooltip": "İş Akışları"
}
}

1780
src/locales/tr/main.json Normal file

File diff suppressed because it is too large Load Diff

8647
src/locales/tr/nodeDefs.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,416 @@
{
"Comfy-Desktop_AutoUpdate": {
"name": "Güncellemeleri otomatik olarak kontrol et"
},
"Comfy-Desktop_SendStatistics": {
"name": "Anonim kullanım metrikleri gönder"
},
"Comfy-Desktop_UV_PypiInstallMirror": {
"name": "Pypi Yükleme Yansısı",
"tooltip": "Varsayılan pip yükleme yansısı"
},
"Comfy-Desktop_UV_PythonInstallMirror": {
"name": "Python Yükleme Yansısı",
"tooltip": "Yönetilen Python kurulumları Astral python-build-standalone projesinden indirilir. Bu değişken, Python kurulumları için farklı bir kaynak kullanmak üzere bir yansıma URL'sine ayarlanabilir. Sağlanan URL, örneğin https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz'deki https://github.com/astral-sh/python-build-standalone/releases/download'ın yerini alacaktır. Dağıtımlar, file:// URL şeması kullanılarak yerel bir dizinden okunabilir."
},
"Comfy-Desktop_UV_TorchInstallMirror": {
"name": "Torch Yükleme Yansısı",
"tooltip": "Pytorch için Pip yükleme yansısı"
},
"Comfy-Desktop_WindowStyle": {
"name": "Pencere Stili",
"options": {
"custom": "özel",
"default": "varsayılan"
},
"tooltip": "Özel: Sistem başlık çubuğunu ComfyUI'nin Üst menüsüyle değiştirin"
},
"Comfy_Canvas_BackgroundImage": {
"name": "Tuval arka plan resmi",
"tooltip": "Tuval arka planı için resim URL'si. Çıktılar panelindeki bir resme sağ tıklayıp \"Arka Plan Olarak Ayarla\"yı seçerek kullanabilir veya yükleme düğmesini kullanarak kendi resminizi yükleyebilirsiniz."
},
"Comfy_Canvas_NavigationMode": {
"name": "Tuval Gezinme Modu",
"options": {
"Drag Navigation": "Sürükleyerek Gezinme",
"Standard (New)": "Standart (Yeni)"
}
},
"Comfy_Canvas_SelectionToolbox": {
"name": "Seçim araç kutusunu göster"
},
"Comfy_ConfirmClear": {
"name": "İş akışını temizlerken onay iste"
},
"Comfy_DOMClippingEnabled": {
"name": "DOM öğesi kırpmayı etkinleştir (etkinleştirmek performansı düşürebilir)"
},
"Comfy_DevMode": {
"name": "Geliştirici modu seçeneklerini etkinleştir (API kaydetme, vb.)"
},
"Comfy_DisableFloatRounding": {
"name": "Varsayılan ondalık sayı widget yuvarlamasını devre dışı bırak.",
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir) Arka uçtaki düğüm tarafından yuvarlama ayarlandığında yuvarlama devre dışı bırakılamaz."
},
"Comfy_DisableSliders": {
"name": "Düğüm widget kaydırıcılarını devre dışı bırak"
},
"Comfy_EditAttention_Delta": {
"name": "Ctrl+yukarı/aşağı hassasiyeti"
},
"Comfy_EnableTooltips": {
"name": "Araç İpuçlarını Etkinleştir"
},
"Comfy_EnableWorkflowViewRestore": {
"name": "İş akışlarında tuval konumunu ve yakınlaştırma seviyesini kaydet ve geri yükle"
},
"Comfy_FloatRoundingPrecision": {
"name": "Ondalık sayı widget yuvarlama ondalık basamakları [0 = otomatik].",
"tooltip": "(sayfanın yeniden yüklenmesini gerektirir)"
},
"Comfy_Graph_CanvasInfo": {
"name": "Sol alt köşede tuval bilgilerini göster (fps, vb.)"
},
"Comfy_Graph_CanvasMenu": {
"name": "Grafik tuval menüsünü göster"
},
"Comfy_Graph_CtrlShiftZoom": {
"name": "Hızlı yakınlaştırma kısayolunu etkinleştir (Ctrl + Shift + Sürükle)"
},
"Comfy_Graph_LinkMarkers": {
"name": "Bağlantı orta nokta işaretçileri",
"options": {
"Arrow": "Ok",
"Circle": "Daire",
"None": "Yok"
}
},
"Comfy_Graph_ZoomSpeed": {
"name": "Tuval yakınlaştırma hızı"
},
"Comfy_GroupSelectedNodes_Padding": {
"name": "Seçili düğümleri gruplandırma dolgusu"
},
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "Düzenlemek için grup başlığına çift tıkla"
},
"Comfy_LinkRelease_Action": {
"name": "Bağlantı bırakıldığında eylem (Değiştirici yok)",
"options": {
"context menu": "bağlam menüsü",
"no action": "eylem yok",
"search box": "arama kutusu"
}
},
"Comfy_LinkRelease_ActionShift": {
"name": "Bağlantı bırakıldığında eylem (Shift)",
"options": {
"context menu": "bağlam menüsü",
"no action": "eylem yok",
"search box": "arama kutusu"
}
},
"Comfy_LinkRenderMode": {
"name": "Bağlantı Oluşturma Modu",
"options": {
"Hidden": "Gizli",
"Linear": "Doğrusal",
"Spline": "Eğri",
"Straight": "Düz"
}
},
"Comfy_Load3D_3DViewerEnable": {
"name": "3D Görüntüleyiciyi Etkinleştir (Beta)",
"tooltip": "Seçili düğümler için 3D Görüntüleyiciyi (Beta) etkinleştirir. Bu özellik, 3D modelleri doğrudan tam boyutlu 3D görüntüleyici içinde görselleştirmenize ve etkileşimde bulunmanıza olanak tanır."
},
"Comfy_Load3D_BackgroundColor": {
"name": "Başlangıç Arka Plan Rengi",
"tooltip": "3D sahnenin varsayılan arka plan rengini kontrol eder. Bu ayar, yeni bir 3D widget oluşturulduğunda arka plan görünümünü belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
},
"Comfy_Load3D_CameraType": {
"name": "Başlangıç Kamera Tipi",
"options": {
"orthographic": "ortografik",
"perspective": "perspektif"
},
"tooltip": "Yeni bir 3D widget oluşturulduğunda kameranın varsayılan olarak perspektif mi yoksa ortografik mi olacağını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "Işık Ayarlama Artışı",
"tooltip": "3D sahnelerde ışık yoğunluğunu ayarlarken artış boyutunu kontrol eder. Daha küçük bir adım değeri, aydınlatma ayarlamaları üzerinde daha ince kontrol sağlarken, daha büyük bir değer ayarlama başına daha belirgin değişikliklere neden olur."
},
"Comfy_Load3D_LightIntensity": {
"name": "Başlangıç Işık Yoğunluğu",
"tooltip": "3D sahnedeki aydınlatmanın varsayılan parlaklık seviyesini ayarlar. Bu değer, yeni bir 3D widget oluşturulduğunda ışıkların nesneleri ne kadar yoğun aydınlatacağını belirler, ancak oluşturulduktan sonra her widget için ayrı ayrı ayarlanabilir."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "Maksimum Işık Yoğunluğu",
"tooltip": "3D sahneler için izin verilen maksimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek üst parlaklık sınırını tanımlar."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "Minimum Işık Yoğunluğu",
"tooltip": "3D sahneler için izin verilen minimum ışık yoğunluğu değerini ayarlar. Bu, herhangi bir 3D widget'ta aydınlatma ayarlanırken ayarlanabilecek alt parlaklık sınırını tanımlar."
},
"Comfy_Load3D_ShowGrid": {
"name": "Başlangıç Izgara Görünürlüğü",
"tooltip": "Yeni bir 3D widget oluşturulduğunda ızgaranın varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
},
"Comfy_Load3D_ShowPreview": {
"name": "Başlangıç Önizleme Görünürlüğü",
"tooltip": "Yeni bir 3D widget oluşturulduğunda önizleme ekranının varsayılan olarak görünür olup olmadığını kontrol eder. Bu varsayılan, oluşturulduktan sonra her widget için ayrı ayrı değiştirilebilir."
},
"Comfy_Locale": {
"name": "Dil"
},
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
"name": "Fırça ayar hızı çarpanı",
"tooltip": "Ayarlama sırasında fırça boyutunun ve sertliğinin ne kadar hızlı değiştiğini kontrol eder. Daha yüksek değerler daha hızlı değişiklikler anlamına gelir."
},
"Comfy_MaskEditor_UseDominantAxis": {
"name": "Fırça ayarını baskın eksene kilitle",
"tooltip": "Etkinleştirildiğinde, fırça ayarları yalnızca daha fazla hareket ettiğiniz yöne bağlı olarak boyutu VEYA sertliği etkileyecektir"
},
"Comfy_MaskEditor_UseNewEditor": {
"name": "Yeni maske düzenleyiciyi kullan",
"tooltip": "Yeni maske düzenleyici arayüzüne geç"
},
"Comfy_ModelLibrary_AutoLoadAll": {
"name": "Tüm model klasörlerini otomatik olarak yükle",
"tooltip": "Doğruysa, model kütüphanesini açar açmaz tüm klasörler yüklenecektir (bu, yüklenirken gecikmelere neden olabilir). Yanlışsa, kök düzeyindeki model klasörleri yalnızca üzerlerine tıkladığınızda yüklenecektir."
},
"Comfy_ModelLibrary_NameFormat": {
"name": "Model kütüphanesi ağaç görünümünde hangi adın görüntüleneceği",
"options": {
"filename": "dosyaadı",
"title": "başlık"
},
"tooltip": "Model listesinde ham dosya adının (dizin veya \".safetensors\" uzantısı olmadan) basitleştirilmiş bir görünümünü oluşturmak için \"dosyaadı\"nı seçin. Yapılandırılabilir model meta veri başlığını görüntülemek için \"başlık\"ı seçin."
},
"Comfy_NodeBadge_NodeIdBadgeMode": {
"name": "Düğüm ID rozeti modu",
"options": {
"None": "Yok",
"Show all": "Tümünü göster"
}
},
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
"name": "Düğüm yaşam döngüsü rozeti modu",
"options": {
"None": "Yok",
"Show all": "Tümünü göster"
}
},
"Comfy_NodeBadge_NodeSourceBadgeMode": {
"name": "Düğüm kaynak rozeti modu",
"options": {
"Hide built-in": "Yerleşik olanı gizle",
"None": "Yok",
"Show all": "Tümünü göster"
}
},
"Comfy_NodeBadge_ShowApiPricing": {
"name": "API düğüm fiyatlandırma rozetini göster"
},
"Comfy_NodeSearchBoxImpl": {
"name": "Düğüm arama kutusu uygulaması",
"options": {
"default": "varsayılan",
"litegraph (legacy)": "litegraph (eski)"
}
},
"Comfy_NodeSearchBoxImpl_NodePreview": {
"name": "Düğüm önizlemesi",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowCategory": {
"name": "Arama sonuçlarında düğüm kategorisini göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowIdName": {
"name": "Arama sonuçlarında düğüm kimliği adını göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
"name": "Arama sonuçlarında düğüm sıklığını göster",
"tooltip": "Yalnızca varsayılan uygulama için geçerlidir"
},
"Comfy_NodeSuggestions_number": {
"name": "Düğüm öneri sayısı",
"tooltip": "Yalnızca litegraph arama kutusu/bağlam menüsü için"
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "Görüntü önizlemesinin altında genişlik × yüksekliği göster"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Bağlantıyı otomatik olarak düğüm yuvasına yapıştır",
"tooltip": "Bir bağlantıyı bir düğümün üzerine sürüklerken, bağlantı otomatik olarak düğüm üzerindeki uygun bir giriş yuvasına yapışır"
},
"Comfy_Node_BypassAllLinksOnDelete": {
"name": "Düğümleri silerken tüm bağlantıları koru",
"tooltip": "Bir düğümü silerken, tüm giriş ve çıkış bağlantılarını yeniden bağlamaya çalışın (silinen düğümü atlayarak)"
},
"Comfy_Node_DoubleClickTitleToEdit": {
"name": "Düzenlemek için düğüm başlığına çift tıkla"
},
"Comfy_Node_MiddleClickRerouteNode": {
"name": "Orta tıklama yeni bir Yeniden Yönlendirme düğümü oluşturur"
},
"Comfy_Node_Opacity": {
"name": "Düğüm opaklığı"
},
"Comfy_Node_ShowDeprecated": {
"name": "Aramada kullanımdan kaldırılmış düğümleri göster",
"tooltip": "Kullanımdan kaldırılmış düğümler arayüzde varsayılan olarak gizlidir, ancak bunları kullanan mevcut iş akışlarında işlevsel kalır."
},
"Comfy_Node_ShowExperimental": {
"name": "Aramada deneysel düğümleri göster",
"tooltip": "Deneysel düğümler arayüzde bu şekilde işaretlenmiştir ve gelecekteki sürümlerde önemli değişikliklere veya kaldırılmaya tabi olabilir. Üretim iş akışlarında dikkatli kullanın"
},
"Comfy_Node_SnapHighlightsNode": {
"name": "Yapıştırma düğümü vurgular",
"tooltip": "Uygun giriş yuvasına sahip bir düğümün üzerine bir bağlantı sürüklerken, düğümü vurgulayın"
},
"Comfy_Notification_ShowVersionUpdates": {
"name": "Sürüm güncellemelerini göster",
"tooltip": "Yeni modeller ve önemli yeni özellikler için güncellemeleri göster."
},
"Comfy_Pointer_ClickBufferTime": {
"name": "İşaretçi tıklama kayma gecikmesi",
"tooltip": "Bir işaretçi düğmesine bastıktan sonra, bu, işaretçi hareketinin göz ardı edilebileceği maksimum süredir (milisaniye cinsinden).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
},
"Comfy_Pointer_ClickDrift": {
"name": "İşaretçi tıklama kayması (maksimum mesafe)",
"tooltip": "İşaretçi bir düğmeyi basılı tutarken bu mesafeden daha fazla hareket ederse, bu sürükleme olarak kabul edilir (tıklama yerine).\n\nTıklarken işaretçi hareket ettirilirse nesnelerin istemeden dürtülmesini önlemeye yardımcı olur."
},
"Comfy_Pointer_DoubleClickTime": {
"name": "Çift tıklama aralığı (maksimum)",
"tooltip": "Çift tıklamanın iki tıklaması arasındaki milisaniye cinsinden maksimum süre. Bu değeri artırmak, çift tıklamaların bazen kaydedilmemesi durumunda yardımcı olabilir."
},
"Comfy_PreviewFormat": {
"name": "Önizleme görüntü formatı",
"tooltip": "Görüntü widget'ında bir önizleme görüntülerken, onu hafif bir görüntüye dönüştürün, örn. webp, jpeg, webp;50, vb."
},
"Comfy_PromptFilename": {
"name": "İş akışını kaydederken dosya adı iste"
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "Toplu iş sayısı sınırı",
"tooltip": "Tek bir düğme tıklamasıyla kuyruğa eklenen maksimum görev sayısı"
},
"Comfy_Queue_MaxHistoryItems": {
"name": "Kuyruk geçmişi boyutu",
"tooltip": "Kuyruk geçmişinde gösterilen maksimum görev sayısı."
},
"Comfy_Sidebar_Location": {
"name": "Kenar çubuğu konumu",
"options": {
"left": "sol",
"right": "sağ"
}
},
"Comfy_Sidebar_Size": {
"name": "Kenar çubuğu boyutu",
"options": {
"normal": "normal",
"small": "küçük"
}
},
"Comfy_Sidebar_UnifiedWidth": {
"name": "Birleşik kenar çubuğu genişliği"
},
"Comfy_SnapToGrid_GridSize": {
"name": "Izgaraya yapıştırma boyutu",
"tooltip": "Shift tuşunu basılı tutarken düğümleri sürükleyip yeniden boyutlandırırken ızgaraya hizalanacaklar, bu o ızgaranın boyutunu kontrol eder."
},
"Comfy_TextareaWidget_FontSize": {
"name": "Metin alanı widget yazı tipi boyutu"
},
"Comfy_TextareaWidget_Spellcheck": {
"name": "Metin alanı widget yazım denetimi"
},
"Comfy_TreeExplorer_ItemPadding": {
"name": "Ağaç gezgini öğe dolgusu"
},
"Comfy_UseNewMenu": {
"name": "Yeni menüyü kullan",
"options": {
"Bottom": "Alt",
"Disabled": "Devre dışı",
"Top": "Üst"
},
"tooltip": "Menü çubuğu konumu. Mobil cihazlarda menü her zaman üstte gösterilir."
},
"Comfy_Validation_Workflows": {
"name": "İş akışlarını doğrula"
},
"Comfy_WidgetControlMode": {
"name": "Widget kontrol modu",
"options": {
"after": "sonra",
"before": "önce"
},
"tooltip": "Widget değerlerinin ne zaman güncelleneceğini (rastgele/artırma/azaltma), istem kuyruğa alınmadan önce veya sonra kontrol eder."
},
"Comfy_Window_UnloadConfirmation": {
"name": "Pencereyi kapatırken onay göster"
},
"Comfy_Workflow_AutoSave": {
"name": "Otomatik Kaydet",
"options": {
"after delay": "gecikmeden sonra",
"off": "kapalı"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "Otomatik Kaydetme Gecikmesi (ms)",
"tooltip": "Yalnızca Otomatik Kaydetme \"gecikmeden sonra\" olarak ayarlandığında geçerlidir."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "İş akışlarını silerken onay göster"
},
"Comfy_Workflow_Persist": {
"name": "İş akışı durumunu koru ve sayfayı (yeniden) yüklediğinde geri yükle"
},
"Comfy_Workflow_ShowMissingModelsWarning": {
"name": "Eksik model uyarısını göster"
},
"Comfy_Workflow_ShowMissingNodesWarning": {
"name": "Eksik düğüm uyarısını göster"
},
"Comfy_Workflow_SortNodeIdOnSave": {
"name": "İş akışını kaydederken düğüm kimliklerini sırala"
},
"Comfy_Workflow_WorkflowTabsPosition": {
"name": "Açılan iş akışları konumu",
"options": {
"Sidebar": "Kenar Çubuğu",
"Topbar": "Üst Çubuk",
"Topbar (2nd-row)": "Üst Çubuk (2. sıra)"
}
},
"LiteGraph_Canvas_MaximumFps": {
"name": "Maksimum FPS",
"tooltip": "Tuvalin saniyede oluşturmasına izin verilen maksimum kare sayısı. Akıcılık pahasına GPU kullanımını sınırlar. 0 ise, ekran yenileme hızı kullanılır. Varsayılan: 0"
},
"LiteGraph_Canvas_MinFontSizeForLOD": {
"name": "Yakınlaştırma Düğümü Ayrıntı Seviyesi - yazı tipi boyutu eşiği",
"tooltip": "Düğümlerin ne zaman düşük kaliteli LOD oluşturmaya geçeceğini kontrol eder. Ne zaman geçiş yapılacağını belirlemek için piksel cinsinden yazı tipi boyutunu kullanır. Devre dışı bırakmak için 0'a ayarlayın. 1-24 arasındaki değerler LOD için minimum yazı tipi boyutu eşiğini ayarlar - daha yüksek değerler (24 piksel) = uzaklaştırırken düğümleri daha erken basitleştirilmiş oluşturmaya geçirin, daha düşük değerler (1 piksel) = tam düğüm kalitesini daha uzun süre koruyun."
},
"LiteGraph_ContextMenu_Scaling": {
"name": "Yakınlaştırıldığında düğüm birleşik widget menülerini (listeleri) ölçeklendir"
},
"LiteGraph_Node_DefaultPadding": {
"name": "Yeni düğümleri her zaman küçült",
"tooltip": "Oluşturulduğunda düğümleri mümkün olan en küçük boyuta yeniden boyutlandırın. Devre dışı bırakıldığında, yeni eklenen bir düğüm widget değerlerini göstermek için biraz genişletilecektir."
},
"LiteGraph_Node_TooltipDelay": {
"name": "Araç İpucu Gecikmesi"
},
"LiteGraph_Reroute_SplineOffset": {
"name": "Yeniden yönlendirme eğri ofseti",
"tooltip": "Yeniden yönlendirme merkez noktasından bezier kontrol noktası ofseti"
},
"pysssss_SnapToGrid": {
"name": "Her zaman ızgaraya yapıştır"
}
}

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "匯出工作流程API 格式)"
},
"Comfy_Feedback": {
"label": "提供回饋"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "將選取內容轉換為子圖"
},
@@ -309,4 +306,4 @@
"label": "切換工作流程側邊欄",
"tooltip": "工作流程"
}
}
}

View File

@@ -324,11 +324,9 @@
"feedback": "意見回饋",
"filter": "篩選",
"findIssues": "尋找問題",
"firstTimeUIMessage": "這是您第一次使用新介面。若要返回舊介面,請前往「選單」>「使用新介面」>「關閉」。",
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
"goToNode": "前往節點",
"help": "說明",
"icon": "圖示",
"imageFailedToLoad": "無法載入圖片",
"imageUrl": "圖片網址",
@@ -733,9 +731,7 @@
"Bottom Panel": "底部面板",
"Browse Templates": "瀏覽範本",
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
"Canvas Toggle Link Visibility": "切換連結可見性",
"Canvas Toggle Lock": "切換畫布鎖定",
"Canvas Toggle Minimap": "畫布切換小地圖",
"Check for Custom Node Updates": "檢查自訂節點更新",
"Check for Updates": "檢查更新",
"Clear Pending Tasks": "清除待處理任務",
@@ -760,8 +756,6 @@
"Export": "匯出",
"Export (API)": "匯出API",
"Fit Group To Contents": "群組貼合內容",
"Fit view to selected nodes": "視圖貼合選取節點",
"Give Feedback": "提供意見回饋",
"Group Selected Nodes": "群組選取節點",
"Help": "說明",
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
@@ -813,13 +807,6 @@
"Toggle Terminal Bottom Panel": "切換終端機底部面板",
"Toggle Theme (Dark/Light)": "切換主題(深色/淺色)",
"Toggle View Controls Bottom Panel": "切換檢視控制底部面板",
"Toggle Bottom Panel": "切換下方面板",
"Toggle Focus Mode": "切換專注模式",
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
"Toggle Queue Sidebar": "切換佇列側邊欄",
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
"Toggle the Custom Nodes Manager": "切換自訂節點管理器",
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
"Undo": "復原",
"Ungroup selected group nodes": "取消群組選取的群組節點",
@@ -828,7 +815,6 @@
"Unlock Canvas": "解除鎖定畫布",
"Unpack the selected Subgraph": "解包所選子圖",
"View": "檢視",
"Workflow": "工作流程",
"Workflows": "工作流程",
"Zoom In": "放大",
"Zoom Out": "縮小"
@@ -908,9 +894,6 @@
"upscale_diffusion": "擴散放大",
"upscaling": "放大",
"utils": "工具",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "影片",
"video_models": "影片模型"
},

View File

@@ -7366,19 +7366,6 @@
}
}
},
"SaveSVG": {
"description": "將 SVG 檔案儲存到磁碟。",
"display_name": "儲存 SVG",
"inputs": {
"filename_prefix": {
"name": "檔名前綴",
"tooltip": "要儲存檔案的字首。這可以包含格式化資訊,例如 %date:yyyy-MM-dd% 或 %Empty Latent Image.width%,以便從節點中包含數值。"
},
"svg": {
"name": "svg"
}
}
},
"SaveVideo": {
"description": "將輸入的影像儲存到您的 ComfyUI 輸出目錄。",
"display_name": "儲存影片",
@@ -8657,4 +8644,4 @@
}
}
}
}
}

View File

@@ -343,14 +343,6 @@
"Comfy_Validation_Workflows": {
"name": "驗證工作流程"
},
"Comfy_VueNodes_Enabled": {
"name": "啟用 Vue 節點渲染",
"tooltip": "將節點以 Vue 元件而非畫布元素方式渲染。實驗性功能。"
},
"Comfy_VueNodes_Widgets": {
"name": "啟用 Vue 小工具",
"tooltip": "在 Vue 節點中以 Vue 元件渲染小工具。"
},
"Comfy_WidgetControlMode": {
"name": "元件控制模式",
"options": {
@@ -396,10 +388,6 @@
"Topbar (2nd-row)": "頂部欄(第二列)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "低品質渲染縮放臨界值",
"tooltip": "當縮小檢視時以低品質渲染圖形"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "最大FPS",
"tooltip": "畫布允許渲染的最大每秒幀數。限制GPU使用率但可能影響流暢度。若設為0則使用螢幕的更新率。預設值0"
@@ -421,4 +409,4 @@
"pysssss_SnapToGrid": {
"name": "總是對齊格線"
}
}
}

View File

@@ -122,9 +122,6 @@
"Comfy_ExportWorkflowAPI": {
"label": "导出工作流API格式"
},
"Comfy_Feedback": {
"label": "反馈"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "将选区转换为子图"
},
@@ -309,4 +306,4 @@
"label": "切换工作流侧边栏",
"tooltip": "工作流"
}
}
}

View File

@@ -326,7 +326,6 @@
"frontendNewer": "前端版本 {frontendVersion} 可能与后端版本 {backendVersion} 不相容。",
"frontendOutdated": "前端版本 {frontendVersion} 已过时。后端需要 {requiredVersion} 或更高版本。",
"goToNode": "转到节点",
"help": "帮助",
"icon": "图标",
"imageFailedToLoad": "图像加载失败",
"imageUrl": "图片网址",
@@ -779,7 +778,6 @@
"File": "文件",
"Fit Group To Contents": "适应组内容",
"Focus Mode": "专注模式",
"Give Feedback": "提供反馈",
"Group Selected Nodes": "将选中节点转换为组节点",
"Help": "帮助",
"Help Center": "帮助中心",
@@ -833,15 +831,11 @@
"Show Settings Dialog": "显示设置对话框",
"Sign Out": "退出登录",
"Toggle Essential Bottom Panel": "切换基础底部面板",
"Toggle Bottom Panel": "切换底部面板",
"Toggle Focus Mode": "切换专注模式",
"Toggle Logs Bottom Panel": "切换日志底部面板",
"Toggle Search Box": "切换搜索框",
"Toggle Terminal Bottom Panel": "切换终端底部面板",
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
"Toggle View Controls Bottom Panel": "切换视图控制底部面板",
"Toggle Workflows Sidebar": "切换工作流侧边栏",
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
"Undo": "撤销",
"Ungroup selected group nodes": "解散选中组节点",
@@ -858,11 +852,7 @@
"renderBypassState": "渲染绕过状态",
"renderErrorState": "渲染错误状态",
"showGroups": "显示框架/分组",
"showLinks": "显示连接",
"sideToolbar_modelLibrary": "侧边工具栏.模型库",
"sideToolbar_nodeLibrary": "侧边工具栏.节点库",
"sideToolbar_queue": "侧边工具栏.队列",
"sideToolbar_workflows": "侧边工具栏.工作流"
"showLinks": "显示连接"
},
"missingModelsDialog": {
"doNotAskAgain": "不再显示此消息",
@@ -939,9 +929,6 @@
"upscale_diffusion": "放大扩散",
"upscaling": "放大",
"utils": "工具",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "视频",
"video_models": "视频模型"
},

View File

@@ -7366,19 +7366,6 @@
}
}
},
"SaveSVG": {
"description": "将 SVG 文件保存到磁盘。",
"display_name": "保存 SVG",
"inputs": {
"filename_prefix": {
"name": "文件名前缀",
"tooltip": "要保存文件的前缀。可以包含格式化信息,如 %date:yyyy-MM-dd% 或 %Empty Latent Image.width%,以包含来自节点的数值。"
},
"svg": {
"name": "svg"
}
}
},
"SaveVideo": {
"description": "将输入图像保存到您的 ComfyUI 输出目录。",
"display_name": "保存视频",
@@ -8657,4 +8644,4 @@
}
}
}
}
}

View File

@@ -343,14 +343,6 @@
"Comfy_Validation_Workflows": {
"name": "校验工作流"
},
"Comfy_VueNodes_Enabled": {
"name": "启用 Vue 节点渲染",
"tooltip": "将节点渲染为 Vue 组件,而不是画布元素。实验性功能。"
},
"Comfy_VueNodes_Widgets": {
"name": "启用Vue小部件",
"tooltip": "在Vue节点中将小部件渲染为Vue组件。"
},
"Comfy_WidgetControlMode": {
"name": "组件控制模式",
"options": {
@@ -396,10 +388,6 @@
"Topbar (2nd-row)": "顶部栏 (第二行)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "低质量渲染缩放阈值",
"tooltip": "在缩小时渲染低质量形状"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "最大FPS",
"tooltip": "画布允许渲染的最大帧数。限制GPU使用以换取流畅度。如果为0则使用屏幕刷新率。默认值0"
@@ -421,4 +409,4 @@
"pysssss_SnapToGrid": {
"name": "始终吸附到网格"
}
}
}

View File

@@ -12,6 +12,7 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore'
const ASSETS_ENDPOINT = '/assets'
const MODELS_TAG = 'models'
const MISSING_TAG = 'missing'
const EXPERIMENTAL_WARNING = `EXPERIMENTAL: If you are seeing this please make sure "Comfy.Assets.UseAssetAPI" is set to "false" in your ComfyUI Settings.\n`
/**
* Input names that are eligible for asset browser
@@ -26,7 +27,9 @@ function validateAssetResponse(data: unknown): AssetResponse {
if (result.success) return result.data
const error = fromZodError(result.error)
throw new Error(`Invalid asset response against zod schema:\n${error}`)
throw new Error(
`${EXPERIMENTAL_WARNING}Invalid asset response against zod schema:\n${error}`
)
}
/**
@@ -44,7 +47,7 @@ function createAssetService() {
const res = await api.fetchApi(url)
if (!res.ok) {
throw new Error(
`Unable to load ${context}: Server returned ${res.status}. Please try again.`
`${EXPERIMENTAL_WARNING}Unable to load ${context}: Server returned ${res.status}. Please try again.`
)
}
const data = await res.json()

View File

@@ -1051,7 +1051,7 @@ export const CORE_SETTINGS: SettingParams[] = [
{
id: 'Comfy.Assets.UseAssetAPI',
name: 'Use Asset API for model library',
type: 'boolean',
type: 'hidden',
tooltip: 'Use new Asset API for model browsing',
defaultValue: false,
experimental: true

View File

@@ -17,7 +17,7 @@ import { downloadBlob } from '@/scripts/utils'
import { useDialogService } from '@/services/dialogService'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { appendJsonExt, generateUUID } from '@/utils/formatUtil'
import { appendJsonExt } from '@/utils/formatUtil'
export const useWorkflowService = () => {
const settingStore = useSettingStore()
@@ -112,13 +112,6 @@ export const useWorkflowService = () => {
await renameWorkflow(workflow, newPath)
await workflowStore.saveWorkflow(workflow)
} else {
// Generate new id when saving existing workflow as a new file
const id = generateUUID()
const state = JSON.parse(
JSON.stringify(workflow.activeState)
) as ComfyWorkflowJSON
state.id = id
const tempWorkflow = workflowStore.saveAs(workflow, newPath)
await openWorkflow(tempWorkflow)
await workflowStore.saveWorkflow(tempWorkflow)

View File

@@ -20,7 +20,7 @@ import {
parseNodeExecutionId,
parseNodeLocatorId
} from '@/types/nodeIdentification'
import { getPathDetails } from '@/utils/formatUtil'
import { generateUUID, getPathDetails } from '@/utils/formatUtil'
import { syncEntities } from '@/utils/syncUtil'
import { isSubgraph } from '@/utils/typeGuardUtil'
@@ -320,12 +320,19 @@ export const useWorkflowStore = defineStore('workflow', () => {
existingWorkflow: ComfyWorkflow,
path: string
): ComfyWorkflow => {
// Generate new id when saving existing workflow as a new file
const id = generateUUID()
const state = JSON.parse(
JSON.stringify(existingWorkflow.activeState)
) as ComfyWorkflowJSON
state.id = id
const workflow: ComfyWorkflow = new (existingWorkflow.constructor as any)({
path,
modified: Date.now(),
size: -1
})
workflow.originalContent = workflow.content = existingWorkflow.content
workflow.originalContent = workflow.content = JSON.stringify(state)
workflowLookup.value[workflow.path] = workflow
return workflow
}

View File

@@ -116,6 +116,12 @@ const router = createRouter({
name: 'DesktopUpdateView',
component: () => import('@/views/DesktopUpdateView.vue'),
beforeEnter: guardElectronAccess
},
{
path: 'desktop-dialog/:dialogId',
name: 'DesktopDialogView',
component: () => import('@/views/DesktopDialogView.vue'),
beforeEnter: guardElectronAccess
}
]
}

View File

@@ -4,16 +4,3 @@ export enum ValidationState {
VALID = 'VALID',
INVALID = 'INVALID'
}
export const mergeValidationStates = (states: ValidationState[]) => {
if (states.some((state) => state === ValidationState.INVALID)) {
return ValidationState.INVALID
}
if (states.some((state) => state === ValidationState.LOADING)) {
return ValidationState.LOADING
}
if (states.every((state) => state === ValidationState.VALID)) {
return ValidationState.VALID
}
return ValidationState.IDLE
}

View File

@@ -0,0 +1,70 @@
<template>
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
<h1 class="font-inter font-semibold text-xl m-0 italic">
{{ t(`desktopDialogs.${id}.title`, title) }}
</h1>
<p class="whitespace-pre-wrap">
{{ t(`desktopDialogs.${id}.message`, message) }}
</p>
<div class="flex w-full gap-2">
<Button
v-for="button in buttons"
:key="button.label"
class="rounded-lg first:mr-auto"
:label="
t(
`desktopDialogs.${id}.buttons.${normalizeI18nKey(button.label)}`,
button.label
)
"
:severity="button.severity ?? 'secondary'"
@click="handleButtonClick(button)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useRoute } from 'vue-router'
import { type DialogAction, getDialog } from '@/constants/desktopDialogs'
import { t } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
const route = useRoute()
const { id, title, message, buttons } = getDialog(route.params.dialogId)
const handleButtonClick = async (button: DialogAction) => {
await electronAPI().Dialog.clickButton(button.returnValue)
}
</script>
<style scoped>
@reference '../assets/css/style.css';
.p-button-secondary {
@apply text-white border-none bg-neutral-600;
}
.p-button-secondary:hover {
@apply bg-neutral-550;
}
.p-button-secondary:active {
@apply bg-neutral-500;
}
.p-button-danger {
@apply bg-coral-red-600;
}
.p-button-danger:hover {
@apply bg-coral-red-500;
}
.p-button-danger:active {
@apply bg-coral-red-400;
}
</style>

View File

@@ -1,11 +1,11 @@
<template>
<BaseViewTemplate dark>
<ProgressSpinner class="m-8 w-48 h-48" />
<StartupDisplay :title="$t('desktopStart.initialising')" />
</BaseViewTemplate>
</template>
<script setup lang="ts">
import ProgressSpinner from 'primevue/progressspinner'
import StartupDisplay from '@/components/common/StartupDisplay.vue'
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
</script>

View File

@@ -0,0 +1,423 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import { nextTick, provide } from 'vue'
import { createMemoryHistory, createRouter } from 'vue-router'
import InstallView from './InstallView.vue'
// Create a mock router for stories
const createMockRouter = () =>
createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: { template: '<div>Home</div>' } },
{
path: '/server-start',
component: { template: '<div>Server Start</div>' }
},
{
path: '/manual-configuration',
component: { template: '<div>Manual Configuration</div>' }
}
]
})
const meta: Meta<typeof InstallView> = {
title: 'Desktop/Views/InstallView',
component: InstallView,
parameters: {
layout: 'fullscreen',
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
{ name: 'neutral-900', value: '#171717' },
{ name: 'neutral-950', value: '#0a0a0a' }
]
}
},
decorators: [
(story) => {
// Create router for this story
const router = createMockRouter()
// Mock electron API
;(window as any).electronAPI = {
getPlatform: () => 'darwin',
Config: {
getDetectedGpu: () => Promise.resolve('mps')
},
Events: {
trackEvent: (eventName: string, data?: any) => {
console.log('Track event:', eventName, data)
}
},
installComfyUI: (options: any) => {
console.log('Install ComfyUI with options:', options)
},
changeTheme: (theme: any) => {
console.log('Change theme:', theme)
},
getSystemPaths: () =>
Promise.resolve({
defaultInstallPath: '/Users/username/ComfyUI'
}),
validateInstallPath: () =>
Promise.resolve({
isValid: true,
exists: false,
canWrite: true,
freeSpace: 100000000000,
requiredSpace: 10000000000,
isNonDefaultDrive: false
}),
validateComfyUISource: () =>
Promise.resolve({
isValid: true
}),
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
}
return {
setup() {
// Provide router for all child components
provide('router', router)
return {
story
}
},
template: '<div style="width: 100vw; height: 100vh;"><story /></div>'
}
}
]
}
export default meta
type Story = StoryObj<typeof meta>
// Default story - start at GPU selection
export const GpuSelection: Story = {
render: () => ({
components: { InstallView },
setup() {
// The component will automatically start at step 1
return {}
},
template: '<InstallView />'
})
}
// Story showing the install location step
export const InstallLocation: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
})
}
// Story showing the migration step (currently empty)
export const MigrationStep: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons1 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn1) {
nextBtn1.click()
}
await nextTick()
// Click Next again to go to step 3
const buttons2 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn2) {
nextBtn2.click()
}
},
template: '<InstallView />'
})
}
// Story showing the desktop settings configuration
export const DesktopSettings: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons1 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn1) {
nextBtn1.click()
}
await nextTick()
// Click Next again to go to step 3
const buttons2 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn2) {
nextBtn2.click()
}
await nextTick()
// Click Next again to go to step 4
const buttons3 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn3 = buttons3.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn3) {
nextBtn3.click()
}
},
template: '<InstallView />'
})
}
// Story with Windows platform (no Apple Metal option)
export const WindowsPlatform: Story = {
render: () => {
// Override the platform to Windows
;(window as any).electronAPI.getPlatform = () => 'win32'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('nvidia')
return {
components: { InstallView },
setup() {
return {}
},
template: '<InstallView />'
}
}
}
// Story with macOS platform (Apple Metal option)
export const MacOSPlatform: Story = {
name: 'macOS Platform',
render: () => {
// Override the platform to macOS
;(window as any).electronAPI.getPlatform = () => 'darwin'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('mps')
return {
components: { InstallView },
setup() {
return {}
},
template: '<InstallView />'
}
}
}
// Story with CPU selected
export const CpuSelected: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Find and click the CPU hardware option
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
// CPU is the button with "CPU" text
for (const button of hardwareButtons) {
if (button.textContent?.includes('CPU')) {
button.click()
break
}
}
},
template: '<InstallView />'
})
}
// Story with manual install selected
export const ManualInstall: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Find and click the Manual Install hardware option
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
// Manual Install is the button with "Manual Install" text
for (const button of hardwareButtons) {
if (button.textContent?.includes('Manual Install')) {
button.click()
break
}
}
},
template: '<InstallView />'
})
}
// Story with error state (invalid install path)
export const ErrorState: Story = {
render: () => {
// Override validation to return an error
;(window as any).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: false,
exists: false,
canWrite: false,
freeSpace: 100000000000,
requiredSpace: 10000000000,
isNonDefaultDrive: false,
error: 'Story mock: Example error state'
})
return {
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2 where error will be shown
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
}
}
}
// Story with warning state (non-default drive)
export const WarningState: Story = {
render: () => {
// Override validation to return a warning about non-default drive
;(window as any).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: true,
exists: false,
canWrite: true,
freeSpace: 500_000_000_000,
requiredSpace: 10_000_000_000,
isNonDefaultDrive: true
})
return {
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll('.hardware-option')
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2 where warning will be shown
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
}
}
}

View File

@@ -1,111 +1,54 @@
<template>
<BaseViewTemplate dark>
<!-- h-full to make sure the stepper does not layout shift between steps
as for each step the stepper height is different. Inherit the center element
placement from BaseViewTemplate would cause layout shift. -->
<Stepper
class="h-full p-8 2xl:p-16"
value="0"
@update:value="handleStepChange"
>
<StepList class="select-none">
<Step value="0">
{{ $t('install.gpu') }}
</Step>
<Step value="1" :disabled="noGpu">
{{ $t('install.installLocation') }}
</Step>
<Step value="2" :disabled="noGpu || hasError || highestStep < 1">
{{ $t('install.migration') }}
</Step>
<Step value="3" :disabled="noGpu || hasError || highestStep < 2">
{{ $t('install.desktopSettings') }}
</Step>
</StepList>
<StepPanels>
<StepPanel v-slot="{ activateCallback }" value="0">
<GpuPicker v-model:device="device" />
<div class="flex pt-6 justify-end">
<Button
:label="$t('g.next')"
icon="pi pi-arrow-right"
icon-pos="right"
:disabled="typeof device !== 'string'"
@click="activateCallback('1')"
<!-- Fixed height container with flexbox layout for proper content management -->
<div class="w-full h-full flex flex-col">
<Stepper
v-model:value="currentStep"
class="flex flex-col h-full"
@update:value="handleStepChange"
>
<!-- Main content area that grows to fill available space -->
<StepPanels
class="flex-1 overflow-auto"
:style="{ scrollbarGutter: 'stable' }"
>
<StepPanel value="1" class="flex">
<GpuPicker v-model:device="device" />
</StepPanel>
<StepPanel value="2">
<InstallLocationPicker
v-model:install-path="installPath"
v-model:path-error="pathError"
v-model:migration-source-path="migrationSourcePath"
v-model:migration-item-ids="migrationItemIds"
v-model:python-mirror="pythonMirror"
v-model:pypi-mirror="pypiMirror"
v-model:torch-mirror="torchMirror"
:device="device"
/>
</div>
</StepPanel>
<StepPanel v-slot="{ activateCallback }" value="1">
<InstallLocationPicker
v-model:install-path="installPath"
v-model:path-error="pathError"
/>
<div class="flex pt-6 justify-between">
<Button
:label="$t('g.back')"
severity="secondary"
icon="pi pi-arrow-left"
@click="activateCallback('0')"
</StepPanel>
<StepPanel value="3">
<DesktopSettingsConfiguration
v-model:auto-update="autoUpdate"
v-model:allow-metrics="allowMetrics"
/>
<Button
:label="$t('g.next')"
icon="pi pi-arrow-right"
icon-pos="right"
:disabled="pathError !== ''"
@click="activateCallback('2')"
/>
</div>
</StepPanel>
<StepPanel v-slot="{ activateCallback }" value="2">
<MigrationPicker
v-model:source-path="migrationSourcePath"
v-model:migration-item-ids="migrationItemIds"
/>
<div class="flex pt-6 justify-between">
<Button
:label="$t('g.back')"
severity="secondary"
icon="pi pi-arrow-left"
@click="activateCallback('1')"
/>
<Button
:label="$t('g.next')"
icon="pi pi-arrow-right"
icon-pos="right"
@click="activateCallback('3')"
/>
</div>
</StepPanel>
<StepPanel v-slot="{ activateCallback }" value="3">
<DesktopSettingsConfiguration
v-model:auto-update="autoUpdate"
v-model:allow-metrics="allowMetrics"
/>
<MirrorsConfiguration
v-model:python-mirror="pythonMirror"
v-model:pypi-mirror="pypiMirror"
v-model:torch-mirror="torchMirror"
:device="device"
class="mt-6"
/>
<div class="flex mt-6 justify-between">
<Button
:label="$t('g.back')"
severity="secondary"
icon="pi pi-arrow-left"
@click="activateCallback('2')"
/>
<Button
:label="$t('g.install')"
icon="pi pi-check"
icon-pos="right"
:disabled="hasError"
@click="install()"
/>
</div>
</StepPanel>
</StepPanels>
</Stepper>
</StepPanel>
</StepPanels>
<!-- Install footer with navigation -->
<InstallFooter
class="w-full max-w-2xl my-6 mx-auto"
:current-step
:can-proceed
:disable-location-step="noGpu"
:disable-migration-step="noGpu || hasError || highestStep < 2"
:disable-settings-step="noGpu || hasError || highestStep < 3"
@previous="goToPreviousStep"
@next="goToNextStep"
@install="install"
/>
</Stepper>
</div>
</BaseViewTemplate>
</template>
@@ -114,9 +57,6 @@ import type {
InstallOptions,
TorchDeviceType
} from '@comfyorg/comfyui-electron-types'
import Button from 'primevue/button'
import Step from 'primevue/step'
import StepList from 'primevue/steplist'
import StepPanel from 'primevue/steppanel'
import StepPanels from 'primevue/steppanels'
import Stepper from 'primevue/stepper'
@@ -125,9 +65,8 @@ import { useRouter } from 'vue-router'
import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue'
import GpuPicker from '@/components/install/GpuPicker.vue'
import InstallFooter from '@/components/install/InstallFooter.vue'
import InstallLocationPicker from '@/components/install/InstallLocationPicker.vue'
import MigrationPicker from '@/components/install/MigrationPicker.vue'
import MirrorsConfiguration from '@/components/install/MirrorsConfiguration.vue'
import { electronAPI } from '@/utils/envUtil'
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
@@ -145,6 +84,9 @@ const pythonMirror = ref('')
const pypiMirror = ref('')
const torchMirror = ref('')
/** Current step in the stepper */
const currentStep = ref('1')
/** Forces each install step to be visited at least once. */
const highestStep = ref(0)
@@ -164,6 +106,40 @@ const setHighestStep = (value: string | number) => {
const hasError = computed(() => pathError.value !== '')
const noGpu = computed(() => typeof device.value !== 'string')
// Computed property to determine if user can proceed to next step
const regex = /^Insufficient space - minimum free space: \d+ GB$/
const canProceed = computed(() => {
switch (currentStep.value) {
case '1':
return typeof device.value === 'string'
case '2':
return pathError.value === '' || regex.test(pathError.value)
case '3':
return !hasError.value
default:
return false
}
})
// Navigation methods
const goToNextStep = () => {
const nextStep = (parseInt(currentStep.value) + 1).toString()
currentStep.value = nextStep
setHighestStep(nextStep)
electronAPI().Events.trackEvent('install_stepper_change', {
step: nextStep
})
}
const goToPreviousStep = () => {
const prevStep = (parseInt(currentStep.value) - 1).toString()
currentStep.value = prevStep
electronAPI().Events.trackEvent('install_stepper_change', {
step: prevStep
})
}
const electron = electronAPI()
const router = useRouter()
const install = async () => {
@@ -195,7 +171,7 @@ onMounted(async () => {
}
electronAPI().Events.trackEvent('install_stepper_change', {
step: '0',
step: currentStep.value,
gpu: detectedGpu
})
})
@@ -205,6 +181,30 @@ onMounted(async () => {
@reference '../assets/css/style.css';
:deep(.p-steppanel) {
@apply mt-8 flex justify-center bg-transparent;
}
/* Remove default padding/margin from StepPanels to make scrollbar flush */
:deep(.p-steppanels) {
@apply p-0 m-0;
}
/* Ensure StepPanel content container has no top/bottom padding */
:deep(.p-steppanel-content) {
@apply p-0;
}
/* Custom overlay scrollbar for WebKit browsers (Electron, Chrome) */
:deep(.p-steppanels::-webkit-scrollbar) {
@apply w-4;
}
:deep(.p-steppanels::-webkit-scrollbar-track) {
@apply bg-transparent;
}
:deep(.p-steppanels::-webkit-scrollbar-thumb) {
@apply bg-white/20 rounded-lg border-[4px] border-transparent;
background-clip: content-box;
}
</style>

View File

@@ -1,75 +1,193 @@
<template>
<BaseViewTemplate dark class="flex-col">
<div class="flex flex-col w-full h-full items-center">
<h2 class="text-2xl font-bold">
{{ t(`serverStart.process.${status}`) }}
<span v-if="status === ProgressStatus.ERROR">
v{{ electronVersion }}
</span>
</h2>
<div
v-if="status === ProgressStatus.ERROR"
class="flex flex-col items-center gap-4"
>
<div class="flex items-center my-4 gap-2">
<Button
icon="pi pi-flag"
severity="secondary"
:label="t('serverStart.reportIssue')"
@click="reportIssue"
/>
<Button
icon="pi pi-file"
severity="secondary"
:label="t('serverStart.openLogs')"
@click="openLogs"
/>
<Button
icon="pi pi-wrench"
:label="t('serverStart.troubleshoot')"
@click="troubleshoot"
/>
<BaseViewTemplate dark>
<div class="relative min-h-screen">
<!-- Terminal Background Layer (always visible during loading) -->
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
<div class="h-full w-full">
<BaseTerminal @created="terminalCreated" />
</div>
</div>
<!-- Semi-transparent overlay -->
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
<!-- Smooth radial gradient overlay -->
<div
v-if="!isError"
class="fixed inset-0 z-8"
style="
background: radial-gradient(
ellipse 800px 600px at center,
rgba(23, 23, 23, 0.95) 0%,
rgba(23, 23, 23, 0.93) 10%,
rgba(23, 23, 23, 0.9) 20%,
rgba(23, 23, 23, 0.85) 30%,
rgba(23, 23, 23, 0.75) 40%,
rgba(23, 23, 23, 0.6) 50%,
rgba(23, 23, 23, 0.4) 60%,
rgba(23, 23, 23, 0.2) 70%,
rgba(23, 23, 23, 0.1) 80%,
rgba(23, 23, 23, 0.05) 90%,
transparent 100%
);
"
></div>
<div class="relative z-10">
<!-- Main startup display using StartupDisplay component -->
<StartupDisplay
:title="displayTitle"
:status-text="displayStatusText"
:progress-percentage="installStageProgress"
:hide-progress="isError"
/>
<!-- Error Section (positioned at bottom) -->
<div
v-if="isError"
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
>
<div class="flex gap-4 justify-center">
<Button
icon="pi pi-flag"
:label="$t('serverStart.reportIssue')"
severity="secondary"
@click="reportIssue"
/>
<Button
icon="pi pi-file"
:label="$t('serverStart.openLogs')"
severity="secondary"
@click="openLogs"
/>
<Button
icon="pi pi-wrench"
:label="$t('serverStart.troubleshoot')"
@click="troubleshoot"
/>
</div>
</div>
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
<div
v-if="terminalVisible && isError"
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
>
<div
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
>
<BaseTerminal @created="terminalCreated" />
</div>
</div>
<Button
v-if="!terminalVisible"
icon="pi pi-search"
severity="secondary"
:label="t('serverStart.showTerminal')"
@click="terminalVisible = true"
/>
</div>
<BaseTerminal v-show="terminalVisible" @created="terminalCreated" />
</div>
</BaseViewTemplate>
</template>
<script setup lang="ts">
import { ProgressStatus } from '@comfyorg/comfyui-electron-types'
import {
InstallStage,
type InstallStageInfo,
type InstallStageName,
ProgressStatus
} from '@comfyorg/comfyui-electron-types'
import { Terminal } from '@xterm/xterm'
import Button from 'primevue/button'
import { Ref, onMounted, ref } from 'vue'
import { Ref, computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
import StartupDisplay from '@/components/common/StartupDisplay.vue'
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
import { electronAPI } from '@/utils/envUtil'
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
const electron = electronAPI()
const { t } = useI18n()
const electron = electronAPI()
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
const electronVersion = ref<string>('')
const terminalVisible = ref(false)
const installStage = ref<InstallStageName | null>(null)
const installStageMessage = ref<string>('')
const installStageProgress = ref<number | undefined>(undefined)
let xterm: Terminal | undefined
const terminalVisible = ref(true)
/**
* Handles installation stage updates from the desktop
*/
const updateInstallStage = (stageInfo: InstallStageInfo) => {
console.warn('[InstallStage.onUpdate] Received:', {
stage: stageInfo.stage,
progress: stageInfo.progress,
message: stageInfo.message,
error: stageInfo.error,
timestamp: stageInfo.timestamp,
fullInfo: stageInfo
})
installStage.value = stageInfo.stage
installStageMessage.value = stageInfo.message || ''
installStageProgress.value = stageInfo.progress
}
const currentStatusLabel = computed(() => {
// Use the message from the Electron API if available
if (installStageMessage.value) {
return installStageMessage.value
}
return t(`serverStart.process.${status.value}`)
})
const isError = computed(
() =>
status.value === ProgressStatus.ERROR ||
installStage.value === InstallStage.ERROR
)
const isInstallationStage = computed(() => {
const installationStages: InstallStageName[] = [
InstallStage.WELCOME_SCREEN,
InstallStage.INSTALL_OPTIONS_SELECTION,
InstallStage.CREATING_DIRECTORIES,
InstallStage.INITIALIZING_CONFIG,
InstallStage.PYTHON_ENVIRONMENT_SETUP,
InstallStage.INSTALLING_REQUIREMENTS,
InstallStage.INSTALLING_PYTORCH,
InstallStage.INSTALLING_COMFYUI_REQUIREMENTS,
InstallStage.INSTALLING_MANAGER_REQUIREMENTS,
InstallStage.MIGRATING_CUSTOM_NODES
]
return (
installStage.value !== null &&
installationStages.includes(installStage.value)
)
})
const displayTitle = computed(() => {
if (isError.value) {
return t('serverStart.errorMessage')
}
if (isInstallationStage.value) {
return t('serverStart.installation.title')
}
return t('serverStart.title')
})
const displayStatusText = computed(() => {
if (isError.value && electronVersion.value) {
return `v${electronVersion.value}`
}
return currentStatusLabel.value
})
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
status.value = newStatus
// Make critical error screen more obvious.
if (newStatus === ProgressStatus.ERROR) terminalVisible.value = false
else xterm?.clear()
}
const terminalCreated = (
@@ -94,9 +212,30 @@ const reportIssue = () => {
}
const openLogs = () => electron.openLogsFolder()
let cleanupInstallStageListener: (() => void) | undefined
onMounted(async () => {
electron.sendReady()
electron.onProgressUpdate(updateProgress)
cleanupInstallStageListener =
electron.InstallStage.onUpdate(updateInstallStage)
const stageInfo = await electron.InstallStage.getCurrent()
updateInstallStage(stageInfo)
electronVersion.value = await electron.getElectronVersion()
})
onUnmounted(() => {
xterm?.dispose()
cleanupInstallStageListener?.()
})
</script>
<style scoped>
@reference '../assets/css/style.css';
/* Hide the xterm scrollbar completely */
:deep(.p-terminal) .xterm-viewport {
overflow: hidden !important;
}
</style>

View File

@@ -1,21 +1,27 @@
<template>
<BaseViewTemplate dark>
<div class="flex flex-col items-center justify-center gap-8 p-8">
<!-- Header -->
<h1 class="animated-gradient-text text-glow select-none">
{{ $t('welcome.title') }}
</h1>
<!-- Get Started Button -->
<Button
:label="$t('welcome.getStarted')"
icon="pi pi-arrow-right"
icon-pos="right"
size="large"
rounded
class="p-4 text-lg fade-in-up"
@click="navigateTo('/install')"
/>
<div class="flex items-center justify-center min-h-screen">
<div class="grid grid-rows-2 gap-8">
<!-- Top container: Logo -->
<div class="flex items-end justify-center">
<img
src="/assets/images/comfy-brand-mark.svg"
:alt="$t('g.logoAlt')"
class="w-60"
/>
</div>
<!-- Bottom container: Title and button -->
<div class="flex flex-col items-center justify-center gap-4">
<Button
:label="$t('welcome.getStarted')"
class="px-8 mt-4 bg-brand-yellow hover:bg-brand-yellow/90 border-0 rounded-lg transition-colors"
:pt="{
label: { class: 'font-inter text-neutral-900 font-black' }
}"
@click="navigateTo('/install')"
/>
</div>
</div>
</div>
</BaseViewTemplate>
</template>
@@ -31,49 +37,3 @@ const navigateTo = async (path: string) => {
await router.push(path)
}
</script>
<style scoped>
@reference '../assets/css/style.css';
.animated-gradient-text {
@apply font-bold;
font-size: clamp(2rem, 8vw, 4rem);
background: linear-gradient(to right, #12c2e9, #c471ed, #f64f59, #12c2e9);
background-size: 300% auto;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradient 8s linear infinite;
}
.text-glow {
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
}
@keyframes gradient {
0% {
background-position: 0% center;
}
100% {
background-position: 300% center;
}
}
.fade-in-up {
animation: fadeInUp 1.5s ease-out;
animation-fill-mode: both;
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -269,7 +269,115 @@ describe('useNodePricing', () => {
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
})
})
// ============================== OpenAIVideoSora2 ==============================
describe('dynamic pricing - OpenAIVideoSora2', () => {
it('should require model, duration & size when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [])
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
})
it('should require duration when duration is invalid or zero', () => {
const { getNodeDisplayPrice } = useNodePricing()
const nodeNaN = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 'oops' },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeNaN)).toBe('Set model, duration & size')
const nodeZero = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 0 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeZero)).toBe('Set model, duration & size')
})
it('should require size when size is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 }
])
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
})
it('should compute pricing for sora-2-pro with 1024x1792', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.5 * 8
})
it('should compute pricing for sora-2-pro with 720x1280', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
it('should reject unsupported size for sora-2-pro', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '640x640' }
])
expect(getNodeDisplayPrice(node)).toBe(
'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
)
})
it('should compute pricing for sora-2 (720x1280 only)', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 10 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.1 * 10
})
it('should reject non-720 sizes for sora-2', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe(
'sora-2 supports only 720x1280 or 1280x720'
)
})
it('should accept duration_s alias for duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration_s', value: 4 },
{ name: 'size', value: '1792x1024' }
])
expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.5 * 4
})
it('should be case-insensitive for model and size', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'SoRa-2-PrO' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '1280x720' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
})
// ============================== MinimaxHailuoVideoNode ==============================
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
it('should return $0.28 for 6s duration and 768P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
@@ -1894,4 +2002,159 @@ describe('useNodePricing', () => {
expect(getNodeDisplayPrice(missingDuration)).toBe('Token-based')
})
})
describe('dynamic pricing - WanTextToVideoApi', () => {
it('should return $1.50 for 10s at 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'size', value: '1080p: 4:3 (1632x1248)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.50/Run') // 0.15 * 10
})
it('should return $0.50 for 5s at 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: 5 },
{ name: 'size', value: '720p: 16:9 (1280x720)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.50/Run') // 0.10 * 5
})
it('should return $0.15 for 3s at 480p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '3' },
{ name: 'size', value: '480p: 1:1 (624x624)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.15/Run') // 0.05 * 3
})
it('should fall back when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingBoth = createMockNode('WanTextToVideoApi', [])
const missingSize = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '5' }
])
const missingDuration = createMockNode('WanTextToVideoApi', [
{ name: 'size', value: '1080p' }
])
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingSize)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
})
it('should fall back on invalid duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: 'invalid' },
{ name: 'size', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
it('should fall back on unknown resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'size', value: '2K' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
})
describe('dynamic pricing - WanImageToVideoApi', () => {
it('should return $0.80 for 8s at 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: 8 },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.80/Run') // 0.10 * 8
})
it('should return $0.60 for 12s at 480P', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '12' },
{ name: 'resolution', value: '480P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.60/Run') // 0.05 * 12
})
it('should return $1.50 for 10s at 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.50/Run') // 0.15 * 10
})
it('should handle "5s" string duration at 1080P', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '5s' },
{ name: 'resolution', value: '1080P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.75/Run') // 0.15 * 5
})
it('should fall back when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingBoth = createMockNode('WanImageToVideoApi', [])
const missingRes = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '5' }
])
const missingDuration = createMockNode('WanImageToVideoApi', [
{ name: 'resolution', value: '1080p' }
])
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingRes)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
})
it('should fall back on invalid duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: 'invalid' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
it('should fall back on unknown resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: 'weird-res' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
})
})

View File

@@ -56,10 +56,10 @@ describe('useManagerState', () => {
})
describe('managerUIState property', () => {
it('should return DISABLED state when --disable-manager is present', () => {
it('should return DISABLED state when --enable-manager is NOT present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--disable-manager'] }
system: { argv: ['python', 'main.py'] } // No --enable-manager flag
}),
isInitialized: ref(true)
} as any)
@@ -76,7 +76,14 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when --enable-manager-legacy-ui is present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
system: {
argv: [
'python',
'main.py',
'--enable-manager',
'--enable-manager-legacy-ui'
]
} // Both flags needed
}),
isInitialized: ref(true)
} as any)
@@ -92,7 +99,9 @@ describe('useManagerState', () => {
it('should return NEW_UI state when client and server both support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -114,7 +123,9 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when server supports v4 but client does not', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -136,7 +147,9 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when legacy manager extension exists', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -155,7 +168,9 @@ describe('useManagerState', () => {
it('should return NEW_UI state when server feature flags are undefined', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -175,7 +190,9 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when server does not support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -212,14 +229,17 @@ describe('useManagerState', () => {
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI)
// When systemStats is null, we can't check for --enable-manager flag, so manager is disabled
expect(managerState.managerUIState.value).toBe(ManagerUIState.DISABLED)
})
})
describe('helper properties', () => {
it('isManagerEnabled should return true when state is not DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -237,7 +257,7 @@ describe('useManagerState', () => {
it('isManagerEnabled should return false when state is DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--disable-manager'] }
system: { argv: ['python', 'main.py'] } // No --enable-manager flag means disabled
}),
isInitialized: ref(true)
} as any)
@@ -252,7 +272,9 @@ describe('useManagerState', () => {
it('isNewManagerUI should return true when state is NEW_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -270,7 +292,14 @@ describe('useManagerState', () => {
it('isLegacyManagerUI should return true when state is LEGACY_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
system: {
argv: [
'python',
'main.py',
'--enable-manager',
'--enable-manager-legacy-ui'
]
} // Both flags needed
}),
isInitialized: ref(true)
} as any)
@@ -285,7 +314,9 @@ describe('useManagerState', () => {
it('shouldShowInstallButton should return true only for NEW_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -302,7 +333,9 @@ describe('useManagerState', () => {
it('shouldShowManagerButtons should return true when not DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({