Compare commits

...

20 Commits

Author SHA1 Message Date
Chenlei Hu
c611c15d40 Update README.md (#271) 2024-07-30 19:12:44 -04:00
Chenlei Hu
269686eebb 1.2.7 (#270) 2024-07-30 19:09:22 -04:00
Chenlei Hu
0e3590d017 Update litegraph (Batch link move with shift + drag) (#268)
* Refactor based on new event data format

* nit

* Add playwright tests

* Update test expectations [skip ci]

* nit

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-07-30 19:06:58 -04:00
Alistor
7d2d6df57b Add spellcheck option to Multiline widget, add Interrupt Queue keybind (#267)
* Add spellcheck option to Multiline widget and set to false by default

* Add Queue Interrupt Keybind

* Update keybinds.ts

Fixed indentation
2024-07-30 17:34:54 -04:00
Chenlei Hu
4462dabc63 Truncate JSON default value in node preview (#264) 2024-07-30 10:12:47 -04:00
Chenlei Hu
53bfc0c95a Block UI interaction when loading (#263) 2024-07-30 09:56:40 -04:00
Chenlei Hu
b78682689e Update litegraph (#262) 2024-07-30 09:25:27 -04:00
Chenlei Hu
6d1dce8255 1.2.6 (#261) 2024-07-29 18:38:51 -04:00
Chenlei Hu
73f4e5143d Attach isLeaf info (#260) 2024-07-29 17:49:57 -04:00
Chenlei Hu
7d75cc99ba Add sort button in node library sidebar tab (#259)
* Add sort button on node library

* tab template
2024-07-29 12:39:54 -04:00
Chenlei Hu
0aa7d0b99a Store spinner state in workspace state store (#256) 2024-07-29 10:54:22 -04:00
Chenlei Hu
66b690e5c8 Manage canvas element in Vue (#255)
* Manage canvas element in Vue

* nit

* Fix unittest
2024-07-29 10:29:29 -04:00
Chenlei Hu
6e27b884fc Fix node preview widget overflow (#254)
* Fix node preview widget overflow

* nit
2024-07-28 21:51:14 -04:00
Chenlei Hu
561162fb3e Update litegraph (Fix drag + alt copy node) (#253)
* Update litegraph

* Update github action

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-07-28 13:39:45 -04:00
Chenlei Hu
7c6bd7ed71 1.2.5 (#252) 2024-07-28 09:41:53 -04:00
Chenlei Hu
fc5bdf80b3 Fix no task message display (#251) 2024-07-28 09:39:17 -04:00
Chenlei Hu
033f242e43 Float workflow selection on top of sidebar (#250) 2024-07-28 09:37:00 -04:00
Chenlei Hu
304429b967 Update README.md (#249) 2024-07-28 08:20:41 -04:00
Chenlei Hu
6dbdb9baa6 Fix clientX/Y offset calculation (#248) 2024-07-28 07:50:00 -04:00
filtered
3e3e909e36 Fix "undo" incorrectly undoing text input (#247)
Fixes an issue where under certain conditions, the ComfyUI custom undo / redo functions would not run when intended to.

When trying to undo an action like deleting several nodes, instead the native browser undo runs - e.g. a textarea gets focus and the last typed text is undone.  Clicking outside the textarea and hitting ctrl + z again just keeps doing the same thing.
2024-07-28 06:57:05 -04:00
30 changed files with 737 additions and 182 deletions

View File

@@ -21,7 +21,7 @@ jobs:
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: "huchenlei/ComfyUI_frontend"
repository: "Comfy-Org/ComfyUI_frontend"
path: "ComfyUI_frontend"
ref: ${{ github.head_ref }}
- uses: actions/setup-node@v3

View File

@@ -14,6 +14,90 @@ For Windows stand-alone build users, please edit the `run_cpu.bat` / `run_nvidia
pause
```
## Release Summary
### Major features
<details>
<summary>v1.2.4: Node library sidebar tab</summary>
#### Drag & Drop
https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f
#### Filter
https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
</details>
<details>
<summary>v1.2.0: Queue/History sidebar tab</summary>
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
</details>
<details>
<summary>v1.1.0: Node search box</summary>
#### Fuzzy search & Node preview
![image](https://github.com/user-attachments/assets/94733e32-ea4e-4a9c-b321-c1a05db48709)
#### Release link with shift
https://github.com/user-attachments/assets/a1b2b5c3-10d1-4256-b620-345de6858f25
</details>
### QoL changes
<details>
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
</details>
<details>
<summary>v1.2.2: **Litegraph** auto connects to correct slot</summary>
#### Before
https://github.com/user-attachments/assets/c253f778-82d5-4e6f-aec0-ea2ccf421651
#### After
https://github.com/user-attachments/assets/b6360ac0-f0d2-447c-9daa-8a2e20c0dc1d
</details>
<details>
<summary>v1.1.8: **Litegraph** hides text overflow on widget value</summary>
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
</details>
### Node developers API
<details>
<summary>v1.2.4: Extension API to register custom side bar tab</summary>
Extensions now can call following API to register a sidebar tab.
```js
app.extensionManager.registerSidebarTab({
id: "search",
icon: "pi pi-search",
title: "search",
tooltip: "search",
type: "custom",
render: (el) => {
el.innerHTML = "<div>Custom search tab</div>";
},
});
```
The list of supported icons can be find here: https://primevue.org/icons/#list
We will support custom icon later.
![image](https://github.com/user-attachments/assets/7bff028a-bf91-4cab-bf97-55c243b3f5e0)
</details>
## Road Map
### What has been done
@@ -25,10 +109,8 @@ pause
- Zod schema for input validation on ComfyUI workflow.
- Make litegraph a npm dependency. <https://github.com/Comfy-Org/ComfyUI_frontend/pull/89>
- Introduce Vue to start managing part of the UI.
- Starting with node search box revamp ![image](https://github.com/user-attachments/assets/ef6ce019-5194-4e55-9f1e-91440e473920)
- Easy install and version management (<https://github.com/comfyanonymous/ComfyUI/pull/3897>).
- Better node management. Sherlock <https://github.com/Nuked88/ComfyUI-N-Sidebar>.
### What to be done
@@ -36,10 +118,9 @@ pause
- Replace the existing ComfyUI front-end impl
- Remove `@ts-ignore`s.
- Turn on `strict` on `tsconfig.json`.
- Introduce a UI library to add more widget types for node developers.
- Add more widget types for node developers.
- LLM streaming node.
- Linear mode (Similar to InvokeAI's linear mode).
- Better node management. Sherlock https://github.com/Nuked88/ComfyUI-N-Sidebar.
- Keybinding settings management. Register keybindings API for custom nodes.
- New extensions API for adding UI-related features.

View File

@@ -0,0 +1,193 @@
{
"last_node_id": 10,
"last_link_id": 9,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
0,
92
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"3Guofeng3_v32Light.safetensors"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
460,
92
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
460,
368
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 10,
"type": "CheckpointLoaderSimple",
"pos": [
0,
276
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"3Guofeng3_v32Light.safetensors"
]
}
],
"links": [
[
3,
4,
1,
6,
0,
"CLIP"
],
[
5,
4,
1,
7,
0,
"CLIP"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -53,4 +53,15 @@ test.describe('Copy Paste', () => {
await comfyPage.ctrlV()
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
})
test('Copy node by dragging + alt', async ({ comfyPage }) => {
// TextEncodeNode1
await comfyPage.page.mouse.move(618, 191)
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.page.mouse.up()
await comfyPage.page.keyboard.up('Alt')
await expect(comfyPage.canvas).toHaveScreenshot('drag-copy-copied-node.png')
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -58,6 +58,28 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
})
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
const outputSlot1Pos = {
x: 304,
y: 127
}
const outputSlot2Pos = {
x: 307,
y: 310
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot(
'batch_move_links_moved.png'
)
})
})
test.describe('Canvas Interaction', () => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -32,4 +32,26 @@ test.describe('Node search box', () => {
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode')
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
})
test('Can auto link batch moved node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('batch_move_links')
const outputSlot1Pos = {
x: 304,
y: 127
}
const emptySpacePos = {
x: 5,
y: 5
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint')
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

35
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{
"name": "comfyui-frontend",
"version": "1.2.4",
"version": "1.2.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "comfyui-frontend",
"version": "1.2.4",
"version": "1.2.7",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
"@comfyorg/litegraph": "^0.7.29",
"@comfyorg/litegraph": "^0.7.34",
"@primevue/themes": "^4.0.0-rc.2",
"@vitejs/plugin-vue": "^5.0.5",
"class-transformer": "^0.5.1",
@@ -54,6 +54,28 @@
"zip-dir": "^2.0.0"
}
},
"../litegraph.js": {
"name": "@comfyorg/litegraph",
"version": "0.7.32",
"extraneous": true,
"license": "MIT",
"devDependencies": {
"@types/jest": "^28.1.3",
"eslint": "^8.37.0 ",
"eslint-plugin-jest": "^27.2.1",
"express": "^4.17.1",
"google-closure-compiler": "^20230411.0.0",
"grunt": "^1.1.0",
"grunt-cli": "^1.2.0",
"grunt-closure-tools": "^1.0.0",
"grunt-contrib-concat": "^2.1.0",
"jest": "^28.1.3",
"jest-cli": "^28.1.3",
"nodemon": "^2.0.22",
"rimraf": "^5.0.0",
"yuidocjs": "^0.10.2"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -1830,9 +1852,10 @@
"dev": true
},
"node_modules/@comfyorg/litegraph": {
"version": "0.7.29",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.29.tgz",
"integrity": "sha512-lXgqcJseywRJQ/B9ClW+5u6VIbDJWy8SMJJ1nxXDgTsE30UUmOnBhZkLZZ3ffMv3QFUcYoNLq5EJn3EFx3g+zA=="
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.34.tgz",
"integrity": "sha512-PLQ9QDTbsVzQE6qr64HWs3z+U8wGHC4GgFeLvEbXL70LLZ1yiWVfNVZI3TsKuVD2jKdsBpDJ8vAdVUW5BwqGyQ==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",

View File

@@ -1,7 +1,7 @@
{
"name": "comfyui-frontend",
"private": true,
"version": "1.2.4",
"version": "1.2.7",
"type": "module",
"scripts": {
"dev": "vite",
@@ -47,7 +47,7 @@
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
"@comfyorg/litegraph": "^0.7.29",
"@comfyorg/litegraph": "^0.7.34",
"@primevue/themes": "^4.0.0-rc.2",
"@vitejs/plugin-vue": "^5.0.5",
"class-transformer": "^0.5.1",

View File

@@ -1,36 +1,22 @@
<template>
<ProgressSpinner v-if="isLoading" class="spinner"></ProgressSpinner>
<div v-else>
<NodeSearchboxPopover v-if="nodeSearchEnabled" />
<teleport to=".graph-canvas-container">
<LiteGraphCanvasSplitterOverlay v-if="betaMenuEnabled">
<template #side-bar-panel>
<SideToolBar />
</template>
</LiteGraphCanvasSplitterOverlay>
</teleport>
</div>
<BlockUI full-screen :blocked="isLoading" />
<GraphCanvas />
</template>
<script setup lang="ts">
import { computed, markRaw, onMounted, onUnmounted, ref, watch } from 'vue'
import NodeSearchboxPopover from '@/components/NodeSearchBoxPopover.vue'
import SideToolBar from '@/components/sidebar/SideToolBar.vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import QueueSideBarTab from '@/components/sidebar/tabs/QueueSideBarTab.vue'
import { computed, markRaw, onMounted, watch } from 'vue'
import BlockUI from 'primevue/blockui'
import ProgressSpinner from 'primevue/progressspinner'
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
import QueueSideBarTab from '@/components/sidebar/tabs/QueueSideBarTab.vue'
import { app } from './scripts/app'
import { useSettingStore } from './stores/settingStore'
import { useI18n } from 'vue-i18n'
import { useWorkspaceStore } from './stores/workspaceStateStore'
import NodeLibrarySideBarTab from './components/sidebar/tabs/NodeLibrarySideBarTab.vue'
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
import { useNodeDefStore } from './stores/nodeDefStore'
const isLoading = ref(true)
const nodeSearchEnabled = computed<boolean>(
() => useSettingStore().get('Comfy.NodeSearchBoxImpl') === 'default'
)
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
const theme = computed<string>(() =>
useSettingStore().get('Comfy.ColorPalette')
)
@@ -47,15 +33,10 @@ watch(
},
{ immediate: true }
)
const betaMenuEnabled = computed(
() => useSettingStore().get('Comfy.UseNewMenu') !== 'Disabled'
)
const { t } = useI18n()
let dropTargetCleanup = () => {}
const init = () => {
useSettingStore().addSettings(app.ui.settings)
app.vueAppReady = true
app.extensionManager = useWorkspaceStore()
app.extensionManager.registerSidebarTab({
id: 'queue',
@@ -73,21 +54,6 @@ const init = () => {
component: markRaw(NodeLibrarySideBarTab),
type: 'vue'
})
dropTargetCleanup = dropTargetForElements({
element: document.querySelector('.graph-canvas-container'),
onDrop: (event) => {
const loc = event.location.current.input
// Add an offset on x to make sure after adding the node, the cursor
// is on the node (top left corner)
const pos = app.clientPosToCanvasPos([loc.clientX - 20, loc.clientY])
const comfyNodeName = event.source.element.getAttribute(
'data-comfy-node-name'
)
const nodeDef = useNodeDefStore().nodeDefsByName[comfyNodeName]
app.addNodeOnGraph(nodeDef, { pos })
}
})
}
onMounted(() => {
@@ -95,14 +61,8 @@ onMounted(() => {
init()
} catch (e) {
console.error('Failed to init Vue app', e)
} finally {
isLoading.value = false
}
})
onUnmounted(() => {
dropTargetCleanup()
})
</script>
<style scoped>

View File

@@ -39,7 +39,9 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
<div class="_sb_col _sb_arrow">&#x25C0;</div>
<div class="_sb_col">{{ widgetInput.name }}</div>
<div class="_sb_col middle-column"></div>
<div class="_sb_col _sb_inherit">{{ widgetInput.default }}</div>
<div class="_sb_col _sb_inherit">
{{ truncateDefaultValue(widgetInput.default) }}
</div>
<div class="_sb_col _sb_arrow">&#x25B6;</div>
</div>
</div>
@@ -71,6 +73,12 @@ const slotInputDefs = allInputDefs.filter(
const widgetInputDefs = allInputDefs.filter((input) =>
nodeDefStore.inputIsWidget(input)
)
const truncateDefaultValue = (value: any): string => {
if (value instanceof Object) {
return _.truncate(JSON.stringify(value), { length: 20 })
}
return value
}
</script>
<style scoped>
@@ -179,7 +187,6 @@ const widgetInputDefs = allInputDefs.filter((input) =>
align-items: center;
padding-left: 9px;
padding-right: 9px;
overflow-x: hidden;
}
._sb_row_string {

View File

@@ -25,14 +25,10 @@ import { app } from '@/scripts/app'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import NodeSearchBox from './NodeSearchBox.vue'
import Dialog from 'primevue/dialog'
import {
INodeSlot,
LiteGraphCanvasEvent,
LGraphNode,
LinkReleaseContext
} from '@comfyorg/litegraph'
import { LiteGraphCanvasEvent, ConnectingLink } from '@comfyorg/litegraph'
import { FilterAndValue } from '@/services/nodeSearchService'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { ConnectingLinkImpl } from '@/types/litegraphTypes'
interface LiteGraphPointerEvent extends Event {
canvasX: number
@@ -67,36 +63,7 @@ const clearFilters = () => {
const closeDialog = () => {
visible.value = false
}
const connectNodeOnLinkRelease = (
node: LGraphNode,
context: LinkReleaseContext
) => {
const destIsInput = context.node_from !== undefined
const srcNode = (
destIsInput ? context.node_from : context.node_to
) as LGraphNode
const srcSlotIndex: number = context.slot_from.slot_index
const linkDataType = destIsInput
? context.type_filter_in
: context.type_filter_out
const destSlots = destIsInput ? node.inputs : node.outputs
const destSlotIndex = destSlots.findIndex(
(slot: INodeSlot) => slot.type === linkDataType
)
if (destSlotIndex === -1) {
console.warn(
`Could not find slot with type ${linkDataType} on node ${node.title}`
)
return
}
if (destIsInput) {
srcNode.connect(srcSlotIndex, node, destSlotIndex)
} else {
node.connect(destSlotIndex, srcNode, srcSlotIndex)
}
}
const addNode = (nodeDef: ComfyNodeDefImpl) => {
closeDialog()
@@ -104,7 +71,9 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
const eventDetail = triggerEvent.value.detail
if (eventDetail.subType === 'empty-release') {
connectNodeOnLinkRelease(node, eventDetail.linkReleaseContext)
eventDetail.linkReleaseContext.links.forEach((link: ConnectingLink) => {
ConnectingLinkImpl.createFromPlainObject(link).connectTo(node)
})
}
}
@@ -117,16 +86,17 @@ const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
}
if (e.detail.subType === 'empty-release') {
const destIsInput = e.detail.linkReleaseContext.node_from !== undefined
const context = e.detail.linkReleaseContext
if (context.links.length === 0) {
console.warn('Empty release with no links! This should never happen')
return
}
const firstLink = ConnectingLinkImpl.createFromPlainObject(context.links[0])
const filter = useNodeDefStore().nodeSearchService.getFilterById(
destIsInput ? 'input' : 'output'
firstLink.releaseSlotType
)
const value = destIsInput
? e.detail.linkReleaseContext.type_filter_in
: e.detail.linkReleaseContext.type_filter_out
addFilter([filter, value])
const dataType = firstLink.type
addFilter([filter, dataType])
}
triggerEvent.value = e
visible.value = true

View File

@@ -0,0 +1,68 @@
<template>
<teleport to=".graph-canvas-container">
<LiteGraphCanvasSplitterOverlay v-if="betaMenuEnabled">
<template #side-bar-panel>
<SideToolBar />
</template>
</LiteGraphCanvasSplitterOverlay>
<canvas ref="canvasRef" id="graph-canvas" tabindex="1" />
</teleport>
<NodeSearchboxPopover v-if="nodeSearchEnabled" />
</template>
<script setup lang="ts">
import SideToolBar from '@/components/sidebar/SideToolBar.vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import NodeSearchboxPopover from '@/components/NodeSearchBoxPopover.vue'
import { ref, onMounted, computed, onUnmounted } from 'vue'
import { app as comfyApp } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
const emit = defineEmits(['ready'])
const canvasRef = ref<HTMLCanvasElement | null>(null)
const settingStore = useSettingStore()
const workspaceStore = useWorkspaceStore()
const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
const nodeSearchEnabled = computed<boolean>(
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
)
let dropTargetCleanup = () => {}
onMounted(async () => {
comfyApp.vueAppReady = true
workspaceStore.spinner = true
await comfyApp.setup(canvasRef.value)
workspaceStore.spinner = false
window['app'] = comfyApp
window['graph'] = comfyApp.graph
dropTargetCleanup = dropTargetForElements({
element: canvasRef.value,
onDrop: (event) => {
const loc = event.location.current.input
// Add an offset on x to make sure after adding the node, the cursor
// is on the node (top left corner)
const pos = comfyApp.clientPosToCanvasPos([loc.clientX - 20, loc.clientY])
const comfyNodeName = event.source.element.getAttribute(
'data-comfy-node-name'
)
const nodeDef = useNodeDefStore().nodeDefsByName[comfyNodeName]
comfyApp.addNodeOnGraph(nodeDef, { pos })
}
})
emit('ready')
})
onUnmounted(() => {
dropTargetCleanup()
})
</script>

View File

@@ -1,63 +1,83 @@
<template>
<TreePlus
class="node-lib-tree"
v-model:expandedKeys="expandedKeys"
selectionMode="single"
:value="renderedRoot.children"
:filter="true"
filterMode="lenient"
dragSelector=".p-tree-node-leaf"
:pt="{
nodeLabel: 'node-lib-tree-node-label',
nodeChildren: ({ props }) => ({
'data-comfy-node-name': props.node?.data?.name,
onMouseenter: (event: MouseEvent) => {
hoveredComfyNodeName = props.node?.data?.name
<SideBarTabTemplate :title="$t('sideToolBar.nodeLibrary')">
<template #tool-buttons>
<ToggleButton
v-model:model-value="alphabeticalSort"
on-icon="pi pi-sort-alpha-down"
off-icon="pi pi-sort-alt"
aria-label="Sort"
:pt="{
label: { style: { display: 'none' } }
}"
v-tooltip="$t('sideToolBar.nodeLibraryTab.sortOrder')"
>
</ToggleButton>
</template>
<template #body>
<TreePlus
class="node-lib-tree"
v-model:expandedKeys="expandedKeys"
selectionMode="single"
:value="renderedRoot.children"
:filter="true"
filterMode="lenient"
dragSelector=".p-tree-node-leaf"
:pt="{
nodeLabel: 'node-lib-tree-node-label',
nodeChildren: ({ props }) => ({
'data-comfy-node-name': props.node?.data?.name,
onMouseenter: (event: MouseEvent) => {
hoveredComfyNodeName = props.node?.data?.name
const hoverTarget = event.target as HTMLElement
const targetRect = hoverTarget.getBoundingClientRect()
nodePreviewStyle.top = `${targetRect.top - 40}px`
nodePreviewStyle.left = `${targetRect.right}px`
},
onMouseleave: () => {
hoveredComfyNodeName = null
}
})
}"
>
<template #folder="{ node }">
<span class="folder-label">{{ node.label }}</span>
<Badge
:value="node.totalNodes"
severity="secondary"
:style="{ marginLeft: '0.5rem' }"
></Badge>
const hoverTarget = event.target as HTMLElement
const targetRect = hoverTarget.getBoundingClientRect()
nodePreviewStyle.top = `${targetRect.top - 40}px`
nodePreviewStyle.left = `${targetRect.right}px`
},
onMouseleave: () => {
hoveredComfyNodeName = null
}
})
}"
>
<template #folder="{ node }">
<span class="folder-label">{{ node.label }}</span>
<Badge
:value="node.totalNodes"
severity="secondary"
:style="{ marginLeft: '0.5rem' }"
></Badge>
</template>
<template #node="{ node }">
<span class="node-label">{{ node.label }}</span>
</template>
</TreePlus>
<div
v-if="hoveredComfyNode"
class="node-lib-node-preview"
:style="nodePreviewStyle"
>
<NodePreview
:key="hoveredComfyNode.name"
:nodeDef="hoveredComfyNode"
></NodePreview>
</div>
</template>
<template #node="{ node }">
<span class="node-label">{{ node.label }}</span>
</template>
</TreePlus>
<div
v-if="hoveredComfyNode"
class="node-lib-node-preview"
:style="nodePreviewStyle"
>
<NodePreview
:key="hoveredComfyNode.name"
:nodeDef="hoveredComfyNode"
></NodePreview>
</div>
</SideBarTabTemplate>
</template>
<script setup lang="ts">
import Badge from 'primevue/badge'
import ToggleButton from 'primevue/togglebutton'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { computed, ref } from 'vue'
import { TreeNode } from 'primevue/treenode'
import TreePlus from '@/components/primevueOverride/TreePlus.vue'
import NodePreview from '@/components/NodePreview.vue'
import SideBarTabTemplate from '@/components/sidebar/tabs/SideBarTabTemplate.vue'
const nodeDefStore = useNodeDefStore()
const alphabeticalSort = ref(false)
const expandedKeys = ref({})
const hoveredComfyNodeName = ref<string | null>(null)
const hoveredComfyNode = computed<ComfyNodeDefImpl | null>(() => {
@@ -72,14 +92,15 @@ const nodePreviewStyle = ref<Record<string, string>>({
left: '0px'
})
const root = computed(() => nodeDefStore.nodeTree)
const root = computed(() =>
alphabeticalSort.value ? nodeDefStore.sortedNodeTree : nodeDefStore.nodeTree
)
const renderedRoot = computed(() => {
return fillNodeInfo(root.value)
})
const fillNodeInfo = (node: TreeNode): TreeNode => {
const isLeaf = node.children === undefined || node.children.length === 0
const isExpanded = expandedKeys.value[node.key]
const icon = isLeaf
const icon = node.leaf
? 'pi pi-circle-fill'
: isExpanded
? 'pi pi-folder-open'
@@ -90,8 +111,8 @@ const fillNodeInfo = (node: TreeNode): TreeNode => {
...node,
icon,
children,
type: isLeaf ? 'node' : 'folder',
totalNodes: isLeaf
type: node.leaf ? 'node' : 'folder',
totalNodes: node.leaf
? 1
: children.reduce((acc, child) => acc + child.totalNodes, 0)
}

View File

@@ -59,7 +59,7 @@
</template>
</Column>
</DataTable>
<div>
<div v-else>
<Message icon="pi pi-info" severity="error">
<span class="ml-2">No tasks</span>
</Message>

View File

@@ -0,0 +1,65 @@
<template>
<div class="comfy-vue-side-bar-container">
<Toolbar class="comfy-vue-side-bar-header">
<template #start>
<span class="comfy-vue-side-bar-header-span">{{
props.title.toUpperCase()
}}</span>
</template>
<template #end>
<slot name="tool-buttons"></slot>
</template>
</Toolbar>
<div class="comfy-vue-side-bar-body">
<slot name="body"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import Toolbar from 'primevue/toolbar'
const props = defineProps({
title: {
type: String,
required: true
}
})
</script>
<style scoped>
.comfy-vue-side-bar-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.comfy-vue-side-bar-header {
flex-shrink: 0;
border-left: none;
border-right: none;
border-top: none;
border-radius: 0;
padding: 0.25rem 1rem;
}
.comfy-vue-side-bar-header-span {
font-size: small;
}
.comfy-vue-side-bar-body {
flex-grow: 1;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}
.comfy-vue-side-bar-body::-webkit-scrollbar {
width: 1px;
}
.comfy-vue-side-bar-body::-webkit-scrollbar-thumb {
background-color: transparent;
}
</style>

View File

@@ -1,4 +1,5 @@
import { app } from '../../scripts/app'
import { api } from '../../scripts/api'
app.registerExtension({
name: 'Comfy.Keybinds',
@@ -6,8 +7,14 @@ app.registerExtension({
const keybindListener = function (event) {
const modifierPressed = event.ctrlKey || event.metaKey
// Queue prompt using ctrl or command + enter
// Queue prompt using (ctrl or command) + enter
if (modifierPressed && event.key === 'Enter') {
// Cancel current prompt using (ctrl or command) + alt + enter
if(event.altKey) {
api.interrupt()
return
}
// Queue prompt as first for generation using (ctrl or command) + shift + enter
app.queuePrompt(event.shiftKey ? -1 : 0).then()
return
}

View File

@@ -6,7 +6,10 @@ const messages = {
settings: 'Settings',
themeToggle: 'Toggle Theme',
queue: 'Queue',
nodeLibrary: 'Node Library'
nodeLibrary: 'Node Library',
nodeLibraryTab: {
sortOrder: 'Sort Order'
}
}
},
zh: {
@@ -14,7 +17,10 @@ const messages = {
settings: '设置',
themeToggle: '主题切换',
queue: '队列',
nodeLibrary: '节点库'
nodeLibrary: '节点库',
nodeLibraryTab: {
sortOrder: '排序顺序'
}
}
}
// TODO: Add more languages

View File

@@ -8,7 +8,6 @@ import Tooltip from 'primevue/tooltip'
import 'primeicons/primeicons.css'
import App from './App.vue'
import { app as comfyApp } from '@/scripts/app'
import { createPinia } from 'pinia'
import { i18n } from './i18n'
@@ -40,8 +39,3 @@ app
.use(pinia)
.use(i18n)
.mount('#vue-app')
comfyApp.setup().then(() => {
window['app'] = comfyApp
window['graph'] = comfyApp.graph
})

View File

@@ -1853,17 +1853,13 @@ export class ComfyApp {
/**
* Set up the app on the page
*/
async setup() {
async setup(canvasEl: HTMLCanvasElement) {
this.canvasEl = canvasEl
await this.#setUser()
// Create and mount the LiteGraph in the DOM
const mainCanvas = document.createElement('canvas')
mainCanvas.style.touchAction = 'none'
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, {
id: 'graph-canvas'
}))
canvasEl.tabIndex = 1
this.canvasContainer.prepend(canvasEl)
this.resizeCanvas()
@@ -2957,7 +2953,7 @@ export class ComfyApp {
const rect = this.canvasContainer.getBoundingClientRect()
const containerOffsets = [rect.left, rect.top]
return _.zip(pos, this.canvas.ds.offset, containerOffsets).map(
([p, o1, o2]) => p / this.canvas.ds.scale - o1 - o2
([p, o1, o2]) => (p - o2) / this.canvas.ds.scale - o1
) as Vector2
}
}

View File

@@ -110,11 +110,11 @@ export class ChangeTracker {
window.addEventListener(
'keydown',
(e) => {
const activeEl = document.activeElement;
requestAnimationFrame(async () => {
let activeEl
let bindInputEl
// If we are auto queue in change mode then we do want to trigger on inputs
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === 'instant') {
activeEl = document.activeElement
if (
activeEl?.tagName === 'INPUT' ||
activeEl?.['type'] === 'textarea'
@@ -122,6 +122,7 @@ export class ChangeTracker {
// Ignore events on inputs, they have their native history
return
}
bindInputEl = activeEl
}
keyIgnored =
@@ -135,7 +136,7 @@ export class ChangeTracker {
if (await changeTracker().undoRedo(e)) return
// If our active element is some type of input then handle changes after they're done
if (ChangeTracker.bindInput(app, activeEl)) return
if (ChangeTracker.bindInput(app, bindInputEl)) return
changeTracker().checkState()
})
},

View File

@@ -261,7 +261,7 @@
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
z-index: 400;
z-index: 1001;
}
.comfyui-workflows-panel {

View File

@@ -313,6 +313,7 @@ function addMultilineWidget(node, name, opts, app) {
inputEl.className = 'comfy-multiline-input'
inputEl.value = opts.defaultVal
inputEl.placeholder = opts.placeholder || name
inputEl.spellcheck = opts.spellcheck || false
const widget = node.addDOMWidget(name, 'customtext', inputEl, {
getValue() {

View File

@@ -218,6 +218,27 @@ export const SYSTEM_NODE_DEFS: ComfyNodeDef[] = [
}
]
function sortedTree(node: TreeNode): TreeNode {
// Create a new node with the same label and data
const newNode: TreeNode = {
...node
}
if (node.children) {
// Sort the children of the current node
const sortedChildren = [...node.children].sort((a, b) =>
a.label.localeCompare(b.label)
)
// Recursively sort the children and add them to the new node
newNode.children = []
for (const child of sortedChildren) {
newNode.children.push(sortedTree(child))
}
}
return newNode
}
interface State {
nodeDefsByName: Record<string, ComfyNodeDefImpl>
widgets: Record<string, ComfyWidgetConstructor>
@@ -239,6 +260,7 @@ export const useNodeDefStore = defineStore('nodeDef', {
const root: TreeNode = {
key: 'root',
label: 'Nodes',
leaf: false,
children: []
}
for (const nodeDef of Object.values(state.nodeDefsByName)) {
@@ -249,7 +271,7 @@ export const useNodeDefStore = defineStore('nodeDef', {
key += `/${part}`
let next = current.children.find((child) => child.label === part)
if (!next) {
next = { key, label: part, children: [] }
next = { key, label: part, children: [], leaf: false }
current.children.push(next)
}
current = next
@@ -257,10 +279,14 @@ export const useNodeDefStore = defineStore('nodeDef', {
current.children.push({
label: nodeDef.display_name,
data: nodeDef,
key: `${key}/${nodeDef.name}`
key: `${key}/${nodeDef.name}`,
leaf: true
})
}
return root
},
sortedNodeTree(): TreeNode {
return sortedTree(this.nodeTree)
}
},
actions: {

View File

@@ -2,12 +2,14 @@ import { SidebarTabExtension } from '@/types/extensionTypes'
import { defineStore } from 'pinia'
interface WorkspaceState {
spinner: boolean
activeSidebarTab: string | null
sidebarTabs: SidebarTabExtension[]
}
export const useWorkspaceStore = defineStore('workspace', {
state: (): WorkspaceState => ({
spinner: false,
activeSidebarTab: null,
sidebarTabs: []
}),

View File

@@ -0,0 +1,74 @@
import {
ConnectingLink,
LGraphNode,
Vector2,
INodeInputSlot,
INodeOutputSlot,
INodeSlot
} from '@comfyorg/litegraph'
export class ConnectingLinkImpl implements ConnectingLink {
node: LGraphNode
slot: number
input: INodeInputSlot | null
output: INodeOutputSlot | null
pos: Vector2
constructor(
node: LGraphNode,
slot: number,
input: INodeInputSlot | null,
output: INodeOutputSlot | null,
pos: Vector2
) {
this.node = node
this.slot = slot
this.input = input
this.output = output
this.pos = pos
}
static createFromPlainObject(obj: ConnectingLink) {
return new ConnectingLinkImpl(
obj.node,
obj.slot,
obj.input,
obj.output,
obj.pos
)
}
get type(): string | null {
const result = this.input ? this.input.type : this.output.type
return result === -1 ? null : result
}
/**
* Which slot type is release and need to be reconnected.
* - 'output' means we need a new node's outputs slot to connect with this link
*/
get releaseSlotType(): 'input' | 'output' {
return this.output ? 'input' : 'output'
}
connectTo(newNode: LGraphNode) {
const newNodeSlots =
this.releaseSlotType === 'output' ? newNode.outputs : newNode.inputs
const newNodeSlot = newNodeSlots.findIndex(
(slot: INodeSlot) => slot.type === this.type
)
if (newNodeSlot === -1) {
console.warn(
`Could not find slot with type ${this.type} on node ${newNode.title}. This should never happen`
)
return
}
if (this.releaseSlotType === 'input') {
this.node.connect(this.slot, newNode, newNodeSlot)
} else {
newNode.connect(newNodeSlot, this.node, this.slot)
}
}
}

View File

@@ -43,7 +43,12 @@ export async function start(config: StartConfig = {}): Promise<StartResult> {
const { app } = await import('../../src/scripts/app')
const { LiteGraph, LGraphCanvas } = await import('@comfyorg/litegraph')
config.preSetup?.(app)
await app.setup()
const canvasEl = document.createElement('canvas')
canvasEl.style.touchAction = 'none'
canvasEl.id = 'graph-canvas'
canvasEl.tabIndex = 1
app.canvasContainer.prepend(canvasEl)
await app.setup(canvasEl)
// @ts-ignore
return { ...Ez.graph(app, LiteGraph, LGraphCanvas), app }