Merge pull request #20 from space-nuko/properties-pane2

Properties pane
This commit is contained in:
space-nuko
2023-05-06 01:11:33 -04:00
committed by GitHub
39 changed files with 7720 additions and 3471 deletions

View File

@@ -6,6 +6,14 @@ This project is *still under construction* and many features are missing, be awa
![Screenshot](./static/screenshot.png) ![Screenshot](./static/screenshot.png)
![Screenshot](./static/screenshot2.png)
## Installation
1. Download the latest release [here](https://nightly.link/space-nuko/ComfyBox/workflows/build-and-publish/master/ComfyBox-dist.zip) 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.
## NOTE ## 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 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.
@@ -13,16 +21,19 @@ This frontend isn't compatible with regular ComfyUI's workflow format since extr
## Proposed Features ## Proposed Features
- All the power of ComfyUI with more convenience on top - All the power of ComfyUI with more convenience on top
- Autocreation of UI widgets from your workflow, quickly creating a personalized dashboard - Autocreation of UI widgets from your workflow, quickly creating a personalized dashboard
- Custom widget and node types - Arrange the UI however you like and attach custom classes/styles to each widget
- Look up queued and finished generations and their configs in realtime - Custom widget types
- See the status of queued and finished generations and their configs in realtime
- Development with TypeScript - Development with TypeScript
## Requirements ## Development
### Requirements
- `pnpm` - `pnpm`
- An installation of vanilla [ComfyUI](https://github.com/comfyanonymous/ComfyUI) for the backend - An installation of vanilla [ComfyUI](https://github.com/comfyanonymous/ComfyUI) for the backend
## Installation ### Installation
1. Clone the repo with submodules: 1. Clone the repo with submodules:
@@ -33,5 +44,5 @@ git clone https://github.com/space-nuko/ComfyBox --recursive
2. `pnpm install` 2. `pnpm install`
4. `pnpm build:css` 4. `pnpm build:css`
5. `pnpm dev` 5. `pnpm dev`
6. Start ComfyUI as usual with `python main.py --enable-cors-header` 6. Start ComfyUI with `python main.py --enable-cors-header`
7. Visit `http://localhost:3000` in your browser 7. Visit `http://localhost:3000` in your browser

11
bin/run.bat Normal file
View File

@@ -0,0 +1,11 @@
@echo off
echo Starting ComfyBox.
echo Be sure you've started ComfyUI already using this command:
echo[
echo python main.py --enable-cors-header
echo[
echo Serving at http://localhost:8000
echo[
python -m http.server 8000

11
bin/run.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env sh
echo "Starting ComfyBox."
echo "Be sure you've started ComfyUI already using this command:"
echo ""
echo " python main.py --enable-cors-header"
echo ""
echo "Serving at http://localhost:8000"
echo ""
python -m http.server 8000

0
dist/.keep vendored
View File

View File

@@ -27,6 +27,7 @@
"svelte-dnd-action": "^0.9.22", "svelte-dnd-action": "^0.9.22",
"typescript": "^5.0.3", "typescript": "^5.0.3",
"vite": "^4.3.1", "vite": "^4.3.1",
"vite-plugin-static-copy": "^0.14.0",
"vite-tsconfig-paths": "^4.0.8", "vite-tsconfig-paths": "^4.0.8",
"vitest": "^0.25.8" "vitest": "^0.25.8"
}, },
@@ -45,6 +46,7 @@
"@litegraph-ts/nodes-basic": "workspace:*", "@litegraph-ts/nodes-basic": "workspace:*",
"@litegraph-ts/nodes-events": "workspace:*", "@litegraph-ts/nodes-events": "workspace:*",
"@litegraph-ts/nodes-math": "workspace:*", "@litegraph-ts/nodes-math": "workspace:*",
"@litegraph-ts/nodes-strings": "workspace:*",
"@litegraph-ts/tsconfig": "workspace:*", "@litegraph-ts/tsconfig": "workspace:*",
"@sveltejs/vite-plugin-svelte": "^2.1.1", "@sveltejs/vite-plugin-svelte": "^2.1.1",
"@tsconfig/svelte": "^4.0.1", "@tsconfig/svelte": "^4.0.1",

35
pnpm-lock.yaml generated
View File

@@ -43,6 +43,9 @@ importers:
'@litegraph-ts/nodes-math': '@litegraph-ts/nodes-math':
specifier: workspace:* specifier: workspace:*
version: link:litegraph/packages/nodes-math version: link:litegraph/packages/nodes-math
'@litegraph-ts/nodes-strings':
specifier: workspace:*
version: link:litegraph/packages/nodes-strings
'@litegraph-ts/tsconfig': '@litegraph-ts/tsconfig':
specifier: workspace:* specifier: workspace:*
version: link:litegraph/packages/tsconfig version: link:litegraph/packages/tsconfig
@@ -122,6 +125,9 @@ importers:
vite: vite:
specifier: ^4.3.1 specifier: ^4.3.1
version: 4.3.1(sass@1.61.0) version: 4.3.1(sass@1.61.0)
vite-plugin-static-copy:
specifier: ^0.14.0
version: 0.14.0(vite@4.3.1)
vite-tsconfig-paths: vite-tsconfig-paths:
specifier: ^4.0.8 specifier: ^4.0.8
version: 4.0.8(typescript@5.0.3)(vite@4.3.1) version: 4.0.8(typescript@5.0.3)(vite@4.3.1)
@@ -805,6 +811,22 @@ importers:
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.3.1 version: 4.3.1
litegraph/packages/nodes-strings:
dependencies:
'@litegraph-ts/core':
specifier: workspace:*
version: link:../core
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: {} litegraph/packages/tsconfig: {}
packages: packages:
@@ -7065,6 +7087,19 @@ packages:
vite: 4.3.1(sass@1.61.0) vite: 4.3.1(sass@1.61.0)
dev: false dev: false
/vite-plugin-static-copy@0.14.0(vite@4.3.1):
resolution: {integrity: sha512-RMFmb4czomcrsbQBiUZs9HcDGN3kxGvF+OrtkfTVocp12CuoUCuJQhcY26RK35A6KS4WasGzEwcYZqHMjkAvVw==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: ^3.0.0 || ^4.0.0
dependencies:
chokidar: 3.5.3
fast-glob: 3.2.12
fs-extra: 11.1.1
picocolors: 1.0.0
vite: 4.3.1(sass@1.61.0)
dev: true
/vite-tsconfig-paths@4.0.8(typescript@5.0.3)(vite@4.3.1): /vite-tsconfig-paths@4.0.8(typescript@5.0.3)(vite@4.3.1):
resolution: {integrity: sha512-p04zH+Ey+NT78571x0pdX7nVRIJSlmKVvYryFglSWOK3Hc72eDL0+JJfbyQiugaIBApJkaEqbBQvqpsFZOSVGg==} resolution: {integrity: sha512-p04zH+Ey+NT78571x0pdX7nVRIJSlmKVvYryFglSWOK3Hc72eDL0+JJfbyQiugaIBApJkaEqbBQvqpsFZOSVGg==}
peerDependencies: peerDependencies:

View File

@@ -1,4 +0,0 @@
@echo off
pushd dist
python -m http.server 8000

4
run.sh
View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
pushd dist/
python -m http.server 8000

View File

@@ -1,7 +1,9 @@
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4 } from "@litegraph-ts/core"; import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode } from "@litegraph-ts/core";
import type ComfyApp from "./components/ComfyApp"; import type ComfyApp from "./components/ComfyApp";
import queueState from "./stores/queueState"; import queueState from "./stores/queueState";
import { get } from "svelte/store"; import { get } from "svelte/store";
import uiState from "./stores/uiState";
import layoutState from "./stores/layoutState";
export type SerializedGraphCanvasState = { export type SerializedGraphCanvasState = {
offset: Vector2, offset: Vector2,
@@ -101,7 +103,39 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
} }
private alignToGrid(node: LGraphNode, ctx: CanvasRenderingContext2D) {
const x = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[0] / LiteGraph.CANVAS_GRID_SIZE);
const y = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[1] / LiteGraph.CANVAS_GRID_SIZE);
const shiftX = x - node.pos[0];
let shiftY = y - node.pos[1];
let w, h;
if (node.flags.collapsed) {
w = node._collapsed_width;
h = LiteGraph.NODE_TITLE_HEIGHT;
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
} else {
w = node.size[0];
h = node.size[1];
let titleMode = node.titleMode
if (titleMode !== TitleMode.TRANSPARENT_TITLE && titleMode !== TitleMode.NO_TITLE) {
h += LiteGraph.NODE_TITLE_HEIGHT;
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
}
}
const f = ctx.fillStyle;
ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
ctx.fillRect(shiftX, shiftY, w, h);
ctx.fillStyle = f;
}
override drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void { override drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void {
if ((window as any)?.app?.shiftDown && this.node_dragged && node.id in this.selected_nodes) {
this.alignToGrid(node, ctx)
}
// Fade out inactive nodes
var editor_alpha = this.editor_alpha; var editor_alpha = this.editor_alpha;
if (node.mode === NodeMode.NEVER) { // never if (node.mode === NodeMode.NEVER) { // never
this.editor_alpha = 0.4; this.editor_alpha = 0.4;
@@ -196,4 +230,23 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
return res; return res;
} }
override onSelectionChange(nodes: Record<number, LGraphNode>) {
const ls = get(layoutState)
ls.currentSelectionNodes = Object.values(nodes)
ls.currentSelection = []
layoutState.set(ls)
}
override onNodeMoved(node: LGraphNode) {
if (super.onNodeMoved)
super.onNodeMoved(node);
if ((window as any)?.app?.shiftDown) {
// Ensure all selected nodes are realigned
for (const id in this.selected_nodes) {
this.selected_nodes[id].alignToGrid();
}
}
}
} }

View File

