diff --git a/src/components/graph/SelectionToolbox.vue b/src/components/graph/SelectionToolbox.vue
index 762d90c26..fdb78e59b 100644
--- a/src/components/graph/SelectionToolbox.vue
+++ b/src/components/graph/SelectionToolbox.vue
@@ -10,6 +10,7 @@
+
diff --git a/src/components/graph/selectionToolbox/EditModelButton.vue b/src/components/graph/selectionToolbox/EditModelButton.vue
new file mode 100644
index 000000000..702b18394
--- /dev/null
+++ b/src/components/graph/selectionToolbox/EditModelButton.vue
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts
index 62039684c..6b291a208 100644
--- a/src/composables/useCoreCommands.ts
+++ b/src/composables/useCoreCommands.ts
@@ -14,6 +14,7 @@ import {
import { t } from '@/i18n'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
+import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService'
@@ -718,6 +719,17 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Move Selected Nodes Right',
versionAdded: moveSelectedNodesVersionAdded,
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)
+ }
}
]
diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts
index 29b853a62..51bec17f4 100644
--- a/src/extensions/core/groupNodeManage.ts
+++ b/src/extensions/core/groupNodeManage.ts
@@ -7,7 +7,8 @@ import {
import { useToastStore } from '@/stores/toastStore'
import { type ComfyApp, app } from '../../scripts/app'
-import { $el, ComfyDialog } from '../../scripts/ui'
+import { $el } from '../../scripts/ui'
+import { ComfyDialog } from '../../scripts/ui/dialog'
import { DraggableList } from '../../scripts/ui/draggableList'
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
import './groupNodeManage.css'
diff --git a/src/locales/en/commands.json b/src/locales/en/commands.json
index 04291916a..ed92a6439 100644
--- a/src/locales/en/commands.json
+++ b/src/locales/en/commands.json
@@ -38,6 +38,9 @@
"Comfy_BrowseTemplates": {
"label": "Browse Templates"
},
+ "Comfy_Canvas_AddEditModelStep": {
+ "label": "Add Edit Model Step"
+ },
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Delete Selected Items"
},
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 10192509c..76ac5bf52 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -793,6 +793,7 @@
"Reinstall": "Reinstall",
"Restart": "Restart",
"Browse Templates": "Browse Templates",
+ "Add Edit Model Step": "Add Edit Model Step",
"Delete Selected Items": "Delete Selected Items",
"Fit view to selected nodes": "Fit view to selected nodes",
"Move Selected Nodes Down": "Move Selected Nodes Down",
diff --git a/src/locales/es/commands.json b/src/locales/es/commands.json
index 8d46a8576..c25a1cfc0 100644
--- a/src/locales/es/commands.json
+++ b/src/locales/es/commands.json
@@ -38,6 +38,9 @@
"Comfy_BrowseTemplates": {
"label": "Explorar plantillas"
},
+ "Comfy_Canvas_AddEditModelStep": {
+ "label": "Agregar paso de edición de modelo"
+ },
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Eliminar elementos seleccionados"
},
diff --git a/src/locales/es/main.json b/src/locales/es/main.json
index c2bb16342..7cdcaeca6 100644
--- a/src/locales/es/main.json
+++ b/src/locales/es/main.json
@@ -684,6 +684,7 @@
},
"menuLabels": {
"About ComfyUI": "Acerca de ComfyUI",
+ "Add Edit Model Step": "Agregar paso de edición de modelo",
"Browse Templates": "Explorar plantillas",
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
diff --git a/src/locales/fr/commands.json b/src/locales/fr/commands.json
index fb39cdeb6..d1f121b05 100644
--- a/src/locales/fr/commands.json
+++ b/src/locales/fr/commands.json
@@ -38,6 +38,9 @@
"Comfy_BrowseTemplates": {
"label": "Parcourir les modèles"
},
+ "Comfy_Canvas_AddEditModelStep": {
+ "label": "Ajouter/Modifier une étape de modèle"
+ },
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Supprimer les éléments sélectionnés"
},
diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json
index 038c349a8..fc4d6e0ec 100644
--- a/src/locales/fr/main.json
+++ b/src/locales/fr/main.json
@@ -684,6 +684,7 @@
},
"menuLabels": {
"About ComfyUI": "À propos de ComfyUI",
+ "Add Edit Model Step": "Ajouter une étape d’édition de modèle",
"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",
diff --git a/src/locales/ja/commands.json b/src/locales/ja/commands.json
index 544e3a8be..c457d0e39 100644
--- a/src/locales/ja/commands.json
+++ b/src/locales/ja/commands.json
@@ -38,6 +38,9 @@
"Comfy_BrowseTemplates": {
"label": "テンプレートを参照"
},
+ "Comfy_Canvas_AddEditModelStep": {
+ "label": "編集モデルステップを追加"
+ },
"Comfy_Canvas_DeleteSelectedItems": {
"label": "選択したアイテムを削除"
},
diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json
index f2b31602f..7f9b733e6 100644
--- a/src/locales/ja/main.json
+++ b/src/locales/ja/main.json
@@ -684,6 +684,7 @@
},
"menuLabels": {
"About ComfyUI": "ComfyUIについて",
+ "Add Edit Model Step": "モデル編集ステップを追加",
"Browse Templates": "テンプレートを参照",
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
diff --git a/src/locales/ko/commands.json b/src/locales/ko/commands.json
index 993418a8d..35ff29d6e 100644
--- a/src/locales/ko/commands.json
+++ b/src/locales/ko/commands.json
@@ -38,6 +38,9 @@
"Comfy_BrowseTemplates": {
"label": "템플릿 탐색"
},
+ "Comfy_Canvas_AddEditModelStep": {
+ "label": "모델 편집 단계 추가"
+ },
"Comfy_Canvas_DeleteSelectedItems": {
"label": "선택한 항목 삭제"
},
diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json
index 53bc55661..6fa26fec7 100644
--- a/src/locales/ko/main.json
+++ b/src/locales/ko/main.json
@@ -684,6 +684,7 @@
},
"menuLabels": {
"About ComfyUI": "ComfyUI에 대하여",
+ "Add Edit Model Step": "모델 편집 단계 추가",
"Browse Templates": "템플릿 탐색",
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
diff --git a/src/locales/ru/commands.json b/src/locales/ru/commands.json
index ebf009d27..ca55f56bd 100644
--- a/src/locales/ru/commands.json
+++ b/src/locales/ru/commands.json
@@ -38,6 +38,9 @@
"Comfy_BrowseTemplates": {
"label": "Просмотр шаблонов"
},
+ "Comfy_Canvas_AddEditModelStep": {
+ "label": "Добавить или изменить шаг модели"
+ },
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Удалить выбранные элементы"
},
diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json
index b0f48ae98..69aec5fec 100644
--- a/src/locales/ru/main.json
+++ b/src/locales/ru/main.json
@@ -684,6 +684,7 @@
},
"menuLabels": {
"About ComfyUI": "О ComfyUI",
+ "Add Edit Model Step": "Добавить или изменить шаг модели",
"Browse Templates": "Просмотреть шаблоны",
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
diff --git a/src/locales/zh/commands.json b/src/locales/zh/commands.json
index 8b767b0f5..f4cf59740 100644
--- a/src/locales/zh/commands.json
+++ b/src/locales/zh/commands.json
@@ -38,6 +38,9 @@
"Comfy_BrowseTemplates": {
"label": "浏览模板"
},
+ "Comfy_Canvas_AddEditModelStep": {
+ "label": "添加编辑模型步骤"
+ },
"Comfy_Canvas_DeleteSelectedItems": {
"label": "删除选定的项目"
},
diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json
index d87d22faf..9083e1507 100644
--- a/src/locales/zh/main.json
+++ b/src/locales/zh/main.json
@@ -684,6 +684,7 @@
},
"menuLabels": {
"About ComfyUI": "关于ComfyUI",
+ "Add Edit Model Step": "添加编辑模型步骤",
"Browse Templates": "浏览模板",
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
"Canvas Toggle Link Visibility": "切换连线可见性",
diff --git a/src/scripts/fluxKontextEditNode.ts b/src/scripts/fluxKontextEditNode.ts
new file mode 100644
index 000000000..454056330
--- /dev/null
+++ b/src/scripts/fluxKontextEditNode.ts
@@ -0,0 +1,693 @@
+import {
+ type INodeOutputSlot,
+ type LGraph,
+ type LGraphNode,
+ LLink,
+ LiteGraph,
+ type Point
+} from '@comfyorg/litegraph'
+import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
+import _ from 'lodash'
+
+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 } }
+) {
+ 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-kontext-dev.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,
+ 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
+}