Merge pull request #38 from space-nuko/output-pane3

Queue/history sidebar
This commit is contained in:
space-nuko
2023-05-12 22:33:46 -05:00
committed by GitHub
44 changed files with 2752 additions and 1149 deletions

View File

@@ -6,8 +6,8 @@ on:
- master
jobs:
build_and_publish:
name: Build
build_and_test:
name: Build and Test
runs-on: ubuntu-latest
@@ -54,3 +54,7 @@ jobs:
run: |
pnpm build:css
pnpm build
- name: Test
run: |
pnpm test

View File

@@ -1,8 +1,6 @@
# ComfyBox
An alternative UI to the backend server of the Stable Diffusion webapp [ComfyUI](https://github.com/comfyanonymous/ComfyUI). Build your workflow in a graph structure and have a custom Stable Diffusion interface created for you automatically.
This project is *still under construction* and many features are missing, be aware of the tradeoffs if you're interested in using it.
ComfyBox is a frontend to Stable Diffusion that lets you create custom image generation interfaces without any code. It uses [ComfyUI](https://github.com/comfyanonymous/ComfyUI) under the hood for maximum power and extensibility.
![Screenshot](./static/screenshot.png)
@@ -10,21 +8,22 @@ This project is *still under construction* and many features are missing, be awa
## Installation
1. Download the latest release [here](https://nightly.link/space-nuko/ComfyBox/workflows/build-and-publish/master/ComfyBox-dist) and extract it somewhere
2. Start the ComfyUI backend with `python main.py --enable-cors-header`
3. In the folder you extracted open the `run.bat`/`run.sh` script (requires Python 3 to be on your PATH). Alternatively you can serve the contents of the folder with a web server.
1. Download the latest release [here](https://nightly.link/space-nuko/ComfyBox/workflows/build-and-publish/master/ComfyBox-dist) and extract it somewhere.
2. Start the ComfyUI backend with `python main.py --enable-cors-header`.
3. In the folder you extracted open the `run.bat`/`run.sh` script (requires Python 3 to be on your PATH). Alternatively you can serve the contents of the folder with any web server.
## NOTE
This frontend isn't compatible with regular ComfyUI's workflow format since extra metadata is saved like panel layout, so you'll have to spend a bit of time recreating them. This project also isn't compatible with regular ComfyUI's frontend extension format, but useful extensions can be integrated into this repo with some effort.
This project is *still under construction* and some features are missing, be aware of the tradeoffs if you're interested in using it.
## Proposed Features
- All the power of ComfyUI with more convenience on top
- Autocreation of UI widgets from your workflow, quickly creating a personalized dashboard
- Arrange the UI however you like and attach custom classes/styles to each widget
- Custom widget types
- See the status of queued and finished generations and their configs in realtime
- Development with TypeScript
This frontend isn't compatible with regular ComfyUI's workflow format since extra metadata is saved like panel layout, so you'll have to spend a bit of time recreating them. This project also isn't compatible with regular ComfyUI's frontend extension format, but useful extensions can be integrated into the base repo with some effort.
## Features
- *No-Code UI Builder* - A novel system for creating your own Stable Diffusion user interfaces from the basic components.
- *Extension Support* - All custom ComfyUI nodes are supported out of the box.
- *Prompt Queue* - Queue up multiple prompts without waiting for them to finish first. Inspect currently queued and executed prompts.
- *Prompt History* - Browse through previously generated prompts and their output images/parameters.
- *Mobile-Friendly Version* - Includes a version of the UI optimized for mobile use, while still supporting the same customized workflows of the desktop version.
## Development

View File

@@ -1,4 +1,3 @@
<!DOCTYPE html>
<html lang="en">
<head>
@@ -9,7 +8,7 @@
<meta name="theme-color" content="#2196f3">
</head>
<body>
<div id="app">
<div id="app" class="mobile">
<script type="module" src='/src/main-mobile.ts'></script>
</body>
</html>

View File

@@ -8,7 +8,8 @@
"preview": "turbo run preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"test:unit": "vitest",
"test": "vitest run",
"test:watch": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"svelte-check": "svelte-check",
@@ -43,6 +44,7 @@
"@gradio/gallery": "workspace:*",
"@gradio/icons": "workspace:*",
"@gradio/image": "workspace:*",
"@gradio/json": "workspace:*",
"@gradio/tabs": "workspace:*",
"@gradio/theme": "workspace:*",
"@gradio/upload": "workspace:*",
@@ -62,6 +64,7 @@
"img-comparison-slider": "^8.0.0",
"pollen-css": "^4.6.2",
"radix-icons-svelte": "^1.2.1",
"svelte-feather-icons": "^4.0.0",
"svelte-preprocess": "^5.0.3",
"svelte-select": "^5.5.3",
"svelte-splitpanes": "^0.7.13",

352
pnpm-lock.yaml generated
View File

@@ -31,6 +31,9 @@ importers:
'@gradio/image':
specifier: workspace:*
version: link:gradio/js/image
'@gradio/json':
specifier: workspace:*
version: link:gradio/js/json
'@gradio/tabs':
specifier: workspace:*
version: link:gradio/js/tabs
@@ -88,6 +91,9 @@ importers:
radix-icons-svelte:
specifier: ^1.2.1
version: 1.2.1
svelte-feather-icons:
specifier: ^4.0.0
version: 4.0.0
svelte-preprocess:
specifier: ^5.0.3
version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
@@ -872,6 +878,28 @@ importers:
specifier: ^4.2.1
version: 4.3.1
litegraph/packages/tests:
dependencies:
'@litegraph-ts/core':
specifier: workspace:*
version: link:../core
'@litegraph-ts/nodes-basic':
specifier: workspace:*
version: link:../nodes-basic
vitest:
specifier: ^0.31.0
version: 0.31.0
devDependencies:
'@litegraph-ts/tsconfig':
specifier: workspace:*
version: link:../tsconfig
typescript:
specifier: ^5.0.3
version: 5.0.3
vite:
specifier: ^4.2.1
version: 4.3.1
litegraph/packages/tsconfig: {}
packages:
@@ -2206,11 +2234,9 @@ packages:
resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==}
dependencies:
'@types/chai': 4.3.4
dev: true
/@types/chai@4.3.4:
resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==}
dev: true
/@types/concat-stream@1.6.1:
resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==}
@@ -2403,6 +2429,45 @@ packages:
eslint-visitor-keys: 3.4.0
dev: true
/@vitest/expect@0.31.0:
resolution: {integrity: sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==}
dependencies:
'@vitest/spy': 0.31.0
'@vitest/utils': 0.31.0
chai: 4.3.7
dev: false
/@vitest/runner@0.31.0:
resolution: {integrity: sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==}
dependencies:
'@vitest/utils': 0.31.0
concordance: 5.0.4
p-limit: 4.0.0
pathe: 1.1.0
dev: false
/@vitest/snapshot@0.31.0:
resolution: {integrity: sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==}
dependencies:
magic-string: 0.30.0
pathe: 1.1.0
pretty-format: 27.5.1
dev: false
/@vitest/spy@0.31.0:
resolution: {integrity: sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==}
dependencies:
tinyspy: 2.1.0
dev: false
/@vitest/utils@0.31.0:
resolution: {integrity: sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==}
dependencies:
concordance: 5.0.4
loupe: 2.3.6
pretty-format: 27.5.1
dev: false
/@zerodevx/svelte-toast@0.9.3(svelte@3.58.0):
resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==}
peerDependencies:
@@ -2422,13 +2487,11 @@ packages:
/acorn-walk@8.2.0:
resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==}
engines: {node: '>=0.4.0'}
dev: true
/acorn@8.8.2:
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
/ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -2448,7 +2511,6 @@ packages:
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
dev: true
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
@@ -2467,7 +2529,6 @@ packages:
/ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
dev: true
/any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -2513,7 +2574,6 @@ packages:
/assertion-error@1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -2655,6 +2715,10 @@ packages:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
/blueimp-md5@2.19.0:
resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==}
dev: false
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
@@ -2712,6 +2776,11 @@ packages:
streamsearch: 1.1.0
dev: true
/cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
dev: false
/call-bind@1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
@@ -2762,7 +2831,6 @@ packages:
loupe: 2.3.6
pathval: 1.1.1
type-detect: 4.0.8
dev: true
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
@@ -2788,7 +2856,6 @@ packages:
/check-error@1.0.2:
resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
dev: true
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
@@ -2965,6 +3032,20 @@ packages:
typedarray: 0.0.6
dev: false
/concordance@5.0.4:
resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==}
engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'}
dependencies:
date-time: 3.1.0
esutils: 2.0.3
fast-diff: 1.2.0
js-string-escape: 1.0.1
lodash: 4.17.21
md5-hex: 3.0.1
semver: 7.5.0
well-known-symbols: 2.0.0
dev: false
/convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
dev: true
@@ -3189,6 +3270,13 @@ packages:
topojson-client: 3.1.0
dev: false
/date-time@3.1.0:
resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==}
engines: {node: '>=6'}
dependencies:
time-zone: 1.0.0
dev: false
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@@ -3209,7 +3297,6 @@ packages:
engines: {node: '>=6'}
dependencies:
type-detect: 4.0.8
dev: true
/deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -3731,7 +3818,6 @@ packages:
/esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
dev: true
/events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
@@ -3819,6 +3905,10 @@ packages:
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
/fast-diff@1.2.0:
resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
dev: false
/fast-glob@3.2.12:
resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
engines: {node: '>=8.6.0'}
@@ -4005,7 +4095,6 @@ packages:
/get-func-name@2.0.0:
resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
dev: true
/get-intrinsic@1.2.0:
resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==}
@@ -4822,6 +4911,11 @@ packages:
resolution: {integrity: sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==}
dev: true
/js-string-escape@1.0.1:
resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==}
engines: {node: '>= 0.8'}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
@@ -4880,6 +4974,10 @@ packages:
hasBin: true
dev: true
/jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: false
/jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
optionalDependencies:
@@ -4943,7 +5041,6 @@ packages:
/local-pkg@0.4.3:
resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
engines: {node: '>=14'}
dev: true
/locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
@@ -4987,7 +5084,6 @@ packages:
resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==}
dependencies:
get-func-name: 2.0.0
dev: true
/lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -5044,6 +5140,13 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: false
/md5-hex@3.0.1:
resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==}
engines: {node: '>=8'}
dependencies:
blueimp-md5: 2.19.0
dev: false
/media-encoder-host-broker@7.0.79:
resolution: {integrity: sha512-uAKboXNaXflOGvHMrj8eOjx3cPJkYDcCOI3mUpAoHBquDzqhgIDovF4yiedCT2YMUPtqPmHOlHUPofH5/3cDmA==}
dependencies:
@@ -5141,6 +5244,15 @@ packages:
hasBin: true
dev: false
/mlly@1.2.1:
resolution: {integrity: sha512-1aMEByaWgBPEbWV2BOPEMySRrzl7rIHXmQxam4DM8jVjalTQDjpN2ZKOLUrwyhfZQO7IXHml2StcHMhooDeEEQ==}
dependencies:
acorn: 8.8.2
pathe: 1.1.0
pkg-types: 1.0.3
ufo: 1.1.2
dev: false
/mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -5268,6 +5380,13 @@ packages:
yocto-queue: 0.1.0
dev: true
/p-limit@4.0.0:
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
yocto-queue: 1.0.0
dev: false
/p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
@@ -5338,9 +5457,12 @@ packages:
engines: {node: '>=8'}
dev: true
/pathe@1.1.0:
resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==}
dev: false
/pathval@1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
dev: true
/performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
@@ -5368,6 +5490,14 @@ packages:
find-up: 4.1.0
dev: true
/pkg-types@1.0.3:
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
dependencies:
jsonc-parser: 3.2.0
mlly: 1.2.1
pathe: 1.1.0
dev: false
/plotly.js-dist-min@2.10.1:
resolution: {integrity: sha512-H0ls1C2uu2U+qWw76djo4/zOGtUKfMILwFhu7tCOaG/wH5ypujrYGCH03N9SQVf1SXcctTfW57USf8LmagSiPQ==}
dev: false
@@ -5477,6 +5607,15 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
/pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
ansi-regex: 5.0.1
ansi-styles: 5.2.0
react-is: 17.0.2
dev: false
/pretty-format@29.5.0:
resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -5539,6 +5678,10 @@ packages:
resolution: {integrity: sha512-svmiMd0ocpdTm9cvAz0klcZpnh639lVctj6psQiawd4pYalVzOG4cX+JizAgRckyTAsRVdzObP7D2EBrSfdghA==}
dev: false
/react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: false
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true
@@ -5783,7 +5926,6 @@ packages:
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
/set-cookie-parser@2.6.0:
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
@@ -5809,6 +5951,10 @@ packages:
object-inspect: 1.12.3
dev: false
/siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
dev: false
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
@@ -5909,6 +6055,10 @@ packages:
escape-string-regexp: 2.0.0
dev: true
/stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
dev: false
/standardized-audio-context@25.3.45:
resolution: {integrity: sha512-d1UVvbz0mDmEqNehvoTKlpSevRJ3YiVZ6kdboeaWX+8cl94H1w8x7c5RNdg0nqxiE049LMeF4tFPuDl5Vm78Kg==}
dependencies:
@@ -5917,6 +6067,10 @@ packages:
tslib: 2.5.0
dev: false
/std-env@3.3.3:
resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==}
dev: false
/streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@@ -5981,7 +6135,6 @@ packages:
resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==}
dependencies:
acorn: 8.8.2
dev: true
/style-mod@4.0.3:
resolution: {integrity: sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==}
@@ -6097,6 +6250,12 @@ packages:
svelte: 3.58.0
dev: true
/svelte-feather-icons@4.0.0:
resolution: {integrity: sha512-4ieUsjp+VYa1r6y80jDt9zRiRUZyJNbESpRdHdJJhiBubyuXX96A7f1UZSK4olxzP6Qsg5ZAuyZlnmvD+/swAA==}
dependencies:
svelte: 3.58.0
dev: false
/svelte-floating-ui@1.2.8:
resolution: {integrity: sha512-8Ifi5CD2Ui7FX7NjJRmutFtXjrB8T/FMNoS2H8P81t5LHK4I9G4NIs007rLWG/nRl7y+zJUXa3tWuTjYXw/O5A==}
dependencies:
@@ -6400,6 +6559,11 @@ packages:
dependencies:
any-promise: 1.3.0
/time-zone@1.0.0:
resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==}
engines: {node: '>=4'}
dev: false
/tiny-glob@0.2.9:
resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
dependencies:
@@ -6413,18 +6577,27 @@ packages:
/tinybench@2.4.0:
resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==}
dev: true
/tinypool@0.3.1:
resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==}
engines: {node: '>=14.0.0'}
dev: true
/tinypool@0.5.0:
resolution: {integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==}
engines: {node: '>=14.0.0'}
dev: false
/tinyspy@1.1.1:
resolution: {integrity: sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==}
engines: {node: '>=14.0.0'}
dev: true
/tinyspy@2.1.0:
resolution: {integrity: sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==}
engines: {node: '>=14.0.0'}
dev: false
/tmpl@1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
dev: true
@@ -6523,7 +6696,6 @@ packages:
/type-detect@4.0.8:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
dev: true
/type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
@@ -6556,6 +6728,10 @@ packages:
engines: {node: '>=12.20'}
hasBin: true
/ufo@1.1.2:
resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==}
dev: false
/undici@5.20.0:
resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==}
engines: {node: '>=12.18'}
@@ -7010,6 +7186,27 @@ packages:
extsprintf: 1.3.0
dev: false
/vite-node@0.31.0(@types/node@18.16.0):
resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==}
engines: {node: '>=v14.18.0'}
hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4
mlly: 1.2.1
pathe: 1.1.0
picocolors: 1.0.0
vite: 4.3.1(@types/node@18.16.0)
transitivePeerDependencies:
- '@types/node'
- less
- sass
- stylus
- sugarss
- supports-color
- terser
dev: false
/vite-plugin-checker@0.5.6(eslint@8.37.0)(typescript@5.0.3)(vite@4.2.1):
resolution: {integrity: sha512-ftRyON0gORUHDxcDt2BErmsikKSkfvl1i2DoP6Jt2zDO9InfvM6tqO1RkXhSjkaXEhKPea6YOnhFaZxW3BzudQ==}
engines: {node: '>=14.16'}
@@ -7251,6 +7448,39 @@ packages:
fsevents: 2.3.2
dev: true
/vite@4.3.1(@types/node@18.16.0):
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
'@types/node': 18.16.0
esbuild: 0.17.18
postcss: 8.4.21
rollup: 3.21.0
optionalDependencies:
fsevents: 2.3.2
dev: false
/vite@4.3.1(@types/node@18.16.0)(sass@1.61.0):
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -7382,6 +7612,71 @@ packages:
- terser
dev: true
/vitest@0.31.0:
resolution: {integrity: sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==}
engines: {node: '>=v14.18.0'}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@vitest/browser': '*'
'@vitest/ui': '*'
happy-dom: '*'
jsdom: '*'
playwright: '*'
safaridriver: '*'
webdriverio: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
playwright:
optional: true
safaridriver:
optional: true
webdriverio:
optional: true
dependencies:
'@types/chai': 4.3.4
'@types/chai-subset': 1.3.3
'@types/node': 18.16.0
'@vitest/expect': 0.31.0
'@vitest/runner': 0.31.0
'@vitest/snapshot': 0.31.0
'@vitest/spy': 0.31.0
'@vitest/utils': 0.31.0
acorn: 8.8.2
acorn-walk: 8.2.0
cac: 6.7.14
chai: 4.3.7
concordance: 5.0.4
debug: 4.3.4
local-pkg: 0.4.3
magic-string: 0.30.0
pathe: 1.1.0
picocolors: 1.0.0
std-env: 3.3.3
strip-literal: 1.0.1
tinybench: 2.4.0
tinypool: 0.5.0
vite: 4.3.1(@types/node@18.16.0)
vite-node: 0.31.0(@types/node@18.16.0)
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
- sass
- stylus
- sugarss
- supports-color
- terser
dev: false
/vscode-jsonrpc@6.0.0:
resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==}
engines: {node: '>=8.0.0 || >=10.0.0'}
@@ -7436,6 +7731,11 @@ packages:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
/well-known-symbols@2.0.0:
resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==}
engines: {node: '>=6'}
dev: false
/whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
@@ -7451,6 +7751,15 @@ packages:
isexe: 2.0.0
dev: true
/why-is-node-running@2.2.2:
resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
engines: {node: '>=8'}
hasBin: true
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
dev: false
/word-wrap@1.2.3:
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
engines: {node: '>=0.10.0'}
@@ -7546,6 +7855,11 @@ packages:
engines: {node: '>=10'}
dev: true
/yocto-queue@1.0.0:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
dev: false
/z-schema@5.0.5:
resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==}
engines: {node: '>=8.0.0'}