@@ -16,14 +16,17 @@
export let zIndex: number = 0; export let zIndex: number = 0;
export let classes: string[] = []; export let classes: string[] = [];
export let showHandles: boolean = false; export let showHandles: boolean = false;
let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] | null = null;
const flipDurationMs = 100; const flipDurationMs = 100;
$: if (container) { $: if (container) {
children = $layoutState.allItems[container.id].children; children = $layoutState.allItems[container.id].children;
attrsChanged = container.attrsChanged
} }
else { else {
children = null; children = null;
attrsChanged = null
} }
function handleConsider(evt: any) { function handleConsider(evt: any) {
@@ -38,26 +41,23 @@
</script> </script>
{#if container && children} {#if container && children}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')}" {@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
{#key $attrsChanged}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.blockVariant === "hidden"}
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(container.id)} class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(container.id)}
class:root-container={zIndex === 0} class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting} class:is-executing={container.isNodeExecuting}
class:container-edit-outline={$uiState.uiEditMode === "widgets" && zIndex > 1}> class:edit={edit}>
<Block> <Block>
{#if container.attrs.showTitle} {#if container.attrs.title !== ""}
<label for={String(container.id)} class={$uiState.uiEditMode === "widgets" ? "edit-title-label" : ""}> <label for={String(container.id)} class={$uiState.uiEditMode === "widgets" ? "edit-title-label" : ""}>
<BlockTitle> <BlockTitle>{container.attrs.title}</BlockTitle>
{#if $uiState.uiEditMode === "widgets"}
<input class="edit-title" bind:value={container.attrs.title} type="text" minlength="1" />
{:else}
{container.attrs.title}
{/if}
</BlockTitle>
</label> </label>
{/if} {/if}
<div class="v-pane" <div class="v-pane"
class:empty={children.length === 0} class:empty={children.length === 0}
class:edit={$uiState.uiEditMode === "widgets" && zIndex > 1} class:edit={edit}
use:dndzone="{{ use:dndzone="{{
items: children, items: children,
flipDurationMs, flipDurationMs,
@@ -70,8 +70,12 @@
on:finalize="{handleFinalize}" on:finalize="{handleFinalize}"
> >
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden}
<div class="animation-wrapper" <div class="animation-wrapper"
animate:flip={{duration:flipDurationMs}}> class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
>
<WidgetContainer dragItem={item} zIndex={zIndex+1} /> <WidgetContainer dragItem={item} zIndex={zIndex+1} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]} {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/> <div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
@@ -79,11 +83,15 @@
</div> </div>
{/each} {/each}
</div> </div>
{#if container.attrs.hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles} {#if showHandles}
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if} {/if}
</Block> </Block>
</div> </div>
{/key}
{/if} {/if}
<style lang="scss"> <style lang="scss">
@@ -97,6 +105,10 @@
min-width: 200px; min-width: 200px;
} }
&:not(.edit) > .animation-wrapper.hidden {
display: none;
}
&.empty { &.empty {
border-width: 3px; border-width: 3px;
border-color: var(--color-grey-400); border-color: var(--color-grey-400);
@@ -134,10 +146,41 @@
.container { .container {
display: flex; display: flex;
> :global(*) {
border-radius: 0;
}
> :global(.padded) {
padding: 10px 12px 0px 10px;
&:last-child {
padding-bottom: 12px;
}
}
:global(.block) { :global(.block) {
height: fit-content; height: fit-content;
} }
.edit > :global(.block) {
border-color: var(--color-pink-500);
border-width: 2px;
border-style: dashed !important;
margin: 0.2em;
padding: 1.4em;
}
:global(.hide-block > .block) {
padding: 0.5em 0.25em;
box-shadow: unset;
border-width: 0;
border-color: unset;
border-radius: unset;
background: var(--block-background-fill);
width: 100%;
line-height: var(--line-sm);
}
&.horizontal { &.horizontal {
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--layout-gap); gap: var(--layout-gap);
@@ -177,14 +220,6 @@
padding: 0.2em; padding: 0.2em;
} }
.animation-wrapper {
position: relative;
&:not(.edit) {
flex-grow: 1;
}
}
.handle { .handle {
cursor: grab; cursor: grab;
z-index: 99999; z-index: 99999;
@@ -195,6 +230,11 @@
height: 100%; height: 100%;
} }
.animation-wrapper {
position: relative;
flex-grow: 100;
}
.handle-widget:hover { .handle-widget:hover {
background-color: #add8e680; background-color: #add8e680;
} }
@@ -249,14 +289,6 @@
color: var(--input-placeholder-color); color: var(--input-placeholder-color);
} }
.container-edit-outline > :global(.block) {
border-color: var(--color-pink-500);
border-width: 2px;
border-style: dashed !important;
margin: 0.2em;
padding: 1.4em;
}
.widget-edit-outline { .widget-edit-outline {
border: 2px dashed var(--color-blue-400); border: 2px dashed var(--color-blue-400);
margin: 0.2em; margin: 0.2em;

View File

@@ -3,9 +3,10 @@
import { get } from "svelte/store"; import { get } from "svelte/store";
import { Pane, Splitpanes } from 'svelte-splitpanes'; import { Pane, Splitpanes } from 'svelte-splitpanes';
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import { BlockTitle } from "@gradio/atoms";
import ComfyUIPane from "./ComfyUIPane.svelte"; import ComfyUIPane from "./ComfyUIPane.svelte";
import ComfyApp, { type SerializedAppState } from "./ComfyApp"; import ComfyApp, { type SerializedAppState } from "./ComfyApp";
import { Checkbox } from "@gradio/form" import { Checkbox, TextBox } from "@gradio/form"
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState"; import layoutState from "$lib/stores/layoutState";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
@@ -16,6 +17,7 @@
import { LGraph } from "@litegraph-ts/core"; import { LGraph } from "@litegraph-ts/core";
import LightboxModal from "./LightboxModal.svelte"; import LightboxModal from "./LightboxModal.svelte";
import ComfyQueue from "./ComfyQueue.svelte"; import ComfyQueue from "./ComfyQueue.svelte";
import ComfyProperties from "./ComfyProperties.svelte";
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
export let app: ComfyApp = undefined; export let app: ComfyApp = undefined;
@@ -23,9 +25,11 @@
let queue: ComfyQueue = undefined; let queue: ComfyQueue = undefined;
let mainElem: HTMLDivElement; let mainElem: HTMLDivElement;
let uiPane: ComfyUIPane = undefined; let uiPane: ComfyUIPane = undefined;
let props: ComfyProperties = undefined;
let containerElem: HTMLDivElement; let containerElem: HTMLDivElement;
let resizeTimeout: NodeJS.Timeout | null; let resizeTimeout: NodeJS.Timeout | null;
let hasShownUIHelpToast: boolean = false; let hasShownUIHelpToast: boolean = false;
let uiTheme: string = "anapnoe";
let debugLayout: boolean = false; let debugLayout: boolean = false;
@@ -43,7 +47,8 @@
function queuePrompt() { function queuePrompt() {
console.log("Queuing!"); console.log("Queuing!");
app.queuePrompt(0, 1); const workflow = $layoutState.attrs.defaultSubgraph;
app.queuePrompt(0, 1, workflow);
} }
$: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked; $: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked;
@@ -52,11 +57,11 @@
$: if ($uiState.uiEditMode) $: if ($uiState.uiEditMode)
$layoutState.currentSelection = [] $layoutState.currentSelection = []
let graphSize = null; let graphSize = 0;
function toggleGraph() { function toggleGraph() {
if (graphSize == 0) { if (graphSize == 0) {
graphSize = 100; graphSize = 50;
app.resizeCanvas(); app.resizeCanvas();
} }
else { else {
@@ -64,15 +69,27 @@
} }
} }
let sidebarSize = 20; let propsSidebarSize = 0; //15;
function toggleSidebar() { function toggleProps() {
if (sidebarSize == 0) { if (propsSidebarSize == 0) {
sidebarSize = 20; propsSidebarSize = 15;
app.resizeCanvas(); app.resizeCanvas();
} }
else { else {
sidebarSize = 0; propsSidebarSize = 0;
}
}
let queueSidebarSize = 15;
function toggleQueue() {
if (queueSidebarSize == 0) {
queueSidebarSize = 15;
app.resizeCanvas();
}
else {
queueSidebarSize = 0;
} }
} }
@@ -141,6 +158,8 @@
(window as any).app = app; (window as any).app = app;
(window as any).appPane = uiPane; (window as any).appPane = uiPane;
await import('../../scss/ux.scss');
refreshView(); refreshView();
}) })
@@ -149,10 +168,21 @@
} }
</script> </script>
<svelte:head>
{#if uiTheme === "anapnoe"}
<link rel="stylesheet" href="/src/scss/ux.scss">
{/if}
</svelte:head>
<div id="main"> <div id="main">
<div id="dropzone" class="dropzone"></div> <div id="dropzone" class="dropzone"></div>
<div id="container" bind:this={containerElem}> <div id="container" bind:this={containerElem}>
<Splitpanes theme="comfy" on:resize={refreshView}> <Splitpanes theme="comfy" on:resize={refreshView}>
<Pane bind:size={propsSidebarSize}>
<div class="sidebar-wrapper pane-wrapper">
<ComfyProperties bind:this={props} />
</div>
</Pane>
<Pane> <Pane>
<Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}"> <Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}">
<Pane> <Pane>
@@ -165,7 +195,7 @@
</Pane> </Pane>
</Splitpanes> </Splitpanes>
</Pane> </Pane>
<Pane bind:size={sidebarSize}> <Pane bind:size={queueSidebarSize}>
<div class="sidebar-wrapper pane-wrapper"> <div class="sidebar-wrapper pane-wrapper">
<ComfyQueue bind:this={queue} /> <ComfyQueue bind:this={queue} />
</div> </div>
@@ -174,13 +204,16 @@
</div> </div>
<div id="bottombar"> <div id="bottombar">
<Button variant="primary" on:click={queuePrompt}> <Button variant="primary" on:click={queuePrompt}>
Run Queue Prompt
</Button> </Button>
<Button variant="secondary" on:click={toggleGraph}> <Button variant="secondary" on:click={toggleGraph}>
Toggle Graph Toggle Graph
</Button> </Button>
<Button variant="secondary" on:click={toggleSidebar}> <Button variant="secondary" on:click={toggleProps}>
Toggle Sidebar Toggle Props
</Button>
<Button variant="secondary" on:click={toggleQueue}>
Toggle Queue
</Button> </Button>
<Button variant="secondary" on:click={doSave}> <Button variant="secondary" on:click={doSave}>
Save Save
@@ -197,14 +230,23 @@
<Button variant="secondary" on:click={doRefreshCombos}> <Button variant="secondary" on:click={doRefreshCombos}>
🔄 🔄
</Button> </Button>
<Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/> <!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> <Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/> <Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
<label for="enable-ui-editing">Enable UI Editing</label> <label class="label" for="enable-ui-editing">
<BlockTitle>Enable UI Editing</BlockTitle>
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}> <select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
<option value="disabled">Disabled</option> <option value="disabled">Disabled</option>
<option value="widgets">Widgets</option> <option value="widgets">Widgets</option>
</select> </select>
</label>
<label class="label" for="ui-theme">
<BlockTitle>Theme</BlockTitle>
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
<option value="">None</option>
<option value="anapnoe">Anapnoe</option>
</select>
</label>
</div> </div>
<LightboxModal /> <LightboxModal />
</div> </div>
@@ -213,7 +255,7 @@
<style lang="scss"> <style lang="scss">
#container { #container {
height: calc(100vh - 60px); height: calc(100vh - 70px);
max-width: 100vw; max-width: 100vw;
display: grid; display: grid;
width: 100%; width: 100%;
@@ -293,7 +335,7 @@
} }
:global(.splitpanes.comfy) { :global(.splitpanes.comfy) {
max-height: calc(100vh - 60px); max-height: calc(100vh - 70px);
max-width: 100vw; max-width: 100vw;
} }
@@ -304,4 +346,8 @@
display: flex; display: flex;
position: relative; position: relative;
} }
label.label > :global(span) {
top: 20%;
}
</style> </style>

View File

