mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-03 05:38:26 +00:00
Compare commits
50 Commits
v1.24.2
...
manager-mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fdbc28dac | ||
|
|
8075db41fa | ||
|
|
4f9470751d | ||
|
|
f1610db470 | ||
|
|
1aee43425a | ||
|
|
bdd1230902 | ||
|
|
b01056e31f | ||
|
|
e780e1d6d6 | ||
|
|
f8953a874d | ||
|
|
aba2e5edfc | ||
|
|
eb5c49f67b | ||
|
|
54a0981031 | ||
|
|
de6ed34836 | ||
|
|
588bd99c3a | ||
|
|
c28d4514fc | ||
|
|
730b278fa0 | ||
|
|
2e2647d1d0 | ||
|
|
7073c9d57f | ||
|
|
1cb94ca677 | ||
|
|
558cfcc527 | ||
|
|
babe324f8b | ||
|
|
a83a7faea4 | ||
|
|
dc8589672c | ||
|
|
0f7e3f7100 | ||
|
|
8fbd076b9c | ||
|
|
096e0c9ad0 | ||
|
|
60e2f14516 | ||
|
|
0f9d1f833d | ||
|
|
4249fe7a76 | ||
|
|
5e4371507f | ||
|
|
39ad01826a | ||
|
|
b683a81239 | ||
|
|
ba1d067c04 | ||
|
|
f241b7f7a0 | ||
|
|
b3486695d3 | ||
|
|
7725b87c80 | ||
|
|
ced03ceee8 | ||
|
|
4a67a83252 | ||
|
|
3862d78fb3 | ||
|
|
05ec65f636 | ||
|
|
dc2a4708f3 | ||
|
|
eee3b74547 | ||
|
|
3c8adf4f80 | ||
|
|
d3a629ce89 | ||
|
|
7c14b26321 | ||
|
|
5867103981 | ||
|
|
363a46b1bf | ||
|
|
1f41367454 | ||
|
|
2aed31ade1 | ||
|
|
366c55070c |
4
.github/workflows/pr-checks.yml
vendored
4
.github/workflows/pr-checks.yml
vendored
@@ -76,10 +76,10 @@ jobs:
|
||||
- name: Check for screen recording
|
||||
id: check-recording
|
||||
if: steps.check-changes.outputs.should_run == 'true'
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
run: |
|
||||
# Check PR body for screen recording
|
||||
PR_BODY="${{ github.event.pull_request.body }}"
|
||||
|
||||
# Check for GitHub user attachments or YouTube links
|
||||
if echo "$PR_BODY" | grep -qiE 'github\.com/user-attachments/assets/[a-f0-9-]+|youtube\.com/watch|youtu\.be/'; then
|
||||
echo "has_recording=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
{
|
||||
"id": "fe4562c0-3a0b-4614-bdec-7039a58d75b8",
|
||||
"revision": 0,
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "e5fb1765-9323-4548-801a-5aead34d879e",
|
||||
"pos": [
|
||||
627.5973510742188,
|
||||
423.0972900390625
|
||||
],
|
||||
"size": [
|
||||
144.15234375,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "e5fb1765-9323-4548-801a-5aead34d879e",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 2,
|
||||
"lastLinkId": 4,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [
|
||||
347.90441582814213,
|
||||
417.3822440655296,
|
||||
120,
|
||||
60
|
||||
]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [
|
||||
892.5973510742188,
|
||||
416.0972900390625,
|
||||
120,
|
||||
60
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "c5cc99d8-a2b6-4bf3-8be7-d4949ef736cd",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"linkIds": [
|
||||
1
|
||||
],
|
||||
"pos": {
|
||||
"0": 447.9044189453125,
|
||||
"1": 437.3822326660156
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "9bd488b9-e907-4c95-a7a4-85c5597a87af",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"linkIds": [
|
||||
2
|
||||
],
|
||||
"pos": {
|
||||
"0": 912.5973510742188,
|
||||
"1": 436.0972900390625
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
554.8743286132812,
|
||||
100.95539093017578
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "model",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "positive",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"localized_name": "negative",
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "latent_image",
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"simple",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "VAEEncode",
|
||||
"pos": [
|
||||
685.1265869140625,
|
||||
439.1734619140625
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "pixels",
|
||||
"name": "pixels",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "vae",
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEEncode"
|
||||
}
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 1,
|
||||
"target_slot": 1,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 1,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "LATENT"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8894351682943402,
|
||||
"offset": [
|
||||
58.7671207025881,
|
||||
137.7124650620126
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.24.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,412 +0,0 @@
|
||||
{
|
||||
"id": "c4a254bb-935e-4013-b380-5e36954de4b0",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 11,
|
||||
"type": "422723e8-4bf6-438c-823f-881ca81acead",
|
||||
"pos": [
|
||||
791.59912109375,
|
||||
386.13336181640625
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
202
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"",
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "422723e8-4bf6-438c-823f-881ca81acead",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 12,
|
||||
"lastLinkId": 16,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [
|
||||
481.59912109375,
|
||||
379.13336181640625,
|
||||
120,
|
||||
160
|
||||
]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [
|
||||
1121.59912109375,
|
||||
379.13336181640625,
|
||||
128.6640625,
|
||||
60
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "0f07c10e-5705-4764-9b24-b69606c6dbcc",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"linkIds": [
|
||||
10
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 399.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "736e5a03-0f7f-4e48-93e4-fd66ea6c30f1",
|
||||
"name": "text_1",
|
||||
"type": "STRING",
|
||||
"linkIds": [
|
||||
11
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 419.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b62e7a0b-cc7e-4ca5-a4e1-c81607a13f58",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"linkIds": [
|
||||
13
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 439.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7a2628da-4879-4f82-a7d3-7b1c00db50a5",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"linkIds": [
|
||||
14
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 459.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "651cf4ad-e8bf-47f6-b181-8f8aeacd6669",
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"linkIds": [
|
||||
15
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 479.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c41765ea-61ef-4a77-8cc6-74113903078f",
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"linkIds": [
|
||||
16
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 499.13336181640625
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "55dd1505-12bd-4cb4-8e75-031a97bb4387",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"linkIds": [
|
||||
12
|
||||
],
|
||||
"pos": {
|
||||
"0": 1141.59912109375,
|
||||
"1": 399.13336181640625
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
661.59912109375,
|
||||
314.13336181640625
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "text",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "text"
|
||||
},
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
668.755859375,
|
||||
571.7766723632812
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "text",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "text"
|
||||
},
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
12
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
671.7379760742188,
|
||||
1.621593713760376
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "model",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 13
|
||||
},
|
||||
{
|
||||
"localized_name": "positive",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 14
|
||||
},
|
||||
{
|
||||
"localized_name": "negative",
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 15
|
||||
},
|
||||
{
|
||||
"localized_name": "latent_image",
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 16
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"simple",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 10,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 11,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"origin_id": 11,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 2,
|
||||
"target_id": 12,
|
||||
"target_slot": 0,
|
||||
"type": "MODEL"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 3,
|
||||
"target_id": 12,
|
||||
"target_slot": 1,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 4,
|
||||
"target_id": 12,
|
||||
"target_slot": 2,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 5,
|
||||
"target_id": 12,
|
||||
"target_slot": 3,
|
||||
"type": "LATENT"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.9581355200690549,
|
||||
"offset": [
|
||||
184.687451089395,
|
||||
80.38288288288285
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.24.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
{
|
||||
"id": "c4a254bb-935e-4013-b380-5e36954de4b0",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 11,
|
||||
"type": "422723e8-4bf6-438c-823f-881ca81acead",
|
||||
"pos": [
|
||||
400,
|
||||
300
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
168
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "422723e8-4bf6-438c-823f-881ca81acead",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 11,
|
||||
"lastLinkId": 15,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [
|
||||
481.59912109375,
|
||||
379.13336181640625,
|
||||
120,
|
||||
160
|
||||
]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [
|
||||
1121.59912109375,
|
||||
379.13336181640625,
|
||||
120,
|
||||
40
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "0f07c10e-5705-4764-9b24-b69606c6dbcc",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"linkIds": [
|
||||
10
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 399.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "214a5060-24dd-4299-ab78-8027dc5b9c59",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"linkIds": [
|
||||
11
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 419.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8ab94c5d-e7df-433c-9177-482a32340552",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"linkIds": [
|
||||
12
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 439.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8a4cd719-8c67-473b-9b44-ac0582d02641",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"linkIds": [
|
||||
13
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 459.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a78d6b3a-ad40-4300-b0a5-2cdbdb8dc135",
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"linkIds": [
|
||||
14
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 479.13336181640625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4c7abe0c-902d-49ef-a5b0-cbf02b50b693",
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"linkIds": [
|
||||
15
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 499.13336181640625
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
661.59912109375,
|
||||
314.13336181640625
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 11
|
||||
},
|
||||
{
|
||||
"localized_name": "text",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "text"
|
||||
},
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "KSampler",
|
||||
"pos": [
|
||||
674.1234741210938,
|
||||
570.5839233398438
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "model",
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 12
|
||||
},
|
||||
{
|
||||
"localized_name": "positive",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 13
|
||||
},
|
||||
{
|
||||
"localized_name": "negative",
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 14
|
||||
},
|
||||
{
|
||||
"localized_name": "latent_image",
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 15
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "LATENT",
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"simple",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 10,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 10,
|
||||
"target_slot": 0,
|
||||
"type": "CLIP"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 2,
|
||||
"target_id": 11,
|
||||
"target_slot": 0,
|
||||
"type": "MODEL"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 3,
|
||||
"target_id": 11,
|
||||
"target_slot": 1,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 4,
|
||||
"target_id": 11,
|
||||
"target_slot": 2,
|
||||
"type": "CONDITIONING"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 5,
|
||||
"target_id": 11,
|
||||
"target_slot": 3,
|
||||
"type": "LATENT"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.9581355200690549,
|
||||
"offset": [
|
||||
258.6405769416877,
|
||||
147.17927927927929
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.24.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
{
|
||||
"id": "c4a254bb-935e-4013-b380-5e36954de4b0",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 11,
|
||||
"type": "422723e8-4bf6-438c-823f-881ca81acead",
|
||||
"pos": [
|
||||
791.59912109375,
|
||||
386.13336181640625
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "422723e8-4bf6-438c-823f-881ca81acead",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 10,
|
||||
"lastLinkId": 10,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "New Subgraph",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [
|
||||
481.59912109375,
|
||||
379.13336181640625,
|
||||
120,
|
||||
60
|
||||
]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [
|
||||
1121.59912109375,
|
||||
379.13336181640625,
|
||||
120,
|
||||
40
|
||||
]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "79e69fca-ad12-499b-8d9b-9f1656b85354",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"linkIds": [
|
||||
10
|
||||
],
|
||||
"pos": {
|
||||
"0": 581.59912109375,
|
||||
"1": 399.13336181640625
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
661.59912109375,
|
||||
314.13336181640625
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 10,
|
||||
"target_slot": 0,
|
||||
"type": "CLIP"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.24.1",
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true,
|
||||
"ds": {
|
||||
"scale": 0.9581355200690549,
|
||||
"offset": [
|
||||
258.6405769416877,
|
||||
147.17927927927929
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from './components/SidebarTab'
|
||||
import { Topbar } from './components/Topbar'
|
||||
import type { Position, Size } from './types'
|
||||
import { NodeReference, SubgraphSlotReference } from './utils/litegraphUtils'
|
||||
import { NodeReference } from './utils/litegraphUtils'
|
||||
import TaskHistory from './utils/taskHistory'
|
||||
|
||||
dotenv.config()
|
||||
@@ -776,524 +776,11 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on a litegraph context menu item (uses .litemenu-entry selector).
|
||||
* Use this for canvas/node context menus, not PrimeVue menus.
|
||||
*/
|
||||
async clickLitegraphContextMenuItem(name: string): Promise<void> {
|
||||
await this.page.locator(`.litemenu-entry:has-text("${name}")`).click()
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Right-clicks on a subgraph input slot to open the context menu.
|
||||
* Must be called when inside a subgraph.
|
||||
*
|
||||
* This method uses the actual slot positions from the subgraph.inputs array,
|
||||
* which contain the correct coordinates for each input slot. These positions
|
||||
* are different from the visual node positions and are specifically where
|
||||
* the slots are rendered on the input node.
|
||||
*
|
||||
* @param inputName Optional name of the specific input slot to target (e.g., 'text').
|
||||
* If not provided, tries all available input slots until one works.
|
||||
* @returns Promise that resolves when the context menu appears
|
||||
*/
|
||||
async rightClickSubgraphInputSlot(inputName?: string): Promise<void> {
|
||||
const foundSlot = await this.page.evaluate(async (targetInputName) => {
|
||||
const app = window['app']
|
||||
const currentGraph = app.canvas.graph
|
||||
|
||||
// Check if we're in a subgraph
|
||||
if (currentGraph.constructor.name !== 'Subgraph') {
|
||||
throw new Error(
|
||||
'Not in a subgraph - this method only works inside subgraphs'
|
||||
)
|
||||
}
|
||||
|
||||
// Get the input node
|
||||
const inputNode = currentGraph.inputNode
|
||||
if (!inputNode) {
|
||||
throw new Error('No input node found in subgraph')
|
||||
}
|
||||
|
||||
// Get available inputs
|
||||
const inputs = currentGraph.inputs
|
||||
if (!inputs || inputs.length === 0) {
|
||||
throw new Error('No input slots found in subgraph')
|
||||
}
|
||||
|
||||
// Filter to specific input if requested
|
||||
const inputsToTry = targetInputName
|
||||
? inputs.filter((inp) => inp.name === targetInputName)
|
||||
: inputs
|
||||
|
||||
if (inputsToTry.length === 0) {
|
||||
throw new Error(
|
||||
targetInputName
|
||||
? `Input slot '${targetInputName}' not found`
|
||||
: 'No input slots available to try'
|
||||
)
|
||||
}
|
||||
|
||||
// Try right-clicking on each input slot position until one works
|
||||
for (const input of inputsToTry) {
|
||||
if (!input.pos) continue
|
||||
|
||||
const testX = input.pos[0]
|
||||
const testY = input.pos[1]
|
||||
|
||||
// Create a right-click event at the input slot position
|
||||
const rightClickEvent = {
|
||||
canvasX: testX,
|
||||
canvasY: testY,
|
||||
button: 2, // Right mouse button
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {}
|
||||
}
|
||||
|
||||
// Trigger the input node's right-click handler
|
||||
if (inputNode.onPointerDown) {
|
||||
inputNode.onPointerDown(
|
||||
rightClickEvent,
|
||||
app.canvas.pointer,
|
||||
app.canvas.linkConnector
|
||||
)
|
||||
}
|
||||
|
||||
// Wait briefly for menu to appear
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Check if litegraph context menu appeared
|
||||
const menuExists = document.querySelector('.litemenu-entry')
|
||||
if (menuExists) {
|
||||
return { success: true, inputName: input.name, x: testX, y: testY }
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false }
|
||||
}, inputName)
|
||||
|
||||
if (!foundSlot.success) {
|
||||
throw new Error(
|
||||
inputName
|
||||
? `Could not open context menu for input slot '${inputName}'`
|
||||
: 'Could not find any input slot position to right-click'
|
||||
)
|
||||
}
|
||||
|
||||
// Wait for the litegraph context menu to be visible
|
||||
await this.page.waitForSelector('.litemenu-entry', {
|
||||
state: 'visible',
|
||||
timeout: 5000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Right-clicks on a subgraph output slot to open the context menu.
|
||||
* Must be called when inside a subgraph.
|
||||
*
|
||||
* Similar to rightClickSubgraphInputSlot but for output slots.
|
||||
*
|
||||
* @param outputName Optional name of the specific output slot to target.
|
||||
* If not provided, tries all available output slots until one works.
|
||||
* @returns Promise that resolves when the context menu appears
|
||||
*/
|
||||
async rightClickSubgraphOutputSlot(outputName?: string): Promise<void> {
|
||||
const foundSlot = await this.page.evaluate(async (targetOutputName) => {
|
||||
const app = window['app']
|
||||
const currentGraph = app.canvas.graph
|
||||
|
||||
// Check if we're in a subgraph
|
||||
if (currentGraph.constructor.name !== 'Subgraph') {
|
||||
throw new Error(
|
||||
'Not in a subgraph - this method only works inside subgraphs'
|
||||
)
|
||||
}
|
||||
|
||||
// Get the output node
|
||||
const outputNode = currentGraph.outputNode
|
||||
if (!outputNode) {
|
||||
throw new Error('No output node found in subgraph')
|
||||
}
|
||||
|
||||
// Get available outputs
|
||||
const outputs = currentGraph.outputs
|
||||
if (!outputs || outputs.length === 0) {
|
||||
throw new Error('No output slots found in subgraph')
|
||||
}
|
||||
|
||||
// Filter to specific output if requested
|
||||
const outputsToTry = targetOutputName
|
||||
? outputs.filter((out) => out.name === targetOutputName)
|
||||
: outputs
|
||||
|
||||
if (outputsToTry.length === 0) {
|
||||
throw new Error(
|
||||
targetOutputName
|
||||
? `Output slot '${targetOutputName}' not found`
|
||||
: 'No output slots available to try'
|
||||
)
|
||||
}
|
||||
|
||||
// Try right-clicking on each output slot position until one works
|
||||
for (const output of outputsToTry) {
|
||||
if (!output.pos) continue
|
||||
|
||||
const testX = output.pos[0]
|
||||
const testY = output.pos[1]
|
||||
|
||||
// Create a right-click event at the output slot position
|
||||
const rightClickEvent = {
|
||||
canvasX: testX,
|
||||
canvasY: testY,
|
||||
button: 2, // Right mouse button
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {}
|
||||
}
|
||||
|
||||
// Trigger the output node's right-click handler
|
||||
if (outputNode.onPointerDown) {
|
||||
outputNode.onPointerDown(
|
||||
rightClickEvent,
|
||||
app.canvas.pointer,
|
||||
app.canvas.linkConnector
|
||||
)
|
||||
}
|
||||
|
||||
// Wait briefly for menu to appear
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Check if litegraph context menu appeared
|
||||
const menuExists = document.querySelector('.litemenu-entry')
|
||||
if (menuExists) {
|
||||
return { success: true, outputName: output.name, x: testX, y: testY }
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false }
|
||||
}, outputName)
|
||||
|
||||
if (!foundSlot.success) {
|
||||
throw new Error(
|
||||
outputName
|
||||
? `Could not open context menu for output slot '${outputName}'`
|
||||
: 'Could not find any output slot position to right-click'
|
||||
)
|
||||
}
|
||||
|
||||
// Wait for the litegraph context menu to be visible
|
||||
await this.page.waitForSelector('.litemenu-entry', {
|
||||
state: 'visible',
|
||||
timeout: 5000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a reference to a subgraph input slot
|
||||
*/
|
||||
async getSubgraphInputSlot(
|
||||
slotName?: string
|
||||
): Promise<SubgraphSlotReference> {
|
||||
return new SubgraphSlotReference('input', slotName || '', this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a reference to a subgraph output slot
|
||||
*/
|
||||
async getSubgraphOutputSlot(
|
||||
slotName?: string
|
||||
): Promise<SubgraphSlotReference> {
|
||||
return new SubgraphSlotReference('output', slotName || '', this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a regular node output to a subgraph input.
|
||||
* This creates a new input slot on the subgraph if targetInputName is not provided.
|
||||
*/
|
||||
async connectToSubgraphInput(
|
||||
sourceNode: NodeReference,
|
||||
sourceSlotIndex: number,
|
||||
targetInputName?: string
|
||||
): Promise<void> {
|
||||
const sourceSlot = await sourceNode.getOutput(sourceSlotIndex)
|
||||
const targetSlot = await this.getSubgraphInputSlot(targetInputName)
|
||||
|
||||
const targetPosition = targetInputName
|
||||
? await targetSlot.getPosition() // Connect to existing slot
|
||||
: await targetSlot.getOpenSlotPosition() // Create new slot
|
||||
|
||||
await this.dragAndDrop(await sourceSlot.getPosition(), targetPosition)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a subgraph input to a regular node input.
|
||||
* This creates a new input slot on the subgraph if sourceInputName is not provided.
|
||||
*/
|
||||
async connectFromSubgraphInput(
|
||||
targetNode: NodeReference,
|
||||
targetSlotIndex: number,
|
||||
sourceInputName?: string
|
||||
): Promise<void> {
|
||||
const sourceSlot = await this.getSubgraphInputSlot(sourceInputName)
|
||||
const targetSlot = await targetNode.getInput(targetSlotIndex)
|
||||
|
||||
const sourcePosition = sourceInputName
|
||||
? await sourceSlot.getPosition() // Connect from existing slot
|
||||
: await sourceSlot.getOpenSlotPosition() // Create new slot
|
||||
|
||||
const targetPosition = await targetSlot.getPosition()
|
||||
|
||||
// Debug: Log the positions we're trying to use
|
||||
console.log('Drag positions:', {
|
||||
source: sourcePosition,
|
||||
target: targetPosition
|
||||
})
|
||||
|
||||
await this.dragAndDrop(sourcePosition, targetPosition)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a regular node output to a subgraph output.
|
||||
* This creates a new output slot on the subgraph if targetOutputName is not provided.
|
||||
*/
|
||||
async connectToSubgraphOutput(
|
||||
sourceNode: NodeReference,
|
||||
sourceSlotIndex: number,
|
||||
targetOutputName?: string
|
||||
): Promise<void> {
|
||||
const sourceSlot = await sourceNode.getOutput(sourceSlotIndex)
|
||||
const targetSlot = await this.getSubgraphOutputSlot(targetOutputName)
|
||||
|
||||
const targetPosition = targetOutputName
|
||||
? await targetSlot.getPosition() // Connect to existing slot
|
||||
: await targetSlot.getOpenSlotPosition() // Create new slot
|
||||
|
||||
await this.dragAndDrop(await sourceSlot.getPosition(), targetPosition)
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a subgraph output to a regular node input.
|
||||
* This creates a new output slot on the subgraph if sourceOutputName is not provided.
|
||||
*/
|
||||
async connectFromSubgraphOutput(
|
||||
targetNode: NodeReference,
|
||||
targetSlotIndex: number,
|
||||
sourceOutputName?: string
|
||||
): Promise<void> {
|
||||
const sourceSlot = await this.getSubgraphOutputSlot(sourceOutputName)
|
||||
const targetSlot = await targetNode.getInput(targetSlotIndex)
|
||||
|
||||
const sourcePosition = sourceOutputName
|
||||
? await sourceSlot.getPosition() // Connect from existing slot
|
||||
: await sourceSlot.getOpenSlotPosition() // Create new slot
|
||||
|
||||
await this.dragAndDrop(sourcePosition, await targetSlot.getPosition())
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a visual marker at a position for debugging
|
||||
*/
|
||||
async debugAddMarker(
|
||||
position: Position,
|
||||
id: string = 'debug-marker'
|
||||
): Promise<void> {
|
||||
await this.page.evaluate(
|
||||
([pos, markerId]) => {
|
||||
// Remove existing marker if present
|
||||
const existing = document.getElementById(markerId)
|
||||
if (existing) existing.remove()
|
||||
|
||||
// Create marker
|
||||
const marker = document.createElement('div')
|
||||
marker.id = markerId
|
||||
marker.style.position = 'fixed'
|
||||
marker.style.left = `${pos.x - 10}px`
|
||||
marker.style.top = `${pos.y - 10}px`
|
||||
marker.style.width = '20px'
|
||||
marker.style.height = '20px'
|
||||
marker.style.border = '2px solid red'
|
||||
marker.style.borderRadius = '50%'
|
||||
marker.style.backgroundColor = 'rgba(255, 0, 0, 0.3)'
|
||||
marker.style.pointerEvents = 'none'
|
||||
marker.style.zIndex = '10000'
|
||||
document.body.appendChild(marker)
|
||||
},
|
||||
[position, id] as const
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove debug markers
|
||||
*/
|
||||
async debugRemoveMarkers(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
document
|
||||
.querySelectorAll('[id^="debug-marker"]')
|
||||
.forEach((el) => el.remove())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a screenshot and attach it to the test report for debugging
|
||||
* This is a convenience method that combines screenshot capture and test attachment
|
||||
*
|
||||
* @param testInfo The Playwright TestInfo object (from test parameters)
|
||||
* @param name Name for the attachment
|
||||
* @param options Optional screenshot options (defaults to page screenshot)
|
||||
*/
|
||||
async debugAttachScreenshot(
|
||||
testInfo: any,
|
||||
name: string,
|
||||
options?: {
|
||||
fullPage?: boolean
|
||||
element?: 'canvas' | 'page'
|
||||
markers?: Array<{ position: Position; id?: string }>
|
||||
}
|
||||
): Promise<void> {
|
||||
// Add markers if requested
|
||||
if (options?.markers) {
|
||||
for (const marker of options.markers) {
|
||||
await this.debugAddMarker(marker.position, marker.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Take screenshot - default to page if not specified
|
||||
let screenshot: Buffer
|
||||
const targetElement = options?.element || 'page'
|
||||
|
||||
if (targetElement === 'canvas') {
|
||||
screenshot = await this.canvas.screenshot()
|
||||
} else if (options?.fullPage) {
|
||||
screenshot = await this.page.screenshot({ fullPage: true })
|
||||
} else {
|
||||
screenshot = await this.page.screenshot()
|
||||
}
|
||||
|
||||
// Attach to test report
|
||||
await testInfo.attach(name, {
|
||||
body: screenshot,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
|
||||
// Clean up markers if we added any
|
||||
if (options?.markers) {
|
||||
await this.debugRemoveMarkers()
|
||||
}
|
||||
}
|
||||
|
||||
async doubleClickCanvas() {
|
||||
await this.page.mouse.dblclick(10, 10, { delay: 5 })
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture the canvas as a PNG and save it for debugging
|
||||
*/
|
||||
async debugSaveCanvasScreenshot(filename: string): Promise<void> {
|
||||
await this.page.evaluate(async (filename) => {
|
||||
const canvas = document.getElementById(
|
||||
'graph-canvas'
|
||||
) as HTMLCanvasElement
|
||||
if (!canvas) {
|
||||
throw new Error('Canvas not found')
|
||||
}
|
||||
|
||||
// Convert canvas to blob
|
||||
return new Promise<void>((resolve) => {
|
||||
canvas.toBlob(async (blob) => {
|
||||
if (!blob) {
|
||||
throw new Error('Failed to create blob from canvas')
|
||||
}
|
||||
|
||||
// Create a download link and trigger it
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
resolve()
|
||||
}, 'image/png')
|
||||
})
|
||||
}, filename)
|
||||
|
||||
// Wait a bit for the download to process
|
||||
await this.page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture canvas as base64 data URL for inspection
|
||||
*/
|
||||
async debugGetCanvasDataURL(): Promise<string> {
|
||||
return await this.page.evaluate(() => {
|
||||
const canvas = document.getElementById(
|
||||
'graph-canvas'
|
||||
) as HTMLCanvasElement
|
||||
if (!canvas) {
|
||||
throw new Error('Canvas not found')
|
||||
}
|
||||
return canvas.toDataURL('image/png')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an overlay div with the canvas image for easier Playwright screenshot
|
||||
*/
|
||||
async debugShowCanvasOverlay(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
const canvas = document.getElementById(
|
||||
'graph-canvas'
|
||||
) as HTMLCanvasElement
|
||||
if (!canvas) {
|
||||
throw new Error('Canvas not found')
|
||||
}
|
||||
|
||||
// Remove existing overlay if present
|
||||
const existingOverlay = document.getElementById('debug-canvas-overlay')
|
||||
if (existingOverlay) {
|
||||
existingOverlay.remove()
|
||||
}
|
||||
|
||||
// Create overlay div
|
||||
const overlay = document.createElement('div')
|
||||
overlay.id = 'debug-canvas-overlay'
|
||||
overlay.style.position = 'fixed'
|
||||
overlay.style.top = '0'
|
||||
overlay.style.left = '0'
|
||||
overlay.style.zIndex = '9999'
|
||||
overlay.style.backgroundColor = 'white'
|
||||
overlay.style.padding = '10px'
|
||||
overlay.style.border = '2px solid red'
|
||||
|
||||
// Create image from canvas
|
||||
const img = document.createElement('img')
|
||||
img.src = canvas.toDataURL('image/png')
|
||||
img.style.maxWidth = '800px'
|
||||
img.style.maxHeight = '600px'
|
||||
overlay.appendChild(img)
|
||||
|
||||
document.body.appendChild(overlay)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the debug canvas overlay
|
||||
*/
|
||||
async debugHideCanvasOverlay(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
const overlay = document.getElementById('debug-canvas-overlay')
|
||||
if (overlay) {
|
||||
overlay.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async clickEmptyLatentNode() {
|
||||
await this.canvas.click({
|
||||
position: {
|
||||
|
||||
@@ -12,128 +12,6 @@ export const getMiddlePoint = (pos1: Position, pos2: Position) => {
|
||||
}
|
||||
}
|
||||
|
||||
export class SubgraphSlotReference {
|
||||
constructor(
|
||||
readonly type: 'input' | 'output',
|
||||
readonly slotName: string,
|
||||
readonly comfyPage: ComfyPage
|
||||
) {}
|
||||
|
||||
async getPosition(): Promise<Position> {
|
||||
const pos: [number, number] = await this.comfyPage.page.evaluate(
|
||||
([type, slotName]) => {
|
||||
const currentGraph = window['app'].canvas.graph
|
||||
|
||||
// Check if we're in a subgraph
|
||||
if (currentGraph.constructor.name !== 'Subgraph') {
|
||||
throw new Error(
|
||||
'Not in a subgraph - this method only works inside subgraphs'
|
||||
)
|
||||
}
|
||||
|
||||
const slots =
|
||||
type === 'input' ? currentGraph.inputs : currentGraph.outputs
|
||||
if (!slots || slots.length === 0) {
|
||||
throw new Error(`No ${type} slots found in subgraph`)
|
||||
}
|
||||
|
||||
// Find the specific slot or use the first one if no name specified
|
||||
const slot = slotName
|
||||
? slots.find((s) => s.name === slotName)
|
||||
: slots[0]
|
||||
|
||||
if (!slot) {
|
||||
throw new Error(`${type} slot '${slotName}' not found`)
|
||||
}
|
||||
|
||||
if (!slot.pos) {
|
||||
throw new Error(`${type} slot '${slotName}' has no position`)
|
||||
}
|
||||
|
||||
// Convert from offset to canvas coordinates
|
||||
const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
|
||||
slot.pos[0],
|
||||
slot.pos[1]
|
||||
])
|
||||
return canvasPos
|
||||
},
|
||||
[this.type, this.slotName] as const
|
||||
)
|
||||
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
|
||||
async getOpenSlotPosition(): Promise<Position> {
|
||||
const pos: [number, number] = await this.comfyPage.page.evaluate(
|
||||
([type]) => {
|
||||
const currentGraph = window['app'].canvas.graph
|
||||
|
||||
if (currentGraph.constructor.name !== 'Subgraph') {
|
||||
throw new Error(
|
||||
'Not in a subgraph - this method only works inside subgraphs'
|
||||
)
|
||||
}
|
||||
|
||||
const node =
|
||||
type === 'input' ? currentGraph.inputNode : currentGraph.outputNode
|
||||
const slots =
|
||||
type === 'input' ? currentGraph.inputs : currentGraph.outputs
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`No ${type} node found in subgraph`)
|
||||
}
|
||||
|
||||
// Calculate position for next available slot
|
||||
// const nextSlotIndex = slots?.length || 0
|
||||
// const slotHeight = 20
|
||||
// const slotY = node.pos[1] + 30 + nextSlotIndex * slotHeight
|
||||
|
||||
// Find last slot position
|
||||
const lastSlot = slots.at(-1)
|
||||
let slotX: number
|
||||
let slotY: number
|
||||
|
||||
if (lastSlot) {
|
||||
// If there are existing slots, position the new one below the last one
|
||||
const gapHeight = 20
|
||||
slotX = lastSlot.pos[0]
|
||||
slotY = lastSlot.pos[1] + gapHeight
|
||||
} else {
|
||||
// No existing slots - use slotAnchorX if available, otherwise calculate from node position
|
||||
if (currentGraph.slotAnchorX !== undefined) {
|
||||
// The actual slot X position seems to be slotAnchorX - 10
|
||||
slotX = currentGraph.slotAnchorX - 10
|
||||
} else {
|
||||
// Fallback: calculate from node edge
|
||||
slotX =
|
||||
type === 'input'
|
||||
? node.pos[0] + node.size[0] - 10 // Right edge for input node
|
||||
: node.pos[0] + 10 // Left edge for output node
|
||||
}
|
||||
// For Y position when no slots exist, use middle of node
|
||||
slotY = node.pos[1] + node.size[1] / 2
|
||||
}
|
||||
|
||||
// Convert from offset to canvas coordinates
|
||||
const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
|
||||
slotX,
|
||||
slotY
|
||||
])
|
||||
return canvasPos
|
||||
},
|
||||
[this.type] as const
|
||||
)
|
||||
|
||||
return {
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeSlotReference {
|
||||
constructor(
|
||||
readonly type: 'input' | 'output',
|
||||
@@ -143,27 +21,11 @@ export class NodeSlotReference {
|
||||
async getPosition() {
|
||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
// Use canvas.graph to get the current graph (works in both main graph and subgraphs)
|
||||
const node = window['app'].canvas.graph.getNodeById(id)
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
|
||||
const rawPos = node.getConnectionPos(type === 'input', index)
|
||||
const convertedPos =
|
||||
window['app'].canvas.ds.convertOffsetToCanvas(rawPos)
|
||||
|
||||
// Debug logging - convert Float32Arrays to regular arrays for visibility
|
||||
console.log(
|
||||
`NodeSlotReference debug for ${type} slot ${index} on node ${id}:`,
|
||||
{
|
||||
nodePos: [node.pos[0], node.pos[1]],
|
||||
nodeSize: [node.size[0], node.size[1]],
|
||||
rawConnectionPos: [rawPos[0], rawPos[1]],
|
||||
convertedPos: [convertedPos[0], convertedPos[1]],
|
||||
currentGraphType: window['app'].canvas.graph.constructor.name
|
||||
}
|
||||
return window['app'].canvas.ds.convertOffsetToCanvas(
|
||||
node.getConnectionPos(type === 'input', index)
|
||||
)
|
||||
|
||||
return convertedPos
|
||||
},
|
||||
[this.type, this.node.id, this.index] as const
|
||||
)
|
||||
@@ -175,7 +37,7 @@ export class NodeSlotReference {
|
||||
async getLinkCount() {
|
||||
return await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
const node = window['app'].canvas.graph.getNodeById(id)
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
if (type === 'input') {
|
||||
return node.inputs[index].link == null ? 0 : 1
|
||||
@@ -188,7 +50,7 @@ export class NodeSlotReference {
|
||||
async removeLinks() {
|
||||
await this.node.comfyPage.page.evaluate(
|
||||
([type, id, index]) => {
|
||||
const node = window['app'].canvas.graph.getNodeById(id)
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
if (type === 'input') {
|
||||
node.disconnectInput(index)
|
||||
@@ -213,7 +75,7 @@ export class NodeWidgetReference {
|
||||
async getPosition(): Promise<Position> {
|
||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||
([id, index]) => {
|
||||
const node = window['app'].canvas.graph.getNodeById(id)
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found.`)
|
||||
const widget = node.widgets[index]
|
||||
if (!widget) throw new Error(`Widget ${index} not found.`)
|
||||
@@ -272,7 +134,7 @@ export class NodeWidgetReference {
|
||||
const pos = await this.getPosition()
|
||||
const canvas = this.node.comfyPage.canvas
|
||||
const canvasPos = (await canvas.boundingBox())!
|
||||
await this.node.comfyPage.dragAndDrop(
|
||||
this.node.comfyPage.dragAndDrop(
|
||||
{
|
||||
x: canvasPos.x + pos.x,
|
||||
y: canvasPos.y + pos.y
|
||||
@@ -304,7 +166,7 @@ export class NodeReference {
|
||||
) {}
|
||||
async exists(): Promise<boolean> {
|
||||
return await this.comfyPage.page.evaluate((id) => {
|
||||
const node = window['app'].canvas.graph.getNodeById(id)
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return !!node
|
||||
}, this.id)
|
||||
}
|
||||
@@ -323,7 +185,7 @@ export class NodeReference {
|
||||
async getBounding(): Promise<Position & Size> {
|
||||
const [x, y, width, height]: [number, number, number, number] =
|
||||
await this.comfyPage.page.evaluate((id) => {
|
||||
const node = window['app'].canvas.graph.getNodeById(id)
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error('Node not found')
|
||||
return node.getBounding()
|
||||
}, this.id)
|
||||
@@ -356,7 +218,7 @@ export class NodeReference {
|
||||
async getProperty<T>(prop: string): Promise<T> {
|
||||
return await this.comfyPage.page.evaluate(
|
||||
([id, prop]) => {
|
||||
const node = window['app'].canvas.graph.getNodeById(id)
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
if (!node) throw new Error('Node not found')
|
||||
return node[prop]
|
||||
},
|
||||
@@ -397,8 +259,7 @@ export class NodeReference {
|
||||
|
||||
await this.comfyPage.canvas.click({
|
||||
...options,
|
||||
position: clickPos,
|
||||
force: true
|
||||
position: clickPos
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
if (moveMouseToEmptyArea) {
|
||||
@@ -458,18 +319,6 @@ export class NodeReference {
|
||||
}
|
||||
return nodes[0]
|
||||
}
|
||||
async convertToSubgraph() {
|
||||
await this.clickContextMenuOption('Convert to Subgraph')
|
||||
await this.comfyPage.nextFrame()
|
||||
await this.comfyPage.page.waitForTimeout(256)
|
||||
const nodes = await this.comfyPage.getNodeRefsByTitle('New Subgraph')
|
||||
if (nodes.length !== 1) {
|
||||
throw new Error(
|
||||
`Did not find single subgraph node (found=${nodes.length})`
|
||||
)
|
||||
}
|
||||
return nodes[0]
|
||||
}
|
||||
async manageGroupNode() {
|
||||
await this.clickContextMenuOption('Manage Group Node')
|
||||
await this.comfyPage.nextFrame()
|
||||
@@ -478,58 +327,4 @@ export class NodeReference {
|
||||
this.comfyPage.page.locator('.comfy-group-manage')
|
||||
)
|
||||
}
|
||||
async navigateIntoSubgraph() {
|
||||
const titleHeight = await this.comfyPage.page.evaluate(() => {
|
||||
return window['LiteGraph']['NODE_TITLE_HEIGHT']
|
||||
})
|
||||
const nodePos = await this.getPosition()
|
||||
const nodeSize = await this.getSize()
|
||||
|
||||
// Try multiple positions to avoid DOM widget interference
|
||||
const clickPositions = [
|
||||
{ x: nodePos.x + nodeSize.width / 2, y: nodePos.y + titleHeight + 5 },
|
||||
{ x: nodePos.x + nodeSize.width / 2, y: nodePos.y + nodeSize.height / 2 },
|
||||
{ x: nodePos.x + 20, y: nodePos.y + titleHeight + 5 }
|
||||
]
|
||||
|
||||
let isInSubgraph = false
|
||||
let attempts = 0
|
||||
const maxAttempts = 3
|
||||
|
||||
while (!isInSubgraph && attempts < maxAttempts) {
|
||||
attempts++
|
||||
|
||||
for (const position of clickPositions) {
|
||||
// Clear any selection first
|
||||
await this.comfyPage.canvas.click({
|
||||
position: { x: 50, y: 50 },
|
||||
force: true
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
// Double-click to enter subgraph
|
||||
await this.comfyPage.canvas.dblclick({ position, force: true })
|
||||
await this.comfyPage.nextFrame()
|
||||
await this.comfyPage.page.waitForTimeout(500)
|
||||
|
||||
// Check if we successfully entered the subgraph
|
||||
isInSubgraph = await this.comfyPage.page.evaluate(() => {
|
||||
const graph = window['app'].canvas.graph
|
||||
return graph?.constructor?.name === 'Subgraph'
|
||||
})
|
||||
|
||||
if (isInSubgraph) break
|
||||
}
|
||||
|
||||
if (!isInSubgraph && attempts < maxAttempts) {
|
||||
await this.comfyPage.page.waitForTimeout(500)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInSubgraph) {
|
||||
throw new Error(
|
||||
'Failed to navigate into subgraph after ' + attempts + ' attempts'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,21 +27,6 @@ test.describe('Node search box', () => {
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('New user (1.24.1+) gets search box by default on link release', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Start fresh to test new user behavior
|
||||
await comfyPage.setup({ clearStorage: true })
|
||||
// Simulate new user with 1.24.1+ installed version
|
||||
await comfyPage.setSetting('Comfy.InstalledVersion', '1.24.1')
|
||||
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
// Don't set LinkRelease settings explicitly to test versioned defaults
|
||||
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
await expect(comfyPage.searchBox.input).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can add node', async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
@@ -187,10 +172,10 @@ test.describe('Node search box', () => {
|
||||
await comfyPage.page.mouse.click(panelBounds!.x - 10, panelBounds!.y - 10)
|
||||
|
||||
// Verify the filter selection panel is hidden
|
||||
await expect(panel.header).not.toBeVisible()
|
||||
expect(panel.header).not.toBeVisible()
|
||||
|
||||
// Verify the node search dialog is still visible
|
||||
await expect(comfyPage.searchBox.input).toBeVisible()
|
||||
expect(comfyPage.searchBox.input).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can add multiple filters', async ({ comfyPage }) => {
|
||||
@@ -279,38 +264,4 @@ test.describe('Release context menu', () => {
|
||||
'link-context-menu-search.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Existing user (pre-1.24.1) gets context menu by default on link release', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Start fresh to test existing user behavior
|
||||
await comfyPage.setup({ clearStorage: true })
|
||||
// Simulate existing user with pre-1.24.1 version
|
||||
await comfyPage.setSetting('Comfy.InstalledVersion', '1.23.0')
|
||||
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
// Don't set LinkRelease settings explicitly to test versioned defaults
|
||||
|
||||
await comfyPage.disconnectEdge()
|
||||
// Context menu should appear, search box should not
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(0)
|
||||
const contextMenu = comfyPage.page.locator('.litecontextmenu')
|
||||
await expect(contextMenu).toBeVisible()
|
||||
})
|
||||
|
||||
test('Explicit setting overrides versioned defaults', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Start fresh and simulate new user who should get search box by default
|
||||
await comfyPage.setup({ clearStorage: true })
|
||||
await comfyPage.setSetting('Comfy.InstalledVersion', '1.24.1')
|
||||
// But explicitly set to context menu (overriding versioned default)
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu')
|
||||
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
|
||||
await comfyPage.disconnectEdge()
|
||||
// Context menu should appear due to explicit setting, not search box
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(0)
|
||||
const contextMenu = comfyPage.page.locator('.litecontextmenu')
|
||||
await expect(contextMenu).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
// Constants
|
||||
const RENAMED_INPUT_NAME = 'renamed_input'
|
||||
const NEW_SUBGRAPH_TITLE = 'New Subgraph'
|
||||
const UPDATED_SUBGRAPH_TITLE = 'Updated Subgraph Title'
|
||||
const TEST_WIDGET_CONTENT = 'Test content that should persist'
|
||||
|
||||
// Common selectors
|
||||
const SELECTORS = {
|
||||
breadcrumb: '.subgraph-breadcrumb',
|
||||
promptDialog: '.graphdialog input',
|
||||
nodeSearchContainer: '.node-search-container',
|
||||
domWidget: '.comfy-multiline-input'
|
||||
} as const
|
||||
|
||||
test.describe('Subgraph Operations', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
// Helper to get subgraph slot count
|
||||
async function getSubgraphSlotCount(
|
||||
comfyPage: typeof test.prototype.comfyPage,
|
||||
type: 'inputs' | 'outputs'
|
||||
): Promise<number> {
|
||||
return await comfyPage.page.evaluate((slotType) => {
|
||||
return window['app'].canvas.graph[slotType]?.length || 0
|
||||
}, type)
|
||||
}
|
||||
|
||||
// Helper to get current graph node count
|
||||
async function getGraphNodeCount(
|
||||
comfyPage: typeof test.prototype.comfyPage
|
||||
): Promise<number> {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
return window['app'].canvas.graph.nodes?.length || 0
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to verify we're in a subgraph
|
||||
async function isInSubgraph(
|
||||
comfyPage: typeof test.prototype.comfyPage
|
||||
): Promise<boolean> {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
const graph = window['app'].canvas.graph
|
||||
return graph?.constructor?.name === 'Subgraph'
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('I/O Slot Management', () => {
|
||||
test('Can add input slots to subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs')
|
||||
const vaeEncodeNode = await comfyPage.getNodeRefById('2')
|
||||
|
||||
await comfyPage.connectFromSubgraphInput(vaeEncodeNode, 0)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const finalCount = await getSubgraphSlotCount(comfyPage, 'inputs')
|
||||
expect(finalCount).toBe(initialCount + 1)
|
||||
})
|
||||
|
||||
test('Can add output slots to subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs')
|
||||
const vaeEncodeNode = await comfyPage.getNodeRefById('2')
|
||||
|
||||
await comfyPage.connectToSubgraphOutput(vaeEncodeNode, 0)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const finalCount = await getSubgraphSlotCount(comfyPage, 'outputs')
|
||||
expect(finalCount).toBe(initialCount + 1)
|
||||
})
|
||||
|
||||
test('Can remove input slots from subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs')
|
||||
expect(initialCount).toBeGreaterThan(0)
|
||||
|
||||
await comfyPage.rightClickSubgraphInputSlot()
|
||||
await comfyPage.clickLitegraphContextMenuItem('Remove Slot')
|
||||
|
||||
// Force re-render
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const finalCount = await getSubgraphSlotCount(comfyPage, 'inputs')
|
||||
expect(finalCount).toBe(initialCount - 1)
|
||||
})
|
||||
|
||||
test('Can remove output slots from subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs')
|
||||
expect(initialCount).toBeGreaterThan(0)
|
||||
|
||||
await comfyPage.rightClickSubgraphOutputSlot()
|
||||
await comfyPage.clickLitegraphContextMenuItem('Remove Slot')
|
||||
|
||||
// Force re-render
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const finalCount = await getSubgraphSlotCount(comfyPage, 'outputs')
|
||||
expect(finalCount).toBe(initialCount - 1)
|
||||
})
|
||||
|
||||
test('Can rename I/O slots', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialInputLabel = await comfyPage.page.evaluate(() => {
|
||||
const graph = window['app'].canvas.graph
|
||||
return graph.inputs?.[0]?.label || null
|
||||
})
|
||||
|
||||
await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
|
||||
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
|
||||
|
||||
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||
state: 'visible'
|
||||
})
|
||||
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_INPUT_NAME)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Force re-render
|
||||
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newInputName = await comfyPage.page.evaluate(() => {
|
||||
const graph = window['app'].canvas.graph
|
||||
return graph.inputs?.[0]?.label || null
|
||||
})
|
||||
|
||||
expect(newInputName).toBe(RENAMED_INPUT_NAME)
|
||||
expect(newInputName).not.toBe(initialInputLabel)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Subgraph Creation and Deletion', () => {
|
||||
test('Can create subgraph from selected nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
|
||||
const initialNodeCount = await getGraphNodeCount(comfyPage)
|
||||
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const node = await comfyPage.getNodeRefById('5')
|
||||
await node.convertToSubgraph()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const subgraphNodes =
|
||||
await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE)
|
||||
expect(subgraphNodes.length).toBe(1)
|
||||
|
||||
const finalNodeCount = await getGraphNodeCount(comfyPage)
|
||||
expect(finalNodeCount).toBe(1)
|
||||
})
|
||||
|
||||
test('Can delete subgraph node', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
expect(await subgraphNode.exists()).toBe(true)
|
||||
|
||||
const initialNodeCount = await getGraphNodeCount(comfyPage)
|
||||
|
||||
await subgraphNode.click('title')
|
||||
await comfyPage.page.keyboard.press('Delete')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const finalNodeCount = await getGraphNodeCount(comfyPage)
|
||||
expect(finalNodeCount).toBe(initialNodeCount - 1)
|
||||
|
||||
const deletedNode = await comfyPage.getNodeRefById('2')
|
||||
expect(await deletedNode.exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Operations Inside Subgraphs', () => {
|
||||
test('Can copy and paste nodes in subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialNodeCount = await getGraphNodeCount(comfyPage)
|
||||
|
||||
const nodesInSubgraph = await comfyPage.page.evaluate(() => {
|
||||
const nodes = window['app'].canvas.graph.nodes
|
||||
return nodes?.[0]?.id || null
|
||||
})
|
||||
|
||||
expect(nodesInSubgraph).not.toBeNull()
|
||||
|
||||
const nodeToClone = await comfyPage.getNodeRefById(
|
||||
String(nodesInSubgraph)
|
||||
)
|
||||
await nodeToClone.click('title')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+c')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+v')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const finalNodeCount = await getGraphNodeCount(comfyPage)
|
||||
expect(finalNodeCount).toBe(initialNodeCount + 1)
|
||||
})
|
||||
|
||||
test('Can undo and redo operations in subgraph', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
// Add a node
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Note')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Get initial node count
|
||||
const initialCount = await getGraphNodeCount(comfyPage)
|
||||
|
||||
// Undo
|
||||
await comfyPage.ctrlZ()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const afterUndoCount = await getGraphNodeCount(comfyPage)
|
||||
expect(afterUndoCount).toBe(initialCount - 1)
|
||||
|
||||
// Redo
|
||||
await comfyPage.ctrlY()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const afterRedoCount = await getGraphNodeCount(comfyPage)
|
||||
expect(afterRedoCount).toBe(initialCount)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Subgraph Navigation and UI', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('Breadcrumb updates when subgraph node title is changed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('nested-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('10')
|
||||
const nodePos = await subgraphNode.getPosition()
|
||||
const nodeSize = await subgraphNode.getSize()
|
||||
|
||||
// Navigate into subgraph
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb, {
|
||||
state: 'visible',
|
||||
timeout: 20000
|
||||
})
|
||||
|
||||
const breadcrumb = comfyPage.page.locator(SELECTORS.breadcrumb)
|
||||
const initialBreadcrumbText = await breadcrumb.textContent()
|
||||
|
||||
// Go back and edit title
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y - 10
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
|
||||
await expect(comfyPage.page.locator('.node-title-editor')).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.press('Control+a')
|
||||
await comfyPage.page.keyboard.type(UPDATED_SUBGRAPH_TITLE)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back into subgraph
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
|
||||
|
||||
const updatedBreadcrumbText = await breadcrumb.textContent()
|
||||
expect(updatedBreadcrumbText).toContain(UPDATED_SUBGRAPH_TITLE)
|
||||
expect(updatedBreadcrumbText).not.toBe(initialBreadcrumbText)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('DOM Widget Promotion', () => {
|
||||
test('DOM widget visibility persists through subgraph navigation', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify promoted widget is visible in parent graph
|
||||
const parentTextarea = comfyPage.page.locator(SELECTORS.domWidget)
|
||||
await expect(parentTextarea).toBeVisible()
|
||||
await expect(parentTextarea).toHaveCount(1)
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('11')
|
||||
expect(await subgraphNode.exists()).toBe(true)
|
||||
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
// Verify widget is visible in subgraph
|
||||
const subgraphTextarea = comfyPage.page.locator(SELECTORS.domWidget)
|
||||
await expect(subgraphTextarea).toBeVisible()
|
||||
await expect(subgraphTextarea).toHaveCount(1)
|
||||
|
||||
// Navigate back
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify widget is still visible
|
||||
const backToParentTextarea = comfyPage.page.locator(SELECTORS.domWidget)
|
||||
await expect(backToParentTextarea).toBeVisible()
|
||||
await expect(backToParentTextarea).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('DOM widget content is preserved through navigation', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
|
||||
|
||||
const textarea = comfyPage.page.locator(SELECTORS.domWidget)
|
||||
await textarea.fill(TEST_WIDGET_CONTENT)
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('11')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const subgraphTextarea = comfyPage.page.locator(SELECTORS.domWidget)
|
||||
await expect(subgraphTextarea).toHaveValue(TEST_WIDGET_CONTENT)
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const parentTextarea = comfyPage.page.locator(SELECTORS.domWidget)
|
||||
await expect(parentTextarea).toHaveValue(TEST_WIDGET_CONTENT)
|
||||
})
|
||||
|
||||
test('DOM elements are cleaned up when subgraph node is removed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
|
||||
|
||||
const initialCount = await comfyPage.page
|
||||
.locator(SELECTORS.domWidget)
|
||||
.count()
|
||||
expect(initialCount).toBe(1)
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('11')
|
||||
|
||||
await subgraphNode.click('title')
|
||||
await comfyPage.page.keyboard.press('Delete')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const finalCount = await comfyPage.page
|
||||
.locator(SELECTORS.domWidget)
|
||||
.count()
|
||||
expect(finalCount).toBe(0)
|
||||
})
|
||||
|
||||
test('DOM elements are cleaned up when widget is disconnected from I/O', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Enable new menu for breadcrumb navigation
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
|
||||
|
||||
const textareaCount = await comfyPage.page
|
||||
.locator(SELECTORS.domWidget)
|
||||
.count()
|
||||
expect(textareaCount).toBe(1)
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('11')
|
||||
|
||||
// Navigate into subgraph (method now handles retries internally)
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
await comfyPage.rightClickSubgraphInputSlot('text')
|
||||
await comfyPage.clickLitegraphContextMenuItem('Remove Slot')
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
// Wait for breadcrumb to be visible
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb, {
|
||||
state: 'visible',
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
// Click breadcrumb to navigate back to parent graph
|
||||
const breadcrumb = comfyPage.page.locator(SELECTORS.breadcrumb).first()
|
||||
await breadcrumb.click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(300)
|
||||
|
||||
// Check that the subgraph node has no widgets after removing the text slot
|
||||
const widgetCount = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].canvas.graph.nodes[0].widgets?.length || 0
|
||||
})
|
||||
|
||||
expect(widgetCount).toBe(0)
|
||||
})
|
||||
|
||||
test('Multiple promoted widgets are handled correctly', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('subgraph-with-multiple-promoted-widgets')
|
||||
|
||||
const parentCount = await comfyPage.page
|
||||
.locator(SELECTORS.domWidget)
|
||||
.count()
|
||||
expect(parentCount).toBeGreaterThan(1)
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('11')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const subgraphCount = await comfyPage.page
|
||||
.locator(SELECTORS.domWidget)
|
||||
.count()
|
||||
expect(subgraphCount).toBe(parentCount)
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const finalCount = await comfyPage.page
|
||||
.locator(SELECTORS.domWidget)
|
||||
.count()
|
||||
expect(finalCount).toBe(parentCount)
|
||||
})
|
||||
})
|
||||
})
|
||||
82
browser_tests/tests/subgraphBreadcrumb.spec.ts
Normal file
82
browser_tests/tests/subgraphBreadcrumb.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Subgraph Breadcrumb Title Sync', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('Breadcrumb updates when subgraph node title is changed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Load a workflow with subgraphs
|
||||
await comfyPage.loadWorkflow('nested-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Get the subgraph node by ID (node 10 is the subgraph)
|
||||
const subgraphNode = await comfyPage.getNodeRefById('10')
|
||||
|
||||
// Get node position and double-click on it to enter the subgraph
|
||||
const nodePos = await subgraphNode.getPosition()
|
||||
const nodeSize = await subgraphNode.getSize()
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y + nodeSize.height / 2 + 10
|
||||
}
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for breadcrumb to appear
|
||||
await comfyPage.page.waitForSelector('.subgraph-breadcrumb', {
|
||||
state: 'visible',
|
||||
timeout: 20000
|
||||
})
|
||||
|
||||
// Get initial breadcrumb text
|
||||
const breadcrumb = comfyPage.page.locator('.subgraph-breadcrumb')
|
||||
const initialBreadcrumbText = await breadcrumb.textContent()
|
||||
|
||||
// Go back to main graph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
// Double-click on the title area of the subgraph node to edit
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y - 10 // Title area is above the node body
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
|
||||
// Wait for title editor to appear
|
||||
await expect(comfyPage.page.locator('.node-title-editor')).toBeVisible()
|
||||
|
||||
// Clear existing text and type new title
|
||||
await comfyPage.page.keyboard.press('Control+a')
|
||||
const newTitle = 'Updated Subgraph Title'
|
||||
await comfyPage.page.keyboard.type(newTitle)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Wait a frame for the update to complete
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Enter the subgraph again
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y + nodeSize.height / 2
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
|
||||
// Wait for breadcrumb
|
||||
await comfyPage.page.waitForSelector('.subgraph-breadcrumb')
|
||||
|
||||
// Check that breadcrumb now shows the new title
|
||||
const updatedBreadcrumbText = await breadcrumb.textContent()
|
||||
expect(updatedBreadcrumbText).toContain(newTitle)
|
||||
expect(updatedBreadcrumbText).not.toBe(initialBreadcrumbText)
|
||||
})
|
||||
})
|
||||
@@ -48,7 +48,7 @@ test.describe('Combo text widget', () => {
|
||||
await comfyPage.page.keyboard.press('r')
|
||||
|
||||
// Wait for nodes' widgets to be updated
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const refreshedComboValues = await getComboValues()
|
||||
expect(refreshedComboValues).not.toEqual(initialComboValues)
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.24.2",
|
||||
"version": "1.24.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.24.2",
|
||||
"version": "1.24.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.16.16",
|
||||
"@comfyorg/litegraph": "^0.16.9",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -949,9 +949,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.16.16",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.16.tgz",
|
||||
"integrity": "sha512-6kfTpu6Z7gF0OxSypUsGM7Ggq7JYP/4dhHGhzA3p+C8tak/2dMuQDiOtc5YXRlcYNEk79A/xekpojGC/DtO6iw==",
|
||||
"version": "0.16.9",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.9.tgz",
|
||||
"integrity": "sha512-ZsvqkLqdG65e2UyM8oTOUTv/7VFEyGbG/C9dCZnhxdNq30UaE+F0iLaKq/17u6w4yewyZuqIn5MoOtjpxPqLDQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.24.2",
|
||||
"version": "1.24.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -77,7 +77,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.16.16",
|
||||
"@comfyorg/litegraph": "^0.16.9",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
130
src/components/common/DotSpinner.vue
Normal file
130
src/components/common/DotSpinner.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center justify-center"
|
||||
:style="{ width: size + 'px', height: size + 'px' }"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
class="animate-spin"
|
||||
:style="{ animationDuration: duration }"
|
||||
>
|
||||
<g clip-path="url(#clip0_776_9582)">
|
||||
<!-- Top dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7 2.21053C7.61042 2.21053 8.10526 1.71568 8.10526 1.10526C8.10526 0.494843 7.61042 0 7 0C6.38958 0 5.89474 0.494843 5.89474 1.10526C5.89474 1.71568 6.38958 2.21053 7 2.21053Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.25s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.21053 7C2.21053 7.61042 1.71568 8.10526 1.10526 8.10526C0.494843 8.10526 0 7.61042 0 7C0 6.38958 0.494843 5.89474 1.10526 5.89474C1.71568 5.89474 2.21053 6.38958 2.21053 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.5s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 7C14 7.61042 13.5052 8.10526 12.8947 8.10526C12.2843 8.10526 11.7895 7.61042 11.7895 7C11.7895 6.38958 12.2843 5.89474 12.8947 5.89474C13.5052 5.89474 14 6.38958 14 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.75s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.10526 12.8947C8.10526 13.5052 7.61041 14 6.99999 14C6.38957 14 5.89473 13.5052 5.89473 12.8947C5.89473 12.2843 6.38957 11.7895 6.99999 11.7895C7.61041 11.7895 8.10526 12.2843 8.10526 12.8947Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.125s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 3.61349C2.48203 4.04513 3.18184 4.04513 3.61347 3.61349C4.0451 3.18186 4.0451 2.48205 3.61347 2.05042C3.18184 1.61878 2.48203 1.61878 2.05039 2.05042C1.61876 2.48205 1.61876 3.18186 2.05039 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.625s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 11.9496C11.518 12.3812 10.8182 12.3812 10.3865 11.9496C9.9549 11.5179 9.9549 10.8181 10.3865 10.3865C10.8182 9.95485 11.518 9.95485 11.9496 10.3865C12.3812 10.8181 12.3812 11.5179 11.9496 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.875s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 11.9496C2.48203 12.3812 3.18184 12.3812 3.61347 11.9496C4.0451 11.5179 4.0451 10.8181 3.61347 10.3865C3.18184 9.95485 2.48203 9.95485 2.05039 10.3865C1.61876 10.8181 1.61876 11.5179 2.05039 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.375s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 3.61349C11.518 4.04513 10.8182 4.04513 10.3865 3.61349C9.9549 3.18186 9.9549 2.48205 10.3865 2.05042C10.8182 1.61878 11.518 1.61878 11.9496 2.05042C12.3812 2.48205 12.3812 3.18186 11.9496 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_776_9582">
|
||||
<rect width="14" height="14" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const { size = 24, duration = '2s' } = defineProps<{
|
||||
size?: number
|
||||
duration?: string
|
||||
}>()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const color = computed(() =>
|
||||
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dot-animation {
|
||||
animation: dot-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</ListBox>
|
||||
<div v-if="isManagerInstalled" class="flex justify-end py-3">
|
||||
<div v-if="!isLegacyManager" class="flex justify-end py-3">
|
||||
<PackInstallButton
|
||||
:disabled="isLoading || !!error || missingNodePacks.length === 0"
|
||||
:node-packs="missingNodePacks"
|
||||
@@ -45,14 +45,12 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -60,22 +58,11 @@ const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error, missingCoreNodes } =
|
||||
useMissingNodes()
|
||||
|
||||
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
||||
// This allows us to conditionally show the Manager button only when the extension is available
|
||||
// TODO: Remove this check when Manager functionality is fully migrated into core
|
||||
const isManagerInstalled = computed(() => {
|
||||
return aboutPanelStore.badges.some(
|
||||
(badge) =>
|
||||
badge.label.includes('ComfyUI-Manager') ||
|
||||
badge.url.includes('ComfyUI-Manager')
|
||||
)
|
||||
})
|
||||
const isLegacyManager = ref(false)
|
||||
|
||||
const uniqueNodes = computed(() => {
|
||||
const seenTypes = new Set()
|
||||
@@ -103,6 +90,13 @@ const openManager = () => {
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const isLegacyResponse = await useComfyManagerService().isLegacyManagerUI()
|
||||
if (isLegacyResponse?.is_legacy_manager_ui) {
|
||||
isLegacyManager.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -30,11 +30,20 @@ const defaultMockTaskLogs = [
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
taskLogs: [...defaultMockTaskLogs]
|
||||
taskLogs: [...defaultMockTaskLogs],
|
||||
succeededTasksLogs: [...defaultMockTaskLogs],
|
||||
failedTasksLogs: [...defaultMockTaskLogs],
|
||||
managerQueue: { historyCount: 2 },
|
||||
isLoading: false
|
||||
})),
|
||||
useManagerProgressDialogStore: vi.fn(() => ({
|
||||
isExpanded: true,
|
||||
collapse: mockCollapse
|
||||
activeTabIndex: 0,
|
||||
getActiveTabIndex: vi.fn(() => 0),
|
||||
setActiveTabIndex: vi.fn(),
|
||||
toggle: vi.fn(),
|
||||
collapse: mockCollapse,
|
||||
expand: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
'max-h-0': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div v-for="(panel, index) in taskPanels" :key="index">
|
||||
<div v-for="(log, index) in focusedLogs" :key="index">
|
||||
<Panel
|
||||
:expanded="collapsedPanels[index] || false"
|
||||
toggleable
|
||||
@@ -27,7 +27,7 @@
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full py-2">
|
||||
<div class="flex flex-col text-sm font-medium leading-normal">
|
||||
<span>{{ panel.taskName }}</span>
|
||||
<span>{{ log.taskName }}</span>
|
||||
<span class="text-muted">
|
||||
{{
|
||||
isInProgress(index)
|
||||
@@ -52,24 +52,24 @@
|
||||
</template>
|
||||
<div
|
||||
:ref="
|
||||
index === taskPanels.length - 1
|
||||
index === focusedLogs.length - 1
|
||||
? (el) => (lastPanelRef = el as HTMLElement)
|
||||
: undefined
|
||||
"
|
||||
class="overflow-y-auto h-64 rounded-lg bg-black"
|
||||
:class="{
|
||||
'h-64': index !== taskPanels.length - 1,
|
||||
'flex-grow': index === taskPanels.length - 1
|
||||
'h-64': index !== focusedLogs.length - 1,
|
||||
'flex-grow': index === focusedLogs.length - 1
|
||||
}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div class="h-full">
|
||||
<div
|
||||
v-for="(log, logIndex) in panel.logs"
|
||||
v-for="(logLine, logIndex) in log.logs"
|
||||
:key="logIndex"
|
||||
class="text-neutral-400 dark-theme:text-muted"
|
||||
>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ log }}</pre>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ logLine }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,17 +90,23 @@ import {
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const { taskLogs } = useComfyManagerStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const isInProgress = (index: number) =>
|
||||
index === taskPanels.value.length - 1 && managerStore.uncompletedCount > 0
|
||||
index === comfyManagerStore.managerQueue.historyCount - 1 &&
|
||||
comfyManagerStore.isLoading
|
||||
|
||||
const taskPanels = computed(() => taskLogs)
|
||||
const isExpanded = computed(() => progressDialogContent.isExpanded)
|
||||
const isCollapsed = computed(() => !isExpanded.value)
|
||||
|
||||
const focusedLogs = computed(() => {
|
||||
if (progressDialogContent.getActiveTabIndex() === 0) {
|
||||
return comfyManagerStore.succeededTasksLogs
|
||||
}
|
||||
return comfyManagerStore.failedTasksLogs
|
||||
})
|
||||
|
||||
const collapsedPanels = ref<Record<number, boolean>>({})
|
||||
const togglePanel = (index: number) => {
|
||||
collapsedPanels.value[index] = !collapsedPanels.value[index]
|
||||
@@ -115,7 +121,7 @@ const { y: scrollY } = useScroll(sectionsContainerRef, {
|
||||
|
||||
const lastPanelRef = ref<HTMLElement | null>(null)
|
||||
const isUserScrolling = ref(false)
|
||||
const lastPanelLogs = computed(() => taskPanels.value?.at(-1)?.logs)
|
||||
const lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)
|
||||
|
||||
const isAtBottom = (el: HTMLElement | null) => {
|
||||
if (!el) return false
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
:suggestions="suggestions"
|
||||
:is-missing-tab="isMissingTab"
|
||||
:sort-options="sortOptions"
|
||||
:is-update-available-tab="isUpdateAvailableTab"
|
||||
/>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
<h2 class="text-lg font-normal text-left">
|
||||
{{ $t('manager.discoverCommunityContent') }}
|
||||
</h2>
|
||||
<Tag
|
||||
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
:value="$t('manager.legacyManagerUI')"
|
||||
class="cursor-help"
|
||||
:pt="{
|
||||
root: { class: 'text-xs' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionBadge from './PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
@@ -120,7 +119,7 @@ describe('PackVersionBadge', () => {
|
||||
|
||||
const badge = wrapper.find('[role="button"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(badge.find('span').text()).toBe('nightly')
|
||||
})
|
||||
|
||||
it('falls back to NIGHTLY when nodePack.id is missing', () => {
|
||||
@@ -134,7 +133,7 @@ describe('PackVersionBadge', () => {
|
||||
|
||||
const badge = wrapper.find('[role="button"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(badge.find('span').text()).toBe('nightly')
|
||||
})
|
||||
|
||||
it('toggles the popover when button is clicked', async () => {
|
||||
|
||||
@@ -42,8 +42,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const TRUNCATED_HASH_LENGTH = 7
|
||||
@@ -64,11 +63,11 @@ const popoverRef = ref()
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const installedVersion = computed(() => {
|
||||
if (!nodePack.id) return SelectedVersion.NIGHTLY
|
||||
if (!nodePack.id) return 'nightly'
|
||||
const version =
|
||||
managerStore.installedPacks[nodePack.id]?.ver ??
|
||||
nodePack.latest_version?.version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
'nightly'
|
||||
|
||||
// If Git hash, truncate to 7 characters
|
||||
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)
|
||||
|
||||
@@ -8,7 +8,8 @@ import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
// SelectedVersion is now using direct strings instead of enum
|
||||
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
@@ -123,8 +124,8 @@ describe('PackVersionSelectorPopover', () => {
|
||||
expect(options.length).toBe(defaultMockVersions.length + 2) // 2 special options + version options
|
||||
|
||||
// Check that special options exist
|
||||
expect(options.some((o) => o.value === SelectedVersion.NIGHTLY)).toBe(true)
|
||||
expect(options.some((o) => o.value === SelectedVersion.LATEST)).toBe(true)
|
||||
expect(options.some((o) => o.value === 'nightly')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'latest')).toBe(true)
|
||||
|
||||
// Check that version options exist
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(true)
|
||||
@@ -304,7 +305,7 @@ describe('PackVersionSelectorPopover', () => {
|
||||
await waitForPromises()
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
})
|
||||
|
||||
it('defaults to nightly when publisher name is "Unclaimed"', async () => {
|
||||
@@ -325,7 +326,7 @@ describe('PackVersionSelectorPopover', () => {
|
||||
await waitForPromises()
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,12 +69,8 @@ import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
@@ -92,19 +88,20 @@ const managerStore = useComfyManagerStore()
|
||||
|
||||
const isQueueing = ref(false)
|
||||
|
||||
const selectedVersion = ref<string>(SelectedVersion.LATEST)
|
||||
const selectedVersion = ref<string>('latest')
|
||||
onMounted(() => {
|
||||
const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST
|
||||
const initialVersion = getInitialSelectedVersion() ?? 'latest'
|
||||
selectedVersion.value =
|
||||
// Use NIGHTLY when version is a Git hash
|
||||
isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY
|
||||
isSemVer(initialVersion) ? initialVersion : 'nightly'
|
||||
})
|
||||
|
||||
const getInitialSelectedVersion = () => {
|
||||
if (!nodePack.id) return
|
||||
|
||||
// If unclaimed, set selected version to nightly
|
||||
if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY
|
||||
if (nodePack.publisher?.name === 'Unclaimed')
|
||||
return 'nightly' as ManagerComponents['schemas']['SelectedVersion']
|
||||
|
||||
// If node pack is installed, set selected version to the installed version
|
||||
if (managerStore.isPackInstalled(nodePack.id))
|
||||
@@ -143,7 +140,7 @@ const onNodePackChange = async () => {
|
||||
// Add Latest option
|
||||
const defaultVersions = [
|
||||
{
|
||||
value: SelectedVersion.LATEST,
|
||||
value: 'latest' as ManagerComponents['schemas']['SelectedVersion'],
|
||||
label: t('manager.latestVersion')
|
||||
}
|
||||
]
|
||||
@@ -151,7 +148,7 @@ const onNodePackChange = async () => {
|
||||
// Add Nightly option if there is a non-empty `repository` field
|
||||
if (nodePack.repository?.length) {
|
||||
defaultVersions.push({
|
||||
value: SelectedVersion.NIGHTLY,
|
||||
value: 'nightly' as ManagerComponents['schemas']['SelectedVersion'],
|
||||
label: t('manager.nightlyVersion')
|
||||
})
|
||||
}
|
||||
@@ -172,12 +169,16 @@ whenever(
|
||||
|
||||
const handleSubmit = async () => {
|
||||
isQueueing.value = true
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
|
||||
await managerStore.installPack.call({
|
||||
id: nodePack.id,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
version: selectedVersion.value,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: 'default' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
selected_version: selectedVersion.value
|
||||
})
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import Button from 'primevue/button'
|
||||
|
||||
const {
|
||||
label,
|
||||
loading = false,
|
||||
loadingMessage,
|
||||
fullWidth = false,
|
||||
variant = 'default'
|
||||
|
||||
@@ -15,12 +15,8 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
InstallPackParams,
|
||||
ManagerChannel,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 256
|
||||
|
||||
@@ -28,37 +24,42 @@ const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackEnabled, enablePack, disablePack, installedPacks } =
|
||||
useComfyManagerStore()
|
||||
const { isPackEnabled, enablePack, disablePack } = useComfyManagerStore()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const isEnabled = computed(() => isPackEnabled(nodePack.id))
|
||||
const version = computed(() => {
|
||||
const id = nodePack.id
|
||||
if (!id) return SelectedVersion.NIGHTLY
|
||||
return (
|
||||
installedPacks[id]?.ver ??
|
||||
nodePack.latest_version?.version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
)
|
||||
})
|
||||
|
||||
const handleEnable = () =>
|
||||
enablePack.call({
|
||||
const handleEnable = () => {
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for enabling')
|
||||
}
|
||||
return enablePack.call({
|
||||
id: nodePack.id,
|
||||
version: version.value,
|
||||
selected_version: version.value,
|
||||
version:
|
||||
nodePack.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']),
|
||||
selected_version:
|
||||
nodePack.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']),
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: 'default' as InstallPackParams['mode']
|
||||
channel: 'default' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
skip_post_install: false
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisable = () =>
|
||||
disablePack({
|
||||
const handleDisable = () => {
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for disabling')
|
||||
}
|
||||
return disablePack({
|
||||
id: nodePack.id,
|
||||
version: version.value
|
||||
version:
|
||||
nodePack.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggle = async (enable: boolean) => {
|
||||
if (isLoading.value) return
|
||||
@@ -67,10 +68,16 @@ const handleToggle = async (enable: boolean) => {
|
||||
if (enable) {
|
||||
await handleEnable()
|
||||
} else {
|
||||
handleDisable()
|
||||
await handleDisable()
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const onToggle = debounce(handleToggle, TOGGLE_DEBOUNCE_MS, { trailing: true })
|
||||
const onToggle = debounce(
|
||||
(enable: boolean) => {
|
||||
void handleToggle(enable)
|
||||
},
|
||||
TOGGLE_DEBOUNCE_MS,
|
||||
{ trailing: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -19,13 +19,9 @@ import { inject, ref } from 'vue'
|
||||
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
IsInstallingKey,
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
@@ -43,19 +39,26 @@ const onClick = (): void => {
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (installItem: NodePack) => {
|
||||
const createPayload = (
|
||||
installItem: NodePack
|
||||
): ManagerComponents['schemas']['InstallPackParams'] => {
|
||||
if (!installItem.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
|
||||
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
|
||||
const versionToInstall = isUnclaimedPack
|
||||
? SelectedVersion.NIGHTLY
|
||||
: installItem.latest_version?.version ?? SelectedVersion.LATEST
|
||||
? ('nightly' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
: installItem.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
|
||||
return {
|
||||
id: installItem.id,
|
||||
version: versionToInstall,
|
||||
repository: installItem.repository ?? '',
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: versionToInstall,
|
||||
version: versionToInstall
|
||||
channel: 'dev' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
selected_version: versionToInstall
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +68,7 @@ const installPack = (item: NodePack) =>
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
isInstalling.value = true
|
||||
// isInstalling.value = true
|
||||
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<script setup lang="ts">
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { ManagerPackInfo } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
@@ -26,16 +25,16 @@ const { nodePacks } = defineProps<{
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (uninstallItem: NodePack): ManagerPackInfo => {
|
||||
return {
|
||||
id: uninstallItem.id,
|
||||
version: uninstallItem.latest_version?.version
|
||||
const uninstallPack = (item: NodePack) => {
|
||||
if (!item.id) {
|
||||
throw new Error('Node ID is required for uninstallation')
|
||||
}
|
||||
return managerStore.uninstallPack({
|
||||
id: item.id,
|
||||
version: item.latest_version?.version ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
const uninstallPack = (item: NodePack) =>
|
||||
managerStore.uninstallPack(createPayload(item))
|
||||
|
||||
const uninstallItems = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
await Promise.all(nodePacks.map(uninstallPack))
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<PackActionButton
|
||||
v-bind="$attrs"
|
||||
variant="black"
|
||||
:label="$t('manager.updateAll')"
|
||||
:loading="isUpdating"
|
||||
:loading-message="$t('g.updating')"
|
||||
@action="updateAllPacks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
}>()
|
||||
|
||||
const isUpdating = ref<boolean>(false)
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (updateItem: NodePack) => {
|
||||
return {
|
||||
id: updateItem.id!,
|
||||
version: updateItem.latest_version!.version!
|
||||
}
|
||||
}
|
||||
|
||||
const updatePack = async (item: NodePack) => {
|
||||
if (!item.id || !item.latest_version?.version) {
|
||||
console.warn('Pack missing required id or version:', item)
|
||||
return
|
||||
}
|
||||
await managerStore.updatePack.call(createPayload(item))
|
||||
}
|
||||
|
||||
const updateAllPacks = async () => {
|
||||
if (!nodePacks?.length) {
|
||||
console.warn('No packs provided for update')
|
||||
return
|
||||
}
|
||||
|
||||
isUpdating.value = true
|
||||
|
||||
const updatablePacks = nodePacks.filter((pack) =>
|
||||
managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
|
||||
if (!updatablePacks.length) {
|
||||
console.info('No installed packs available for update')
|
||||
isUpdating.value = false
|
||||
return
|
||||
}
|
||||
|
||||
console.info(`Starting update of ${updatablePacks.length} packs`)
|
||||
|
||||
try {
|
||||
await Promise.all(updatablePacks.map(updatePack))
|
||||
managerStore.updatePack.clear()
|
||||
console.info('All packs updated successfully')
|
||||
} catch (error) {
|
||||
console.error('Pack update failed:', error)
|
||||
console.error(
|
||||
'Failed packs info:',
|
||||
updatablePacks.map((p) => p.id)
|
||||
)
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -119,12 +119,20 @@ provide(IsInstallingKey, isInstalling)
|
||||
|
||||
const { isPackInstalled, isPackEnabled } = useComfyManagerStore()
|
||||
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const isDisabled = computed(
|
||||
() => isInstalled.value && !isPackEnabled(nodePack?.id)
|
||||
)
|
||||
const isDisabled = ref(false)
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
whenever(isInstalled, () => (isInstalling.value = false))
|
||||
// Watch the installedPacks object directly (which gets updated from WebSocket)
|
||||
whenever(
|
||||
() => managerStore.installedPacksIds,
|
||||
() => {
|
||||
const isInstalled = isPackInstalled(nodePack?.id)
|
||||
isDisabled.value = isInstalled && !isPackEnabled(nodePack?.id)
|
||||
|
||||
// Update isInstalling state after installation is complete
|
||||
if (isInstalling.value && isInstalled) isInstalling.value = false
|
||||
}
|
||||
)
|
||||
|
||||
const nodesCount = computed(() =>
|
||||
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
:node-packs="missingNodePacks"
|
||||
:label="$t('manager.installAllMissingNodes')"
|
||||
/>
|
||||
<PackUpdateButton
|
||||
v-if="isUpdateAvailableTab && hasUpdateAvailable"
|
||||
:node-packs="updateAvailableNodePacks"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-3 text-sm">
|
||||
<div class="flex gap-6 ml-1">
|
||||
@@ -65,8 +69,10 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackUpdateButton from '@/components/dialog/content/manager/button/PackUpdateButton.vue'
|
||||
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
|
||||
import {
|
||||
type SearchOption,
|
||||
SortableAlgoliaField
|
||||
@@ -83,6 +89,7 @@ const { searchResults, sortOptions } = defineProps<{
|
||||
suggestions?: QuerySuggestion[]
|
||||
sortOptions?: SortableField[]
|
||||
isMissingTab?: boolean
|
||||
isUpdateAvailableTab?: boolean
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>('searchQuery')
|
||||
@@ -96,6 +103,10 @@ const { t } = useI18n()
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
|
||||
// Use the composable to get update available nodes
|
||||
const { hasUpdateAvailable, updateAvailableNodePacks } =
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
const hasResults = computed(
|
||||
() => searchQuery.value?.trim() && searchResults?.length
|
||||
)
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full px-6 py-4 shadow-lg flex items-center justify-between"
|
||||
class="w-full px-6 py-2 shadow-lg flex items-center justify-between"
|
||||
:class="{
|
||||
'rounded-t-none': progressDialogContent.isExpanded,
|
||||
'rounded-lg': !progressDialogContent.isExpanded
|
||||
}"
|
||||
>
|
||||
<div class="justify-center text-sm font-bold leading-none">
|
||||
<div class="flex items-center text-base leading-none">
|
||||
<div class="flex items-center">
|
||||
<template v-if="isInProgress">
|
||||
<i class="pi pi-spin pi-spinner mr-2 text-3xl" />
|
||||
<DotSpinner duration="1s" class="mr-2" />
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<template v-else-if="isRestartCompleted">
|
||||
<span class="mr-2">🎉</span>
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="pi pi-check-circle mr-2 text-green-500" />
|
||||
<span class="leading-none">{{
|
||||
$t('manager.restartToApplyChanges')
|
||||
}}</span>
|
||||
<span class="mr-2">✅</span>
|
||||
<span>{{ $t('manager.restartToApplyChanges') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
|
||||
{{ comfyManagerStore.uncompletedCount }} {{ $t('g.progressCountOf') }}
|
||||
<span v-if="isInProgress" class="text-sm text-neutral-700">
|
||||
{{ completedTasksCount }} {{ $t('g.progressCountOf') }}
|
||||
{{ comfyManagerStore.taskLogs.length }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="!isInProgress"
|
||||
v-if="!isInProgress && !isRestartCompleted"
|
||||
rounded
|
||||
outlined
|
||||
class="px-4 py-2 rounded-md mr-4"
|
||||
class="mr-4 rounded-md border-2 px-3 text-neutral-600 border-neutral-900 hover:bg-neutral-100 !dark-theme:bg-transparent dark-theme:text-white dark-theme:border-white dark-theme:hover:bg-neutral-800"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('g.restart') }}
|
||||
{{ $t('manager.applyChanges') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="!isRestartCompleted"
|
||||
:icon="
|
||||
progressDialogContent.isExpanded
|
||||
? 'pi pi-chevron-up'
|
||||
@@ -44,6 +47,7 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
:aria-label="progressDialogContent.isExpanded ? 'Collapse' : 'Expand'"
|
||||
@click.stop="progressDialogContent.toggle"
|
||||
@@ -53,6 +57,7 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
@@ -65,9 +70,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
@@ -77,41 +83,93 @@ import {
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isInProgress = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
// State management for restart process
|
||||
const isRestarting = ref<boolean>(false)
|
||||
const isRestartCompleted = ref<boolean>(false)
|
||||
|
||||
const isInProgress = computed(
|
||||
() => comfyManagerStore.isProcessingTasks || isRestarting.value
|
||||
)
|
||||
|
||||
const completedTasksCount = computed(() => {
|
||||
return (
|
||||
comfyManagerStore.succeededTasksIds.length +
|
||||
comfyManagerStore.failedTasksIds.length
|
||||
)
|
||||
})
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
||||
}
|
||||
|
||||
const fallbackTaskName = t('g.installing')
|
||||
const fallbackTaskName = t('manager.installingDependencies')
|
||||
const currentTaskName = computed(() => {
|
||||
if (isRestarting.value) {
|
||||
return t('manager.restartingBackend')
|
||||
}
|
||||
if (isRestartCompleted.value) {
|
||||
return t('manager.extensionsSuccessfullyInstalled')
|
||||
}
|
||||
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
|
||||
const task = comfyManagerStore.taskLogs.at(-1)
|
||||
return task?.taskName ?? fallbackTaskName
|
||||
})
|
||||
|
||||
const handleRestart = async () => {
|
||||
const onReconnect = async () => {
|
||||
// Refresh manager state
|
||||
// Store original toast setting value
|
||||
const originalToastSetting = settingStore.get(
|
||||
'Comfy.Toast.DisableReconnectingToast'
|
||||
)
|
||||
|
||||
comfyManagerStore.clearLogs()
|
||||
comfyManagerStore.setStale()
|
||||
try {
|
||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
||||
|
||||
// Refresh node definitions
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
isRestarting.value = true
|
||||
|
||||
// Reload workflow
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
const onReconnect = async () => {
|
||||
try {
|
||||
comfyManagerStore.setStale()
|
||||
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
} finally {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
closeDialog()
|
||||
comfyManagerStore.resetTaskState()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
} catch (error) {
|
||||
// If restart fails, restore settings and reset state
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = false
|
||||
closeDialog() // Close dialog on error
|
||||
throw error
|
||||
}
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
closeDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,16 +18,40 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabMenu from 'primevue/tabmenu'
|
||||
import { ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const activeTabIndex = ref(0)
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const activeTabIndex = computed({
|
||||
get: () => progressDialogContent.getActiveTabIndex(),
|
||||
set: (value: number) => progressDialogContent.setActiveTabIndex(value)
|
||||
})
|
||||
const { t } = useI18n()
|
||||
const tabs = [
|
||||
{ label: t('manager.installationQueue') },
|
||||
{ label: t('manager.failed', { count: 0 }) }
|
||||
]
|
||||
|
||||
const failedCount = computed(() => comfyManagerStore.failedTasksIds.length)
|
||||
|
||||
const queueSuffix = computed(() => {
|
||||
const queueLength = comfyManagerStore.managerQueue.queueLength
|
||||
if (queueLength === 0) {
|
||||
return ''
|
||||
}
|
||||
return ` (${queueLength})`
|
||||
})
|
||||
const failedSuffix = computed(() => {
|
||||
if (failedCount.value === 0) {
|
||||
return ''
|
||||
}
|
||||
return ` (${failedCount.value})`
|
||||
})
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ label: t('manager.installationQueue') + queueSuffix.value },
|
||||
{ label: t('manager.failed') + failedSuffix.value }
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -20,35 +21,24 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const widgetStates = computed(() => [...domWidgetStore.widgetStates.values()])
|
||||
const widgetStates = computed(() => domWidgetStore.activeWidgetStates)
|
||||
|
||||
const updateWidgets = () => {
|
||||
const lgCanvas = canvasStore.canvas
|
||||
if (!lgCanvas) return
|
||||
|
||||
const lowQuality = lgCanvas.low_quality
|
||||
const currentGraph = lgCanvas.graph
|
||||
|
||||
for (const widgetState of widgetStates.value) {
|
||||
const widget = widgetState.widget
|
||||
const node = widget.node as LGraphNode
|
||||
|
||||
// Early exit for non-visible widgets
|
||||
if (!widget.isVisible()) {
|
||||
widgetState.visible = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the widget's node is in the current graph
|
||||
const node = widget.node
|
||||
const isInCorrectGraph = currentGraph?.nodes.includes(node)
|
||||
|
||||
widgetState.visible =
|
||||
!!isInCorrectGraph &&
|
||||
const visible =
|
||||
lgCanvas.isNodeVisible(node) &&
|
||||
!(widget.options.hideOnZoom && lowQuality)
|
||||
!(widget.options.hideOnZoom && lowQuality) &&
|
||||
widget.isVisible()
|
||||
|
||||
if (widgetState.visible && node) {
|
||||
widgetState.visible = visible
|
||||
if (visible) {
|
||||
const margin = widget.margin
|
||||
widgetState.pos = [node.pos[0] + margin, node.pos[1] + margin + widget.y]
|
||||
widgetState.size = [
|
||||
|
||||
@@ -72,6 +72,7 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
@@ -191,26 +192,22 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Update the progress of executing nodes
|
||||
// Update the progress of the executing node
|
||||
watch(
|
||||
() =>
|
||||
[executionStore.nodeLocationProgressStates, canvasStore.canvas] as const,
|
||||
([nodeLocationProgressStates, canvas]) => {
|
||||
if (!canvas?.graph) return
|
||||
for (const node of canvas.graph.nodes) {
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(node.id)
|
||||
const progressState = nodeLocationProgressStates[nodeLocatorId]
|
||||
if (progressState && progressState.state === 'running') {
|
||||
node.progress = progressState.value / progressState.max
|
||||
[
|
||||
executionStore.executingNodeId,
|
||||
executionStore.executingNodeProgress
|
||||
] satisfies [NodeId | null, number | null],
|
||||
([executingNodeId, executingNodeProgress]) => {
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
if (node.id == executingNodeId) {
|
||||
node.progress = executingNodeProgress ?? undefined
|
||||
} else {
|
||||
node.progress = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Force canvas redraw to ensure progress updates are visible
|
||||
canvas.graph.setDirtyCanvas(true, false)
|
||||
},
|
||||
{ deep: true }
|
||||
}
|
||||
)
|
||||
|
||||
// Update node slot errors
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementBounding, useEventListener } from '@vueuse/core'
|
||||
import { CSSProperties, computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { CSSProperties, computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import { useDomClipping } from '@/composables/element/useDomClipping'
|
||||
@@ -61,13 +61,10 @@ const updateDomClipping = () => {
|
||||
if (!lgCanvas || !widgetElement.value) return
|
||||
|
||||
const selectedNode = Object.values(lgCanvas.selected_nodes ?? {})[0]
|
||||
if (!selectedNode) {
|
||||
// Clear clipping when no node is selected
|
||||
updateClipPath(widgetElement.value, lgCanvas.canvas, false, undefined)
|
||||
return
|
||||
}
|
||||
if (!selectedNode) return
|
||||
|
||||
const isSelected = selectedNode === widget.node
|
||||
const node = widget.node
|
||||
const isSelected = selectedNode === node
|
||||
const renderArea = selectedNode?.renderArea
|
||||
const offset = lgCanvas.ds.offset
|
||||
const scale = lgCanvas.ds.scale
|
||||
@@ -125,10 +122,7 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Set up event listeners only after the widget is mounted and visible
|
||||
const setupDOMEventListeners = () => {
|
||||
if (!isDOMWidget(widget) || !widgetState.visible) return
|
||||
|
||||
if (isDOMWidget(widget)) {
|
||||
if (widget.element.blur) {
|
||||
useEventListener(document, 'mousedown', (event) => {
|
||||
if (!widget.element.contains(event.target as HTMLElement)) {
|
||||
@@ -146,46 +140,14 @@ const setupDOMEventListeners = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Set up event listeners when widget becomes visible
|
||||
watch(
|
||||
() => widgetState.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
setupDOMEventListeners()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const inputSpec = widget.node.constructor.nodeData
|
||||
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
|
||||
|
||||
// Mount DOM element when widget is or becomes visible
|
||||
const mountElementIfVisible = () => {
|
||||
if (widgetState.visible && isDOMWidget(widget) && widgetElement.value) {
|
||||
// Only append if not already a child
|
||||
if (!widgetElement.value.contains(widget.element)) {
|
||||
widgetElement.value.appendChild(widget.element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check on mount - but only after next tick to ensure visibility is calculated
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
mountElementIfVisible()
|
||||
}).catch((error) => {
|
||||
console.error('Error mounting DOM widget element:', error)
|
||||
})
|
||||
})
|
||||
|
||||
// And watch for visibility changes
|
||||
watch(
|
||||
() => widgetState.visible,
|
||||
() => {
|
||||
mountElementIfVisible()
|
||||
if (isDOMWidget(widget) && widgetElement.value) {
|
||||
widgetElement.value.appendChild(widget.element)
|
||||
}
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -20,44 +20,33 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
widget?: object
|
||||
nodeId: NodeId
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let parentNodeId: NodeId | null = null
|
||||
let executingNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
// Get the parent node ID from props if provided
|
||||
// For backward compatibility, fall back to the first executing node
|
||||
parentNodeId = props.nodeId
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
})
|
||||
|
||||
// Watch for either a new node has starting execution or overall execution ending
|
||||
const stopWatching = watch(
|
||||
[() => executionStore.executingNodeIds, () => executionStore.isIdle],
|
||||
[() => executionStore.executingNode, () => executionStore.isIdle],
|
||||
() => {
|
||||
if (executionStore.isIdle) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if parent node is no longer in the executing nodes list
|
||||
if (
|
||||
parentNodeId &&
|
||||
!executionStore.executingNodeIds.includes(parentNodeId)
|
||||
executionStore.isIdle ||
|
||||
(executionStore.executingNode &&
|
||||
executionStore.executingNode.id !== executingNodeId)
|
||||
) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
}
|
||||
|
||||
// Set parent node ID if not set yet
|
||||
if (!parentNodeId && executionStore.executingNodeIds.length > 0) {
|
||||
parentNodeId = executionStore.executingNodeIds[0]
|
||||
if (!executingNodeId) {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
@update-up-direction="handleUpdateUpDirection"
|
||||
@update-material-mode="handleUpdateMaterialMode"
|
||||
@update-edge-threshold="handleUpdateEdgeThreshold"
|
||||
@upload-texture="handleUploadTexture"
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
<div
|
||||
@@ -214,6 +215,30 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
|
||||
node.properties['Background Image'] = backgroundImage.value
|
||||
}
|
||||
|
||||
const handleUploadTexture = async (file: File) => {
|
||||
if (!load3DSceneRef.value?.load3d) {
|
||||
useToastStore().addAlert(t('toastMessages.no3dScene'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
|
||||
|
||||
const subfolder = resourceFolder.trim()
|
||||
? `3d/${resourceFolder.trim()}`
|
||||
: '3d'
|
||||
|
||||
const texturePath = await Load3dUtils.uploadFile(file, subfolder)
|
||||
|
||||
await load3DSceneRef.value.load3d.applyTexture(texturePath)
|
||||
|
||||
node.properties['Texture'] = texturePath
|
||||
} catch (error) {
|
||||
console.error('Error applying texture:', error)
|
||||
useToastStore().addAlert(t('toastMessages.failedToApplyTexture'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateFOV = (value: number) => {
|
||||
fov.value = value
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
@update-up-direction="handleUpdateUpDirection"
|
||||
@update-material-mode="handleUpdateMaterialMode"
|
||||
@update-edge-threshold="handleUpdateEdgeThreshold"
|
||||
@upload-texture="handleUploadTexture"
|
||||
/>
|
||||
|
||||
<CameraControls
|
||||
@@ -182,6 +183,7 @@ const emit = defineEmits<{
|
||||
(e: 'updateMaterialMode', mode: MaterialMode): void
|
||||
(e: 'updateEdgeThreshold', value: number): void
|
||||
(e: 'exportModel', format: string): void
|
||||
(e: 'uploadTexture', file: File): void
|
||||
}>()
|
||||
|
||||
const backgroundColor = ref(props.backgroundColor)
|
||||
@@ -230,6 +232,10 @@ const handleUpdateEdgeThreshold = (value: number) => {
|
||||
emit('updateEdgeThreshold', value)
|
||||
}
|
||||
|
||||
const handleUploadTexture = (file: File) => {
|
||||
emit('uploadTexture', file)
|
||||
}
|
||||
|
||||
const handleUpdateLightIntensity = (value: number) => {
|
||||
emit('updateLightIntensity', value)
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ const eventConfig = {
|
||||
exportLoadingEnd: () => {
|
||||
loadingOverlayRef.value?.endLoading()
|
||||
},
|
||||
textureLoadingStart: () =>
|
||||
loadingOverlayRef.value?.startLoading(t('load3d.applyingTexture')),
|
||||
textureLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
|
||||
recordingStatusChange: (value: boolean) =>
|
||||
emit('recordingStatusChange', value)
|
||||
} as const
|
||||
|
||||
@@ -59,6 +59,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
materialMode === 'original' &&
|
||||
!props.inputSpec.isAnimation &&
|
||||
!props.inputSpec.isPreview
|
||||
"
|
||||
class="relative show-texture-upload"
|
||||
>
|
||||
<Button class="p-button-rounded p-button-text" @click="openTextureUpload">
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: t('load3d.uploadTexture'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-image text-white text-lg"
|
||||
/>
|
||||
<input
|
||||
ref="texturePickerRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="absolute opacity-0 w-0 h-0 p-0 m-0 pointer-events-none"
|
||||
@change="uploadTexture"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="materialMode === 'lineart'" class="relative show-edge-threshold">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
@@ -116,6 +142,7 @@ const emit = defineEmits<{
|
||||
(e: 'updateUpDirection', direction: UpDirection): void
|
||||
(e: 'updateMaterialMode', mode: MaterialMode): void
|
||||
(e: 'updateEdgeThreshold', value: number): void
|
||||
(e: 'uploadTexture', file: File): void
|
||||
}>()
|
||||
|
||||
const upDirection = ref(props.upDirection || 'original')
|
||||
@@ -124,6 +151,7 @@ const edgeThreshold = ref(props.edgeThreshold || 85)
|
||||
const showUpDirection = ref(false)
|
||||
const showMaterialMode = ref(false)
|
||||
const showEdgeThreshold = ref(false)
|
||||
const texturePickerRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const upDirections: UpDirection[] = [
|
||||
'original',
|
||||
@@ -219,6 +247,18 @@ const updateEdgeThreshold = () => {
|
||||
emit('updateEdgeThreshold', edgeThreshold.value)
|
||||
}
|
||||
|
||||
const openTextureUpload = () => {
|
||||
texturePickerRef.value?.click()
|
||||
}
|
||||
|
||||
const uploadTexture = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
emit('uploadTexture', input.files[0])
|
||||
}
|
||||
}
|
||||
|
||||
const closeSceneSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
|
||||
@@ -1004,194 +1004,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
|
||||
return '$2.25/Run'
|
||||
}
|
||||
},
|
||||
// Runway nodes - using actual node names from ComfyUI
|
||||
RunwayTextToImageNode: {
|
||||
displayPrice: '$0.08/Run'
|
||||
},
|
||||
RunwayImageToVideoNodeGen3a: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
|
||||
if (!durationWidget) return '$0.05/second'
|
||||
|
||||
const duration = Number(durationWidget.value) || 5
|
||||
const cost = (0.05 * duration).toFixed(2)
|
||||
return `$${cost}/Run`
|
||||
}
|
||||
},
|
||||
RunwayImageToVideoNodeGen4: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
|
||||
if (!durationWidget) return '$0.05/second'
|
||||
|
||||
const duration = Number(durationWidget.value) || 5
|
||||
const cost = (0.05 * duration).toFixed(2)
|
||||
return `$${cost}/Run`
|
||||
}
|
||||
},
|
||||
RunwayFirstLastFrameNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
|
||||
if (!durationWidget) return '$0.05/second'
|
||||
|
||||
const duration = Number(durationWidget.value) || 5
|
||||
const cost = (0.05 * duration).toFixed(2)
|
||||
return `$${cost}/Run`
|
||||
}
|
||||
},
|
||||
// Rodin nodes - all have the same pricing structure
|
||||
Rodin3D_Regular: {
|
||||
displayPrice: '$0.4/Run'
|
||||
},
|
||||
Rodin3D_Detail: {
|
||||
displayPrice: '$1.2/Run'
|
||||
},
|
||||
Rodin3D_Smooth: {
|
||||
displayPrice: '$1.2/Run'
|
||||
},
|
||||
Rodin3D_Sketch: {
|
||||
displayPrice: '$0.4/Run'
|
||||
},
|
||||
// Tripo nodes - using actual node names from ComfyUI
|
||||
TripoTextToModelNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w) => w.name === 'model' || w.name === 'model_version'
|
||||
) as IComboWidget
|
||||
const textureQualityWidget = node.widgets?.find(
|
||||
(w) => w.name === 'texture_quality'
|
||||
) as IComboWidget
|
||||
|
||||
if (!modelWidget)
|
||||
return '$0.2-0.3/Run (varies with model & texture quality)'
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
const textureQuality = String(textureQualityWidget?.value || 'standard')
|
||||
|
||||
// V2.5 pricing
|
||||
if (model.includes('v2.5') || model.includes('2.5')) {
|
||||
return textureQuality.includes('detailed') ? '$0.3/Run' : '$0.2/Run'
|
||||
}
|
||||
// V2.0 pricing
|
||||
else if (model.includes('v2.0') || model.includes('2.0')) {
|
||||
return textureQuality.includes('detailed') ? '$0.3/Run' : '$0.2/Run'
|
||||
}
|
||||
// V1.4 or legacy pricing
|
||||
else {
|
||||
return '$0.2/Run'
|
||||
}
|
||||
}
|
||||
},
|
||||
TripoImageToModelNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w) => w.name === 'model' || w.name === 'model_version'
|
||||
) as IComboWidget
|
||||
const textureQualityWidget = node.widgets?.find(
|
||||
(w) => w.name === 'texture_quality'
|
||||
) as IComboWidget
|
||||
|
||||
if (!modelWidget)
|
||||
return '$0.3-0.4/Run (varies with model & texture quality)'
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
const textureQuality = String(textureQualityWidget?.value || 'standard')
|
||||
|
||||
// V2.5 and V2.0 have same pricing structure
|
||||
if (
|
||||
model.includes('v2.5') ||
|
||||
model.includes('2.5') ||
|
||||
model.includes('v2.0') ||
|
||||
model.includes('2.0')
|
||||
) {
|
||||
return textureQuality.includes('detailed') ? '$0.4/Run' : '$0.3/Run'
|
||||
}
|
||||
// V1.4 or legacy pricing (image_to_model is always $0.3)
|
||||
else {
|
||||
return '$0.3/Run'
|
||||
}
|
||||
}
|
||||
},
|
||||
TripoRefineNode: {
|
||||
displayPrice: '$0.3/Run'
|
||||
},
|
||||
TripoTextureNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const textureQualityWidget = node.widgets?.find(
|
||||
(w) => w.name === 'texture_quality'
|
||||
) as IComboWidget
|
||||
|
||||
if (!textureQualityWidget) return '$0.1-0.2/Run (varies with quality)'
|
||||
|
||||
const textureQuality = String(textureQualityWidget.value)
|
||||
return textureQuality.includes('detailed') ? '$0.2/Run' : '$0.1/Run'
|
||||
}
|
||||
},
|
||||
// Google/Gemini nodes
|
||||
GeminiNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w) => w.name === 'model'
|
||||
) as IComboWidget
|
||||
|
||||
if (!modelWidget) return 'Token-based'
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
|
||||
// Google Veo video generation
|
||||
if (model.includes('veo-2.0')) {
|
||||
return '$0.5/second'
|
||||
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
||||
return '$0.0035/$0.0008 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-flash-preview-04-17')) {
|
||||
return '$0.0015/$0.0004 per 1K tokens'
|
||||
}
|
||||
// For other Gemini models, show token-based pricing info
|
||||
return 'Token-based'
|
||||
}
|
||||
},
|
||||
// OpenAI nodes
|
||||
OpenAIChatNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w) => w.name === 'model'
|
||||
) as IComboWidget
|
||||
|
||||
if (!modelWidget) return 'Token-based'
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
|
||||
// Specific pricing for exposed models based on official pricing data (converted to per 1K tokens)
|
||||
if (model.includes('o4-mini')) {
|
||||
return '$0.0011/$0.0044 per 1K tokens'
|
||||
} else if (model.includes('o1-pro')) {
|
||||
return '$0.15/$0.60 per 1K tokens'
|
||||
} else if (model.includes('o1')) {
|
||||
return '$0.015/$0.06 per 1K tokens'
|
||||
} else if (model.includes('o3-mini')) {
|
||||
return '$0.0011/$0.0044 per 1K tokens'
|
||||
} else if (model.includes('o3')) {
|
||||
return '$0.01/$0.04 per 1K tokens'
|
||||
} else if (model.includes('gpt-4o')) {
|
||||
return '$0.0025/$0.01 per 1K tokens'
|
||||
} else if (model.includes('gpt-4.1-nano')) {
|
||||
return '$0.0001/$0.0004 per 1K tokens'
|
||||
} else if (model.includes('gpt-4.1-mini')) {
|
||||
return '$0.0004/$0.0016 per 1K tokens'
|
||||
} else if (model.includes('gpt-4.1')) {
|
||||
return '$0.002/$0.008 per 1K tokens'
|
||||
}
|
||||
return 'Token-based'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1263,19 +1075,7 @@ export const useNodePricing = () => {
|
||||
RecraftGenerateVectorImageNode: ['n'],
|
||||
MoonvalleyTxt2VideoNode: ['length'],
|
||||
MoonvalleyImg2VideoNode: ['length'],
|
||||
MoonvalleyVideo2VideoNode: ['length'],
|
||||
// Runway nodes
|
||||
RunwayImageToVideoNodeGen3a: ['duration'],
|
||||
RunwayImageToVideoNodeGen4: ['duration'],
|
||||
RunwayFirstLastFrameNode: ['duration'],
|
||||
// Tripo nodes
|
||||
TripoTextToModelNode: ['model', 'model_version', 'texture_quality'],
|
||||
TripoImageToModelNode: ['model', 'model_version', 'texture_quality'],
|
||||
TripoTextureNode: ['texture_quality'],
|
||||
// Google/Gemini nodes
|
||||
GeminiNode: ['model'],
|
||||
// OpenAI nodes
|
||||
OpenAIChatNode: ['model']
|
||||
MoonvalleyVideo2VideoNode: ['length']
|
||||
}
|
||||
return widgetMap[nodeType] || []
|
||||
}
|
||||
|
||||
65
src/composables/nodePack/useUpdateAvailableNodes.ts
Normal file
65
src/composables/nodePack/useUpdateAvailableNodes.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Composable to find NodePacks that have updates available
|
||||
* Uses the same filtering approach as ManagerDialogContent.vue
|
||||
* Automatically fetches installed pack data when initialized
|
||||
*/
|
||||
export const useUpdateAvailableNodes = () => {
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { installedPacks, isLoading, error, startFetchInstalled } =
|
||||
useInstalledPacks()
|
||||
|
||||
// Check if a pack has updates available (same logic as usePackUpdateStatus)
|
||||
const isOutdatedPack = (pack: components['schemas']['Node']) => {
|
||||
const isInstalled = comfyManagerStore.isPackInstalled(pack?.id)
|
||||
if (!isInstalled) return false
|
||||
|
||||
const installedVersion = comfyManagerStore.getInstalledPackVersion(
|
||||
pack.id ?? ''
|
||||
)
|
||||
const latestVersion = pack.latest_version?.version
|
||||
|
||||
const isNightlyPack = !!installedVersion && !isSemVer(installedVersion)
|
||||
|
||||
if (isNightlyPack || !latestVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
return compareVersions(latestVersion, installedVersion) > 0
|
||||
}
|
||||
|
||||
// Same filtering logic as ManagerDialogContent.vue
|
||||
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter(isOutdatedPack)
|
||||
|
||||
// Filter only outdated packs from installed packs
|
||||
const updateAvailableNodePacks = computed(() => {
|
||||
if (!installedPacks.value.length) return []
|
||||
return filterOutdatedPacks(installedPacks.value)
|
||||
})
|
||||
|
||||
// Check if there are any outdated packs
|
||||
const hasUpdateAvailable = computed(() => {
|
||||
return updateAvailableNodePacks.value.length > 0
|
||||
})
|
||||
|
||||
// Automatically fetch installed pack data when composable is used
|
||||
onMounted(async () => {
|
||||
if (!installedPacks.value.length && !isLoading.value) {
|
||||
await startFetchInstalled()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
updateAvailableNodePacks,
|
||||
hasUpdateAvailable,
|
||||
isLoading,
|
||||
error
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { app } from '@/scripts/app'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { SelectedVersion, UseNodePacksOptions } from '@/types/comfyManagerTypes'
|
||||
import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type WorkflowPack = {
|
||||
@@ -65,8 +65,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
return {
|
||||
id: CORE_NODES_PACK_NAME,
|
||||
version:
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ?? 'nightly'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +75,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
if (pack) {
|
||||
return {
|
||||
id: pack.id,
|
||||
version: pack.latest_version?.version ?? SelectedVersion.NIGHTLY
|
||||
version: pack.latest_version?.version ?? 'nightly'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
@@ -37,34 +36,11 @@ export const useBrowserTabTitle = () => {
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() => {
|
||||
// Check if any nodes are in progress
|
||||
const nodeProgressEntries = Object.entries(
|
||||
executionStore.nodeProgressStates
|
||||
)
|
||||
const runningNodes = nodeProgressEntries.filter(
|
||||
([_, state]) => state.state === 'running'
|
||||
)
|
||||
|
||||
if (runningNodes.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// If multiple nodes are running
|
||||
if (runningNodes.length > 1) {
|
||||
return `${executionText.value}[${runningNodes.length} ${t('g.nodesRunning', 'nodes running')}]`
|
||||
}
|
||||
|
||||
// If only one node is running
|
||||
const [nodeId, state] = runningNodes[0]
|
||||
const progress = Math.round((state.value / state.max) * 100)
|
||||
const nodeType =
|
||||
executionStore.activePrompt?.workflow?.changeTracker?.activeState.nodes.find(
|
||||
(n) => String(n.id) === nodeId
|
||||
)?.type || 'Node'
|
||||
|
||||
return `${executionText.value}[${progress}%] ${nodeType}`
|
||||
})
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
|
||||
@@ -15,10 +15,11 @@ import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { type ComfyCommand, useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
@@ -29,6 +30,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
const moveSelectedNodesVersionAdded = '1.22.2'
|
||||
|
||||
@@ -660,12 +662,54 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager',
|
||||
icon: 'pi pi-puzzle',
|
||||
label: 'Toggle the Custom Nodes Manager',
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
icon: 'pi pi-objects-column',
|
||||
label: 'Custom Nodes Manager',
|
||||
versionAdded: '1.12.10',
|
||||
function: async () => {
|
||||
const { is_legacy_manager_ui } =
|
||||
(await useComfyManagerService().isLegacyManagerUI()) ?? {}
|
||||
|
||||
if (is_legacy_manager_ui === true) {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
} else {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowUpdateAvailablePacks',
|
||||
icon: 'pi pi-sync',
|
||||
label: 'Check for Custom Node Updates',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
dialogService.toggleManagerDialog()
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.UpdateAvailable
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowMissingPacks',
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
label: 'Install Missing Custom Nodes',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -754,9 +798,88 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { node } = res
|
||||
canvas.select(node)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',
|
||||
icon: 'pi pi-bars',
|
||||
label: 'Custom Nodes (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
|
||||
)
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowLegacyManagerMenu',
|
||||
icon: 'mdi mdi-puzzle',
|
||||
label: 'Manager Menu (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModels',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModels'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: false })
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModelsAndExecutionCache',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models and Execution Cache',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: true })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,101 +1,145 @@
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { computed, readonly, ref } from 'vue'
|
||||
import { pickBy } from 'lodash'
|
||||
import { Ref, computed, ref } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { ManagerWsQueueStatus } from '@/types/comfyManagerTypes'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
type QueuedTask<T> = {
|
||||
task: () => Promise<T>
|
||||
onComplete?: () => void
|
||||
}
|
||||
type ManagerTaskHistory = Record<
|
||||
string,
|
||||
components['schemas']['TaskHistoryItem']
|
||||
>
|
||||
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
|
||||
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
|
||||
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
|
||||
type QueueTaskItem = components['schemas']['QueueTaskItem']
|
||||
type HistoryTaskItem = components['schemas']['TaskHistoryItem']
|
||||
type Task = QueueTaskItem | HistoryTaskItem
|
||||
|
||||
const MANAGER_WS_MSG_TYPE = 'cm-queue-status'
|
||||
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
|
||||
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
|
||||
|
||||
export const useManagerQueue = () => {
|
||||
const clientQueueItems = ref<QueuedTask<unknown>[]>([])
|
||||
const clientQueueLength = computed(() => clientQueueItems.value.length)
|
||||
const onCompletedQueue = ref<((() => void) | undefined)[]>([])
|
||||
const onCompleteWaitingCount = ref(0)
|
||||
const uncompletedCount = computed(
|
||||
() => clientQueueLength.value + onCompleteWaitingCount.value
|
||||
export const useManagerQueue = (
|
||||
taskHistory: Ref<ManagerTaskHistory>,
|
||||
taskQueue: Ref<ManagerTaskQueue>,
|
||||
installedPacks: Ref<Record<string, any>>
|
||||
) => {
|
||||
const { showManagerProgressDialog } = useDialogService()
|
||||
|
||||
// Task queue state (read-only from server)
|
||||
const maxHistoryItems = ref(64)
|
||||
const isLoading = ref(false)
|
||||
const isProcessing = ref(false)
|
||||
|
||||
// Computed values
|
||||
const currentQueueLength = computed(
|
||||
() =>
|
||||
taskQueue.value.running_queue.length +
|
||||
taskQueue.value.pending_queue.length
|
||||
)
|
||||
|
||||
const serverQueueStatus = ref<ManagerWsQueueStatus>(ManagerWsQueueStatus.DONE)
|
||||
const isServerIdle = computed(
|
||||
() => serverQueueStatus.value === ManagerWsQueueStatus.DONE
|
||||
)
|
||||
/**
|
||||
* Update the processing state based on the current queue length.
|
||||
* If the queue is empty, or all tasks in the queue are associated
|
||||
* with different clients, then this client is not processing any tasks.
|
||||
*/
|
||||
const updateProcessingState = (): void => {
|
||||
isProcessing.value = currentQueueLength.value > 0
|
||||
}
|
||||
|
||||
const allTasksDone = computed(
|
||||
() => isServerIdle.value && clientQueueLength.value === 0
|
||||
)
|
||||
const nextTaskReady = computed(
|
||||
() => isServerIdle.value && clientQueueLength.value > 0
|
||||
)
|
||||
const allTasksDone = computed(() => currentQueueLength.value === 0)
|
||||
const historyCount = computed(() => Object.keys(taskHistory.value).length)
|
||||
|
||||
const cleanupListener = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_MSG_TYPE,
|
||||
(event: CustomEvent<{ status: ManagerWsQueueStatus }>) => {
|
||||
if (event?.type === MANAGER_WS_MSG_TYPE && event.detail?.status) {
|
||||
serverQueueStatus.value = event.detail.status
|
||||
/**
|
||||
* Check if a task is associated with this client.
|
||||
* Task can be from running queue, pending queue, or history.
|
||||
* @param task - The task to check
|
||||
* @returns True if the task belongs to this client
|
||||
*/
|
||||
const isTaskFromThisClient = (task: Task): boolean =>
|
||||
task.client_id === app.api.clientId
|
||||
|
||||
/**
|
||||
* Filter queue tasks by client id.
|
||||
* Ensures that only tasks associated with this client are processed and
|
||||
* added to client state.
|
||||
* @param tasks - Array of queue tasks to filter
|
||||
* @returns Filtered array containing only tasks from this client
|
||||
*/
|
||||
const filterQueueByClientId = (tasks: QueueTaskItem[]): QueueTaskItem[] =>
|
||||
tasks.filter(isTaskFromThisClient)
|
||||
|
||||
/**
|
||||
* Filter history tasks by client id using lodash pickBy for optimal performance.
|
||||
* Returns a new object containing only tasks associated with this client.
|
||||
* @param history - The history object to filter
|
||||
* @returns Filtered history object containing only tasks from this client
|
||||
*/
|
||||
const filterHistoryByClientId = (history: ManagerTaskHistory) =>
|
||||
pickBy(history, isTaskFromThisClient)
|
||||
|
||||
/**
|
||||
* Update task queue and history state with filtered data from server.
|
||||
* Ensures only tasks from this client are stored in local state.
|
||||
* @param state - The task state message from the server
|
||||
*/
|
||||
const updateTaskState = (state: ManagerTaskQueue) => {
|
||||
taskQueue.value.running_queue = filterQueueByClientId(state.running_queue)
|
||||
taskQueue.value.pending_queue = filterQueueByClientId(state.pending_queue)
|
||||
taskHistory.value = filterHistoryByClientId(state.history)
|
||||
|
||||
if (state.installed_packs) {
|
||||
installedPacks.value = state.installed_packs
|
||||
}
|
||||
updateProcessingState()
|
||||
}
|
||||
|
||||
// WebSocket event listener for task done
|
||||
const cleanupTaskDoneListener = useEventListener(
|
||||
app.api,
|
||||
MANAGER_WS_TASK_DONE_NAME,
|
||||
(event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_DONE_NAME) {
|
||||
const { state } = event.detail
|
||||
updateTaskState(state)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const startNextTask = () => {
|
||||
const nextTask = clientQueueItems.value.shift()
|
||||
if (!nextTask) return
|
||||
|
||||
const { task, onComplete } = nextTask
|
||||
if (onComplete) {
|
||||
// Set the task's onComplete to be executed the next time the server is idle
|
||||
onCompletedQueue.value.push(onComplete)
|
||||
onCompleteWaitingCount.value++
|
||||
}
|
||||
|
||||
task().catch((e) => {
|
||||
const message = `Error enqueuing task for ComfyUI Manager: ${e}`
|
||||
console.error(message)
|
||||
})
|
||||
}
|
||||
|
||||
const enqueueTask = <T>(task: QueuedTask<T>): void => {
|
||||
clientQueueItems.value.push(task)
|
||||
}
|
||||
|
||||
const clearQueue = () => {
|
||||
clientQueueItems.value = []
|
||||
onCompletedQueue.value = []
|
||||
onCompleteWaitingCount.value = 0
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
clearQueue()
|
||||
cleanupListener()
|
||||
}
|
||||
|
||||
whenever(nextTaskReady, startNextTask)
|
||||
whenever(isServerIdle, () => {
|
||||
if (onCompletedQueue.value?.length) {
|
||||
while (
|
||||
onCompleteWaitingCount.value > 0 &&
|
||||
onCompletedQueue.value.length > 0
|
||||
) {
|
||||
const onComplete = onCompletedQueue.value.shift()
|
||||
onComplete?.()
|
||||
onCompleteWaitingCount.value--
|
||||
// WebSocket event listener for task started
|
||||
const cleanupTaskStartedListener = useEventListener(
|
||||
app.api,
|
||||
MANAGER_WS_TASK_STARTED_NAME,
|
||||
(event: CustomEvent<ManagerWsTaskStartedMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_STARTED_NAME) {
|
||||
const { state } = event.detail
|
||||
updateTaskState(state)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
whenever(currentQueueLength, () => showManagerProgressDialog())
|
||||
|
||||
const stopListening = () => {
|
||||
cleanupTaskDoneListener()
|
||||
cleanupTaskStartedListener()
|
||||
}
|
||||
|
||||
return {
|
||||
allTasksDone,
|
||||
statusMessage: readonly(serverQueueStatus),
|
||||
queueLength: clientQueueLength,
|
||||
uncompletedCount,
|
||||
// Queue state (read-only from server)
|
||||
taskHistory,
|
||||
taskQueue,
|
||||
maxHistoryItems,
|
||||
isLoading,
|
||||
|
||||
enqueueTask,
|
||||
clearQueue,
|
||||
cleanup
|
||||
// Computed state
|
||||
allTasksDone,
|
||||
isProcessing,
|
||||
queueLength: currentQueueLength,
|
||||
historyCount,
|
||||
|
||||
// Actions
|
||||
stopListening
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
@@ -8,11 +9,12 @@ interface RefreshableItem {
|
||||
refresh: () => Promise<void> | void
|
||||
}
|
||||
|
||||
const isRefreshableWidget = (widget: unknown): widget is RefreshableItem =>
|
||||
widget != null &&
|
||||
typeof widget === 'object' &&
|
||||
'refresh' in widget &&
|
||||
typeof widget.refresh === 'function'
|
||||
type RefreshableWidget = IBaseWidget & RefreshableItem
|
||||
|
||||
const isRefreshableWidget = (
|
||||
widget: IBaseWidget
|
||||
): widget is RefreshableWidget =>
|
||||
'refresh' in widget && typeof widget.refresh === 'function'
|
||||
|
||||
/**
|
||||
* Tracks selected nodes and their refreshable widgets
|
||||
@@ -25,17 +27,10 @@ export const useRefreshableSelection = () => {
|
||||
selectedNodes.value = graphStore.selectedItems.filter(isLGraphNode)
|
||||
})
|
||||
|
||||
const refreshableWidgets = computed<RefreshableItem[]>(() =>
|
||||
selectedNodes.value.flatMap((node) => {
|
||||
if (!node.widgets) return []
|
||||
const items: RefreshableItem[] = []
|
||||
for (const widget of node.widgets) {
|
||||
if (isRefreshableWidget(widget)) {
|
||||
items.push(widget)
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
const refreshableWidgets = computed(() =>
|
||||
selectedNodes.value.flatMap(
|
||||
(node) => node.widgets?.filter(isRefreshableWidget) ?? []
|
||||
)
|
||||
)
|
||||
|
||||
const isRefreshable = computed(() => refreshableWidgets.value.length > 0)
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { LogsWsMessage } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
const LOGS_MESSAGE_TYPE = 'logs'
|
||||
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
|
||||
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
|
||||
|
||||
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
|
||||
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
|
||||
|
||||
interface UseServerLogsOptions {
|
||||
ui_id: string
|
||||
immediate?: boolean
|
||||
messageFilter?: (message: string) => boolean
|
||||
}
|
||||
|
||||
export const useServerLogs = (options: UseServerLogsOptions = {}) => {
|
||||
export const useServerLogs = (options: UseServerLogsOptions) => {
|
||||
const {
|
||||
immediate = false,
|
||||
messageFilter = (msg: string) => Boolean(msg.trim())
|
||||
} = options
|
||||
|
||||
const logs = ref<string[]>([])
|
||||
let stop: ReturnType<typeof useEventListener> | null = null
|
||||
const isTaskStarted = ref(false)
|
||||
let stopLogs: ReturnType<typeof useEventListener> | null = null
|
||||
let stopTaskDone: ReturnType<typeof useEventListener> | null = null
|
||||
let stopTaskStarted: ReturnType<typeof useEventListener> | null = null
|
||||
|
||||
const isValidLogEvent = (event: CustomEvent<LogsWsMessage>) =>
|
||||
event?.type === LOGS_MESSAGE_TYPE && event.detail?.entries?.length > 0
|
||||
@@ -27,34 +37,81 @@ export const useServerLogs = (options: UseServerLogsOptions = {}) => {
|
||||
event.detail.entries.map((e) => e.m).filter(messageFilter)
|
||||
|
||||
const handleLogMessage = (event: CustomEvent<LogsWsMessage>) => {
|
||||
// Only capture logs if this task has started
|
||||
if (!isTaskStarted.value) return
|
||||
|
||||
if (isValidLogEvent(event)) {
|
||||
logs.value.push(...parseLogMessage(event))
|
||||
const messages = parseLogMessage(event)
|
||||
if (messages.length > 0) {
|
||||
logs.value.push(...messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
const handleTaskStarted = (event: CustomEvent<ManagerWsTaskStartedMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_STARTED_NAME) {
|
||||
// Check if this is our task starting
|
||||
const isOurTask = event.detail.ui_id === options.ui_id
|
||||
if (isOurTask) {
|
||||
isTaskStarted.value = true
|
||||
void stopTaskStarted?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskDone = (event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_DONE_NAME) {
|
||||
const { state } = event.detail
|
||||
// Check if our task is now in the history (completed)
|
||||
const isOurTaskDone = state.history[options.ui_id]
|
||||
if (isOurTaskDone) {
|
||||
isTaskStarted.value = false
|
||||
void stopListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startListening = async () => {
|
||||
await api.subscribeLogs(true)
|
||||
stop = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
|
||||
stopLogs = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
|
||||
stopTaskStarted = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_TASK_STARTED_NAME,
|
||||
handleTaskStarted
|
||||
)
|
||||
stopTaskDone = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_TASK_DONE_NAME,
|
||||
handleTaskDone
|
||||
)
|
||||
}
|
||||
|
||||
const stopListening = async () => {
|
||||
stop?.()
|
||||
stop = null
|
||||
await api.subscribeLogs(false)
|
||||
stopLogs?.()
|
||||
stopTaskStarted?.()
|
||||
stopTaskDone?.()
|
||||
stopLogs = null
|
||||
stopTaskStarted = null
|
||||
stopTaskDone = null
|
||||
// TODO: move subscribe/unsubscribe logs to useManagerQueue. Subscribe when task starts if not already subscribed.
|
||||
// Unsubscribe ONLY when there are no tasks running or queued up and the only remaining task finishes.
|
||||
// await api.subscribeLogs(false)
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
void start()
|
||||
void startListening()
|
||||
}
|
||||
|
||||
onUnmounted(async () => {
|
||||
const cleanup = async () => {
|
||||
await stopListening()
|
||||
logs.value = []
|
||||
})
|
||||
isTaskStarted.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
logs,
|
||||
startListening: start,
|
||||
stopListening
|
||||
startListening,
|
||||
stopListening,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,14 @@ import { ref } from 'vue'
|
||||
|
||||
import ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
ComponentWidgetImpl,
|
||||
type ComponentWidgetStandardProps,
|
||||
addWidget
|
||||
} from '@/scripts/domWidget'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
type ChatHistoryCustomProps = Omit<
|
||||
InstanceType<typeof ChatHistoryWidget>['$props'],
|
||||
ComponentWidgetStandardProps
|
||||
>
|
||||
|
||||
const PADDING = 16
|
||||
|
||||
export const useChatHistoryWidget = (
|
||||
options: {
|
||||
props?: ChatHistoryCustomProps
|
||||
props?: Omit<InstanceType<typeof ChatHistoryWidget>['$props'], 'widget'>
|
||||
} = {}
|
||||
) => {
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
@@ -29,7 +20,7 @@ export const useChatHistoryWidget = (
|
||||
const widgetValue = ref<string>('')
|
||||
const widget = new ComponentWidgetImpl<
|
||||
string | object,
|
||||
ChatHistoryCustomProps
|
||||
InstanceType<typeof ChatHistoryWidget>['$props']
|
||||
>({
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
|
||||
@@ -3,18 +3,9 @@ import { ref } from 'vue'
|
||||
|
||||
import TextPreviewWidget from '@/components/graph/widgets/TextPreviewWidget.vue'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
ComponentWidgetImpl,
|
||||
type ComponentWidgetStandardProps,
|
||||
addWidget
|
||||
} from '@/scripts/domWidget'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
type TextPreviewCustomProps = Omit<
|
||||
InstanceType<typeof TextPreviewWidget>['$props'],
|
||||
ComponentWidgetStandardProps
|
||||
>
|
||||
|
||||
const PADDING = 16
|
||||
|
||||
export const useTextPreviewWidget = (
|
||||
@@ -27,17 +18,11 @@ export const useTextPreviewWidget = (
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
const widgetValue = ref<string>('')
|
||||
const widget = new ComponentWidgetImpl<
|
||||
string | object,
|
||||
TextPreviewCustomProps
|
||||
>({
|
||||
const widget = new ComponentWidgetImpl<string | object>({
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: TextPreviewWidget,
|
||||
inputSpec,
|
||||
props: {
|
||||
nodeId: node.id
|
||||
},
|
||||
options: {
|
||||
getValue: () => widgetValue.value,
|
||||
setValue: (value: string | object) => {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"supports_preview_metadata": true
|
||||
"supports_preview_metadata": false
|
||||
}
|
||||
|
||||
@@ -11,9 +11,24 @@ export const CORE_MENU_COMMANDS = [
|
||||
]
|
||||
],
|
||||
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
|
||||
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
|
||||
[
|
||||
['Edit'],
|
||||
[
|
||||
'Comfy.RefreshNodeDefinitions',
|
||||
'Comfy.Memory.UnloadModels',
|
||||
'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
]
|
||||
],
|
||||
[['Edit'], ['Comfy.ClearWorkflow']],
|
||||
[['Edit'], ['Comfy.OpenClipspace']],
|
||||
[
|
||||
['Manager'],
|
||||
[
|
||||
'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
'Comfy.Manager.ShowMissingPacks',
|
||||
'Comfy.Manager.ShowUpdateAvailablePacks'
|
||||
]
|
||||
],
|
||||
[
|
||||
['Help'],
|
||||
[
|
||||
|
||||
@@ -14,6 +14,13 @@ import type { SettingParams } from '@/types/settingTypes'
|
||||
* when they are no longer needed.
|
||||
*/
|
||||
export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'Comfy.Memory.AllowManualUnload',
|
||||
name: 'Allow manual unload of models and execution cache via user command',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Validation.Workflows',
|
||||
name: 'Validate workflows',
|
||||
@@ -35,10 +42,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Action on link release (No modifier)',
|
||||
type: 'combo',
|
||||
options: Object.values(LinkReleaseTriggerAction),
|
||||
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU,
|
||||
defaultsByInstallVersion: {
|
||||
'1.24.1': LinkReleaseTriggerAction.SEARCH_BOX
|
||||
}
|
||||
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU
|
||||
},
|
||||
{
|
||||
id: 'Comfy.LinkRelease.ActionShift',
|
||||
@@ -46,10 +50,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Action on link release (Shift)',
|
||||
type: 'combo',
|
||||
options: Object.values(LinkReleaseTriggerAction),
|
||||
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX,
|
||||
defaultsByInstallVersion: {
|
||||
'1.24.1': LinkReleaseTriggerAction.CONTEXT_MENU
|
||||
}
|
||||
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeSearchBoxImpl.NodePreview',
|
||||
@@ -890,5 +891,12 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Release seen timestamp',
|
||||
type: 'hidden',
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.AllowManualUnload',
|
||||
name: 'Allow manual unload of models and execution cache via user command',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.0'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
@@ -1225,10 +1224,9 @@ export class GroupNodeHandler {
|
||||
node.onDrawForeground = function (ctx) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onDrawForeground?.apply?.(this, arguments)
|
||||
const progressState = useExecutionStore().nodeProgressStates[this.id]
|
||||
if (
|
||||
progressState &&
|
||||
progressState.state === 'running' &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
+app.runningNodeId === this.id &&
|
||||
this.runningInternalNodeId !== null
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -1342,7 +1340,6 @@ export class GroupNodeHandler {
|
||||
this.node.onRemoved = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onRemoved?.apply(this, arguments)
|
||||
// api.removeEventListener('progress_state', progress_state)
|
||||
api.removeEventListener('executing', executing)
|
||||
api.removeEventListener('executed', executed)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ async function handleModelUpload(files: FileList, node: any) {
|
||||
(w: any) => w.name === 'model_file'
|
||||
) as IStringWidget
|
||||
|
||||
node.properties['Texture'] = undefined
|
||||
|
||||
try {
|
||||
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
|
||||
|
||||
@@ -213,6 +215,8 @@ useExtensionService().registerExtension({
|
||||
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
|
||||
if (modelWidget) {
|
||||
modelWidget.value = ''
|
||||
|
||||
node.properties['Texture'] = undefined
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -160,6 +160,12 @@ class Load3DConfiguration {
|
||||
|
||||
this.load3d.setEdgeThreshold(edgeThreshold)
|
||||
|
||||
const texturePath = this.load3d.loadNodeProperty('Texture', null)
|
||||
|
||||
if (texturePath) {
|
||||
await this.load3d.applyTexture(texturePath)
|
||||
}
|
||||
|
||||
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
|
||||
try {
|
||||
this.load3d.setCameraState(cameraState)
|
||||
|
||||
@@ -293,6 +293,23 @@ class Load3d {
|
||||
}
|
||||
}
|
||||
|
||||
async applyTexture(texturePath: string): Promise<void> {
|
||||
if (!this.modelManager.currentModel) {
|
||||
throw new Error('No model to apply texture to')
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('textureLoadingStart', null)
|
||||
|
||||
try {
|
||||
await this.modelManager.applyTexture(texturePath)
|
||||
} catch (error) {
|
||||
console.error('Error applying texture:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.eventManager.emitEvent('textureLoadingEnd', null)
|
||||
}
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string): void {
|
||||
this.sceneManager.setBackgroundColor(color)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeome
|
||||
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils'
|
||||
|
||||
import Load3dUtils from './Load3dUtils'
|
||||
import { ColoredShadowMaterial } from './conditional-lines/ColoredShadowMaterial'
|
||||
import { ConditionalEdgesGeometry } from './conditional-lines/ConditionalEdgesGeometry'
|
||||
import { ConditionalEdgesShader } from './conditional-lines/ConditionalEdgesShader.js'
|
||||
@@ -134,6 +135,66 @@ export class ModelManager implements ModelManagerInterface {
|
||||
})
|
||||
}
|
||||
|
||||
async applyTexture(texturePath: string): Promise<void> {
|
||||
if (!this.currentModel) {
|
||||
throw new Error('No model available to apply texture to')
|
||||
}
|
||||
|
||||
if (this.appliedTexture) {
|
||||
this.appliedTexture.dispose()
|
||||
}
|
||||
|
||||
try {
|
||||
let imageUrl = Load3dUtils.getResourceURL(
|
||||
...Load3dUtils.splitFilePath(texturePath)
|
||||
)
|
||||
|
||||
if (!imageUrl.startsWith('/api')) {
|
||||
imageUrl = '/api' + imageUrl
|
||||
}
|
||||
|
||||
this.appliedTexture = await new Promise<THREE.Texture>(
|
||||
(resolve, reject) => {
|
||||
this.textureLoader.load(
|
||||
imageUrl,
|
||||
(texture) => {
|
||||
texture.colorSpace = THREE.SRGBColorSpace
|
||||
texture.wrapS = THREE.RepeatWrapping
|
||||
texture.wrapT = THREE.RepeatWrapping
|
||||
resolve(texture)
|
||||
},
|
||||
undefined,
|
||||
(error) => reject(error)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (this.materialMode === 'original') {
|
||||
this.currentModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
map: this.appliedTexture,
|
||||
metalness: 0.1,
|
||||
roughness: 0.8,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
|
||||
child.material = material
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
} catch (error) {
|
||||
console.error('Error applying texture:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
disposeLineartModel(): void {
|
||||
this.disposeEdgesModel()
|
||||
this.disposeShadowModel()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
@@ -10,8 +9,6 @@ import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type { DOMWidget } from '@/scripts/domWidget'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { NodeLocatorId } from '@/types'
|
||||
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
import { api } from '../../scripts/api'
|
||||
import { app } from '../../scripts/app'
|
||||
@@ -140,27 +137,14 @@ app.registerExtension({
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
}
|
||||
}
|
||||
|
||||
audioUIWidget.onRemove = useChainCallback(
|
||||
audioUIWidget.onRemove,
|
||||
() => {
|
||||
if (!audioUIWidget.element) return
|
||||
audioUIWidget.element.pause()
|
||||
audioUIWidget.element.src = ''
|
||||
audioUIWidget.element.remove()
|
||||
}
|
||||
)
|
||||
|
||||
return { widget: audioUIWidget }
|
||||
}
|
||||
}
|
||||
},
|
||||
onNodeOutputsUpdated(nodeOutputs: Record<NodeLocatorId, any>) {
|
||||
for (const [nodeLocatorId, output] of Object.entries(nodeOutputs)) {
|
||||
onNodeOutputsUpdated(nodeOutputs: Record<number, any>) {
|
||||
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
|
||||
const node = app.graph.getNodeById(nodeId)
|
||||
if ('audio' in output) {
|
||||
const node = getNodeByLocatorId(app.graph, nodeLocatorId)
|
||||
if (!node) continue
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const audioUIWidget = node.widgets.find(
|
||||
(w) => w.name === 'audioUI'
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Load Default Workflow"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Toggle the Custom Nodes Manager"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Custom Nodes Manager"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Install Missing"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Check for Updates"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Toggle the Custom Nodes Manager Progress Bar"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Open Mask Editor for Selected Node"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Unload Models"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Unload Models and Execution Cache"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "New Blank Workflow"
|
||||
},
|
||||
|
||||
@@ -134,10 +134,13 @@
|
||||
"releaseTitle": "{package} {version} Release",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"nodesRunning": "nodes running"
|
||||
"commandProhibited": "Command {command} is prohibited. Contact an administrator for more information."
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"legacyMenuNotAvailable": "Legacy manager menu is not available, defaulting to the new manager menu.",
|
||||
"legacyManagerUI": "Use Legacy UI",
|
||||
"legacyManagerUIDescription": "To use the legacy Manager UI, start ComfyUI with --enable-manager-legacy-ui",
|
||||
"failed": "Failed ({count})",
|
||||
"noNodesFound": "No nodes found",
|
||||
"noNodesFoundDescription": "The pack's nodes either could not be parsed, or the pack is a frontend extension only and doesn't have any nodes.",
|
||||
@@ -914,6 +917,7 @@
|
||||
"menuLabels": {
|
||||
"Workflow": "Workflow",
|
||||
"Edit": "Edit",
|
||||
"Manager": "Manager",
|
||||
"Help": "Help",
|
||||
"Check for Updates": "Check for Updates",
|
||||
"Open Custom Nodes Folder": "Open Custom Nodes Folder",
|
||||
@@ -969,6 +973,14 @@
|
||||
"Toggle the Custom Nodes Manager": "Toggle the Custom Nodes Manager",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
|
||||
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
|
||||
"Custom Nodes (Beta)": "Custom Nodes (Beta)",
|
||||
"Custom Nodes (Legacy)": "Custom Nodes (Legacy)",
|
||||
"Manager Menu (Legacy)": "Manager Menu (Legacy)",
|
||||
"Custom Nodes Manager": "Custom Nodes Manager",
|
||||
"Install Missing": "Install Missing",
|
||||
"Toggle Progress Dialog": "Toggle Progress Dialog",
|
||||
"Unload Models": "Unload Models",
|
||||
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
"Open": "Open",
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Cargar flujo de trabajo predeterminado"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Administrador de nodos personalizados"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Nodos personalizados (Beta)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Instalar faltantes"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Buscar actualizaciones"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Alternar diálogo de progreso del administrador"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Abrir editor de máscara para el nodo seleccionado"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Descargar modelos"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Descargar modelos y caché de ejecución"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nuevo flujo de trabajo en blanco"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "Color",
|
||||
"comingSoon": "Próximamente",
|
||||
"command": "Comando",
|
||||
"commandProhibited": "El comando {command} está prohibido. Contacta a un administrador para más información.",
|
||||
"community": "Comunidad",
|
||||
"completed": "Completado",
|
||||
"confirm": "Confirmar",
|
||||
@@ -336,7 +337,6 @@
|
||||
"noTasksFoundMessage": "No hay tareas en la cola.",
|
||||
"noWorkflowsFound": "No se encontraron flujos de trabajo.",
|
||||
"nodes": "Nodos",
|
||||
"nodesRunning": "nodos en ejecución",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Abrir nuevo problema",
|
||||
"overwrite": "Sobrescribir",
|
||||
@@ -629,6 +629,9 @@
|
||||
"installationQueue": "Cola de Instalación",
|
||||
"lastUpdated": "Última Actualización",
|
||||
"latestVersion": "Última",
|
||||
"legacyManagerUI": "Usar UI antigua",
|
||||
"legacyManagerUIDescription": "Para usar la UI antigua del Manager, inicia ComfyUI con --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "El menú del administrador antiguo no está disponible en esta versión de ComfyUI. Por favor, utiliza el nuevo menú del administrador en su lugar.",
|
||||
"license": "Licencia",
|
||||
"loadingVersions": "Cargando versiones...",
|
||||
"nightlyVersion": "Nocturna",
|
||||
@@ -735,6 +738,7 @@
|
||||
"Contact Support": "Contactar soporte",
|
||||
"Convert Selection to Subgraph": "Convertir selección en subgrafo",
|
||||
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
|
||||
"Custom Nodes Manager": "Administrador de Nodos Personalizados",
|
||||
"Delete Selected Items": "Eliminar elementos seleccionados",
|
||||
"Desktop User Guide": "Guía de usuario de escritorio",
|
||||
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
|
||||
@@ -746,6 +750,7 @@
|
||||
"Give Feedback": "Dar retroalimentación",
|
||||
"Group Selected Nodes": "Agrupar nodos seleccionados",
|
||||
"Help": "Ayuda",
|
||||
"Install Missing": "Instalar Faltantes",
|
||||
"Interrupt": "Interrumpir",
|
||||
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
|
||||
"Manage group nodes": "Gestionar nodos de grupo",
|
||||
@@ -753,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
|
||||
"Move Selected Nodes Right": "Mover nodos seleccionados hacia la derecha",
|
||||
"Move Selected Nodes Up": "Mover nodos seleccionados hacia arriba",
|
||||
"Manager": "Administrador",
|
||||
"Mute/Unmute Selected Nodes": "Silenciar/Activar sonido de nodos seleccionados",
|
||||
"New": "Nuevo",
|
||||
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
|
||||
@@ -797,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
|
||||
"Undo": "Deshacer",
|
||||
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
|
||||
"Unload Models": "Descargar modelos",
|
||||
"Unload Models and Execution Cache": "Descargar modelos y caché de ejecución",
|
||||
"Workflow": "Flujo de trabajo",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Charger le flux de travail par défaut"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Gestionnaire de Nœuds Personnalisés"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Nœuds personnalisés (Beta)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Installer manquants"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Vérifier les mises à jour"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Basculer la boîte de dialogue de progression"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Ouvrir l'éditeur de masque pour le nœud sélectionné"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Décharger les modèles"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Décharger les modèles et le cache d'exécution"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nouveau flux de travail vierge"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "Couleur",
|
||||
"comingSoon": "Bientôt disponible",
|
||||
"command": "Commande",
|
||||
"commandProhibited": "La commande {command} est interdite. Contactez un administrateur pour plus d'informations.",
|
||||
"community": "Communauté",
|
||||
"completed": "Terminé",
|
||||
"confirm": "Confirmer",
|
||||
@@ -336,7 +337,6 @@
|
||||
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
|
||||
"noWorkflowsFound": "Aucun flux de travail trouvé.",
|
||||
"nodes": "Nœuds",
|
||||
"nodesRunning": "nœuds en cours d’exécution",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Ouvrir un nouveau problème",
|
||||
"overwrite": "Écraser",
|
||||
@@ -629,6 +629,9 @@
|
||||
"installationQueue": "File d'attente d'installation",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"latestVersion": "Dernière",
|
||||
"legacyManagerUI": "Utiliser l'interface utilisateur héritée",
|
||||
"legacyManagerUIDescription": "Pour utiliser l'interface utilisateur de gestion héritée, démarrez ComfyUI avec --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "Le menu du gestionnaire de l'ancienne version n'est pas disponible dans cette version de ComfyUI. Veuillez utiliser le nouveau menu du gestionnaire à la place.",
|
||||
"license": "Licence",
|
||||
"loadingVersions": "Chargement des versions...",
|
||||
"nightlyVersion": "Nocturne",
|
||||
@@ -722,7 +725,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
||||
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
|
||||
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
|
||||
"Check for Updates": "Vérifier les mises à jour",
|
||||
"Check for Updates": "Vérifier les Mises à Jour",
|
||||
"Clear Pending Tasks": "Effacer les tâches en attente",
|
||||
"Clear Workflow": "Effacer le flux de travail",
|
||||
"Clipspace": "Espace de clip",
|
||||
@@ -735,6 +738,7 @@
|
||||
"Contact Support": "Contacter le support",
|
||||
"Convert Selection to Subgraph": "Convertir la sélection en sous-graphe",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
|
||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
@@ -746,6 +750,7 @@
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Install Missing": "Installer Manquants",
|
||||
"Interrupt": "Interrompre",
|
||||
"Load Default Workflow": "Charger le flux de travail par défaut",
|
||||
"Manage group nodes": "Gérer les nœuds de groupe",
|
||||
@@ -753,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
|
||||
"Move Selected Nodes Right": "Déplacer les nœuds sélectionnés vers la droite",
|
||||
"Move Selected Nodes Up": "Déplacer les nœuds sélectionnés vers le haut",
|
||||
"Manager": "Gestionnaire",
|
||||
"Mute/Unmute Selected Nodes": "Mettre en sourdine/Activer le son des nœuds sélectionnés",
|
||||
"New": "Nouveau",
|
||||
"Next Opened Workflow": "Prochain flux de travail ouvert",
|
||||
@@ -797,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
|
||||
"Undo": "Annuler",
|
||||
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
|
||||
"Unload Models": "Décharger les modèles",
|
||||
"Unload Models and Execution Cache": "Décharger les modèles et le cache d'exécution",
|
||||
"Workflow": "Flux de travail",
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "デフォルトのワークフローを読み込む"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "カスタムノードマネージャ"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "カスタムノード(ベータ版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "不足しているパックをインストール"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "更新を確認"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "プログレスダイアログの切り替え"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "選択したノードのマスクエディタを開く"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "モデルのアンロード"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "モデルと実行キャッシュのアンロード"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新しい空のワークフロー"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "色",
|
||||
"comingSoon": "近日公開",
|
||||
"command": "コマンド",
|
||||
"commandProhibited": "コマンド {command} は禁止されています。詳細は管理者にお問い合わせください。",
|
||||
"community": "コミュニティ",
|
||||
"completed": "完了",
|
||||
"confirm": "確認",
|
||||
@@ -336,7 +337,6 @@
|
||||
"noTasksFoundMessage": "キューにタスクがありません。",
|
||||
"noWorkflowsFound": "ワークフローが見つかりません。",
|
||||
"nodes": "ノード",
|
||||
"nodesRunning": "ノードが実行中",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "新しい問題を開く",
|
||||
"overwrite": "上書き",
|
||||
@@ -629,6 +629,9 @@
|
||||
"installationQueue": "インストールキュー",
|
||||
"lastUpdated": "最終更新日",
|
||||
"latestVersion": "最新",
|
||||
"legacyManagerUI": "レガシーUIを使用する",
|
||||
"legacyManagerUIDescription": "レガシーManager UIを使用するには、--enable-manager-legacy-uiを付けてComfyUIを起動してください",
|
||||
"legacyMenuNotAvailable": "このバージョンのComfyUIでは、レガシーマネージャーメニューは利用できません。新しいマネージャーメニューを使用してください。",
|
||||
"license": "ライセンス",
|
||||
"loadingVersions": "バージョンを読み込んでいます...",
|
||||
"nightlyVersion": "ナイトリー",
|
||||
@@ -722,7 +725,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
||||
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
||||
"Check for Updates": "更新を確認する",
|
||||
"Check for Updates": "更新を確認",
|
||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||
"Clear Workflow": "ワークフローをクリア",
|
||||
"Clipspace": "クリップスペース",
|
||||
@@ -735,6 +738,7 @@
|
||||
"Contact Support": "サポートに連絡",
|
||||
"Convert Selection to Subgraph": "選択範囲をサブグラフに変換",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Custom Nodes Manager": "カスタムノードマネージャ",
|
||||
"Delete Selected Items": "選択したアイテムを削除",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
@@ -746,6 +750,7 @@
|
||||
"Give Feedback": "フィードバックを送る",
|
||||
"Group Selected Nodes": "選択したノードをグループ化",
|
||||
"Help": "ヘルプ",
|
||||
"Install Missing": "不足しているものをインストール",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "デフォルトワークフローを読み込む",
|
||||
"Manage group nodes": "グループノードを管理",
|
||||
@@ -753,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "選択したノードを左へ移動",
|
||||
"Move Selected Nodes Right": "選択したノードを右へ移動",
|
||||
"Move Selected Nodes Up": "選択したノードを上へ移動",
|
||||
"Manager": "マネージャー",
|
||||
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
|
||||
"New": "新規",
|
||||
"Next Opened Workflow": "次に開いたワークフロー",
|
||||
@@ -797,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
|
||||
"Undo": "元に戻す",
|
||||
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
|
||||
"Unload Models": "モデルのアンロード",
|
||||
"Unload Models and Execution Cache": "モデルと実行キャッシュのアンロード",
|
||||
"Workflow": "ワークフロー",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "기본 워크플로 로드"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "사용자 정의 노드 관리자"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "사용자 정의 노드 (베타)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "누락된 팩 설치"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "업데이트 확인"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "진행 상황 대화 상자 전환"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "선택한 노드 마스크 편집기 열기"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "모델 언로드"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "모델 및 실행 캐시 언로드"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "새로운 빈 워크플로"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "색상",
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
"commandProhibited": "명령 {command}은 금지되었습니다. 자세한 정보는 관리자에게 문의하십시오.",
|
||||
"community": "커뮤니티",
|
||||
"completed": "완료됨",
|
||||
"confirm": "확인",
|
||||
@@ -336,7 +337,6 @@
|
||||
"noTasksFoundMessage": "대기열에 작업이 없습니다.",
|
||||
"noWorkflowsFound": "워크플로를 찾을 수 없습니다.",
|
||||
"nodes": "노드",
|
||||
"nodesRunning": "노드 실행 중",
|
||||
"ok": "확인",
|
||||
"openNewIssue": "새 문제 열기",
|
||||
"overwrite": "덮어쓰기",
|
||||
@@ -629,6 +629,9 @@
|
||||
"installationQueue": "설치 대기열",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"latestVersion": "최신",
|
||||
"legacyManagerUI": "레거시 UI 사용",
|
||||
"legacyManagerUIDescription": "레거시 매니저 UI를 사용하려면, ComfyUI를 --enable-manager-legacy-ui로 시작하세요",
|
||||
"legacyMenuNotAvailable": "이 버전의 ComfyUI에서는 레거시 매니저 메뉴를 사용할 수 없습니다. 대신 새로운 매니저 메뉴를 사용하십시오.",
|
||||
"license": "라이선스",
|
||||
"loadingVersions": "버전 로딩 중...",
|
||||
"nightlyVersion": "최신 테스트 버전(nightly)",
|
||||
@@ -735,6 +738,7 @@
|
||||
"Contact Support": "고객 지원 문의",
|
||||
"Convert Selection to Subgraph": "선택 영역을 서브그래프로 변환",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Custom Nodes Manager": "사용자 정의 노드 관리자",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
@@ -746,6 +750,7 @@
|
||||
"Give Feedback": "피드백 제공",
|
||||
"Group Selected Nodes": "선택한 노드 그룹화",
|
||||
"Help": "도움말",
|
||||
"Install Missing": "누락된 설치",
|
||||
"Interrupt": "중단",
|
||||
"Load Default Workflow": "기본 워크플로 불러오기",
|
||||
"Manage group nodes": "그룹 노드 관리",
|
||||
@@ -753,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
|
||||
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
|
||||
"Move Selected Nodes Up": "선택한 노드 위로 이동",
|
||||
"Manager": "매니저",
|
||||
"Mute/Unmute Selected Nodes": "선택한 노드 활성화/비활성화",
|
||||
"New": "새로 만들기",
|
||||
"Next Opened Workflow": "다음 열린 워크플로",
|
||||
@@ -797,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
|
||||
"Undo": "실행 취소",
|
||||
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
|
||||
"Unload Models": "모델 언로드",
|
||||
"Unload Models and Execution Cache": "모델 및 실행 캐시 언로드",
|
||||
"Workflow": "워크플로",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Загрузить стандартный рабочий процесс"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Менеджер Пользовательских Узлов"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Пользовательские узлы (Бета)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Установить отсутствующие"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Проверить наличие обновлений"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Переключить диалоговое окно прогресса"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Открыть редактор масок для выбранной ноды"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Выгрузить модели"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Выгрузить модели и кэш выполнения"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Новый пустой рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "Цвет",
|
||||
"comingSoon": "Скоро будет",
|
||||
"command": "Команда",
|
||||
"commandProhibited": "Команда {command} запрещена. Свяжитесь с администратором для получения дополнительной информации.",
|
||||
"community": "Сообщество",
|
||||
"completed": "Завершено",
|
||||
"confirm": "Подтвердить",
|
||||
@@ -336,7 +337,6 @@
|
||||
"noTasksFoundMessage": "В очереди нет задач.",
|
||||
"noWorkflowsFound": "Рабочие процессы не найдены.",
|
||||
"nodes": "Узлы",
|
||||
"nodesRunning": "запущено узлов",
|
||||
"ok": "ОК",
|
||||
"openNewIssue": "Открыть новую проблему",
|
||||
"overwrite": "Перезаписать",
|
||||
@@ -629,6 +629,9 @@
|
||||
"installationQueue": "Очередь установки",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"latestVersion": "Последняя",
|
||||
"legacyManagerUI": "Использовать устаревший UI",
|
||||
"legacyManagerUIDescription": "Чтобы использовать устаревший UI менеджера, запустите ComfyUI с --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "Устаревшее меню менеджера недоступно в этой версии ComfyUI. Пожалуйста, используйте новое меню менеджера.",
|
||||
"license": "Лицензия",
|
||||
"loadingVersions": "Загрузка версий...",
|
||||
"nightlyVersion": "Ночная",
|
||||
@@ -722,7 +725,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||
"Check for Updates": "Проверить наличие обновлений",
|
||||
"Check for Updates": "Проверить Обновления",
|
||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||
"Clear Workflow": "Очистить рабочий процесс",
|
||||
"Clipspace": "Клиппространство",
|
||||
@@ -735,6 +738,7 @@
|
||||
"Contact Support": "Связаться с поддержкой",
|
||||
"Convert Selection to Subgraph": "Преобразовать выделенное в подграф",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
|
||||
"Delete Selected Items": "Удалить выбранные элементы",
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
@@ -746,6 +750,7 @@
|
||||
"Give Feedback": "Оставить отзыв",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные ноды",
|
||||
"Help": "Помощь",
|
||||
"Install Missing": "Установить Отсутствующие",
|
||||
"Interrupt": "Прервать",
|
||||
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
|
||||
"Manage group nodes": "Управление групповыми нодами",
|
||||
@@ -753,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
|
||||
"Move Selected Nodes Right": "Переместить выбранные узлы вправо",
|
||||
"Move Selected Nodes Up": "Переместить выбранные узлы вверх",
|
||||
"Manager": "Менеджер",
|
||||
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
|
||||
"New": "Новый",
|
||||
"Next Opened Workflow": "Следующий открытый рабочий процесс",
|
||||
@@ -797,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
|
||||
"Undo": "Отменить",
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
|
||||
"Unload Models": "Выгрузить модели",
|
||||
"Unload Models and Execution Cache": "Выгрузить модели и кэш выполнения",
|
||||
"Workflow": "Рабочий процесс",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить"
|
||||
|
||||
@@ -152,8 +152,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "載入預設工作流程"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "切換自訂節點管理器"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "顯示自訂節點管理器"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "自訂節點(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "管理選單(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "安裝缺少的自訂節點"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "檢查自訂節點更新"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切換自訂節點管理器進度條"
|
||||
@@ -161,6 +173,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "為選取的節點開啟 Mask 編輯器"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "卸載模型"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "卸載模型與執行快取"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新增空白工作流程"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "顏色",
|
||||
"comingSoon": "即將推出",
|
||||
"command": "指令",
|
||||
"commandProhibited": "指令 {command} 已被禁止。如需更多資訊,請聯絡管理員。",
|
||||
"community": "社群",
|
||||
"completed": "已完成",
|
||||
"confirm": "確認",
|
||||
@@ -336,7 +337,6 @@
|
||||
"noTasksFoundMessage": "佇列中沒有任務。",
|
||||
"noWorkflowsFound": "找不到工作流程。",
|
||||
"nodes": "節點",
|
||||
"nodesRunning": "節點執行中",
|
||||
"ok": "確定",
|
||||
"openNewIssue": "開啟新問題",
|
||||
"overwrite": "覆蓋",
|
||||
@@ -610,12 +610,14 @@
|
||||
"title": "維護"
|
||||
},
|
||||
"manager": {
|
||||
"applyChanges": "套用變更",
|
||||
"changingVersion": "正在將版本從 {from} 變更為 {to}",
|
||||
"createdBy": "建立者",
|
||||
"dependencies": "相依套件",
|
||||
"discoverCommunityContent": "探索社群製作的節點包、擴充功能等...",
|
||||
"downloads": "下載次數",
|
||||
"errorConnecting": "連線至 Comfy Node Registry 時發生錯誤。",
|
||||
"extensionsSuccessfullyInstalled": "擴充功能安裝成功,已可使用!",
|
||||
"failed": "失敗({count})",
|
||||
"filter": {
|
||||
"disabled": "已停用",
|
||||
@@ -627,8 +629,12 @@
|
||||
"installAllMissingNodes": "安裝所有缺少的節點",
|
||||
"installSelected": "安裝所選項目",
|
||||
"installationQueue": "安裝佇列",
|
||||
"installingDependencies": "正在安裝相依套件……",
|
||||
"lastUpdated": "最後更新",
|
||||
"latestVersion": "最新版本",
|
||||
"legacyManagerUI": "使用舊版介面",
|
||||
"legacyManagerUIDescription": "若要使用舊版管理介面,請以 --enable-manager-legacy-ui 啟動 ComfyUI",
|
||||
"legacyMenuNotAvailable": "舊版管理選單不可用,已預設切換至新版管理選單。",
|
||||
"license": "授權條款",
|
||||
"loadingVersions": "正在載入版本...",
|
||||
"nightlyVersion": "每夜建置版",
|
||||
@@ -640,6 +646,7 @@
|
||||
"packsSelected": "已選擇套件",
|
||||
"repository": "儲存庫",
|
||||
"restartToApplyChanges": "請重新啟動 ComfyUI 以套用變更",
|
||||
"restartingBackend": "正在重新啟動後端以套用變更……",
|
||||
"searchPlaceholder": "搜尋",
|
||||
"selectVersion": "選擇版本",
|
||||
"sort": {
|
||||
@@ -664,6 +671,8 @@
|
||||
"uninstallSelected": "解除安裝所選項目",
|
||||
"uninstalling": "正在解除安裝",
|
||||
"update": "更新",
|
||||
"updateAll": "全部更新",
|
||||
"updateSelected": "更新所選項目",
|
||||
"updatingAllPacks": "正在更新所有套件",
|
||||
"version": "版本"
|
||||
},
|
||||
@@ -722,6 +731,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||
"Canvas Toggle Link Visibility": "切換連結可見性",
|
||||
"Canvas Toggle Lock": "切換畫布鎖定",
|
||||
"Check for Custom Node Updates": "檢查自訂節點更新",
|
||||
"Check for Updates": "檢查更新",
|
||||
"Clear Pending Tasks": "清除待處理任務",
|
||||
"Clear Workflow": "清除工作流程",
|
||||
@@ -735,6 +745,7 @@
|
||||
"Contact Support": "聯絡支援",
|
||||
"Convert Selection to Subgraph": "將選取內容轉為子圖",
|
||||
"Convert selected nodes to group node": "將選取節點轉為群組節點",
|
||||
"Custom Nodes (Legacy)": "自訂節點(舊版)",
|
||||
"Delete Selected Items": "刪除選取項目",
|
||||
"Desktop User Guide": "桌面應用程式使用指南",
|
||||
"Duplicate Current Workflow": "複製目前工作流程",
|
||||
@@ -746,9 +757,12 @@
|
||||
"Give Feedback": "提供意見回饋",
|
||||
"Group Selected Nodes": "群組選取節點",
|
||||
"Help": "說明",
|
||||
"Install Missing Custom Nodes": "安裝缺少的自訂節點",
|
||||
"Interrupt": "中斷",
|
||||
"Load Default Workflow": "載入預設工作流程",
|
||||
"Manage group nodes": "管理群組節點",
|
||||
"Manager": "管理員",
|
||||
"Manager Menu (Legacy)": "管理員選單(舊版)",
|
||||
"Move Selected Nodes Down": "選取節點下移",
|
||||
"Move Selected Nodes Left": "選取節點左移",
|
||||
"Move Selected Nodes Right": "選取節點右移",
|
||||
@@ -782,6 +796,7 @@
|
||||
"Save": "儲存",
|
||||
"Save As": "另存新檔",
|
||||
"Show Settings Dialog": "顯示設定對話框",
|
||||
"Show the Custom Nodes Manager": "顯示自訂節點管理員",
|
||||
"Sign Out": "登出",
|
||||
"Toggle Bottom Panel": "切換下方面板",
|
||||
"Toggle Focus Mode": "切換專注模式",
|
||||
@@ -793,10 +808,11 @@
|
||||
"Toggle Terminal Bottom Panel": "切換終端機底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切換主題(深色/淺色)",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle the Custom Nodes Manager": "切換自訂節點管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
|
||||
"Undo": "復原",
|
||||
"Ungroup selected group nodes": "取消群組選取的群組節點",
|
||||
"Unload Models": "卸載模型",
|
||||
"Unload Models and Execution Cache": "卸載模型與執行快取",
|
||||
"Workflow": "工作流程",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "加载默认工作流"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "自定义节点管理器"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "自定义节点(测试版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "安装缺失的包"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "检查更新"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切换进度对话框"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "打开选中节点的遮罩编辑器"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "卸载模型"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "卸载模型和执行缓存"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新建空白工作流"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "颜色",
|
||||
"comingSoon": "即将推出",
|
||||
"command": "指令",
|
||||
"commandProhibited": "命令 {command} 被禁止。请联系管理员获取更多信息。",
|
||||
"community": "社区",
|
||||
"completed": "已完成",
|
||||
"confirm": "确认",
|
||||
@@ -336,7 +337,6 @@
|
||||
"noTasksFoundMessage": "队列中没有任务。",
|
||||
"noWorkflowsFound": "未找到工作流。",
|
||||
"nodes": "节点",
|
||||
"nodesRunning": "节点正在运行",
|
||||
"ok": "确定",
|
||||
"openNewIssue": "打开新问题",
|
||||
"overwrite": "覆盖",
|
||||
@@ -629,6 +629,9 @@
|
||||
"installationQueue": "安装队列",
|
||||
"lastUpdated": "最后更新",
|
||||
"latestVersion": "最新",
|
||||
"legacyManagerUI": "使用旧版UI",
|
||||
"legacyManagerUIDescription": "要使用旧版的管理器UI,请启动ComfyUI并使用 --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "在此版本的ComfyUI中,不提供旧版的管理器菜单。请使用新的管理器菜单。",
|
||||
"license": "许可证",
|
||||
"loadingVersions": "正在加载版本...",
|
||||
"nightlyVersion": "每夜",
|
||||
@@ -735,6 +738,7 @@
|
||||
"Contact Support": "联系支持",
|
||||
"Convert Selection to Subgraph": "将选中内容转换为子图",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Custom Nodes Manager": "自定义节点管理器",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
@@ -746,6 +750,7 @@
|
||||
"Give Feedback": "提供反馈",
|
||||
"Group Selected Nodes": "将选中节点转换为组节点",
|
||||
"Help": "帮助",
|
||||
"Install Missing": "安装缺失",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "加载默认工作流",
|
||||
"Manage group nodes": "管理组节点",
|
||||
@@ -753,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "左移所选节点",
|
||||
"Move Selected Nodes Right": "右移所选节点",
|
||||
"Move Selected Nodes Up": "上移所选节点",
|
||||
"Manager": "管理器",
|
||||
"Mute/Unmute Selected Nodes": "静音/取消静音选定节点",
|
||||
"New": "新建",
|
||||
"Next Opened Workflow": "下一个打开的工作流",
|
||||
@@ -786,17 +792,19 @@
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Model Library Sidebar": "切换模型库侧边栏",
|
||||
"Toggle Node Library Sidebar": "切换节点库侧边栏",
|
||||
"Toggle Queue Sidebar": "切换队列侧边栏",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle Workflows Sidebar": "切换工作流侧边栏",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
"Ungroup selected group nodes": "解散选中组节点",
|
||||
"Unload Models": "卸载模型",
|
||||
"Unload Models and Execution Cache": "卸载模型和执行缓存",
|
||||
"Workflow": "工作流",
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面"
|
||||
|
||||
@@ -48,22 +48,6 @@ const zProgressWsMessage = z.object({
|
||||
node: zNodeId
|
||||
})
|
||||
|
||||
const zNodeProgressState = z.object({
|
||||
value: z.number(),
|
||||
max: z.number(),
|
||||
state: z.enum(['pending', 'running', 'finished', 'error']),
|
||||
node_id: zNodeId,
|
||||
prompt_id: zPromptId,
|
||||
display_node_id: zNodeId.optional(),
|
||||
parent_node_id: zNodeId.optional(),
|
||||
real_node_id: zNodeId.optional()
|
||||
})
|
||||
|
||||
const zProgressStateWsMessage = z.object({
|
||||
prompt_id: zPromptId,
|
||||
nodes: z.record(zNodeId, zNodeProgressState)
|
||||
})
|
||||
|
||||
const zExecutingWsMessage = z.object({
|
||||
node: zNodeId,
|
||||
display_node: zNodeId,
|
||||
@@ -150,8 +134,6 @@ export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
|
||||
export type DisplayComponentWsMessage = z.infer<
|
||||
typeof zDisplayComponentWsMessage
|
||||
>
|
||||
export type NodeProgressState = z.infer<typeof zNodeProgressState>
|
||||
export type ProgressStateWsMessage = z.infer<typeof zProgressStateWsMessage>
|
||||
export type FeatureFlagsWsMessage = z.infer<typeof zFeatureFlagsWsMessage>
|
||||
// End of ws messages
|
||||
|
||||
@@ -492,6 +474,7 @@ const zSettings = z.object({
|
||||
'Comfy.Load3D.LightIntensityMinimum': z.number(),
|
||||
'Comfy.Load3D.LightAdjustmentIncrement': z.number(),
|
||||
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
|
||||
'Comfy.Memory.AllowManualUnload': z.boolean(),
|
||||
'pysssss.SnapToGrid': z.boolean(),
|
||||
/** VHS setting is used for queue video preview support. */
|
||||
'VHS.AdvancedPreviews': z.string(),
|
||||
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
PendingTaskItem,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage,
|
||||
PromptResponse,
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
type ComfyNodeDef,
|
||||
validateComfyNodeDef
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
||||
|
||||
interface QueuePromptRequestBody {
|
||||
@@ -106,17 +106,7 @@ interface BackendApiCalls {
|
||||
logs: LogsWsMessage
|
||||
/** Binary preview/progress data */
|
||||
b_preview: Blob
|
||||
/** Binary preview with metadata (node_id, prompt_id) */
|
||||
b_preview_with_metadata: {
|
||||
blob: Blob
|
||||
nodeId: string
|
||||
parentNodeId: string
|
||||
displayNodeId: string
|
||||
realNodeId: string
|
||||
promptId: string
|
||||
}
|
||||
progress_text: ProgressTextWsMessage
|
||||
progress_state: ProgressStateWsMessage
|
||||
display_component: DisplayComponentWsMessage
|
||||
feature_flags: FeatureFlagsWsMessage
|
||||
}
|
||||
@@ -468,33 +458,6 @@ export class ComfyApi extends EventTarget {
|
||||
})
|
||||
this.dispatchCustomEvent('b_preview', imageBlob)
|
||||
break
|
||||
case 4:
|
||||
// PREVIEW_IMAGE_WITH_METADATA
|
||||
const decoder4 = new TextDecoder()
|
||||
const metadataLength = view.getUint32(4)
|
||||
const metadataBytes = event.data.slice(8, 8 + metadataLength)
|
||||
const metadata = JSON.parse(decoder4.decode(metadataBytes))
|
||||
const imageData4 = event.data.slice(8 + metadataLength)
|
||||
|
||||
let imageMime4 = metadata.image_type
|
||||
|
||||
const imageBlob4 = new Blob([imageData4], {
|
||||
type: imageMime4
|
||||
})
|
||||
|
||||
// Dispatch enhanced preview event with metadata
|
||||
this.dispatchCustomEvent('b_preview_with_metadata', {
|
||||
blob: imageBlob4,
|
||||
nodeId: metadata.node_id,
|
||||
displayNodeId: metadata.display_node_id,
|
||||
parentNodeId: metadata.parent_node_id,
|
||||
realNodeId: metadata.real_node_id,
|
||||
promptId: metadata.prompt_id
|
||||
})
|
||||
|
||||
// Also dispatch legacy b_preview for backward compatibility
|
||||
this.dispatchCustomEvent('b_preview', imageBlob4)
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown binary websocket message of type ${eventType}`
|
||||
@@ -524,7 +487,6 @@ export class ComfyApi extends EventTarget {
|
||||
case 'execution_cached':
|
||||
case 'execution_success':
|
||||
case 'progress':
|
||||
case 'progress_state':
|
||||
case 'executed':
|
||||
case 'graphChanged':
|
||||
case 'promptQueued':
|
||||
@@ -1026,6 +988,56 @@ export class ComfyApi extends EventTarget {
|
||||
return (await axios.get(this.internalURL('/folder_paths'))).data
|
||||
}
|
||||
|
||||
/* Frees memory by unloading models and optionally freeing execution cache
|
||||
* @param {Object} options - The options object
|
||||
* @param {boolean} options.freeExecutionCache - If true, also frees execution cache
|
||||
*/
|
||||
async freeMemory(options: { freeExecutionCache: boolean }) {
|
||||
try {
|
||||
let mode = ''
|
||||
if (options.freeExecutionCache) {
|
||||
mode = '{"unload_models": true, "free_memory": true}'
|
||||
} else {
|
||||
mode = '{"unload_models": true}'
|
||||
}
|
||||
|
||||
const res = await this.fetchApi(`/free`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: mode
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
if (options.freeExecutionCache) {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models and Execution Cache have been cleared.',
|
||||
life: 3000
|
||||
})
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models have been unloaded.',
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: 'An error occurred while trying to unload models.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom nodes i18n data from the server.
|
||||
*
|
||||
|
||||
@@ -47,7 +47,6 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
@@ -61,10 +60,6 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
|
||||
import { ExtensionManager } from '@/types/extensionTypes'
|
||||
import { ColorAdjustOptions, adjustColor } from '@/utils/colorUtil'
|
||||
import { graphToPrompt } from '@/utils/executionUtil'
|
||||
import {
|
||||
getNodeByExecutionId,
|
||||
triggerCallbackOnAllNodes
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import {
|
||||
executeWidgetsCallback,
|
||||
fixLinkInputSlots,
|
||||
@@ -199,8 +194,6 @@ export class ComfyApp {
|
||||
|
||||
/**
|
||||
* @deprecated Use useExecutionStore().executingNodeId instead
|
||||
* TODO: Update to support multiple executing nodes. This getter returns only the first executing node.
|
||||
* Consider updating consumers to handle multiple nodes or use executingNodeIds array.
|
||||
*/
|
||||
get runningNodeId(): NodeId | null {
|
||||
return useExecutionStore().executingNodeId
|
||||
@@ -642,24 +635,36 @@ export class ComfyApp {
|
||||
|
||||
api.addEventListener('executing', () => {
|
||||
this.graph.setDirtyCanvas(true, false)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.revokePreviews(this.runningNodeId)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
delete this.nodePreviewImages[this.runningNodeId]
|
||||
})
|
||||
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const executionId = String(detail.display_node || detail.node)
|
||||
|
||||
nodeOutputStore.setNodeOutputsByExecutionId(executionId, detail.output, {
|
||||
merge: detail.merge
|
||||
})
|
||||
|
||||
const node = getNodeByExecutionId(this.graph, executionId)
|
||||
if (node && node.onExecuted) {
|
||||
node.onExecuted(detail.output)
|
||||
const output = this.nodeOutputs[detail.display_node || detail.node]
|
||||
if (detail.merge && output) {
|
||||
for (const k in detail.output ?? {}) {
|
||||
const v = output[k]
|
||||
if (v instanceof Array) {
|
||||
output[k] = v.concat(detail.output[k])
|
||||
} else {
|
||||
output[k] = detail.output[k]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.nodeOutputs[detail.display_node || detail.node] = detail.output
|
||||
}
|
||||
const node = this.graph.getNodeById(detail.display_node || detail.node)
|
||||
if (node) {
|
||||
if (node.onExecuted) node.onExecuted(detail.output)
|
||||
}
|
||||
})
|
||||
|
||||
api.addEventListener('execution_start', () => {
|
||||
triggerCallbackOnAllNodes(this.graph, 'onExecutionStart')
|
||||
this.graph.nodes.forEach((node) => {
|
||||
if (node.onExecutionStart) node.onExecutionStart()
|
||||
})
|
||||
})
|
||||
|
||||
api.addEventListener('execution_error', ({ detail }) => {
|
||||
@@ -684,16 +689,15 @@ export class ComfyApp {
|
||||
this.canvas.draw(true, true)
|
||||
})
|
||||
|
||||
api.addEventListener('b_preview_with_metadata', ({ detail }) => {
|
||||
// Enhanced preview with explicit node context
|
||||
const { blob, displayNodeId } = detail
|
||||
const { setNodePreviewsByExecutionId, revokePreviewsByExecutionId } =
|
||||
useNodeOutputStore()
|
||||
// Ensure clean up if `executing` event is missed.
|
||||
revokePreviewsByExecutionId(displayNodeId)
|
||||
api.addEventListener('b_preview', ({ detail }) => {
|
||||
const id = this.runningNodeId
|
||||
if (id == null) return
|
||||
|
||||
const blob = detail
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
// Preview cleanup is handled in progress_state event to support multiple concurrent previews
|
||||
setNodePreviewsByExecutionId(displayNodeId, [blobUrl])
|
||||
// Ensure clean up if `executing` event is missed.
|
||||
this.revokePreviews(id)
|
||||
this.nodePreviewImages[id] = [blobUrl]
|
||||
})
|
||||
|
||||
api.init()
|
||||
@@ -1672,13 +1676,25 @@ export class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees memory allocated to image preview blobs for a specific node, by revoking the URLs associated with them.
|
||||
* @param nodeId ID of the node to revoke all preview images of
|
||||
*/
|
||||
revokePreviews(nodeId: NodeId) {
|
||||
if (!this.nodePreviewImages[nodeId]?.[Symbol.iterator]) return
|
||||
for (const url of this.nodePreviewImages[nodeId]) {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clean current state
|
||||
*/
|
||||
clean() {
|
||||
this.nodeOutputs = {}
|
||||
const { revokeAllPreviews } = useNodeOutputStore()
|
||||
revokeAllPreviews()
|
||||
for (const id of Object.keys(this.nodePreviewImages)) {
|
||||
this.revokePreviews(id)
|
||||
}
|
||||
this.nodePreviewImages = {}
|
||||
const executionStore = useExecutionStore()
|
||||
executionStore.lastNodeErrors = null
|
||||
executionStore.lastExecutionError = null
|
||||
|
||||
@@ -44,30 +44,12 @@ export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
||||
inputEl?: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional props that can be passed to component widgets.
|
||||
* These are in addition to the standard props that are always provided:
|
||||
* - modelValue: The widget's value (handled by v-model)
|
||||
* - widget: Reference to the widget instance
|
||||
* - onUpdate:modelValue: The update handler for v-model
|
||||
*/
|
||||
export type ComponentWidgetCustomProps = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Standard props that are handled separately by DomWidget.vue and should be
|
||||
* omitted when defining custom props for component widgets
|
||||
*/
|
||||
export type ComponentWidgetStandardProps =
|
||||
| 'modelValue'
|
||||
| 'widget'
|
||||
| 'onUpdate:modelValue'
|
||||
|
||||
/**
|
||||
* A DOM widget that wraps a Vue component as a litegraph widget.
|
||||
*/
|
||||
export interface ComponentWidget<
|
||||
V extends object | string,
|
||||
P extends ComponentWidgetCustomProps = ComponentWidgetCustomProps
|
||||
P = Record<string, unknown>
|
||||
> extends BaseDOMWidget<V> {
|
||||
readonly component: Component
|
||||
readonly inputSpec: InputSpec
|
||||
@@ -176,18 +158,6 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
override onRemove(): void {
|
||||
useDomWidgetStore().unregisterWidget(this.id)
|
||||
}
|
||||
|
||||
override createCopyForNode(node: LGraphNode): this {
|
||||
// @ts-expect-error
|
||||
const cloned: this = new (this.constructor as typeof this)({
|
||||
node: node,
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
options: this.options
|
||||
})
|
||||
cloned.value = this.value
|
||||
return cloned
|
||||
}
|
||||
}
|
||||
|
||||
export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
@@ -207,19 +177,6 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
this.element = obj.element
|
||||
}
|
||||
|
||||
override createCopyForNode(node: LGraphNode): this {
|
||||
// @ts-expect-error
|
||||
const cloned: this = new (this.constructor as typeof this)({
|
||||
node: node,
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
element: this.element, // Include the element!
|
||||
options: this.options
|
||||
})
|
||||
cloned.value = this.value
|
||||
return cloned
|
||||
}
|
||||
|
||||
/** Extract DOM widget size info */
|
||||
override computeLayoutSize(node: LGraphNode) {
|
||||
if (this.type === 'hidden') {
|
||||
@@ -265,7 +222,7 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
|
||||
export class ComponentWidgetImpl<
|
||||
V extends object | string,
|
||||
P extends ComponentWidgetCustomProps = ComponentWidgetCustomProps
|
||||
P = Record<string, unknown>
|
||||
>
|
||||
extends BaseDOMWidgetImpl<V>
|
||||
implements ComponentWidget<V, P>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import axios, { AxiosError, AxiosResponse } from 'axios'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import {
|
||||
type InstallPackParams,
|
||||
type InstalledPacksResponse,
|
||||
type ManagerPackInfo,
|
||||
type ManagerQueueStatus,
|
||||
SelectedVersion,
|
||||
type UpdateAllPacksParams
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
type ManagerQueueStatus = components['schemas']['QueueStatus']
|
||||
type InstallPackParams = components['schemas']['InstallPackParams']
|
||||
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
|
||||
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
|
||||
type ManagerTaskHistory = components['schemas']['HistoryResponse']
|
||||
type QueueTaskItem = components['schemas']['QueueTaskItem']
|
||||
|
||||
const GENERIC_SECURITY_ERR_MSG =
|
||||
'Forbidden: A security error has occurred. Please check the terminal logs'
|
||||
|
||||
@@ -19,20 +20,23 @@ const GENERIC_SECURITY_ERR_MSG =
|
||||
* API routes for ComfyUI Manager
|
||||
*/
|
||||
enum ManagerRoute {
|
||||
START_QUEUE = 'manager/queue/start',
|
||||
RESET_QUEUE = 'manager/queue/reset',
|
||||
QUEUE_STATUS = 'manager/queue/status',
|
||||
INSTALL = 'manager/queue/install',
|
||||
UPDATE = 'manager/queue/update',
|
||||
UPDATE_ALL = 'manager/queue/update_all',
|
||||
UNINSTALL = 'manager/queue/uninstall',
|
||||
DISABLE = 'manager/queue/disable',
|
||||
FIX_NODE = 'manager/queue/fix',
|
||||
LIST_INSTALLED = 'customnode/installed',
|
||||
GET_NODES = 'customnode/getmappings',
|
||||
GET_PACKS = 'customnode/getlist',
|
||||
IMPORT_FAIL_INFO = 'customnode/import_fail_info',
|
||||
REBOOT = 'manager/reboot'
|
||||
START_QUEUE = 'v2/manager/queue/start',
|
||||
RESET_QUEUE = 'v2/manager/queue/reset',
|
||||
QUEUE_STATUS = 'v2/manager/queue/status',
|
||||
INSTALL = 'v2/manager/queue/install',
|
||||
UPDATE = 'v2/manager/queue/update',
|
||||
UPDATE_ALL = 'v2/manager/queue/update_all',
|
||||
UNINSTALL = 'v2/manager/queue/uninstall',
|
||||
DISABLE = 'v2/manager/queue/disable',
|
||||
FIX_NODE = 'v2/manager/queue/fix',
|
||||
LIST_INSTALLED = 'v2/customnode/installed',
|
||||
GET_NODES = 'v2/customnode/getmappings',
|
||||
GET_PACKS = 'v2/customnode/getlist',
|
||||
IMPORT_FAIL_INFO = 'v2/customnode/import_fail_info',
|
||||
REBOOT = 'v2/manager/reboot',
|
||||
IS_LEGACY_MANAGER_UI = 'v2/manager/is_legacy_manager_ui',
|
||||
TASK_HISTORY = 'v2/manager/queue/history',
|
||||
QUEUE_TASK = 'v2/manager/queue/task'
|
||||
}
|
||||
|
||||
const managerApiClient = axios.create({
|
||||
@@ -49,7 +53,6 @@ const managerApiClient = axios.create({
|
||||
export const useComfyManagerService = () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const didStartQueue = ref(false)
|
||||
|
||||
const handleRequestError = (
|
||||
err: unknown,
|
||||
@@ -110,28 +113,21 @@ export const useComfyManagerService = () => {
|
||||
201: 'Created: ComfyUI-Manager job queue is already running'
|
||||
}
|
||||
|
||||
didStartQueue.value = true
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.get(ManagerRoute.START_QUEUE, { signal }),
|
||||
{ errorContext, routeSpecificErrors }
|
||||
)
|
||||
}
|
||||
|
||||
const getQueueStatus = async (signal?: AbortSignal) => {
|
||||
const getQueueStatus = async (client_id?: string, signal?: AbortSignal) => {
|
||||
const errorContext = 'Getting ComfyUI-Manager queue status'
|
||||
|
||||
return executeRequest<ManagerQueueStatus>(
|
||||
() => managerApiClient.get(ManagerRoute.QUEUE_STATUS, { signal }),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
const resetQueue = async (signal?: AbortSignal) => {
|
||||
const errorContext = 'Resetting ComfyUI-Manager queue'
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.get(ManagerRoute.RESET_QUEUE, { signal }),
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.QUEUE_STATUS, {
|
||||
params: client_id ? { client_id } : undefined,
|
||||
signal
|
||||
}),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
@@ -154,73 +150,66 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const installPack = async (
|
||||
params: InstallPackParams,
|
||||
const queueTask = async (
|
||||
kind: QueueTaskItem['kind'],
|
||||
params: QueueTaskItem['params'],
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = `Installing pack ${params.id}`
|
||||
const task: QueueTaskItem = {
|
||||
kind,
|
||||
params,
|
||||
ui_id: ui_id || uuidv4(),
|
||||
client_id: api.clientId ?? api.initialClientId ?? 'unknown'
|
||||
}
|
||||
|
||||
const errorContext = `Queueing ${task.kind} task`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG,
|
||||
404:
|
||||
params.selected_version === SelectedVersion.NIGHTLY
|
||||
? `Not Found: Node pack ${params.id} does not provide nightly version`
|
||||
: GENERIC_SECURITY_ERR_MSG
|
||||
404: `Not Found: Task could not be queued`
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.INSTALL, params, { signal }),
|
||||
() => managerApiClient.post(ManagerRoute.QUEUE_TASK, task, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
|
||||
const installPack = async (
|
||||
params: InstallPackParams,
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
return queueTask('install', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const uninstallPack = async (
|
||||
params: ManagerPackInfo,
|
||||
params: components['schemas']['UninstallPackParams'],
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = `Uninstalling pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.UNINSTALL, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
return queueTask('uninstall', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const disablePack = async (
|
||||
params: ManagerPackInfo,
|
||||
params: components['schemas']['DisablePackParams'],
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<null> => {
|
||||
const errorContext = `Disabling pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
404: `Pack ${params.id} not found or not installed`,
|
||||
409: `Pack ${params.id} is already disabled`
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.DISABLE, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
return queueTask('disable', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const updatePack = async (
|
||||
params: ManagerPackInfo,
|
||||
params: components['schemas']['UpdatePackParams'],
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<null> => {
|
||||
const errorContext = `Updating pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.UPDATE, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
return queueTask('update', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const updateAllPacks = async (
|
||||
params?: UpdateAllPacksParams,
|
||||
params: UpdateAllPacksParams = {},
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = 'Updating all packs'
|
||||
@@ -229,8 +218,18 @@ export const useComfyManagerService = () => {
|
||||
401: 'Unauthorized: ComfyUI-Manager job queue is busy'
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
mode: params.mode,
|
||||
client_id: api.clientId ?? api.initialClientId ?? 'unknown',
|
||||
ui_id: ui_id || uuidv4()
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.get(ManagerRoute.UPDATE_ALL, { params, signal }),
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.UPDATE_ALL, {
|
||||
params: queryParams,
|
||||
signal
|
||||
}),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
@@ -247,6 +246,36 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const isLegacyManagerUI = async (signal?: AbortSignal) => {
|
||||
const errorContext = 'Checking if user set Manager to use the legacy UI'
|
||||
|
||||
return executeRequest<{ is_legacy_manager_ui: boolean }>(
|
||||
() => managerApiClient.get(ManagerRoute.IS_LEGACY_MANAGER_UI, { signal }),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
const getTaskHistory = async (
|
||||
options: {
|
||||
ui_id?: string
|
||||
max_items?: number
|
||||
client_id?: string
|
||||
offset?: number
|
||||
} = {},
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = 'Getting ComfyUI-Manager task history'
|
||||
|
||||
return executeRequest<ManagerTaskHistory>(
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.TASK_HISTORY, {
|
||||
params: options,
|
||||
signal
|
||||
}),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
@@ -254,8 +283,8 @@ export const useComfyManagerService = () => {
|
||||
|
||||
// Queue operations
|
||||
startQueue,
|
||||
resetQueue,
|
||||
getQueueStatus,
|
||||
getTaskHistory,
|
||||
|
||||
// Pack management
|
||||
listInstalledPacks,
|
||||
@@ -268,6 +297,7 @@ export const useComfyManagerService = () => {
|
||||
updateAllPacks,
|
||||
|
||||
// System operations
|
||||
rebootComfyUI
|
||||
rebootComfyUI,
|
||||
isLegacyManagerUI
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,7 @@ import type {
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { isComponentWidget, isDOMWidget } from '@/scripts/domWidget'
|
||||
import { $el } from '@/scripts/ui'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
@@ -90,37 +87,6 @@ export const useLitegraphService = () => {
|
||||
constructor() {
|
||||
super(app.graph, subgraph, instanceData)
|
||||
|
||||
// Set up event listener for promoted widget registration
|
||||
subgraph.events.addEventListener('widget-promoted', (event) => {
|
||||
const { widget } = event.detail
|
||||
// Only handle DOM widgets
|
||||
if (!isDOMWidget(widget) && !isComponentWidget(widget)) return
|
||||
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
if (!domWidgetStore.widgetStates.has(widget.id)) {
|
||||
domWidgetStore.registerWidget(widget)
|
||||
// Set initial visibility based on whether the widget's node is in the current graph
|
||||
const widgetState = domWidgetStore.widgetStates.get(widget.id)
|
||||
if (widgetState) {
|
||||
const currentGraph = canvasStore.getCanvas().graph
|
||||
widgetState.visible =
|
||||
currentGraph?.nodes.includes(widget.node) ?? false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Set up event listener for promoted widget removal
|
||||
subgraph.events.addEventListener('widget-demoted', (event) => {
|
||||
const { widget } = event.detail
|
||||
// Only handle DOM widgets
|
||||
if (!isDOMWidget(widget) && !isComponentWidget(widget)) return
|
||||
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
if (domWidgetStore.widgetStates.has(widget.id)) {
|
||||
domWidgetStore.unregisterWidget(widget.id)
|
||||
}
|
||||
})
|
||||
|
||||
this.#setupStrokeStyles()
|
||||
this.#addInputs(ComfyNode.nodeData.inputs)
|
||||
this.#addOutputs(ComfyNode.nodeData.outputs)
|
||||
@@ -141,11 +107,7 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||
const nodeId = String(this.id)
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId)
|
||||
const state =
|
||||
useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state
|
||||
if (state === 'running') {
|
||||
if (this.id == app.runningNodeId) {
|
||||
return { color: '#0f0' }
|
||||
}
|
||||
}
|
||||
@@ -400,11 +362,7 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||
const nodeId = String(this.id)
|
||||
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId)
|
||||
const state =
|
||||
useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state
|
||||
if (state === 'running') {
|
||||
if (this.id == app.runningNodeId) {
|
||||
return { color: '#0f0' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import { useServerLogs } from '@/composables/useServerLogs'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import {
|
||||
InstallPackParams,
|
||||
InstalledPacksResponse,
|
||||
ManagerPackInfo,
|
||||
ManagerPackInstalled,
|
||||
TaskLog,
|
||||
UpdateAllPacksParams
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { TaskLog } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
type InstallPackParams = components['schemas']['InstallPackParams']
|
||||
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
|
||||
type ManagerPackInfo = components['schemas']['ManagerPackInfo']
|
||||
type ManagerPackInstalled = components['schemas']['ManagerPackInstalled']
|
||||
type ManagerTaskHistory = Record<
|
||||
string,
|
||||
components['schemas']['TaskHistoryItem']
|
||||
>
|
||||
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
|
||||
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
|
||||
|
||||
/**
|
||||
* Store for state of installed node packs
|
||||
@@ -31,14 +38,62 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const installedPacksIds = ref<Set<string>>(new Set())
|
||||
const isStale = ref(true)
|
||||
const taskLogs = ref<TaskLog[]>([])
|
||||
const succeededTasksLogs = ref<TaskLog[]>([])
|
||||
const failedTasksLogs = ref<TaskLog[]>([])
|
||||
|
||||
const { statusMessage, allTasksDone, enqueueTask, uncompletedCount } =
|
||||
useManagerQueue()
|
||||
const taskHistory = ref<ManagerTaskHistory>({})
|
||||
const succeededTasksIds = ref<string[]>([])
|
||||
const failedTasksIds = ref<string[]>([])
|
||||
const taskQueue = ref<ManagerTaskQueue>({
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
})
|
||||
|
||||
const managerQueue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const setStale = () => {
|
||||
isStale.value = true
|
||||
}
|
||||
|
||||
const partitionTaskLogs = () => {
|
||||
const successTaskLogs: TaskLog[] = []
|
||||
const failTaskLogs: TaskLog[] = []
|
||||
for (const log of taskLogs.value) {
|
||||
if (failedTasksIds.value.includes(log.taskId)) {
|
||||
failTaskLogs.push(log)
|
||||
} else {
|
||||
successTaskLogs.push(log)
|
||||
}
|
||||
}
|
||||
succeededTasksLogs.value = successTaskLogs
|
||||
failedTasksLogs.value = failTaskLogs
|
||||
}
|
||||
|
||||
const partitionTasks = () => {
|
||||
const successTasksIds = []
|
||||
const failTasksIds = []
|
||||
for (const task of Object.values(taskHistory.value)) {
|
||||
if (task.status?.status_str === 'success') {
|
||||
successTasksIds.push(task.ui_id)
|
||||
} else {
|
||||
failTasksIds.push(task.ui_id)
|
||||
}
|
||||
}
|
||||
succeededTasksIds.value = successTasksIds
|
||||
failedTasksIds.value = failTasksIds
|
||||
}
|
||||
|
||||
whenever(
|
||||
taskHistory,
|
||||
() => {
|
||||
partitionTasks()
|
||||
partitionTaskLogs()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const getPackId = (pack: ManagerPackInstalled) => pack.cnr_id || pack.aux_id
|
||||
|
||||
const isInstalledPackId = (packName: string | undefined): boolean =>
|
||||
@@ -97,11 +152,13 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
|
||||
const updateInstalledIds = (packs: ManagerPackInstalled[]) => {
|
||||
installedPacksIds.value = packsToIdSet(packs)
|
||||
const newIds = packsToIdSet(packs)
|
||||
installedPacksIds.value = newIds
|
||||
}
|
||||
|
||||
const onPacksChanged = () => {
|
||||
const packs = Object.values(installedPacks.value)
|
||||
|
||||
updateDisabledIds(packs)
|
||||
updateInstalledIds(packs)
|
||||
}
|
||||
@@ -115,23 +172,46 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
whenever(uncompletedCount, () => showManagerProgressDialog())
|
||||
|
||||
const withLogs = (task: () => Promise<null>, taskName: string) => {
|
||||
const { startListening, stopListening, logs } = useServerLogs()
|
||||
const enqueueTaskWithLogs = async (
|
||||
task: (taskId: string) => Promise<null>,
|
||||
taskName: string
|
||||
) => {
|
||||
const taskId = uuidv4()
|
||||
const { logs } = useServerLogs({
|
||||
ui_id: taskId,
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const loggedTask = async () => {
|
||||
taskLogs.value.push({ taskName, logs: logs.value })
|
||||
await startListening()
|
||||
return task()
|
||||
try {
|
||||
// Show progress dialog immediately when task is queued
|
||||
showManagerProgressDialog()
|
||||
managerQueue.isProcessing.value = true
|
||||
|
||||
// Prepare logging hook
|
||||
taskLogs.value.push({ taskName, taskId, logs: logs.value })
|
||||
|
||||
// Queue the task to the server
|
||||
await task(taskId)
|
||||
} catch (error) {
|
||||
// Reset processing state on error
|
||||
managerQueue.isProcessing.value = false
|
||||
|
||||
// The server has authority over task history in general, but in rare
|
||||
// case of client-side error, we add that to failed tasks from the client side
|
||||
taskHistory.value[taskId] = {
|
||||
ui_id: taskId,
|
||||
client_id: api.clientId || 'unknown',
|
||||
kind: 'error',
|
||||
result: 'failed',
|
||||
status: {
|
||||
status_str: 'error',
|
||||
completed: false,
|
||||
messages: [error instanceof Error ? error.message : String(error)]
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const onComplete = async () => {
|
||||
await stopListening()
|
||||
setStale()
|
||||
}
|
||||
|
||||
return { task: loggedTask, onComplete }
|
||||
}
|
||||
|
||||
const installPack = useCachedRequest<InstallPackParams, void>(
|
||||
@@ -152,39 +232,62 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const task = () => managerService.installPack(params, signal)
|
||||
enqueueTask(withLogs(task, `${actionDescription} ${params.id}`))
|
||||
const task = (taskId: string) =>
|
||||
managerService.installPack(params, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, `${actionDescription} ${params.id}`)
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const uninstallPack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const uninstallPack = async (
|
||||
params: ManagerPackInfo,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
installPack.clear()
|
||||
installPack.cancel()
|
||||
const task = () => managerService.uninstallPack(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id })))
|
||||
const uninstallParams: components['schemas']['UninstallPackParams'] = {
|
||||
node_name: params.id,
|
||||
is_unknown: false
|
||||
}
|
||||
const task = (taskId: string) =>
|
||||
managerService.uninstallPack(uninstallParams, taskId, signal)
|
||||
await enqueueTaskWithLogs(
|
||||
task,
|
||||
t('manager.uninstalling', { id: params.id })
|
||||
)
|
||||
}
|
||||
|
||||
const updatePack = useCachedRequest<ManagerPackInfo, void>(
|
||||
async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
updateAllPacks.cancel()
|
||||
const task = () => managerService.updatePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id })))
|
||||
const updateParams: components['schemas']['UpdatePackParams'] = {
|
||||
node_name: params.id,
|
||||
node_ver: params.version
|
||||
}
|
||||
const task = (taskId: string) =>
|
||||
managerService.updatePack(updateParams, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('g.updating', { id: params.id }))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const updateAllPacks = useCachedRequest<UpdateAllPacksParams, void>(
|
||||
async (params: UpdateAllPacksParams, signal?: AbortSignal) => {
|
||||
const task = () => managerService.updateAllPacks(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.updatingAllPacks')))
|
||||
const task = (taskId: string) =>
|
||||
managerService.updateAllPacks(params, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('manager.updatingAllPacks'))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const task = () => managerService.disablePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id })))
|
||||
const disablePack = async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const disableParams: components['schemas']['DisablePackParams'] = {
|
||||
node_name: params.id,
|
||||
is_unknown: false
|
||||
}
|
||||
const task = (taskId: string) =>
|
||||
managerService.disablePack(disableParams, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('g.disabling', { id: params.id }))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
@@ -196,15 +299,31 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
taskLogs.value = []
|
||||
}
|
||||
|
||||
const resetTaskState = () => {
|
||||
// Clear all task-related reactive state for fresh start after restart
|
||||
taskLogs.value = []
|
||||
taskHistory.value = {}
|
||||
succeededTasksIds.value = []
|
||||
failedTasksIds.value = []
|
||||
succeededTasksLogs.value = []
|
||||
failedTasksLogs.value = []
|
||||
|
||||
// Reset task queue to initial state
|
||||
taskQueue.value = {
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Manager state
|
||||
isLoading: managerService.isLoading,
|
||||
error: managerService.error,
|
||||
statusMessage,
|
||||
allTasksDone,
|
||||
uncompletedCount,
|
||||
taskLogs,
|
||||
clearLogs,
|
||||
resetTaskState,
|
||||
setStale,
|
||||
|
||||
// Installed packs state
|
||||
@@ -215,6 +334,15 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
getInstalledPackVersion,
|
||||
refreshInstalledList,
|
||||
|
||||
// Task queue state and actions
|
||||
taskHistory,
|
||||
isProcessingTasks: managerQueue.isProcessing,
|
||||
succeededTasksIds,
|
||||
failedTasksIds,
|
||||
succeededTasksLogs,
|
||||
failedTasksLogs,
|
||||
managerQueue,
|
||||
|
||||
// Pack actions
|
||||
installPack,
|
||||
uninstallPack,
|
||||
@@ -234,6 +362,15 @@ export const useManagerProgressDialogStore = defineStore(
|
||||
'managerProgressDialog',
|
||||
() => {
|
||||
const isExpanded = ref(false)
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
const setActiveTabIndex = (index: number) => {
|
||||
activeTabIndex.value = index
|
||||
}
|
||||
|
||||
const getActiveTabIndex = () => {
|
||||
return activeTabIndex.value
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
@@ -250,7 +387,9 @@ export const useManagerProgressDialogStore = defineStore(
|
||||
isExpanded,
|
||||
toggle,
|
||||
collapse,
|
||||
expand
|
||||
expand,
|
||||
setActiveTabIndex,
|
||||
getActiveTabIndex
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,8 +12,6 @@ import type {
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
NodeError,
|
||||
NodeProgressState,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage
|
||||
} from '@/schemas/apiSchema'
|
||||
@@ -23,10 +21,6 @@ import type {
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
import { useCanvasStore } from './graphStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from './workflowStore'
|
||||
@@ -52,97 +46,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
||||
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
||||
// This is the progress of all nodes in the currently executing workflow
|
||||
const nodeProgressStates = ref<Record<string, NodeProgressState>>({})
|
||||
|
||||
/**
|
||||
* Convert execution context node IDs to NodeLocatorIds
|
||||
* @param nodeId The node ID from execution context (could be execution ID)
|
||||
* @returns The NodeLocatorId
|
||||
*/
|
||||
const executionIdToNodeLocatorId = (
|
||||
nodeId: string | number
|
||||
): NodeLocatorId => {
|
||||
const nodeIdStr = String(nodeId)
|
||||
|
||||
if (!nodeIdStr.includes(':')) {
|
||||
// It's a top-level node ID
|
||||
return nodeIdStr
|
||||
}
|
||||
|
||||
// It's an execution node ID
|
||||
const parts = nodeIdStr.split(':')
|
||||
const localNodeId = parts[parts.length - 1]
|
||||
const subgraphs = getSubgraphsFromInstanceIds(app.graph, parts)
|
||||
const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId)
|
||||
return nodeLocatorId
|
||||
}
|
||||
|
||||
const mergeExecutionProgressStates = (
|
||||
currentState: NodeProgressState | undefined,
|
||||
newState: NodeProgressState
|
||||
): NodeProgressState => {
|
||||
if (currentState === undefined) {
|
||||
return newState
|
||||
}
|
||||
|
||||
const mergedState = { ...currentState }
|
||||
if (mergedState.state === 'error') {
|
||||
return mergedState
|
||||
} else if (newState.state === 'running') {
|
||||
const newPerc = newState.max > 0 ? newState.value / newState.max : 0.0
|
||||
const oldPerc =
|
||||
mergedState.max > 0 ? mergedState.value / mergedState.max : 0.0
|
||||
if (
|
||||
mergedState.state !== 'running' ||
|
||||
oldPerc === 0.0 ||
|
||||
newPerc < oldPerc
|
||||
) {
|
||||
mergedState.value = newState.value
|
||||
mergedState.max = newState.max
|
||||
}
|
||||
mergedState.state = 'running'
|
||||
}
|
||||
|
||||
return mergedState
|
||||
}
|
||||
|
||||
const nodeLocationProgressStates = computed<
|
||||
Record<NodeLocatorId, NodeProgressState>
|
||||
>(() => {
|
||||
const result: Record<NodeLocatorId, NodeProgressState> = {}
|
||||
|
||||
const states = nodeProgressStates.value // Apparently doing this inside `Object.entries` causes issues
|
||||
for (const state of Object.values(states)) {
|
||||
const parts = String(state.display_node_id).split(':')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const executionId = parts.slice(0, i + 1).join(':')
|
||||
const locatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!locatorId) continue
|
||||
|
||||
result[locatorId] = mergeExecutionProgressStates(
|
||||
result[locatorId],
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// Easily access all currently executing node IDs
|
||||
const executingNodeIds = computed<NodeId[]>(() => {
|
||||
return Object.entries(nodeProgressStates)
|
||||
.filter(([_, state]) => state.state === 'running')
|
||||
.map(([nodeId, _]) => nodeId)
|
||||
})
|
||||
|
||||
// @deprecated For backward compatibility - stores the primary executing node ID
|
||||
const executingNodeId = computed<NodeId | null>(() => {
|
||||
return executingNodeIds.value.length > 0 ? executingNodeIds.value[0] : null
|
||||
})
|
||||
|
||||
// For backward compatibility - returns the primary executing node
|
||||
const executingNodeId = ref<NodeId | null>(null)
|
||||
const executingNode = computed<ComfyNode | null>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
|
||||
@@ -189,7 +93,30 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs)
|
||||
}
|
||||
|
||||
// This is the progress of the currently executing node (for backward compatibility)
|
||||
const executionIdToCurrentId = (id: string) => {
|
||||
const subgraph = workflowStore.activeSubgraph
|
||||
|
||||
// Short-circuit: ID belongs to the parent workflow / no active subgraph
|
||||
if (!id.includes(':')) {
|
||||
return !subgraph ? id : undefined
|
||||
} else if (!subgraph) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the hierarchical ID (e.g., "123:456:789")
|
||||
const subgraphNodeIds = id.split(':')
|
||||
|
||||
// If the last subgraph is the active subgraph, return the node ID
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
subgraph.rootGraph,
|
||||
subgraphNodeIds
|
||||
)
|
||||
if (subgraphs.at(-1) === subgraph) {
|
||||
return subgraphNodeIds.at(-1)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the progress of the currently executing node, if any
|
||||
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
||||
const executingNodeProgress = computed(() =>
|
||||
_executingNodeProgress.value
|
||||
@@ -223,29 +150,24 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
function bindExecutionEvents() {
|
||||
api.addEventListener('execution_start', handleExecutionStart)
|
||||
api.addEventListener('execution_cached', handleExecutionCached)
|
||||
api.addEventListener('execution_interrupted', handleExecutionInterrupted)
|
||||
api.addEventListener('executed', handleExecuted)
|
||||
api.addEventListener('executing', handleExecuting)
|
||||
api.addEventListener('progress', handleProgress)
|
||||
api.addEventListener('progress_state', handleProgressState)
|
||||
api.addEventListener('status', handleStatus)
|
||||
api.addEventListener('execution_error', handleExecutionError)
|
||||
api.addEventListener('progress_text', handleProgressText)
|
||||
api.addEventListener('display_component', handleDisplayComponent)
|
||||
}
|
||||
api.addEventListener('progress_text', handleProgressText)
|
||||
api.addEventListener('display_component', handleDisplayComponent)
|
||||
|
||||
function unbindExecutionEvents() {
|
||||
api.removeEventListener('execution_start', handleExecutionStart)
|
||||
api.removeEventListener('execution_cached', handleExecutionCached)
|
||||
api.removeEventListener('execution_interrupted', handleExecutionInterrupted)
|
||||
api.removeEventListener('executed', handleExecuted)
|
||||
api.removeEventListener('executing', handleExecuting)
|
||||
api.removeEventListener('progress', handleProgress)
|
||||
api.removeEventListener('progress_state', handleProgressState)
|
||||
api.removeEventListener('status', handleStatus)
|
||||
api.removeEventListener('execution_error', handleExecutionError)
|
||||
api.removeEventListener('progress_text', handleProgressText)
|
||||
api.removeEventListener('display_component', handleDisplayComponent)
|
||||
}
|
||||
|
||||
function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) {
|
||||
@@ -261,10 +183,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function handleExecutionInterrupted() {
|
||||
nodeProgressStates.value = {}
|
||||
}
|
||||
|
||||
function handleExecuted(e: CustomEvent<ExecutedWsMessage>) {
|
||||
if (!activePrompt.value) return
|
||||
activePrompt.value.nodes[e.detail.node] = true
|
||||
@@ -276,42 +194,19 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
|
||||
if (!activePrompt.value) return
|
||||
|
||||
// Update the executing nodes list
|
||||
if (typeof e.detail !== 'string') {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
activePromptId.value = null
|
||||
if (executingNodeId.value && activePrompt.value) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
activePrompt.value.nodes[executingNodeId.value] = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) {
|
||||
const { nodes } = e.detail
|
||||
|
||||
// Revoke previews for nodes that are starting to execute
|
||||
for (const nodeId in nodes) {
|
||||
const nodeState = nodes[nodeId]
|
||||
if (nodeState.state === 'running' && !nodeProgressStates.value[nodeId]) {
|
||||
// This node just started executing, revoke its previews
|
||||
// Note that we're doing the *actual* node id instead of the display node id
|
||||
// here intentionally. That way, we don't clear the preview every time a new node
|
||||
// within an expanded graph starts executing.
|
||||
const { revokePreviewsByExecutionId } = useNodeOutputStore()
|
||||
revokePreviewsByExecutionId(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the progress states for all nodes
|
||||
nodeProgressStates.value = nodes
|
||||
|
||||
// If we have progress for the currently executing node, update it for backwards compatibility
|
||||
if (executingNodeId.value && nodes[executingNodeId.value]) {
|
||||
const nodeState = nodes[executingNodeId.value]
|
||||
_executingNodeProgress.value = {
|
||||
value: nodeState.value,
|
||||
max: nodeState.max,
|
||||
prompt_id: nodeState.prompt_id,
|
||||
node: nodeState.display_node_id || nodeState.node_id
|
||||
if (typeof e.detail === 'string') {
|
||||
executingNodeId.value = executionIdToCurrentId(e.detail) ?? null
|
||||
} else {
|
||||
executingNodeId.value = e.detail
|
||||
if (executingNodeId.value === null) {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
activePromptId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,7 +239,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const { nodeId, text } = e.detail
|
||||
if (!text || !nodeId) return
|
||||
|
||||
// Handle execution node IDs for subgraphs
|
||||
// Handle hierarchical node IDs for subgraphs
|
||||
const currentId = getNodeIdIfExecuting(nodeId)
|
||||
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||
if (!node) return
|
||||
@@ -355,7 +250,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
function handleDisplayComponent(e: CustomEvent<DisplayComponentWsMessage>) {
|
||||
const { node_id: nodeId, component, props = {} } = e.detail
|
||||
|
||||
// Handle execution node IDs for subgraphs
|
||||
// Handle hierarchical node IDs for subgraphs
|
||||
const currentId = getNodeIdIfExecuting(nodeId)
|
||||
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||
if (!node) return
|
||||
@@ -395,18 +290,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a NodeLocatorId to an execution context ID
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @returns The execution ID or null if conversion fails
|
||||
*/
|
||||
const nodeLocatorIdToExecutionId = (
|
||||
locatorId: NodeLocatorId | string
|
||||
): string | null => {
|
||||
const executionId = workflowStore.nodeLocatorIdToNodeExecutionId(locatorId)
|
||||
return executionId
|
||||
}
|
||||
|
||||
return {
|
||||
isIdle,
|
||||
clientId,
|
||||
@@ -427,13 +310,9 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
lastExecutionError,
|
||||
/**
|
||||
* The id of the node that is currently being executed (backward compatibility)
|
||||
* The id of the node that is currently being executed
|
||||
*/
|
||||
executingNodeId,
|
||||
/**
|
||||
* The list of all nodes that are currently executing
|
||||
*/
|
||||
executingNodeIds,
|
||||
/**
|
||||
* The prompt that is currently being executed
|
||||
*/
|
||||
@@ -451,25 +330,17 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
executionProgress,
|
||||
/**
|
||||
* The node that is currently being executed (backward compatibility)
|
||||
* The node that is currently being executed
|
||||
*/
|
||||
executingNode,
|
||||
/**
|
||||
* The progress of the executing node (backward compatibility)
|
||||
* The progress of the executing node (if the node reports progress)
|
||||
*/
|
||||
executingNodeProgress,
|
||||
/**
|
||||
* All node progress states from progress_state events
|
||||
*/
|
||||
nodeProgressStates,
|
||||
nodeLocationProgressStates,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
storePrompt,
|
||||
// Raw executing progress data for backward compatibility in ComfyApp.
|
||||
_executingNodeProgress,
|
||||
// NodeLocatorId conversion helpers
|
||||
executionIdToNodeLocatorId,
|
||||
nodeLocatorIdToExecutionId
|
||||
_executingNodeProgress
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,9 +8,6 @@ import {
|
||||
} from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { parseFilePath } from '@/utils/formatUtil'
|
||||
import { isVideoNode } from '@/utils/litegraphUtil'
|
||||
|
||||
@@ -25,22 +22,17 @@ const createOutputs = (
|
||||
}
|
||||
}
|
||||
|
||||
interface SetOutputOptions {
|
||||
merge?: boolean
|
||||
}
|
||||
|
||||
export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
const { nodeIdToNodeLocatorId } = useWorkflowStore()
|
||||
const { executionIdToNodeLocatorId } = useExecutionStore()
|
||||
const getNodeId = (node: LGraphNode): string => node.id.toString()
|
||||
|
||||
function getNodeOutputs(
|
||||
node: LGraphNode
|
||||
): ExecutedWsMessage['output'] | undefined {
|
||||
return app.nodeOutputs[nodeIdToNodeLocatorId(node.id)]
|
||||
return app.nodeOutputs[getNodeId(node)]
|
||||
}
|
||||
|
||||
function getNodePreviews(node: LGraphNode): string[] | undefined {
|
||||
return app.nodePreviewImages[nodeIdToNodeLocatorId(node.id)]
|
||||
return app.nodePreviewImages[getNodeId(node)]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,35 +86,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function to set outputs by NodeLocatorId.
|
||||
* Handles the merge logic when needed.
|
||||
*/
|
||||
function setOutputsByLocatorId(
|
||||
nodeLocatorId: NodeLocatorId,
|
||||
outputs: ExecutedWsMessage['output'] | ResultItem,
|
||||
options: SetOutputOptions = {}
|
||||
) {
|
||||
if (options.merge) {
|
||||
const existingOutput = app.nodeOutputs[nodeLocatorId]
|
||||
if (existingOutput && outputs) {
|
||||
for (const k in outputs) {
|
||||
const existingValue = existingOutput[k]
|
||||
const newValue = (outputs as Record<NodeLocatorId, any>)[k]
|
||||
|
||||
if (Array.isArray(existingValue) && Array.isArray(newValue)) {
|
||||
existingOutput[k] = existingValue.concat(newValue)
|
||||
} else {
|
||||
existingOutput[k] = newValue
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
app.nodeOutputs[nodeLocatorId] = outputs
|
||||
}
|
||||
|
||||
function setNodeOutputs(
|
||||
node: LGraphNode,
|
||||
filenames: string | string[] | ResultItem,
|
||||
@@ -133,149 +96,24 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
) {
|
||||
if (!filenames || !node) return
|
||||
|
||||
const nodeId = getNodeId(node)
|
||||
|
||||
if (typeof filenames === 'string') {
|
||||
setNodeOutputsByNodeId(
|
||||
node.id,
|
||||
createOutputs([filenames], folder, isAnimated)
|
||||
)
|
||||
app.nodeOutputs[nodeId] = createOutputs([filenames], folder, isAnimated)
|
||||
} else if (!Array.isArray(filenames)) {
|
||||
setNodeOutputsByNodeId(node.id, filenames)
|
||||
app.nodeOutputs[nodeId] = filenames
|
||||
} else {
|
||||
const resultItems = createOutputs(filenames, folder, isAnimated)
|
||||
if (!resultItems?.images?.length) return
|
||||
setNodeOutputsByNodeId(node.id, resultItems)
|
||||
app.nodeOutputs[nodeId] = resultItems
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node outputs by execution ID (hierarchical ID from backend).
|
||||
* Converts the execution ID to a NodeLocatorId before storing.
|
||||
*
|
||||
* @param executionId - The execution ID (e.g., "123:456:789" or "789")
|
||||
* @param outputs - The outputs to store
|
||||
* @param options - Options for setting outputs
|
||||
* @param options.merge - If true, merge with existing outputs (arrays are concatenated)
|
||||
*/
|
||||
function setNodeOutputsByExecutionId(
|
||||
executionId: string,
|
||||
outputs: ExecutedWsMessage['output'] | ResultItem,
|
||||
options: SetOutputOptions = {}
|
||||
) {
|
||||
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!nodeLocatorId) return
|
||||
|
||||
setOutputsByLocatorId(nodeLocatorId, outputs, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node outputs by node ID.
|
||||
* Uses the current graph context to create the appropriate NodeLocatorId.
|
||||
*
|
||||
* @param nodeId - The node ID
|
||||
* @param outputs - The outputs to store
|
||||
* @param options - Options for setting outputs
|
||||
* @param options.merge - If true, merge with existing outputs (arrays are concatenated)
|
||||
*/
|
||||
function setNodeOutputsByNodeId(
|
||||
nodeId: string | number,
|
||||
outputs: ExecutedWsMessage['output'] | ResultItem,
|
||||
options: SetOutputOptions = {}
|
||||
) {
|
||||
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
|
||||
if (!nodeLocatorId) return
|
||||
|
||||
setOutputsByLocatorId(nodeLocatorId, outputs, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node preview images by execution ID (hierarchical ID from backend).
|
||||
* Converts the execution ID to a NodeLocatorId before storing.
|
||||
*
|
||||
* @param executionId - The execution ID (e.g., "123:456:789" or "789")
|
||||
* @param previewImages - Array of preview image URLs to store
|
||||
*/
|
||||
function setNodePreviewsByExecutionId(
|
||||
executionId: string,
|
||||
previewImages: string[]
|
||||
) {
|
||||
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!nodeLocatorId) return
|
||||
|
||||
app.nodePreviewImages[nodeLocatorId] = previewImages
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node preview images by node ID.
|
||||
* Uses the current graph context to create the appropriate NodeLocatorId.
|
||||
*
|
||||
* @param nodeId - The node ID
|
||||
* @param previewImages - Array of preview image URLs to store
|
||||
*/
|
||||
function setNodePreviewsByNodeId(
|
||||
nodeId: string | number,
|
||||
previewImages: string[]
|
||||
) {
|
||||
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
|
||||
app.nodePreviewImages[nodeLocatorId] = previewImages
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke preview images by execution ID.
|
||||
* Frees memory allocated to image preview blobs by revoking the URLs.
|
||||
*
|
||||
* @param executionId - The execution ID
|
||||
*/
|
||||
function revokePreviewsByExecutionId(executionId: string) {
|
||||
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!nodeLocatorId) return
|
||||
|
||||
revokePreviewsByLocatorId(nodeLocatorId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke preview images by node locator ID.
|
||||
* Frees memory allocated to image preview blobs by revoking the URLs.
|
||||
*
|
||||
* @param nodeLocatorId - The node locator ID
|
||||
*/
|
||||
function revokePreviewsByLocatorId(nodeLocatorId: NodeLocatorId) {
|
||||
const previews = app.nodePreviewImages[nodeLocatorId]
|
||||
if (!previews?.[Symbol.iterator]) return
|
||||
|
||||
for (const url of previews) {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
delete app.nodePreviewImages[nodeLocatorId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke all preview images.
|
||||
* Frees memory allocated to all image preview blobs.
|
||||
*/
|
||||
function revokeAllPreviews() {
|
||||
for (const nodeLocatorId of Object.keys(app.nodePreviewImages)) {
|
||||
const previews = app.nodePreviewImages[nodeLocatorId]
|
||||
if (!previews?.[Symbol.iterator]) continue
|
||||
|
||||
for (const url of previews) {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
app.nodePreviewImages = {}
|
||||
}
|
||||
|
||||
return {
|
||||
getNodeOutputs,
|
||||
getNodeImageUrls,
|
||||
getNodePreviews,
|
||||
setNodeOutputs,
|
||||
setNodeOutputsByExecutionId,
|
||||
setNodeOutputsByNodeId,
|
||||
setNodePreviewsByExecutionId,
|
||||
setNodePreviewsByNodeId,
|
||||
revokePreviewsByExecutionId,
|
||||
revokeAllPreviews,
|
||||
getPreviewParam
|
||||
}
|
||||
})
|
||||
|
||||
@@ -251,41 +251,11 @@ export function createDummyFolderNodeDef(folderPath: string): ComfyNodeDefImpl {
|
||||
} as ComfyNodeDefV1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a filter for node definitions in the node library.
|
||||
* Filters are applied in a single pass to determine node visibility.
|
||||
*/
|
||||
export interface NodeDefFilter {
|
||||
/**
|
||||
* Unique identifier for the filter.
|
||||
* Convention: Use dot notation like 'core.deprecated' or 'extension.myfilter'
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* Display name for the filter (used in UI/debugging).
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Optional description explaining what the filter does.
|
||||
*/
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* The filter function that returns true if the node should be visible.
|
||||
* @param nodeDef - The node definition to evaluate
|
||||
* @returns true if the node should be visible, false to hide it
|
||||
*/
|
||||
predicate: (nodeDef: ComfyNodeDefImpl) => boolean
|
||||
}
|
||||
|
||||
export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const nodeDefsByName = ref<Record<string, ComfyNodeDefImpl>>({})
|
||||
const nodeDefsByDisplayName = ref<Record<string, ComfyNodeDefImpl>>({})
|
||||
const showDeprecated = ref(false)
|
||||
const showExperimental = ref(false)
|
||||
const nodeDefFilters = ref<NodeDefFilter[]>([])
|
||||
|
||||
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
|
||||
const nodeDataTypes = computed(() => {
|
||||
@@ -300,11 +270,13 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
}
|
||||
return types
|
||||
})
|
||||
const visibleNodeDefs = computed(() => {
|
||||
return nodeDefs.value.filter((nodeDef) =>
|
||||
nodeDefFilters.value.every((filter) => filter.predicate(nodeDef))
|
||||
const visibleNodeDefs = computed(() =>
|
||||
nodeDefs.value.filter(
|
||||
(nodeDef: ComfyNodeDefImpl) =>
|
||||
(showDeprecated.value || !nodeDef.deprecated) &&
|
||||
(showExperimental.value || !nodeDef.experimental)
|
||||
)
|
||||
})
|
||||
)
|
||||
const nodeSearchService = computed(
|
||||
() => new NodeSearchService(visibleNodeDefs.value)
|
||||
)
|
||||
@@ -338,68 +310,11 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
return nodeDefsByName.value[node.constructor?.nodeData?.name] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a node definition filter.
|
||||
* @param filter - The filter to register
|
||||
*/
|
||||
function registerNodeDefFilter(filter: NodeDefFilter) {
|
||||
nodeDefFilters.value = [...nodeDefFilters.value, filter]
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a node definition filter by ID.
|
||||
* @param id - The ID of the filter to remove
|
||||
*/
|
||||
function unregisterNodeDefFilter(id: string) {
|
||||
nodeDefFilters.value = nodeDefFilters.value.filter((f) => f.id !== id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the core node definition filters.
|
||||
*/
|
||||
function registerCoreNodeDefFilters() {
|
||||
// Deprecated nodes filter
|
||||
registerNodeDefFilter({
|
||||
id: 'core.deprecated',
|
||||
name: 'Hide Deprecated Nodes',
|
||||
description: 'Hides nodes marked as deprecated unless explicitly enabled',
|
||||
predicate: (nodeDef) => showDeprecated.value || !nodeDef.deprecated
|
||||
})
|
||||
|
||||
// Experimental nodes filter
|
||||
registerNodeDefFilter({
|
||||
id: 'core.experimental',
|
||||
name: 'Hide Experimental Nodes',
|
||||
description:
|
||||
'Hides nodes marked as experimental unless explicitly enabled',
|
||||
predicate: (nodeDef) => showExperimental.value || !nodeDef.experimental
|
||||
})
|
||||
|
||||
// Subgraph nodes filter
|
||||
// @todo Remove this filter when subgraph v2 is released
|
||||
registerNodeDefFilter({
|
||||
id: 'core.subgraph',
|
||||
name: 'Hide Subgraph Nodes',
|
||||
description:
|
||||
'Temporarily hides subgraph nodes from node library and search',
|
||||
predicate: (nodeDef) => {
|
||||
// Hide subgraph nodes (identified by category='subgraph' and python_module='nodes')
|
||||
return !(
|
||||
nodeDef.category === 'subgraph' && nodeDef.python_module === 'nodes'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Register core filters on store initialization
|
||||
registerCoreNodeDefFilters()
|
||||
|
||||
return {
|
||||
nodeDefsByName,
|
||||
nodeDefsByDisplayName,
|
||||
showDeprecated,
|
||||
showExperimental,
|
||||
nodeDefFilters,
|
||||
|
||||
nodeDefs,
|
||||
nodeDataTypes,
|
||||
@@ -409,9 +324,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
|
||||
updateNodeDefs,
|
||||
addNodeDef,
|
||||
fromLGraphNode,
|
||||
registerNodeDefFilter,
|
||||
unregisterNodeDefFilter
|
||||
fromLGraphNode
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ import type {
|
||||
import type { ComfyWorkflowJSON, NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
|
||||
// Task type used in the API.
|
||||
export type APITaskType = 'queue' | 'history'
|
||||
@@ -379,18 +377,7 @@ export class TaskItemImpl {
|
||||
}
|
||||
await app.loadGraphData(toRaw(this.workflow))
|
||||
if (this.outputs) {
|
||||
const nodeOutputsStore = useNodeOutputStore()
|
||||
const rawOutputs = toRaw(this.outputs)
|
||||
for (const nodeExecutionId in rawOutputs) {
|
||||
nodeOutputsStore.setNodeOutputsByExecutionId(
|
||||
nodeExecutionId,
|
||||
rawOutputs[nodeExecutionId]
|
||||
)
|
||||
}
|
||||
useExtensionService().invokeExtensions(
|
||||
'onNodeOutputsUpdated',
|
||||
app.nodeOutputs
|
||||
)
|
||||
app.nodeOutputs = toRaw(this.outputs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,10 @@ import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import {
|
||||
createNodeExecutionId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
@@ -171,15 +163,6 @@ export interface WorkflowStore {
|
||||
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
|
||||
updateActiveGraph: () => void
|
||||
executionIdToCurrentId: (id: string) => any
|
||||
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
|
||||
nodeExecutionIdToNodeLocatorId: (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
) => NodeLocatorId | null
|
||||
nodeLocatorIdToNodeId: (locatorId: NodeLocatorId | string) => NodeId | null
|
||||
nodeLocatorIdToNodeExecutionId: (
|
||||
locatorId: NodeLocatorId | string,
|
||||
targetSubgraph?: Subgraph
|
||||
) => NodeExecutionId | null
|
||||
}
|
||||
|
||||
export const useWorkflowStore = defineStore('workflow', () => {
|
||||
@@ -490,7 +473,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the execution ID (e.g., "123:456:789")
|
||||
// Parse the hierarchical ID (e.g., "123:456:789")
|
||||
const subgraphNodeIds = id.split(':')
|
||||
|
||||
// Start from the root graph
|
||||
@@ -505,136 +488,6 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
|
||||
watch(activeWorkflow, updateActiveGraph)
|
||||
|
||||
/**
|
||||
* Convert a node ID to a NodeLocatorId
|
||||
* @param nodeId The local node ID
|
||||
* @param subgraph The subgraph containing the node (defaults to active subgraph)
|
||||
* @returns The NodeLocatorId (for root graph nodes, returns the node ID as-is)
|
||||
*/
|
||||
const nodeIdToNodeLocatorId = (
|
||||
nodeId: NodeId,
|
||||
subgraph?: Subgraph
|
||||
): NodeLocatorId => {
|
||||
const targetSubgraph = subgraph ?? activeSubgraph.value
|
||||
if (!targetSubgraph) {
|
||||
// Node is in the root graph, return the node ID as-is
|
||||
return String(nodeId)
|
||||
}
|
||||
|
||||
return createNodeLocatorId(targetSubgraph.id, nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an execution ID to a NodeLocatorId
|
||||
* @param nodeExecutionId The execution node ID (e.g., "123:456:789")
|
||||
* @returns The NodeLocatorId or null if conversion fails
|
||||
*/
|
||||
const nodeExecutionIdToNodeLocatorId = (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
): NodeLocatorId | null => {
|
||||
// Handle simple node IDs (root graph - no colons)
|
||||
if (!nodeExecutionId.includes(':')) {
|
||||
return nodeExecutionId
|
||||
}
|
||||
|
||||
const parts = parseNodeExecutionId(nodeExecutionId)
|
||||
if (!parts || parts.length === 0) return null
|
||||
|
||||
const nodeId = parts[parts.length - 1]
|
||||
const subgraphNodeIds = parts.slice(0, -1)
|
||||
|
||||
if (subgraphNodeIds.length === 0) {
|
||||
// Node is in root graph, return the node ID as-is
|
||||
return String(nodeId)
|
||||
}
|
||||
|
||||
try {
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
comfyApp.graph,
|
||||
subgraphNodeIds.map((id) => String(id))
|
||||
)
|
||||
const immediateSubgraph = subgraphs[subgraphs.length - 1]
|
||||
return createNodeLocatorId(immediateSubgraph.id, nodeId)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the node ID from a NodeLocatorId
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @returns The local node ID or null if invalid
|
||||
*/
|
||||
const nodeLocatorIdToNodeId = (
|
||||
locatorId: NodeLocatorId | string
|
||||
): NodeId | null => {
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
return parsed?.localNodeId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a NodeLocatorId to an execution ID for a specific context
|
||||
* @param locatorId The NodeLocatorId
|
||||
* @param targetSubgraph The subgraph context (defaults to active subgraph)
|
||||
* @returns The execution ID or null if the node is not accessible from the target context
|
||||
*/
|
||||
const nodeLocatorIdToNodeExecutionId = (
|
||||
locatorId: NodeLocatorId | string,
|
||||
targetSubgraph?: Subgraph
|
||||
): NodeExecutionId | null => {
|
||||
const parsed = parseNodeLocatorId(locatorId)
|
||||
if (!parsed) return null
|
||||
|
||||
const { subgraphUuid, localNodeId } = parsed
|
||||
|
||||
// If no subgraph UUID, this is a root graph node
|
||||
if (!subgraphUuid) {
|
||||
return String(localNodeId)
|
||||
}
|
||||
|
||||
// Find the path from root to the subgraph with this UUID
|
||||
const findSubgraphPath = (
|
||||
graph: LGraph | Subgraph,
|
||||
targetUuid: string,
|
||||
path: NodeId[] = []
|
||||
): NodeId[] | null => {
|
||||
if (isSubgraph(graph) && graph.id === targetUuid) {
|
||||
return path
|
||||
}
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (node.isSubgraphNode() && node.subgraph) {
|
||||
const result = findSubgraphPath(node.subgraph, targetUuid, [
|
||||
...path,
|
||||
node.id
|
||||
])
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const path = findSubgraphPath(comfyApp.graph, subgraphUuid)
|
||||
if (!path) return null
|
||||
|
||||
// If we have a target subgraph, check if the path goes through it
|
||||
if (
|
||||
targetSubgraph &&
|
||||
!path.some((_, idx) => {
|
||||
const subgraphs = getSubgraphsFromInstanceIds(
|
||||
comfyApp.graph,
|
||||
path.slice(0, idx + 1).map((id) => String(id))
|
||||
)
|
||||
return subgraphs[subgraphs.length - 1] === targetSubgraph
|
||||
})
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return createNodeExecutionId([...path, localNodeId])
|
||||
}
|
||||
|
||||
return {
|
||||
activeWorkflow,
|
||||
isActive,
|
||||
@@ -661,11 +514,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
isSubgraphActive,
|
||||
activeSubgraph,
|
||||
updateActiveGraph,
|
||||
executionIdToCurrentId,
|
||||
nodeIdToNodeLocatorId,
|
||||
nodeExecutionIdToNodeLocatorId,
|
||||
nodeLocatorIdToNodeId,
|
||||
nodeLocatorIdToNodeExecutionId
|
||||
executionIdToCurrentId
|
||||
}
|
||||
}) satisfies () => WorkflowStore
|
||||
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { AlgoliaNodePack } from '@/types/algoliaTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { SearchMode } from '@/types/searchServiceTypes'
|
||||
|
||||
type WorkflowNodeProperties = ComfyWorkflowJSON['nodes'][0]['properties']
|
||||
|
||||
export type RegistryPack = components['schemas']['Node']
|
||||
export type MergedNodePack = RegistryPack & AlgoliaNodePack
|
||||
export const isMergedNodePack = (
|
||||
nodePack: RegistryPack | AlgoliaNodePack
|
||||
): nodePack is MergedNodePack => 'comfy_nodes' in nodePack
|
||||
|
||||
export type PackField = keyof RegistryPack | null
|
||||
|
||||
export const IsInstallingKey: InjectionKey<Ref<boolean>> =
|
||||
Symbol('isInstalling')
|
||||
|
||||
export enum ManagerWsQueueStatus {
|
||||
DONE = 'done',
|
||||
DONE = 'all-done',
|
||||
IN_PROGRESS = 'in_progress'
|
||||
}
|
||||
|
||||
@@ -52,6 +40,7 @@ export interface SearchOption<T> {
|
||||
|
||||
export type TaskLog = {
|
||||
taskName: string
|
||||
taskId: string
|
||||
logs: string[]
|
||||
}
|
||||
|
||||
@@ -60,185 +49,25 @@ export interface UseNodePacksOptions {
|
||||
maxConcurrent?: number
|
||||
}
|
||||
|
||||
enum ManagerPackState {
|
||||
/** Pack is installed and enabled */
|
||||
INSTALLED = 'installed',
|
||||
/** Pack is installed but disabled */
|
||||
DISABLED = 'disabled',
|
||||
/** Pack is not installed */
|
||||
NOT_INSTALLED = 'not_installed',
|
||||
/** Pack failed to import */
|
||||
IMPORT_FAILED = 'import_failed',
|
||||
/** Pack has an update available */
|
||||
NEEDS_UPDATE = 'needs_update'
|
||||
}
|
||||
// Node pack types from different sources
|
||||
export type RegistryPack = components['schemas']['Node']
|
||||
|
||||
enum ManagerPackInstallType {
|
||||
/** Installed via git clone */
|
||||
GIT = 'git-clone',
|
||||
/** Installed via file copy */
|
||||
COPY = 'copy',
|
||||
/** Installed from the Comfy Registry */
|
||||
REGISTRY = 'cnr'
|
||||
}
|
||||
|
||||
export enum SelectedVersion {
|
||||
/** Latest version of the pack from the registry */
|
||||
LATEST = 'latest',
|
||||
/** Latest commit of the pack from its GitHub repository */
|
||||
NIGHTLY = 'nightly'
|
||||
}
|
||||
|
||||
export enum ManagerChannel {
|
||||
/** All packs except those with instability or security issues */
|
||||
DEFAULT = 'default',
|
||||
/** Packs that were recently updated */
|
||||
RECENT = 'recent',
|
||||
/** Packs that were superseded by distinct replacements of some type */
|
||||
LEGACY = 'legacy',
|
||||
/** Packs that were forked as a result of the original pack going unmaintained */
|
||||
FORKED = 'forked',
|
||||
/** Packs with instability or security issues suitable only for developers */
|
||||
DEV = 'dev',
|
||||
/** Packs suitable for beginners */
|
||||
TUTORIAL = 'tutorial'
|
||||
}
|
||||
|
||||
export enum ManagerDatabaseSource {
|
||||
/** Get pack info from the Comfy Registry */
|
||||
REMOTE = 'remote',
|
||||
/** If set to `local`, the channel is ignored */
|
||||
LOCAL = 'local',
|
||||
/** Get pack info from the cached response from the Comfy Registry (1 day TTL) */
|
||||
CACHE = 'cache'
|
||||
}
|
||||
|
||||
export interface ManagerQueueStatus {
|
||||
/** `done_count` + `in_progress_count` + number of items queued */
|
||||
total_count: number
|
||||
/** Task worker thread is alive, a queued operation is running */
|
||||
is_processing: boolean
|
||||
/** Number of items in the queue that have been completed */
|
||||
done_count: number
|
||||
/** Number of items in the queue that are currently running */
|
||||
in_progress_count: number
|
||||
}
|
||||
|
||||
export interface ManagerPackInfo {
|
||||
/** Either github-author/github-repo or name of pack from the registry (not id) */
|
||||
id: WorkflowNodeProperties['aux_id'] | WorkflowNodeProperties['cnr_id']
|
||||
/** Semantic version or Git commit hash */
|
||||
version: WorkflowNodeProperties['ver']
|
||||
}
|
||||
|
||||
export interface ManagerPackInstalled {
|
||||
/**
|
||||
* The version of the pack that is installed.
|
||||
* Git commit hash or semantic version.
|
||||
*/
|
||||
ver: WorkflowNodeProperties['ver']
|
||||
/**
|
||||
* The name of the pack if the pack is installed from the registry.
|
||||
* Corresponds to `Node#name` in comfy-api.
|
||||
*/
|
||||
cnr_id: WorkflowNodeProperties['cnr_id']
|
||||
/**
|
||||
* The name of the pack if the pack is installed from github.
|
||||
* In the format author/repo-name. If the pack is installed from the registry, this is `null`.
|
||||
*/
|
||||
aux_id: WorkflowNodeProperties['aux_id'] | null
|
||||
enabled: boolean
|
||||
}
|
||||
// MergedNodePack is the intersection of AlgoliaNodePack and RegistryPack
|
||||
// created by lodash merge operation: merge({}, algoliaNodePack, registryPack)
|
||||
export type MergedNodePack = AlgoliaNodePack & RegistryPack
|
||||
|
||||
/**
|
||||
* Returned by `/customnode/installed`
|
||||
* Type guard to check if a node pack is from Algolia (has comfy_nodes)
|
||||
*/
|
||||
export type InstalledPacksResponse = Record<
|
||||
NonNullable<RegistryPack['name']>,
|
||||
ManagerPackInstalled
|
||||
>
|
||||
|
||||
/**
|
||||
* Returned by `/customnode/getlist`
|
||||
*/
|
||||
export interface ManagerPack extends ManagerPackInfo {
|
||||
/** Pack author name or 'Unclaimed' if the pack was added automatically via GitHub crawl. */
|
||||
author: components['schemas']['Node']['author']
|
||||
/** Files included in the pack */
|
||||
files: string[]
|
||||
/** The type of installation that was used to install the pack */
|
||||
reference: string
|
||||
/** The display name of the pack */
|
||||
title: string
|
||||
/** The latest version of the pack */
|
||||
cnr_latest: SelectedVersion
|
||||
/** The github link to the repository of the pack */
|
||||
repository: string
|
||||
/** The state of the pack */
|
||||
state: ManagerPackState
|
||||
/** The state of the pack update */
|
||||
'update-state': 'false' | 'true' | null
|
||||
/** The number of stars the pack has on GitHub. Distinct from registry stars */
|
||||
stars: number
|
||||
/**
|
||||
* The last time the pack was updated. In ISO 8601 format.
|
||||
* @example '2024-05-22 20:00:00'
|
||||
*/
|
||||
last_update: string
|
||||
health: string
|
||||
description: string
|
||||
trust: boolean
|
||||
install_type: ManagerPackInstallType
|
||||
}
|
||||
|
||||
/**
|
||||
* Returned by `/customnode/getmappings`.
|
||||
*/
|
||||
export type ManagerMappings = Record<
|
||||
NonNullable<components['schemas']['Node']['name']>,
|
||||
[
|
||||
/** List of ComfyNode names included in the pack */
|
||||
Array<components['schemas']['ComfyNode']['comfy_node_name']>,
|
||||
{
|
||||
/** The display name of the pack */
|
||||
title_aux: string
|
||||
}
|
||||
]
|
||||
>
|
||||
|
||||
/**
|
||||
* Payload for `/manager/queue/install`
|
||||
*/
|
||||
export interface InstallPackParams extends ManagerPackInfo {
|
||||
/**
|
||||
* Semantic version, Git commit hash, `latest`, or `nightly`.
|
||||
*/
|
||||
selected_version: WorkflowNodeProperties['ver'] | SelectedVersion
|
||||
/**
|
||||
* The GitHub link to the repository of the pack to install.
|
||||
* Required if `selected_version` is `nightly`.
|
||||
*/
|
||||
repository: string
|
||||
/**
|
||||
* List of PyPi dependency names associated with the pack.
|
||||
* Used in coordination with pip package whitelist and version lock features.
|
||||
*/
|
||||
pip?: string[]
|
||||
mode: ManagerDatabaseSource
|
||||
channel: ManagerChannel
|
||||
skip_post_install?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Params for `/manager/queue/update_all`
|
||||
*/
|
||||
export interface UpdateAllPacksParams {
|
||||
mode?: ManagerDatabaseSource
|
||||
export function isMergedNodePack(
|
||||
pack: MergedNodePack | RegistryPack
|
||||
): pack is MergedNodePack {
|
||||
return 'comfy_nodes' in pack && Array.isArray(pack.comfy_nodes)
|
||||
}
|
||||
|
||||
export interface ManagerState {
|
||||
selectedTabId: ManagerTab
|
||||
searchQuery: string
|
||||
searchMode: SearchMode
|
||||
searchMode: 'nodes' | 'packs'
|
||||
sortField: string
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,16 +31,6 @@ export type { ComfyApi } from '@/scripts/api'
|
||||
export type { ComfyApp } from '@/scripts/app'
|
||||
export type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
export type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
export type {
|
||||
NodeLocatorId,
|
||||
NodeExecutionId,
|
||||
isNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
parseNodeLocatorId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
createNodeExecutionId
|
||||
} from './nodeIdentification'
|
||||
export type {
|
||||
EmbeddingsResponse,
|
||||
ExtensionsResponse,
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
|
||||
/**
|
||||
* A globally unique identifier for nodes that maintains consistency across
|
||||
* multiple instances of the same subgraph.
|
||||
*
|
||||
* Format:
|
||||
* - For subgraph nodes: `<immediate-contained-subgraph-uuid>:<local-node-id>`
|
||||
* - For root graph nodes: `<local-node-id>`
|
||||
*
|
||||
* Examples:
|
||||
* - "a1b2c3d4-e5f6-7890-abcd-ef1234567890:123" (node in subgraph)
|
||||
* - "456" (node in root graph)
|
||||
*
|
||||
* Unlike execution IDs which change based on the instance path,
|
||||
* NodeLocatorId remains the same for all instances of a particular node.
|
||||
*/
|
||||
export type NodeLocatorId = string
|
||||
|
||||
/**
|
||||
* An execution identifier representing a node's position in nested subgraphs.
|
||||
* Also known as ExecutionId in some contexts.
|
||||
*
|
||||
* Format: Colon-separated path of node IDs
|
||||
* Example: "123:456:789" (node 789 in subgraph 456 in subgraph 123)
|
||||
*/
|
||||
export type NodeExecutionId = string
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a NodeLocatorId
|
||||
*/
|
||||
export function isNodeLocatorId(value: unknown): value is NodeLocatorId {
|
||||
if (typeof value !== 'string') return false
|
||||
|
||||
// Check if it's a simple node ID (root graph node)
|
||||
const parts = value.split(':')
|
||||
if (parts.length === 1) {
|
||||
// Simple node ID - must be non-empty
|
||||
return value.length > 0
|
||||
}
|
||||
|
||||
// Check for UUID:nodeId format
|
||||
if (parts.length !== 2) return false
|
||||
|
||||
// Check that node ID part is not empty
|
||||
if (!parts[1]) return false
|
||||
|
||||
// Basic UUID format check (8-4-4-4-12 hex characters)
|
||||
const uuidPattern =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
return uuidPattern.test(parts[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a NodeExecutionId
|
||||
*/
|
||||
export function isNodeExecutionId(value: unknown): value is NodeExecutionId {
|
||||
if (typeof value !== 'string') return false
|
||||
// Must contain at least one colon to be an execution ID
|
||||
return value.includes(':')
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a NodeLocatorId into its components
|
||||
* @param id The NodeLocatorId to parse
|
||||
* @returns The subgraph UUID and local node ID, or null if invalid
|
||||
*/
|
||||
export function parseNodeLocatorId(
|
||||
id: string
|
||||
): { subgraphUuid: string | null; localNodeId: NodeId } | null {
|
||||
if (!isNodeLocatorId(id)) return null
|
||||
|
||||
const parts = id.split(':')
|
||||
|
||||
if (parts.length === 1) {
|
||||
// Simple node ID (root graph)
|
||||
return {
|
||||
subgraphUuid: null,
|
||||
localNodeId: isNaN(Number(id)) ? id : Number(id)
|
||||
}
|
||||
}
|
||||
|
||||
const [subgraphUuid, localNodeId] = parts
|
||||
return {
|
||||
subgraphUuid,
|
||||
localNodeId: isNaN(Number(localNodeId)) ? localNodeId : Number(localNodeId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a NodeLocatorId from components
|
||||
* @param subgraphUuid The UUID of the immediate containing subgraph
|
||||
* @param localNodeId The local node ID within that subgraph
|
||||
* @returns A properly formatted NodeLocatorId
|
||||
*/
|
||||
export function createNodeLocatorId(
|
||||
subgraphUuid: string,
|
||||
localNodeId: NodeId
|
||||
): NodeLocatorId {
|
||||
return `${subgraphUuid}:${localNodeId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a NodeExecutionId into its component node IDs
|
||||
* @param id The NodeExecutionId to parse
|
||||
* @returns Array of node IDs from root to target, or null if not an execution ID
|
||||
*/
|
||||
export function parseNodeExecutionId(id: string): NodeId[] | null {
|
||||
if (!isNodeExecutionId(id)) return null
|
||||
|
||||
return id
|
||||
.split(':')
|
||||
.map((part) => (isNaN(Number(part)) ? part : Number(part)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a NodeExecutionId from an array of node IDs
|
||||
* @param nodeIds Array of node IDs from root to target
|
||||
* @returns A properly formatted NodeExecutionId
|
||||
*/
|
||||
export function createNodeExecutionId(nodeIds: NodeId[]): NodeExecutionId {
|
||||
return nodeIds.join(':')
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
import type { LGraph, LGraphNode, Subgraph } from '@comfyorg/litegraph'
|
||||
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { parseNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
/**
|
||||
* Parses an execution ID into its component parts.
|
||||
*
|
||||
* @param executionId - The execution ID (e.g., "123:456:789" or "789")
|
||||
* @returns Array of node IDs in the path, or null if invalid
|
||||
*/
|
||||
export function parseExecutionId(executionId: string): string[] | null {
|
||||
if (!executionId || typeof executionId !== 'string') return null
|
||||
return executionId.split(':').filter((part) => part.length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the local node ID from an execution ID.
|
||||
*
|
||||
* @param executionId - The execution ID (e.g., "123:456:789" or "789")
|
||||
* @returns The local node ID or null if invalid
|
||||
*/
|
||||
export function getLocalNodeIdFromExecutionId(
|
||||
executionId: string
|
||||
): string | null {
|
||||
const parts = parseExecutionId(executionId)
|
||||
return parts ? parts[parts.length - 1] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the subgraph path from an execution ID.
|
||||
*
|
||||
* @param executionId - The execution ID (e.g., "123:456:789" or "789")
|
||||
* @returns Array of subgraph node IDs (excluding the final node ID), or empty array
|
||||
*/
|
||||
export function getSubgraphPathFromExecutionId(executionId: string): string[] {
|
||||
const parts = parseExecutionId(executionId)
|
||||
return parts ? parts.slice(0, -1) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits each node in a graph (non-recursive, single level).
|
||||
*
|
||||
* @param graph - The graph to visit nodes from
|
||||
* @param visitor - Function called for each node
|
||||
*/
|
||||
export function visitGraphNodes(
|
||||
graph: LGraph | Subgraph,
|
||||
visitor: (node: LGraphNode) => void
|
||||
): void {
|
||||
for (const node of graph.nodes) {
|
||||
visitor(node)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses a path of subgraphs to reach a target graph.
|
||||
*
|
||||
* @param startGraph - The graph to start from
|
||||
* @param path - Array of subgraph node IDs to traverse
|
||||
* @returns The target graph or null if path is invalid
|
||||
*/
|
||||
export function traverseSubgraphPath(
|
||||
startGraph: LGraph | Subgraph,
|
||||
path: string[]
|
||||
): LGraph | Subgraph | null {
|
||||
let currentGraph: LGraph | Subgraph = startGraph
|
||||
|
||||
for (const nodeId of path) {
|
||||
const node = currentGraph.getNodeById(nodeId)
|
||||
if (!node?.isSubgraphNode?.() || !node.subgraph) return null
|
||||
currentGraph = node.subgraph
|
||||
}
|
||||
|
||||
return currentGraph
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses all nodes in a graph hierarchy (including subgraphs) and invokes
|
||||
* a callback on each node that has the specified property.
|
||||
*
|
||||
* @param graph - The root graph to start traversal from
|
||||
* @param callbackProperty - The name of the callback property to invoke on each node
|
||||
*/
|
||||
export function triggerCallbackOnAllNodes(
|
||||
graph: LGraph | Subgraph,
|
||||
callbackProperty: keyof LGraphNode
|
||||
): void {
|
||||
visitGraphNodes(graph, (node) => {
|
||||
// Recursively process subgraphs first
|
||||
if (node.isSubgraphNode?.() && node.subgraph) {
|
||||
triggerCallbackOnAllNodes(node.subgraph, callbackProperty)
|
||||
}
|
||||
|
||||
// Invoke callback if it exists on the node
|
||||
const callback = node[callbackProperty]
|
||||
if (typeof callback === 'function') {
|
||||
callback.call(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all nodes in a graph hierarchy (including subgraphs) into a flat array.
|
||||
*
|
||||
* @param graph - The root graph to collect nodes from
|
||||
* @param filter - Optional filter function to include only specific nodes
|
||||
* @returns Array of all nodes in the graph hierarchy
|
||||
*/
|
||||
export function collectAllNodes(
|
||||
graph: LGraph | Subgraph,
|
||||
filter?: (node: LGraphNode) => boolean
|
||||
): LGraphNode[] {
|
||||
const nodes: LGraphNode[] = []
|
||||
|
||||
visitGraphNodes(graph, (node) => {
|
||||
// Recursively collect from subgraphs
|
||||
if (node.isSubgraphNode?.() && node.subgraph) {
|
||||
nodes.push(...collectAllNodes(node.subgraph, filter))
|
||||
}
|
||||
|
||||
// Add node if it passes the filter (or no filter provided)
|
||||
if (!filter || filter(node)) {
|
||||
nodes.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a node by ID anywhere in the graph hierarchy.
|
||||
*
|
||||
* @param graph - The root graph to search
|
||||
* @param nodeId - The ID of the node to find
|
||||
* @returns The node if found, null otherwise
|
||||
*/
|
||||
export function findNodeInHierarchy(
|
||||
graph: LGraph | Subgraph,
|
||||
nodeId: string | number
|
||||
): LGraphNode | null {
|
||||
// Check current graph
|
||||
const node = graph.getNodeById(nodeId)
|
||||
if (node) return node
|
||||
|
||||
// Search in subgraphs
|
||||
for (const node of graph.nodes) {
|
||||
if (node.isSubgraphNode?.() && node.subgraph) {
|
||||
const found = findNodeInHierarchy(node.subgraph, nodeId)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a subgraph by its UUID anywhere in the graph hierarchy.
|
||||
*
|
||||
* @param graph - The root graph to search
|
||||
* @param targetUuid - The UUID of the subgraph to find
|
||||
* @returns The subgraph if found, null otherwise
|
||||
*/
|
||||
export function findSubgraphByUuid(
|
||||
graph: LGraph | Subgraph,
|
||||
targetUuid: string
|
||||
): Subgraph | null {
|
||||
// Check all nodes in the current graph
|
||||
for (const node of graph._nodes) {
|
||||
if (node.isSubgraphNode?.() && node.subgraph) {
|
||||
if (node.subgraph.id === targetUuid) {
|
||||
return node.subgraph
|
||||
}
|
||||
// Recursively search in nested subgraphs
|
||||
const found = findSubgraphByUuid(node.subgraph, targetUuid)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node by its execution ID from anywhere in the graph hierarchy.
|
||||
* Execution IDs use hierarchical format like "123:456:789" for nested nodes.
|
||||
*
|
||||
* @param rootGraph - The root graph to search from
|
||||
* @param executionId - The execution ID (e.g., "123:456:789" or "789")
|
||||
* @returns The node if found, null otherwise
|
||||
*/
|
||||
export function getNodeByExecutionId(
|
||||
rootGraph: LGraph,
|
||||
executionId: string
|
||||
): LGraphNode | null {
|
||||
if (!rootGraph) return null
|
||||
|
||||
const localNodeId = getLocalNodeIdFromExecutionId(executionId)
|
||||
if (!localNodeId) return null
|
||||
|
||||
const subgraphPath = getSubgraphPathFromExecutionId(executionId)
|
||||
|
||||
// If no subgraph path, it's in the root graph
|
||||
if (subgraphPath.length === 0) {
|
||||
return rootGraph.getNodeById(localNodeId) || null
|
||||
}
|
||||
|
||||
// Traverse to the target subgraph
|
||||
const targetGraph = traverseSubgraphPath(rootGraph, subgraphPath)
|
||||
if (!targetGraph) return null
|
||||
|
||||
// Get the node from the target graph
|
||||
return targetGraph.getNodeById(localNodeId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node by its locator ID from anywhere in the graph hierarchy.
|
||||
* Locator IDs use UUID format like "uuid:nodeId" for subgraph nodes.
|
||||
*
|
||||
* @param rootGraph - The root graph to search from
|
||||
* @param locatorId - The locator ID (e.g., "uuid:123" or "123")
|
||||
* @returns The node if found, null otherwise
|
||||
*/
|
||||
export function getNodeByLocatorId(
|
||||
rootGraph: LGraph,
|
||||
locatorId: NodeLocatorId | string
|
||||
): LGraphNode | null {
|
||||
if (!rootGraph) return null
|
||||
|
||||
const parsedIds = parseNodeLocatorId(locatorId)
|
||||
if (!parsedIds) return null
|
||||
|
||||
const { subgraphUuid, localNodeId } = parsedIds
|
||||
|
||||
// If no subgraph UUID, it's in the root graph
|
||||
if (!subgraphUuid) {
|
||||
return rootGraph.getNodeById(localNodeId) || null
|
||||
}
|
||||
|
||||
// Find the subgraph with the matching UUID
|
||||
const targetSubgraph = findSubgraphByUuid(rootGraph, subgraphUuid)
|
||||
if (!targetSubgraph) return null
|
||||
|
||||
return targetSubgraph.getNodeById(localNodeId) || null
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
isFloatInputSpec,
|
||||
isIntInputSpec
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
|
||||
import { lcm } from './mathUtil'
|
||||
|
||||
@@ -139,11 +138,3 @@ export const mergeInputSpec = (
|
||||
|
||||
return mergeCommonInputSpec(spec1, spec2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node definition represents a subgraph node.
|
||||
* Subgraph nodes are created with category='subgraph' and python_module='nodes'.
|
||||
*/
|
||||
export const isSubgraphNode = (nodeDef: ComfyNodeDefImpl): boolean => {
|
||||
return nodeDef.category === 'subgraph' && nodeDef.python_module === 'nodes'
|
||||
}
|
||||
|
||||
@@ -3,20 +3,12 @@ import { nextTick, reactive } from 'vue'
|
||||
|
||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
|
||||
// Mock i18n module
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string, fallback: string) =>
|
||||
key === 'g.nodesRunning' ? 'nodes running' : fallback
|
||||
}))
|
||||
|
||||
// Mock the execution store
|
||||
const executionStore = reactive({
|
||||
isIdle: true,
|
||||
executionProgress: 0,
|
||||
executingNode: null as any,
|
||||
executingNodeProgress: 0,
|
||||
nodeProgressStates: {} as any,
|
||||
activePrompt: null as any
|
||||
executingNodeProgress: 0
|
||||
})
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => executionStore
|
||||
@@ -45,8 +37,6 @@ describe('useBrowserTabTitle', () => {
|
||||
executionStore.executionProgress = 0
|
||||
executionStore.executingNode = null as any
|
||||
executionStore.executingNodeProgress = 0
|
||||
executionStore.nodeProgressStates = {}
|
||||
executionStore.activePrompt = null
|
||||
|
||||
// reset setting and workflow stores
|
||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||
@@ -107,41 +97,13 @@ describe('useBrowserTabTitle', () => {
|
||||
expect(document.title).toBe('[30%]ComfyUI')
|
||||
})
|
||||
|
||||
it('shows node execution title when executing a node using nodeProgressStates', async () => {
|
||||
it('shows node execution title when executing a node', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
|
||||
}
|
||||
executionStore.activePrompt = {
|
||||
workflow: {
|
||||
changeTracker: {
|
||||
activeState: {
|
||||
nodes: [{ id: 1, type: 'Foo' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
executionStore.executingNodeProgress = 0.5
|
||||
executionStore.executingNode = { type: 'Foo' }
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][50%] Foo')
|
||||
})
|
||||
|
||||
it('shows multiple nodes running when multiple nodes are executing', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': {
|
||||
state: 'running',
|
||||
value: 5,
|
||||
max: 10,
|
||||
node: '1',
|
||||
prompt_id: 'test'
|
||||
},
|
||||
'2': { state: 'running', value: 8, max: 10, node: '2', prompt_id: 'test' }
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][2 nodes running]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1022,402 +1022,5 @@ describe('useNodePricing', () => {
|
||||
getRelevantWidgetNames('RecraftGenerateColorFromImageNode')
|
||||
).toEqual(['n'])
|
||||
})
|
||||
|
||||
it('should include relevant widget names for new nodes', () => {
|
||||
const { getRelevantWidgetNames } = useNodePricing()
|
||||
|
||||
expect(getRelevantWidgetNames('RunwayImageToVideoNodeGen3a')).toEqual([
|
||||
'duration'
|
||||
])
|
||||
expect(getRelevantWidgetNames('RunwayImageToVideoNodeGen4')).toEqual([
|
||||
'duration'
|
||||
])
|
||||
expect(getRelevantWidgetNames('RunwayFirstLastFrameNode')).toEqual([
|
||||
'duration'
|
||||
])
|
||||
expect(getRelevantWidgetNames('TripoTextToModelNode')).toEqual([
|
||||
'model',
|
||||
'model_version',
|
||||
'texture_quality'
|
||||
])
|
||||
expect(getRelevantWidgetNames('TripoImageToModelNode')).toEqual([
|
||||
'model',
|
||||
'model_version',
|
||||
'texture_quality'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('New API nodes pricing', () => {
|
||||
describe('RunwayML nodes', () => {
|
||||
it('should return static price for RunwayTextToImageNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('RunwayTextToImageNode')
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.08/Run')
|
||||
})
|
||||
|
||||
it('should calculate dynamic pricing for RunwayImageToVideoNodeGen3a', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
|
||||
{ name: 'duration', value: 10 }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.50/Run') // 0.05 * 10
|
||||
})
|
||||
|
||||
it('should return fallback for RunwayImageToVideoNodeGen3a without duration', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('RunwayImageToVideoNodeGen3a', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05/second')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rodin nodes', () => {
|
||||
it('should return base price for Rodin3D_Regular', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Rodin3D_Regular')
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.4/Run')
|
||||
})
|
||||
|
||||
it('should return addon price for Rodin3D_Detail', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Rodin3D_Detail')
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.2/Run')
|
||||
})
|
||||
|
||||
it('should return addon price for Rodin3D_Smooth', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('Rodin3D_Smooth')
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.2/Run')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tripo nodes', () => {
|
||||
it('should return v2.5 standard pricing for TripoTextToModelNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoTextToModelNode', [
|
||||
{ name: 'model_version', value: 'v2.5-20250123' },
|
||||
{ name: 'texture_quality', value: 'standard' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.2/Run')
|
||||
})
|
||||
|
||||
it('should return v2.5 detailed pricing for TripoTextToModelNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoTextToModelNode', [
|
||||
{ name: 'model_version', value: 'v2.5-20250123' },
|
||||
{ name: 'texture_quality', value: 'detailed' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.3/Run')
|
||||
})
|
||||
|
||||
it('should return v2.0 detailed pricing for TripoImageToModelNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoImageToModelNode', [
|
||||
{ name: 'model_version', value: 'v2.0-20240919' },
|
||||
{ name: 'texture_quality', value: 'detailed' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.4/Run')
|
||||
})
|
||||
|
||||
it('should return legacy pricing for TripoTextToModelNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoTextToModelNode', [
|
||||
{ name: 'model_version', value: 'v1.4-legacy' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.2/Run')
|
||||
})
|
||||
|
||||
it('should return static price for TripoRefineNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoRefineNode')
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.3/Run')
|
||||
})
|
||||
|
||||
it('should return fallback for TripoTextToModelNode without model', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoTextToModelNode', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.2-0.3/Run (varies with model & texture quality)')
|
||||
})
|
||||
|
||||
it('should return texture-based pricing for TripoTextureNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const standardNode = createMockNode('TripoTextureNode', [
|
||||
{ name: 'texture_quality', value: 'standard' }
|
||||
])
|
||||
const detailedNode = createMockNode('TripoTextureNode', [
|
||||
{ name: 'texture_quality', value: 'detailed' }
|
||||
])
|
||||
|
||||
expect(getNodeDisplayPrice(standardNode)).toBe('$0.1/Run')
|
||||
expect(getNodeDisplayPrice(detailedNode)).toBe('$0.2/Run')
|
||||
})
|
||||
|
||||
it('should handle various Tripo model version formats', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
// Test different model version formats
|
||||
const testCases = [
|
||||
{ model: 'v2.0-20240919', expected: '$0.2/Run' },
|
||||
{ model: 'v2.5-20250123', expected: '$0.2/Run' },
|
||||
{ model: 'v1.4', expected: '$0.2/Run' },
|
||||
{ model: 'unknown-model', expected: '$0.2/Run' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ model, expected }) => {
|
||||
const node = createMockNode('TripoTextToModelNode', [
|
||||
{ name: 'model_version', value: model },
|
||||
{ name: 'texture_quality', value: 'standard' }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Gemini and OpenAI Chat nodes', () => {
|
||||
it('should return specific pricing for supported Gemini models', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
model: 'gemini-2.5-pro-preview-05-06',
|
||||
expected: '$0.0035/$0.0008 per 1K tokens'
|
||||
},
|
||||
{
|
||||
model: 'gemini-2.5-flash-preview-04-17',
|
||||
expected: '$0.0015/$0.0004 per 1K tokens'
|
||||
},
|
||||
{ model: 'unknown-gemini-model', expected: 'Token-based' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ model, expected }) => {
|
||||
const node = createMockNode('GeminiNode', [
|
||||
{ name: 'model', value: model }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return per-second pricing for Gemini Veo models', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('GeminiNode', [
|
||||
{ name: 'model', value: 'veo-2.0-generate-001' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.5/second')
|
||||
})
|
||||
|
||||
it('should return fallback for GeminiNode without model widget', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('GeminiNode', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('Token-based')
|
||||
})
|
||||
|
||||
it('should return token-based pricing for OpenAIChatNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIChatNode', [
|
||||
{ name: 'model', value: 'unknown-model' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('Token-based')
|
||||
})
|
||||
|
||||
it('should return correct pricing for all exposed OpenAI models', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
const testCases = [
|
||||
{ model: 'o4-mini', expected: '$0.0011/$0.0044 per 1K tokens' },
|
||||
{ model: 'o1-pro', expected: '$0.15/$0.60 per 1K tokens' },
|
||||
{ model: 'o1', expected: '$0.015/$0.06 per 1K tokens' },
|
||||
{ model: 'o3-mini', expected: '$0.0011/$0.0044 per 1K tokens' },
|
||||
{ model: 'o3', expected: '$0.01/$0.04 per 1K tokens' },
|
||||
{ model: 'gpt-4o', expected: '$0.0025/$0.01 per 1K tokens' },
|
||||
{ model: 'gpt-4.1-nano', expected: '$0.0001/$0.0004 per 1K tokens' },
|
||||
{ model: 'gpt-4.1-mini', expected: '$0.0004/$0.0016 per 1K tokens' },
|
||||
{ model: 'gpt-4.1', expected: '$0.002/$0.008 per 1K tokens' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ model, expected }) => {
|
||||
const node = createMockNode('OpenAIChatNode', [
|
||||
{ name: 'model', value: model }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle model ordering correctly (specific before general)', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
// Test that more specific patterns are matched before general ones
|
||||
const testCases = [
|
||||
{
|
||||
model: 'gpt-4.1-nano-test',
|
||||
expected: '$0.0001/$0.0004 per 1K tokens'
|
||||
},
|
||||
{
|
||||
model: 'gpt-4.1-mini-test',
|
||||
expected: '$0.0004/$0.0016 per 1K tokens'
|
||||
},
|
||||
{ model: 'gpt-4.1-test', expected: '$0.002/$0.008 per 1K tokens' },
|
||||
{ model: 'o1-pro-test', expected: '$0.15/$0.60 per 1K tokens' },
|
||||
{ model: 'o1-test', expected: '$0.015/$0.06 per 1K tokens' },
|
||||
{ model: 'o3-mini-test', expected: '$0.0011/$0.0044 per 1K tokens' },
|
||||
{ model: 'unknown-model', expected: 'Token-based' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ model, expected }) => {
|
||||
const node = createMockNode('OpenAIChatNode', [
|
||||
{ name: 'model', value: model }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return fallback for OpenAIChatNode without model widget', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('OpenAIChatNode', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('Token-based')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Additional RunwayML edge cases', () => {
|
||||
it('should handle edge cases for RunwayML duration-based pricing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
// Test edge cases
|
||||
const testCases = [
|
||||
{ duration: 0, expected: '$0.25/Run' }, // Falls back to 5 seconds (0 || 5)
|
||||
{ duration: 1, expected: '$0.05/Run' },
|
||||
{ duration: 30, expected: '$1.50/Run' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ duration, expected }) => {
|
||||
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
|
||||
{ name: 'duration', value: duration }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle invalid duration values gracefully', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
|
||||
{ name: 'duration', value: 'invalid-string' }
|
||||
])
|
||||
// When Number('invalid-string') returns NaN, it falls back to 5 seconds
|
||||
expect(getNodeDisplayPrice(node)).toBe('$0.25/Run')
|
||||
})
|
||||
|
||||
it('should handle missing duration widget gracefully', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const nodes = [
|
||||
'RunwayImageToVideoNodeGen3a',
|
||||
'RunwayImageToVideoNodeGen4',
|
||||
'RunwayFirstLastFrameNode'
|
||||
]
|
||||
|
||||
nodes.forEach((nodeType) => {
|
||||
const node = createMockNode(nodeType, [])
|
||||
expect(getNodeDisplayPrice(node)).toBe('$0.05/second')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Complete Rodin node coverage', () => {
|
||||
it('should return correct pricing for all Rodin variants', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
const testCases = [
|
||||
{ nodeType: 'Rodin3D_Regular', expected: '$0.4/Run' },
|
||||
{ nodeType: 'Rodin3D_Sketch', expected: '$0.4/Run' },
|
||||
{ nodeType: 'Rodin3D_Detail', expected: '$1.2/Run' },
|
||||
{ nodeType: 'Rodin3D_Smooth', expected: '$1.2/Run' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ nodeType, expected }) => {
|
||||
const node = createMockNode(nodeType)
|
||||
expect(getNodeDisplayPrice(node)).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Comprehensive Tripo edge case testing', () => {
|
||||
it('should handle TripoImageToModelNode with various model versions', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
const testCases = [
|
||||
{ model: 'v1.4-legacy', texture: 'standard', expected: '$0.3/Run' },
|
||||
{ model: 'v2.0-20240919', texture: 'standard', expected: '$0.3/Run' },
|
||||
{ model: 'v2.0-20240919', texture: 'detailed', expected: '$0.4/Run' },
|
||||
{ model: 'v2.5-20250123', texture: 'standard', expected: '$0.3/Run' },
|
||||
{ model: 'v2.5-20250123', texture: 'detailed', expected: '$0.4/Run' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ model, texture, expected }) => {
|
||||
const node = createMockNode('TripoImageToModelNode', [
|
||||
{ name: 'model_version', value: model },
|
||||
{ name: 'texture_quality', value: texture }
|
||||
])
|
||||
expect(getNodeDisplayPrice(node)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return correct fallback for TripoImageToModelNode', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoImageToModelNode', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.3-0.4/Run (varies with model & texture quality)')
|
||||
})
|
||||
|
||||
it('should handle missing texture quality widget', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoTextToModelNode', [
|
||||
{ name: 'model_version', value: 'v2.0-20240919' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.2/Run') // Default to standard texture pricing
|
||||
})
|
||||
|
||||
it('should handle missing model version widget', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('TripoTextToModelNode', [
|
||||
{ name: 'texture_quality', value: 'detailed' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.2-0.3/Run (varies with model & texture quality)')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,22 +24,22 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should initialize with empty logs array', () => {
|
||||
const { logs } = useServerLogs()
|
||||
const { logs } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
expect(logs.value).toEqual([])
|
||||
})
|
||||
|
||||
it('should not subscribe to logs by default', () => {
|
||||
useServerLogs()
|
||||
useServerLogs({ ui_id: 'test-ui-id' })
|
||||
expect(api.subscribeLogs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should subscribe to logs when immediate is true', () => {
|
||||
useServerLogs({ immediate: true })
|
||||
useServerLogs({ ui_id: 'test-ui-id', immediate: true })
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should start listening when startListening is called', async () => {
|
||||
const { startListening } = useServerLogs()
|
||||
const { startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
|
||||
await startListening()
|
||||
|
||||
@@ -47,16 +47,21 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should stop listening when stopListening is called', async () => {
|
||||
const { startListening, stopListening } = useServerLogs()
|
||||
const { startListening, stopListening } = useServerLogs({
|
||||
ui_id: 'test-ui-id'
|
||||
})
|
||||
|
||||
await startListening()
|
||||
await stopListening()
|
||||
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(false)
|
||||
// TODO: Update this test when subscribeLogs(false) is re-enabled
|
||||
// Currently commented out in useServerLogs to prevent logs from stopping
|
||||
// after 1st of multiple queue tasks
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should register event listener when starting', async () => {
|
||||
const { startListening } = useServerLogs()
|
||||
const { startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
|
||||
await startListening()
|
||||
|
||||
@@ -68,16 +73,30 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should handle log messages correctly', async () => {
|
||||
const { logs, startListening } = useServerLogs()
|
||||
const { logs, startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
|
||||
await startListening()
|
||||
|
||||
// Get the callback that was registered with useEventListener
|
||||
const eventCallback = vi.mocked(useEventListener).mock.calls[0][2] as (
|
||||
// Get the callbacks that were registered with useEventListener
|
||||
const mockCalls = vi.mocked(useEventListener).mock.calls
|
||||
const logsCallback = mockCalls.find((call) => call[1] === 'logs')?.[2] as (
|
||||
event: CustomEvent<LogsWsMessage>
|
||||
) => void
|
||||
const taskStartedCallback = mockCalls.find(
|
||||
(call) => call[1] === 'cm-task-started'
|
||||
)?.[2] as (event: CustomEvent<any>) => void
|
||||
|
||||
// Simulate receiving a log event
|
||||
// First, simulate task started event
|
||||
const taskStartedEvent = new CustomEvent('cm-task-started', {
|
||||
detail: {
|
||||
type: 'cm-task-started',
|
||||
ui_id: 'test-ui-id'
|
||||
}
|
||||
})
|
||||
taskStartedCallback(taskStartedEvent)
|
||||
await nextTick()
|
||||
|
||||
// Now simulate receiving a log event
|
||||
const mockEvent = new CustomEvent('logs', {
|
||||
detail: {
|
||||
type: 'logs',
|
||||
@@ -85,7 +104,7 @@ describe('useServerLogs', () => {
|
||||
} as unknown as LogsWsMessage
|
||||
}) as CustomEvent<LogsWsMessage>
|
||||
|
||||
eventCallback(mockEvent)
|
||||
logsCallback(mockEvent)
|
||||
await nextTick()
|
||||
|
||||
expect(logs.value).toEqual(['Log message 1', 'Log message 2'])
|
||||
@@ -93,15 +112,32 @@ describe('useServerLogs', () => {
|
||||
|
||||
it('should use the message filter if provided', async () => {
|
||||
const { logs, startListening } = useServerLogs({
|
||||
ui_id: 'test-ui-id',
|
||||
messageFilter: (msg) => msg !== 'remove me'
|
||||
})
|
||||
|
||||
await startListening()
|
||||
|
||||
const eventCallback = vi.mocked(useEventListener).mock.calls[0][2] as (
|
||||
// Get the callbacks that were registered with useEventListener
|
||||
const mockCalls = vi.mocked(useEventListener).mock.calls
|
||||
const logsCallback = mockCalls.find((call) => call[1] === 'logs')?.[2] as (
|
||||
event: CustomEvent<LogsWsMessage>
|
||||
) => void
|
||||
const taskStartedCallback = mockCalls.find(
|
||||
(call) => call[1] === 'cm-task-started'
|
||||
)?.[2] as (event: CustomEvent<any>) => void
|
||||
|
||||
// First, simulate task started event
|
||||
const taskStartedEvent = new CustomEvent('cm-task-started', {
|
||||
detail: {
|
||||
type: 'cm-task-started',
|
||||
ui_id: 'test-ui-id'
|
||||
}
|
||||
})
|
||||
taskStartedCallback(taskStartedEvent)
|
||||
await nextTick()
|
||||
|
||||
// Now simulate receiving a log event
|
||||
const mockEvent = new CustomEvent('logs', {
|
||||
detail: {
|
||||
type: 'logs',
|
||||
@@ -113,7 +149,7 @@ describe('useServerLogs', () => {
|
||||
} as unknown as LogsWsMessage
|
||||
}) as CustomEvent<LogsWsMessage>
|
||||
|
||||
eventCallback(mockEvent)
|
||||
logsCallback(mockEvent)
|
||||
await nextTick()
|
||||
|
||||
expect(logs.value).toEqual(['Log message 1 dont remove me', ''])
|
||||
|
||||
360
tests-ui/tests/composables/useUpdateAvailableNodes.test.ts
Normal file
360
tests-ui/tests/composables/useUpdateAvailableNodes.test.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
// Import mocked utils
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
// Mock Vue's onMounted to execute immediately for testing
|
||||
vi.mock('vue', async () => {
|
||||
const actual = await vi.importActual('vue')
|
||||
return {
|
||||
...actual,
|
||||
onMounted: (cb: () => void) => cb()
|
||||
}
|
||||
})
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('@/composables/nodePack/useInstalledPacks', () => ({
|
||||
useInstalledPacks: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/formatUtil', () => ({
|
||||
compareVersions: vi.fn(),
|
||||
isSemVer: vi.fn()
|
||||
}))
|
||||
|
||||
const mockUseInstalledPacks = vi.mocked(useInstalledPacks)
|
||||
const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore)
|
||||
|
||||
const mockCompareVersions = vi.mocked(compareVersions)
|
||||
const mockIsSemVer = vi.mocked(isSemVer)
|
||||
|
||||
describe('useUpdateAvailableNodes', () => {
|
||||
const mockInstalledPacks = [
|
||||
{
|
||||
id: 'pack-1',
|
||||
name: 'Outdated Pack',
|
||||
latest_version: { version: '2.0.0' }
|
||||
},
|
||||
{
|
||||
id: 'pack-2',
|
||||
name: 'Up to Date Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
},
|
||||
{
|
||||
id: 'pack-3',
|
||||
name: 'Nightly Pack',
|
||||
latest_version: { version: '1.5.0' }
|
||||
},
|
||||
{
|
||||
id: 'pack-4',
|
||||
name: 'No Latest Version',
|
||||
latest_version: null
|
||||
}
|
||||
]
|
||||
|
||||
const mockStartFetchInstalled = vi.fn()
|
||||
const mockIsPackInstalled = vi.fn()
|
||||
const mockGetInstalledPackVersion = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Default setup
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
mockGetInstalledPackVersion.mockImplementation((id: string) => {
|
||||
switch (id) {
|
||||
case 'pack-1':
|
||||
return '1.0.0' // outdated
|
||||
case 'pack-2':
|
||||
return '1.0.0' // up to date
|
||||
case 'pack-3':
|
||||
return 'nightly-abc123' // nightly
|
||||
case 'pack-4':
|
||||
return '1.0.0' // no latest version
|
||||
default:
|
||||
return '1.0.0'
|
||||
}
|
||||
})
|
||||
|
||||
mockIsSemVer.mockImplementation(
|
||||
(version: string): version is `${number}.${number}.${number}` => {
|
||||
return !version.includes('nightly')
|
||||
}
|
||||
)
|
||||
|
||||
mockCompareVersions.mockImplementation(
|
||||
(latest: string | undefined, installed: string | undefined) => {
|
||||
if (latest === '2.0.0' && installed === '1.0.0') return 1 // outdated
|
||||
if (latest === '1.0.0' && installed === '1.0.0') return 0 // up to date
|
||||
return 0
|
||||
}
|
||||
)
|
||||
|
||||
mockUseComfyManagerStore.mockReturnValue({
|
||||
isPackInstalled: mockIsPackInstalled,
|
||||
getInstalledPackVersion: mockGetInstalledPackVersion
|
||||
} as any)
|
||||
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([]),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
})
|
||||
|
||||
describe('core filtering logic', () => {
|
||||
it('identifies outdated packs correctly', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref(mockInstalledPacks),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
// Should only include pack-1 (outdated)
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(1)
|
||||
expect(updateAvailableNodePacks.value[0].id).toBe('pack-1')
|
||||
})
|
||||
|
||||
it('excludes up-to-date packs', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[1]]), // pack-2: up to date
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('excludes nightly packs from updates', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('excludes packs with no latest version', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[3]]), // pack-4: no latest version
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('excludes uninstalled packs', () => {
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref(mockInstalledPacks),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns empty array when no installed packs exist', () => {
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasUpdateAvailable computed', () => {
|
||||
it('returns true when updates are available', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasUpdateAvailable } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasUpdateAvailable.value).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when no updates are available', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[1]]), // pack-2: up to date
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasUpdateAvailable } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasUpdateAvailable.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('automatic data fetching', () => {
|
||||
it('fetches installed packs automatically when none exist', () => {
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
expect(mockStartFetchInstalled).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not fetch when packs already exist', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref(mockInstalledPacks),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
expect(mockStartFetchInstalled).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not fetch when already loading', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([]),
|
||||
isLoading: ref(true),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
expect(mockStartFetchInstalled).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('state management', () => {
|
||||
it('exposes loading state from useInstalledPacks', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([]),
|
||||
isLoading: ref(true),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { isLoading } = useUpdateAvailableNodes()
|
||||
|
||||
expect(isLoading.value).toBe(true)
|
||||
})
|
||||
|
||||
it('exposes error state from useInstalledPacks', () => {
|
||||
const testError = 'Failed to fetch installed packs'
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([]),
|
||||
isLoading: ref(false),
|
||||
error: ref(testError),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { error } = useUpdateAvailableNodes()
|
||||
|
||||
expect(error.value).toBe(testError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reactivity', () => {
|
||||
it('updates when installed packs change', async () => {
|
||||
const installedPacksRef = ref([])
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: installedPacksRef,
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks, hasUpdateAvailable } =
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
// Initially empty
|
||||
expect(updateAvailableNodePacks.value).toEqual([])
|
||||
expect(hasUpdateAvailable.value).toBe(false)
|
||||
|
||||
// Update installed packs
|
||||
installedPacksRef.value = [mockInstalledPacks[0]] as any // pack-1: outdated
|
||||
await nextTick()
|
||||
|
||||
// Should update available updates
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(1)
|
||||
expect(hasUpdateAvailable.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('version comparison logic', () => {
|
||||
it('calls compareVersions with correct parameters', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
// Access the computed to trigger the logic
|
||||
expect(updateAvailableNodePacks.value).toBeDefined()
|
||||
|
||||
expect(mockCompareVersions).toHaveBeenCalledWith('2.0.0', '1.0.0')
|
||||
})
|
||||
|
||||
it('calls isSemVer to check nightly versions', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
// Access the computed to trigger the logic
|
||||
expect(updateAvailableNodePacks.value).toBeDefined()
|
||||
|
||||
expect(mockIsSemVer).toHaveBeenCalledWith('nightly-abc123')
|
||||
})
|
||||
|
||||
it('calls isPackInstalled for each pack', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref(mockInstalledPacks),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
// Access the computed to trigger the logic
|
||||
expect(updateAvailableNodePacks.value).toBeDefined()
|
||||
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-1')
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-2')
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-3')
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-4')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -28,7 +28,7 @@ describe('useManagerQueue', () => {
|
||||
const getEventListenerCallback = () =>
|
||||
vi.mocked(api.addEventListener).mock.calls[0][1]
|
||||
|
||||
const simulateServerStatus = async (status: 'done' | 'in_progress') => {
|
||||
const simulateServerStatus = async (status: 'all-done' | 'in_progress') => {
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
detail: { status }
|
||||
})
|
||||
@@ -49,7 +49,7 @@ describe('useManagerQueue', () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -104,7 +104,7 @@ describe('useManagerQueue', () => {
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
})
|
||||
|
||||
it('should handle missing status property gracefully', async () => {
|
||||
@@ -119,7 +119,7 @@ describe('useManagerQueue', () => {
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('useManagerQueue', () => {
|
||||
it('should start the next task when server is idle and queue has items', async () => {
|
||||
const { queue, mockTask } = createQueueWithMockTask()
|
||||
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Task should have been started
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
@@ -138,7 +138,7 @@ describe('useManagerQueue', () => {
|
||||
const { mockTask } = createQueueWithMockTask()
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Simulate task completion
|
||||
@@ -148,7 +148,7 @@ describe('useManagerQueue', () => {
|
||||
await simulateServerStatus('in_progress')
|
||||
expect(mockTask.onComplete).not.toHaveBeenCalled()
|
||||
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('useManagerQueue', () => {
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Simulate task completion
|
||||
@@ -167,7 +167,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Simulate server cycle
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Should not throw errors even without onComplete
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
@@ -184,14 +184,14 @@ describe('useManagerQueue', () => {
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
|
||||
// Process first task
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Process second task
|
||||
@@ -201,7 +201,7 @@ describe('useManagerQueue', () => {
|
||||
// Complete second task
|
||||
await mockTask2.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask2.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Queue should be empty and all tasks done
|
||||
@@ -219,7 +219,7 @@ describe('useManagerQueue', () => {
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Let the promise rejection happen
|
||||
@@ -231,7 +231,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Simulate server cycle
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// onComplete should still be called for failed tasks
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
@@ -252,7 +252,7 @@ describe('useManagerQueue', () => {
|
||||
])
|
||||
|
||||
// Task 1
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
@@ -266,7 +266,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Task 2
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
@@ -279,7 +279,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Task 3
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
expect(mockTask3.task).toHaveBeenCalled()
|
||||
@@ -297,7 +297,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Add first task and start processing
|
||||
queue.enqueueTask(mockTask1)
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Add second task while first is processing
|
||||
@@ -307,7 +307,7 @@ describe('useManagerQueue', () => {
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Second task should now be processed
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
@@ -318,9 +318,9 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Cycle server status without any tasks
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Should not cause any errors
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
|
||||
// Mock DOM widget for testing
|
||||
const createMockDOMWidget = (id: string) => {
|
||||
const element = document.createElement('input')
|
||||
return {
|
||||
id,
|
||||
element,
|
||||
node: {
|
||||
id: 'node-1',
|
||||
title: 'Test Node',
|
||||
pos: [0, 0],
|
||||
size: [200, 100]
|
||||
} as any,
|
||||
name: 'test_widget',
|
||||
type: 'text',
|
||||
value: 'test',
|
||||
options: {},
|
||||
y: 0,
|
||||
margin: 10,
|
||||
isVisible: () => true,
|
||||
containerNode: undefined as any
|
||||
}
|
||||
}
|
||||
|
||||
describe('domWidgetStore', () => {
|
||||
let store: ReturnType<typeof useDomWidgetStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
store = useDomWidgetStore()
|
||||
})
|
||||
|
||||
describe('widget registration', () => {
|
||||
it('should register a widget with default state', () => {
|
||||
const widget = createMockDOMWidget('widget-1')
|
||||
|
||||
store.registerWidget(widget)
|
||||
|
||||
expect(store.widgetStates.has('widget-1')).toBe(true)
|
||||
const state = store.widgetStates.get('widget-1')
|
||||
expect(state).toBeDefined()
|
||||
expect(state!.widget).toBe(widget)
|
||||
expect(state!.visible).toBe(true)
|
||||
expect(state!.active).toBe(true)
|
||||
expect(state!.readonly).toBe(false)
|
||||
expect(state!.zIndex).toBe(0)
|
||||
expect(state!.pos).toEqual([0, 0])
|
||||
expect(state!.size).toEqual([0, 0])
|
||||
})
|
||||
|
||||
it('should not register the same widget twice', () => {
|
||||
const widget = createMockDOMWidget('widget-1')
|
||||
|
||||
store.registerWidget(widget)
|
||||
store.registerWidget(widget)
|
||||
|
||||
// Should still only have one entry
|
||||
const states = Array.from(store.widgetStates.values())
|
||||
expect(states.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('widget unregistration', () => {
|
||||
it('should unregister a widget by id', () => {
|
||||
const widget = createMockDOMWidget('widget-1')
|
||||
|
||||
store.registerWidget(widget)
|
||||
expect(store.widgetStates.has('widget-1')).toBe(true)
|
||||
|
||||
store.unregisterWidget('widget-1')
|
||||
expect(store.widgetStates.has('widget-1')).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle unregistering non-existent widget gracefully', () => {
|
||||
// Should not throw
|
||||
expect(() => {
|
||||
store.unregisterWidget('non-existent')
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('widget state management', () => {
|
||||
it('should activate a widget', () => {
|
||||
const widget = createMockDOMWidget('widget-1')
|
||||
store.registerWidget(widget)
|
||||
|
||||
// Set to inactive first
|
||||
const state = store.widgetStates.get('widget-1')!
|
||||
state.active = false
|
||||
|
||||
store.activateWidget('widget-1')
|
||||
expect(state.active).toBe(true)
|
||||
})
|
||||
|
||||
it('should deactivate a widget', () => {
|
||||
const widget = createMockDOMWidget('widget-1')
|
||||
store.registerWidget(widget)
|
||||
|
||||
store.deactivateWidget('widget-1')
|
||||
const state = store.widgetStates.get('widget-1')
|
||||
expect(state!.active).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle activating non-existent widget gracefully', () => {
|
||||
expect(() => {
|
||||
store.activateWidget('non-existent')
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('computed states', () => {
|
||||
it('should separate active and inactive widget states', () => {
|
||||
const widget1 = createMockDOMWidget('widget-1')
|
||||
const widget2 = createMockDOMWidget('widget-2')
|
||||
|
||||
store.registerWidget(widget1)
|
||||
store.registerWidget(widget2)
|
||||
|
||||
// Deactivate widget2
|
||||
store.deactivateWidget('widget-2')
|
||||
|
||||
expect(store.activeWidgetStates.length).toBe(1)
|
||||
expect(store.activeWidgetStates[0].widget.id).toBe('widget-1')
|
||||
|
||||
expect(store.inactiveWidgetStates.length).toBe(1)
|
||||
expect(store.inactiveWidgetStates[0].widget.id).toBe('widget-2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear functionality', () => {
|
||||
it('should clear all widget states', () => {
|
||||
const widget1 = createMockDOMWidget('widget-1')
|
||||
const widget2 = createMockDOMWidget('widget-2')
|
||||
|
||||
store.registerWidget(widget1)
|
||||
store.registerWidget(widget2)
|
||||
|
||||
expect(store.widgetStates.size).toBe(2)
|
||||
|
||||
store.clear()
|
||||
|
||||
expect(store.widgetStates.size).toBe(0)
|
||||
expect(store.activeWidgetStates.length).toBe(0)
|
||||
expect(store.inactiveWidgetStates.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,10 +4,10 @@ import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
InstalledPacksResponse,
|
||||
ManagerPackInstalled
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
|
||||
type ManagerPackInstalled = components['schemas']['ManagerPackInstalled']
|
||||
|
||||
vi.mock('@/services/comfyManagerService', () => ({
|
||||
useComfyManagerService: vi.fn()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user