View File

@@ -2,8 +2,8 @@
"createdBy": "ComfyBox",
"version": 1,
"workflow": {
"last_node_id": 465,
"last_link_id": 719,
"last_node_id": 467,
"last_link_id": 721,
"nodes": [
{
"id": 35,
@@ -57,11 +57,11 @@
"hidden": false
},
"widgets_values": [
"worst quality"
"lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, deformed, disfigured, poorly drawn face, mutation, mutated, extra limb, poorly drawn hands, missing limb, floating limbs, disconnected limbs, malformed hands, out of focus, long neck, long body, fuzzy, abstract"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": "worst quality",
"comfyValue": "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, deformed, disfigured, poorly drawn face, mutation, mutated, extra limb, poorly drawn hands, missing limb, floating limbs, disconnected limbs, malformed hands, out of focus, long neck, long body, fuzzy, abstract",
"shownOutputProperties": {},
"saveUserState": true
},
@@ -299,11 +299,11 @@
"hidden": false
},
"widgets_values": [
"normal"
"karras"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": "normal",
"comfyValue": "karras",
"shownOutputProperties": {},
"saveUserState": true
},
@@ -464,11 +464,11 @@
"hidden": false
},
"widgets_values": [
"euler"
"dpmpp_2s_ancestral"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": "euler",
"comfyValue": "dpmpp_2s_ancestral",
"shownOutputProperties": {},
"saveUserState": true
},
@@ -548,11 +548,11 @@
"hidden": true
},
"widgets_values": [
"0.650"
"0.630"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": 0.65,
"comfyValue": 0.63,
"shownOutputProperties": {
"min": {
"type": "number",
@@ -687,14 +687,14 @@
"flags": {
"collapsed": true
},
"order": 245,
"order": 247,
"mode": 0,
"inputs": [
{
"name": "value",
"type": 0,
"link": 87,
"label": "5.000"
"label": "1.000"
}
],
"outputs": [],
@@ -816,11 +816,11 @@
"multiline": false
},
"widgets_values": [
"none"
"cached"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": "none",
"comfyValue": "cached",
"shownOutputProperties": {},
"saveUserState": true
},
@@ -971,7 +971,7 @@
66
],
"flags": {},
"order": 244,
"order": 246,
"mode": 0,
"inputs": [
{
@@ -1275,11 +1275,11 @@
"multiline": false
},
"widgets_values": [
"2"
"1536"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": "2",
"comfyValue": "1536",
"shownOutputProperties": {},
"saveUserState": true
},
@@ -1370,11 +1370,11 @@
"multiline": false
},
"widgets_values": [
"2"
"1024"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": "2",
"comfyValue": "1024",
"shownOutputProperties": {},
"saveUserState": true
},
@@ -2196,7 +2196,7 @@
106
],
"flags": {},
"order": 250,
"order": 252,
"mode": 0,
"inputs": [
{
@@ -2274,7 +2274,7 @@
46
],
"flags": {},
"order": 251,
"order": 253,
"mode": 0,
"inputs": [
{
@@ -2930,7 +2930,7 @@
126
],
"flags": {},
"order": 243,
"order": 245,
"mode": 0,
"inputs": [
{
@@ -2994,7 +2994,7 @@
"title": "Comfy.ValueControl",
"properties": {
"tags": [],
"value": 149773913879886,
"value": 485829053103511,
"action": "randomize",
"min": 0,
"max": 18446744073709552000,
@@ -3091,11 +3091,11 @@
"hidden": false
},
"widgets_values": [
"149773913879886.000"
"485829053103511.000"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": 149773913879886,
"comfyValue": 485829053103511,
"shownOutputProperties": {
"min": {
"type": "number",
@@ -3203,7 +3203,7 @@
"outputs": [
{
"name": "",
"type": "number",
"type": "*",
"links": [
299
],
@@ -3851,11 +3851,11 @@
"hidden": false
},
"widgets_values": [
"512.000"
"768.000"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": 512,
"comfyValue": 768,
"shownOutputProperties": {
"min": {
"type": "number",
@@ -4161,7 +4161,7 @@
"title": "Operation",
"properties": {
"A": 2,
"B": 1,
"B": 768,
"OP": "*",
"tags": []
}
@@ -4207,7 +4207,7 @@
"title": "Operation",
"properties": {
"A": 2,
"B": 1,
"B": 512,
"OP": "*",
"tags": []
}
@@ -4233,7 +4233,7 @@
"name": "value",
"type": 0,
"link": 184,
"label": "null"
"label": "22.000"
}
],
"outputs": [],
@@ -4697,7 +4697,7 @@
"outputs": [
{
"name": "",
"type": "number",
"type": "*",
"links": [
353
],
@@ -4747,7 +4747,7 @@
"outputs": [
{
"name": "",
"type": "number",
"type": "*",
"links": [
354
],
@@ -6044,7 +6044,7 @@
"flags": {
"collapsed": true
},
"order": 246,
"order": 248,
"mode": 0,
"inputs": [
{
@@ -6615,7 +6615,7 @@
"flags": {
"collapsed": true
},
"order": 248,
"order": 250,
"mode": 2,
"inputs": [
{
@@ -6719,7 +6719,7 @@
"flags": {
"collapsed": true
},
"order": 249,
"order": 251,
"mode": 2,
"inputs": [
{
@@ -10087,11 +10087,11 @@
"hidden": false
},
"widgets_values": [
"a fluffy corgi wearing sunglasses"
"masterpiece, hyper detailed background, 1girl, solo a fluffy corgi girl wearing sunglasses, dark theme, baggy nylon jacket, gyaru, cyberpunk, airy, futuristic city, animal ear fluff, animal ears, smile, v, trees, leaves, nature, forest, overgrowth"
],
"color": "#223",
"bgColor": "#335",
"comfyValue": "a fluffy corgi wearing sunglasses",
"comfyValue": "masterpiece, hyper detailed background, 1girl, solo a fluffy corgi girl wearing sunglasses, dark theme, baggy nylon jacket, gyaru, cyberpunk, airy, futuristic city, animal ear fluff, animal ears, smile, v, trees, leaves, nature, forest, overgrowth",
"shownOutputProperties": {},
"saveUserState": true
},
@@ -11554,7 +11554,7 @@
"properties": {
"tags": [],
"defaultValue": null,
"index": null,
"index": 4,
"updateMode": "append",
"values": []
},
@@ -12163,89 +12163,6 @@
},
"saveUserState": true
},
{
"id": 104,
"type": "ui/gallery",
"pos": [
862.248693361997,
640.4778575549986
],
"size": [
210,
166
],
"flags": {},
"order": 122,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "OUTPUT",
"link": null
},
{
"name": "store",
"type": -1,
"link": 243,
"shape": 1
},
{
"name": "clear",
"type": -1,
"link": 657,
"shape": 1
}
],
"outputs": [
{
"name": "selected_index",
"type": "number",
"links": [
168,
184
],
"slot_index": 0
},
{
"name": "width",
"type": "number",
"links": [
356
],
"slot_index": 1
},
{
"name": "height",
"type": "number",
"links": [
357
],
"slot_index": 2
},
{
"name": "filename",
"type": "string",
"links": [
670
],
"slot_index": 3
}
],
"title": "UI.Gallery",
"properties": {
"tags": [],
"defaultValue": null,
"index": null,
"updateMode": "append",
"values": []
},
"widgets_values": [],
"color": "#223",
"bgColor": "#335",
"comfyValue": [],
"shownOutputProperties": {},
"saveUserState": false
},
{
"id": 116,
"type": "image/cache",
@@ -12935,82 +12852,6 @@
},
"saveUserState": true
},
{
"id": 418,
"type": "ui/image_upload",
"pos": [
-555,
1286
],
"size": [
210,
138
],
"flags": {},
"order": 225,
"mode": 2,
"inputs": [
{
"name": "store",
"type": -1,
"link": 699,
"shape": 1,
"slot_index": 0
}
],
"outputs": [
{
"name": "filename",
"type": "string",
"links": [
627,
628
],
"slot_index": 0
},
{
"name": "width",
"type": "number",
"links": [
630
],
"slot_index": 1
},
{
"name": "height",
"type": "number",
"links": [
629
],
"slot_index": 2
},
{
"name": "image_count",
"type": "number",
"links": null
},
{
"name": "changed",
"type": -2,
"links": null,
"shape": 1
}
],
"title": "UI.ImageUpload",
"properties": {
"defaultValue": null,
"tags": [
"i2i"
],
"fileCount": "single"
},
"widgets_values": [],
"color": "#223",
"bgColor": "#335",
"comfyValue": [],
"shownOutputProperties": {},
"saveUserState": false
},
{
"id": 74,
"type": "CheckpointLoaderSimple",
@@ -13342,7 +13183,7 @@
106
],
"flags": {},
"order": 247,
"order": 249,
"mode": 2,
"inputs": [
{
@@ -14081,6 +13922,234 @@
"color": "#432",
"bgColor": "#653",
"saveUserState": true
},
{
"id": 418,
"type": "ui/image_upload",
"pos": [
-555,
1286
],
"size": [
210,
138
],
"flags": {},
"order": 225,
"mode": 2,
"inputs": [
{
"name": "store",
"type": -1,
"link": 699,
"shape": 1,
"slot_index": 0
}
],
"outputs": [
{
"name": "filename",
"type": "string",
"links": [
627,
628,
720
],
"slot_index": 0
},
{
"name": "width",
"type": "number",
"links": [
630
],
"slot_index": 1
},
{
"name": "height",
"type": "number",
"links": [
629
],
"slot_index": 2
},
{
"name": "image_count",
"type": "number",
"links": null
},
{
"name": "changed",
"type": -2,
"links": null,
"shape": 1
}
],
"title": "UI.ImageUpload",
"properties": {
"defaultValue": null,
"tags": [
"i2i"
],
"fileCount": "single"
},
"widgets_values": [],
"color": "#223",
"bgColor": "#335",
"comfyValue": [],
"shownOutputProperties": {},
"saveUserState": false
},
{
"id": 466,
"type": "actions/set_prompt_thumbnails",
"pos": [
-328,
1334
],
"size": [
260.4,
26
],
"flags": {
"collapsed": true
},
"order": 243,
"mode": 2,
"inputs": [
{
"name": "filenames",
"type": "*",
"link": 720
}
],
"outputs": [],
"title": "Comfy.SetPromptThumbnailsAction",
"properties": {
"tags": [
"txt2img",
"i2i"
],
"defaultFolderType": "input"
},
"saveUserState": true
},
{
"id": 104,
"type": "ui/gallery",
"pos": [
862.248693361997,
640.4778575549986
],
"size": [
210,
166
],
"flags": {},
"order": 122,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "OUTPUT",
"link": null
},
{
"name": "store",
"type": -1,
"link": 243,
"shape": 1
},
{
"name": "clear",
"type": -1,
"link": 657,
"shape": 1
}
],
"outputs": [
{
"name": "selected_index",
"type": "number",
"links": [
168,
184
],
"slot_index": 0
},
{
"name": "width",
"type": "number",
"links": [
356
],
"slot_index": 1
},
{
"name": "height",
"type": "number",
"links": [
357
],
"slot_index": 2
},
{
"name": "filename",
"type": "string",
"links": [
670,
721
],
"slot_index": 3
}
],
"title": "UI.Gallery",
"properties": {
"tags": [],
"defaultValue": null,
"index": 22,
"updateMode": "append",
"values": []
},
"widgets_values": [],
"color": "#223",
"bgColor": "#335",
"comfyValue": [],
"shownOutputProperties": {},
"saveUserState": false
},
{
"id": 467,
"type": "actions/set_prompt_thumbnails",
"pos": [
1116,
772
],
"size": [
260.4,
26
],
"flags": {
"collapsed": true
},
"order": 244,
"mode": 0,
"inputs": [
{
"name": "filenames",
"type": "*",
"link": 721
}
],
"outputs": [],
"title": "Comfy.SetPromptThumbnailsAction",
"properties": {
"tags": [
"hr"
],
"defaultFolderType": "input"
},
"saveUserState": true
}
],
"links": [
@@ -16731,6 +16800,22 @@
119,
0,
"MODEL"
],
[
720,
418,
0,
466,
0,
"*"
],
[
721,
104,
3,
467,
0,
"*"
]
],
"groups": [
@@ -20051,9 +20136,9 @@
},
"canvas": {
"offset": [
269.1028161789952,
-277.9338410386712
55.778573178994975,
-62.68933903867331
],
"scale": 0.5644739300537776
"scale": 0.6209213230591556
}
}

View File

@@ -1,4 +1,4 @@
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas } from "@litegraph-ts/core";
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas, type LGraphRemoveNodeOptions } from "@litegraph-ts/core";
import GraphSync from "./GraphSync";
import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
@@ -22,14 +22,8 @@ type ComfyGraphEvents = {
}
export default class ComfyGraph extends LGraph {
graphSync: GraphSync;
eventBus: TypedEmitter<ComfyGraphEvents> = new EventEmitter() as TypedEmitter<ComfyGraphEvents>;
constructor() {
super();
this.graphSync = new GraphSync(this)
}
override onConfigure() {
console.debug("Configured");
this.eventBus.emit("configured", this);
@@ -50,8 +44,7 @@ export default class ComfyGraph extends LGraph {
}
override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
layoutState.nodeAdded(node)
this.graphSync.onNodeAdded(node);
layoutState.nodeAdded(node, options)
// All nodes whether they come from base litegraph or ComfyBox should
// have tags added to them. Can't override serialization for existing
@@ -92,7 +85,7 @@ export default class ComfyGraph extends LGraph {
if (get(uiState).autoAddUI) {
console.warn("ADD", node.type, options)
if (!("svelteComponentType" in node) && options.addedByDeserialize == null) {
if (!("svelteComponentType" in node) && options.addedBy == null) {
console.debug("[ComfyGraph] AutoAdd UI")
const comfyNode = node as ComfyGraphNode;
const widgetNodesAdded = []
@@ -127,9 +120,8 @@ export default class ComfyGraph extends LGraph {
this.eventBus.emit("nodeAdded", node);
}
override onNodeRemoved(node: LGraphNode) {
layoutState.nodeRemoved(node);
this.graphSync.onNodeRemoved(node);
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
layoutState.nodeRemoved(node, options);
console.debug("Removed", node);
this.eventBus.emit("nodeRemoved", node);

View File

@@ -1,4 +1,4 @@
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem } from "@litegraph-ts/core";
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph } from "@litegraph-ts/core";
import type ComfyApp from "./components/ComfyApp";
import queueState from "./stores/queueState";
import { get } from "svelte/store";
@@ -60,7 +60,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
let state = get(queueState);
let color = null;
if (node.id === +state.runningNodeId) {
if (node.id === +state.runningNodeID) {
color = "#0f0";
// this.app can be null inside the constructor if rendering is taking place already
} else if (this.app && this.app.dragOverNode && node.id === this.app.dragOverNode.id) {
@@ -322,6 +322,34 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
}
}
private convertToSubgraph(_value: IContextMenuItem, _options, mouseEvent, prevMenu, callback?: (node: LGraphNode) => void) {
if (Object.keys(this.selected_nodes).length === 0)
return
const selected = Object.values(this.selected_nodes).filter(n => n != null);
this.selected_nodes = {}
const subgraph = LiteGraph.createNode(Subgraph);
subgraph.buildFromNodes(selected)
this.graph.add(subgraph)
}
override getCanvasMenuOptions(): ContextMenuItem[] {
const options = super.getCanvasMenuOptions();
options.push(
{
content: "Convert to Subgraph",
has_submenu: false,
disabled: Object.keys(this.selected_nodes).length === 0,
callback: this.convertToSubgraph.bind(this)
},
)
return options
}
override getNodeMenuOptions(node: LGraphNode): ContextMenuItem[] {
const options = super.getNodeMenuOptions(node);

View File

@@ -1,77 +0,0 @@
import type { LGraph, LGraphNode } from "@litegraph-ts/core";
import type ComfyApp from "./components/ComfyApp";
import type { Unsubscriber, Writable } from "svelte/store";
import type { ComfyWidgetNode } from "./nodes";
import type ComfyGraph from "./ComfyGraph";
type WidgetSubStore = {
store: WidgetUIStateStore,
unsubscribe: Unsubscriber
}
/*
* Responsible for watching for and synchronizing state changes from the
* frontend to the litegraph instance.
*
* The other way around is unnecessary since the nodes in ComfyBox can't be
* interacted with. If that were true the implementation would be way more
* complex since litegraph doesn't (currently) expose a global
* event-emitter-like thing for when nodes/widgets are changed.
*
* Assumptions:
* - Widgets can't be added to a node after they're created (messes up the indices in WidgetSubStore[])
* - Widgets can't be interacted with from the graph, only from the frontend
* - Only one workflow/graph can ever be loaded into the program
*/
export default class GraphSync {
graph: LGraph;
// nodeId -> widgetSubStore
private stores: Record<string, WidgetSubStore> = {}
constructor(graph: ComfyGraph) {
this.graph = graph;
}
onNodeAdded(node: LGraphNode) {
// TODO assumes only a single graph's widget state.
if ("svelteComponentType" in node) {
this.addStore(node as ComfyWidgetNode);
}
this.graph.setDirtyCanvas(true, true);
}
onNodeRemoved(node: LGraphNode) {
if ("svelteComponentType" in node) {
this.removeStore(node as ComfyWidgetNode);
}
this.graph.setDirtyCanvas(true, true);
}
private addStore(node: ComfyWidgetNode) {
if (this.stores[node.id]) {
console.warn("[GraphSync] Stores already exist!", node.id, this.stores[node.id])
}
const unsub = node.value.subscribe((v) => this.onWidgetStateChanged(node, v))
this.stores[node.id] = ({ store: node.value, unsubscribe: unsub });
console.debug("[GraphSync] NEWSTORE", this.stores[node.id])
}
private removeStore(node: ComfyWidgetNode) {
console.debug("[GraphSync] DELSTORE", this.stores[node.id])
this.stores[node.id].unsubscribe()
delete this.stores[node.id]
}
/*
* Fired when a single widget's value changes.
*/
private onWidgetStateChanged(node: ComfyWidgetNode, value: any) {
this.graph.setDirtyCanvas(true, true);
}
}

View File

@@ -1,6 +1,10 @@
import { negmod } from "./utils";
export class ImageViewer {
root: HTMLDivElement;
lightboxModal: HTMLDivElement;
currentImages: string[] = []
selectedIndex: number = -1;
currentGallery: HTMLDivElement | null = null;
private static _instance: ImageViewer;
@@ -22,6 +26,8 @@ export class ImageViewer {
// A full size 'lightbox' preview modal shown when left clicking on gallery previews
closeModal() {
this.lightboxModal.style.display = "none";
this.currentImages = []
this.selectedIndex = -1;
this.currentGallery = null;
}
@@ -36,84 +42,75 @@ export class ImageViewer {
return visibleGalleryButtons;
}
static selected_gallery_button(gallery: HTMLDivElement): HTMLButtonElement | null {
var allCurrentButtons = gallery.querySelectorAll('.preview > .thumbnails > .thumbnail-item.thumbnail-small.selected');
static selected_gallery_button(gallery: HTMLDivElement): [HTMLButtonElement | null, number] {
var allCurrentButtons = gallery.querySelectorAll('.preview > .thumbnails > .thumbnail-item.thumbnail-small');
console.log(allCurrentButtons)
var visibleCurrentButton = null;
allCurrentButtons.forEach((elem) => {
if (elem.parentElement.offsetParent) {
let index = -1;
allCurrentButtons.forEach((elem, i) => {
if (elem.parentElement.offsetParent && elem.classList.contains("selected")) {
visibleCurrentButton = elem;
index = i;
}
})
return visibleCurrentButton;
return [visibleCurrentButton, index];
}
showModal(event: Event) {
const source = (event.target || event.srcElement) as HTMLImageElement;
const galleryElem = source.closest<HTMLDivElement>("div.block")
console.debug("[ImageViewer] showModal", event, source, galleryElem);
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
console.error("No buttons found on gallery element!", galleryElem)
return;
}
showModal(imageUrls: string[], index: number, galleryElem?: HTMLDivElement) {
this.currentImages = imageUrls
this.selectedIndex = index
this.currentGallery = galleryElem;
this.modalImage.src = source.src
if (this.modalImage.style.display === 'none') {
this.lightboxModal.style.setProperty('background-image', 'url(' + source.src + ')');
}
this.setModalImageSrc(imageUrls[index])
this.lightboxModal.style.display = "flex";
setTimeout(() => {
this.modalImage.focus()
}, 200)
event.stopPropagation()
}
static negmod(n: number, m: number) {
return ((n % m) + m) % m;
static get_gallery_urls(galleryElem: HTMLDivElement): string[] {
return ImageViewer.all_gallery_buttons(galleryElem)
.map(b => (b.children[0] as HTMLImageElement).src)
}
updateOnBackgroundChange() {
refreshImages() {
if (this.currentGallery) {
this.currentImages = ImageViewer.get_gallery_urls(this.currentGallery)
let [_currentButton, index] = ImageViewer.selected_gallery_button(this.currentGallery);
this.selectedIndex = index;
}
const selectedImageUrl = this.currentImages[this.selectedIndex];
this.setModalImageSrc(selectedImageUrl)
}
private setModalImageSrc(src: string, isTiling: boolean = false) {
const modalImage = this.modalImage
if (modalImage && modalImage.offsetParent && this.currentGallery) {
let currentButton = ImageViewer.selected_gallery_button(this.currentGallery);
if (currentButton?.children?.length > 0 && modalImage.src != currentButton.children[0].src) {
modalImage.src = currentButton.children[0].src;
if (modalImage.style.display === 'none') {
this.lightboxModal.style.setProperty('background-image', `url(${modalImage.src})`)
}
}
const modal = this.lightboxModal
modalImage.src = src;
if (isTiling) {
modalImage.style.display = 'none';
modal.style.setProperty('background-image', `url(${modalImage.src})`)
} else {
modalImage.style.display = 'block';
modal.style.setProperty('background-image', 'none')
}
}
modalImageSwitch(offset: number) {
if (!this.currentGallery)
return
this.selectedIndex = negmod(this.selectedIndex + offset, this.currentImages.length);
const selectedImageUrl = this.currentImages[this.selectedIndex];
var galleryButtons = ImageViewer.all_gallery_buttons(this.currentGallery);
this.setModalImageSrc(selectedImageUrl)
if (galleryButtons.length > 1) {
var currentButton = ImageViewer.selected_gallery_button(this.currentGallery);
var result = -1
galleryButtons.forEach((v, i) => {
if (v == currentButton) {
result = i
}
})
if (result != -1) {
const nextButton = galleryButtons[ImageViewer.negmod((result + offset), galleryButtons.length)]
if (this.currentGallery) {
const galleryButtons = ImageViewer.all_gallery_buttons(this.currentGallery);
const nextButton = galleryButtons[this.selectedIndex];
if (nextButton) {
nextButton.click()
const modalImage = this.modalImage;
const modal = this.lightboxModal
modalImage.src = nextButton.children[0].src;
if (modalImage.style.display === 'none') {
modal.style.setProperty('background-image', `url(${modalImage.src})`)
}
setTimeout(() => { modal.focus() }, 10)
}
}
setTimeout(() => { this.lightboxModal.focus() }, 10)
}
modalNextImage(event) {
@@ -140,7 +137,7 @@ export class ImageViewer {
}
}
setupImageForLightbox(e: HTMLImageElement) {
setupGalleryImageForLightbox(e: HTMLImageElement) {
if (e.dataset.modded === "true")
return;
@@ -161,7 +158,22 @@ export class ImageViewer {
const initiallyZoomed = true
this.modalZoomSet(this.modalImage, initiallyZoomed)
evt.preventDefault()
this.showModal(evt)
const source = evt.target as HTMLImageElement;
const galleryElem = source.closest<HTMLDivElement>("div.block")
console.debug("[ImageViewer] showModal", event, source, galleryElem);
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
console.error("No buttons found on gallery element!", galleryElem)
return;
}
let urls = ImageViewer.get_gallery_urls(galleryElem)
const [_currentButton, index] = ImageViewer.selected_gallery_button(galleryElem)
console.warn("Gallery!", index, urls, galleryElem)
this.showModal(urls, index, galleryElem)
evt.stopPropagation();
}, true);
}
@@ -181,17 +193,8 @@ export class ImageViewer {
}
modalTileImageToggle(event: Event) {
const modalImage = this.modalImage
const modal = this.lightboxModal
const isTiling = modalImage.style.display === 'none';
if (isTiling) {
modalImage.style.display = 'block';
modal.style.setProperty('background-image', 'none')
} else {
modalImage.style.display = 'none';
modal.style.setProperty('background-image', `url(${modalImage.src})`)
}
const isTiling = this.modalImage.style.display === 'none';
this.setModalImageSrc(this.modalImage.src, isTiling)
event.stopPropagation()
}
}

View File

@@ -1,34 +1,92 @@
type PromptRequestBody = {
client_id: string,
prompt: any,
extra_data: any,
front: boolean,
number: number | undefined
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp";
import type TypedEmitter from "typed-emitter";
import EventEmitter from "events";
import type { GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
import type { SerializedLGraph } from "@litegraph-ts/core";
export type ComfyPromptRequest = {
client_id?: string,
prompt: SerializedPromptInputsAll,
extra_data: ComfyPromptExtraData,
front?: boolean,
number?: number
}
export type QueueItemType = "queue" | "history";
export type ComfyAPIQueueStatus = {
exec_info: {
queue_remaining: number | "X";
}
export type ComfyAPIStatusExecInfo = {
queueRemaining: number | "X";
}
export default class ComfyAPI extends EventTarget {
private registered: Set<string> = new Set<string>();
export type ComfyAPIStatusResponse = {
execInfo?: ComfyAPIStatusExecInfo,
error?: string
}
export type ComfyAPIQueueResponse = {
running: ComfyAPIHistoryItem[],
pending: ComfyAPIHistoryItem[],
error?: string
}
export type NodeID = string;
export type PromptID = string; // UUID
export type ComfyAPIHistoryItem = [
number, // prompt number
PromptID,
SerializedPromptInputsAll,
ComfyPromptExtraData,
NodeID[] // good outputs
]
export type ComfyAPIPromptResponse = {
promptID?: PromptID,
error?: string
}
export type ComfyAPIHistoryEntry = {
prompt: ComfyAPIHistoryItem,
outputs: SerializedPromptOutputs
}
export type ComfyAPIHistoryResponse = {
history: Record<PromptID, ComfyAPIHistoryEntry>,
error?: string
}
export type ComfyPromptPNGInfo = {
workflow: SerializedLGraph
}
export type ComfyPromptExtraData = {
extra_pnginfo?: ComfyPromptPNGInfo,
client_id?: string, // UUID
subgraphs: string[],
thumbnails?: GalleryOutputEntry[]
}
type ComfyAPIEvents = {
status: (status: ComfyAPIStatusResponse | null, error?: Error | null) => void,
progress: (progress: Progress) => void,
reconnecting: () => void,
reconnected: () => void,
executing: (promptID: PromptID | null, runningNodeID: NodeID | null) => void,
executed: (promptID: PromptID, nodeID: NodeID, output: SerializedPromptOutput) => void,
execution_cached: (promptID: PromptID, nodes: NodeID[]) => void,
execution_error: (promptID: PromptID, message: string) => void,
}
export default class ComfyAPI {
private eventBus: TypedEmitter<ComfyAPIEvents> = new EventEmitter() as TypedEmitter<ComfyAPIEvents>;
socket: WebSocket | null = null;
clientId: string | null = null;
hostname: string | null = null;
port: number | null = 8188;
constructor() {
super();
}
override addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean) {
super.addEventListener(type, callback, options);
this.registered.add(type);
addEventListener<E extends keyof ComfyAPIEvents>(type: E, callback: ComfyAPIEvents[E]) {
this.eventBus.addListener(type, callback);
}
/**
@@ -39,9 +97,9 @@ export default class ComfyAPI extends EventTarget {
try {
const resp = await fetch(this.getBackendUrl() + "/prompt");
const status = await resp.json();
this.dispatchEvent(new CustomEvent("status", { detail: status }));
this.eventBus.emit("status", { execInfo: { queueRemaining: status.exec_info.queue_remaining } });
} catch (error) {
this.dispatchEvent(new CustomEvent("status", { detail: null }));
this.eventBus.emit("status", { error: error.toString() });
}
}, 1000);
}
@@ -77,7 +135,7 @@ export default class ComfyAPI extends EventTarget {
this.socket.addEventListener("open", () => {
opened = true;
if (isReconnect) {
this.dispatchEvent(new CustomEvent("reconnected"));
this.eventBus.emit("reconnected");
}
});
@@ -94,8 +152,8 @@ export default class ComfyAPI extends EventTarget {
this.createSocket(true);
}, 300);
if (opened) {
this.dispatchEvent(new CustomEvent("status", { detail: null }));
this.dispatchEvent(new CustomEvent("reconnecting"));
this.eventBus.emit("status", null);
this.eventBus.emit("reconnecting");
}
});
@@ -108,32 +166,28 @@ export default class ComfyAPI extends EventTarget {
this.clientId = msg.data.sid;
sessionStorage["Comfy.SessionId"] = this.clientId;
}
this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
this.eventBus.emit("status", { execInfo: { queueRemaining: msg.data.status.exec_info.queue_remaining } });
break;
case "progress":
this.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
this.eventBus.emit("progress", msg.data as Progress);
break;
case "executing":
this.dispatchEvent(new CustomEvent("executing", { detail: msg.data }));
this.eventBus.emit("executing", msg.data.prompt_id, msg.data.node);
break;
case "executed":
this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
this.eventBus.emit("executed", msg.data.prompt_id, msg.data.node, msg.data.output);
break;
case "execution_cached":
this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data }));
this.eventBus.emit("execution_cached", msg.data.prompt_id, msg.data.nodes);
break;
case "execution_error":
this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data }));
this.eventBus.emit("execution_error", msg.data.prompt_id, msg.data.message);
break;
default:
if (this.registered.has(msg.type)) {
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
} else {
throw new Error("Unknown message type");
}
console.warn("Unhandled message:", event.data);
}
} catch (error) {
console.warn("Unhandled message:", event.data);
console.error("Error handling message", event.data, error);
}
});
}
@@ -149,27 +203,27 @@ export default class ComfyAPI extends EventTarget {
* Gets a list of extension urls
* @returns An array of script urls to import
*/
async getExtensions() {
const resp = await fetch(this.getBackendUrl() + `/extensions`, { cache: "no-store" });
return await resp.json();
async getExtensions(): Promise<any> {
return fetch(this.getBackendUrl() + `/extensions`, { cache: "no-store" })
.then(resp => resp.json())
}
/**
* Gets a list of embedding names
* @returns An array of script urls to import
*/
async getEmbeddings() {
const resp = await fetch(this.getBackendUrl() + "/embeddings", { cache: "no-store" });
return await resp.json();
async getEmbeddings(): Promise<any> {
return fetch(this.getBackendUrl() + "/embeddings", { cache: "no-store" })
.then(resp => resp.json())
}
/**
* Loads node object definitions for the graph
* @returns The node definitions
*/
async getNodeDefs() {
const resp = await fetch(this.getBackendUrl() + "/object_info", { cache: "no-store" });
return await resp.json();
async getNodeDefs(): Promise<any> {
return fetch(this.getBackendUrl() + "/object_info", { cache: "no-store" })
.then(resp => resp.json())
}
/**
@@ -177,82 +231,59 @@ export default class ComfyAPI extends EventTarget {
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
* @param {object} prompt The prompt data to queue
*/
async queuePrompt(number: number, { output, workflow }) {
const body: PromptRequestBody = {
client_id: this.clientId,
prompt: output,
extra_data: { extra_pnginfo: { workflow } },
front: false,
number: number
};
async queuePrompt(body: ComfyPromptRequest): Promise<ComfyAPIPromptResponse> {
body.client_id = this.clientId;
if (number === -1) {
if (body.number === -1) {
body.front = true;
} else if (number != 0) {
body.number = number;
}
const res = await fetch(this.getBackendUrl() + "/prompt", {
let postBody = null;
try {
postBody = JSON.stringify(body)
}
catch (error) {
return Promise.reject({ error })
}
return fetch(this.getBackendUrl() + "/prompt", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (res.status !== 200) {
throw {
response: await res.text(),
};
}
}
/**
* Loads a list of items (queue or history)
* @param {string} type The type of items to load, queue or history
* @returns The items of the specified type grouped by their status
*/
async getItems(type: QueueItemType) {
if (type === "queue") {
return this.getQueue();
}
return this.getHistory();
body: postBody
})
.then(res => res.json())
.then(raw => { return { promptID: raw.prompt_id } })
.catch(res => { throw res.text() })
.catch(error => { return { error } })
}
/**
* Gets the current state of the queue
* @returns The currently running and queued items
*/
async getQueue() {
try {
const res = await fetch(this.getBackendUrl() + "/queue");
const data = await res.json();
async getQueue(): Promise<ComfyAPIQueueResponse> {
return fetch(this.getBackendUrl() + "/queue")
.then(res => res.json())
.then(data => {
return {
// Running action uses a different endpoint for cancelling
Running: data.queue_running.map((prompt) => ({
prompt,
remove: { name: "Cancel", cb: () => this.interrupt() },
})),
Pending: data.queue_pending.map((prompt) => ({ prompt })),
};
} catch (error) {
console.error(error);
return { Running: [], Pending: [], error };
running: data.queue_running,
pending: data.queue_pending,
}
})
.catch(error => { return { running: [], pending: [], error } })
}
/**
* Gets the prompt execution history
* @returns Prompt history including node outputs
*/
async getHistory() {
try {
const res = await fetch(this.getBackendUrl() + "/history");
return { History: Object.values(await res.json()) };
} catch (error) {
console.error(error);
return { History: [], error };
}
async getHistory(): Promise<ComfyAPIHistoryResponse> {
return fetch(this.getBackendUrl() + "/history")
.then(res => res.json())
.then(history => { return { history } })
.catch(error => { return { history: {}, error } })
}
/**
@@ -260,18 +291,21 @@ export default class ComfyAPI extends EventTarget {
* @param {*} type The endpoint to post to
* @param {*} body Optional POST data
*/
private async postItem(type: string, body: any) {
private async postItem(type: QueueItemType, body: any): Promise<Response> {
try {
await fetch("/" + type, {
body = body ? JSON.stringify(body) : body
}
catch (error) {
return Promise.reject(error)
}
return fetch(this.getBackendUrl() + "/" + type, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
body: body
});
} catch (error) {
console.error(error);
}
}
/**
@@ -279,22 +313,22 @@ export default class ComfyAPI extends EventTarget {
* @param {string} type The type of item to delete, queue or history
* @param {number} id The id of the item to delete
*/
async deleteItem(type: string, id: number) {
await this.postItem(type, { delete: [id] });
async deleteItem(type: QueueItemType, id: number): Promise<Response> {
return this.postItem(type, { delete: [id] });
}
/**
* Clears the specified list
* @param {string} type The type of list to clear, queue or history
*/
async clearItems(type: string) {
await this.postItem(type, { clear: true });
async clearItems(type: QueueItemType): Promise<Response> {
return this.postItem(type, { clear: true });
}
/**
* Interrupts the execution of the running prompt
*/
async interrupt() {
await this.postItem("interrupt", null);
async interrupt(): Promise<Response> {
return fetch(this.getBackendUrl() + "/interrupt", { method: "POST" });
}
}

View File

@@ -179,8 +179,8 @@
}
}
:global(.label-wrap > span:not(.icon)) {
/* color: var(--block-title-text-color); */
:global(.label-wrap > span) {
color: var(--block-title-text-color);
font-size: 16px;
}

View File

@@ -31,7 +31,7 @@
let containerElem: HTMLDivElement;
let resizeTimeout: NodeJS.Timeout | null;
let hasShownUIHelpToast: boolean = false;
let uiTheme: string = "";
let uiTheme: string = "gradio-dark";
let fileInput: HTMLInputElement = undefined;
let debugLayout: boolean = false;
@@ -78,7 +78,7 @@
}
}
let propsSidebarSize = 15; //15;
let propsSidebarSize = 0;
function toggleProps() {
if (propsSidebarSize == 0) {
@@ -90,11 +90,11 @@
}
}
let queueSidebarSize = 15;
let queueSidebarSize = 20;
function toggleQueue() {
if (queueSidebarSize == 0) {
queueSidebarSize = 15;
queueSidebarSize = 20;
app.resizeCanvas();
}
else {
@@ -182,6 +182,13 @@
async function doRefreshCombos() {
await app.refreshComboInNodes(true)
}
$: if (uiTheme === "gradio-dark") {
document.getElementById("app").classList.add("dark")
}
else {
document.getElementById("app").classList.remove("dark")
}
</script>
<svelte:head>
@@ -190,7 +197,7 @@
{/if}
</svelte:head>
<div id="main">
<div id="main" class:dark={uiTheme === "gradio-dark"}>
<div id="dropzone" class="dropzone"></div>
<div id="container" bind:this={containerElem}>
<Splitpanes theme="comfy" on:resize={refreshView}>
@@ -264,7 +271,8 @@
<span class="label" for="ui-theme">
<BlockTitle>Theme</BlockTitle>
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
<option value="">None</option>
<option value="gradio-dark">Gradio Dark</option>
<option value="gradio-light">Gradio Light</option>
<option value="anapnoe">Anapnoe</option>
</select>
</span>
@@ -276,12 +284,13 @@
<LightboxModal />
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
</div>
<SvelteToast options={toastOptions} />
<style lang="scss">
$bottom-bar-height: 70px;
#container {
height: calc(100vh - 70px);
height: calc(100vh - $bottom-bar-height);
max-width: 100vw;
display: grid;
width: 100%;
@@ -301,7 +310,7 @@
padding-right: 1em;
margin-top: auto;
overflow-x: auto;
height: 70px;
height: $bottom-bar-height;
> .left {
flex-shrink: 0;
@@ -338,28 +347,30 @@
}
:global(.splitpanes.comfy>.splitpanes__splitter) {
background-color: var(--secondary-100);
background: var(--comfy-splitpanes-background-fill);
&:hover:not([disabled]) {
background-color: var(--secondary-300);
background: var(--comfy-splitpanes-background-fill-hover);
}
&:active:not([disabled]) {
background-color: var(--secondary-400);
background: var(--comfy-splitpanes-background-fill-active);
}
}
$splitter-size: 1rem;
:global(.splitpanes.comfy.splitpanes--horizontal>.splitpanes__splitter) {
min-height: 20px;
min-height: $splitter-size;
cursor: row-resize;
}
:global(.splitpanes.comfy.splitpanes--vertical>.splitpanes__splitter) {
min-width: 20px;
min-width: $splitter-size;
cursor: col-resize;
}
:global(.splitpanes.comfy) {
max-height: calc(100vh - 70px);
max-height: calc(100vh - $bottom-bar-height);
max-width: 100vw;
}

View File

@@ -1,6 +1,6 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI, { type ComfyAPIQueueStatus } from "$lib/api"
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyPromptExtraData, type ComfyPromptRequest, type NodeID, type PromptID } from "$lib/api"
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
@@ -32,11 +32,13 @@ import { download, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis }
import notify from "$lib/notify";
import configState from "$lib/stores/configState";
import { blankGraph } from "$lib/defaultGraph";
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
export const COMFYBOX_SERIAL_VERSION = 1;
LiteGraph.catch_exceptions = false;
LiteGraph.CANVAS_GRID_SIZE = 32;
LiteGraph.default_subgraph_lgraph_factory = () => new ComfyGraph();
if (typeof window !== "undefined") {
// Load default visibility
@@ -54,20 +56,23 @@ export type SerializedAppState = {
}
/** [link origin, link index] | value */
export type SerializedPromptInput = [string, number] | any
export type SerializedPromptInput = [NodeID, number] | any
export type SerializedPromptInputs = {
/* property name -> value or link */
inputs: Record<string, SerializedPromptInput>,
class_type: string
}
export type SerializedPromptOutput = Record<string, SerializedPromptInputs>
export type SerializedPromptInputsAll = Record<NodeID, SerializedPromptInputs>
export type SerializedPrompt = {
workflow: SerializedLGraph,
output: SerializedPromptOutput
output: SerializedPromptInputsAll
}
export type SerializedPromptOutputs = Record<NodeID, GalleryOutput>
export type Progress = {
value: number,
max: number
@@ -175,6 +180,8 @@ export default class ComfyApp {
this.addPasteHandler();
this.addKeyboardHandler();
await this.updateHistoryAndQueue();
// await this.#invokeExtensionsAsync("setup");
// Ensure the canvas fills the window
@@ -318,47 +325,44 @@ export default class ComfyApp {
* Handles updates from the API socket
*/
private addApiUpdateHandlers() {
this.api.addEventListener("status", ({ detail: ComfyAPIStatus }: CustomEvent) => {
// this.ui.setStatus(detail);
this.api.addEventListener("status", (status: ComfyAPIStatusResponse) => {
queueState.statusUpdated(status);
});
this.api.addEventListener("reconnecting", () => {
// this.ui.dialog.show("Reconnecting...");
uiState.reconnecting()
});
this.api.addEventListener("reconnected", () => {
// this.ui.dialog.close();
uiState.reconnected()
});
this.api.addEventListener("progress", ({ detail }: CustomEvent) => {
queueState.progressUpdated(detail);
this.api.addEventListener("progress", (progress: Progress) => {
queueState.progressUpdated(progress);
this.lGraph.setDirtyCanvas(true, false);
});
this.api.addEventListener("executing", ({ detail }: CustomEvent) => {
queueState.executingUpdated(detail.node);
this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: NodeID | null) => {
queueState.executingUpdated(promptID, nodeID);
this.lGraph.setDirtyCanvas(true, false);
});
this.api.addEventListener("status", (ev: CustomEvent) => {
queueState.statusUpdated(ev.detail as ComfyAPIQueueStatus);
});
this.api.addEventListener("executed", ({ detail }: CustomEvent) => {
this.nodeOutputs[detail.node] = detail.output;
const node = this.lGraph.getNodeById(detail.node) as ComfyGraphNode;
this.api.addEventListener("executed", (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => {
this.nodeOutputs[nodeID] = output;
const node = this.lGraph.getNodeById(parseInt(nodeID)) as ComfyGraphNode;
if (node?.onExecuted) {
node.onExecuted(detail.output);
node.onExecuted(output);
}
queueState.onExecuted(promptID, nodeID, output)
});
this.api.addEventListener("execution_cached", ({ detail }: CustomEvent) => {
// TODO detail.nodes
this.api.addEventListener("execution_cached", (promptID: PromptID, nodes: NodeID[]) => {
queueState.executionCached(promptID, nodes)
});
this.api.addEventListener("execution_error", ({ detail }: CustomEvent) => {
queueState.update(s => { s.progress = null; s.runningNodeId = null; return s; })
notify(`Execution error: ${detail.message}`, { type: "error", timeout: 10000 })
this.api.addEventListener("execution_error", (promptID: PromptID, message: string) => {
queueState.executionError(promptID, message)
notify(`Execution error: ${message}`, { type: "error", timeout: 10000 })
});
this.api.init();
@@ -370,7 +374,7 @@ export default class ComfyApp {
// Queue prompt using ctrl or command + enter
if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) {
this.queuePrompt(e.shiftKey ? -1 : 0);
this.runDefaultQueueAction();
}
});
window.addEventListener("keyup", (e) => {
@@ -378,6 +382,13 @@ export default class ComfyApp {
});
}
private async updateHistoryAndQueue() {
const queue = await this.api.getQueue();
const history = await this.api.getHistory();
queueState.queueUpdated(queue);
queueState.historyUpdated(history);
}
private requestPermissions() {
if (Notification.permission === "default") {
Notification.requestPermission()
@@ -456,6 +467,7 @@ export default class ComfyApp {
state = structuredClone(blankGraph)
}
await this.deserialize(state)
uiState.update(s => { s.uiUnlocked = true; return s; })
}
/**
@@ -514,7 +526,6 @@ export default class ComfyApp {
if (get(layoutState).attrs.queuePromptButtonRunWorkflow) {
this.queuePrompt(0, 1);
notify("Prompt queued.");
}
}
@@ -601,7 +612,7 @@ export default class ComfyApp {
// The reasoning behind this check:
// We only want to serialize inputs to nodes with backend equivalents.
// And in ComfyBox, the nodes in litegraph *never* have widgets, instead they're all inputs.
// And in ComfyBox, the backend nodes in litegraph *never* have widgets, instead they're all inputs.
// All values are passed by separate frontend-only nodes,
// either UI-bound or something like ConstantInteger.
// So we know that any value passed into a backend node *must* come from
@@ -718,8 +729,26 @@ export default class ComfyApp {
({ num, batchCount } = this.queueItems.pop());
console.debug(`Queue get! ${num} ${batchCount} ${tag}`);
const thumbnails = []
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
if (node.mode !== NodeMode.ALWAYS
|| (tag != null
&& Array.isArray(node.properties.tags)
&& node.properties.tags.indexOf(tag) === -1))
continue;
if ("getPromptThumbnails" in node) {
const thumbsToAdd = (node as ComfyGraphNode).getPromptThumbnails();
if (thumbsToAdd)
thumbnails.push(...thumbsToAdd)
}
}
for (let i = 0; i < batchCount; i++) {
for (const node of this.lGraph._nodes_in_order) {
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
if (node.mode !== NodeMode.ALWAYS)
continue;
if ("beforeQueued" in node) {
(node as ComfyGraphNode).beforeQueued(tag);
}
@@ -728,10 +757,38 @@ export default class ComfyApp {
const p = await this.graphToPrompt(tag);
console.debug(promptToGraphVis(p))
const extraData: ComfyPromptExtraData = {
extra_pnginfo: {
workflow: p.workflow
},
subgraphs: [tag],
thumbnails
}
let error = null;
let promptID = null;
const request: ComfyPromptRequest = {
number: num,
extra_data: extraData,
prompt: p.output
}
try {
await this.api.queuePrompt(num, p);
const response = await this.api.queuePrompt(request);
// BUG: This can cause race conditions updating frontend state
// since we don't have a prompt ID until the backend
// returns!
promptID = response.promptID;
queueState.afterQueued(promptID, num, p.output, extraData)
error = response.error;
} catch (error) {
// this.ui.dialog.show(error.response || error.toString());
error = error.toString();
}
if (error != null) {
const mes = error.response || error.toString()
notify(`Error queuing prompt:\n${mes}`, { type: "error" })
console.error(promptToGraphVis(p))
@@ -747,7 +804,6 @@ export default class ComfyApp {
}
this.lCanvas.draw(true, true);
// await this.ui.queue.update();
}
}
} finally {
@@ -766,7 +822,7 @@ export default class ComfyApp {
if (pngInfo.comfyBoxConfig) {
this.deserialize(JSON.parse(pngInfo.comfyBoxConfig));
} else if (pngInfo.parameters) {
throw "TODO import A111 import!"
throw "TODO A111 import!"
// importA1111(this.lGraph, pngInfo.parameters, this.api);
}
else {
@@ -835,7 +891,7 @@ export default class ComfyApp {
const isComfyInput = isComfyComboInput(input)
const isComfyCombo = isComfyComboNode(inputNode)
console.debug("[refreshComboInNodes] CHECK", backendNode.type, input.name, "isComfyCombo", isComfyCombo, "isComfyInput", isComfyInput)
// console.debug("[refreshComboInNodes] CHECK", backendNode.type, input.name, "isComfyCombo", isComfyCombo, "isComfyInput", isComfyInput)
return isComfyCombo && isComfyInput
});

View File

@@ -443,8 +443,8 @@
flex-direction: row;
}
.target-name {
border-color: var(--neutral-400);
.target-name {
background: var(--input-background-fill);
border-color: var(--input-border-color);
padding: 0.8rem 1.0rem;
@@ -458,13 +458,14 @@
}
}
.category-name {
background: var(--panel-background-fill);
border-color: var(--panel-border-color);
padding: 0.4rem 1.0rem;
border-color: var(--neutral-300);
padding: 0.4rem 1.0rem;
}
.target-name, .category-name {
border-width: var(--block-border-width);
color: var(--body-text-color);
.type {
@@ -479,10 +480,5 @@
bottom: 0;
padding: 0.5em; */
}
:global(input[type=text]:disabled) {
@include disable-input;
}
:global(textarea:disabled) {
@include disable-input;
@include disable-inputs;

View File

@@ -1,159 +1,527 @@
<script lang="ts">
import queueState from "$lib/stores/queueState";
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
import ProgressBar from "./ProgressBar.svelte";
import { getNodeInfo } from "$lib/utils"
import Spinner from "./Spinner.svelte";
import PromptDisplay from "./PromptDisplay.svelte";
import { ListIcon as List } from "svelte-feather-icons";
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo } from "$lib/utils"
import type { Writable } from "svelte/store";
import type { QueueItemType } from "$lib/api";
import { ImageViewer } from "$lib/ImageViewer";
import { Button } from "@gradio/button";
import type ComfyApp from "./ComfyApp";
import { tick } from "svelte";
import Modal from "./Modal.svelte";
const entries = [
{
"outputs": {
44: {
"images": [
{
"filename": "ComfyUI_00052_.png",
"subfolder": "",
"type": "output"
}
]
}
}
},
{
"outputs": {
44: {
"images": [
{
"filename": "ComfyUI_00052_.png",
"subfolder": "",
"type": "output"
}
]
}
}
},
{
"outputs": {
44: {
"images": [
{
"filename": "ComfyUI_00052_.png",
"subfolder": "",
"type": "output"
}
]
}
}
}
]
let queuePending: Writable<QueueEntry[]> | null = null;
let queueRunning: Writable<QueueEntry[]> | null = null;
let queueCompleted: Writable<CompletedQueueEntry[]> | null = null;
let queueList: HTMLDivElement | null = null;
let _entries: any[] = []
type QueueUIEntry = {
entry: QueueEntry,
message: string,
submessage: string,
date?: string,
status: QueueEntryStatus | "pending" | "running",
images?: string[], // URLs
details?: string // shown in a tooltip on hover
}
$: if (entries) {
$: if ($queueState) {
queuePending = $queueState.queuePending
queueRunning = $queueState.queueRunning
queueCompleted = $queueState.queueCompleted
}
let mode: QueueItemType = "queue";
function switchMode(newMode: QueueItemType) {
const changed = mode !== newMode
mode = newMode
if (changed)
_entries = []
// for (const entry of entries) {
// for (const outputs of Object.values(entry.outputs)) {
// const allImages = outputs.images.map(r => {
// // TODO configure backend URL
// const url = "http://localhost:8188/view?"
// const params = new URLSearchParams(r)
// return url + params
// });
//
// _entries.push({ allImages, name: "Output" })
// }
// }
}
let _entries: QueueUIEntry[] = []
$: if (mode === "queue" && $queuePending && $queuePending.length != _entries.length) {
updateFromQueue();
}
else if (mode === "history" && $queueCompleted && $queueCompleted.length != _entries.length) {
updateFromHistory();
}
function formatDate(date: Date): string {
const time = date.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true });
const day = date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }).replace(',', '');
return [time, day].join(", ")
}
function convertEntry(entry: QueueEntry): QueueUIEntry {
let date = entry.finishedAt || entry.queuedAt;
let dateStr = null;
if (date) {
dateStr = formatDate(date);
}
let message = "Prompt";
if (entry.extraData.subgraphs)
message = `Prompt: ${entry.extraData.subgraphs.join(', ')}`
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
if (Object.keys(entry.outputs).length > 0) {
const imageCount = Object.values(entry.outputs).flatMap(o => o.images).length
submessage = `Images: ${imageCount}`
}
return {
entry,
message,
submessage,
dateStr,
status: "pending",
images: []
}
}
function convertPendingEntry(entry: QueueEntry): QueueUIEntry {
const result = convertEntry(entry);
const thumbnails = entry.extraData?.thumbnails
if (thumbnails) {
result.images = thumbnails.map(convertComfyOutputToComfyURL);
}
return result;
}
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
const result = convertEntry(entry.entry);
result.status = entry.status;
const images = Object.values(entry.entry.outputs).flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
result.images = images
if (entry.message)
result.submessage = entry.message
else if (entry.status === "interrupted" || entry.status === "all_cached")
result.submessage = "Prompt was interrupted."
if (entry.error)
result.details = entry.error
return result;
}
async function updateFromQueue() {
_entries = $queuePending.map(convertPendingEntry).reverse(); // newest entries appear at the top
if (queueList) {
await tick(); // Wait for list size to be recalculated
queueList.scroll({ top: queueList.scrollHeight })
}
console.warn("[ComfyQueue] BUILDQUEUE", _entries, $queuePending)
}
async function updateFromHistory() {
_entries = $queueCompleted.map(convertCompletedEntry).reverse();
if (queueList) {
queueList.scrollTo(0, 0);
}
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted)
}
function showLightbox(entry: QueueUIEntry, index: number, e: Event) {
e.preventDefault()
if (!entry.images)
return
ImageViewer.instance.showModal(entry.images, index);
e.stopPropagation()
}
async function interrupt() {
if ($queueState.isInterrupting)
return
const app = (window as any).app as ComfyApp;
if (!app || !app.api)
return;
await app.api.interrupt()
.then(() => {
queueState.update(s => { s.isInterrupting = true; return s })
});
}
let showModal = false;
let selectedPrompt = null;
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
selectedPrompt = entry.entry.prompt;
showModal = true;
}
$: if(!showModal)
selectedPrompt = null;
let queued = false
$: queued = Boolean($queueState.runningNodeID || $queueState.progress);
let inProgress = false;
$: inProgress = typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0;
</script>
<div class="queue">
<div class="queue-entries">
{#each _entries as entry}
<div class="queue-entry">
<img class="queue-entry-image" src={entry.allImages[0]} alt="thumbnail" />
<div class="queue-entry-details">
{entry.name}
<Modal bind:showModal>
<div slot="header" class="prompt-modal-header">
<h1 style="padding-bottom: 1rem;">Prompt Details</h1>
</div>
{#if selectedPrompt}
<PromptDisplay prompt={selectedPrompt} />
{/if}
</Modal>
<div class="queue">
<div class="queue-entries {mode}-mode" bind:this={queueList}>
{#if _entries.length > 0}
{#each _entries as entry}
<div class="queue-entry {entry.status}" on:click={(e) => showPrompt(entry, e)}>
{#if entry.images.length > 0}
<div class="queue-entry-images"
style="--cols: {Math.ceil(Math.sqrt(Math.min(entry.images.length, 4)))}" >
{#each entry.images.slice(0, 4) as image, i}
<div>
<img class="queue-entry-image"
on:click={(e) => showLightbox(entry, i, e)}
src={image}
alt="thumbnail" />
</div>
{/each}
</div>
<div class="bottom">
{#if $queueState.runningNodeId || $queueState.progress}
<div class="node-name">
<span>Node: {getNodeInfo($queueState.runningNodeId)}</span>
{:else}
<!-- <div class="queue-entry-image-placeholder" /> -->
{/if}
<div class="queue-entry-details">
<div class="queue-entry-message">
{entry.message}
</div>
<div class="queue-entry-submessage">
{entry.submessage}
</div>
</div>
</div>
<div class="queue-entry-rest {entry.status}">
{#if entry.date != null}
<span class="queue-entry-queued-at">
{entry.date}
</span>
{/if}
</div>
{/each}
{:else}
<div class="queue-empty">
<div class="queue-empty-container">
<div class="queue-empty-icon">
<List size="120rem" />
</div>
<div class="queue-empty-message">
(No entries)
</div>
</div>
<div>
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} styles="height: 30px;" />
</div>
{/if}
{#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0}
<div class="queue-remaining in-progress">
<div>
Queued prompts: {$queueState.queueRemaining}.
</div>
<div class="mode-buttons">
<div class="mode-button secondary"
on:click={() => switchMode("queue")}
class:mode-selected={mode === "queue"}>
Queue
</div>
<div class="mode-button secondary"
on:click={() => switchMode("history")}
class:mode-selected={mode === "history"}>
History
</div>
</div>
<div class="bottom">
<div class="queue-remaining" class:queued class:in-progress={inProgress}>
{#if inProgress}
<Spinner />
<div class="status">
Queued prompts: {$queueState.queueRemaining}
</div>
{:else}
<div class="queue-remaining done">
<div>
Nothing queued.
</div>
{/if}
</div>
{#if queued}
<div class="node-name">
<span>Node: {getNodeInfo($queueState.runningNodeID)}</span>
</div>
<div>
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />
</div>
<div class="queue-action-buttons">
<Button variant="secondary"
disabled={$queueState.isInterrupting}
on:click={interrupt}
style={{ full_width: true }}>
Interrupt
</Button>
</div>
{/if}
</div>
</div>
<style lang="scss">
.queue-remaining {
height: 5em;
width: 100%;
text-align: center;
border: 5px solid #CCC;
position: relative;
$pending-height: 200px;
$bottom-bar-height: 70px;
$mode-buttons-height: 30px;
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height});
.prompt-modal-header {
padding-left: 0.2rem;
h1 {
font-size: large;
}
}
.queue {
color: var(--body-text-color);
}
.queue-entries {
height: $queue-height;
max-height: $queue-height;
display: flex;
justify-content: center;
align-items: center;
overflow-y: auto;
flex-flow: column nowrap;
&.queue-mode > :first-child {
// elements stick to bottom in queue mode only
// next element in queue is on the bottom
margin-top: auto !important;
}
> .queue-empty {
display: flex;
color: var(--comfy-accent-soft);
flex-direction: row;
margin: auto;
height: 100%;
> .queue-empty-container {
margin: auto;
display: flex;
flex-direction: column;
> .queue-empty-icon {
margin: auto;
}
> .queue-empty-message {
margin: auto;
font-size: 32px;
font-weight: bolder;
}
}
}
}
.queue-entry {
padding: 0.5rem 0.5rem 0 0.5rem;
padding: 1.0rem;
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--block-border-color);
border-top: 1px solid var(--table-border-color);
background: var(--panel-background-fill);
&:hover:not(:has(img:hover)) {
cursor: pointer;
background: var(--block-background-fill);
}
.queue-entry-image {
width: var(--size-20)
&.success {
/* background: green; */
}
&.error {
background: red;
}
&.all_cached, &.interrupted {
filter: brightness(80%);
background: var(--comfy-disabled-textbox-background-fill);
color: var(--comfy-disable-textbox-text-color);
}
&.running {
/* background: lightblue; */
}
&.pending, &.unknown {
/* background: orange; */
}
}
.queue-entry-rest {
width: 100%;
position: relative;
&.all_cached, &.interrupted {
filter: brightness(80%);
color: var(--neutral-300);
}
}
$thumbnails-size: 12rem;
.queue-entry-images {
--cols: 1;
margin: auto;
width: calc($thumbnails-size * 2);
display: grid;
display: inline-grid;
grid-template-columns: repeat(var(--cols), 1fr);
grid-template-rows: repeat(var(--cols), 1fr);
column-gap: 1px;
row-gap: 1px;
vertical-align: top;
img {
aspect-ratio: 1 / 1;
object-fit: cover;
&:hover {
cursor: pointer;
filter: brightness(120%) contrast(120%);
}
}
}
.queue-entry-details {
width: var(--size-20)
position: relative;
padding: 1rem;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.node-name {
border: 5px solid #CCC;
background-color: var(--color-red-300);
padding: 0.2em;
.queue-entry-message {
font-size: 15px;
}
.queue-entry-submessage {
font-size: 12px;
}
.queue-entry-queued-at {
width: auto;
font-size: 12px;
position:absolute;
right: 0px;
bottom: 0px;
padding: 0.4rem 0.6rem;
color: var(--body-text-color);
}
.mode-buttons {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 100%;
> .mode-button {
width: 100%;
}
}
.mode-button {
height: calc($mode-buttons-height);
padding: 0.2rem;
border: 1px solid var(--panel-border-color);
font-weight: bold;
text-align: center;
margin: auto;
&.primary {
background: var(--button-primary-background-fill);
&:hover {
background: var(--button-primary-background-fill-hover);
}
}
&.secondary {
background: var(--button-secondary-background-fill);
&:hover {
background: var(--button-secondary-background-fill-hover);
}
}
&:hover {
filter: brightness(85%);
}
&:active {
filter: brightness(50%)
}
&.mode-selected {
filter: brightness(80%)
}
}
:global(.dark) .mode-button {
filter: none;
&:hover {
filter: brightness(120%);
}
&:active {
filter: brightness(50%)
}
&.mode-selected {
filter: brightness(150%)
}
}
.bottom {
width: 100%;
height: auto;
height: calc($pending-height);
position: absolute;
bottom: 0;
padding: 0.5em;
.node-name {
background-color: var(--comfy-node-name-background);
color: var(--comfy-node-name-foreground);
padding: 0.2em;
margin: 5px;
display: flex;
justify-content: center;
align-items: center;
}
.in-progress {
background-color: var(--secondary-300);
}
.done {
background-color: var(--color-grey-200);
}
.queue-item {
height: 1.5em;
width: 10em;
.queue-remaining {
height: calc($pending-height - $bottom-bar-height - 50px);
width: 100%;
text-align: center;
border: 1px solid black;
position: relative;
display: flex;
justify-content: space-evenly;
align-items: center;
background: var(--panel-background-fill);
> .status {
}
&.queued {
height: calc($pending-height - $mode-buttons-height - $bottom-bar-height - 16px);
}
}
.queue-action-buttons {
margin: 5px;
height: 20px;
:global(button) {
border-radius: 0px !important;
}
}
}
</style>

View File

@@ -95,12 +95,12 @@
position: absolute;
top: 50%;
width: auto;
padding: 16px;
padding: 60px;
margin-top: -50px;
color: white;
font-weight: bold;
font-size: 20px;
transition: 0.6s ease;
font-size: 40px;
transition: 0.3s ease;
border-radius: 0 3px 3px 0;
user-select: none;
-webkit-user-select: none;
@@ -113,6 +113,6 @@
.modalPrev:hover,
.modalNext:hover {
background-color: rgba(0, 0, 0, 0.8);
background-color: rgba(180, 180, 180, 0.8);
}
</style>

View File

@@ -0,0 +1,61 @@
<script>
export let showModal; // boolean
let dialog; // HTMLDialogElement
$: if (dialog && showModal) dialog.showModal();
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<dialog
bind:this={dialog}
on:close={() => (showModal = false)}
on:click|self={() => dialog.close()}
>
<div on:click|stopPropagation>
<slot name="header" />
<slot />
<!-- svelte-ignore a11y-autofocus -->
<button autofocus on:click={() => dialog.close()}>Close</button>
</div>
</dialog>
<style>
dialog {
max-width: 75vw;
border-radius: 0.2em;
border: none;
padding: 0;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.3);
}
dialog > div {
padding: 1em;
}
dialog[open] {
animation: zoom 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes zoom {
from {
transform: scale(0.95);
}
to {
transform: scale(1);
}
}
dialog[open]::backdrop {
animation: fade 0.2s ease-out;
}
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
button {
display: block;
}
</style>

View File

@@ -23,17 +23,17 @@
<style>
.progress {
width: 100%;
height: 100%;
height: 30px;
margin: 5px;
text-align: center;
background-color: lightgrey;
background: var(--comfy-progress-bar-background);
padding: 0px;
position: relative;
}
.bar {
height: 100%;
background-color: #B3D8A9;
background: var(--comfy-progress-bar-foreground);
}
.label {

View File

@@ -0,0 +1,165 @@
<script lang="ts">
import { fade } from "svelte/transition";
import { TextBox } from "@gradio/form";
import type { SerializedPromptInput, SerializedPromptInputsAll } from "./ComfyApp";
import { Block, BlockLabel, BlockTitle } from "@gradio/atoms";
import { JSON as JSONComponent } from "@gradio/json";
import { JSON as JSONIcon, Copy, Check } from "@gradio/icons";
import Accordion from "$lib/components/gradio/app/Accordion.svelte";
const splitLength = 50;
export let prompt: SerializedPromptInputsAll;
function isInputLink(input: SerializedPromptInput): boolean {
return Array.isArray(input)
&& input.length === 2
&& typeof input[0] === "string"
&& typeof input[1] === "number"
}
function countNewLines(str: string): number {
return str.split(/\r\n|\r|\n/).length
}
function isMultiline(input: any): boolean {
return typeof input === "string" && (input.length > splitLength || countNewLines(input) > 1);
}
function formatInput(input: any): string {
if (typeof input === "string")
return input
return JSON.stringify(input, null, 2);
}
let copiedNodeID: any | null = null;
let copiedInputName: string | null = null;
let timer: NodeJS.Timeout;
function copyFeedback(nodeID: string, inputName: string) {
copiedNodeID = nodeID;
copiedInputName = inputName;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
copiedNodeID = null;
copiedInputName = null;
}, 1000);
}
async function handleCopy(nodeID: string, inputName: string, input: any) {
if ("clipboard" in navigator) {
await navigator.clipboard.writeText(formatInput(input));
copyFeedback(nodeID, inputName);
}
}
</script>
<div class="prompt-display">
<Block>
{#each Object.entries(prompt) as [nodeID, inputs], i}
{@const classType = inputs.class_type}
{@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))}
{#if filtered.length > 0}
<div class="accordion">
<Block padding={true}>
<Accordion label="Node {nodeID}: {classType}" open={false}>
{#each filtered as [inputName, input]}
<Block>
<BlockTitle>Input: {inputName}</BlockTitle>
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
{#if copiedNodeID === nodeID && copiedInputName === inputName}
<span class="copied-icon">
<Check />
</span>
{:else}
<span class="copy-text"><Copy /></span>
{/if}
</button>
<div>
{#if isInputLink(input)}
Link {input[0]} -> {input[1]}
{:else if typeof input === "object"}
<Block>
<BlockLabel
Icon={JSONIcon}
show_label={true}
label={inputName}
float={true}
/>
<JSONComponent value={input} />
</Block>
{:else if isMultiline(input)}
{@const lines = Math.max(countNewLines(input), input.length / splitLength)}
<TextBox label={inputName} value={formatInput(input)} {lines} max_lines={lines} />
{:else}
<TextBox label={inputName} value={formatInput(input)} lines={1} max_lines={1} />
{/if}
</div>
</Block>
{/each}
</Accordion>
</Block>
</div>
{/if}
{/each}
</Block>
</div>
<style lang="scss">
.prompt-display {
overflow-y: scroll;
width: 50vw;
height: 60vh;
color: none;
.copy-button {
display: flex;
position: absolute;
top: var(--block-label-margin);
right: var(--block-label-margin);
align-items: center;
box-shadow: var(--shadow-drop);
border: 1px solid var(--border-color-primary);
border-top: none;
border-right: none;
border-radius: var(--block-label-right-radius);
background: var(--block-label-background-fill);
padding: 5px;
width: 30px;
height: 30px;
overflow: hidden;
color: var(--block-label-text-color);
font: var(--font);
font-size: var(--button-small-text-size);
}
@keyframes -light-up {
from {
color: var(--color-yellow-400);
}
to {
color: none;
}
}
.copied-icon {
animation: light-up 1s ease-out;
:global(.svelte-select) {
animation: light-up 1s ease-out;
}
}
:global(> .block) {
background: var(--panel-background-fill);
}
.accordion {
background: var(--panel-background-fill);
:global(> .block .block) {
background: var(--panel-background-fill);
}
}
}
</style>

View File

@@ -0,0 +1,24 @@
<span class="loader" {...$$restProps}/>
<style lang="scss">
$border-bottom-color_1: var(--comfy-spinner-accent-color);
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loader {
width: 48px;
height: 48px;
border: 5px solid var(--comfy-spinner-main-color);
border-bottom-color: $border-bottom-color_1;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 2s linear infinite;
}
</style>

View File

@@ -50,7 +50,7 @@
$: if ($queueState && widget && widget.node) {
dragItem.isNodeExecuting = $queueState.runningNodeId === widget.node.id;
dragItem.isNodeExecuting = $queueState.runningNodeID === widget.node.id;
}
function getWidgetClass() {
@@ -72,7 +72,7 @@
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
class:edit={edit}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeId == widget.node.id}
class:hidden={hidden}
>
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />

View File

@@ -13,7 +13,7 @@
}
</script>
<div on:click={handleClick} class="label-wrap" class:open>
<div class="label-wrap" on:click={handleClick} class:open>
<span>{label}</span>
<span style:transform={open ? "rotate(0)" : "rotate(90deg)"} class="icon">

View File

@@ -7,6 +7,7 @@ import { get } from "svelte/store";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
import type { NotifyOptions } from "$lib/notify";
import type { FileData as GradioFileData } from "@gradio/upload";
import { convertComfyOutputToGradio, uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
export class ComfyQueueEvents extends ComfyGraphNode {
@@ -659,3 +660,54 @@ LiteGraph.registerNodeType({
desc: "Uploads an image from the specified ComfyUI folder into its input folder",
type: "actions/store_images"
})
export interface ComfySetPromptThumbnailsActionProperties extends ComfyGraphNodeProperties {
defaultFolderType: string | null
}
export class ComfySetPromptThumbnailsAction extends ComfyGraphNode {
override properties: ComfySetPromptThumbnailsActionProperties = {
tags: [],
defaultFolderType: "input",
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "filenames", type: "*" },
]
}
_value: any = null;
override getPromptThumbnails(): GalleryOutputEntry[] | null {
const data = this.getInputData(0)
const folderType = this.properties.folderType || "input";
const convertString = (s: string): GalleryOutputEntry => {
return { filename: data, subfolder: "", type: folderType }
}
if (typeof data === "string") {
return [convertString(data)]
}
else if (data != null && typeof data === "object") {
if ("filename" in data && "type" in data)
return [data as GalleryOutputEntry];
}
else if (Array.isArray(data) && data.length > 0) {
if (typeof data[0] === "string")
return data.map(convertString)
else if (typeof data[0] === "object" && "filename" in data[0] && "type" in data[0])
return data as GalleryOutputEntry[]
}
return null;
}
}
LiteGraph.registerNodeType({
class: ComfySetPromptThumbnailsAction,
title: "Comfy.SetPromptThumbnailsAction",
desc: "When a subgraph containing this node is executed, sets the thumbnails in the queue sidebar to the input filename(s).",
type: "actions/set_prompt_thumbnails"
})

View File

@@ -1,7 +1,7 @@
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
import ComfyGraphNode from "./ComfyGraphNode";
import ComfyWidgets from "$lib/widgets"
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes";
import { BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
@@ -110,7 +110,7 @@ export class ComfyBackendNode extends ComfyGraphNode {
}
}
override onExecuted(outputData: any) {
override onExecuted(outputData: GalleryOutput) {
console.warn("onExecuted outputs", outputData)
this.triggerSlot(0, outputData)
}

View File

@@ -3,7 +3,7 @@ import type { SerializedPrompt } from "$lib/components/ComfyApp";
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
import type { SvelteComponentDev } from "svelte/internal";
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import uiState from "$lib/stores/uiState";
import { get } from "svelte/store";
@@ -48,7 +48,14 @@ export default class ComfyGraphNode extends LGraphNode {
* Triggered when the backend sends a finished output back with this node's ID.
* Valid for output nodes like SaveImage and PreviewImage.
*/
onExecuted?(output: any): void;
onExecuted?(output: GalleryOutput): void;
/*
* When a prompt is queued, this will be called on the node if it can
* provide any thumbnails for use with the prompt queue. Useful for HR Fix
* or img2img workloads.
*/
getPromptThumbnails?(): GalleryOutputEntry[] | null
/*
* Allows you to manually specify an auto-config for certain input slot

View File

@@ -161,7 +161,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
}
private triggerChangeEvent(value: any) {
console.debug("[Widget] trigger changed", this, value)
// console.debug("[Widget] trigger changed", this, value)
const changedOutput = this.outputs[this.changedIndex]
if (changedOutput.type === BuiltInSlotType.EVENT)
this.triggerSlot(this.changedIndex, value)
@@ -271,7 +271,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
}
}
console.debug("Property copy", input, this.properties)
// console.debug("Property copy", input, this.properties)
this.setValue(get(this.value))
@@ -288,7 +288,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if (layoutEntry && layoutEntry.parent) {
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
}
console.debug("propsChanged", this)
// console.debug("propsChanged", this)
this.propsChanged.set(get(this.propsChanged) + 1)
}

View File

@@ -45,6 +45,11 @@ function notifyToast(text: string, options: NotifyOptions) {
'--toastBackground': 'var(--color-blue-500)',
}
}
else if (options.type === "warning") {
toastOptions.theme = {
'--toastBackground': 'var(--color-yellow-500)',
}
}
else if (options.type === "error") {
toastOptions.theme = {
'--toastBackground': 'var(--color-red-500)',

View File

@@ -1,7 +1,7 @@
import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import type ComfyApp from "$lib/components/ComfyApp"
import { type LGraphNode, type IWidget, type LGraph, NodeMode } from "@litegraph-ts/core"
import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemoveNodeOptions, type LGraphAddNodeOptions } from "@litegraph-ts/core"
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyWidgetNode } from '$lib/nodes';
@@ -79,7 +79,7 @@ export type LayoutState = {
* If true, a saved workflow is being deserialized, so ignore any
* nodeAdded/nodeRemoved events.
*
* TODO: instead use LGraphAddNodeOptions.addedByDeserialize
* TODO: instead use LGraphAddNodeOptions.addedBy
*/
isConfiguring: boolean,
@@ -649,8 +649,8 @@ type LayoutStateOps = {
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index?: number) => WidgetLayout,
findDefaultContainerForInsertion: () => ContainerLayout | null,
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
nodeAdded: (node: LGraphNode) => void,
nodeRemoved: (node: LGraphNode) => void,
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => void,
nodeRemoved: (node: LGraphNode, options: LGraphRemoveNodeOptions) => void,
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout,
ungroup: (container: ContainerLayout) => void,
getCurrentSelection: () => IDragItem[],
@@ -757,19 +757,6 @@ function updateChildren(parent: IDragItem, newChildren?: IDragItem[]): IDragItem
return state.allItems[parent.id].children
}
function nodeAdded(node: LGraphNode) {
const state = get(store)
if (state.isConfiguring)
return;
const parent = findDefaultContainerForInsertion();
console.debug("[layoutState] nodeAdded", node)
if ("svelteComponentType" in node) {
addWidget(parent, node as ComfyWidgetNode);
}
}
function removeEntry(state: LayoutState, id: DragItemID) {
const entry = state.allItems[id]
if (entry.children && entry.children.length > 0) {
@@ -788,7 +775,34 @@ function removeEntry(state: LayoutState, id: DragItemID) {
delete state.allItems[id]
}
function nodeRemoved(node: LGraphNode) {
function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
const state = get(store)
if (state.isConfiguring)
return;
if (options.addedBy === "moveIntoSubgraph" || options.addedBy === "moveOutOfSubgraph") {
// All we need to do is update the nodeID linked to this node.
const item = state.allItemsByNode[options.prevNodeId]
delete state.allItemsByNode[options.prevNodeId]
state.allItemsByNode[node.id] = item
return;
}
const parent = findDefaultContainerForInsertion();
console.debug("[layoutState] nodeAdded", node)
if ("svelteComponentType" in node) {
addWidget(parent, node as ComfyWidgetNode);
}
}
function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
if (options.removedBy === "moveIntoSubgraph" || options.removedBy === "moveOutOfSubgraph") {
// This node is being moved into a subgraph, so it will be readded under
// a new node ID shortly.
return
}
const state = get(store)
console.debug("[layoutState] nodeRemoved", node)

View File

@@ -1,54 +1,287 @@
import type { ComfyAPIQueueStatus } from "$lib/api";
import type { Progress } from "$lib/components/ComfyApp";
import { writable, type Writable } from "svelte/store";
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyPromptExtraData, NodeID, PromptID } from "$lib/api";
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
import notify from "$lib/notify";
import { get, writable, type Writable } from "svelte/store";
export type QueueItem = {
name: string
}
export type QueueEntryStatus = "success" | "error" | "interrupted" | "all_cached" | "unknown";
type QueueStateOps = {
statusUpdated: (status: ComfyAPIQueueStatus | null) => void,
executingUpdated: (runningNodeId: string | null) => void,
progressUpdated: (progress: Progress | null) => void
queueUpdated: (resp: ComfyAPIQueueResponse) => void,
historyUpdated: (resp: ComfyAPIHistoryResponse) => void,
statusUpdated: (status: ComfyAPIStatusResponse | null) => void,
executingUpdated: (promptID: PromptID | null, runningNodeID: NodeID | null) => void,
executionCached: (promptID: PromptID, nodes: NodeID[]) => void,
executionError: (promptID: PromptID, message: string) => void,
progressUpdated: (progress: Progress) => void
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
onExecuted: (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => void
}
export type QueueEntry = {
/* Data preserved on page refresh */
number: number,
queuedAt?: Date,
finishedAt?: Date,
promptID: PromptID,
prompt: SerializedPromptInputsAll,
extraData: ComfyPromptExtraData,
goodOutputs: NodeID[],
/* Data not sent by Comfy's API, lost on page refresh */
/* Prompt outputs, collected while the prompt is still executing */
outputs: SerializedPromptOutputs,
/* Nodes in of the workflow that have finished running so far. */
nodesRan: Set<NodeID>,
cachedNodes: Set<NodeID>
}
export type CompletedQueueEntry = {
entry: QueueEntry,
status: QueueEntryStatus,
message?: string,
error?: string,
}
export type QueueState = {
queueRunning: Writable<QueueEntry[]>,
queuePending: Writable<QueueEntry[]>,
queueCompleted: Writable<CompletedQueueEntry[]>,
queueRemaining: number | "X" | null;
runningNodeId: number | null;
progress: Progress | null
runningNodeID: number | null;
progress: Progress | null,
isInterrupting: boolean
}
type WritableQueueStateStore = Writable<QueueState> & QueueStateOps;
const store: Writable<QueueState> = writable({ queueRemaining: null, runningNodeId: null, progress: null })
const store: Writable<QueueState> = writable({
queueRunning: writable([]),
queuePending: writable([]),
queueCompleted: writable([]),
queueRemaining: null,
runningNodeID: null,
progress: null,
isInterrupting: false
})
function statusUpdated(status: ComfyAPIQueueStatus | null) {
function toQueueEntry(resp: ComfyAPIHistoryItem): QueueEntry {
const [num, promptID, prompt, extraData, goodOutputs] = resp
return {
number: num,
queuedAt: null, // TODO when ComfyUI passes the date
finishedAt: null,
promptID,
prompt,
extraData,
goodOutputs,
outputs: {},
nodesRan: new Set(), // TODO can ComfyUI send this too?
cachedNodes: new Set()
}
}
function toCompletedQueueEntry(resp: ComfyAPIHistoryEntry): CompletedQueueEntry {
const entry = toQueueEntry(resp.prompt)
entry.outputs = resp.outputs;
return {
entry,
status: Object.values(entry.outputs).length > 0 ? "success" : "all_cached",
error: null
}
}
function queueUpdated(resp: ComfyAPIQueueResponse) {
console.debug("[queueState] queueUpdated", resp.running.length, resp.pending.length)
store.update((s) => {
if (status !== null)
s.queueRemaining = status.exec_info.queue_remaining;
s.queueRunning.set(resp.running.map(toQueueEntry));
s.queuePending.set(resp.pending.map(toQueueEntry));
s.queueRemaining = resp.pending.length;
return s
})
}
function executingUpdated(runningNodeId: string | null) {
function historyUpdated(resp: ComfyAPIHistoryResponse) {
console.debug("[queueState] historyUpdated", Object.values(resp.history).length)
store.update((s) => {
s.progress = null;
s.runningNodeId = parseInt(runningNodeId);
const values = Object.values(resp.history) // TODO Order by prompt finished date!
s.queueCompleted.set(values.map(toCompletedQueueEntry));
return s
})
}
function progressUpdated(progress: Progress | null) {
function progressUpdated(progress: Progress) {
// console.debug("[queueState] progressUpdated", progress)
store.update((s) => {
s.progress = progress;
return s
})
}
function statusUpdated(status: ComfyAPIStatusResponse | null) {
console.debug("[queueState] statusUpdated", status)
store.update((s) => {
if (status !== null)
s.queueRemaining = status.execInfo.queueRemaining;
return s
})
}
function findEntryInPending(promptID: PromptID): [number, QueueEntry | null, Writable<QueueEntry[]> | null] {
const state = get(store);
let index = get(state.queuePending).findIndex(e => e.promptID === promptID)
if (index !== -1)
return [index, get(state.queuePending)[index], state.queuePending]
index = get(state.queueRunning).findIndex(e => e.promptID === promptID)
if (index !== -1)
return [index, get(state.queueRunning)[index], state.queueRunning]
return [-1, null, null]
}
function moveToCompleted(index: number, queue: Writable<QueueEntry[]>, status: QueueEntryStatus, message?: string, error?: string) {
const state = get(store)
const entry = get(queue)[index];
console.debug("[queueState] Move to completed", entry.promptID, index, status, message, error)
entry.finishedAt = new Date() // Now
queue.update(qp => { qp.splice(index, 1); return qp });
state.queueCompleted.update(qc => {
const completed: CompletedQueueEntry = { entry, status, message, error }
qc.push(completed)
return qc
})
store.set(state)
}
function executingUpdated(promptID: PromptID, runningNodeID: NodeID | null) {
console.debug("[queueState] executingUpdated", promptID, runningNodeID)
store.update((s) => {
s.progress = null;
const [index, entry, queue] = findEntryInPending(promptID);
if (runningNodeID != null) {
if (entry != null) {
entry.nodesRan.add(runningNodeID)
}
s.runningNodeID = parseInt(runningNodeID);
}
else {
// Prompt finished executing.
if (entry != null) {
const totalNodesInPrompt = Object.keys(entry.prompt).length
if (entry.cachedNodes.size >= Object.keys(entry.prompt).length) {
notify("Prompt was cached, nothing to run.", { type: "warning" })
moveToCompleted(index, queue, "all_cached", "(Execution was cached)");
}
else if (entry.nodesRan.size >= totalNodesInPrompt) {
moveToCompleted(index, queue, "success")
}
else {
notify("Interrupted prompt.")
moveToCompleted(index, queue, "interrupted", `Interrupted after ${entry.nodesRan.size}/${totalNodesInPrompt} nodes`)
}
}
else {
console.debug("[queueState] Could not find in pending! (executingUpdated)", promptID)
}
s.progress = null;
s.runningNodeID = null;
}
return s
})
}
function executionCached(promptID: PromptID, nodes: NodeID[]) {
console.debug("[queueState] executionCached", promptID, nodes)
store.update(s => {
const [index, entry, queue] = findEntryInPending(promptID);
if (entry != null) {
for (const nodeID of nodes) {
entry.nodesRan.add(nodeID);
entry.cachedNodes.add(nodeID);
}
}
else {
console.error("[queueState] Could not find in pending! (executionCached)", promptID, "pending", JSON.stringify(get(get(store).queuePending).map(p => p.promptID)), "running", JSON.stringify(get(get(store).queueRunning).map(p => p.promptID)))
}
s.isInterrupting = false; // TODO move to start
s.progress = null;
s.runningNodeID = null;
return s
})
}
function executionError(promptID: PromptID, message: string) {
console.debug("[queueState] executionError", promptID, message)
store.update(s => {
const [index, entry, queue] = findEntryInPending(promptID);
if (entry != null) {
moveToCompleted(index, queue, "error", "Error executing", message)
}
else {
console.error("[queueState] Could not find in pending! (executionError)", promptID)
}
s.progress = null;
s.runningNodeID = null;
return s
})
}
function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) {
console.debug("[queueState] afterQueued", promptID, Object.keys(prompt))
store.update(s => {
const entry: QueueEntry = {
number,
queuedAt: new Date(), // Now
finishedAt: null,
promptID,
prompt,
extraData,
goodOutputs: [],
outputs: {},
nodesRan: new Set(),
cachedNodes: new Set()
}
s.queuePending.update(qp => { qp.push(entry); return qp })
console.debug("[queueState] ADD PROMPT", promptID)
s.isInterrupting = false;
return s
})
}
function onExecuted(promptID: PromptID, nodeID: NodeID, output: GalleryOutput) {
console.debug("[queueState] onExecuted", promptID, nodeID, output)
store.update(s => {
const [index, entry, queue] = findEntryInPending(promptID)
if (entry != null) {
entry.outputs[nodeID] = output;
queue.set(get(queue))
}
else {
console.error("[queueState] Could not find in pending! (onExecuted)", promptID)
}
return s
})
}
const queueStateStore: WritableQueueStateStore =
{
...store,
queueUpdated,
historyUpdated,
statusUpdated,
progressUpdated,
executingUpdated,
progressUpdated
executionCached,
executionError,
afterQueued,
onExecuted
}
export default queueStateStore;

View File

@@ -10,11 +10,17 @@ export type UIState = {
uiUnlocked: boolean,
uiEditMode: UIEditMode,
reconnecting: boolean,
isSavingToLocalStorage: boolean
}
export type WritableUIStateStore = Writable<UIState>;
const store: WritableUIStateStore = writable(
type UIStateOps = {
reconnecting: () => void,
reconnected: () => void,
}
export type WritableUIStateStore = Writable<UIState> & UIStateOps;
const store: Writable<UIState> = writable(
{
graphLocked: false,
nodesLocked: false,
@@ -22,11 +28,22 @@ const store: WritableUIStateStore = writable(
uiUnlocked: false,
uiEditMode: "widgets",
reconnecting: false,
isSavingToLocalStorage: false
})
function reconnecting() {
store.update(s => { s.reconnecting = true; return s; })
}
function reconnected() {
store.update(s => { s.reconnecting = false; return s; })
}
const uiStateStore: WritableUIStateStore =
{
...store
...store,
reconnecting,
reconnected
}
export default uiStateStore;

View File

@@ -13,6 +13,10 @@ export function clamp(n: number, min: number, max: number): number {
return Math.min(Math.max(n, min), max)
}
export function negmod(n: number, m: number): number {
return ((n % m) + m) % m;
}
export function range(size: number, startAt: number = 0): ReadonlyArray<number> {
return [...Array(size).keys()].map(i => i + startAt);
}
@@ -104,7 +108,7 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
export function getNodeInfo(nodeId: number): string {
let app = (window as any).app;
if (!app)
if (!app || !app.lGraph)
return String(nodeId);
const title = app.lGraph.getNodeById(nodeId)?.title || String(nodeId);
@@ -135,6 +139,12 @@ export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileDat
});
}
export function convertComfyOutputToComfyURL(output: GalleryOutputEntry): string {
const params = new URLSearchParams(output)
const url = `http://${location.hostname}:8188` // TODO make configurable
return url + "/view?" + params
}
export function convertFilenameToComfyURL(filename: string,
subfolder: string = "",
type: "input" | "output" | "temp" = "output"): string {

View File

@@ -246,6 +246,22 @@
width: auto;
--font-size: 13px;
--height: 32px;
--background: var(--input-background-fill);
--selected-item-color: var(--body-text-color);
--input-color: var(--body-text-color);
--chevron-color: var(--body-text-color);
--border: 1px solid var(--input-border-color);
--border-hover: 1px solid var(--input-border-color-hover);
--border-focused: 1px solid var(--input-border-color-focus);
--border-radius-focused: 0px;
--border-radius: 0px;
--list-background: var(--comfy-dropdown-list-background);
--item-border: var(--comfy-dropdown-border-color);
--item-color: var(--body-text-color);
--item-color-hover: var(--comfy-dropdown-item-color-hover);
--item-background-hover: var(--comfy-dropdown-item-background-hover);
--item-color-active: var(--comfy-dropdown-item-color-active);
--item-background-active: var(--comfy-dropdown-item-background-active);
}
:global(.svelte-select-list) {
@@ -263,10 +279,11 @@
.comfy-select-list {
width: 30rem;
color: var(--item-color);
> :global(.virtual-list-wrapper) {
box-shadow: var(--block-shadow);
background-color: white;
background-color: var(--list-background);
}
.comfy-empty-list {
@@ -279,35 +296,34 @@
}
.comfy-select-item {
border: 1px solid var(--neutral-300);
border: 1px solid var(--item-border);
border-top: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
background-color: white;
background-color: var(--list-background);
font-size: 14px;
padding: 0.2rem;
.comfy-select-label {
}
&.mobile {
font-size: 16px;
padding: 1.2rem;
}
&.hover {
color: white;
background: var(--neutral-400);
color: var(--item-color-hover);
background: var(--item-background-hover);
cursor: pointer;
}
&.active {
color: white;
background: var(--color-blue-500);
}
.comfy-select-label {
color: var(--item-color-active);
background: var(--item-background-active);
}
}

View File

@@ -122,14 +122,14 @@
// the event might fire too early
const callback = isMobile ? setupImageForMobileLightbox
: ImageViewer.instance.setupImageForLightbox.bind(ImageViewer.instance)
: ImageViewer.instance.setupGalleryImageForLightbox.bind(ImageViewer.instance)
setTimeout(() => {
const images = element.querySelectorAll<HTMLImageElement>('div.block div > img')
if (images != null) {
images.forEach(callback);
}
ImageViewer.instance.updateOnBackgroundChange();
ImageViewer.instance.refreshImages();
}, 200)
// Update index

View File

@@ -51,12 +51,7 @@
padding: 2px;
width: 100%;
:global(input[type=text]:disabled) {
@include disable-input;
}
:global(textarea:disabled) {
@include disable-input;
}
@include disable-inputs;
}
:global(span.hide) {

View File

@@ -5,6 +5,7 @@ import { f7 } from 'framework7-svelte';
import ComfyApp from '$lib/components/ComfyApp';
import uiState from '$lib/stores/uiState';
import { LiteGraph } from '@litegraph-ts/core';
import ComfyGraph from '$lib/ComfyGraph';
Framework7.use(Framework7Svelte);

View File

@@ -57,9 +57,9 @@
</script>
<div class="bottom">
{#if $queueState.runningNodeId || $queueState.progress}
{#if $queueState.runningNodeID || $queueState.progress}
<div class="node-name">
<span>Node: {getNodeInfo($queueState.runningNodeId)}</span>
<span>Node: {getNodeInfo($queueState.runningNodeID)}</span>
</div>
<div class="progress-bar">
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- <link rel="icon" href="%sveltekit.assets%/favicon.png" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#2196f3">
</head>
<body>
<div id="app">
<script type="module" src='/src/main-mobile.ts'></script>
</body>
</html>

View File

@@ -7,14 +7,154 @@ body {
overscroll-behavior-y: contain;
}
#app {
background: var(--body-background-fill);
}
:root {
--color-blue-500: #3985f5;
--comfy-accent-soft: var(--neutral-300);
--comfy-disabled-label-color: var(--neutral-400);
--comfy-disabled-textbox-background-fill: var(--neutral-200);
--comfy-disabled-textbox-border-color: var(--neutral-300);
--comfy-disabled-textbox-text-color: var(--neutral-500);
--comfy-splitpanes-background-fill: var(--secondary-100);
--comfy-splitpanes-background-fill-hover: var(--secondary-300);
--comfy-splitpanes-background-fill-active: var(--secondary-400);
--comfy-dropdown-item-color-hover: white;
--comfy-dropdown-item-background-hover: var(--neutral-400);
--comfy-dropdown-item-color-active: var(--neutral-100);
--comfy-dropdown-item-background-active: var(--secondary-600);
--comfy-progress-bar-background: var(--neutral-300);
--comfy-progress-bar-foreground: var(--secondary-300);
--comfy-node-name-background: var(--color-red-300);
--comfy-node-name-foreground: var(--body-text-color);
--comfy-spinner-main-color: var(--neutral-400);
--comfy-spinner-accent-color: var(--secondary-500);
}
.dark {
color-scheme: dark;
--comfy-accent-soft: var(--neutral-600);
--comfy-disabled-label-color: var(--neutral-500);
--comfy-disabled-textbox-background-fill: var(--neutral-800);
--comfy-disabled-textbox-border-color: var(--neutral-700);
--comfy-disabled-textbox-text-color: var(--neutral-500);
--comfy-splitpanes-background-fill: var(--panel-border-color);
--comfy-splitpanes-background-fill-hover: var(--secondary-500);
--comfy-splitpanes-background-fill-active: var(--secondary-300);
--comfy-dropdown-list-background: var(--neutral-800);
--comfy-dropdown-item-color-hover: var(--neutral-400);
--comfy-dropdown-item-background-hover: var(--neutral-600);
--comfy-dropdown-item-background-active: var(--secondary-600);
--comfy-dropdown-border-color: var(--neutral-600);
--comfy-progress-bar-background: var(--neutral-500);
--comfy-progress-bar-foreground: var(--secondary-400);
--comfy-node-name-background: var(--neutral-700);
--comfy-node-name-foreground: var(--body-text-color);
--comfy-spinner-main-color: var(--neutral-600);
--comfy-spinner-accent-color: var(--secondary-600);
}
.mobile {
--comfy-progress-bar-background: lightgrey;
--comfy-progress-bar-foreground: #B3D8A9
}
@mixin disable-input {
-webkit-text-fill-color: var(--neutral-500);
background-color: var(--neutral-200);
border-color: var(--neutral-300);
-webkit-text-fill-color: var(--comfy-disabled-textbox-text-color);
background-color: var(--comfy-disabled-textbox-background-fill);
border-color: var(--comfy-disabled-textbox-border-color);
box-shadow: 0 0 0 var(--shadow-spread) transparent, rgba(0, 0, 0, 0.08) 0px 2px 4px 0px inset;
cursor: not-allowed;
}
@mixin disable-inputs {
:global(input[type=text]:disabled) {
@include disable-input;
}
:global(textarea:disabled) {
@include disable-input;
}
:global(label:has(input:disabled) > span) {
color: var(--comfy-disabled-label-color);
}
:global(label:has(textarea:disabled) > span) {
color: var(--comfy-disabled-label-color);
}
}
hr {
color: var(--panel-border-color);
}
select {
color: var(--body-text-color);
background: var(--block-background-fill);
}
.container {
background: var(--body-background-fill) !important;
&.selected {
background: var(--ae-primary-color) !important;
> .block.padded {
background: var(--ae-primary-color) !important;
}
}
> .block {
background: var(--body-background-fill) !important;
// border-radius: var(--ae-panel-border-radius) !important;
}
&.z-index0 {
> .block {
background: var(--panel-background-fill) !important;
}
}
// &:not(.edit) {
// &.z-index1 > .block {
// padding: calc(var(--ae-outside-gap-size) / 2) !important;
// border-width: 0px !important;
// }
// > .block {
// border: solid var(--ae-panel-border-width) var(--ae-panel-border-color) !important;
// }
// }
}
// button {
// filter: none;
// &.primary:active {
// filter: brightness(80%)
// }
// &.secondary:active {
// filter: brightness(80%)
// }
// }
button {
&.primary:active {
border-color: var(--button-primary-border-color-active) !important;
background: var(--button-primary-background-fill-active) !important;
color: var(--button-primary-text-color-active) !important;
}
&.secondary:active {
border-color: var(--button-secondary-border-color-active) !important;
background: var(--button-secondary-background-fill-active) !important;
color: var(--button-secondary-text-color-active) !important;
}
}
.widget {
// padding: var(--ae-outside-gap-size);
// border: 1px solid var(--ae-panel-border-color);
.block {
// background: var(--ae-frame-bg-color) !important;
background: var(--block-background-fill) !important;
}
}

View File

@@ -1,4 +1,5 @@
:root {
--name: default;
--primary-50: #fff7ed;
--primary-100: #ffedd5;
--primary-200: #fed7aa;
@@ -53,27 +54,26 @@
--text-lg: 16px;
--text-xl: 22px;
--text-xxl: 26px;
--font: 'Source Sans Pro', 'ui-sans-serif', 'system-ui', sans-serif;
--font-mono: 'IBM Plex Mono', 'ui-monospace', 'Consolas', monospace;
--body-background-fill: var(--background-fill-primary);
--body-text-color: var(--neutral-800);
--body-text-size: var(--text-md);
--body-text-weight: 400;
--embed-radius: var(--radius-lg);
--color-accent: var(--primary-500);
--color-accent-soft: var(--primary-50);
--background-fill-primary: white;
--background-fill-secondary: var(--neutral-50);
--border-color-accent: var(--primary-300);
--border-color-primary: var(--neutral-200);
--text-color-code-background-fill: var(--neutral-200);
--text-color-code-border: var(--border-color-primary);
--link-text-color: var(--secondary-600);
--link-text-color-active: var(--secondary-600);
--link-text-color-hover: var(--secondary-700);
--link-text-color-visited: var(--secondary-500);
--body-text-color-subdued: var(--neutral-400);
--body-background-fill: var(--background-fill-primary);
--body-text-color: var(--neutral-800);
--body-text-size: var(--text-md);
--body-text-weight: 400;
--embed-radius: var(--radius-lg);
--shadow-drop: rgba(0,0,0,0.05) 0px 1px 2px 0px;
--shadow-drop-lg: 0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-drop-lg: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-inset: rgba(0,0,0,0.05) 0px 2px 4px 0px inset;
--shadow-spread: 3px;
--block-background-fill: var(--background-fill-primary);
@@ -85,14 +85,12 @@
--block-label-background-fill: var(--background-fill-primary);
--block-label-border-color: var(--border-color-primary);
--block-label-border-width: 1px;
--block-label-shadow: var(--block-shadow);
--block-label-text-color: var(--neutral-500);
--block-label-icon-color: var(--block-label-text-color);
--block-label-margin: 0;
--block-label-padding: var(--spacing-sm) var(--spacing-lg);
--block-label-radius: calc(var(--radius-lg) - 1px) 0
calc(var(--radius-lg) - 1px) 0;
--block-label-right-radius: 0 calc(var(--radius-lg) - 1px) 0
calc(var(--radius-lg) - 1px);
--block-label-radius: calc(var(--radius-lg) - 1px) 0 calc(var(--radius-lg) - 1px) 0;
--block-label-right-radius: 0 calc(var(--radius-lg) - 1px) 0 calc(var(--radius-lg) - 1px);
--block-label-text-size: var(--text-sm);
--block-label-text-weight: 400;
--block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px);
@@ -115,8 +113,8 @@
--section-header-text-size: var(--text-md);
--section-header-text-weight: 400;
--checkbox-background-color: var(--background-fill-primary);
--checkbox-background-color-focus: var(--background-fill-primary);
--checkbox-background-color-hover: var(--background-fill-primary);
--checkbox-background-color-focus: var(--checkbox-background-color);
--checkbox-background-color-hover: var(--checkbox-background-color);
--checkbox-background-color-selected: var(--secondary-600);
--checkbox-border-color: var(--neutral-300);
--checkbox-border-color-focus: var(--secondary-500);
@@ -124,78 +122,54 @@
--checkbox-border-color-selected: var(--secondary-600);
--checkbox-border-radius: var(--radius-sm);
--checkbox-border-width: var(--input-border-width);
--checkbox-label-background-fill: linear-gradient(
to top,
var(--neutral-50),
white
);
--checkbox-label-background-fill-hover: linear-gradient(
to top,
var(--neutral-100),
white
);
--checkbox-label-background-fill-selected: var(
--checkbox-label-background-fill
);
--checkbox-label-background-fill: linear-gradient(to top, var(--neutral-50), white);
--checkbox-label-background-fill-hover: linear-gradient(to top, var(--neutral-100), white);
--checkbox-label-background-fill-selected: var(--checkbox-label-background-fill);
--checkbox-label-border-color: var(--border-color-primary);
--checkbox-label-border-color-hover: var(--border-color-primary);
--checkbox-label-border-color-hover: var(--checkbox-label-border-color);
--checkbox-label-border-width: var(--input-border-width);
--checkbox-label-gap: var(--spacing-lg);
--checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md));
--checkbox-label-shadow: var(--shadow-drop);
--checkbox-label-text-size: var(--text-md);
--checkbox-label-text-weight: 400;
--checkbox-check: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
--radio-circle: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
--checkbox-shadow: var(--input-shadow);
--checkbox-label-text-color: var(--body-text-color);
--checkbox-label-text-color-selected: var(--checkbox-label-text-color);
--error-background-fill: linear-gradient(
to right,
#fee2e2,
var(--background-fill-secondary)
);
--error-background-fill: linear-gradient(to right, #fee2e2, var(--background-fill-secondary));
--error-border-color: #fecaca;
--error-border-width: 1px;
--error-text-color: #ef4444;
--prose-header-text-weight: 600;
--input-background-fill: white;
--input-background-fill-focus: var(--secondary-500);
--input-background-fill-hover: var(--input-background-fill);
--input-border-color: var(--border-color-primary);
--input-border-color-focus: var(--secondary-300);
--input-border-color-hover: var(--border-color-primary);
--input-border-color-hover: var(--input-border-color);
--input-border-width: 1px;
--input-padding: var(--spacing-xl);
--input-placeholder-color: var(--neutral-400);
--input-radius: var(--radius-lg);
--input-shadow: 0 0 0 var(--shadow-spread) transparent, var(--shadow-inset);
--input-shadow-focus: 0 0 0 var(--shadow-spread) var(--secondary-50),
var(--shadow-inset);
--input-shadow-focus: 0 0 0 var(--shadow-spread) var(--secondary-50), var(--shadow-inset);
--input-text-size: var(--text-md);
--input-text-weight: 400;
--loader-color: var(--color-accent);
--prose-text-size: var(--text-md);
--prose-text-weight: 400;
--stat-background-fill: linear-gradient(
to right,
var(--primary-400),
var(--primary-200)
);
--prose-header-text-weight: 600;
--slider-color: auto;
--stat-background-fill: linear-gradient(to right, var(--primary-400), var(--primary-200));
--table-border-color: var(--neutral-300);
--table-even-background-fill: white;
--table-odd-background-fill: var(--neutral-50);
--table-radius: var(--radius-lg);
--table-row-focus: var(--color-accent-soft);
--button-border-width: var(--input-border-width);
--button-cancel-background-fill: linear-gradient(
to bottom right,
#fee2e2,
#fecaca
);
--button-cancel-background-fill-hover: linear-gradient(
to bottom right,
#fee2e2,
#fee2e2
);
--button-cancel-background-fill: linear-gradient(to bottom right, #fee2e2, #fecaca);
--button-cancel-background-fill-hover: linear-gradient(to bottom right, #fee2e2, #fee2e2);
--button-cancel-border-color: #fecaca;
--button-cancel-border-color-hover: var(--button-cancel-border-color);
--button-cancel-text-color: #dc2626;
@@ -204,34 +178,24 @@
--button-large-radius: var(--radius-lg);
--button-large-text-size: var(--text-lg);
--button-large-text-weight: 600;
--button-primary-background-fill: linear-gradient(
to bottom right,
var(--primary-100),
var(--primary-300)
);
--button-primary-background-fill-hover: linear-gradient(
to bottom right,
var(--primary-100),
var(--primary-200)
);
--button-primary-background-fill: linear-gradient(to bottom right, var(--primary-100), var(--primary-300));
--button-primary-background-fill-hover: linear-gradient(to bottom right, var(--primary-100), var(--primary-200));
--button-primary-background-fill-active: linear-gradient(to bottom right, var(--primary-200), var(--primary-400));
--button-primary-border-color: var(--primary-200);
--button-primary-border-color-hover: var(--button-primary-border-color);
--button-primary-border-color-active: var(--button-primary-border-color);
--button-primary-text-color: var(--primary-600);
--button-primary-text-color-hover: var(--button-primary-text-color);
--button-secondary-background-fill: linear-gradient(
to bottom right,
var(--neutral-100),
var(--neutral-200)
);
--button-secondary-background-fill-hover: linear-gradient(
to bottom right,
var(--neutral-100),
var(--neutral-100)
);
--button-primary-text-color-active: var(--button-primary-text-color);
--button-secondary-background-fill: linear-gradient(to bottom right, var(--neutral-100), var(--neutral-200));
--button-secondary-background-fill-hover: linear-gradient(to bottom right, var(--neutral-100), var(--neutral-100));
--button-secondary-background-fill-active: linear-gradient(to bottom right, var(--neutral-200), var(--neutral-300));
--button-secondary-border-color: var(--neutral-200);
--button-secondary-border-color-hover: var(--button-secondary-border-color);
--button-secondary-border-color-active: var(--button-secondary-border-color);
--button-secondary-text-color: var(--neutral-700);
--button-secondary-text-color-hover: var(--button-secondary-text-color);
--button-secondary-text-color-active: var(--neutral-800);
--button-shadow: var(--shadow-drop);
--button-shadow-active: var(--shadow-inset);
--button-shadow-hover: var(--shadow-drop-lg);
@@ -241,36 +205,37 @@
--button-small-text-weight: 400;
--button-transition: none;
}
.dark {
--color-accent-soft: var(--neutral-900);
--body-background-fill: var(--background-fill-primary);
--body-text-color: var(--neutral-100);
--color-accent-soft: var(--neutral-700);
--background-fill-primary: var(--neutral-950);
--background-fill-secondary: var(--neutral-900);
--border-color-accent: var(--neutral-600);
--border-color-primary: var(--neutral-700);
--text-color-code-background-fill: var(--neutral-800);
--link-text-color-active: var(--secondary-500);
--link-text-color: var(--secondary-500);
--link-text-color-hover: var(--secondary-400);
--link-text-color-visited: var(--secondary-600);
--body-text-color-subdued: var(--neutral-400);
--body-background-fill: var(--background-fill-primary);
--body-text-color: var(--neutral-100);
--shadow-spread: 1px;
--block-background-fill: var(--neutral-800);
--block-border-color: var(--border-color-primary);
--block-border-width: 1px;
--block_border_width: None;
--block-info-text-color: var(--body-text-color-subdued);
--block-label-background-fill: var(--background-fill-secondary);
--block-label-border-color: var(--border-color-primary);
--block-label-border-width: 1px;
--block_label_border_width: None;
--block-label-text-color: var(--neutral-200);
--block-shadow: none;
--block-title-background-fill: none;
--block-title-border-color: none;
--block-title-border-width: 0px;
--block_shadow: None;
--block_title_background_fill: None;
--block_title_border_color: None;
--block_title_border_width: None;
--block-title-text-color: var(--neutral-200);
--panel-background-fill: var(--background-fill-secondary);
--panel-border-color: var(--border-color-primary);
--panel_border_width: None;
--checkbox-background-color: var(--neutral-800);
--checkbox-background-color-focus: var(--checkbox-background-color);
--checkbox-background-color-hover: var(--checkbox-background-color);
@@ -279,87 +244,184 @@
--checkbox-border-color-focus: var(--secondary-500);
--checkbox-border-color-hover: var(--neutral-600);
--checkbox-border-color-selected: var(--secondary-600);
--checkbox-label-background-fill: linear-gradient(
to top,
var(--neutral-900),
var(--neutral-800)
);
--checkbox-label-background-fill-hover: linear-gradient(
to top,
var(--neutral-900),
var(--neutral-800)
);
--checkbox-label-background-fill-selected: var(
--checkbox-label-background-fill
);
--checkbox-border-width: var(--input-border-width);
--checkbox-label-background-fill: linear-gradient(to top, var(--neutral-900), var(--neutral-800));
--checkbox-label-background-fill-hover: linear-gradient(to top, var(--neutral-900), var(--neutral-800));
--checkbox-label-background-fill-selected: var(--checkbox-label-background-fill);
--checkbox-label-border-color: var(--border-color-primary);
--checkbox-label-border-color-hover: var(--border-color-primary);
--checkbox-label-border-color-hover: var(--checkbox-label-border-color);
--checkbox-label-border-width: var(--input-border-width);
--checkbox-label-text-color: var(--body-text-color);
--checkbox-label-text-color-selected: var(--checkbox-label-text-color);
--error-background-fill: var(--background-fill-primary);
--error-border-color: var(--border-color-primary);
--error-border-width: var(--error-border-width);
--error_border_width: None;
--error-text-color: #ef4444;
--input-background-fill: var(--neutral-800);
--input-background-fill-focus: var(--secondary-600);
--input-background-fill-hover: var(--input-background-fill);
--input-border-color: var(--border-color-primary);
--input-border-color-focus: var(--neutral-700);
--input-border-color-hover: var(--border-color-primary);
--input-border-color-hover: var(--input-border-color);
--input_border_width: None;
--input-placeholder-color: var(--neutral-500);
--input-shadow: var(--input-shadow);
--input-shadow-focus: 0 0 0 var(--shadow-spread) var(--neutral-700),
var(--shadow-inset);
--loader-color: var(--loader-color);
--stat-background-fill: linear-gradient(
to right,
var(--primary-400),
var(--primary-600)
);
--input_shadow: None;
--input-shadow-focus: 0 0 0 var(--shadow-spread) var(--neutral-700), var(--shadow-inset);
--loader_color: None;
--slider_color: None;
--stat-background-fill: linear-gradient(to right, var(--primary-400), var(--primary-600));
--table-border-color: var(--neutral-700);
--table-even-background-fill: var(--neutral-950);
--table-odd-background-fill: var(--neutral-900);
--table-row-focus: var(--color-accent-soft);
--button-cancel-background-fill: linear-gradient(
to bottom right,
#dc2626,
#b91c1c
);
--button-cancel-background-fill-hover: linear-gradient(
to bottom right,
#dc2626,
#dc2626
);
--button-border-width: var(--input-border-width);
--button-cancel-background-fill: linear-gradient(to bottom right, #dc2626, #b91c1c);
--button-cancel-background-fill-hover: linear-gradient(to bottom right, #dc2626, #dc2626);
--button-cancel-border-color: #dc2626;
--button-cancel-border-color-hover: var(--button-cancel-border-color);
--button-cancel-text-color: white;
--button-cancel-text-color-hover: var(--button-cancel-text-color);
--button-primary-background-fill: linear-gradient(
to bottom right,
var(--primary-600),
var(--primary-700)
);
--button-primary-background-fill-hover: linear-gradient(
to bottom right,
var(--primary-600),
var(--primary-600)
);
--button-primary-border-color: var(--primary-600);
--button-primary-background-fill: linear-gradient(to bottom right, var(--primary-500), var(--primary-600));
--button-primary-background-fill-hover: linear-gradient(to bottom right, var(--primary-500), var(--primary-500));
--button-primary-background-fill-active: linear-gradient(to bottom right, var(--primary-600), var(--primary-700));
--button-primary-border-color: var(--primary-500);
--button-primary-border-color-hover: var(--button-primary-border-color);
--button-primary-border-color-active: var(--button-primary-border-color);
--button-primary-text-color: white;
--button-primary-text-color-hover: var(--button-primary-text-color);
--button-secondary-background-fill: linear-gradient(
to bottom right,
var(--neutral-600),
var(--neutral-700)
);
--button-secondary-background-fill-hover: linear-gradient(
to bottom right,
var(--neutral-600),
var(--neutral-600)
);
--button-primary-text-color-active: var(--neutral-300);
--button-secondary-background-fill: linear-gradient(to bottom right, var(--neutral-600), var(--neutral-700));
--button-secondary-background-fill-hover: linear-gradient(to bottom right, var(--neutral-600), var(--neutral-600));
--button-secondary-background-fill-active: linear-gradient(to bottom right, var(--neutral-700), var(--neutral-800));
--button-secondary-border-color: var(--neutral-600);
--button-secondary-border-color-hover: var(--button-secondary-border-color);
--button-secondary-border-color-active: var(--button-secondary-border-color);
--button-secondary-text-color: white;
--button-secondary-text-color-hover: var(--button-secondary-text-color);
--button-secondary-text-color-active: var(--neutral-300);
--name: default;
--primary-50: #fff7ed;
--primary-100: #ffedd5;
--primary-200: #fed7aa;
--primary-300: #fdba74;
--primary-400: #fb923c;
--primary-500: #f97316;
--primary-600: #ea580c;
--primary-700: #c2410c;
--primary-800: #9a3412;
--primary-900: #7c2d12;
--primary-950: #6c2e12;
--secondary-50: #eff6ff;
--secondary-100: #dbeafe;
--secondary-200: #bfdbfe;
--secondary-300: #93c5fd;
--secondary-400: #60a5fa;
--secondary-500: #3b82f6;
--secondary-600: #2563eb;
--secondary-700: #1d4ed8;
--secondary-800: #1e40af;
--secondary-900: #1e3a8a;
--secondary-950: #1d3660;
--neutral-50: #f9fafb;
--neutral-100: #f3f4f6;
--neutral-200: #e5e7eb;
--neutral-300: #d1d5db;
--neutral-400: #9ca3af;
--neutral-500: #6b7280;
--neutral-600: #4b5563;
--neutral-700: #374151;
--neutral-800: #1f2937;
--neutral-900: #111827;
--neutral-950: #0b0f19;
--spacing-xxs: 1px;
--spacing-xs: 2px;
--spacing-sm: 4px;
--spacing-md: 6px;
--spacing-lg: 8px;
--spacing-xl: 10px;
--spacing-xxl: 16px;
--radius-xxs: 1px;
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-xxl: 22px;
--text-xxs: 9px;
--text-xs: 10px;
--text-sm: 12px;
--text-md: 14px;
--text-lg: 16px;
--text-xl: 22px;
--text-xxl: 26px;
--font: 'Source Sans Pro', 'ui-sans-serif', 'system-ui', sans-serif;
--font-mono: 'IBM Plex Mono', 'ui-monospace', 'Consolas', monospace;
--body-text-size: var(--text-md);
--body-text-weight: 400;
--embed-radius: var(--radius-lg);
--color-accent: var(--primary-500);
--shadow-drop: rgba(0,0,0,0.05) 0px 1px 2px 0px;
--shadow-drop-lg: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-inset: rgba(0,0,0,0.05) 0px 2px 4px 0px inset;
--block-border-width: 1px;
--block-info-text-size: var(--text-sm);
--block-info-text-weight: 400;
--block-label-border-width: 1px;
--block-label-shadow: var(--block-shadow);
--block-label-margin: 0;
--block-label-padding: var(--spacing-sm) var(--spacing-lg);
--block-label-radius: calc(var(--radius-lg) - 1px) 0 calc(var(--radius-lg) - 1px) 0;
--block-label-right-radius: 0 calc(var(--radius-lg) - 1px) 0 calc(var(--radius-lg) - 1px);
--block-label-text-size: var(--text-sm);
--block-label-text-weight: 400;
--block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px);
--block-radius: var(--radius-lg);
--block-shadow: var(--shadow-drop);
--block-title-background-fill: none;
--block-title-border-color: none;
--block-title-border-width: 0px;
--block-title-padding: 0;
--block-title-radius: none;
--block-title-text-size: var(--text-md);
--block-title-text-weight: 400;
--container-radius: var(--radius-lg);
--form-gap-width: 1px;
--layout-gap: var(--spacing-xxl);
--panel-border-width: 0;
--section-header-text-size: var(--text-md);
--section-header-text-weight: 400;
--checkbox-border-radius: var(--radius-sm);
--checkbox-label-gap: var(--spacing-lg);
--checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md));
--checkbox-label-shadow: var(--shadow-drop);
--checkbox-label-text-size: var(--text-md);
--checkbox-label-text-weight: 400;
--checkbox-check: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
--radio-circle: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
--checkbox-shadow: var(--input-shadow);
--error-border-width: 1px;
--input-border-width: 1px;
--input-padding: var(--spacing-xl);
--input-radius: var(--radius-lg);
--input-shadow: 0 0 0 var(--shadow-spread) transparent, var(--shadow-inset);
--input-text-size: var(--text-md);
--input-text-weight: 400;
--loader-color: var(--color-accent);
--prose-text-size: var(--text-md);
--prose-text-weight: 400;
--prose-header-text-weight: 600;
--slider-color: auto;
--table-radius: var(--radius-lg);
--button-large-padding: var(--spacing-lg) calc(2 * var(--spacing-lg));
--button-large-radius: var(--radius-lg);
--button-large-text-size: var(--text-lg);
--button-large-text-weight: 600;
--button-shadow: var(--shadow-drop);
--button-shadow-active: var(--shadow-inset);
--button-shadow-hover: var(--shadow-drop-lg);
--button-small-padding: var(--spacing-sm) calc(2 * var(--spacing-sm));
--button-small-radius: var(--radius-lg);
--button-small-text-size: var(--text-md);
--button-small-text-weight: 400;
--button-transition: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -70,6 +70,6 @@ export default defineConfig({
// }
},
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
include: ['litegraph/packages/tests/src/main.ts']
}
});