@@ -10,6 +10,8 @@ import type TypedEmitter from "typed-emitter";
import "@litegraph-ts/nodes-basic" import "@litegraph-ts/nodes-basic"
import "@litegraph-ts/nodes-events" import "@litegraph-ts/nodes-events"
import "@litegraph-ts/nodes-math" import "@litegraph-ts/nodes-math"
import "@litegraph-ts/nodes-strings"
import "$lib/nodes/index"
import * as nodes from "$lib/nodes/index" import * as nodes from "$lib/nodes/index"
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas"; import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
@@ -24,10 +26,13 @@ import { toast } from '@zerodevx/svelte-toast'
import ComfyGraph from "$lib/ComfyGraph"; import ComfyGraph from "$lib/ComfyGraph";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import { get } from "svelte/store"; import { get } from "svelte/store";
import uiState from "$lib/stores/uiState";
import { promptToGraphVis } from "$lib/utils";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
LiteGraph.catch_exceptions = false; LiteGraph.catch_exceptions = false;
LiteGraph.CANVAS_GRID_SIZE = 32;
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// Load default visibility // Load default visibility
@@ -52,9 +57,11 @@ export type SerializedPromptInputs = {
class_type: string class_type: string
} }
export type SerializedPromptOutput = Record<string, SerializedPromptInputs>
export type SerializedPrompt = { export type SerializedPrompt = {
workflow: SerializedLGraph, workflow: SerializedLGraph,
output: Record<string, SerializedPromptInputs> output: SerializedPromptOutput
} }
export type Progress = { export type Progress = {
@@ -318,12 +325,12 @@ export default class ComfyApp {
} }
// Distinguish frontend/backend connections // Distinguish frontend/backend connections
const BACKEND_TYPES = ["CLIP", "CLIP_VISION", "CLIP_VISION_OUTPUT", "CONDITIONING", "CONTROL_NET", "IMAGE", "LATENT", "MASK", "MODEL", "STYLE_MODEL", "VAE"] const BACKEND_TYPES = ["CLIP", "CLIP_VISION", "CLIP_VISION_OUTPUT", "CONDITIONING", "CONTROL_NET", "LATENT", "MASK", "MODEL", "STYLE_MODEL", "VAE"]
for (const type of BACKEND_TYPES) { for (const type of BACKEND_TYPES) {
setColor(type, "orange") setColor(type, "orange")
} }
setColor("OUTPUT", "rebeccapurple") setColor("IMAGE", "rebeccapurple")
setColor(BuiltInSlotType.EVENT, "lightseagreen") setColor(BuiltInSlotType.EVENT, "lightseagreen")
setColor(BuiltInSlotType.ACTION, "lightseagreen") setColor(BuiltInSlotType.ACTION, "lightseagreen")
} }
@@ -418,7 +425,7 @@ export default class ComfyApp {
* Converts the current graph workflow for sending to the API * Converts the current graph workflow for sending to the API
* @returns The workflow and node links * @returns The workflow and node links
*/ */
async graphToPrompt(): Promise<SerializedPrompt> { async graphToPrompt(tag: string | null = null): Promise<SerializedPrompt> {
// Run frontend-only logic // Run frontend-only logic
this.lGraph.runStep(1) this.lGraph.runStep(1)
@@ -436,6 +443,11 @@ export default class ComfyApp {
const node = node_ as ComfyBackendNode; const node = node_ as ComfyBackendNode;
if (tag && node.tags.indexOf(tag) === -1) {
console.debug("Skipping tagged node", tag, node.tags)
continue;
}
if (node.mode === NodeMode.NEVER) { if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes // Don't serialize muted nodes
continue; continue;
@@ -449,6 +461,11 @@ export default class ComfyApp {
const inp = node.inputs[i]; const inp = node.inputs[i];
const inputLink = node.getInputLink(i) const inputLink = node.getInputLink(i)
const inputNode = node.getInputNode(i) const inputNode = node.getInputNode(i)
if (inputNode && tag && "tags" in inputNode && (inputNode.tags as string[]).indexOf(tag) === -1) {
continue;
}
if (!inputLink || !inputNode) { if (!inputLink || !inputNode) {
if ("config" in inp) { if ("config" in inp) {
const defaultValue = (inp as IComfyInputSlot).config?.defaultValue const defaultValue = (inp as IComfyInputSlot).config?.defaultValue
@@ -487,17 +504,36 @@ export default class ComfyApp {
if (parent) { if (parent) {
const seen = {} const seen = {}
let link = node.getInputLink(i); let link = node.getInputLink(i);
while (parent && !parent.isBackendNode) {
const isValidParent = (parent: ComfyGraphNode) => {
if (!parent || parent.isBackendNode)
return false;
if ("tags" in parent && (parent.tags as string[]).indexOf(tag) === -1)
return false;
return true;
}
while (isValidParent(parent)) {
link = parent.getInputLink(link.origin_slot); link = parent.getInputLink(link.origin_slot);
if (link && !seen[link.id]) { if (link && !seen[link.id]) {
seen[link.id] = true seen[link.id] = true
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNode; const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
if (inputNode && "tags" in inputNode && tag && (inputNode.tags as string[]).indexOf(tag) === -1) {
console.debug("Skipping tagged parent node", tag, node.tags)
parent = null;
}
else {
parent = inputNode;
}
} else { } else {
parent = null; parent = null;
} }
} }
if (link && parent && parent.isBackendNode) { if (link && parent && parent.isBackendNode) {
if ("tags" in parent && tag && (parent.tags as string[]).indexOf(tag) === -1)
continue;
const input = node.inputs[i] const input = node.inputs[i]
// TODO can null be a legitimate value in some cases? // TODO can null be a legitimate value in some cases?
// Nodes like CLIPLoader will never have a value in the frontend, hence "null". // Nodes like CLIPLoader will never have a value in the frontend, hence "null".
@@ -520,15 +556,19 @@ export default class ComfyApp {
if (Array.isArray(output[o].inputs[i]) if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2 && output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) { && !output[output[o].inputs[i][0]]) {
console.debug("Prune removed node link", o, i, output[o].inputs[i])
delete output[o].inputs[i]; delete output[o].inputs[i];
} }
} }
} }
console.warn({ workflow, output })
console.warn(promptToGraphVis({ workflow, output }))
return { workflow, output }; return { workflow, output };
} }
async queuePrompt(num: number, batchCount: number = 1) { async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
this.queueItems.push({ num, batchCount }); this.queueItems.push({ num, batchCount });
// Only have one action process the items so each one gets a unique seed correctly // Only have one action process the items so each one gets a unique seed correctly
@@ -536,14 +576,23 @@ export default class ComfyApp {
return; return;
} }
if (tag === "")
tag = null;
this.processingQueue = true; this.processingQueue = true;
try { try {
while (this.queueItems.length) { while (this.queueItems.length) {
({ num, batchCount } = this.queueItems.pop()); ({ num, batchCount } = this.queueItems.pop());
console.log(`Queue get! ${num} ${batchCount}`); console.debug(`Queue get! ${num} ${batchCount} ${tag}`);
for (let i = 0; i < batchCount; i++) { for (let i = 0; i < batchCount; i++) {
const p = await this.graphToPrompt(); for (const node of this.lGraph._nodes_in_order) {
if ("beforeQueued" in node) {
(node as ComfyGraphNode).beforeQueued();
}
}
const p = await this.graphToPrompt(tag);
try { try {
await this.api.queuePrompt(num, p); await this.api.queuePrompt(num, p);
@@ -624,12 +673,8 @@ export default class ComfyApp {
if ("config" in input) { if ("config" in input) {
const comfyInput = input as IComfyInputSlot; const comfyInput = input as IComfyInputSlot;
console.warn("RefreshCombo", comfyInput.defaultWidgetNode, comfyInput)
if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) { if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) {
comfyInput.config.values = def["input"]["required"][comfyInput.name][0]; comfyInput.config.values = def["input"]["required"][comfyInput.name][0];
console.warn("RefreshCombo", comfyInput.config.values, def["input"]["required"][comfyInput.name])
const inputNode = node.getInputNode(index) const inputNode = node.getInputNode(index)
if (inputNode && "doAutoConfig" in inputNode) { if (inputNode && "doAutoConfig" in inputNode) {

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import { BlockTitle } from "@gradio/atoms";
import { createEventDispatcher } from "svelte";
export let value: string = "";
export let values: string[] = [""];
export let name: string = "";
let value_: string = ""
$: value;
$: handleChange(value);
const dispatch = createEventDispatcher<{
change: string;
submit: undefined;
blur: undefined;
}>();
function handleChange(val: string) {
console.debug("combo handleChange", val, value_)
if (val != value_)
dispatch("change", val);
value_ = val
}
</script>
<label class="select-wrapper">
<BlockTitle>{name}</BlockTitle>
<div class="select">
<select on:blur bind:value>
{#each values as value}
<option {value}>
{value}
</option>
{/each}
</select>
</div>
</label>
<style lang="scss">
.select-wrapper {
width: 100%;
.select {
width: 100%;
select {
width: 100%
}
}
}
.select-title {
padding: 0.2rem;
}
</style>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { BlockTitle } from "@gradio/atoms";
import { createEventDispatcher } from "svelte";
export let value: number = 0;
export let step: number = 1;
export let name: string = "";
let value_: number = 0;
$: value;
$: handleChange(value);
const dispatch = createEventDispatcher<{
change: number;
submit: undefined;
blur: undefined;
}>();
function handleChange(val: number) {
if (val != value_)
dispatch("change", val);
value_ = val
}
</script>
<label class="number-wrapper">
<BlockTitle>{name}</BlockTitle>
<div class="number">
<input type="number" bind:value {step}>
</div>
</label>
<style lang="scss">
.number-wrapper {
width: 100%;
.number {
width: 100%;
input {
width: 100%
}
}
}
</style>

View File

@@ -0,0 +1,310 @@
<script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms";
import { TextBox, Checkbox } from "@gradio/form";
import { LGraphNode } from "@litegraph-ts/core"
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
import { get } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes";
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
import ComfyComboProperty from "./ComfyComboProperty.svelte";
let target: IDragItem | null = null;
let node: LGraphNode | null = null;
$: if ($layoutState.currentSelection.length > 0) {
const targetId = $layoutState.currentSelection.slice(-1)[0]
target = $layoutState.allItems[targetId].dragItem
if (target.type === "widget") {
node = (target as WidgetLayout).node
}
else {
node = null;
}
}
else if ($layoutState.currentSelectionNodes.length > 0) {
target = null;
node = $layoutState.currentSelectionNodes[0]
}
else {
target = null
node = null;
}
let targetType: string = "???"
$: {
if (node != null)
targetType = node.type || "Node"
else if (target)
targetType = "Group"
else
targetType = ""
}
function validNodeProperty(spec: AttributesSpec, node: LGraphNode): boolean {
if (spec.validNodeTypes) {
return spec.validNodeTypes.indexOf(node.type) !== -1;
}
return spec.name in node.properties
}
function updateAttribute(entry: AttributesSpec, target: IDragItem, value: any) {
if (target) {
const name = entry.name
console.warn("updateAttribute", name, value)
target.attrs[name] = value
target.attrsChanged.set(!get(target.attrsChanged))
if (node && "propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
}
}
function updateProperty(entry: AttributesSpec, value: any) {
if (node) {
const name = entry.name
console.warn("updateProperty", name, value)
node.properties[name] = value;
if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
}
}
function getVar(node: LGraphNode, entry: AttributesSpec) {
let value = node[entry.name]
if (entry.serialize)
value = entry.serialize(value)
console.debug("[ComfyProperties] getVar", entry, value, node)
return value
}
function updateVar(entry: any, value: any) {
if (node) {
const name = entry.name
console.warn("updateProperty", name, value)
if (entry.deserialize)
value = entry.deserialize(value)
console.debug("[ComfyProperties] updateVar", entry, value, name, node)
node[name] = value;
if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
}
}
function updateWorkflowAttribute(entry: AttributesSpec, value: any) {
const name = entry.name
console.warn("updateWorkflowAttribute", name, value)
$layoutState.attrs[name] = value
$layoutState = $layoutState
}
</script>
<div class="props">
<div class="top">
<div class="target-name">
<span>
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
{#if targetType !== ""}
<span class="type">({targetType})</span>
{/if}
</span>
</div>
</div>
<div class="props-entries">
{#each ALL_ATTRIBUTES as category(category.categoryName)}
<div class="category-name">
<span>
<span class="title">{category.categoryName}</span>
</span>
</div>
{#each category.specs as spec(spec.name)}
{#if spec.location === "widget" && target && spec.name in target.attrs}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, target, e.detail)}
on:input={(e) => updateAttribute(spec, target, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, target, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={target.attrs[spec.name]}
step={1}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={target.attrs[spec.name]}
values={spec.values}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{/if}
</div>
{:else if node}
{#if spec.location === "nodeProps" && validNodeProperty(spec, node)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={node.properties[spec.name]}
on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={node.properties[spec.name]}
label={spec.name}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={node.properties[spec.name]}
step={1}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={node.properties[spec.name]}
values={spec.values}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{/if}
</div>
{:else if spec.location === "nodeVars" && spec.name in node}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
on:input={(e) => updateVar(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getVar(node, spec)}
step={1}
on:change={(e) => updateVar(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getVar(node, spec)}
values={spec.values}
on:change={(e) => updateVar(spec, e.detail)}
/>
{/if}
</div>
{/if}
{:else if spec.location === "workflow" && spec.name in $layoutState.attrs}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={$layoutState.attrs[spec.name]}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={$layoutState.attrs[spec.name]}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={$layoutState.attrs[spec.name]}
step={1}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={$layoutState.attrs[spec.name]}
values={spec.values}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{/if}
</div>
{/if}
{/each}
{/each}
</div>
</div>
<style lang="scss">
.props-entry {
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
flex-direction: row;
}
.target-name {
border-color: var(--neutral-400);
background: var(--neutral-300);
padding: 0.8rem 1.0rem;
.title {
font-weight: bold;
}
}
.category-name {
padding: 0.4rem 1.0rem;
border-color: var(--neutral-300);
background: var(--neutral-200);
}
.target-name, .category-name {
border-width: var(--block-border-width);
.type {
color: var(--neutral-500);
}
}
.bottom {
/* width: 100%;
height: auto;
position: absolute;
bottom: 0;
padding: 0.5em; */
}
</style>

View File

@@ -57,18 +57,18 @@
$: if (entries) { $: if (entries) {
_entries = [] _entries = []
for (const entry of entries) { // for (const entry of entries) {
for (const outputs of Object.values(entry.outputs)) { // for (const outputs of Object.values(entry.outputs)) {
const allImages = outputs.images.map(r => { // const allImages = outputs.images.map(r => {
// TODO configure backend URL // // TODO configure backend URL
const url = "http://localhost:8188/view?" // const url = "http://localhost:8188/view?"
const params = new URLSearchParams(r) // const params = new URLSearchParams(r)
return url + params // return url + params
}); // });
//
_entries.push({ allImages, name: "Output" }) // _entries.push({ allImages, name: "Output" })
} // }
} // }
} }
</script> </script>

View File

@@ -5,11 +5,15 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import BlockContainer from "./BlockContainer.svelte" import BlockContainer from "./BlockContainer.svelte"
import { type Writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes";
export let dragItem: IDragItem | null = null; export let dragItem: IDragItem | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
export let classes: string[] = []; export let classes: string[] = [];
let container: ContainerLayout | null = null; let container: ContainerLayout | null = null;
let attrsChanged: Writable<boolean> | null = null;
let propsChanged: Writable<number> | null = null;
let widget: WidgetLayout | null = null; let widget: WidgetLayout | null = null;
let showHandles: boolean = false; let showHandles: boolean = false;
@@ -17,14 +21,23 @@
dragItem = null; dragItem = null;
container = null; container = null;
widget = null; widget = null;
attrsChanged = null;
propsChanged = null;
} }
else if (dragItem.type === "container") { else if (dragItem.type === "container") {
container = dragItem as ContainerLayout; container = dragItem as ContainerLayout;
attrsChanged = container.attrsChanged;
widget = null; widget = null;
propsChanged = null;
} }
else if (dragItem.type === "widget") { else if (dragItem.type === "widget") {
widget = dragItem as WidgetLayout; widget = dragItem as WidgetLayout;
attrsChanged = widget.attrsChanged;
container = null; container = null;
if (widget.node && "propsChanged" in widget.node)
propsChanged = (widget.node as ComfyWidgetNode).propsChanged
else
propsChanged = null;
} }
$: showHandles = $uiState.uiEditMode === "widgets" // TODO $: showHandles = $uiState.uiEditMode === "widgets" // TODO
@@ -35,21 +48,38 @@
$: if ($queueState && widget && widget.node) { $: if ($queueState && widget && widget.node) {
dragItem.isNodeExecuting = $queueState.runningNodeId === widget.node.id; dragItem.isNodeExecuting = $queueState.runningNodeId === widget.node.id;
} }
function getWidgetClass() {
const title = widget.node.type.replace("/", "-").replace(".", "-")
return `widget--${title}`
}
</script> </script>
{#if container} {#if container}
{#key $attrsChanged}
<BlockContainer {container} {classes} {zIndex} {showHandles} /> <BlockContainer {container} {classes} {zIndex} {showHandles} />
{/key}
{:else if widget && widget.node} {:else if widget && widget.node}
<div class="widget" class:widget-edit-outline={$uiState.uiEditMode === "widgets" && zIndex > 1} {@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
{#key $attrsChanged}
{#key $propsChanged}
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
class:edit={edit}
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)} class:selected={$uiState.uiEditMode !== "disabled" && $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={widget.attrs.hidden}
> >
<svelte:component this={widget.node.svelteComponentType} {widget} /> <svelte:component this={widget.node.svelteComponentType} {widget} />
</div> </div>
{#if widget.attrs.hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles} {#if showHandles}
<div class="handle handle-widget" style="z-index: {zIndex+100}" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-widget" style="z-index: {zIndex+100}" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if} {/if}
{/key}
{/key}
{/if} {/if}
<style lang="scss"> <style lang="scss">
@@ -66,6 +96,10 @@
padding: 0.2em; padding: 0.2em;
} }
.hidden:not(.edit) {
display: none;
}
.handle { .handle {
cursor: grab; cursor: grab;
z-index: 99999; z-index: 99999;
@@ -76,6 +110,10 @@
height: 100%; height: 100%;
} }
.handle-hidden {
background-color: #40404080;
}
.handle-widget:hover { .handle-widget:hover {
background-color: #add8e680; background-color: #add8e680;
} }
@@ -85,7 +123,7 @@
color: var(--neutral-400); color: var(--neutral-400);
} }
.widget-edit-outline { .edit {
border: 2px dashed var(--color-blue-400); border: 2px dashed var(--color-blue-400);
margin: 0.2em; margin: 0.2em;
padding: 0.2em; padding: 0.2em;

File diff suppressed because it is too large Load Diff

View File

@@ -2,18 +2,21 @@ import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnec
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode from "./ComfyGraphNode";
import { Watch } from "@litegraph-ts/nodes-basic"; import { Watch } from "@litegraph-ts/nodes-basic";
import type { SerializedPrompt } from "$lib/components/ComfyApp"; import type { SerializedPrompt } from "$lib/components/ComfyApp";
import { toast } from '@zerodevx/svelte-toast'
import type { GalleryOutput } from "./ComfyWidgetNodes";
export interface ComfyAfterQueuedAction extends Record<any, any> { export interface ComfyQueueEventsProperties extends Record<any, any> {
prompt: SerializedPrompt prompt: SerializedPrompt | null
} }
export class ComfyAfterQueuedAction extends ComfyGraphNode { export class ComfyQueueEvents extends ComfyGraphNode {
override properties: ComfyCopyActionProperties = { override properties: ComfyQueueEventsProperties = {
prompt: null prompt: null
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
outputs: [ outputs: [
{ name: "beforeQueued", type: BuiltInSlotType.EVENT },
{ name: "afterQueued", type: BuiltInSlotType.EVENT }, { name: "afterQueued", type: BuiltInSlotType.EVENT },
{ name: "prompt", type: "*" } { name: "prompt", type: "*" }
], ],
@@ -21,17 +24,22 @@ export class ComfyAfterQueuedAction extends ComfyGraphNode {
override onPropertyChanged(property: string, value: any, prevValue?: any) { override onPropertyChanged(property: string, value: any, prevValue?: any) {
if (property === "value") { if (property === "value") {
this.setOutputData(0, this.properties.prompt) this.setOutputData(2, this.properties.prompt)
} }
} }
override onExecute() { override onExecute() {
this.setOutputData(0, this.properties.prompt) this.setOutputData(2, this.properties.prompt)
}
override beforeQueued() {
this.setProperty("value", null)
this.triggerSlot(0, "bang")
} }
override afterQueued(p: SerializedPrompt) { override afterQueued(p: SerializedPrompt) {
this.setProperty("value", p) this.setProperty("value", p)
this.triggerSlot(0, "bang") this.triggerSlot(1, "bang")
} }
override onSerialize(o: SerializedLGraphNode) { override onSerialize(o: SerializedLGraphNode) {
@@ -41,10 +49,52 @@ export class ComfyAfterQueuedAction extends ComfyGraphNode {
} }
LiteGraph.registerNodeType({ LiteGraph.registerNodeType({
class: ComfyAfterQueuedAction, class: ComfyQueueEvents,
title: "Comfy.AfterQueuedAction", title: "Comfy.QueueEvents",
desc: "Triggers a 'bang' event when a prompt is queued.", desc: "Triggers a 'bang' event when a prompt is queued.",
type: "actions/after_queued" type: "actions/queue_events"
})
export interface ComfyOnExecutedEventProperties extends Record<any, any> {
images: GalleryOutput | null,
filename: string | null
}
export class ComfyOnExecutedEvent extends ComfyGraphNode {
override properties: ComfyOnExecutedEventProperties = {
images: null,
filename: null
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "images", type: "IMAGE" }
],
outputs: [
{ name: "images", type: "OUTPUT" },
{ name: "onExecuted", type: BuiltInSlotType.EVENT },
],
}
override onExecute() {
if (this.properties.images !== null)
this.setOutputData(0, this.properties.images)
}
override receiveOutput(output: any) {
if (output && "images" in output) {
this.setProperty("images", output as GalleryOutput)
this.setOutputData(0, this.properties.images)
this.triggerSlot(1, "bang")
}
}
}
LiteGraph.registerNodeType({
class: ComfyOnExecutedEvent,
title: "Comfy.OnExecutedEvent",
desc: "Triggers a 'bang' event when a prompt output is received.",
type: "actions/on_executed"
}) })
export interface ComfyCopyActionProperties extends Record<any, any> { export interface ComfyCopyActionProperties extends Record<any, any> {
@@ -130,3 +180,74 @@ LiteGraph.registerNodeType({
desc: "Swaps two inputs when triggered", desc: "Swaps two inputs when triggered",
type: "actions/swap" type: "actions/swap"
}) })
export interface ComfyNotifyActionProperties extends Record<any, any> {
message: string
}
export class ComfyNotifyAction extends ComfyGraphNode {
override properties: ComfyNotifyActionProperties = {
message: "Nya."
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "message", type: "string" },
{ name: "trigger", type: BuiltInSlotType.ACTION }
],
}
override onAction(action: any, param: any) {
const message = this.getInputData(0);
if (message) {
toast.push(message);
}
};
}
LiteGraph.registerNodeType({
class: ComfyNotifyAction,
title: "Comfy.NotifyAction",
desc: "Displays a message.",
type: "actions/notify"
})
export interface ComfyExecuteSubgraphActionProperties extends Record<any, any> {
tag: string | null,
}
export class ComfyExecuteSubgraphAction extends ComfyGraphNode {
override properties: ComfyExecuteSubgraphActionProperties = {
tag: null
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "execute", type: BuiltInSlotType.ACTION },
{ name: "tag", type: "string" }
],
}
override onExecute() {
const tag = this.getInputData(1)
if (tag)
this.setProperty("tag", tag)
}
override onAction(action: any, param: any) {
const tag = this.getInputData(1) || this.properties.tag;
const app = (window as any)?.app;
if (!app)
return;
app.queuePrompt(0, 1, tag);
}
}
LiteGraph.registerNodeType({
class: ComfyExecuteSubgraphAction,
title: "Comfy.ExecuteSubgraphAction",
desc: "Runs a part of the graph based on a tag",
type: "actions/execute_subgraph"
})

View File

@@ -2,6 +2,7 @@ import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode from "./ComfyGraphNode";
import ComfyWidgets from "$lib/widgets" import ComfyWidgets from "$lib/widgets"
import type { ComfyWidgetNode } from "./ComfyWidgetNodes"; import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
import type { SerializedLGraphNode } from "@litegraph-ts/core";
/* /*
* Base class for any node with configuration sent by the backend. * Base class for any node with configuration sent by the backend.
@@ -25,10 +26,16 @@ export class ComfyBackendNode extends ComfyGraphNode {
// It just returns a hash like { "ui": { "images": results } } internally. // It just returns a hash like { "ui": { "images": results } } internally.
// So this will need to be hardcoded for now. // So this will need to be hardcoded for now.
if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) { if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) {
this.addOutput("output", "OUTPUT"); this.addOutput("output", "IMAGE");
} }
} }
/*
* Tags this node belongs to
* Allows you to run subsections of the graph
*/
tags: string[] = []
private setup(nodeData: any) { private setup(nodeData: any) {
var inputs = nodeData["input"]["required"]; var inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined) { if (nodeData["input"]["optional"] != undefined) {
@@ -74,16 +81,27 @@ export class ComfyBackendNode extends ComfyGraphNode {
// app.#invokeExtensionsAsync("nodeCreated", this); // app.#invokeExtensionsAsync("nodeCreated", this);
} }
override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o);
(o as any).tags = this.tags
}
override onConfigure(o: SerializedLGraphNode) {
super.onConfigure(o);
this.tags = (o as any).tags || []
}
override onExecuted(outputData: any) { override onExecuted(outputData: any) {
console.warn("onExecuted outputs", outputData) console.warn("onExecuted outputs", outputData)
for (let index = 0; index < this.outputs.length; index++) { for (let index = 0; index < this.outputs.length; index++) {
const output = this.outputs[index] const output = this.outputs[index]
if (output.type === "OUTPUT") { if (output.type === "IMAGE") {
this.setOutputData(index, outputData) this.setOutputData(index, outputData)
for (const node of this.getOutputNodes(index)) { for (const node of this.getOutputNodes(index)) {
console.warn(node)
if ("receiveOutput" in node) { if ("receiveOutput" in node) {
const widgetNode = node as ComfyWidgetNode; const widgetNode = node as ComfyGraphNode;
widgetNode.receiveOutput(); widgetNode.receiveOutput(outputData);
} }
} }
} }

View File

@@ -1,10 +1,12 @@
import type { ComfyInputConfig } from "$lib/IComfyInputSlot"; import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
import type { SerializedPrompt } from "$lib/components/ComfyApp"; import type { SerializedPrompt } from "$lib/components/ComfyApp";
import type ComfyWidget from "$lib/components/widgets/ComfyWidget"; import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode } from "@litegraph-ts/core"; import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import type { ComfyWidgetNode } from "./ComfyWidgetNodes"; import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
import type IComfyInputSlot from "$lib/IComfyInputSlot"; import type IComfyInputSlot from "$lib/IComfyInputSlot";
import uiState from "$lib/stores/uiState";
import { get } from "svelte/store";
export type DefaultWidgetSpec = { export type DefaultWidgetSpec = {
defaultWidgetNode: new (name?: string) => ComfyWidgetNode, defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
@@ -18,11 +20,28 @@ export type DefaultWidgetLayout = {
export default class ComfyGraphNode extends LGraphNode { export default class ComfyGraphNode extends LGraphNode {
isBackendNode?: boolean; isBackendNode?: boolean;
beforeQueued?(): void;
afterQueued?(prompt: SerializedPrompt): void; afterQueued?(prompt: SerializedPrompt): void;
onExecuted?(output: any): void; onExecuted?(output: any): void;
defaultWidgets?: DefaultWidgetLayout defaultWidgets?: DefaultWidgetLayout
/** Called when a backend node sends a ComfyUI output over a link */
receiveOutput(output: any) {
}
override onResize(size: Vector2) {
if ((window as any)?.app?.shiftDown) {
const w = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.size[0] / LiteGraph.CANVAS_GRID_SIZE);
const h = LiteGraph.CANVAS_GRID_SIZE * Math.round(this.size[1] / LiteGraph.CANVAS_GRID_SIZE);
this.size[0] = w;
this.size[1] = h;
}
if (super.onResize)
super.onResize(size)
}
override onSerialize(o: SerializedLGraphNode) { override onSerialize(o: SerializedLGraphNode) {
for (let index = 0; index < this.inputs.length; index++) { for (let index = 0; index < this.inputs.length; index++) {
const input = this.inputs[index] const input = this.inputs[index]
@@ -43,7 +62,7 @@ export default class ComfyGraphNode extends LGraphNode {
for (let index = 0; index < this.inputs.length; index++) { for (let index = 0; index < this.inputs.length; index++) {
const input = this.inputs[index] const input = this.inputs[index]
const serInput = o.inputs[index] const serInput = o.inputs[index]
if ("widgetNodeType" in serInput) { if (serInput && "widgetNodeType" in serInput) {
const comfyInput = input as IComfyInputSlot const comfyInput = input as IComfyInputSlot
const ty: string = serInput.widgetNodeType as any const ty: string = serInput.widgetNodeType as any
const widgetNode = Object.values(LiteGraph.registered_node_types) const widgetNode = Object.values(LiteGraph.registered_node_types)

View File

@@ -0,0 +1,208 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import type { GalleryOutput } from "./ComfyWidgetNodes";
export interface ComfyImageCacheNodeProperties extends Record<any, any> {
images: GalleryOutput | null,
index: number,
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
genNumber: number
}
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
/*
* A node that can act as both an input and output image node by uploading
* the output file into ComfyUI's input folder.
*/
export default class ComfyImageCacheNode extends ComfyGraphNode {
override properties: ComfyImageCacheNodeProperties = {
images: null,
index: 0,
filenames: {},
genNumber: 0
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "images", type: "OUTPUT" },
{ name: "index", type: "number" },
{ name: "store", type: BuiltInSlotType.ACTION },
{ name: "clear", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "filename", type: "string" },
{ name: "state", type: "string" },
]
}
private _uploadPromise: Promise<void> | null = null;
private _state: ImageCacheState = "none"
stateWidget: ITextWidget;
filenameWidget: ITextWidget;
constructor(name?: string) {
super(name)
this.stateWidget = this.addWidget<ITextWidget>(
"text",
"State",
"none"
);
this.stateWidget.disabled = true;
this.filenameWidget = this.addWidget<ITextWidget>(
"text",
"File",
""
);
this.filenameWidget.disabled = true;
}
override onPropertyChanged(property: string, value: any, prevValue?: any) {
if (property === "images") {
if (value != null)
this.properties.index = clamp(this.properties.index, 0, value.length)
else
this.properties.index = 0
}
if (this.properties.filenames && this.properties.images) {
const fileCount = this.properties.images.images.length;
const cachedCount = Object.keys(this.properties.filenames).length
console.warn(cachedCount, this.properties.filenames)
this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached`
}
}
override onExecute() {
const index = this.getInputData(1)
if (typeof index === "number")
this.setIndex(index)
const existing = this.properties.filenames[this.properties.index]
let state = "none"
if (existing)
state = existing.status
this.stateWidget.value = state
let filename = null
if (this.properties.index in this.properties.filenames)
filename = this.properties.filenames[this.properties.index].filename
this.setOutputData(0, filename)
this.setOutputData(1, state)
}
private setIndex(newIndex: number, force: boolean = false) {
if (newIndex === this.properties.index && !force)
return;
if (!this.properties.images || newIndex < 0 || newIndex >= this.properties.images.images.length) {
return
}
this.setProperty("index", newIndex)
const data = this.properties.images.images[newIndex]
if (data == null) {
return;
}
this.properties.filenames ||= {}
const existing = this.properties.filenames[newIndex]
if (existing != null && existing.status === "cached") {
return
}
const lastGenNumber = this.properties.genNumber
// ComfyUI's LoadImage node only operates on files in its input
// folder. Usually we're dealing with an image in either the output
// folder (SaveImage) or the temp folder (PreviewImage). So we have
// to copy the image into ComfyUI's input folder first by using
// their upload API.
if (data.subfolder === "input") {
// Already in the correct folder for use by LoadImage
this.properties.filenames[newIndex] = { filename: data.filename, status: "cached" }
this.onPropertyChanged("filenames", this.properties.filenames)
}
else {
this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
this.onPropertyChanged("filenames", this.properties.filenames)
const url = "http://localhost:8188" // TODO make configurable
const params = new URLSearchParams(data)
const promise = fetch(url + "/view?" + params)
.then((r) => r.blob())
.then((blob) => {
console.debug("Fetchin", url, params)
const formData = new FormData();
formData.append("image", blob, data.filename);
return fetch(
new Request(url + "/upload/image", {
body: formData,
method: 'POST'
})
)
})
.then((r) => r.json())
.then((json) => {
console.debug("Gottem", json)
if (lastGenNumber === this.properties.genNumber) {
this.properties.filenames[newIndex] = { filename: data.filename, status: "cached" }
this.onPropertyChanged("filenames", this.properties.filenames)
}
else {
console.warn("[ComfyImageCacheNode] New generation since index switched!")
}
this._uploadPromise = null;
})
.catch((e) => {
console.error("Error uploading:", e)
if (lastGenNumber === this.properties.genNumber) {
this.properties.filenames[newIndex] = { filename: null, status: "failed" }
this.onPropertyChanged("filenames", this.properties.filenames)
}
else {
console.warn("[ComfyImageCacheNode] New generation since index switched!")
}
})
if (this._uploadPromise)
this._uploadPromise.then(() => promise)
else
this._uploadPromise = promise
}
}
override onAction(action: any) {
if (action === "clear") {
this.setProperty("images", null)
this.setProperty("filenames", {})
this.setProperty("index", 0)
return
}
const link = this.getInputLink(0)
if (link.data && "images" in link.data) {
this.setProperty("genNumber", this.properties.genNumber + 1)
this.setProperty("images", link.data as GalleryOutput)
this.setProperty("filenames", {})
console.debug("[ComfyImageCacheNode] Received output!", link.data)
this.setIndex(0, true)
}
}
}
LiteGraph.registerNodeType({
class: ComfyImageCacheNode,
title: "Comfy.ImageCache",
desc: "Allows reusing a previously output image by uploading it into ComfyUI's input folder.",
type: "image/cache"
})

View File

@@ -94,9 +94,9 @@ export class ComfySelectorTwo extends ComfyGraphNode {
ctx.fillStyle = "#AFB"; ctx.fillStyle = "#AFB";
var y = (this.selected + 1) * LiteGraph.NODE_SLOT_HEIGHT + 6; var y = (this.selected + 1) * LiteGraph.NODE_SLOT_HEIGHT + 6;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(50, y); ctx.moveTo(65, y);
ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT); ctx.lineTo(65, y + LiteGraph.NODE_SLOT_HEIGHT);
ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5); ctx.lineTo(49, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
ctx.fill(); ctx.fill();
}; };

View File

@@ -48,9 +48,9 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
override isBackendNode = false; override isBackendNode = false;
override serialize_widgets = true; override serialize_widgets = true;
outputIndex: number = 0; outputIndex: number | null = 0;
inputIndex: number = 0; inputIndex: number = 0;
changedIndex: number = 1; changedIndex: number | null = 1;
displayWidget: ITextWidget; displayWidget: ITextWidget;
@@ -91,10 +91,10 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
console.debug("[Widget] valueUpdated", this, value) console.debug("[Widget] valueUpdated", this, value)
this.displayWidget.value = this.formatValue(value) this.displayWidget.value = this.formatValue(value)
if (this.outputs.length >= this.outputIndex) { if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
this.setOutputData(this.outputIndex, get(this.value)) this.setOutputData(this.outputIndex, get(this.value))
} }
if (this.outputs.length >= this.changedIndex) { if (this.changedIndex !== null & this.outputs.length >= this.changedIndex) {
const changedOutput = this.outputs[this.changedIndex] const changedOutput = this.outputs[this.changedIndex]
if (changedOutput.type === BuiltInSlotType.EVENT) if (changedOutput.type === BuiltInSlotType.EVENT)
this.triggerSlot(this.changedIndex, "changed") this.triggerSlot(this.changedIndex, "changed")
@@ -119,7 +119,6 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if (this.inputs.length >= this.inputIndex) { if (this.inputs.length >= this.inputIndex) {
const data = this.getInputData(this.inputIndex) const data = this.getInputData(this.inputIndex)
if (data) { // TODO can "null" be a legitimate value here? if (data) { // TODO can "null" be a legitimate value here?
console.log(data)
this.setValue(data) this.setValue(data)
const input = this.getInputLink(this.inputIndex) const input = this.getInputLink(this.inputIndex)
input.data = null; input.data = null;
@@ -135,10 +134,6 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
} }
} }
/** Called when a backend node sends a ComfyUI output over a link */
receiveOutput() {
}
onConnectOutput( onConnectOutput(
outputIndex: number, outputIndex: number,
inputType: INodeInputSlot["type"], inputType: INodeInputSlot["type"],
@@ -146,7 +141,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
inputNode: LGraphNode, inputNode: LGraphNode,
inputIndex: number inputIndex: number
): boolean { ): boolean {
if (this.autoConfig && "config" in input) { if (this.autoConfig && "config" in input && this.outputs.length === 0) {
this.doAutoConfig(input as IComfyInputSlot) this.doAutoConfig(input as IComfyInputSlot)
} }
@@ -402,31 +397,57 @@ export type GalleryOutputEntry = {
} }
export interface ComfyGalleryProperties extends ComfyWidgetProperties { export interface ComfyGalleryProperties extends ComfyWidgetProperties {
index: number
} }
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> { export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
override properties: ComfyGalleryProperties = { override properties: ComfyGalleryProperties = {
defaultValue: [] defaultValue: [],
index: 0
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
{ name: "images", type: "OUTPUT" } { name: "images", type: "OUTPUT" },
{ name: "store", type: BuiltInSlotType.ACTION },
{ name: "clear", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "selected_index", type: "number" }
] ]
} }
override svelteComponentType = GalleryWidget override svelteComponentType = GalleryWidget
override copyFromInputLink = false; override copyFromInputLink = false;
override outputIndex = null;
override changedIndex = null;
constructor(name?: string) { constructor(name?: string) {
super(name, []) super(name, [])
} }
override afterQueued() { override onExecute() {
let queue = get(queueState) this.setOutputData(0, this.properties.index)
if (!(typeof queue.queueRemaining === "number" && queue.queueRemaining > 1)) { }
override onAction(action: any) {
if (action === "clear") {
this.setValue([]) this.setValue([])
} }
else if (action === "store") {
const link = this.getInputLink(0)
if (link.data && "images" in link.data) {
const data = link.data as GalleryOutput
console.debug("[ComfyGalleryNode] Received output!", data)
const galleryItems: GradioFileData[] = this.convertItems(link.data)
// const currentValue = get(this.value)
// this.setValue(currentValue.concat(galleryItems))
this.setValue(galleryItems)
}
this.setProperty("index", 0)
}
} }
override formatValue(value: GradioFileData[] | null): string { override formatValue(value: GradioFileData[] | null): string {
@@ -452,18 +473,10 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
else { else {
super.setValue([]) super.setValue([])
} }
}
receiveOutput() { const len = get(this.value).length
const link = this.getInputLink(0) if (this.properties.index < 0 || this.properties.index >= len) {
if (link.data && "images" in link.data) { this.setProperty("index", clamp(this.properties.index, 0, len))
const data = link.data as GalleryOutput
console.debug("[ComfyGalleryNode] Received output!", data)
const galleryItems: GradioFileData[] = this.convertItems(link.data)
const currentValue = get(this.value)
this.setValue(currentValue.concat(galleryItems))
} }
} }
} }

View File

@@ -1,5 +1,6 @@
export { default as ComfyReroute } from "./ComfyReroute" export { default as ComfyReroute } from "./ComfyReroute"
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes" export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
export { ComfyCopyAction, ComfySwapAction } from "./ComfyActionNodes" export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyOnExecutedEvent, ComfyExecuteSubgraphAction } from "./ComfyActionNodes"
export { default as ComfyValueControl } from "./ComfyValueControl" export { default as ComfyValueControl } from "./ComfyValueControl"
export { default as ComfySelector } from "./ComfySelector" export { default as ComfySelector } from "./ComfySelector"
export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode"

View File

@@ -11,20 +11,45 @@ type DragItemEntry = {
parent: IDragItem | null parent: IDragItem | null
} }
export type LayoutAttributes = {
defaultSubgraph: string
}
export type LayoutState = { export type LayoutState = {
root: IDragItem | null, root: IDragItem | null,
allItems: Record<DragItemID, DragItemEntry>, allItems: Record<DragItemID, DragItemEntry>,
allItemsByNode: Record<number, DragItemEntry>, allItemsByNode: Record<number, DragItemEntry>,
currentId: number, currentId: number,
currentSelection: DragItemID[], currentSelection: DragItemID[],
currentSelectionNodes: LGraphNode[],
isConfiguring: boolean, isConfiguring: boolean,
isMenuOpen: boolean isMenuOpen: boolean,
attrs: LayoutAttributes
}
export type Attributes = {
direction: "horizontal" | "vertical",
title: string,
showTitle: boolean,
classes: string,
blockVariant?: "block" | "hidden",
hidden?: boolean,
disabled?: boolean,
flexGrow?: number
} }
export type AttributesSpec = { export type AttributesSpec = {
name: string, name: string,
type: string, type: string,
editable: boolean location: "widget" | "nodeProps" | "nodeVars" | "workflow"
editable: boolean,
values?: string[],
hidden?: boolean,
validNodeTypes?: string[],
serialize?: (arg: any) => string,
deserialize?: (arg: string) => any,
} }
export type AttributesCategorySpec = { export type AttributesCategorySpec = {
@@ -41,40 +66,116 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
{ {
name: "title", name: "title",
type: "string", type: "string",
location: "widget",
editable: true, editable: true,
}, },
{ {
name: "showTitle", name: "hidden",
type: "boolean", type: "boolean",
editable: true, location: "widget",
editable: true
},
{
name: "disabled",
type: "boolean",
location: "widget",
editable: true
}, },
{ {
name: "direction", name: "direction",
type: "string", type: "enum",
location: "widget",
editable: true, editable: true,
values: ["horizontal", "vertical"]
},
{
name: "flexGrow",
type: "number",
location: "widget",
editable: true
}, },
{ {
name: "classes", name: "classes",
type: "string", type: "string",
location: "widget",
editable: true, editable: true,
}, },
{
name: "blockVariant",
type: "enum",
location: "widget",
editable: true,
values: ["block", "hidden"]
},
]
},
{
categoryName: "behavior",
specs: [
// Node variables
{
name: "tags",
type: "string",
location: "nodeVars",
editable: true,
serialize: (arg: string[]) => arg.join(","),
deserialize: (arg: string) => {
if (arg === "")
return []
return arg.split(",").map(s => s.trim())
}
},
// Range
{
name: "min",
type: "number",
location: "nodeProps",
editable: true,
validNodeTypes: ["ui/slider"],
},
{
name: "max",
type: "number",
location: "nodeProps",
editable: true,
validNodeTypes: ["ui/slider"],
},
{
name: "step",
type: "number",
location: "nodeProps",
editable: true,
validNodeTypes: ["ui/slider"],
},
// Button
{
name: "message",
type: "string",
location: "nodeProps",
editable: true,
validNodeTypes: ["ui/button"],
},
// Workflow
{
name: "defaultSubgraph",
type: "string",
location: "workflow",
editable: true
}
] ]
} }
]; ];
export { ALL_ATTRIBUTES }; export { ALL_ATTRIBUTES };
export type Attributes = {
direction: "horizontal" | "vertical",
title: string,
showTitle: boolean,
classes: string
}
export interface IDragItem { export interface IDragItem {
type: string, type: string,
id: DragItemID, id: DragItemID,
isNodeExecuting?: boolean, isNodeExecuting?: boolean,
attrs: Attributes attrs: Attributes,
attrsChanged: Writable<boolean>
} }
export interface ContainerLayout extends IDragItem { export interface ContainerLayout extends IDragItem {
@@ -112,8 +213,12 @@ const store: Writable<LayoutState> = writable({
allItemsByNode: {}, allItemsByNode: {},
currentId: 0, currentId: 0,
currentSelection: [], currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: true isConfiguring: true,
attrs: {
defaultSubgraph: ""
}
}) })
function findDefaultContainerForInsertion(): ContainerLayout | null { function findDefaultContainerForInsertion(): ContainerLayout | null {
@@ -141,11 +246,14 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
const dragItem: ContainerLayout = { const dragItem: ContainerLayout = {
type: "container", type: "container",
id: `${state.currentId++}`, id: `${state.currentId++}`,
attrsChanged: writable(false),
attrs: { attrs: {
title: "Container", title: "Container",
showTitle: true, showTitle: true,
direction: "vertical", direction: "vertical",
classes: "", classes: "",
blockVariant: "block",
flexGrow: 100,
...attrs ...attrs
} }
} }
@@ -166,11 +274,13 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
type: "widget", type: "widget",
id: `${state.currentId++}`, id: `${state.currentId++}`,
node: node, node: node,
attrsChanged: writable(false),
attrs: { attrs: {
title: widgetName, title: widgetName,
showTitle: true, showTitle: true,
direction: "horizontal", direction: "horizontal",
classes: "", classes: "",
flexGrow: 100,
...attrs ...attrs
} }
} }
@@ -310,7 +420,9 @@ function groupItems(dragItems: IDragItem[], attrs: Partial<Attributes> = {}): Co
index = indexFound index = indexFound
} }
const container = addContainer(parent as ContainerLayout, attrs, index) const title = dragItems.length <= 1 ? "" : "Group";
const container = addContainer(parent as ContainerLayout, { title, ...attrs }, index)
for (const item of dragItems) { for (const item of dragItems) {
moveItem(item, container) moveItem(item, container)
@@ -366,15 +478,17 @@ function initDefaultLayout() {
store.set({ store.set({
root: null, root: null,
allItems: {}, allItems: {},
allItemsByNode: {},
currentId: 0, currentId: 0,
currentSelection: [], currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false isConfiguring: false
}) })
const root = addContainer(null, { direction: "horizontal", showTitle: false }); const root = addContainer(null, { direction: "horizontal", title: "" });
const left = addContainer(root, { direction: "vertical", showTitle: false }); const left = addContainer(root, { direction: "vertical", title: "" });
const right = addContainer(root, { direction: "vertical", showTitle: false }); const right = addContainer(root, { direction: "vertical", title: "" });
const state = get(store) const state = get(store)
state.root = root; state.root = root;
@@ -387,6 +501,7 @@ export type SerializedLayoutState = {
root: DragItemID | null, root: DragItemID | null,
allItems: Record<DragItemID, SerializedDragEntry>, allItems: Record<DragItemID, SerializedDragEntry>,
currentId: number, currentId: number,
attrs: LayoutAttributes
} }
export type SerializedDragEntry = { export type SerializedDragEntry = {
@@ -424,6 +539,7 @@ function serialize(): SerializedLayoutState {
root: state.root?.id, root: state.root?.id,
allItems, allItems,
currentId: state.currentId, currentId: state.currentId,
attrs: state.attrs
} }
} }
@@ -436,7 +552,8 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
const dragItem: IDragItem = { const dragItem: IDragItem = {
type: entry.dragItem.type, type: entry.dragItem.type,
id: entry.dragItem.id, id: entry.dragItem.id,
attrs: entry.dragItem.attrs attrs: entry.dragItem.attrs,
attrsChanged: writable(false)
}; };
const dragEntry: DragItemEntry = { const dragEntry: DragItemEntry = {
@@ -476,8 +593,10 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
allItemsByNode, allItemsByNode,
currentId: data.currentId, currentId: data.currentId,
currentSelection: [], currentSelection: [],
currentSelectionNodes: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false isConfiguring: false,
attrs: data.attrs
} }
console.debug("[layoutState] deserialize", data, state) console.debug("[layoutState] deserialize", data, state)

View File

@@ -9,7 +9,7 @@ export type UIState = {
nodesLocked: boolean, nodesLocked: boolean,
graphLocked: boolean, graphLocked: boolean,
autoAddUI: boolean, autoAddUI: boolean,
uiEditMode: UIEditMode uiEditMode: UIEditMode,
} }
export type WritableUIStateStore = Writable<UIState>; export type WritableUIStateStore = Writable<UIState>;

View File

@@ -1,10 +1,11 @@
import ComfyApp from "./components/ComfyApp"; import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
import ComboWidget from "$lib/widgets/ComboWidget.svelte"; import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte"; import RangeWidget from "$lib/widgets/RangeWidget.svelte";
import TextWidget from "$lib/widgets/TextWidget.svelte"; import TextWidget from "$lib/widgets/TextWidget.svelte";
import { get } from "svelte/store" import { get } from "svelte/store"
import layoutState from "$lib/stores/layoutState" import layoutState from "$lib/stores/layoutState"
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import type { SerializedLGraph } from "@litegraph-ts/core";
export function clamp(n: number, min: number, max: number): number { export function clamp(n: number, min: number, max: number): number {
return Math.min(Math.max(n, min), max) return Math.min(Math.max(n, min), max)
@@ -36,6 +37,8 @@ export function startDrag(evt: MouseEvent) {
const item = ls.allItems[dragItemId].dragItem const item = ls.allItems[dragItemId].dragItem
console.debug("startDrag", item)
if (evt.ctrlKey) { if (evt.ctrlKey) {
const index = ls.currentSelection.indexOf(item.id) const index = ls.currentSelection.indexOf(item.id)
if (index === -1) if (index === -1)
@@ -47,8 +50,48 @@ export function startDrag(evt: MouseEvent) {
else { else {
ls.currentSelection = [item.id] ls.currentSelection = [item.id]
} }
ls.currentSelectionNodes = [];
layoutState.set(ls) layoutState.set(ls)
}; };
export function stopDrag(evt: MouseEvent) { export function stopDrag(evt: MouseEvent) {
}; };
export function workflowToGraphVis(workflow: SerializedLGraph): string {
let out = "digraph {\n"
for (const link of workflow.links) {
const nodeA = workflow.nodes.find(n => n.id === link[1])
const nodeB = workflow.nodes.find(n => n.id === link[3])
out += `"${link[1]}_${nodeA.title}" -> "${link[3]}_${nodeB.title}"\n`;
}
out += "}"
return out
}
export function promptToGraphVis(prompt: SerializedPrompt): string {
let out = "digraph {\n"
for (const pair of Object.entries(prompt.output)) {
const [id, o] = pair;
const outNode = prompt.workflow.nodes.find(n => n.id == id)
for (const pair2 of Object.entries(o.inputs)) {
const [inpName, i] = pair2;
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
// Link
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
out += `"${inpNode.title}" -> "${outNode.title}"\n`
}
else {
// Value
out += `"${id}-${inpName}-${typeof i}" -> "${outNode.title}"\n`
}
}
}
out += "}"
return out
}

View File

@@ -28,15 +28,19 @@
} }
</script> </script>
<div class="wrapper gr-button"> <div class="wrapper gradio-button">
{#if node !== null} {#if node !== null}
<Button on:click={onClick} variant="primary" {style}> <Button
disabled={widget.attrs.disabled}
on:click={onClick}
variant="primary"
{style}>
{widget.attrs.title} {widget.attrs.title}
</Button> </Button>
{/if} {/if}
</div> </div>
<style> <style lang="scss">
.wrapper { .wrapper {
padding: 2px; padding: 2px;
width: 100%; width: 100%;

View File

@@ -63,16 +63,19 @@
} }
</script> </script>
<div class="wrapper gr-combo" class:updated={werePropsChanged}> <div class="wrapper comfy-combo" class:updated={werePropsChanged}>
{#key $propsChanged} {#key $propsChanged}
{#if node !== null && nodeValue !== null} {#if node !== null && nodeValue !== null}
<label> <label>
{#if widget.attrs.title !== ""}
<BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle> <BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
{/if}
<Select <Select
bind:value={option} bind:value={option}
bind:items={node.properties.values} items={node.properties.values}
disabled={node.properties.values.length === 0} disabled={widget.attrs.disabled || node.properties.values.length === 0}
clearable={false} clearable={false}
showChevron={true}
on:change on:change
on:select on:select
on:filter on:filter

View File

@@ -7,6 +7,8 @@
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes"; import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
import type { FileData as GradioFileData } from "@gradio/upload"; import type { FileData as GradioFileData } from "@gradio/upload";
import type { SelectData as GradioSelectData } from "@gradio/utils";
import { clamp } from "$lib/utils";
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
let node: ComfyGalleryNode | null = null; let node: ComfyGalleryNode | null = null;
@@ -21,6 +23,11 @@
node = widget.node as ComfyGalleryNode node = widget.node as ComfyGalleryNode
nodeValue = node.value; nodeValue = node.value;
propsChanged = node.propsChanged; propsChanged = node.propsChanged;
const len = $nodeValue.length
if (node.properties.index < 0 || node.properties.index >= len) {
node.setProperty("index", clamp(node.properties.index, 0, len))
}
} }
}; };
@@ -31,7 +38,8 @@
} }
let element: HTMLDivElement; let element: HTMLDivElement;
function updateForLightbox() { function onSelect(e: CustomEvent<GradioSelectData>) {
// Setup lightbox
// Wait for gradio gallery to show the large preview image, if no timeout then // Wait for gradio gallery to show the large preview image, if no timeout then
// the event might fire too early // the event might fire too early
setTimeout(() => { setTimeout(() => {
@@ -41,21 +49,24 @@
} }
ImageViewer.instance.updateOnBackgroundChange(); ImageViewer.instance.updateOnBackgroundChange();
}, 200) }, 200)
// Update index
node.setProperty("index", e.detail.index as number)
} }
</script> </script>
<div class="wrapper comfy-gallery-widget gr-gallery" bind:this={element}> <div class="wrapper comfy-gallery-widget gradio-gallery" bind:this={element}>
{#if widget && node && nodeValue} {#if widget && node && nodeValue}
<Block variant="solid" padding={false}> <Block variant="solid" padding={false}>
<div class="padding"> <div class="padding">
<Gallery <Gallery
bind:value={$nodeValue} bind:value={$nodeValue}
label={widget.attrs.title} label={widget.attrs.title}
show_label={true} show_label={widget.attrs.title !== ""}
{style} {style}
root={""} root={""}
root_url={""} root_url={""}
on:select={updateForLightbox} on:select={onSelect}
/> />
</div> </div>
</Block> </Block>

View File

@@ -37,12 +37,26 @@
$nodeValue = option $nodeValue = option
} }
} }
let gradient: string = ""
let elem: HTMLDivElement = null;
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
const slider = elem.querySelector("input[type='range']") as any
//const range_selectors = "[id$='_clone']:is(input[type='range'])";
let spacing = ((slider.step / ( slider.max - slider.min )) * 100.0);
let tsp = 'max(3px, calc('+spacing+'% - 2px))';
let fsp = 'max(4px, calc('+spacing+'% + 0px))';
const style = elem.style;
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
}
</script> </script>
<div class="wrapper gr-range"> <div class="wrapper gradio-slider" bind:this={elem}>
{#if node !== null && option !== null} {#if node !== null && option !== null}
<Range <Range
bind:value={option} bind:value={option}
disabled={widget.attrs.disabled}
minimum={node.properties.min} minimum={node.properties.min}
maximum={node.properties.max} maximum={node.properties.max}
step={node.properties.step} step={node.properties.step}

View File

@@ -27,11 +27,12 @@
} }
</script> </script>
<div class="wrapper gr-textbox"> <div class="wrapper gradio-textbox">
{#if node !== null && nodeValue !== null} {#if node !== null && nodeValue !== null}
<TextBox <TextBox
bind:value={$nodeValue} bind:value={$nodeValue}
label={widget.attrs.title} label={widget.attrs.title}
disabled={widget.attrs.disabled}
lines={node.properties.multiline ? 5 : 1} lines={node.properties.multiline ? 5 : 1}
max_lines={node.properties.multiline ? 5 : 1} max_lines={node.properties.multiline ? 5 : 1}
show_label={true} show_label={true}

View File

@@ -1 +1,5 @@
@import "gradio" @import "gradio";
body {
overflow: hidden;
}

815
src/scss/ux.scss Normal file
View File

@@ -0,0 +1,815 @@
/*
Theme Name: DarkUX
Author: anapnoe
Author URI: https://github.com/anapnoe/stable-diffusion-webui
Version: 1.0
License: GNU General Public License
*/
:root{
--ae-extra-networks-card-size: 1;
--ae-extra-networks-card-real-size: calc(var(--ae-extra-networks-card-size) * 14vh);
--ae-extra-networks-visible-rows: 2;
--ae-extra-networks-height: calc((var(--ae-extra-networks-card-real-size) * var(--ae-extra-networks-visible-rows)) + ( var(--ae-inside-padding-size) * 2 ) );
--ae-extra-networks-name-size: calc(var(--ae-extra-networks-card-size) * 1em);
--ae-top-header-padding-top:16px;
--ae-top-header-padding-bottom:16px;
--ae-top-header-inner-height:38px;
--ae-top-header-height: calc( var(--ae-top-header-padding-top) + var(--ae-top-header-inner-height) + var(--ae-top-header-padding-bottom) );
--ae-container-padding:16px;
--ae-footer-height: calc( 32px + (var(--ae-container-padding) * 2) );
--ae-gallery-bottom-height: calc(24px + (var(--ae-max-padding) * 2) + 16px + (var(--ae-inside-padding-size) * 2) + (var(--ae-outside-gap-size)* 3 ));
--ae-subtract-total: calc( var(--ae-top-header-height) + var(--ae-footer-height));
--ae-container-height : calc(100vh - var(--ae-subtract-total));
--ae-container-total-height : calc( var(--ae-container-height) - (var(--ae-outside-gap-size) * 2) - (var(--ae-inside-padding-size) * 2));
--ae-container-height-gap : calc( var(--ae-container-height) - (var(--ae-outside-gap-size) * 2));
--ae-container-height-pad : calc( var(--ae-container-height) - (var(--ae-inside-padding-size) * 2));
--ae-processing-border : 2px;
--ae-processing-border-double: var(--ae-processing-border) * 2;
--ae-slider-bg-overlay : transparent;
--ae-border-width: 1px;
--ae-accordion-vertical-padding: max(8px, var(--ae-inside-padding-size));
--ae-accordion-horizontal-padding: max(4px, var(--ae-inside-padding-size));
--ae-accordion-line-height: 24px;
--ae-accordion-header-height: calc(var(--ae-accordion-line-height) + var(--ae-accordion-vertical-padding) * 2);
--ae-results-height: calc(100vh - (var(--ae-top-header-height) + var(--ae-footer-height) + var(--ae-accordion-header-height) + var(--ae-outside-gap-size) * 4 + 38px));
}
:root{
--ae-main-bg-color:hsl(0deg 0% 10%);
--ae-primary-color:hsl(168deg 96% 42%);
--ae-input-bg-color:hsl(225deg 6% 13%);
--ae-input-border-color:hsl(214deg 5% 30%);
--ae-panel-bg-color:hsl(225deg 5% 17%);
--ae-panel-border-color:hsl(214deg 5% 30%);
--ae-panel-border-radius:0px;
--ae-panel-border-width:1px;
--ae-subgroup-bg-color:hsl(0deg 0% 10%);
--ae-subgroup-input-bg-color:hsl(225deg 6% 13%);
--ae-subgroup-input-border-color:hsl(214deg 5% 30%);
--ae-subpanel-bg-color:hsl(220deg 4% 14%);
--ae-subpanel-border-color:hsl(214deg 5% 30%);
--ae-subpanel-border-radius:8px;
--ae-textarea-focus-color:hsl(210deg 3% 36%);
--ae-input-focus-color:hsl(168deg 97% 41%);
--ae-outside-gap-size:8px;
--ae-inside-padding-size:8px;
--ae-tool-button-size:34px;
--ae-tool-button-radius:16px;
--ae-generate-button-height:70px;
--ae-cancel-color:hsl(0deg 84% 60%);
--ae-max-padding:max(var(--ae-outside-gap-size),var(--ae-inside-padding-size));
--ae-icon-color:hsl(168deg 96% 42%);
--ae-icon-hover-color:hsl(0deg 0% 10%);
--ae-icon-size:22px;
--ae-nav-bg-color:hsl(0deg 0% 4%);
--ae-nav-color:hsl(210deg 4% 80%);
--ae-nav-hover-color:hsl(0deg 0% 4%);
--ae-input-color:hsl(210deg 4% 80%);
--ae-label-color:hsl(210deg 4% 80%);
--ae-subgroup-input-color:hsl(0deg 100% 100%);
--ae-placeholder-color:hsl(214deg 5% 30%);
--ae-text-color:hsl(210deg 4% 80%);
--ae-mobile-outside-gap-size:3px;
--ae-mobile-inside-padding-size:3px;
--ae-frame-bg-color:hsl(225deg 6% 13%);
--ae-modal-bg-color:hsl(0deg 0% 10%);
--ae-modal-icon-color:hsl(168deg 97% 41%);
--ae-selected-color:hsl(42deg, 100%, 42%);
}/*BREAKPOINT_CSS_CONTENT*/
:root {
--toastColor: var(--ae-text-color);
--toastBackground: var(--ae-panel-bg-color);
--toastBorder: 1px solid var(--ae-panel-border-color)
}
@media only screen and (max-width: 860px) {
:root{
--ae-outside-gap-size: var(--ae-mobile-outside-gap-size);
--ae-inside-padding-size: var(--ae-mobile-inside-padding-size);
}
}
body {
background-color: var(--ae-main-bg-color) !important;
}
#main {
position: relative;
margin: auto;
padding: var(--size-4);
padding-top: 0;
width: 100%;
min-height: 100vh !important;
min-width: unset !important;
max-width: unset !important;
background-color: var(--ae-main-bg-color);
}
.v-pane {
gap: var(--ae-outside-gap-size) !important;
&.empty {
border-color: var(--ae-subpanel-border-color) !important;
border-radius: 0 !important;
background: var(--ae-subpanel-bg-color) !important;
}
}
.container {
&.selected {
background: var(--ae-primary-color) !important;
> .block.padded {
background: var(--ae-primary-color) !important;
}
}
> .block {
background: var(--ae-panel-bg-color) !important;
border-radius: var(--ae-panel-border-radius) !important;
}
&.z-index0 {
> .block {
background: var(--ae-main-bg-color) !important;
}
}
&.z-index1, &.z-index2 {
// padding: var(--ae-outside-gap-size);
// border: 1px solid var(--ae-panel-border-color);
> .block {
background: var(--ae-frame-bg-color) !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;
}
}
}
.handle {
&.handle-hidden {
background-color: hsla(0deg 84% 60% / 70%);
}
}
.animation-wrapper > .widget:not(.selected) {
background: var(--ae-panel-bg-color) !important;
}
.container {
.z-index0, .z-index1, .z-index2 {
> .block > .v-pane > .animation-wrapper > .widget:not(.edit) {
padding: var(--ae-inside-padding-size) !important;
border: 1px solid var(--ae-panel-border-color) !important;
}
}
}
.widget:has(> .gradio-button) {
height: 100%;
.gradio-button {
height: 100%;
button {
height: 100%;
}
}
}
.gradio-gallery > .block {
background: var(--ae-main-bg-color);
border-color: var(--ae-panel-border-color);
border-radius: 0px;
.thumbnail-item {
box-shadow: none !important;
border: 1px solid var(--ae-panel-border-color) !important;
border-radius: 0 !important;
background: var(--ae-main-bg-color)!important;
aspect-ratio: unset !important;
overflow: visible !important;
object-fit: contain !important;
}
.icon {
color: var(--ae-input-color);
}
.preview {
background: var(--ae-main-bg-color);
button {
outline: none!important;
box-shadow: none!important;
border: 1px solid var(--ae-input-border-color)!important;
border-radius: var(--ae-panel-border-radius)!important;
background: var(--ae-input-bg-color)!important;
text-align: left!important;
min-width: unset;
&:hover {
color: var(--ae-input-color)!important;
}
}
}
}
.gradio-gallery .preview.fixed-height {
height: auto;
min-height: auto;
width: 100%;
min-width: 100%;
max-height: calc(var(--container-height) - 4px);
}
/* small info upload*/
div.float {
background: var(--ae-main-bg-color)!important;
border: 0 !important;
color: var(--ae-primary-color)!important;
}
.widget {
&.selected {
background: var(--ae-primary-color) !important;
}
&.edit:not(.selected) {
border-width: 2px;
border-color: var(--ae-primary-color) !important;
}
}
.target-name {
background: var(--ae-subpanel-bg-color) !important;
border-color: var(--ae-subpanel-border-color) !important;
.title, .type {
color: var(--ae-label-color) !important;
}
}
.category-name {
background: var(--ae-panel-bg-color) !important;
border-color: var(--ae-panel-border-color) !important;
.title, .type {
color: var(--ae-label-color)
}
}
.props-entry {
border-width: 1px;
border-left: 1px var(--ae-panel-border-color) !important;
border-right: 1px var(--ae-panel-border-color) !important;
}
// .container > .block {
// box-shadow: none !important;
// border-color: var(--ae-panel-border-color) !important;
// border-radius: 0 !important;
// background: var(--ae-input-bg-color) !important;
// > .v-pane {
// box-shadow: none !important;
// border-color: var(--ae-panel-border-color) !important;
// border-radius: 0 !important;
// background: var(--ae-input-bg-color) !important;
// }
// }
.block.gradio-accordion:hover .label-wrap {
color: var(--ae-main-bg-color) !important;
background-color: var(--ae-primary-color) !important;
}
.block.gradio-accordion > div.wrap {
pointer-events: all !important;
cursor: pointer;
width: auto !important;
height: var(--ae-accordion-header-height)!important;
z-index: 1;
left: 0 !important;
top: 0 !important;
opacity: 0 !important;
}
.form>.gradio-row>.form{
border:0 !important;
}
.padded {
padding: var(--ae-inside-padding-size) !important
}
.gradio-row,
.gap {
gap: var(--ae-outside-gap-size) !important
}
button.tool {
max-width: 34px;
min-height: 34px;
min-width: 34px !important;
}
div.block.padded {
/*box-shadow: var(--block-shadow);*/
border-width: var(--ae-border-width);
border-color: var(--ae-panel-border-color);
border-radius: var(--ae-panel-border-radius) !important;
background: var(--ae-panel-bg-color);
/*width: 100%;
line-height: var(--line-sm);*/
}
fieldset.block.padded
{
background-color: var(--ae-panel-bg-color) !important;
/*border-width: var(--ae-border-width) !important;*/
/*border-color: var(--ae-panel-border-color) !important;*/
border-radius: var(--ae-panel-border-radius) !important;
}
div.svelte-b6y5bg,
div.gradio-row>.form{
/*box-shadow: var(--block-shadow);*/
border-width: var(--ae-border-width) !important;
border-color: var(--ae-panel-border-color) !important;
border-radius: var(--ae-panel-border-radius) !important;
background: var(--ae-panel-border-color) !important;
/*width: 100%;
line-height: var(--line-sm);*/
}
.block.gradio-dropdown,
.block.gradio-slider,
.block.gradio-checkbox,
.block.gradio-textbox,
.block.gradio-radio,
.block.gradio-checkboxgroup,
.block.gradio-number,
.block.gradio-colorpicker
{
border-width: 0;
box-shadow: none !important;
}
.wrapper.gradio-textbox textarea {
overflow-y: scroll;
box-sizing: border-box;
}
.gradio-dropdown input{
margin:0 !important;
}
.block.gradio-dropdown span.single-select{
color: var(--ae-input-color)!important;
}
.dropdown-arrow.svelte-p5edak {
fill: var(--ae-input-color)!important;
}
.wrap.svelte-1p9xokt.svelte-1p9xokt.svelte-1p9xokt label,
.wrap.svelte-1qxcj04.svelte-1qxcj04.svelte-1qxcj04 label,
button.tool.secondary,
button.secondary,
.gradio-dropdown label .wrap,
input[type=text],
input[type=password],
input[type=email],
textarea,
input[type=number],
select {
outline: none!important;
box-shadow: none!important;
border: 1px solid var(--ae-input-border-color)!important;
border-radius: var(--ae-panel-border-radius)!important;
background: var(--ae-input-bg-color)!important;
color: var(--ae-input-color)!important;
text-align: left!important;
min-width: unset;
}
select {
appearance: auto;
}
select:after{
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #f00;
position: absolute;
top: 40%;
right: 5px;
content: "";
z-index: 98;
}
button.tool.secondary,
button.secondary{
text-align: center!important;
}
.gradio-container-3-28-1 .prose * {
color: var(--ae-label-color);
}
.gradio-container-3-23-0 .prose code {
background-color: var(--ae-panel-bg-color);
border-radius: var(--ae-panel-bg-color);
border: 1px solid var(--ae-panel-border-color);
padding: 0 !important;
margin: 0!important;
white-space: break-spaces !important;
}
[type=text],
[type=email],
[type=url],
[type=password],
[type=number],
[type=date],
[type=datetime-local],
[type=month],
[type=search],
[type=tel],
[type=time],
[type=week],
[multiple],
textarea,
select {
line-height: 1.5rem;
padding: 4px 8px;
}
button.tool.secondary,
button.secondary,
.gradio-dropdown label .wrap,
input[type=text],
input[type=password],
input[type=email],
textarea,
input[type=number] {
outline: none!important;
box-shadow: none!important;
border: 1px solid var(--ae-input-border-color)!important;
border-radius: var(--ae-panel-border-radius)!important;
background: var(--ae-input-bg-color)!important;
color: var(--ae-input-color)!important;
text-align: left!important;
min-width: unset;
}
input[type=checkbox], input[type=radio] {
background-color: var(--ae-input-bg-color) !important;
border: 1px solid var(--ae-input-border-color) !important;
border-radius: var(--ae-panel-border-radius) !important;
}
input[type=checkbox]:checked, input[type=radio]:checked {
background-color: var(--ae-primary-color) !important;
}
.gradio-slider input[type=number] {
padding-right: 2px!important;
max-height:24px !important;
// width: 64px !important;
margin-bottom: var(--ae-inside-padding-size);
}
.gradio-dropdown:not(.multiselect) .wrap-inner{
padding: 0px 5px !important;
height:32px !important;
}
fieldset span,
label > span{
color:var(--ae-label-color) !important;
}
.gradio-radio label > span{
color:var(--ae-input-color) !important;
}
input[type=number],
input[type=text],
input[type=password],
input[type=email] {
height: 34px;
}
.gradio-slider input[type=range] {
align-self: flex-start;
}
span.svelte-1gfkn6j:not(.has-info) {
margin-top: 1px;
margin-left: 1px;
margin-bottom: var(--ae-inside-padding-size);
}
/* input column alignment */
label.block{
display: flex;
justify-content: space-between;
flex-direction: column;
min-height: 100%;
}
div.block.padded.gradio-slider {
display: flex;
flex-wrap: wrap;
align-content: space-between;
}
/***********/
/* Buttons */
/***********/
button.secondary,
button.primary {
border: 1px solid var(--ae-input-border-color) !important;
border-radius: var(--ae-panel-border-radius) !important;
background: var(--ae-input-bg-color) !important;
color: var(--ae-input-color) !important;
}
button.secondary:hover,
button.primary:hover {
background: var(--ae-primary-color) !important;
color: var(--ae-input-bg-color) !important;
}
/**********************/
/* Sliders Scrollbars */
/**********************/
::-webkit-scrollbar {
width: 16px;
}
[id$="2img_settings_scroll"]::-webkit-scrollbar
{
width: 12px;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 10px 10px var(--ae-main-bg-color);
}
::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 10px 10px var(--ae-panel-bg-color);
&.horizontal:hover, &.vertical:hover {
background: var(--ae-primary-color)
}
}
::-webkit-scrollbar-button {
display: none;
}
::-webkit-scrollbar-thumb,
::-webkit-scrollbar-track {
border-left: solid 6px var(--ae-main-bg-color);
border-radius: 0;
}
/**********/
/* Ranges */
/**********/
@media screen and (-webkit-min-device-pixel-ratio:0) {
input[type=range] {
overflow: hidden;
width: 100%;
-webkit-appearance: none;
background-color: var(--ae-input-bg-color);
border: 1px solid var(--ae-input-border-color);
position:relative;
}
input[type=range]::after {
content: '';
position: absolute;
height: 13px;
background-image: var(--ae-slider-bg-overlay);
opacity: 0.15;
width: 100%;
}
input[type=range]::-webkit-slider-runnable-track {
height: 14px;
-webkit-appearance: none;
color: var(--ae-primary-color);
margin-top: -1px;
}
input[type=range]::-webkit-slider-thumb {
width: 0px;
-webkit-appearance: none;
height: 14px;
cursor: ew-resize;
background-color: var(--ae-primary-color);
box-shadow: -1024px 0 0 1024px var(--ae-primary-color);
}
[id$="_sub-group"] input[type=range]
{
background-color: var(--ae-subgroup-input-bg-color);
border: 1px solid var(--ae-subgroup-input-border-color);
}
}
/* Firefox */
input[type=range]::-moz-range-progress {
background-color: var(--ae-primary-color);
height: 14px;
border: 1px solid var(--ae-primary-color);
}
input[type=range]::-moz-range-track {
background-color: var(--ae-input-bg-color);
}
input[type=range]::after {
content: '';
position: absolute;
height: 13px;
background-image: var(--ae-slider-bg-overlay);
opacity: 0.15;
width: 100%;
}
#quicksettings_overflow_container,
#theme_overflow_container,
[id$="2img_checkpoints_cards"],
[id$="2img_results"],
[id$="2img_settings_scroll"]
{
scrollbar-color: var(--ae-panel-bg-color) var(--ae-main-bg-color) !important;
scrollbar-width: thin !important;
/*padding: 0 1px;*/
}
input[type=range]{
width: 100%;
}
input[type=range]::-moz-range-track {
width: 100%;
background-color: var(--ae-input-bg-color);
border: none;
border-radius: 0px;
position: relative;
height: 100%;
background-image: var(--ae-slider-bg-overlay);
opacity: 0.15;
width: 100%;
}
input[type=range]::-moz-range-thumb {
border: 0px solid var(--ae-primary-color);
width: 0px;
border-radius: 0%;
background-color: var(--ae-primary-color);
}
/*hide the outline behind the border*/
input[type=range]:-moz-focusring{
outline: 1px solid var(--ae-primary-color);
outline-offset: -1px;
}
input[type=range]:focus::-moz-range-track {
background-color: var(--ae-input-bg-color);
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]:hover,
input[type="number"]:focus {
-moz-appearance: initial;
}
/* IE maybe later */
input[type=range]::-ms-fill-lower {
background-color: var(--ae-primary-color);
}
input[type=range]::-ms-fill-upper {
background-color: var(--ae-input-bg-color);
}
@keyframes light-up {
from {
background-color: var(--ae-selected-color);
}
to {
background-color: none;
}
}
.comfy-combo.updated {
}
.splitpanes.comfy .splitpanes__splitter {
background: var(--ae-panel-bg-color);
border: 1px solid var(--ae-panel-border-color);
&:hover:not([disabled]) {
background: var(--ae-primary-color);
}
}
.queue .bottom {
color: var(--ae-text-color);
> .queue-remaining {
background-color: var(--ae-panel-bg-color);
border: 1px solid var(--ae-panel-border-color);
}
> .node-name {
background-color: var(--ae-panel-bg-color);
border: 1px solid var(--ae-panel-border-color);
}
.progress {
background-color: var(--ae-panel-bg-color);
border: 1px solid var(--ae-panel-border-color);
> .bar {
color: var(--ae-main-bg-color);
background-color: var(--ae-primary-color);
> .label {
color: var(--ae-text-color);
}
}
}
}
.widget > .wrapper.comfy-combo .svelte-select {
--font-size: 13px;
--height: 24px;
--input-padding: 0px;
--chevron-width: 24px;
--chevron-height: 24px;
--padding: 0 0 0 8px;
background: var(--ae-input-bg-color) !important;
border-radius: 0px !important;
border-color: var(--ae-input-border-color) !important;
> .svelte-select-list {
background: var(--ae-panel-bg-color);
border-radius: 0px !important;
> .list-item > .item {
border-radius: 0px !important;
color: var(--ae-input-color) !important;
transition: none;
&.active {
background: var(--ae-input-focus-color);
color: var(--ae-input-bg-color) !important;
}
&.hover:not(.active) {
background: var(--ae-textarea-focus-color)
}
}
}
> .value-container {
> .selected-item {
color: var(--ae-input-color) !important;
z-index: 100;
}
> input {
border: none !important;
}
}
.icon.chevron {
color: var(--ae-input-color) !important;
}
}

BIN
static/screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -3,12 +3,25 @@ import { defineConfig } from 'vitest/config';
import { resolve } from 'path'; import { resolve } from 'path';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import FullReload from 'vite-plugin-full-reload'; import FullReload from 'vite-plugin-full-reload';
import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig({ export default defineConfig({
clearScreen: false, clearScreen: false,
plugins: [ plugins: [
svelte(), FullReload(["src/**/*.{js,ts,scss,svelte}"]),
// FullReload(["src/**/*.{js,ts,svelte}"]) svelte(), ,
viteStaticCopy({
targets: [
{
src: 'bin/run.sh',
dest: './'
},
{
src: 'bin/run.bat',
dest: './'
}
]
})
], ],
resolve: { resolve: {
alias: { alias: {