Merge branch 'main' into feat/new-workflow-templates

This commit is contained in:
Johnpaul
2025-08-20 17:39:06 +01:00
30 changed files with 171 additions and 843 deletions

View File

@@ -93,6 +93,44 @@ export const WithVariant: Story = {
## Development Tips ## Development Tips
## ComfyUI Storybook Guidelines
### Scope When to Create Stories
- **PrimeVue components**:
No need to create stories. Just refer to the official PrimeVue documentation.
- **Custom shared components (design system components)**:
Always create stories. These components are built in collaboration with designers, and Storybook serves as both documentation and a communication tool.
- **Container components (logic-heavy)**:
Do not create stories. Only the underlying pure UI components should be included in Storybook.
### Maintenance Philosophy
- Stories are lightweight and generally stable.
Once created, they rarely need updates unless:
- The design changes
- New props (e.g. size, color variants) are introduced
- For existing usage patterns, simply copy real code examples into Storybook to create stories.
### File Placement
- Keep `*.stories.ts` files at the **same level as the component** (similar to test files).
- This makes it easier to check usage examples without navigating to another directory.
### Developer/Designer Workflow
- **UI vs Container**: Separate pure UI components from container components.
Only UI components should live in Storybook.
- **Communication Tool**: Storybook is not just about code quality—it enables designers and developers to see:
- Which props exist
- What cases are covered
- How variants (e.g. size, colors) look in isolation
- **Example**:
`PackActionButton.vue` wraps a PrimeVue button with additional logic.
→ Only create a story for the base UI button, not for the wrapper.
### Suggested Workflow
1. Use PrimeVue docs for standard components
2. Use Storybook for **shared/custom components** that define our design system
3. Keep story files alongside components
4. When in doubt, focus on components reused across the app or those that need to be showcased to designers
### Best Practices ### Best Practices
1. **Keep Stories Simple**: Each story should demonstrate one specific use case 1. **Keep Stories Simple**: Each story should demonstrate one specific use case
@@ -135,6 +173,7 @@ export const WithLongText: Story = {
- **`main.ts`**: Core Storybook configuration and Vite integration - **`main.ts`**: Core Storybook configuration and Vite integration
- **`preview.ts`**: Global decorators, parameters, and Vue app setup - **`preview.ts`**: Global decorators, parameters, and Vue app setup
- **`manager.ts`**: Storybook UI customization (if needed) - **`manager.ts`**: Storybook UI customization (if needed)
- **`preview-head.html`**: Injects custom HTML into the `<head>` of every Storybook iframe (used for global styles, fonts, or fixes for iframe-specific issues)
## Chromatic Visual Testing ## Chromatic Visual Testing
@@ -170,4 +209,4 @@ This Storybook setup includes:
- PrimeVue component library integration - PrimeVue component library integration
- Proper alias resolution for `@/` imports - Proper alias resolution for `@/` imports
For component-specific examples, see the NodePreview stories in `src/components/node/`. For component-specific examples, see the NodePreview stories in `src/components/node/`.

View File

@@ -392,16 +392,6 @@ Option 2 - Generate local baselines for comparison:
npx playwright test --update-snapshots npx playwright test --update-snapshots
``` ```
### Getting Test Artifacts from GitHub Actions
When tests fail in CI, you can download screenshots and traces:
1. Go to the failed workflow run in GitHub Actions
2. Scroll to "Artifacts" section at the bottom
3. Download `playwright-report` or `test-results`
4. Extract and open the HTML report locally
5. View actual vs expected screenshots and execution traces
### Creating New Screenshot Baselines ### Creating New Screenshot Baselines
For PRs from `Comfy-Org/ComfyUI_frontend` branches: For PRs from `Comfy-Org/ComfyUI_frontend` branches:
@@ -412,17 +402,19 @@ For PRs from `Comfy-Org/ComfyUI_frontend` branches:
> **Note:** Fork PRs cannot auto-commit screenshots. A maintainer will need to commit the screenshots manually for you (don't worry, they'll do it). > **Note:** Fork PRs cannot auto-commit screenshots. A maintainer will need to commit the screenshots manually for you (don't worry, they'll do it).
## CI/CD Integration ## Viewing Test Reports
### Automated Test Deployment ### Automated Test Deployment
The project automatically deploys Playwright test reports to Cloudflare Pages for every PR and push to main branches. This provides: The project automatically deploys Playwright test reports to Cloudflare Pages for every PR and push to main branches.
- **Live test reports** with interactive HTML views ### Accessing Test Reports
- **Cross-browser testing** across chromium, mobile-chrome, and different viewport sizes
- **Real-time PR comments** with test status and links to detailed reports
#### How it works: - **From PR comments**: Click the "View Report" links for each browser
- **Direct URLs**: Reports are available at `https://[branch].comfyui-playwright-[browser].pages.dev` (branch-specific deployments)
- **From GitHub Actions**: Download artifacts from failed runs
### How It Works
1. **Test execution**: All browser tests run in parallel across multiple browsers 1. **Test execution**: All browser tests run in parallel across multiple browsers
2. **Report generation**: HTML reports are generated for each browser configuration 2. **Report generation**: HTML reports are generated for each browser configuration
@@ -437,21 +429,6 @@ The project automatically deploys Playwright test reports to Cloudflare Pages fo
- Direct links to interactive test reports - Direct links to interactive test reports
- Real-time progress updates as tests complete - Real-time progress updates as tests complete
#### Accessing test reports:
- **From PR comments**: Click the "View Report" links for each browser
- **From GitHub Actions**: Download artifacts from failed runs
- **Direct URLs**: Reports are available at `https://[branch].comfyui-playwright-[browser].pages.dev` (branch-specific deployments)
#### Report features:
- **Interactive HTML reports** with test results, screenshots, and traces
- **Detailed failure analysis** with before/after screenshots
- **Test execution videos** for failed tests
- **Network logs** and console output for debugging
This integration ensures that test results are easily accessible to reviewers and maintainers, making it simple to verify that changes don't break existing functionality across different browsers and viewport sizes.
## Resources ## Resources
- [Playwright UI Mode](https://playwright.dev/docs/test-ui-mode) - Interactive test debugging - [Playwright UI Mode](https://playwright.dev/docs/test-ui-mode) - Interactive test debugging

View File

@@ -1,7 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 5V3C14 2.44772 13.5523 2 13 2H11C10.4477 2 10 2.44772 10 3V5C10 5.55228 10.4477 6 11 6H13C13.5523 6 14 5.55228 14 5Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/> <path d="M8.99999 4H6.99999M8.99999 12H7.6231C5.02081 12 3.11138 9.55445 3.74252 7.02986L3.99999 6M13.6894 3.24254L13.1894 5.24254C13.0781 5.6877 12.6781 6 12.2192 6H10.2808C9.63019 6 9.15284 5.38861 9.31062 4.75746L9.81062 2.75746C9.92192 2.3123 10.3219 2 10.7808 2H12.7192C13.3698 2 13.8471 2.61139 13.6894 3.24254ZM6.68936 3.24254L6.18936 5.24254C6.07806 5.6877 5.67808 6 5.21921 6H3.28077C2.63019 6 2.15284 5.38861 2.31062 4.75746L2.81062 2.75746C2.92191 2.3123 3.3219 2 3.78077 2H5.71921C6.36978 2 6.84714 2.61139 6.68936 3.24254ZM13.6894 11.2425L13.1894 13.2425C13.0781 13.6877 12.6781 14 12.2192 14H10.2808C9.63019 14 9.15284 13.3886 9.31062 12.7575L9.81062 10.7575C9.92192 10.3123 10.3219 10 10.7808 10H12.7192C13.3698 10 13.8471 10.6114 13.6894 11.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M6 5V3C6 2.44772 5.55228 2 5 2H3C2.44772 2 2 2.44772 2 3V5C2 5.55228 2.44772 6 3 6H5C5.55228 6 6 5.55228 6 5Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M14 13V11C14 10.4477 13.5523 10 13 10H11C10.4477 10 10 10.4477 10 11V13C10 13.5523 10.4477 14 11 14H13C13.5523 14 14 13.5523 14 13Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M10 4H6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M10 12H8C5.79086 12 4 10.2091 4 8V6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 890 B

After

Width:  |  Height:  |  Size: 910 B

View File

@@ -12,7 +12,6 @@
<ColorPickerButton /> <ColorPickerButton />
<BypassButton /> <BypassButton />
<PinButton /> <PinButton />
<EditModelButton />
<Load3DViewerButton /> <Load3DViewerButton />
<MaskEditorButton /> <MaskEditorButton />
<ConvertToSubgraphButton /> <ConvertToSubgraphButton />
@@ -35,7 +34,6 @@ import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue' import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue' import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue'
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue' import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
import EditModelButton from '@/components/graph/selectionToolbox/EditModelButton.vue'
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue' import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue' import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue' import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'

View File

@@ -1,37 +0,0 @@
<template>
<Button
v-show="isImageOutputSelected"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_AddEditModelStep.label'),
showDelay: 1000
}"
severity="secondary"
text
icon="pi pi-pen-to-square"
@click="() => commandStore.execute('Comfy.Canvas.AddEditModelStep')"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { isImageNode, isLGraphNode } from '@/utils/litegraphUtil'
const { t } = useI18n()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const isImageOutputOrEditModelNode = (node: unknown) =>
isLGraphNode(node) &&
(isImageNode(node) || node.type === 'workflow>FLUX.1 Kontext Image Edit')
const isImageOutputSelected = computed(
() =>
canvasStore.selectedItems.length === 1 &&
isImageOutputOrEditModelNode(canvasStore.selectedItems[0])
)
</script>

View File

@@ -9,6 +9,41 @@
:max-selected-labels="0" :max-selected-labels="0"
:pt="pt" :pt="pt"
> >
<template
v-if="hasSearchBox || showSelectedCount || hasClearButton"
#header
>
<div class="p-2 flex flex-col gap-y-4 pb-0">
<SearchBox
v-if="hasSearchBox"
v-model="searchQuery"
:has-border="true"
:place-holder="searchPlaceholder"
/>
<div class="flex items-center justify-between">
<span
v-if="showSelectedCount"
class="text-sm text-neutral-400 dark-theme:text-zinc-500 px-1"
>
{{
selectedCount > 0
? $t('g.itemsSelected', { selectedCount })
: $t('g.itemSelected', { selectedCount })
}}
</span>
<TextButton
v-if="hasClearButton"
:label="$t('g.clearAll')"
type="transparent"
size="fit-content"
class="text-sm !text-blue-500 !dark-theme:text-blue-600"
@click.stop="selectedItems = []"
/>
</div>
<div class="h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
</div>
</template>
<!-- Trigger value (keep text scale identical) --> <!-- Trigger value (keep text scale identical) -->
<template #value> <template #value>
<span class="text-sm text-zinc-700 dark-theme:text-gray-200"> <span class="text-sm text-zinc-700 dark-theme:text-gray-200">
@@ -42,7 +77,7 @@
</template> </template>
</MultiSelect> </MultiSelect>
<!-- Selected count badge (unchanged) --> <!-- Selected count badge -->
<div <div
v-if="selectedCount > 0" v-if="selectedCount > 0"
class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white" class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white"
@@ -58,22 +93,41 @@ import MultiSelect, {
} from 'primevue/multiselect' } from 'primevue/multiselect'
import { computed } from 'vue' import { computed } from 'vue'
const { label, options } = defineProps<{ import SearchBox from '@/components/input/SearchBox.vue'
label?: string
options: { name: string; value: string }[]
}>()
const selectedItems = defineModel<{ name: string; value: string }[]>({ import TextButton from '../button/TextButton.vue'
type Option = { name: string; value: string }
interface Props {
/** Input label shown on the trigger button */
label?: string
/** Static options for the multiselect (when not using async search) */
options: Option[]
/** Show search box in the panel header */
hasSearchBox?: boolean
/** Show selected count text in the panel header */
showSelectedCount?: boolean
/** Show "Clear all" action in the panel header */
hasClearButton?: boolean
/** Placeholder for the search input */
searchPlaceholder?: string
}
const {
label,
options,
hasSearchBox = false,
showSelectedCount = false,
hasClearButton = false,
searchPlaceholder = 'Search...'
} = defineProps<Props>()
const selectedItems = defineModel<Option[]>({
required: true required: true
}) })
const searchQuery = defineModel<string>('searchQuery')
const selectedCount = computed(() => selectedItems.value.length) const selectedCount = computed(() => selectedItems.value.length)
/**
* Pure unstyled mode using only the PrimeVue PT API.
* All PrimeVue built-in checkboxes/headers are hidden via PT (no :deep hacks).
* Visual output matches the previous version exactly.
*/
const pt = computed(() => ({ const pt = computed(() => ({
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({ root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
class: [ class: [
@@ -97,19 +151,19 @@ const pt = computed(() => ({
dropdown: { dropdown: {
class: 'flex shrink-0 cursor-pointer items-center justify-center px-3' class: 'flex shrink-0 cursor-pointer items-center justify-center px-3'
}, },
header: { class: 'hidden' }, header: () => ({
class:
hasSearchBox || showSelectedCount || hasClearButton ? 'block' : 'hidden'
}),
// Overlay & list visuals unchanged // Overlay & list visuals unchanged
overlay: overlay:
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100', 'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100 dark-theme:border-zinc-700',
list: { list: {
class: 'flex flex-col gap-1 p-0 list-none border-none text-xs' class: 'flex flex-col gap-1 p-0 list-none border-none text-xs'
}, },
// Option row hover tone identical // Option row hover tone identical
option: option:
'flex gap-1 items-center p-2 hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50', 'flex gap-1 items-center p-2 hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
// Hide built-in checkboxes entirely via PT (no :deep) // Hide built-in checkboxes entirely via PT (no :deep)
pcHeaderCheckbox: { pcHeaderCheckbox: {
root: { class: 'hidden' }, root: { class: 'hidden' },

View File

@@ -1,8 +1,6 @@
<template> <template>
<div <div :class="wrapperStyle">
class="flex w-full items-center rounded-lg px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800" <i-lucide:search :class="iconColorStyle" />
>
<i-lucide:search class="text-neutral" />
<InputText <InputText
v-model="searchQuery" v-model="searchQuery"
:placeholder="placeHolder || 'Search...'" :placeholder="placeHolder || 'Search...'"
@@ -15,10 +13,21 @@
<script setup lang="ts"> <script setup lang="ts">
import InputText from 'primevue/inputtext' import InputText from 'primevue/inputtext'
import { defineModel } from 'vue' import { computed, defineModel } from 'vue'
const { placeHolder } = defineProps<{ const { placeHolder, hasBorder = false } = defineProps<{
placeHolder?: string placeHolder?: string
hasBorder?: boolean
}>() }>()
const searchQuery = defineModel<string>('') const searchQuery = defineModel<string>('')
const wrapperStyle = computed(() => {
return hasBorder
? 'flex w-full items-center rounded gap-2 bg-white dark-theme:bg-zinc-800 p-1 border border-solid border-zinc-200 dark-theme:border-zinc-700'
: 'flex w-full items-center rounded px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800'
})
const iconColorStyle = computed(() => {
return !hasBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
})
</script> </script>

View File

@@ -99,7 +99,7 @@ const pt = computed(() => ({
overlay: { overlay: {
class: [ class: [
// dropdown panel // dropdown panel
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg' 'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100 dark-theme:border-zinc-700'
] ]
}, },
list: { list: {

View File

@@ -59,6 +59,10 @@
<div class="relative px-6 pt-2 pb-4 flex gap-2"> <div class="relative px-6 pt-2 pb-4 flex gap-2">
<MultiSelect <MultiSelect
v-model="selectedFrameworks" v-model="selectedFrameworks"
class="w-[250px]"
:has-search-box="true"
:show-selected-count="true"
:has-clear-button="true"
label="Select Frameworks" label="Select Frameworks"
:options="frameworkOptions" :options="frameworkOptions"
/> />

View File

@@ -16,7 +16,6 @@ import {
import { Point } from '@/lib/litegraph/src/litegraph' import { Point } from '@/lib/litegraph/src/litegraph'
import { api } from '@/scripts/api' import { api } from '@/scripts/api'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService' import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService' import { useWorkflowService } from '@/services/workflowService'
@@ -775,17 +774,6 @@ export function useCoreCommands(): ComfyCommand[] {
versionAdded: moveSelectedNodesVersionAdded, versionAdded: moveSelectedNodesVersionAdded,
function: () => moveSelectedNodes(([x, y], gridSize) => [x + gridSize, y]) function: () => moveSelectedNodes(([x, y], gridSize) => [x + gridSize, y])
}, },
{
id: 'Comfy.Canvas.AddEditModelStep',
icon: 'pi pi-pen-to-square',
label: 'Add Edit Model Step',
versionAdded: '1.23.3',
function: async () => {
const node = app.canvas.selectedItems.values().next().value
if (!(node instanceof LGraphNode)) return
await addFluxKontextGroupNode(node)
}
},
{ {
id: 'Comfy.Graph.ConvertToSubgraph', id: 'Comfy.Graph.ConvertToSubgraph',
icon: 'pi pi-sitemap', icon: 'pi pi-sitemap',

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "تصفح القوالب" "label": "تصفح القوالب"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "إضافة خطوة تحرير النموذج"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "حذف العناصر المحددة" "label": "حذف العناصر المحددة"
}, },

View File

@@ -272,6 +272,7 @@
"category": "الفئة", "category": "الفئة",
"choose_file_to_upload": "اختر ملفاً للرفع", "choose_file_to_upload": "اختر ملفاً للرفع",
"clear": "مسح", "clear": "مسح",
"clearAll": "مسح الكل",
"clearFilters": "مسح الفلاتر", "clearFilters": "مسح الفلاتر",
"close": "إغلاق", "close": "إغلاق",
"color": "اللون", "color": "اللون",
@@ -327,6 +328,8 @@
"installed": "مثبت", "installed": "مثبت",
"installing": "جارٍ التثبيت", "installing": "جارٍ التثبيت",
"interrupted": "تمت المقاطعة", "interrupted": "تمت المقاطعة",
"itemSelected": "تم تحديد عنصر واحد",
"itemsSelected": "تم تحديد {selectedCount} عناصر",
"keybinding": "اختصار لوحة المفاتيح", "keybinding": "اختصار لوحة المفاتيح",
"keybindingAlreadyExists": "الاختصار موجود بالفعل في", "keybindingAlreadyExists": "الاختصار موجود بالفعل في",
"learnMore": "اعرف المزيد", "learnMore": "اعرف المزيد",
@@ -762,7 +765,6 @@
}, },
"menuLabels": { "menuLabels": {
"About ComfyUI": "حول ComfyUI", "About ComfyUI": "حول ComfyUI",
"Add Edit Model Step": "إضافة خطوة تعديل النموذج",
"Bottom Panel": "لوحة سفلية", "Bottom Panel": "لوحة سفلية",
"Browse Templates": "تصفح القوالب", "Browse Templates": "تصفح القوالب",
"Bypass/Unbypass Selected Nodes": "تجاوز/إلغاء تجاوز العقد المحددة", "Bypass/Unbypass Selected Nodes": "تجاوز/إلغاء تجاوز العقد المحددة",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "Browse Templates" "label": "Browse Templates"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "Add Edit Model Step"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "Delete Selected Items" "label": "Delete Selected Items"
}, },

View File

@@ -137,8 +137,11 @@
"copy": "Copy", "copy": "Copy",
"imageUrl": "Image URL", "imageUrl": "Image URL",
"clear": "Clear", "clear": "Clear",
"clearAll": "Clear all",
"copyURL": "Copy URL", "copyURL": "Copy URL",
"releaseTitle": "{package} {version} Release", "releaseTitle": "{package} {version} Release",
"itemSelected": "{selectedCount} item selected",
"itemsSelected": "{selectedCount} items selected",
"progressCountOf": "of", "progressCountOf": "of",
"keybindingAlreadyExists": "Keybinding already exists on", "keybindingAlreadyExists": "Keybinding already exists on",
"startRecording": "Start Recording", "startRecording": "Start Recording",
@@ -973,7 +976,6 @@
"Restart": "Restart", "Restart": "Restart",
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node", "Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
"Browse Templates": "Browse Templates", "Browse Templates": "Browse Templates",
"Add Edit Model Step": "Add Edit Model Step",
"Delete Selected Items": "Delete Selected Items", "Delete Selected Items": "Delete Selected Items",
"Zoom to fit": "Zoom to fit", "Zoom to fit": "Zoom to fit",
"Move Selected Nodes Down": "Move Selected Nodes Down", "Move Selected Nodes Down": "Move Selected Nodes Down",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "Explorar plantillas" "label": "Explorar plantillas"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "Agregar paso de edición de modelo"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "Eliminar elementos seleccionados" "label": "Eliminar elementos seleccionados"
}, },

View File

@@ -272,6 +272,7 @@
"category": "Categoría", "category": "Categoría",
"choose_file_to_upload": "elige archivo para subir", "choose_file_to_upload": "elige archivo para subir",
"clear": "Limpiar", "clear": "Limpiar",
"clearAll": "Borrar todo",
"clearFilters": "Borrar filtros", "clearFilters": "Borrar filtros",
"close": "Cerrar", "close": "Cerrar",
"color": "Color", "color": "Color",
@@ -327,6 +328,8 @@
"installed": "Instalado", "installed": "Instalado",
"installing": "Instalando", "installing": "Instalando",
"interrupted": "Interrumpido", "interrupted": "Interrumpido",
"itemSelected": "{selectedCount} elemento seleccionado",
"itemsSelected": "{selectedCount} elementos seleccionados",
"keybinding": "Combinación de teclas", "keybinding": "Combinación de teclas",
"keybindingAlreadyExists": "La combinación de teclas ya existe en", "keybindingAlreadyExists": "La combinación de teclas ya existe en",
"learnMore": "Aprende más", "learnMore": "Aprende más",
@@ -762,7 +765,6 @@
}, },
"menuLabels": { "menuLabels": {
"About ComfyUI": "Acerca de ComfyUI", "About ComfyUI": "Acerca de ComfyUI",
"Add Edit Model Step": "Agregar paso de edición de modelo",
"Bottom Panel": "Panel inferior", "Bottom Panel": "Panel inferior",
"Browse Templates": "Explorar plantillas", "Browse Templates": "Explorar plantillas",
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados", "Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "Parcourir les modèles" "label": "Parcourir les modèles"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "Ajouter/Modifier une étape de modèle"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "Supprimer les éléments sélectionnés" "label": "Supprimer les éléments sélectionnés"
}, },

View File

@@ -272,6 +272,7 @@
"category": "Catégorie", "category": "Catégorie",
"choose_file_to_upload": "choisissez le fichier à télécharger", "choose_file_to_upload": "choisissez le fichier à télécharger",
"clear": "Effacer", "clear": "Effacer",
"clearAll": "Tout effacer",
"clearFilters": "Effacer les filtres", "clearFilters": "Effacer les filtres",
"close": "Fermer", "close": "Fermer",
"color": "Couleur", "color": "Couleur",
@@ -327,6 +328,8 @@
"installed": "Installé", "installed": "Installé",
"installing": "Installation", "installing": "Installation",
"interrupted": "Interrompu", "interrupted": "Interrompu",
"itemSelected": "{selectedCount} élément sélectionné",
"itemsSelected": "{selectedCount} éléments sélectionnés",
"keybinding": "Raccourci clavier", "keybinding": "Raccourci clavier",
"keybindingAlreadyExists": "Le raccourci clavier existe déjà", "keybindingAlreadyExists": "Le raccourci clavier existe déjà",
"learnMore": "En savoir plus", "learnMore": "En savoir plus",
@@ -762,7 +765,6 @@
}, },
"menuLabels": { "menuLabels": {
"About ComfyUI": "À propos de ComfyUI", "About ComfyUI": "À propos de ComfyUI",
"Add Edit Model Step": "Ajouter une étape dédition de modèle",
"Bottom Panel": "Panneau inférieur", "Bottom Panel": "Panneau inférieur",
"Browse Templates": "Parcourir les modèles", "Browse Templates": "Parcourir les modèles",
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés", "Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "テンプレートを参照" "label": "テンプレートを参照"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "編集モデルステップを追加"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "選択したアイテムを削除" "label": "選択したアイテムを削除"
}, },

View File

@@ -272,6 +272,7 @@
"category": "カテゴリ", "category": "カテゴリ",
"choose_file_to_upload": "アップロードするファイルを選択", "choose_file_to_upload": "アップロードするファイルを選択",
"clear": "クリア", "clear": "クリア",
"clearAll": "すべてクリア",
"clearFilters": "フィルターをクリア", "clearFilters": "フィルターをクリア",
"close": "閉じる", "close": "閉じる",
"color": "色", "color": "色",
@@ -327,6 +328,8 @@
"installed": "インストール済み", "installed": "インストール済み",
"installing": "インストール中", "installing": "インストール中",
"interrupted": "中断されました", "interrupted": "中断されました",
"itemSelected": "{selectedCount}件選択済み",
"itemsSelected": "{selectedCount}件選択済み",
"keybinding": "キーバインディング", "keybinding": "キーバインディング",
"keybindingAlreadyExists": "このキー割り当てはすでに存在します", "keybindingAlreadyExists": "このキー割り当てはすでに存在します",
"learnMore": "詳細を学ぶ", "learnMore": "詳細を学ぶ",
@@ -762,7 +765,6 @@
}, },
"menuLabels": { "menuLabels": {
"About ComfyUI": "ComfyUIについて", "About ComfyUI": "ComfyUIについて",
"Add Edit Model Step": "モデル編集ステップを追加",
"Bottom Panel": "下部パネル", "Bottom Panel": "下部パネル",
"Browse Templates": "テンプレートを参照", "Browse Templates": "テンプレートを参照",
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除", "Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "템플릿 탐색" "label": "템플릿 탐색"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "모델 편집 단계 추가"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "선택한 항목 삭제" "label": "선택한 항목 삭제"
}, },

View File

@@ -272,6 +272,7 @@
"category": "카테고리", "category": "카테고리",
"choose_file_to_upload": "업로드할 파일 선택", "choose_file_to_upload": "업로드할 파일 선택",
"clear": "지우기", "clear": "지우기",
"clearAll": "모두 지우기",
"clearFilters": "필터 지우기", "clearFilters": "필터 지우기",
"close": "닫기", "close": "닫기",
"color": "색상", "color": "색상",
@@ -327,6 +328,8 @@
"installed": "설치됨", "installed": "설치됨",
"installing": "설치 중", "installing": "설치 중",
"interrupted": "중단됨", "interrupted": "중단됨",
"itemSelected": "{selectedCount}개 선택됨",
"itemsSelected": "{selectedCount}개 선택됨",
"keybinding": "키 바인딩", "keybinding": "키 바인딩",
"keybindingAlreadyExists": "단축키가 이미 존재합니다", "keybindingAlreadyExists": "단축키가 이미 존재합니다",
"learnMore": "더 알아보기", "learnMore": "더 알아보기",
@@ -762,7 +765,6 @@
}, },
"menuLabels": { "menuLabels": {
"About ComfyUI": "ComfyUI에 대하여", "About ComfyUI": "ComfyUI에 대하여",
"Add Edit Model Step": "모델 편집 단계 추가",
"Bottom Panel": "하단 패널", "Bottom Panel": "하단 패널",
"Browse Templates": "템플릿 탐색", "Browse Templates": "템플릿 탐색",
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제", "Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "Просмотр шаблонов" "label": "Просмотр шаблонов"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "Добавить или изменить шаг модели"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "Удалить выбранные элементы" "label": "Удалить выбранные элементы"
}, },

View File

@@ -272,6 +272,7 @@
"category": "Категория", "category": "Категория",
"choose_file_to_upload": "выберите файл для загрузки", "choose_file_to_upload": "выберите файл для загрузки",
"clear": "Очистить", "clear": "Очистить",
"clearAll": "Очистить всё",
"clearFilters": "Сбросить фильтры", "clearFilters": "Сбросить фильтры",
"close": "Закрыть", "close": "Закрыть",
"color": "Цвет", "color": "Цвет",
@@ -327,6 +328,8 @@
"installed": "Установлено", "installed": "Установлено",
"installing": "Установка", "installing": "Установка",
"interrupted": "Прервано", "interrupted": "Прервано",
"itemSelected": "Выбран {selectedCount} элемент",
"itemsSelected": "Выбрано {selectedCount} элементов",
"keybinding": "Привязка клавиш", "keybinding": "Привязка клавиш",
"keybindingAlreadyExists": "Горячая клавиша уже существует", "keybindingAlreadyExists": "Горячая клавиша уже существует",
"learnMore": "Узнать больше", "learnMore": "Узнать больше",
@@ -762,7 +765,6 @@
}, },
"menuLabels": { "menuLabels": {
"About ComfyUI": "О ComfyUI", "About ComfyUI": "О ComfyUI",
"Add Edit Model Step": "Добавить или изменить шаг модели",
"Bottom Panel": "Нижняя панель", "Bottom Panel": "Нижняя панель",
"Browse Templates": "Просмотреть шаблоны", "Browse Templates": "Просмотреть шаблоны",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды", "Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "瀏覽範本" "label": "瀏覽範本"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "新增編輯模型步驟"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "刪除選取項目" "label": "刪除選取項目"
}, },

View File

@@ -272,6 +272,7 @@
"category": "分類", "category": "分類",
"choose_file_to_upload": "選擇要上傳的檔案", "choose_file_to_upload": "選擇要上傳的檔案",
"clear": "清除", "clear": "清除",
"clearAll": "全部清除",
"clearFilters": "清除篩選", "clearFilters": "清除篩選",
"close": "關閉", "close": "關閉",
"color": "顏色", "color": "顏色",
@@ -327,6 +328,8 @@
"installed": "已安裝", "installed": "已安裝",
"installing": "安裝中", "installing": "安裝中",
"interrupted": "已中斷", "interrupted": "已中斷",
"itemSelected": "已選取 {selectedCount} 項",
"itemsSelected": "已選取 {selectedCount} 項",
"keybinding": "快捷鍵", "keybinding": "快捷鍵",
"keybindingAlreadyExists": "快捷鍵已存在於", "keybindingAlreadyExists": "快捷鍵已存在於",
"learnMore": "了解更多", "learnMore": "了解更多",
@@ -762,7 +765,6 @@
}, },
"menuLabels": { "menuLabels": {
"About ComfyUI": "關於 ComfyUI", "About ComfyUI": "關於 ComfyUI",
"Add Edit Model Step": "新增編輯模型步驟",
"Bottom Panel": "底部面板", "Bottom Panel": "底部面板",
"Browse Templates": "瀏覽範本", "Browse Templates": "瀏覽範本",
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點", "Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",

View File

@@ -41,9 +41,6 @@
"Comfy_BrowseTemplates": { "Comfy_BrowseTemplates": {
"label": "浏览模板" "label": "浏览模板"
}, },
"Comfy_Canvas_AddEditModelStep": {
"label": "添加编辑模型步骤"
},
"Comfy_Canvas_DeleteSelectedItems": { "Comfy_Canvas_DeleteSelectedItems": {
"label": "删除选定的项目" "label": "删除选定的项目"
}, },

View File

@@ -272,6 +272,7 @@
"category": "类别", "category": "类别",
"choose_file_to_upload": "选择要上传的文件", "choose_file_to_upload": "选择要上传的文件",
"clear": "清除", "clear": "清除",
"clearAll": "全部清除",
"clearFilters": "清除筛选", "clearFilters": "清除筛选",
"close": "关闭", "close": "关闭",
"color": "颜色", "color": "颜色",
@@ -327,6 +328,8 @@
"installed": "已安装", "installed": "已安装",
"installing": "正在安装", "installing": "正在安装",
"interrupted": "已中断", "interrupted": "已中断",
"itemSelected": "已选择 {selectedCount} 项",
"itemsSelected": "已选择 {selectedCount} 项",
"keybinding": "按键绑定", "keybinding": "按键绑定",
"keybindingAlreadyExists": "快捷键已存在", "keybindingAlreadyExists": "快捷键已存在",
"learnMore": "了解更多", "learnMore": "了解更多",
@@ -762,7 +765,6 @@
}, },
"menuLabels": { "menuLabels": {
"About ComfyUI": "关于ComfyUI", "About ComfyUI": "关于ComfyUI",
"Add Edit Model Step": "添加编辑模型步骤",
"Bottom Panel": "底部面板", "Bottom Panel": "底部面板",
"Browse Templates": "浏览模板", "Browse Templates": "浏览模板",
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点", "Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",

View File

@@ -1,693 +0,0 @@
import _ from 'es-toolkit/compat'
import {
type INodeOutputSlot,
type LGraph,
type LGraphNode,
LLink,
LiteGraph,
type Point
} from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { parseFilePath } from '@/utils/formatUtil'
import { app } from './app'
const fluxKontextGroupNode = {
nodes: [
{
id: -1,
type: 'Reroute',
pos: [2354.87890625, -127.23468780517578],
size: [75, 26],
flags: {},
order: 20,
mode: 0,
inputs: [{ name: '', type: '*', link: null }],
outputs: [{ name: '', type: '*', links: null }],
properties: { showOutputText: false, horizontal: false },
index: 0
},
{
id: -1,
type: 'ReferenceLatent',
pos: [2730, -220],
size: [197.712890625, 46],
flags: {},
order: 22,
mode: 0,
inputs: [
{
localized_name: 'conditioning',
name: 'conditioning',
type: 'CONDITIONING',
link: null
},
{
localized_name: 'latent',
name: 'latent',
shape: 7,
type: 'LATENT',
link: null
}
],
outputs: [
{
localized_name: 'CONDITIONING',
name: 'CONDITIONING',
type: 'CONDITIONING',
links: []
}
],
properties: {
'Node name for S&R': 'ReferenceLatent',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
index: 1
},
{
id: -1,
type: 'VAEDecode',
pos: [3270, -110],
size: [210, 46],
flags: {},
order: 25,
mode: 0,
inputs: [
{
localized_name: 'samples',
name: 'samples',
type: 'LATENT',
link: null
},
{
localized_name: 'vae',
name: 'vae',
type: 'VAE',
link: null
}
],
outputs: [
{
localized_name: 'IMAGE',
name: 'IMAGE',
type: 'IMAGE',
slot_index: 0,
links: []
}
],
properties: {
'Node name for S&R': 'VAEDecode',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
index: 2
},
{
id: -1,
type: 'KSampler',
pos: [2930, -110],
size: [315, 262],
flags: {},
order: 24,
mode: 0,
inputs: [
{
localized_name: 'model',
name: 'model',
type: 'MODEL',
link: null
},
{
localized_name: 'positive',
name: 'positive',
type: 'CONDITIONING',
link: null
},
{
localized_name: 'negative',
name: 'negative',
type: 'CONDITIONING',
link: null
},
{
localized_name: 'latent_image',
name: 'latent_image',
type: 'LATENT',
link: null
},
{
localized_name: 'seed',
name: 'seed',
type: 'INT',
widget: { name: 'seed' },
link: null
},
{
localized_name: 'steps',
name: 'steps',
type: 'INT',
widget: { name: 'steps' },
link: null
},
{
localized_name: 'cfg',
name: 'cfg',
type: 'FLOAT',
widget: { name: 'cfg' },
link: null
},
{
localized_name: 'sampler_name',
name: 'sampler_name',
type: 'COMBO',
widget: { name: 'sampler_name' },
link: null
},
{
localized_name: 'scheduler',
name: 'scheduler',
type: 'COMBO',
widget: { name: 'scheduler' },
link: null
},
{
localized_name: 'denoise',
name: 'denoise',
type: 'FLOAT',
widget: { name: 'denoise' },
link: null
}
],
outputs: [
{
localized_name: 'LATENT',
name: 'LATENT',
type: 'LATENT',
slot_index: 0,
links: []
}
],
properties: {
'Node name for S&R': 'KSampler',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: [972054013131369, 'fixed', 20, 1, 'euler', 'simple', 1],
index: 3
},
{
id: -1,
type: 'FluxGuidance',
pos: [2940, -220],
size: [211.60000610351562, 58],
flags: {},
order: 23,
mode: 0,
inputs: [
{
localized_name: 'conditioning',
name: 'conditioning',
type: 'CONDITIONING',
link: null
},
{
localized_name: 'guidance',
name: 'guidance',
type: 'FLOAT',
widget: { name: 'guidance' },
link: null
}
],
outputs: [
{
localized_name: 'CONDITIONING',
name: 'CONDITIONING',
type: 'CONDITIONING',
slot_index: 0,
links: []
}
],
properties: {
'Node name for S&R': 'FluxGuidance',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: [2.5],
index: 4
},
{
id: -1,
type: 'SaveImage',
pos: [3490, -110],
size: [985.3012084960938, 1060.3828125],
flags: {},
order: 26,
mode: 0,
inputs: [
{
localized_name: 'images',
name: 'images',
type: 'IMAGE',
link: null
},
{
localized_name: 'filename_prefix',
name: 'filename_prefix',
type: 'STRING',
widget: { name: 'filename_prefix' },
link: null
}
],
outputs: [],
properties: { cnr_id: 'comfy-core', ver: '0.3.38' },
widgets_values: ['ComfyUI'],
index: 5
},
{
id: -1,
type: 'CLIPTextEncode',
pos: [2500, -110],
size: [422.84503173828125, 164.31304931640625],
flags: {},
order: 12,
mode: 0,
inputs: [
{
localized_name: 'clip',
name: 'clip',
type: 'CLIP',
link: null
},
{
localized_name: 'text',
name: 'text',
type: 'STRING',
widget: { name: 'text' },
link: null
}
],
outputs: [
{
localized_name: 'CONDITIONING',
name: 'CONDITIONING',
type: 'CONDITIONING',
slot_index: 0,
links: []
}
],
title: 'CLIP Text Encode (Positive Prompt)',
properties: {
'Node name for S&R': 'CLIPTextEncode',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: ['there is a bright light'],
color: '#232',
bgcolor: '#353',
index: 6
},
{
id: -1,
type: 'CLIPTextEncode',
pos: [2504.1435546875, 97.9598617553711],
size: [422.84503173828125, 164.31304931640625],
flags: { collapsed: true },
order: 13,
mode: 0,
inputs: [
{
localized_name: 'clip',
name: 'clip',
type: 'CLIP',
link: null
},
{
localized_name: 'text',
name: 'text',
type: 'STRING',
widget: { name: 'text' },
link: null
}
],
outputs: [
{
localized_name: 'CONDITIONING',
name: 'CONDITIONING',
type: 'CONDITIONING',
slot_index: 0,
links: []
}
],
title: 'CLIP Text Encode (Negative Prompt)',
properties: {
'Node name for S&R': 'CLIPTextEncode',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: [''],
color: '#322',
bgcolor: '#533',
index: 7
},
{
id: -1,
type: 'UNETLoader',
pos: [2630, -370],
size: [270, 82],
flags: {},
order: 6,
mode: 0,
inputs: [
{
localized_name: 'unet_name',
name: 'unet_name',
type: 'COMBO',
widget: { name: 'unet_name' },
link: null
},
{
localized_name: 'weight_dtype',
name: 'weight_dtype',
type: 'COMBO',
widget: { name: 'weight_dtype' },
link: null
}
],
outputs: [
{
localized_name: 'MODEL',
name: 'MODEL',
type: 'MODEL',
links: []
}
],
properties: {
'Node name for S&R': 'UNETLoader',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: ['flux1-kontext-dev.safetensors', 'default'],
color: '#223',
bgcolor: '#335',
index: 8
},
{
id: -1,
type: 'DualCLIPLoader',
pos: [2100, -290],
size: [337.76861572265625, 130],
flags: {},
order: 8,
mode: 0,
inputs: [
{
localized_name: 'clip_name1',
name: 'clip_name1',
type: 'COMBO',
widget: { name: 'clip_name1' },
link: null
},
{
localized_name: 'clip_name2',
name: 'clip_name2',
type: 'COMBO',
widget: { name: 'clip_name2' },
link: null
},
{
localized_name: 'type',
name: 'type',
type: 'COMBO',
widget: { name: 'type' },
link: null
},
{
localized_name: 'device',
name: 'device',
shape: 7,
type: 'COMBO',
widget: { name: 'device' },
link: null
}
],
outputs: [
{
localized_name: 'CLIP',
name: 'CLIP',
type: 'CLIP',
links: []
}
],
properties: {
'Node name for S&R': 'DualCLIPLoader',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: [
'clip_l.safetensors',
't5xxl_fp8_e4m3fn_scaled.safetensors',
'flux',
'default'
],
color: '#223',
bgcolor: '#335',
index: 9
},
{
id: -1,
type: 'VAELoader',
pos: [2960, -370],
size: [270, 58],
flags: {},
order: 7,
mode: 0,
inputs: [
{
localized_name: 'vae_name',
name: 'vae_name',
type: 'COMBO',
widget: { name: 'vae_name' },
link: null
}
],
outputs: [
{
localized_name: 'VAE',
name: 'VAE',
type: 'VAE',
links: []
}
],
properties: {
'Node name for S&R': 'VAELoader',
cnr_id: 'comfy-core',
ver: '0.3.38'
},
widgets_values: ['ae.safetensors'],
color: '#223',
bgcolor: '#335',
index: 10
}
],
links: [
[6, 0, 1, 0, 72, 'CONDITIONING'],
[0, 0, 1, 1, 66, '*'],
[3, 0, 2, 0, 69, 'LATENT'],
[10, 0, 2, 1, 76, 'VAE'],
[8, 0, 3, 0, 74, 'MODEL'],
[4, 0, 3, 1, 70, 'CONDITIONING'],
[7, 0, 3, 2, 73, 'CONDITIONING'],
[0, 0, 3, 3, 66, '*'],
[1, 0, 4, 0, 67, 'CONDITIONING'],
[2, 0, 5, 0, 68, 'IMAGE'],
[9, 0, 6, 0, 75, 'CLIP'],
[9, 0, 7, 0, 75, 'CLIP']
],
external: [],
config: {
'0': {},
'1': {},
'2': { output: { '0': { visible: true } } },
'3': {
output: { '0': { visible: true } },
input: {
denoise: { visible: false },
cfg: { visible: false }
}
},
'4': {},
'5': {},
'6': {},
'7': { input: { text: { visible: false } } },
'8': { input: { weight_dtype: { visible: false } } },
'9': { input: { type: { visible: false }, device: { visible: false } } },
'10': {}
}
}
export async function ensureGraphHasFluxKontextGroupNode(
graph: LGraph & { extra: { groupNodes?: Record<string, any> } }
) {
graph.extra ??= {}
graph.extra.groupNodes ??= {}
if (graph.extra.groupNodes['FLUX.1 Kontext Image Edit']) return
graph.extra.groupNodes['FLUX.1 Kontext Image Edit'] =
structuredClone(fluxKontextGroupNode)
// Lazy import to avoid circular dependency issues
const { GroupNodeConfig } = await import('@/extensions/core/groupNode')
await GroupNodeConfig.registerFromWorkflow(
{
'FLUX.1 Kontext Image Edit':
graph.extra.groupNodes['FLUX.1 Kontext Image Edit']
},
[]
)
}
export async function addFluxKontextGroupNode(fromNode: LGraphNode) {
const { canvas } = app
const { graph } = canvas
if (!graph) throw new TypeError('Graph is not initialized')
await ensureGraphHasFluxKontextGroupNode(graph)
const node = LiteGraph.createNode('workflow>FLUX.1 Kontext Image Edit')
if (!node) throw new TypeError('Failed to create node')
const pos = getPosToRightOfNode(fromNode)
graph.add(node)
node.pos = pos
app.canvas.processSelect(node, undefined)
connectPreviousLatent(fromNode, node)
const symb = Object.getOwnPropertySymbols(node)[0]
// @ts-expect-error It's there -- promise.
node[symb].populateWidgets()
setWidgetValues(node)
}
function setWidgetValues(node: LGraphNode) {
const seedInput = node.widgets?.find((x) => x.name === 'seed')
if (!seedInput) throw new TypeError('Seed input not found')
seedInput.value = Math.floor(Math.random() * 1_125_899_906_842_624)
const firstClip = node.widgets?.find((x) => x.name === 'clip_name1')
setPreferredValue('t5xxl_fp8_e4m3fn_scaled.safetensors', 't5xxl', firstClip)
const secondClip = node.widgets?.find((x) => x.name === 'clip_name2')
setPreferredValue('clip_l.safetensors', 'clip_l', secondClip)
const unet = node.widgets?.find((x) => x.name === 'unet_name')
setPreferredValue('flux1-dev-kontext_fp8_scaled.safetensors', 'kontext', unet)
const vae = node.widgets?.find((x) => x.name === 'vae_name')
setPreferredValue('ae.safetensors', 'ae.s', vae)
}
function setPreferredValue(
preferred: string,
match: string,
widget: IBaseWidget | undefined
): void {
if (!widget) throw new TypeError('Widget not found')
const { values } = widget.options
if (!Array.isArray(values)) return
// Match against filename portion only
const mapped = values.map((x) => parseFilePath(x).filename)
const value =
mapped.find((x) => x === preferred) ??
mapped.find((x) => x.includes?.(match))
widget.value = value ?? preferred
}
function getPosToRightOfNode(fromNode: LGraphNode) {
const nodes = app.canvas.graph?.nodes
if (!nodes) throw new TypeError('Could not get graph nodes')
const pos = [
fromNode.pos[0] + fromNode.size[0] + 100,
fromNode.pos[1]
] satisfies Point
while (nodes.find((x) => isPointTooClose(x.pos, pos))) {
pos[0] += 20
pos[1] += 20
}
return pos
}
function connectPreviousLatent(fromNode: LGraphNode, toEditNode: LGraphNode) {
const { canvas } = app
const { graph } = canvas
if (!graph) throw new TypeError('Graph is not initialized')
const l = findNearestOutputOfType([fromNode], 'LATENT')
if (!l) {
const imageOutput = findNearestOutputOfType([fromNode], 'IMAGE')
if (!imageOutput) throw new TypeError('No image output found')
const vaeEncode = LiteGraph.createNode('VAEEncode')
if (!vaeEncode) throw new TypeError('Failed to create node')
const { node: imageNode, index: imageIndex } = imageOutput
graph.add(vaeEncode)
vaeEncode.pos = getPosToRightOfNode(fromNode)
vaeEncode.pos[1] -= 200
vaeEncode.connect(0, toEditNode, 0)
imageNode.connect(imageIndex, vaeEncode, 0)
return
}
const { node, index } = l
node.connect(index, toEditNode, 0)
}
function getInputNodes(node: LGraphNode): LGraphNode[] {
return node.inputs
.map((x) => LLink.resolve(x.link, app.graph)?.outputNode)
.filter((x) => !!x)
}
function getOutputOfType(
node: LGraphNode,
type: string
): {
output: INodeOutputSlot
index: number
} {
const index = node.outputs.findIndex((x) => x.type === type)
const output = node.outputs[index]
return { output, index }
}
function findNearestOutputOfType(
nodes: Iterable<LGraphNode>,
type: string = 'LATENT',
depth: number = 0
): { node: LGraphNode; index: number } | undefined {
for (const node of nodes) {
const { output, index } = getOutputOfType(node, type)
if (output) return { node, index }
}
if (depth < 3) {
const closestNodes = new Set([...nodes].flatMap((x) => getInputNodes(x)))
const res = findNearestOutputOfType(closestNodes, type, depth + 1)
if (res) return res
}
}
function isPointTooClose(a: Point, b: Point, precision: number = 5) {
return Math.abs(a[0] - b[0]) < precision && Math.abs(a[1] - b[1]) < precision
}

View File

@@ -1,13 +1,14 @@
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from 'vue'
export interface BaseButtonProps { export interface BaseButtonProps {
size?: 'sm' | 'md' size?: 'fit-content' | 'sm' | 'md'
type?: 'primary' | 'secondary' | 'transparent' type?: 'primary' | 'secondary' | 'transparent'
class?: HTMLAttributes['class'] class?: HTMLAttributes['class']
} }
export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => { export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => {
const sizeClasses = { const sizeClasses = {
'fit-content': '',
sm: 'px-2 py-1.5 text-xs', sm: 'px-2 py-1.5 text-xs',
md: 'px-2.5 py-2 text-sm' md: 'px-2.5 py-2 text-sm'
} }
@@ -31,6 +32,7 @@ export const getIconButtonSizeClasses = (
size: BaseButtonProps['size'] = 'md' size: BaseButtonProps['size'] = 'md'
) => { ) => {
const sizeClasses = { const sizeClasses = {
'fit-content': 'w-auto h-auto',
sm: 'w-6 h-6 text-xs !rounded-md', sm: 'w-6 h-6 text-xs !rounded-md',
md: 'w-8 h-8 text-sm' md: 'w-8 h-8 text-sm'
} }