From 85d676b0f901cce8b34633bf435e224dd8425c99 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Thu, 4 May 2023 22:36:50 -0500 Subject: [PATCH 01/14] Update README --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8cce78e..16100e1 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,27 @@ This frontend isn't compatible with regular ComfyUI's workflow format since extr ## Proposed Features - All the power of ComfyUI with more convenience on top - Autocreation of UI widgets from your workflow, quickly creating a personalized dashboard -- Custom widget and node types -- Look up queued and finished generations and their configs in realtime +- Arrange the UI however you like and attach custom classes/styles to each widget +- Custom widget types +- See the status of queued and finished generations and their configs in realtime - Development with TypeScript -## Requirements +## 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 server with `python main.py --enable-cors-header` +3. In the folder you extracted to run `python -m http.server 8000` (or whatever web server you want) +4. Alternately extract the `.zip` into the `dist/` folder of this repository, then open `run.bat`/`run.sh` +5. Visit `http://localhost:8000` in your browser + +## Development + +### Requirements - `pnpm` - An installation of vanilla [ComfyUI](https://github.com/comfyanonymous/ComfyUI) for the backend -## Installation +### Installation 1. Clone the repo with submodules: From 7f64b743a7d082a0ff0327636f41b4f0c502226e Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Fri, 5 May 2023 00:49:34 -0500 Subject: [PATCH 02/14] Start of properties panel --- src/lib/components/BlockContainer.svelte | 87 ++++++------ src/lib/components/ComfyApp.svelte | 38 +++-- src/lib/components/ComfyProperties.svelte | 166 ++++++++++++++++++++++ src/lib/components/WidgetContainer.svelte | 35 +++-- src/lib/stores/layoutState.ts | 14 +- src/lib/widgets/ComboWidget.svelte | 4 +- src/scss/global.scss | 2 +- 7 files changed, 278 insertions(+), 68 deletions(-) create mode 100644 src/lib/components/ComfyProperties.svelte diff --git a/src/lib/components/BlockContainer.svelte b/src/lib/components/BlockContainer.svelte index 263ebfb..5667c15 100644 --- a/src/lib/components/BlockContainer.svelte +++ b/src/lib/components/BlockContainer.svelte @@ -16,14 +16,17 @@ export let zIndex: number = 0; export let classes: string[] = []; export let showHandles: boolean = false; + let attrsChanged: Writable | null = null; let children: IDragItem[] | null = null; const flipDurationMs = 100; $: if (container) { children = $layoutState.allItems[container.id].children; + attrsChanged = container.attrsChanged } else { children = null; + attrsChanged = null } function handleConsider(evt: any) { @@ -38,52 +41,48 @@ {#if container && children} -
1}> - - {#if container.attrs.showTitle} - - {/if} -
1} - use:dndzone="{{ - items: children, - flipDurationMs, - centreDraggedOnCursor: true, - morphDisabled: true, - dropFromOthersDisabled: zIndex === 0, - dragDisabled: zIndex === 0 || $layoutState.currentSelection.length > 2 || $uiState.uiEditMode === "disabled" - }}" - on:consider="{handleConsider}" - on:finalize="{handleFinalize}" - > - {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} -
- - {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]} -
- {/if} + {#key $attrsChanged} +
1}> + + {#if container.attrs.showTitle} + + {/if} +
1} + use:dndzone="{{ + items: children, + flipDurationMs, + centreDraggedOnCursor: true, + morphDisabled: true, + dropFromOthersDisabled: zIndex === 0, + dragDisabled: zIndex === 0 || $layoutState.currentSelection.length > 2 || $uiState.uiEditMode === "disabled" + }}" + on:consider="{handleConsider}" + on:finalize="{handleFinalize}" + > + {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} +
+ + {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]} +
+ {/if} +
+ {/each}
- {/each} + {#if showHandles} +
+ {/if} +
- {#if showHandles} -
- {/if} - -
+ {/key} {/if} diff --git a/src/lib/components/WidgetContainer.svelte b/src/lib/components/WidgetContainer.svelte index 1fd3b41..f37712e 100644 --- a/src/lib/components/WidgetContainer.svelte +++ b/src/lib/components/WidgetContainer.svelte @@ -5,11 +5,13 @@ import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import { startDrag, stopDrag } from "$lib/utils" import BlockContainer from "./BlockContainer.svelte" + import { type Writable } from "svelte/store" export let dragItem: IDragItem | null = null; export let zIndex: number = 0; export let classes: string[] = []; let container: ContainerLayout | null = null; + let attrsChanged: Writable | null = null; let widget: WidgetLayout | null = null; let showHandles: boolean = false; @@ -17,13 +19,16 @@ dragItem = null; container = null; widget = null; + attrsChanged = null; } else if (dragItem.type === "container") { container = dragItem as ContainerLayout; + attrsChanged = container.attrsChanged; widget = null; } else if (dragItem.type === "widget") { widget = dragItem as WidgetLayout; + attrsChanged = widget.attrsChanged; container = null; } @@ -35,21 +40,31 @@ $: if ($queueState && widget && widget.node) { dragItem.isNodeExecuting = $queueState.runningNodeId === widget.node.id; } + + function getWidgetClass() { + const title = widget.node.type.replace("/", "-").replace(".", "-") + return `widget--${title}` + } {#if container} - + {#key $attrsChanged} + + {/key} {:else if widget && widget.node} -
1} - class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)} - class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id} - > - -
- {#if showHandles} -
- {/if} + {#key $attrsChanged} +
1} + class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)} + class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id} + > + +
+ {#if showHandles} +
+ {/if} + {/key} {/if} diff --git a/src/lib/components/ComfyProperties.svelte b/src/lib/components/ComfyProperties.svelte index 285f449..6f874f2 100644 --- a/src/lib/components/ComfyProperties.svelte +++ b/src/lib/components/ComfyProperties.svelte @@ -90,6 +90,7 @@ updateAttribute(spec, e.detail)} + on:input={(e) => updateAttribute(spec, e.detail)} label={spec.name} max_lines={1} /> @@ -107,6 +108,7 @@ value={target.attrs[spec.name]} step={1} on:change={(e) => updateAttribute(spec, e.currentTarget.valueAsNumber)} + on:input={(e) => updateAttribute(spec, e.currentTarget.valueAsNumber)} /> {:else if spec.type === "enum"} @@ -133,6 +135,7 @@ updateProperty(spec, e.detail)} + on:input={(e) => updateAttribute(spec, e.detail)} label={spec.name} max_lines={1} /> @@ -151,6 +154,7 @@ value={node.properties[spec.name]} step={1} on:change={(e) => updateProperty(spec, e.currentTarget.valueAsNumber)} + on:input={(e) => updateProperty(spec, e.currentTarget.valueAsNumber)} />
diff --git a/src/lib/components/WidgetContainer.svelte b/src/lib/components/WidgetContainer.svelte index 59c9f6d..261e414 100644 --- a/src/lib/components/WidgetContainer.svelte +++ b/src/lib/components/WidgetContainer.svelte @@ -68,11 +68,11 @@ class:edit={edit} class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)} class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id} - class:hidden={widget.node.properties.hidden} + class:hidden={widget.attrs.hidden} >
- {#if widget.node.properties.hidden && edit} + {#if widget.attrs.hidden && edit}
{/if} {#if showHandles} diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts index 971c029..2110285 100644 --- a/src/lib/nodes/ComfyActionNodes.ts +++ b/src/lib/nodes/ComfyActionNodes.ts @@ -3,12 +3,12 @@ import ComfyGraphNode from "./ComfyGraphNode"; import { Watch } from "@litegraph-ts/nodes-basic"; import type { SerializedPrompt } from "$lib/components/ComfyApp"; -export interface ComfyAfterQueuedAction extends Record { +export interface ComfyAfterQueuedEventProperties extends Record { prompt: SerializedPrompt } -export class ComfyAfterQueuedAction extends ComfyGraphNode { - override properties: ComfyCopyActionProperties = { +export class ComfyAfterQueuedEvent extends ComfyGraphNode { + override properties: ComfyAfterQueuedEventProperties = { prompt: null } @@ -41,8 +41,8 @@ export class ComfyAfterQueuedAction extends ComfyGraphNode { } LiteGraph.registerNodeType({ - class: ComfyAfterQueuedAction, - title: "Comfy.AfterQueuedAction", + class: ComfyAfterQueuedEvent, + title: "Comfy.AfterQueuedEvent", desc: "Triggers a 'bang' event when a prompt is queued.", type: "actions/after_queued" }) diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts index cd4ad72..7c127a7 100644 --- a/src/lib/nodes/ComfyWidgetNodes.ts +++ b/src/lib/nodes/ComfyWidgetNodes.ts @@ -15,7 +15,6 @@ import type { FileData as GradioFileData } from "@gradio/upload"; import queueState from "$lib/stores/queueState"; export interface ComfyWidgetProperties extends Record { - hidden?: boolean, defaultValue: any } @@ -60,7 +59,6 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { constructor(name: string, value: T) { const color = LGraphCanvas.node_colors["blue"] super(name) - this.setProperty("hidden", false) this.value = writable(value) this.color ||= color.color this.bgColor ||= color.bgColor @@ -215,7 +213,6 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { override onConfigure(o: SerializedLGraphNode) { this.value.set((o as any).comfyValue); this.shownOutputProperties = (o as any).shownOutputProperties; - this.setProperty("hidden", false) } } diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts index 5c3f0c2..c37a316 100644 --- a/src/lib/stores/layoutState.ts +++ b/src/lib/stores/layoutState.ts @@ -28,6 +28,7 @@ export type AttributesSpec = { editable: boolean, values?: string[], + hidden?: boolean } export type AttributesCategorySpec = { @@ -47,6 +48,12 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ location: "widget", editable: true, }, + { + name: "hidden", + type: "boolean", + location: "widget", + editable: true + }, { name: "direction", type: "enum", @@ -72,12 +79,6 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ { categoryName: "behavior", specs: [ - { - name: "hidden", - type: "boolean", - location: "nodeProps", - editable: true - }, { name: "min", type: "number", @@ -106,7 +107,8 @@ export type Attributes = { title: string, showTitle: boolean, classes: string, - blockVariant?: "block" | "hidden" + blockVariant?: "block" | "hidden", + hidden?: boolean } export interface IDragItem { @@ -353,7 +355,9 @@ function groupItems(dragItems: IDragItem[], attrs: Partial = {}): Co 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) { moveItem(item, container) diff --git a/src/lib/widgets/ButtonWidget.svelte b/src/lib/widgets/ButtonWidget.svelte index b2b8b6e..f2a0dcc 100644 --- a/src/lib/widgets/ButtonWidget.svelte +++ b/src/lib/widgets/ButtonWidget.svelte @@ -28,7 +28,7 @@ } -
+
{#if node !== null}
- diff --git a/src/lib/components/ComfyNumberProperty.svelte b/src/lib/components/ComfyNumberProperty.svelte new file mode 100644 index 0000000..5e96516 --- /dev/null +++ b/src/lib/components/ComfyNumberProperty.svelte @@ -0,0 +1,45 @@ + + + + + diff --git a/src/lib/components/ComfyProperties.svelte b/src/lib/components/ComfyProperties.svelte index 82b7c03..b5aad70 100644 --- a/src/lib/components/ComfyProperties.svelte +++ b/src/lib/components/ComfyProperties.svelte @@ -2,10 +2,11 @@ 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 } from "$lib/stores/layoutState" + import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState" import { get } from "svelte/store" - import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; - import type { ComfyWidgetNode } from "$lib/nodes"; + 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; @@ -20,6 +21,10 @@ node = null; } } + else if ($layoutState.currentSelectionNodes.length > 0) { + target = null; + node = $layoutState.currentSelectionNodes[0] + } else { target = null node = null; @@ -29,14 +34,14 @@ $: { if (node != null) - targetType = node.type || "Widget" + targetType = node.type || "Node" else if (target) - targetType = "group" + targetType = "Group" else - targetType = "???" + targetType = "" } - function updateAttribute(entry: any, value: any) { + function updateAttribute(entry: AttributesSpec, value: any) { if (target) { const name = entry.name console.warn("updateAttribute", name, value) @@ -51,7 +56,7 @@ } } - function updateProperty(entry: any, value: any) { + function updateProperty(entry: AttributesSpec, value: any) { if (node) { const name = entry.name console.warn("updateProperty", name, value) @@ -64,125 +69,195 @@ } } } + + 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 + }
- {#if target} -
-
- - {target.attrs.title} +
+
+ + {target?.attrs?.title || node?.title || "Workflow"} + {#if targetType !== ""} ({targetType}) + {/if} + +
+
+
+ {#each ALL_ATTRIBUTES as category(category.categoryName)} +
+ + {category.categoryName}
-
-
- {#each ALL_ATTRIBUTES as category(category.categoryName)} -
- - {category.categoryName} - -
- {#each category.specs as spec(spec.name)} - {#if spec.location === "widget" && spec.name in target.attrs} + {#each category.specs as spec(spec.name)} + {#if spec.location === "widget" && target && spec.name in target.attrs} +
+ {#if spec.type === "string"} + updateAttribute(spec, e.detail)} + on:input={(e) => updateAttribute(spec, e.detail)} + label={spec.name} + max_lines={1} + /> + {:else if spec.type === "boolean"} + updateAttribute(spec, e.detail)} + label={spec.name} + /> + {:else if spec.type === "number"} + updateAttribute(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateAttribute(spec, e.detail)} + /> + {/if} +
+ {:else if node} + {#if spec.location === "nodeProps" && spec.name in node.properties}
{#if spec.type === "string"} updateAttribute(spec, e.detail)} - on:input={(e) => updateAttribute(spec, e.detail)} + 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"} updateAttribute(spec, e.detail)} + value={node.properties[spec.name]} + label={spec.name} + on:change={(e) => updateProperty(spec, e.detail)} + /> + {:else if spec.type === "number"} + updateProperty(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateProperty(spec, e.detail)} + /> + {/if} +
+ {:else if spec.location === "nodeVars" && spec.name in node} +
+ {#if spec.type === "string"} + updateVar(spec, e.detail)} + on:input={(e) => updateVar(spec, e.detail)} + label={spec.name} + max_lines={1} + /> + {:else if spec.type === "boolean"} + updateVar(spec, e.detail)} label={spec.name} /> {:else if spec.type === "number"} - + updateVar(spec, e.detail)} + /> {:else if spec.type === "enum"} - + updateVar(spec, e.detail)} + /> {/if}
- {:else if node} - {#if spec.location === "nodeProps" && spec.name in node.properties} -
- {#if spec.type === "string"} - updateProperty(spec, e.detail)} - on:input={(e) => updateProperty(spec, e.detail)} - label={spec.name} - max_lines={1} - /> - {:else if spec.type === "boolean"} - updateProperty(spec, e.detail)} - label={spec.name} - /> - {:else if spec.type === "number"} - - {:else if spec.type === "enum"} - - {/if} -
- {/if} {/if} - {/each} + {:else if spec.location === "workflow" && spec.name in $layoutState.attrs} +
+ {#if spec.type === "string"} + updateWorkflowAttribute(spec, e.detail)} + on:input={(e) => updateWorkflowAttribute(spec, e.detail)} + label={spec.name} + max_lines={1} + /> + {:else if spec.type === "boolean"} + updateWorkflowAttribute(spec, e.detail)} + label={spec.name} + /> + {:else if spec.type === "number"} + updateWorkflowAttribute(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateWorkflowAttribute(spec, e.detail)} + /> + {/if} +
+ {/if} {/each} -
- {/if} + {/each} +