mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
v1.2.0 Side Bar & Menu rework (#189)
* Basic side tool bar skeleton + Theme toggle (#164) * Side bar skeleton * Fix grid layout * nit * Add theme toggle logic * Change primevue color theme to blue to match beta menu UI * Add litegraph canvas splitter overlay (#177) * Add vue wrapper * Splitter overlay * Move teleport to side bar comp * Toolbar placeholder * Move settings button from top menu to side bar (#178) * Reverse relationship between splitter overlay and sidebar component (#180) * Reverse relationship between splitter overlay and sidebar component * nit * Remove border on splitter * Fix canvas shift (#186) * Move queue/history display to side bar (#185) * Side bar placeholder * Pinia store for queue items * Flatten task item * Fix schema * computed * Switch running / pending order * Use class-transformer * nit * Show display status * Add tag severity style * Add execution time * nit * Rename to execution success * Add time display * Sort queue desc order * nit * Add remove item feature * Load workflow * Add confirmation popup * Add empty table placeholder * Remove beta menu UI's queue button/list * Add tests on litegraph widget text truncate (#191) * Add tests on litegraph widget text truncate * Updated screenshots * Revert port change * Remove screenshots * Update test expectations [skip ci] * Add back menu.settingsGroup for compatibility (#192) * Close side bar on menu location set as disabled (#194) * Remove placeholder side bar tabs (#196) --------- Co-authored-by: bymyself <abolkonsky.rem@gmail.com> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -8,6 +8,11 @@ interface Position {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Size {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
class ComfyNodeSearchBox {
|
class ComfyNodeSearchBox {
|
||||||
public readonly input: Locator;
|
public readonly input: Locator;
|
||||||
public readonly dropdown: Locator;
|
public readonly dropdown: Locator;
|
||||||
@@ -214,6 +219,65 @@ export class ComfyPage {
|
|||||||
await this.page.keyboard.up("Control");
|
await this.page.keyboard.up("Control");
|
||||||
await this.nextFrame();
|
await this.nextFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async closeMenu() {
|
||||||
|
await this.page.click("button.comfy-close-menu-btn");
|
||||||
|
await this.nextFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
async resizeNode(nodePos: Position, nodeSize: Size, ratioX: number, ratioY: number, revertAfter: boolean = false) {
|
||||||
|
const bottomRight = {
|
||||||
|
x: nodePos.x + nodeSize.width,
|
||||||
|
y: nodePos.y + nodeSize.height,
|
||||||
|
}
|
||||||
|
const target = {
|
||||||
|
x: nodePos.x + nodeSize.width * ratioX,
|
||||||
|
y: nodePos.y + nodeSize.height * ratioY,
|
||||||
|
}
|
||||||
|
await this.dragAndDrop(bottomRight, target);
|
||||||
|
await this.nextFrame();
|
||||||
|
if (revertAfter) {
|
||||||
|
await this.dragAndDrop(target, bottomRight);
|
||||||
|
await this.nextFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async resizeKsamplerNode(percentX: number, percentY: number, revertAfter: boolean = false) {
|
||||||
|
const ksamplerPos = {
|
||||||
|
x: 864,
|
||||||
|
y: 157
|
||||||
|
}
|
||||||
|
const ksamplerSize = {
|
||||||
|
width: 315,
|
||||||
|
height: 292,
|
||||||
|
}
|
||||||
|
this.resizeNode(ksamplerPos, ksamplerSize, percentX, percentY, revertAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resizeLoadCheckpointNode(percentX: number, percentY: number, revertAfter: boolean = false) {
|
||||||
|
const loadCheckpointPos = {
|
||||||
|
x: 25,
|
||||||
|
y: 440,
|
||||||
|
}
|
||||||
|
const loadCheckpointSize = {
|
||||||
|
width: 320,
|
||||||
|
height: 120,
|
||||||
|
}
|
||||||
|
this.resizeNode(loadCheckpointPos, loadCheckpointSize, percentX, percentY, revertAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resizeEmptyLatentNode(percentX: number, percentY: number, revertAfter: boolean = false) {
|
||||||
|
const emptyLatentPos = {
|
||||||
|
x: 475,
|
||||||
|
y: 580,
|
||||||
|
}
|
||||||
|
const emptyLatentSize = {
|
||||||
|
width: 303,
|
||||||
|
height: 132,
|
||||||
|
}
|
||||||
|
this.resizeNode(emptyLatentPos, emptyLatentSize, percentX, percentY, revertAfter);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||||
|
|||||||
28
browser_tests/textWidgetTruncate.spec.ts
Normal file
28
browser_tests/textWidgetTruncate.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import { comfyPageFixture as test } from "./ComfyPage";
|
||||||
|
|
||||||
|
test.describe("Combo text widget", () => {
|
||||||
|
test("Truncates text when resized", async ({ comfyPage }) => {
|
||||||
|
await comfyPage.resizeLoadCheckpointNode(0.2, 1);
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
|
"load-checkpoint-resized-min-width.png"
|
||||||
|
);
|
||||||
|
await comfyPage.closeMenu();
|
||||||
|
await comfyPage.resizeKsamplerNode(0.2, 1);
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
|
`ksampler-resized-min-width.png`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Doesn't truncate when space still available", async ({ comfyPage }) => {
|
||||||
|
await comfyPage.resizeEmptyLatentNode(0.8, 0.8);
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
|
"empty-latent-resized-80-percent.png"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can revert to full text", async ({ comfyPage }) => {
|
||||||
|
await comfyPage.resizeLoadCheckpointNode(0.8, 1, true);
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot("resized-to-original.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
@@ -12,6 +12,7 @@
|
|||||||
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
||||||
}
|
}
|
||||||
</style> -->
|
</style> -->
|
||||||
|
<script src="node_modules/reflect-metadata/Reflect.js"></script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
window["__COMFYUI_FRONTEND_VERSION__"] = __COMFYUI_FRONTEND_VERSION__;
|
window["__COMFYUI_FRONTEND_VERSION__"] = __COMFYUI_FRONTEND_VERSION__;
|
||||||
console.log("ComfyUI Front-end version:", __COMFYUI_FRONTEND_VERSION__);
|
console.log("ComfyUI Front-end version:", __COMFYUI_FRONTEND_VERSION__);
|
||||||
|
|||||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -11,11 +11,14 @@
|
|||||||
"@comfyorg/litegraph": "^0.7.26",
|
"@comfyorg/litegraph": "^0.7.26",
|
||||||
"@primevue/themes": "^4.0.0-rc.2",
|
"@primevue/themes": "^4.0.0-rc.2",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.0.0-rc.2",
|
"primevue": "^4.0.0-rc.2",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"vue": "^3.4.31",
|
"vue": "^3.4.31",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-validation-error": "^3.3.0"
|
"zod-validation-error": "^3.3.0"
|
||||||
@@ -3535,6 +3538,11 @@
|
|||||||
"@vue/shared": "3.4.31"
|
"@vue/shared": "3.4.31"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/devtools-api": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw=="
|
||||||
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.4.31",
|
"version": "3.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
|
||||||
@@ -4199,6 +4207,11 @@
|
|||||||
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
|
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/class-transformer": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
|
||||||
|
},
|
||||||
"node_modules/cli-cursor": {
|
"node_modules/cli-cursor": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
||||||
@@ -8133,6 +8146,56 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "2.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",
|
||||||
|
"integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.5.0",
|
||||||
|
"vue-demi": ">=0.14.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.4.0",
|
||||||
|
"typescript": ">=4.4.4",
|
||||||
|
"vue": "^2.6.14 || ^3.3.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
|
||||||
|
"integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pirates": {
|
"node_modules/pirates": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||||
@@ -8510,6 +8573,11 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reflect-metadata": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
|
||||||
|
},
|
||||||
"node_modules/regenerate": {
|
"node_modules/regenerate": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||||
|
|||||||
@@ -49,11 +49,14 @@
|
|||||||
"@comfyorg/litegraph": "^0.7.26",
|
"@comfyorg/litegraph": "^0.7.26",
|
||||||
"@primevue/themes": "^4.0.0-rc.2",
|
"@primevue/themes": "^4.0.0-rc.2",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.0.0-rc.2",
|
"primevue": "^4.0.0-rc.2",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"vue": "^3.4.31",
|
"vue": "^3.4.31",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-validation-error": "^3.3.0"
|
"zod-validation-error": "^3.3.0"
|
||||||
|
|||||||
@@ -2,12 +2,21 @@
|
|||||||
<ProgressSpinner v-if="isLoading" class="spinner"></ProgressSpinner>
|
<ProgressSpinner v-if="isLoading" class="spinner"></ProgressSpinner>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<NodeSearchboxPopover v-if="nodeSearchEnabled" />
|
<NodeSearchboxPopover v-if="nodeSearchEnabled" />
|
||||||
|
<teleport to="#graph-canvas-container">
|
||||||
|
<LiteGraphCanvasSplitterOverlay>
|
||||||
|
<template #side-bar-panel="{ setPanelVisible }">
|
||||||
|
<SideToolBar @change="setPanelVisible($event)" />
|
||||||
|
</template>
|
||||||
|
</LiteGraphCanvasSplitterOverlay>
|
||||||
|
</teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, provide, ref } from "vue";
|
import { onMounted, onUnmounted, provide, ref } from "vue";
|
||||||
import NodeSearchboxPopover from "@/components/NodeSearchBoxPopover.vue";
|
import NodeSearchboxPopover from "@/components/NodeSearchBoxPopover.vue";
|
||||||
|
import SideToolBar from "@/components/sidebar/SideToolBar.vue";
|
||||||
|
import LiteGraphCanvasSplitterOverlay from "@/components/LiteGraphCanvasSplitterOverlay.vue";
|
||||||
import ProgressSpinner from "primevue/progressspinner";
|
import ProgressSpinner from "primevue/progressspinner";
|
||||||
import {
|
import {
|
||||||
NodeSearchService,
|
NodeSearchService,
|
||||||
|
|||||||
@@ -47,9 +47,34 @@ body {
|
|||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
+------------------+------------------+------------------+
|
||||||
|
| |
|
||||||
|
| .comfyui-body- |
|
||||||
|
| top |
|
||||||
|
| (spans all cols) |
|
||||||
|
| |
|
||||||
|
+------------------+------------------+------------------+
|
||||||
|
| | | |
|
||||||
|
| .comfyui-body- | #graph-canvas | .comfyui-body- |
|
||||||
|
| left | | right |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
+------------------+------------------+------------------+
|
||||||
|
| |
|
||||||
|
| .comfyui-body- |
|
||||||
|
| bottom |
|
||||||
|
| (spans all cols) |
|
||||||
|
| |
|
||||||
|
+------------------+------------------+------------------+
|
||||||
|
*/
|
||||||
|
|
||||||
.comfyui-body-top {
|
.comfyui-body-top {
|
||||||
order: -5;
|
order: -5;
|
||||||
|
/* Span across all columns */
|
||||||
grid-column: 1/-1;
|
grid-column: 1/-1;
|
||||||
|
/* Position at the first row */
|
||||||
|
grid-row: 1;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -57,25 +82,41 @@ body {
|
|||||||
|
|
||||||
.comfyui-body-left {
|
.comfyui-body-left {
|
||||||
order: -4;
|
order: -4;
|
||||||
|
/* Position in the first column */
|
||||||
|
grid-column: 1;
|
||||||
|
/* Position below the top element */
|
||||||
|
grid-row: 2;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#graph-canvas-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
order: -3;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#graph-canvas {
|
#graph-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
order: -3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comfyui-body-right {
|
.comfyui-body-right {
|
||||||
order: -2;
|
order: -2;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
grid-column: 3;
|
||||||
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comfyui-body-bottom {
|
.comfyui-body-bottom {
|
||||||
order: -1;
|
order: 4;
|
||||||
|
/* Span across all columns */
|
||||||
grid-column: 1/-1;
|
grid-column: 1/-1;
|
||||||
|
grid-row: 3;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
58
src/components/LiteGraphCanvasSplitterOverlay.vue
Normal file
58
src/components/LiteGraphCanvasSplitterOverlay.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<Splitter class="splitter-overlay" :pt:gutter="gutterClass">
|
||||||
|
<SplitterPanel
|
||||||
|
class="side-bar-panel"
|
||||||
|
:minSize="10"
|
||||||
|
:size="20"
|
||||||
|
v-show="sideBarPanelVisible"
|
||||||
|
>
|
||||||
|
<slot name="side-bar-panel" :setPanelVisible="setPanelVisible"></slot>
|
||||||
|
</SplitterPanel>
|
||||||
|
<SplitterPanel class="graph-canvas-panel" :size="100">
|
||||||
|
<div></div>
|
||||||
|
</SplitterPanel>
|
||||||
|
</Splitter>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Splitter from "primevue/splitter";
|
||||||
|
import SplitterPanel from "primevue/splitterpanel";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
const sideBarPanelVisible = ref(false);
|
||||||
|
const setPanelVisible = (visible: boolean) => {
|
||||||
|
sideBarPanelVisible.value = visible;
|
||||||
|
};
|
||||||
|
const gutterClass = computed(() => {
|
||||||
|
return sideBarPanelVisible.value ? "" : "gutter-hidden";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.p-splitter-gutter {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.side-bar-panel {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitter-overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/components/sidebar/SideBarIcon.vue
Normal file
50
src/components/sidebar/SideBarIcon.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
:icon="props.icon"
|
||||||
|
text
|
||||||
|
:pt="{
|
||||||
|
root: `side-bar-button ${
|
||||||
|
props.selected
|
||||||
|
? 'p-button-primary side-bar-button-selected'
|
||||||
|
: 'p-button-secondary'
|
||||||
|
}`,
|
||||||
|
icon: 'side-bar-button-icon',
|
||||||
|
}"
|
||||||
|
@click="emit('click', $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from "primevue/button";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
icon: String,
|
||||||
|
selected: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["click"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.p-button-icon.side-bar-button-icon {
|
||||||
|
font-size: var(--sidebar-icon-size) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar-button-selected .p-button-icon.side-bar-button-icon {
|
||||||
|
font-size: var(--sidebar-icon-size) !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.side-bar-button {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
height: var(--sidebar-width);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar-button.side-bar-button-selected,
|
||||||
|
.side-bar-button.side-bar-button-selected:hover {
|
||||||
|
border-left: 4px solid var(--p-button-text-primary-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
src/components/sidebar/SideBarSettingsToggleIcon.vue
Normal file
12
src/components/sidebar/SideBarSettingsToggleIcon.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<SideBarIcon icon="pi pi-cog" @click="showSetting" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { app } from "@/scripts/app";
|
||||||
|
import SideBarIcon from "./SideBarIcon.vue";
|
||||||
|
|
||||||
|
const showSetting = () => {
|
||||||
|
app.ui.settings.show();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
20
src/components/sidebar/SideBarThemeToggleIcon.vue
Normal file
20
src/components/sidebar/SideBarThemeToggleIcon.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<SideBarIcon :icon="icon" @click="toggleTheme" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import SideBarIcon from "./SideBarIcon.vue";
|
||||||
|
import { app } from "@/scripts/app";
|
||||||
|
|
||||||
|
const isDarkMode = ref(false);
|
||||||
|
const icon = computed(() => (isDarkMode.value ? "pi pi-sun" : "pi pi-moon"));
|
||||||
|
const themeId = computed(() => (isDarkMode.value ? "dark" : "light"));
|
||||||
|
const toggleTheme = () => {
|
||||||
|
isDarkMode.value = !isDarkMode.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(themeId, (newThemeId) => {
|
||||||
|
app.ui.settings.setSettingValue("Comfy.ColorPalette", newThemeId);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
90
src/components/sidebar/SideToolBar.vue
Normal file
90
src/components/sidebar/SideToolBar.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<teleport to=".comfyui-body-left">
|
||||||
|
<nav class="side-tool-bar-container">
|
||||||
|
<SideBarIcon
|
||||||
|
v-for="item in items"
|
||||||
|
:icon="item.icon"
|
||||||
|
:selected="item === selectedItem"
|
||||||
|
@click="onItemClick(item)"
|
||||||
|
/>
|
||||||
|
<div class="side-tool-bar-end">
|
||||||
|
<SideBarThemeToggleIcon />
|
||||||
|
<SideBarSettingsToggleIcon />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</teleport>
|
||||||
|
<component :is="selectedItem?.component" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SideBarIcon from "./SideBarIcon.vue";
|
||||||
|
import SideBarThemeToggleIcon from "./SideBarThemeToggleIcon.vue";
|
||||||
|
import SideBarSettingsToggleIcon from "./SideBarSettingsToggleIcon.vue";
|
||||||
|
import NodeDetailSideBarItem from "./items/NodeDetailSideBarItem.vue";
|
||||||
|
import QueueSideBarItem from "./items/QueueSideBarItem.vue";
|
||||||
|
import { markRaw, onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
|
|
||||||
|
const items = ref([
|
||||||
|
// { icon: "pi pi-map", component: markRaw(NodeDetailSideBarItem) },
|
||||||
|
{ icon: "pi pi-history", component: markRaw(QueueSideBarItem) },
|
||||||
|
]);
|
||||||
|
const selectedItem = ref(null);
|
||||||
|
const onItemClick = (item) => {
|
||||||
|
if (selectedItem.value === item) {
|
||||||
|
selectedItem.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedItem.value = item;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(["change"]);
|
||||||
|
watch(selectedItem, (newVal) => {
|
||||||
|
emit("change", newVal !== null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onBetaMenuDisabled = () => {
|
||||||
|
selectedItem.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener(
|
||||||
|
"comfy:setting:beta-menu-disabled",
|
||||||
|
onBetaMenuDisabled
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener(
|
||||||
|
"comfy:setting:beta-menu-disabled",
|
||||||
|
onBetaMenuDisabled
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--sidebar-width: 64px;
|
||||||
|
--sidebar-icon-size: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.side-tool-bar-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-color: var(--comfy-menu-bg);
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-tool-bar-end {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
src/components/sidebar/items/NodeDetailSideBarItem.vue
Normal file
20
src/components/sidebar/items/NodeDetailSideBarItem.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionPanel v-for="node in nodes" :key="node.id">
|
||||||
|
<AccordionHeader>
|
||||||
|
<div>{{ node.id }} - {{ node.type }}</div>
|
||||||
|
</AccordionHeader>
|
||||||
|
</AccordionPanel>
|
||||||
|
</Accordion>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defaultGraph } from "@/scripts/defaultGraph";
|
||||||
|
import Accordion from "primevue/accordion";
|
||||||
|
import AccordionHeader from "primevue/accordionheader";
|
||||||
|
import AccordionPanel from "primevue/accordionpanel";
|
||||||
|
|
||||||
|
/* Placeholder workflow display */
|
||||||
|
const workflow = defaultGraph;
|
||||||
|
const nodes = workflow.nodes.sort((a, b) => a.id - b.id);
|
||||||
|
</script>
|
||||||
176
src/components/sidebar/items/QueueSideBarItem.vue
Normal file
176
src/components/sidebar/items/QueueSideBarItem.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<DataTable
|
||||||
|
v-if="tasks.length > 0"
|
||||||
|
:value="tasks"
|
||||||
|
dataKey="promptId"
|
||||||
|
class="queue-table"
|
||||||
|
>
|
||||||
|
<Column header="STATUS">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag :severity="taskTagSeverity(data.displayStatus)">
|
||||||
|
{{ data.displayStatus.toUpperCase() }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="TIME" :pt="{ root: { class: 'queue-time-cell' } }">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div v-if="data.isHistory" class="queue-time-cell-content">
|
||||||
|
{{ formatTime(data.executionTimeInSeconds) }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="data.isRunning" class="queue-time-cell-content">
|
||||||
|
<i class="pi pi-spin pi-spinner"></i>
|
||||||
|
</div>
|
||||||
|
<div v-else class="queue-time-cell-content">...</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column
|
||||||
|
:pt="{
|
||||||
|
headerCell: {
|
||||||
|
class: 'queue-tool-header-cell',
|
||||||
|
},
|
||||||
|
bodyCell: {
|
||||||
|
class: 'queue-tool-body-cell',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<Toast />
|
||||||
|
<ConfirmPopup />
|
||||||
|
<Button
|
||||||
|
icon="pi pi-trash"
|
||||||
|
text
|
||||||
|
severity="primary"
|
||||||
|
@click="confirmRemoveAll($event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-file-export"
|
||||||
|
text
|
||||||
|
severity="primary"
|
||||||
|
@click="data.loadWorkflow()"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-times"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
@click="removeTask(data)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
<div>
|
||||||
|
<Message icon="pi pi-info" severity="error">
|
||||||
|
<span class="ml-2">No tasks</span>
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DataTable from "primevue/datatable";
|
||||||
|
import Column from "primevue/column";
|
||||||
|
import Tag from "primevue/tag";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import ConfirmPopup from "primevue/confirmpopup";
|
||||||
|
import Toast from "primevue/toast";
|
||||||
|
import Message from "primevue/message";
|
||||||
|
import { useConfirm } from "primevue/useconfirm";
|
||||||
|
import { useToast } from "primevue/usetoast";
|
||||||
|
import {
|
||||||
|
TaskItemDisplayStatus,
|
||||||
|
TaskItemImpl,
|
||||||
|
useQueueStore,
|
||||||
|
} from "@/stores/queueStore";
|
||||||
|
import { computed, onMounted } from "vue";
|
||||||
|
import { api } from "@/scripts/api";
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const toast = useToast();
|
||||||
|
const queueStore = useQueueStore();
|
||||||
|
|
||||||
|
const tasks = computed(() => queueStore.tasks);
|
||||||
|
const taskTagSeverity = (status: TaskItemDisplayStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case TaskItemDisplayStatus.Pending:
|
||||||
|
return "secondary";
|
||||||
|
case TaskItemDisplayStatus.Running:
|
||||||
|
return "info";
|
||||||
|
case TaskItemDisplayStatus.Completed:
|
||||||
|
return "success";
|
||||||
|
case TaskItemDisplayStatus.Failed:
|
||||||
|
return "danger";
|
||||||
|
case TaskItemDisplayStatus.Cancelled:
|
||||||
|
return "warning";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const formatTime = (time?: number) => {
|
||||||
|
if (time === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${time.toFixed(2)}s`;
|
||||||
|
};
|
||||||
|
const removeTask = (task: TaskItemImpl) => {
|
||||||
|
if (task.isRunning) {
|
||||||
|
api.interrupt();
|
||||||
|
}
|
||||||
|
queueStore.delete(task);
|
||||||
|
};
|
||||||
|
const removeAllTasks = async () => {
|
||||||
|
await queueStore.clear();
|
||||||
|
};
|
||||||
|
const confirmRemoveAll = (event) => {
|
||||||
|
confirm.require({
|
||||||
|
target: event.currentTarget,
|
||||||
|
message: "Do you want to delete all tasks?",
|
||||||
|
icon: "pi pi-info-circle",
|
||||||
|
rejectProps: {
|
||||||
|
label: "Cancel",
|
||||||
|
severity: "secondary",
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: "Delete",
|
||||||
|
severity: "danger",
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
await removeAllTasks();
|
||||||
|
toast.add({
|
||||||
|
severity: "info",
|
||||||
|
summary: "Confirmed",
|
||||||
|
detail: "Tasks deleted",
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
api.addEventListener("status", () => {
|
||||||
|
queueStore.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
queueStore.update();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.queue-tool-header-cell {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-tool-body-cell {
|
||||||
|
display: table-cell;
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.queue-time-cell-content {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-table {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
src/main.ts
17
src/main.ts
@@ -1,12 +1,24 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import PrimeVue from "primevue/config";
|
import PrimeVue from "primevue/config";
|
||||||
import Aura from "@primevue/themes/aura";
|
import Aura from "@primevue/themes/aura";
|
||||||
|
import { definePreset } from "@primevue/themes";
|
||||||
|
import ConfirmationService from "primevue/confirmationservice";
|
||||||
|
import ToastService from "primevue/toastservice";
|
||||||
import "primeicons/primeicons.css";
|
import "primeicons/primeicons.css";
|
||||||
|
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import { app as comfyApp } from "@/scripts/app";
|
import { app as comfyApp } from "@/scripts/app";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
|
const ComfyUIPreset = definePreset(Aura, {
|
||||||
|
semantic: {
|
||||||
|
// @ts-ignore
|
||||||
|
primary: Aura.primitive.blue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
const pinia = createPinia();
|
||||||
|
|
||||||
comfyApp.setup().then(() => {
|
comfyApp.setup().then(() => {
|
||||||
window["app"] = comfyApp;
|
window["app"] = comfyApp;
|
||||||
@@ -15,7 +27,7 @@ comfyApp.setup().then(() => {
|
|||||||
app
|
app
|
||||||
.use(PrimeVue, {
|
.use(PrimeVue, {
|
||||||
theme: {
|
theme: {
|
||||||
preset: Aura,
|
preset: ComfyUIPreset,
|
||||||
options: {
|
options: {
|
||||||
prefix: "p",
|
prefix: "p",
|
||||||
cssLayer: false,
|
cssLayer: false,
|
||||||
@@ -25,5 +37,8 @@ comfyApp.setup().then(() => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.use(ConfirmationService)
|
||||||
|
.use(ToastService)
|
||||||
|
.use(pinia)
|
||||||
.mount("#vue-app");
|
.mount("#vue-app");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -184,6 +184,11 @@ class ComfyApi extends EventTarget {
|
|||||||
new CustomEvent("execution_start", { detail: msg.data })
|
new CustomEvent("execution_start", { detail: msg.data })
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "execution_success":
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("execution_success", { detail: msg.data })
|
||||||
|
);
|
||||||
|
break;
|
||||||
case "execution_error":
|
case "execution_error":
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("execution_error", { detail: msg.data })
|
new CustomEvent("execution_error", { detail: msg.data })
|
||||||
@@ -315,10 +320,14 @@ class ComfyApi extends EventTarget {
|
|||||||
return {
|
return {
|
||||||
// Running action uses a different endpoint for cancelling
|
// Running action uses a different endpoint for cancelling
|
||||||
Running: data.queue_running.map((prompt) => ({
|
Running: data.queue_running.map((prompt) => ({
|
||||||
|
taskType: "Running",
|
||||||
prompt,
|
prompt,
|
||||||
remove: { name: "Cancel", cb: () => api.interrupt() },
|
remove: { name: "Cancel", cb: () => api.interrupt() },
|
||||||
})),
|
})),
|
||||||
Pending: data.queue_pending.map((prompt) => ({ prompt })),
|
Pending: data.queue_pending.map((prompt) => ({
|
||||||
|
taskType: "Pending",
|
||||||
|
prompt,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -335,7 +344,11 @@ class ComfyApi extends EventTarget {
|
|||||||
): Promise<{ History: HistoryTaskItem[] }> {
|
): Promise<{ History: HistoryTaskItem[] }> {
|
||||||
try {
|
try {
|
||||||
const res = await this.fetchApi(`/history?max_items=${max_items}`);
|
const res = await this.fetchApi(`/history?max_items=${max_items}`);
|
||||||
return { History: Object.values(await res.json()) };
|
return {
|
||||||
|
History: Object.values(await res.json()).map(
|
||||||
|
(item: HistoryTaskItem) => ({ ...item, taskType: "History" })
|
||||||
|
),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return { History: [] };
|
return { History: [] };
|
||||||
|
|||||||
@@ -1845,13 +1845,18 @@ export class ComfyApp {
|
|||||||
await this.#setUser();
|
await this.#setUser();
|
||||||
|
|
||||||
// Create and mount the LiteGraph in the DOM
|
// Create and mount the LiteGraph in the DOM
|
||||||
|
const canvasContainer = document.createElement("div");
|
||||||
|
canvasContainer.id = "graph-canvas-container";
|
||||||
|
|
||||||
const mainCanvas = document.createElement("canvas");
|
const mainCanvas = document.createElement("canvas");
|
||||||
mainCanvas.style.touchAction = "none";
|
mainCanvas.style.touchAction = "none";
|
||||||
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, {
|
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, {
|
||||||
id: "graph-canvas",
|
id: "graph-canvas",
|
||||||
}));
|
}));
|
||||||
canvasEl.tabIndex = 1;
|
canvasEl.tabIndex = 1;
|
||||||
document.body.prepend(canvasEl);
|
canvasContainer.prepend(canvasEl);
|
||||||
|
document.body.prepend(canvasContainer);
|
||||||
|
|
||||||
this.resizeCanvas();
|
this.resizeCanvas();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import { downloadBlob } from "../../utils";
|
|||||||
import { ComfyButton } from "../components/button";
|
import { ComfyButton } from "../components/button";
|
||||||
import { ComfyButtonGroup } from "../components/buttonGroup";
|
import { ComfyButtonGroup } from "../components/buttonGroup";
|
||||||
import { ComfySplitButton } from "../components/splitButton";
|
import { ComfySplitButton } from "../components/splitButton";
|
||||||
import { ComfyViewHistoryButton } from "./viewHistory";
|
|
||||||
import { ComfyQueueButton } from "./queueButton";
|
import { ComfyQueueButton } from "./queueButton";
|
||||||
import { ComfyWorkflowsMenu } from "./workflows";
|
import { ComfyWorkflowsMenu } from "./workflows";
|
||||||
import { ComfyViewQueueButton } from "./viewQueue";
|
|
||||||
import { getInteruptButton } from "./interruptButton";
|
import { getInteruptButton } from "./interruptButton";
|
||||||
import "./menu.css";
|
import "./menu.css";
|
||||||
import type { ComfySettingsDialog } from "../settings";
|
import type { ComfySettingsDialog } from "../settings";
|
||||||
@@ -128,19 +126,10 @@ export class ComfyAppMenu {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.settingsGroup = new ComfyButtonGroup(
|
// Keep the settings group as there are custom scripts attaching extra
|
||||||
new ComfyButton({
|
// elements to it.
|
||||||
icon: "cog",
|
this.settingsGroup = new ComfyButtonGroup();
|
||||||
content: "Settings",
|
|
||||||
tooltip: "Open settings",
|
|
||||||
action: () => {
|
|
||||||
app.ui.settings.show();
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.viewGroup = new ComfyButtonGroup(
|
this.viewGroup = new ComfyButtonGroup(
|
||||||
new ComfyViewHistoryButton(app).element,
|
|
||||||
new ComfyViewQueueButton(app).element,
|
|
||||||
getInteruptButton("nlg-hide").element
|
getInteruptButton("nlg-hide").element
|
||||||
);
|
);
|
||||||
this.mobileMenuButton = new ComfyButton({
|
this.mobileMenuButton = new ComfyButton({
|
||||||
@@ -193,6 +182,7 @@ export class ComfyAppMenu {
|
|||||||
app.ui.menuContainer.style.removeProperty("display");
|
app.ui.menuContainer.style.removeProperty("display");
|
||||||
this.element.style.display = "none";
|
this.element.style.display = "none";
|
||||||
app.ui.restoreMenuPosition();
|
app.ui.restoreMenuPosition();
|
||||||
|
document.dispatchEvent(new Event("comfy:setting:beta-menu-disabled"));
|
||||||
}
|
}
|
||||||
window.dispatchEvent(new Event("resize"));
|
window.dispatchEvent(new Event("resize"));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { ComfyApp } from "@/scripts/app";
|
|
||||||
import { ComfyButton } from "../components/button";
|
|
||||||
import { ComfyViewList, ComfyViewListButton } from "./viewList";
|
|
||||||
|
|
||||||
export class ComfyViewHistoryButton extends ComfyViewListButton {
|
|
||||||
constructor(app: ComfyApp) {
|
|
||||||
super(app, {
|
|
||||||
button: new ComfyButton({
|
|
||||||
content: "View History",
|
|
||||||
icon: "history",
|
|
||||||
tooltip: "View history",
|
|
||||||
classList: "comfyui-button comfyui-history-button",
|
|
||||||
}),
|
|
||||||
list: ComfyViewHistoryList,
|
|
||||||
mode: "History",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyViewHistoryList extends ComfyViewList {
|
|
||||||
async loadItems() {
|
|
||||||
const items = await super.loadItems();
|
|
||||||
items["History"].reverse();
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
import { ComfyButton } from "../components/button";
|
|
||||||
import { $el } from "../../ui";
|
|
||||||
import { api } from "../../api";
|
|
||||||
import { ComfyPopup } from "../components/popup";
|
|
||||||
import type { ComfyApp } from "@/scripts/app";
|
|
||||||
|
|
||||||
type ViewListMode = "Queue" | "History";
|
|
||||||
|
|
||||||
export class ComfyViewListButton {
|
|
||||||
popup: ComfyPopup;
|
|
||||||
app: ComfyApp;
|
|
||||||
button: ComfyButton;
|
|
||||||
element: HTMLDivElement;
|
|
||||||
list: ComfyViewList;
|
|
||||||
|
|
||||||
get open() {
|
|
||||||
return this.popup.open;
|
|
||||||
}
|
|
||||||
|
|
||||||
set open(open) {
|
|
||||||
this.popup.open = open;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
app: ComfyApp,
|
|
||||||
{
|
|
||||||
button,
|
|
||||||
list,
|
|
||||||
mode,
|
|
||||||
}: { button: ComfyButton; list: typeof ComfyViewList; mode: ViewListMode }
|
|
||||||
) {
|
|
||||||
this.app = app;
|
|
||||||
this.button = button;
|
|
||||||
this.element = $el(
|
|
||||||
"div.comfyui-button-wrapper",
|
|
||||||
this.button.element
|
|
||||||
) as HTMLDivElement;
|
|
||||||
this.popup = new ComfyPopup({
|
|
||||||
target: this.element,
|
|
||||||
container: this.element,
|
|
||||||
horizontal: "right",
|
|
||||||
});
|
|
||||||
this.list = new (list ?? ComfyViewList)(app, mode, this.popup);
|
|
||||||
this.popup.children = [this.list.element];
|
|
||||||
this.popup.addEventListener("open", () => {
|
|
||||||
this.list.update();
|
|
||||||
});
|
|
||||||
this.popup.addEventListener("close", () => {
|
|
||||||
this.list.close();
|
|
||||||
});
|
|
||||||
this.button.withPopup(this.popup);
|
|
||||||
|
|
||||||
api.addEventListener("status", () => {
|
|
||||||
if (this.popup.open) {
|
|
||||||
this.popup.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyViewList {
|
|
||||||
app: ComfyApp;
|
|
||||||
mode: ViewListMode;
|
|
||||||
popup: ComfyPopup;
|
|
||||||
type: string;
|
|
||||||
items: HTMLElement;
|
|
||||||
clear: ComfyButton;
|
|
||||||
refresh: ComfyButton;
|
|
||||||
element: HTMLElement;
|
|
||||||
constructor(app: ComfyApp, mode: ViewListMode, popup: ComfyPopup) {
|
|
||||||
this.app = app;
|
|
||||||
this.mode = mode;
|
|
||||||
this.popup = popup;
|
|
||||||
this.type = mode.toLowerCase();
|
|
||||||
|
|
||||||
this.items = $el(`div.comfyui-${this.type}-items.comfyui-view-list-items`);
|
|
||||||
this.clear = new ComfyButton({
|
|
||||||
icon: "cancel",
|
|
||||||
content: "Clear",
|
|
||||||
action: async () => {
|
|
||||||
this.showSpinner(false);
|
|
||||||
await api.clearItems(this.type);
|
|
||||||
await this.update();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.refresh = new ComfyButton({
|
|
||||||
icon: "refresh",
|
|
||||||
content: "Refresh",
|
|
||||||
action: async () => {
|
|
||||||
await this.update(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.element = $el(
|
|
||||||
`div.comfyui-${this.type}-popup.comfyui-view-list-popup`,
|
|
||||||
[
|
|
||||||
$el("h3", mode),
|
|
||||||
$el("header", [this.clear.element, this.refresh.element]),
|
|
||||||
this.items,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
api.addEventListener("status", () => {
|
|
||||||
if (this.popup.open) {
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
this.items.replaceChildren();
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(resize = true) {
|
|
||||||
this.showSpinner(resize);
|
|
||||||
const res = await this.loadItems();
|
|
||||||
let any = false;
|
|
||||||
|
|
||||||
const names = Object.keys(res);
|
|
||||||
const sections = names
|
|
||||||
.map((section) => {
|
|
||||||
const items = res[section];
|
|
||||||
if (items?.length) {
|
|
||||||
any = true;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = [];
|
|
||||||
if (names.length > 1) {
|
|
||||||
rows.push($el("h5", section));
|
|
||||||
}
|
|
||||||
rows.push(...items.flatMap((item) => this.createRow(item, section)));
|
|
||||||
return $el("section", rows);
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (any) {
|
|
||||||
this.items.replaceChildren(...sections);
|
|
||||||
} else {
|
|
||||||
this.items.replaceChildren($el("h5", "None"));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.popup.update();
|
|
||||||
this.clear.enabled = this.refresh.enabled = true;
|
|
||||||
this.element.style.removeProperty("height");
|
|
||||||
}
|
|
||||||
|
|
||||||
showSpinner(resize = true) {
|
|
||||||
// if (!this.spinner) {
|
|
||||||
// this.spinner = createSpinner();
|
|
||||||
// }
|
|
||||||
// if (!resize) {
|
|
||||||
// this.element.style.height = this.element.clientHeight + "px";
|
|
||||||
// }
|
|
||||||
// this.clear.enabled = this.refresh.enabled = false;
|
|
||||||
// this.items.replaceChildren(
|
|
||||||
// $el(
|
|
||||||
// "div",
|
|
||||||
// {
|
|
||||||
// style: {
|
|
||||||
// fontSize: "18px",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// this.spinner
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
// this.popup.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadItems() {
|
|
||||||
return await api.getItems(this.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRow(item, section) {
|
|
||||||
return {
|
|
||||||
text: item.prompt[0] + "",
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
text: "Load",
|
|
||||||
action: async () => {
|
|
||||||
try {
|
|
||||||
await this.app.loadGraphData(
|
|
||||||
item.prompt[3].extra_pnginfo.workflow
|
|
||||||
);
|
|
||||||
if (item.outputs) {
|
|
||||||
this.app.nodeOutputs = item.outputs;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
alert("Error loading workflow: " + error.message);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Delete",
|
|
||||||
action: async () => {
|
|
||||||
try {
|
|
||||||
await api.deleteItem(this.type, item.prompt[1]);
|
|
||||||
this.update();
|
|
||||||
} catch (error) {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
createRow = (item, section) => {
|
|
||||||
const row = this.getRow(item, section);
|
|
||||||
return [
|
|
||||||
$el("span", row.text),
|
|
||||||
...row.actions.map(
|
|
||||||
(a) =>
|
|
||||||
new ComfyButton({
|
|
||||||
content: a.text,
|
|
||||||
action: async (e, btn) => {
|
|
||||||
btn.enabled = false;
|
|
||||||
try {
|
|
||||||
await a.action();
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
btn.enabled = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}).element
|
|
||||||
),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { ComfyButton } from "../components/button";
|
|
||||||
import { ComfyViewList, ComfyViewListButton } from "./viewList";
|
|
||||||
import { api } from "../../api";
|
|
||||||
import type { ComfyApp } from "@/scripts/app";
|
|
||||||
|
|
||||||
export class ComfyViewQueueButton extends ComfyViewListButton {
|
|
||||||
constructor(app: ComfyApp) {
|
|
||||||
super(app, {
|
|
||||||
button: new ComfyButton({
|
|
||||||
content: "View Queue",
|
|
||||||
icon: "format-list-numbered",
|
|
||||||
tooltip: "View queue",
|
|
||||||
classList: "comfyui-button comfyui-queue-button",
|
|
||||||
}),
|
|
||||||
list: ComfyViewQueueList,
|
|
||||||
mode: "Queue",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyViewQueueList extends ComfyViewList {
|
|
||||||
getRow = (item, section) => {
|
|
||||||
if (section !== "Running") {
|
|
||||||
return super.getRow(item, section);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
text: item.prompt[0] + "",
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
text: "Load",
|
|
||||||
action: async () => {
|
|
||||||
try {
|
|
||||||
await this.app.loadGraphData(
|
|
||||||
item.prompt[3].extra_pnginfo.workflow
|
|
||||||
);
|
|
||||||
if (item.outputs) {
|
|
||||||
this.app.nodeOutputs = item.outputs;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
alert("Error loading workflow: " + error.message);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Cancel",
|
|
||||||
action: async () => {
|
|
||||||
try {
|
|
||||||
await api.interrupt();
|
|
||||||
} catch (error) {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
206
src/stores/queueStore.ts
Normal file
206
src/stores/queueStore.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { api } from "@/scripts/api";
|
||||||
|
import { app } from "@/scripts/app";
|
||||||
|
import {
|
||||||
|
validateTaskItem,
|
||||||
|
TaskItem,
|
||||||
|
TaskType,
|
||||||
|
TaskPrompt,
|
||||||
|
TaskStatus,
|
||||||
|
TaskOutput,
|
||||||
|
} from "@/types/apiTypes";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { toRaw } from "vue";
|
||||||
|
|
||||||
|
// Task type used in the API.
|
||||||
|
export type APITaskType = "queue" | "history";
|
||||||
|
|
||||||
|
export enum TaskItemDisplayStatus {
|
||||||
|
Running = "Running",
|
||||||
|
Pending = "Pending",
|
||||||
|
Completed = "Completed",
|
||||||
|
Failed = "Failed",
|
||||||
|
Cancelled = "Cancelled",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TaskItemImpl {
|
||||||
|
taskType: TaskType;
|
||||||
|
prompt: TaskPrompt;
|
||||||
|
status?: TaskStatus;
|
||||||
|
outputs?: TaskOutput;
|
||||||
|
|
||||||
|
get apiTaskType(): APITaskType {
|
||||||
|
switch (this.taskType) {
|
||||||
|
case "Running":
|
||||||
|
case "Pending":
|
||||||
|
return "queue";
|
||||||
|
case "History":
|
||||||
|
return "history";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get queueIndex() {
|
||||||
|
return this.prompt[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
get promptId() {
|
||||||
|
return this.prompt[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get promptInputs() {
|
||||||
|
return this.prompt[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
get extraData() {
|
||||||
|
return this.prompt[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
get outputsToExecute() {
|
||||||
|
return this.prompt[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
get extraPngInfo() {
|
||||||
|
return this.extraData.extra_pnginfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
get clientId() {
|
||||||
|
return this.extraData.client_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get workflow() {
|
||||||
|
return this.extraPngInfo.workflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
get messages() {
|
||||||
|
return this.status?.messages || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get interrupted() {
|
||||||
|
return _.some(
|
||||||
|
this.messages,
|
||||||
|
(message) => message[0] === "execution_interrupted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isHistory() {
|
||||||
|
return this.taskType === "History";
|
||||||
|
}
|
||||||
|
|
||||||
|
get isRunning() {
|
||||||
|
return this.taskType === "Running";
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayStatus(): TaskItemDisplayStatus {
|
||||||
|
switch (this.taskType) {
|
||||||
|
case "Running":
|
||||||
|
return TaskItemDisplayStatus.Running;
|
||||||
|
case "Pending":
|
||||||
|
return TaskItemDisplayStatus.Pending;
|
||||||
|
case "History":
|
||||||
|
switch (this.status!.status_str) {
|
||||||
|
case "success":
|
||||||
|
return TaskItemDisplayStatus.Completed;
|
||||||
|
case "error":
|
||||||
|
return this.interrupted
|
||||||
|
? TaskItemDisplayStatus.Cancelled
|
||||||
|
: TaskItemDisplayStatus.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get executionStartTimestamp() {
|
||||||
|
const message = this.messages.find(
|
||||||
|
(message) => message[0] === "execution_start"
|
||||||
|
);
|
||||||
|
return message ? message[1].timestamp : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get executionEndTimestamp() {
|
||||||
|
const messages = this.messages.filter((message) =>
|
||||||
|
[
|
||||||
|
"execution_success",
|
||||||
|
"execution_interrupted",
|
||||||
|
"execution_error",
|
||||||
|
].includes(message[0])
|
||||||
|
);
|
||||||
|
if (!messages.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return _.max(messages.map((message) => message[1].timestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
get executionTime() {
|
||||||
|
if (!this.executionStartTimestamp || !this.executionEndTimestamp) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.executionEndTimestamp - this.executionStartTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
get executionTimeInSeconds() {
|
||||||
|
return this.executionTime !== undefined
|
||||||
|
? this.executionTime / 1000
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadWorkflow() {
|
||||||
|
await app.loadGraphData(toRaw(this.workflow));
|
||||||
|
if (this.outputs) {
|
||||||
|
app.nodeOutputs = toRaw(this.outputs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
runningTasks: TaskItemImpl[];
|
||||||
|
pendingTasks: TaskItemImpl[];
|
||||||
|
historyTasks: TaskItemImpl[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useQueueStore = defineStore("queue", {
|
||||||
|
state: (): State => ({
|
||||||
|
runningTasks: [],
|
||||||
|
pendingTasks: [],
|
||||||
|
historyTasks: [],
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
tasks(state) {
|
||||||
|
return [
|
||||||
|
...state.pendingTasks,
|
||||||
|
...state.runningTasks,
|
||||||
|
...state.historyTasks,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Fetch the queue data from the API
|
||||||
|
async update() {
|
||||||
|
const [queue, history] = await Promise.all([
|
||||||
|
api.getQueue(),
|
||||||
|
api.getHistory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const toClassAll = (tasks: TaskItem[]): TaskItemImpl[] =>
|
||||||
|
tasks
|
||||||
|
.map((task) => validateTaskItem(task))
|
||||||
|
.filter((result) => result.success)
|
||||||
|
.map((result) => plainToClass(TaskItemImpl, result.data))
|
||||||
|
// Desc order to show the latest tasks first
|
||||||
|
.sort((a, b) => b.queueIndex - a.queueIndex);
|
||||||
|
|
||||||
|
this.runningTasks = toClassAll(queue.Running);
|
||||||
|
this.pendingTasks = toClassAll(queue.Pending);
|
||||||
|
this.historyTasks = toClassAll(history.History);
|
||||||
|
},
|
||||||
|
async clear() {
|
||||||
|
await Promise.all(
|
||||||
|
["queue", "history"].map((type) => api.clearItems(type))
|
||||||
|
);
|
||||||
|
await this.update();
|
||||||
|
},
|
||||||
|
async delete(task: TaskItemImpl) {
|
||||||
|
await api.deleteItem(task.apiTaskType, task.promptId);
|
||||||
|
await this.update();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,17 +2,17 @@ import { ZodType, z } from "zod";
|
|||||||
import { zComfyWorkflow } from "./comfyWorkflow";
|
import { zComfyWorkflow } from "./comfyWorkflow";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
const zNodeId = z.number();
|
const zNodeId = z.union([z.number(), z.string()]);
|
||||||
const zNodeType = z.string();
|
const zNodeType = z.string();
|
||||||
const zQueueIndex = z.number();
|
const zQueueIndex = z.number();
|
||||||
const zPromptId = z.string();
|
const zPromptId = z.string();
|
||||||
|
|
||||||
const zPromptItem = z.object({
|
const zPromptInputItem = z.object({
|
||||||
inputs: z.record(z.string(), z.any()),
|
inputs: z.record(z.string(), z.any()),
|
||||||
class_type: zNodeType,
|
class_type: zNodeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zPrompt = z.array(zPromptItem);
|
const zPromptInputs = z.record(zPromptInputItem);
|
||||||
|
|
||||||
const zExtraPngInfo = z
|
const zExtraPngInfo = z
|
||||||
.object({
|
.object({
|
||||||
@@ -26,26 +26,32 @@ const zExtraData = z.object({
|
|||||||
});
|
});
|
||||||
const zOutputsToExecute = z.array(zNodeId);
|
const zOutputsToExecute = z.array(zNodeId);
|
||||||
|
|
||||||
|
const zMessageDetailBase = z.object({
|
||||||
|
prompt_id: zPromptId,
|
||||||
|
timestamp: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
const zExecutionStartMessage = z.tuple([
|
const zExecutionStartMessage = z.tuple([
|
||||||
z.literal("execution_start"),
|
z.literal("execution_start"),
|
||||||
z.object({
|
zMessageDetailBase,
|
||||||
prompt_id: zPromptId,
|
]);
|
||||||
}),
|
|
||||||
|
const zExecutionSuccessMessage = z.tuple([
|
||||||
|
z.literal("execution_success"),
|
||||||
|
zMessageDetailBase,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zExecutionCachedMessage = z.tuple([
|
const zExecutionCachedMessage = z.tuple([
|
||||||
z.literal("execution_cached"),
|
z.literal("execution_cached"),
|
||||||
z.object({
|
zMessageDetailBase.extend({
|
||||||
prompt_id: zPromptId,
|
|
||||||
nodes: z.array(zNodeId),
|
nodes: z.array(zNodeId),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zExecutionInterruptedMessage = z.tuple([
|
const zExecutionInterruptedMessage = z.tuple([
|
||||||
z.literal("execution_interrupted"),
|
z.literal("execution_interrupted"),
|
||||||
z.object({
|
zMessageDetailBase.extend({
|
||||||
// InterruptProcessingException
|
// InterruptProcessingException
|
||||||
prompt_id: zPromptId,
|
|
||||||
node_id: zNodeId,
|
node_id: zNodeId,
|
||||||
node_type: zNodeType,
|
node_type: zNodeType,
|
||||||
executed: z.array(zNodeId),
|
executed: z.array(zNodeId),
|
||||||
@@ -54,8 +60,7 @@ const zExecutionInterruptedMessage = z.tuple([
|
|||||||
|
|
||||||
const zExecutionErrorMessage = z.tuple([
|
const zExecutionErrorMessage = z.tuple([
|
||||||
z.literal("execution_error"),
|
z.literal("execution_error"),
|
||||||
z.object({
|
zMessageDetailBase.extend({
|
||||||
prompt_id: zPromptId,
|
|
||||||
node_id: zNodeId,
|
node_id: zNodeId,
|
||||||
node_type: zNodeType,
|
node_type: zNodeType,
|
||||||
executed: z.array(zNodeId),
|
executed: z.array(zNodeId),
|
||||||
@@ -70,6 +75,7 @@ const zExecutionErrorMessage = z.tuple([
|
|||||||
|
|
||||||
const zStatusMessage = z.union([
|
const zStatusMessage = z.union([
|
||||||
zExecutionStartMessage,
|
zExecutionStartMessage,
|
||||||
|
zExecutionSuccessMessage,
|
||||||
zExecutionCachedMessage,
|
zExecutionCachedMessage,
|
||||||
zExecutionInterruptedMessage,
|
zExecutionInterruptedMessage,
|
||||||
zExecutionErrorMessage,
|
zExecutionErrorMessage,
|
||||||
@@ -87,13 +93,15 @@ const zOutput = z.any();
|
|||||||
const zTaskPrompt = z.tuple([
|
const zTaskPrompt = z.tuple([
|
||||||
zQueueIndex,
|
zQueueIndex,
|
||||||
zPromptId,
|
zPromptId,
|
||||||
zPrompt,
|
zPromptInputs,
|
||||||
zExtraData,
|
zExtraData,
|
||||||
zOutputsToExecute,
|
zOutputsToExecute,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zRunningTaskItem = z.object({
|
const zRunningTaskItem = z.object({
|
||||||
|
taskType: z.literal("Running"),
|
||||||
prompt: zTaskPrompt,
|
prompt: zTaskPrompt,
|
||||||
|
// @Deprecated
|
||||||
remove: z.object({
|
remove: z.object({
|
||||||
name: z.literal("Cancel"),
|
name: z.literal("Cancel"),
|
||||||
cb: z.function(),
|
cb: z.function(),
|
||||||
@@ -101,13 +109,17 @@ const zRunningTaskItem = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const zPendingTaskItem = z.object({
|
const zPendingTaskItem = z.object({
|
||||||
|
taskType: z.literal("Pending"),
|
||||||
prompt: zTaskPrompt,
|
prompt: zTaskPrompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const zTaskOutput = z.record(zNodeId, zOutput);
|
||||||
|
|
||||||
const zHistoryTaskItem = z.object({
|
const zHistoryTaskItem = z.object({
|
||||||
|
taskType: z.literal("History"),
|
||||||
prompt: zTaskPrompt,
|
prompt: zTaskPrompt,
|
||||||
status: zStatus.optional(),
|
status: zStatus.optional(),
|
||||||
outputs: z.record(zNodeId, zOutput),
|
outputs: zTaskOutput,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zTaskItem = z.union([
|
const zTaskItem = z.union([
|
||||||
@@ -116,6 +128,17 @@ const zTaskItem = z.union([
|
|||||||
zHistoryTaskItem,
|
zHistoryTaskItem,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const zTaskType = z.union([
|
||||||
|
z.literal("Running"),
|
||||||
|
z.literal("Pending"),
|
||||||
|
z.literal("History"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type TaskType = z.infer<typeof zTaskType>;
|
||||||
|
export type TaskPrompt = z.infer<typeof zTaskPrompt>;
|
||||||
|
export type TaskStatus = z.infer<typeof zStatus>;
|
||||||
|
export type TaskOutput = z.infer<typeof zTaskOutput>;
|
||||||
|
|
||||||
// `/queue`
|
// `/queue`
|
||||||
export type RunningTaskItem = z.infer<typeof zRunningTaskItem>;
|
export type RunningTaskItem = z.infer<typeof zRunningTaskItem>;
|
||||||
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>;
|
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>;
|
||||||
@@ -123,7 +146,17 @@ export type PendingTaskItem = z.infer<typeof zPendingTaskItem>;
|
|||||||
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>;
|
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>;
|
||||||
export type TaskItem = z.infer<typeof zTaskItem>;
|
export type TaskItem = z.infer<typeof zTaskItem>;
|
||||||
|
|
||||||
// TODO: validate `/history` `/queue` API endpoint responses.
|
export function validateTaskItem(taskItem: unknown) {
|
||||||
|
const result = zTaskItem.safeParse(taskItem);
|
||||||
|
if (!result.success) {
|
||||||
|
const zodError = fromZodError(result.error);
|
||||||
|
// TODO accept a callback to report error.
|
||||||
|
console.warn(
|
||||||
|
`Invalid TaskItem: ${JSON.stringify(taskItem)}\n${zodError.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function inputSpec(
|
function inputSpec(
|
||||||
spec: [ZodType, ZodType],
|
spec: [ZodType, ZodType],
|
||||||
|
|||||||
Reference in New Issue
Block a user