diff --git a/README.md b/README.md index 09c36f5..7aaa936 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ This project is *still under construction* and some features are missing, be awa This frontend isn't compatible with regular ComfyUI's workflow format since extra metadata is saved like panel layout, so you'll have to spend a bit of time recreating them. This project also isn't compatible with regular ComfyUI's frontend extension format, but useful extensions can be integrated into the base repo with some effort. +Also note that the saved workflow format is subject to change until it's been finalized after enough testing, so be prepared to lose some of your work from time to time. + ## Features - *No-Code UI Builder* - A novel system for creating your own Stable Diffusion user interfaces from the basic components. - *Extension Support* - All custom ComfyUI nodes are supported out of the box. diff --git a/litegraph b/litegraph index cd4f68e..42adb8d 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit cd4f68ef42c52e7337009eec81f5e539de8999ad +Subproject commit 42adb8dba1631da0a743486f4d8eee9748ce70c8 diff --git a/public/workflows/defaultWorkflow.json b/public/workflows/defaultWorkflow.json index f445e4d..1e89326 100644 --- a/public/workflows/defaultWorkflow.json +++ b/public/workflows/defaultWorkflow.json @@ -1,6 +1,11 @@ { "createdBy": "ComfyBox", "version": 1, + "attrs": { + "title": "Default", + "queuePromptButtonName": "Queue txt2img", + "queuePromptButtonRunWorkflow": false + }, "workflow": { "last_node_id": 0, "last_link_id": 0, @@ -25709,10 +25714,6 @@ ], "parent": "eae32e42-1ccc-4a4a-923f-7ab4ccdac97a" } - }, - "attrs": { - "queuePromptButtonName": "Queue txt2img", - "queuePromptButtonRunWorkflow": false } }, "canvas": { diff --git a/src/lib/ComfyGraph.ts b/src/lib/ComfyGraph.ts index 578a6fd..17a0382 100644 --- a/src/lib/ComfyGraph.ts +++ b/src/lib/ComfyGraph.ts @@ -2,7 +2,6 @@ import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, Li import GraphSync from "./GraphSync"; import EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; -import layoutState from "./stores/layoutState"; import uiState from "./stores/uiState"; import { get } from "svelte/store"; import type ComfyGraphNode from "./nodes/ComfyGraphNode"; @@ -10,6 +9,11 @@ import type IComfyInputSlot from "./IComfyInputSlot"; import type { ComfyBackendNode } from "./nodes/ComfyBackendNode"; import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets"; import selectionState from "./stores/selectionState"; +import type { WritableLayoutStateStore } from "./stores/layoutStates"; +import type { WorkflowInstID } from "./components/ComfyApp"; +import layoutStates from "./stores/layoutStates"; +import type { ComfyWorkflow } from "./stores/workflowState"; +import workflowState from "./stores/workflowState"; type ComfyGraphEvents = { configured: (graph: LGraph) => void @@ -25,11 +29,28 @@ type ComfyGraphEvents = { export default class ComfyGraph extends LGraph { eventBus: TypedEmitter = new EventEmitter() as TypedEmitter; + workflowID: WorkflowInstID | null = null; + + get workflow(): ComfyWorkflow | null { + const workflowID = (this.getRootGraph() as ComfyGraph)?.workflowID; + if (workflowID == null) + return null; + return workflowState.getWorkflow(workflowID) + } + + constructor(workflowID?: WorkflowInstID) { + super(); + this.workflowID = workflowID; + } + override onConfigure() { console.debug("Configured"); } override onBeforeChange(graph: LGraph, info: any) { + if (this.workflow != null) + this.workflow.notifyModified() + console.debug("BeforeChange", info); } @@ -50,25 +71,33 @@ export default class ComfyGraph extends LGraph { override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { // Don't add nodes in subgraphs until this callback reaches the root // graph - if (node.getRootGraph() == null || this._is_subgraph) - return; + // Only root graphs will have a workflow ID, so we don't mind subgraphs + // missing it + if (node.getRootGraph() != null && !this._is_subgraph && this.workflowID != null) { + const layoutState = get(layoutStates).all[this.workflowID] + if (layoutState === null) { + throw new Error(`LGraph with workflow missing layout! ${this.workflowID}`) + } - this.doAddNode(node, options); + this.doAddNode(node, layoutState, options); + } + + if (this.workflow != null) + this.workflow.notifyModified() - // console.debug("Added", node); this.eventBus.emit("nodeAdded", node); } /* * Add widget UI/groups for newly added nodes. */ - private doAddNode(node: LGraphNode, options: LGraphAddNodeOptions) { + private doAddNode(node: LGraphNode, layoutState: WritableLayoutStateStore, options: LGraphAddNodeOptions) { layoutState.nodeAdded(node, options) // All nodes whether they come from base litegraph or ComfyBox should - // have tags added to them. Can't override serialization for existing - // node types to add `tags` as a new field so putting it in properties - // is better. + // have tags added to them. Can't override serialization for litegraph's + // base node types to add `tags` as a new field so putting it in + // properties is better. if (node.properties.tags == null) node.properties.tags = [] @@ -104,7 +133,6 @@ export default class ComfyGraph extends LGraph { } if (get(uiState).autoAddUI) { - console.warn("ADD", node.type, options) if (!("svelteComponentType" in node) && options.addedBy == null) { console.debug("[ComfyGraph] AutoAdd UI") const comfyNode = node as ComfyGraphNode; @@ -144,28 +172,49 @@ export default class ComfyGraph extends LGraph { // ************** RECURSION ALERT ! ************** if (node.is(Subgraph)) { for (const child of node.subgraph.iterateNodesInOrder()) { - this.doAddNode(child, options) + this.doAddNode(child, layoutState, options) } } // ************** RECURSION ALERT ! ************** + + if (this.workflow != null) + this.workflow.notifyModified() } override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) { selectionState.clear(); // safest option - layoutState.nodeRemoved(node, options); - // Handle subgraphs being removed - if (node.is(Subgraph)) { - for (const child of node.subgraph.iterateNodesInOrder()) { - this.onNodeRemoved(child, options) + if (!this._is_subgraph && this.workflowID != null) { + const layoutState = get(layoutStates).all[this.workflowID] + if (layoutState === null) { + throw new Error(`ComfyGraph with workflow missing layout! ${this.workflowID}`) + } + + layoutState.nodeRemoved(node, options); + + // Handle subgraphs being removed + if (node.is(Subgraph)) { + for (const child of node.subgraph.iterateNodesInOrder()) { + this.onNodeRemoved(child, options) + } } } - // console.debug("Removed", node); + if (this.workflow != null) + this.workflow.notifyModified() + this.eventBus.emit("nodeRemoved", node); } + override onInputsOutputsChange() { + if (this.workflow != null) + this.workflow.notifyModified() + } + override onNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) { + if (this.workflow != null) + this.workflow.notifyModified() + // console.debug("ConnectionChange", node); this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot); } diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index f09953e..4070323 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -3,11 +3,12 @@ import type ComfyApp from "./components/ComfyApp"; import queueState from "./stores/queueState"; import { get, type Unsubscriber } from "svelte/store"; import uiState from "./stores/uiState"; -import layoutState from "./stores/layoutState"; import { Watch } from "@litegraph-ts/nodes-basic"; import { ComfyReroute } from "./nodes"; import type { Progress } from "./components/ComfyApp"; import selectionState from "./stores/selectionState"; +import type ComfyGraph from "./ComfyGraph"; +import layoutStates from "./stores/layoutStates"; export type SerializedGraphCanvasState = { offset: Vector2, @@ -18,9 +19,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas { app: ComfyApp | null; private _unsubscribe: Unsubscriber; + get comfyGraph(): ComfyGraph | null { + return this.graph as ComfyGraph; + } + constructor( app: ComfyApp, canvas: HTMLCanvasElement | string, + graph?: ComfyGraph, options: { skip_render?: boolean; skip_events?: boolean; @@ -28,7 +34,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { viewport?: Vector4; } = {} ) { - super(canvas, app.lGraph, options); + super(canvas, graph, options); this.app = app; this._unsubscribe = selectionState.subscribe(ss => { for (const node of Object.values(this.selected_nodes)) { @@ -281,11 +287,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas { selectionState.update(ss => { ss.currentSelectionNodes = Object.values(nodes) ss.currentSelection = [] - const ls = get(layoutState) - for (const node of ss.currentSelectionNodes) { - const widget = ls.allItemsByNode[node.id] - if (widget) - ss.currentSelection.push(widget.dragItem.id) + const layoutState = layoutStates.getLayoutByGraph(this.graph); + if (layoutState) { + const ls = get(layoutState) + for (const node of ss.currentSelectionNodes) { + const widget = ls.allItemsByNode[node.id] + if (widget) + ss.currentSelection.push(widget.dragItem.id) + } } return ss }) @@ -298,11 +307,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas { ss.currentHoveredNodes.add(node.id) } ss.currentHovered.clear() - const ls = get(layoutState) - for (const nodeID of ss.currentHoveredNodes) { - const widget = ls.allItemsByNode[nodeID] - if (widget) - ss.currentHovered.add(widget.dragItem.id) + const layoutState = layoutStates.getLayoutByGraph(this.graph); + if (layoutState) { + const ls = get(layoutState) + for (const nodeID of ss.currentHoveredNodes) { + const widget = ls.allItemsByNode[nodeID] + if (widget) + ss.currentHovered.add(widget.dragItem.id) + } } return ss }) diff --git a/src/lib/ComfyNodeDef.ts b/src/lib/ComfyNodeDef.ts index de281ab..7a9a25d 100644 --- a/src/lib/ComfyNodeDef.ts +++ b/src/lib/ComfyNodeDef.ts @@ -27,7 +27,18 @@ export type ComfyNodeDefInput = [ComfyNodeDefInputType, ComfyNodeDefInputOptions export type ComfyNodeDefInputType = any[] | keyof typeof ComfyWidgets | string export type ComfyNodeDefInputOptions = { - forceInput?: boolean + forceInput?: boolean; + + // NOTE: For COMBO type inputs, the default value is always the first entry the list. + default?: any, + + // INT/FLOAT options + min?: number, + max?: number, + step?: number, + + // STRING options + multiline?: boolean, } // TODO if/when comfy refactors diff --git a/src/lib/api.ts b/src/lib/api.ts index 78ea676..3d0a960 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -3,7 +3,7 @@ import type TypedEmitter from "typed-emitter"; import EventEmitter from "events"; import type { ComfyImageLocation } from "$lib/utils"; import type { SerializedLGraph, UUID } from "@litegraph-ts/core"; -import type { SerializedLayoutState } from "./stores/layoutState"; +import type { SerializedLayoutState } from "./stores/layoutStates"; import type { ComfyNodeDef } from "./ComfyNodeDef"; export type ComfyPromptRequest = { diff --git a/src/lib/components/AccordionContainer.svelte b/src/lib/components/AccordionContainer.svelte index 279aafe..cde9290 100644 --- a/src/lib/components/AccordionContainer.svelte +++ b/src/lib/components/AccordionContainer.svelte @@ -11,11 +11,12 @@ // notice - fade in works fine but don't add svelte's fade-out (known issue) import {cubicIn} from 'svelte/easing'; import { flip } from 'svelte/animate'; - import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; + import { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutStates"; import { startDrag, stopDrag } from "$lib/utils" import { writable, type Writable } from "svelte/store"; import { isHidden } from "$lib/widgets/utils"; + export let layoutState: WritableLayoutStateStore; export let container: ContainerLayout | null = null; export let zIndex: number = 0; export let classes: string[] = []; @@ -59,6 +60,14 @@ navigator.vibrate(20) $isOpen = e.detail } + + function _startDrag(e: MouseEvent | TouchEvent) { + startDrag(e, layoutState) + } + + function _stopDrag(e: MouseEvent | TouchEvent) { + stopDrag(e, layoutState) + } {#if container} @@ -93,7 +102,7 @@ animate:flip={{duration:flipDurationMs}} style={item?.attrs?.style || ""} > - + {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
{/if} @@ -101,10 +110,18 @@ {/each}
{#if isHidden(container) && edit} -
+
{/if} {#if showHandles} -
+
{/if} @@ -112,7 +129,7 @@ {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} - + {/each} diff --git a/src/lib/components/BlockContainer.svelte b/src/lib/components/BlockContainer.svelte index 80b85ae..5d230b5 100644 --- a/src/lib/components/BlockContainer.svelte +++ b/src/lib/components/BlockContainer.svelte @@ -10,11 +10,12 @@ // notice - fade in works fine but don't add svelte's fade-out (known issue) import {cubicIn} from 'svelte/easing'; import { flip } from 'svelte/animate'; - import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; + import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates"; import { startDrag, stopDrag } from "$lib/utils" import type { Writable } from "svelte/store"; import { isHidden } from "$lib/widgets/utils"; + export let layoutState: WritableLayoutStateStore; export let container: ContainerLayout | null = null; export let zIndex: number = 0; export let classes: string[] = []; @@ -53,6 +54,14 @@ children = layoutState.updateChildren(container, evt.detail.items) // Ensure dragging is stopped on drag finish }; + + function _startDrag(e: MouseEvent | TouchEvent) { + startDrag(e, layoutState) + } + + function _stopDrag(e: MouseEvent | TouchEvent) { + stopDrag(e, layoutState) + } {#if container} @@ -92,7 +101,7 @@ animate:flip={{duration:flipDurationMs}} style={item?.attrs?.style || ""} > - + {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
{/if} @@ -100,10 +109,18 @@ {/each}
{#if isHidden(container) && edit} -
+
{/if} {#if showHandles} -
+
{/if}
diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index e3f11e2..28e77bb 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -1,44 +1,21 @@ @@ -208,198 +49,39 @@ {/if} - ($a1111Prompt = null)}> -
-

A1111 Prompt Details

-
- -
- -
-
+
-
- - - - - - - - - - - - - - - - - - -
-
-
- {#if $layoutState.attrs.queuePromptButtonName != ""} - - {/if} - - - - - - - - - - - - - - - UI Edit mode - - - - Theme - - -
-
- -
+
+ + + + + + +
-
diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index d49fbe2..a713d31 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -1,4 +1,4 @@ -import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex } from "@litegraph-ts/core"; +import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core"; import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api" import { getPngMetadata, importA1111 } from "$lib/pnginfo"; @@ -21,8 +21,7 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; import queueState from "$lib/stores/queueState"; import { type SvelteComponentDev } from "svelte/internal"; import type IComfyInputSlot from "$lib/IComfyInputSlot"; -import type { SerializedLayoutState } from "$lib/stores/layoutState"; -import layoutState from "$lib/stores/layoutState"; +import type { LayoutState, SerializedLayoutState, WritableLayoutStateStore } from "$lib/stores/layoutStates"; import { toast } from '@zerodevx/svelte-toast' import ComfyGraph from "$lib/ComfyGraph"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; @@ -33,7 +32,7 @@ import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, wor import notify from "$lib/notify"; import configState from "$lib/stores/configState"; import { blankGraph } from "$lib/defaultGraph"; -import type { ComfyExecutionResult } from "$lib/utils"; +import type { SerializedPromptOutput } from "$lib/utils"; import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer"; import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef"; import { ComfyComboNode } from "$lib/nodes/widgets"; @@ -41,6 +40,10 @@ import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111"; import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt"; import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer"; +import selectionState from "$lib/stores/selectionState"; +import layoutStates from "$lib/stores/layoutStates"; +import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState"; +import workflowState from "$lib/stores/workflowState"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -51,12 +54,11 @@ if (typeof window !== "undefined") { /* * Queued prompt that hasn't been sent to the backend yet. - * TODO: Assumes the currently active graph will be serialized, needs to change - * for multiple loaded workflow support */ -type QueueItem = { +type PromptQueueItem = { num: number, batchCount: number + workflow: ComfyWorkflow } export type A1111PromptAndInfo = { @@ -78,6 +80,8 @@ export type SerializedAppState = { commitHash?: string, /** Graph state */ workflow: SerializedLGraph, + /** Workflow attributes */ + attrs: WorkflowAttributes, /** UI state */ layout: SerializedLayoutState, /** Position/offset of the canvas at the time of saving */ @@ -111,7 +115,7 @@ export type SerializedPrompt = { /* * Outputs for each node. */ -export type SerializedPromptOutputs = Record +export type SerializedPromptOutputs = Record export type Progress = { value: number, @@ -128,15 +132,19 @@ type BackendComboNode = { backendNode: ComfyBackendNode } +type CanvasState = { + canvasEl: HTMLCanvasElement, + canvasCtx: CanvasRenderingContext2D, + canvas: ComfyGraphCanvas, +} + export default class ComfyApp { api: ComfyAPI; + rootEl: HTMLDivElement | null = null; canvasEl: HTMLCanvasElement | null = null; canvasCtx: CanvasRenderingContext2D | null = null; - lGraph: ComfyGraph | null = null; lCanvas: ComfyGraphCanvas | null = null; - dropZone: HTMLElement | null = null; - nodeOutputs: Record = {}; shiftDown: boolean = false; ctrlDown: boolean = false; @@ -144,7 +152,7 @@ export default class ComfyApp { alreadySetup: Writable = writable(false); a1111Prompt: Writable = writable(null); - private queueItems: QueueItem[] = []; + private queueItems: PromptQueueItem[] = []; private processingQueue: boolean = false; private promptSerializer: ComfyPromptSerializer; private stdPromptSerializer: ComfyBoxStdPromptSerializer; @@ -157,7 +165,7 @@ export default class ComfyApp { async setup(): Promise { if (get(this.alreadySetup)) { - console.error("Already setup") + console.log("Already setup") return; } @@ -165,7 +173,6 @@ export default class ComfyApp { this.rootEl = document.getElementById("app-root") as HTMLDivElement; this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement; - this.lGraph = new ComfyGraph(); this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl); this.canvasCtx = this.canvasEl.getContext("2d"); @@ -174,17 +181,13 @@ export default class ComfyApp { this.lCanvas.allow_interaction = uiUnlocked; // await this.#invokeExtensionsAsync("init"); - await this.registerNodes(); + const defs = await this.api.getNodeDefs(); + await this.registerNodes(defs); // Load previous workflow let restored = false; try { - const json = localStorage.getItem("workflow"); - if (json) { - const state = JSON.parse(json) as SerializedAppState; - await this.deserialize(state) - restored = true; - } + restored = await this.loadStateFromLocalStorage(defs); } catch (err) { console.error("Error loading previous workflow", err); notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null }) @@ -192,7 +195,7 @@ export default class ComfyApp { // We failed to restore a workflow so load the default if (!restored) { - await this.initDefaultGraph(); + await this.initDefaultWorkflow(defs); } // Save current workflow automatically @@ -225,12 +228,37 @@ export default class ComfyApp { this.lCanvas.draw(true, true); } + serialize(workflow: ComfyWorkflow): SerializedAppState { + const layoutState = layoutStates.getLayout(workflow.id); + if (layoutState == null) + throw new Error("Workflow has no layout!") + + const { graph, layout, attrs } = workflow.serialize(layoutState); + const canvas = this.lCanvas.serialize(); + + return { + createdBy: "ComfyBox", + version: COMFYBOX_SERIAL_VERSION, + commitHash: __GIT_COMMIT_HASH__, + workflow: graph, + attrs, + layout, + canvas + } + } + saveStateToLocalStorage() { try { uiState.update(s => { s.isSavingToLocalStorage = true; return s; }) - const savedWorkflow = this.serialize(); - const json = JSON.stringify(savedWorkflow); - localStorage.setItem("workflow", json) + const state = get(workflowState) + const workflows = state.openedWorkflows + const savedWorkflows = workflows.map(w => this.serialize(w)); + const activeWorkflowIndex = workflows.findIndex(w => state.activeWorkflowID === w.id); + const json = JSON.stringify({ workflows: savedWorkflows, activeWorkflowIndex }); + localStorage.setItem("workflows", json) + for (const workflow of workflows) + workflow.isModified = false; + workflowState.set(get(workflowState)); notify("Saved to local storage.") } catch (err) { @@ -241,13 +269,33 @@ export default class ComfyApp { } } + async loadStateFromLocalStorage(defs: Record): Promise { + const json = localStorage.getItem("workflows"); + if (!json) { + return false + } + + const state = JSON.parse(json); + if (!("workflows" in state)) + return false; + + const workflows = state.workflows as SerializedAppState[]; + for (const workflow of workflows) { + await this.openWorkflow(workflow, defs) + } + + if (typeof state.activeWorkflowIndex === "number") { + workflowState.setActiveWorkflow(this.lCanvas, state.activeWorkflowIndex); + selectionState.clear(); + } + + return true; + } + static node_type_overrides: Record = {} static widget_type_overrides: Record = {} - private async registerNodes() { - // Load node definitions from the backend - const defs = await this.api.getNodeDefs(); - + private async registerNodes(defs: Record) { // Register a node for each definition for (const [nodeId, nodeDef] of Object.entries(defs)) { const typeOverride = ComfyApp.node_type_overrides[nodeId] @@ -351,8 +399,12 @@ export default class ComfyApp { } catch (error) { } } - if (workflow && workflow.version && workflow.nodes && workflow.extra) { - this.loadGraphData(workflow); + if (workflow && workflow.createdBy === "ComfyBox") { + this.openWorkflow(workflow); + } + else { + // TODO handle vanilla workflows + throw new Error("Workflow was not in ComfyBox format!") } }); } @@ -375,21 +427,29 @@ export default class ComfyApp { this.api.addEventListener("progress", (progress: Progress) => { queueState.progressUpdated(progress); - this.lGraph.setDirtyCanvas(true, false); + workflowState.getActiveWorkflow()?.graph?.setDirtyCanvas(true, false); // TODO PromptID }); this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => { - queueState.executingUpdated(promptID, nodeID); - this.lGraph.setDirtyCanvas(true, false); + const queueEntry = queueState.executingUpdated(promptID, nodeID); + if (queueEntry != null) { + const workflow = workflowState.getWorkflow(queueEntry.workflowID); + workflow?.graph?.setDirtyCanvas(true, false); + } }); - this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => { - this.nodeOutputs[nodeID] = output; - const node = this.lGraph.getNodeByIdRecursive(nodeID) as ComfyGraphNode; - if (node?.onExecuted) { - node.onExecuted(output); + this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutput) => { + const queueEntry = queueState.onExecuted(promptID, nodeID, output) + if (queueEntry != null) { + const workflow = workflowState.getWorkflow(queueEntry.workflowID); + if (workflow != null) { + workflow.graph.setDirtyCanvas(true, false); + const node = workflow.graph.getNodeByIdRecursive(nodeID) as ComfyGraphNode; + if (node?.onExecuted) { + node.onExecuted(output); + } + } } - queueState.onExecuted(promptID, nodeID, output) }); this.api.addEventListener("execution_start", (promptID: PromptID) => { @@ -456,49 +516,49 @@ export default class ComfyApp { setColor(BuiltInSlotType.ACTION, "lightseagreen") } - serialize(): SerializedAppState { - const graph = this.lGraph; - - const serializedGraph = graph.serialize() - const serializedLayout = layoutState.serialize() - const serializedCanvas = this.lCanvas.serialize(); - - return { - createdBy: "ComfyBox", - version: COMFYBOX_SERIAL_VERSION, - workflow: serializedGraph, - layout: serializedLayout, - canvas: serializedCanvas - } - } - - async deserialize(data: SerializedAppState) { + async openWorkflow(data: SerializedAppState, refreshCombos: boolean | Record = true): Promise { if (data.version !== COMFYBOX_SERIAL_VERSION) { throw `Invalid ComfyBox saved data format: ${data.version}` } + this.clean(); - // Ensure loadGraphData does not trigger any state changes in layoutState - // (isConfiguring is set to true here) - // lGraph.configure will add new nodes, triggering onNodeAdded, but we - // want to restore the layoutState ourselves - layoutState.onStartConfigure(); - - this.loadGraphData(data.workflow) - - // Now restore the layout - // Subsequent added nodes will add the UI data to layoutState - layoutState.deserialize(data.layout, this.lGraph) + const workflow = workflowState.openWorkflow(this.lCanvas, data); // Restore canvas offset/zoom this.lCanvas.deserialize(data.canvas) - await this.refreshComboInNodes(); + if (refreshCombos) { + let defs = null; + if (typeof refreshCombos === "object") + defs = refreshCombos; + await this.refreshComboInNodes(workflow, defs); + } - this.lGraph.start(); - this.lGraph.eventBus.on("afterExecute", () => this.lCanvas.draw(true)) + return workflow; } - async initDefaultGraph() { + setActiveWorkflow(id: WorkflowInstID) { + const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id) + if (index === -1) + return; + workflowState.setActiveWorkflow(this.lCanvas, index); + selectionState.clear(); + } + + createNewWorkflow() { + workflowState.createNewWorkflow(this.lCanvas, undefined, true); + selectionState.clear(); + } + + closeWorkflow(id: WorkflowInstID) { + const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id) + if (index === -1) + return; + workflowState.closeWorkflow(this.lCanvas, index); + selectionState.clear(); + } + + async initDefaultWorkflow(defs?: Record) { let state = null; try { const graphResponse = await fetch("/workflows/defaultWorkflow.json"); @@ -509,50 +569,14 @@ export default class ComfyApp { notify(`Failed to load default graph: ${error}`, { type: "error" }) state = structuredClone(blankGraph) } - await this.deserialize(state) - } - - /** - * Populates the graph with the specified workflow data - * @param {*} graphData A serialized graph object - */ - loadGraphData(graphData: SerializedLGraph) { - this.clean(); - - // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now - for (let n of graphData.nodes) { - if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; - } - - this.lGraph.configure(graphData); - - for (const node of this.lGraph._nodes) { - const size = node.computeSize(); - size[0] = Math.max(node.size[0], size[0]); - size[1] = Math.max(node.size[1], size[1]); - node.size = size; - // this.#invokeExtensions("loadedGraphNode", node); - } + await this.openWorkflow(state, defs) } clear() { this.clean(); - const blankGraph: SerializedLGraph = { - last_node_id: 0, - last_link_id: 0, - nodes: [], - links: [], - groups: [], - config: {}, - extra: {}, - version: 0 - } - - layoutState.onStartConfigure(); this.lCanvas.closeAllSubgraphs(); - this.lGraph.configure(blankGraph) - layoutState.initDefaultLayout(); + workflowState.closeAllWorkflows(this.lCanvas); uiState.update(s => { s.uiUnlocked = true; s.uiEditMode = "widgets"; @@ -561,13 +585,17 @@ export default class ComfyApp { } runDefaultQueueAction() { - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + const workflow = workflowState.getActiveWorkflow(); + if (workflow == null) + return; + + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if ("onDefaultQueueAction" in node) { (node as ComfyGraphNode).onDefaultQueueAction() } } - if (get(layoutState).attrs.queuePromptButtonRunWorkflow) { + if (workflow.attrs.queuePromptButtonRunWorkflow) { // Hold control to queue at the front const num = this.ctrlDown ? -1 : 0; this.queuePrompt(num, 1); @@ -575,6 +603,12 @@ export default class ComfyApp { } querySave() { + const workflow = workflowState.getActiveWorkflow(); + if (workflow == null) { + notify("No active workflow!", { type: "error" }) + return; + } + const promptFilename = get(configState).promptForWorkflowName; let filename = "workflow.json"; @@ -592,10 +626,13 @@ export default class ComfyApp { } const indent = 2 - const json = JSON.stringify(this.serialize(), null, indent) + const json = JSON.stringify(this.serialize(workflow), null, indent) download(filename, json, "application/json") + workflow.isModified = false; + workflowState.set(get(workflowState)); + console.debug(jsonToJsObject(json)) } @@ -603,12 +640,18 @@ export default class ComfyApp { * Converts the current graph workflow for sending to the API * @returns The workflow and node links */ - graphToPrompt(tag: string | null = null): SerializedPrompt { - return this.promptSerializer.serialize(this.lGraph, tag) + graphToPrompt(workflow: ComfyWorkflow, tag: string | null = null): SerializedPrompt { + return this.promptSerializer.serialize(workflow.graph, tag) } async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) { - this.queueItems.push({ num, batchCount }); + const activeWorkflow = workflowState.getActiveWorkflow(); + if (activeWorkflow == null) { + notify("No workflow is opened!", { type: "error" }) + return; + } + + this.queueItems.push({ num, batchCount, workflow: activeWorkflow }); // Only have one action process the items so each one gets a unique seed correctly if (this.processingQueue) { @@ -619,13 +662,15 @@ export default class ComfyApp { tag = null; this.processingQueue = true; + let workflow; + try { while (this.queueItems.length) { - ({ num, batchCount } = this.queueItems.pop()); + ({ num, batchCount, workflow } = this.queueItems.pop()); console.debug(`Queue get! ${num} ${batchCount} ${tag}`); const thumbnails = [] - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if (node.mode !== NodeMode.ALWAYS || (tag != null && Array.isArray(node.properties.tags) @@ -640,7 +685,7 @@ export default class ComfyApp { } for (let i = 0; i < batchCount; i++) { - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if (node.mode !== NodeMode.ALWAYS) continue; @@ -649,9 +694,9 @@ export default class ComfyApp { } } - const p = this.graphToPrompt(tag); - const l = layoutState.serialize(); - console.debug(graphToGraphVis(this.lGraph)) + const p = this.graphToPrompt(workflow, tag); + const l = workflow.layout.serialize(); + console.debug(graphToGraphVis(workflow.graph)) console.debug(promptToGraphVis(p)) const stdPrompt = this.stdPromptSerializer.serialize(p); @@ -681,7 +726,7 @@ export default class ComfyApp { error = response.error; } else { - queueState.afterQueued(response.promptID, num, p.output, extraData) + queueState.afterQueued(workflow.id, response.promptID, num, p.output, extraData) } } catch (err) { error = err?.toString(); @@ -690,13 +735,13 @@ export default class ComfyApp { if (error != null) { const mes: string = error; notify(`Error queuing prompt:\n${mes}`, { type: "error" }) - console.error(graphToGraphVis(this.lGraph)) + console.error(graphToGraphVis(workflow.graph)) console.error(promptToGraphVis(p)) console.error("Error queuing prompt", error, num, p) break; } - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if ("afterQueued" in node) { (node as ComfyGraphNode).afterQueued(p, tag); } @@ -719,7 +764,7 @@ export default class ComfyApp { const pngInfo = await getPngMetadata(file); if (pngInfo) { if (pngInfo.comfyBoxConfig) { - this.deserialize(JSON.parse(pngInfo.comfyBoxConfig)); + await this.openWorkflow(JSON.parse(pngInfo.comfyBoxConfig)); } else if (pngInfo.parameters) { const parsed = parseA1111(pngInfo.parameters) if ("error" in parsed) { @@ -741,8 +786,8 @@ export default class ComfyApp { } } else if (file.type === "application/json" || file.name.endsWith(".json")) { const reader = new FileReader(); - reader.onload = () => { - this.deserialize(JSON.parse(reader.result as string)); + reader.onload = async () => { + await this.openWorkflow(JSON.parse(reader.result as string)); }; reader.readAsText(file); } @@ -761,8 +806,15 @@ export default class ComfyApp { /** * Refresh combo list on whole nodes */ - async refreshComboInNodes(flashUI: boolean = false) { - const defs = await this.api.getNodeDefs(); + async refreshComboInNodes(workflow?: ComfyWorkflow, defs?: Record, flashUI: boolean = false) { + workflow ||= workflowState.getActiveWorkflow(); + if (workflow == null) { + notify("No active workflow!", { type: "error" }) + return + } + + if (defs == null) + defs = await this.api.getNodeDefs(); const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => { return node @@ -805,7 +857,7 @@ export default class ComfyApp { return result } - for (const node of this.lGraph.iterateNodesInOrderRecursive()) { + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { if (!isActiveBackendNode(node)) continue; @@ -827,7 +879,7 @@ export default class ComfyApp { console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos) // Mark combo nodes without backend configs as being loaded already. - for (const node of this.lGraph.iterateNodesOfClassRecursive(ComfyComboNode)) { + for (const node of workflow.graph.iterateNodesOfClassRecursive(ComfyComboNode)) { if (backendUpdatedCombos[node.id] != null) { continue; } @@ -859,12 +911,15 @@ export default class ComfyApp { // Load definitions from the backend. for (const { comboNode, comfyInput, backendNode } of Object.values(backendUpdatedCombos)) { const def = defs[backendNode.type]; - const rawValues = def["input"]["required"][comfyInput.name][0]; + const [rawValues, opts] = def.input.required[comfyInput.name]; console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length) comboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false }) - comboNode.formatValues(rawValues as string[], true) + const values = rawValues as string[] + const defaultValue = rawValues[0]; + + comboNode.formatValues(values, defaultValue, true) if (!rawValues?.includes(get(comboNode.value))) { comboNode.setValue(rawValues[0], comfyInput.config.defaultValue) } @@ -875,7 +930,6 @@ export default class ComfyApp { * Clean current state */ clean() { - this.nodeOutputs = {}; this.a1111Prompt.set(null); } } diff --git a/src/lib/components/ComfyPromptSerializer.ts b/src/lib/components/ComfyPromptSerializer.ts index de55bf8..1f88a33 100644 --- a/src/lib/components/ComfyPromptSerializer.ts +++ b/src/lib/components/ComfyPromptSerializer.ts @@ -35,7 +35,18 @@ export function isActiveBackendNode(node: LGraphNode, tag: string | null = null) if (!(node as any).isBackendNode) return false; - return isActiveNode(node, tag); + if (!isActiveNode(node, tag)) + return false; + + // Make sure this node is not contained in an inactive subgraph, even if the + // node itself is considered active + if (node.graph._is_subgraph) { + const isInsideDisabledSubgraph = Array.from(node.iterateParentSubgraphNodes()).some(n => !isActiveNode(n, tag)) + if (isInsideDisabledSubgraph) + return false; + } + + return true; } export class UpstreamNodeLocator { @@ -166,7 +177,7 @@ export default class ComfyPromptSerializer { // We don't check tags for non-backend nodes. // Just check for node inactivity (so you can toggle groups of // tagged frontend nodes on/off) - if (inputNode && inputNode.mode === NodeMode.NEVER) { + if (inputNode && inputNode.mode !== NodeMode.ALWAYS) { console.debug("Skipping inactive node", inputNode) continue; } @@ -248,6 +259,8 @@ export default class ComfyPromptSerializer { const inputs = this.serializeInputValues(node); const links = this.serializeBackendLinks(node, tag); + console.warn("OUTPUT", node.id, node.comfyClass, node.mode) + output[String(node.id)] = { inputs: { ...inputs, ...links }, class_type: node.comfyClass, diff --git a/src/lib/components/ComfyProperties.svelte b/src/lib/components/ComfyProperties.svelte index c3efcb4..384684e 100644 --- a/src/lib/components/ComfyProperties.svelte +++ b/src/lib/components/ComfyProperties.svelte @@ -2,43 +2,50 @@ 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 { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec, type WritableLayoutStateStore } from "$lib/stores/layoutStates" import uiState from "$lib/stores/uiState" + import workflowState from "$lib/stores/workflowState" + import layoutStates from "$lib/stores/layoutStates" import selectionState from "$lib/stores/selectionState" import { get, type Writable, writable } from "svelte/store" import ComfyNumberProperty from "./ComfyNumberProperty.svelte"; import ComfyComboProperty from "./ComfyComboProperty.svelte"; - import type { ComfyWidgetNode } from "$lib/nodes/widgets"; + import type { ComfyWidgetNode } from "$lib/nodes/widgets"; + import type { ComfyWorkflow } from "$lib/stores/workflowState"; + + export let workflow: ComfyWorkflow | null; + + let layoutState: WritableLayoutStateStore | null = null + + $: layoutState = workflow?.layout let target: IDragItem | null = null; let node: LGraphNode | null = null; - let attrsChanged: Writable | null = null; - let refreshPropsPanel: Writable | null - - $: refreshPropsPanel = $layoutState.refreshPropsPanel; - - $: if ($selectionState.currentSelection.length > 0) { - node = null; - const targetId = $selectionState.currentSelection.slice(-1)[0] - const entry = $layoutState.allItems[targetId] - if (entry != null) { - target = entry.dragItem - attrsChanged = target.attrsChanged; - if (target.type === "widget") { - node = (target as WidgetLayout).node + $: if (layoutState) { + if ($selectionState.currentSelection.length > 0) { + node = null; + const targetId = $selectionState.currentSelection.slice(-1)[0] + const entry = $layoutState.allItems[targetId] + if (entry != null) { + target = entry.dragItem + if (target.type === "widget") { + node = (target as WidgetLayout).node + } } } - } - else if ($selectionState.currentSelectionNodes.length > 0) { - target = null; - node = $selectionState.currentSelectionNodes[0] - attrsChanged = null; + else if ($selectionState.currentSelectionNodes.length > 0) { + target = null; + node = $selectionState.currentSelectionNodes[0] + } + else { + target = null + node = null; + } } else { - target = null + target = null; node = null; - attrsChanged = null; } $: if (target) { @@ -55,7 +62,7 @@ let value = spec.defaultValue; target.attrs[spec.name] = value; if (spec.refreshPanelOnChange) - $refreshPropsPanel += 1; + doRefreshPanel(); } } } @@ -123,7 +130,10 @@ if (spec.location !== "workflow") return false; - return spec.name in $layoutState.attrs + if (workflow == null) + return false; + + return spec.name in workflow.attrs } function getAttribute(target: IDragItem, spec: AttributesSpec): any { @@ -162,6 +172,8 @@ if (spec.refreshPanelOnChange) { doRefreshPanel() } + + workflow.notifyModified() } function getProperty(node: LGraphNode, spec: AttributesSpec) { @@ -197,6 +209,8 @@ if (spec.refreshPanelOnChange) doRefreshPanel() + + workflow.notifyModified() } function getVar(node: LGraphNode, spec: AttributesSpec) { @@ -233,10 +247,15 @@ if (spec.refreshPanelOnChange) { doRefreshPanel() } + + workflow.notifyModified(); } function getWorkflowAttribute(spec: AttributesSpec): any { - let value = $layoutState.attrs[spec.name] + if (workflow == null) + throw new Error("Active workflow is null!"); + + let value = workflow.attrs[spec.name] if (value == null) value = spec.defaultValue else if (spec.serialize) @@ -249,23 +268,28 @@ if (!spec.editable) return; + if (workflow == null) + throw new Error("Active workflow is null!"); + const name = spec.name // console.warn("[ComfyProperties] updateWorkflowAttribute", name, value) const prevValue = value - $layoutState.attrs[name] = value - $layoutState = $layoutState + workflow.attrs[name] = value + $workflowState = $workflowState; if (spec.onChanged) spec.onChanged($layoutState, value, prevValue) if (spec.refreshPanelOnChange) doRefreshPanel() + + workflow.notifyModified(); } function doRefreshPanel() { console.warn("[ComfyProperties] doRefreshPanel") - $refreshPropsPanel += 1; + $layoutStates.refreshPropsPanel += 1; } @@ -278,176 +302,180 @@ ({targetType}) {/if} - +
- {#key $refreshPropsPanel} - {#each ALL_ATTRIBUTES as category(category.categoryName)} -
- - {category.categoryName} - -
- {#each category.specs as spec(spec.id)} - {#if validWidgetAttribute(spec, target)} -
- {#if spec.type === "string"} - updateAttribute(spec, target, e.detail)} - on:input={(e) => updateAttribute(spec, target, e.detail)} - disabled={!$uiState.uiUnlocked || !spec.editable} - label={spec.name} - max_lines={spec.multiline ? 5 : 1} - /> - {:else if spec.type === "boolean"} - updateAttribute(spec, target, e.detail)} - disabled={!$uiState.uiUnlocked || !spec.editable} - label={spec.name} - /> - {:else if spec.type === "number"} - updateAttribute(spec, target, e.detail)} - /> - {:else if spec.type === "enum"} - updateAttribute(spec, target, e.detail)} - /> - {/if} + {#if workflow != null && layoutState != null} + {#key workflow.id} + {#key $layoutStates.refreshPropsPanel} + {#each ALL_ATTRIBUTES as category(category.categoryName)} +
+ + {category.categoryName} +
- {:else if node} - {#if validNodeProperty(spec, node)} -
- {#if spec.type === "string"} - updateProperty(spec, e.detail)} - on:input={(e) => updateProperty(spec, e.detail)} - label={spec.name} - disabled={!$uiState.uiUnlocked || !spec.editable} - max_lines={spec.multiline ? 5 : 1} - /> - {:else if spec.type === "boolean"} - + {#if spec.type === "string"} + updateAttribute(spec, target, e.detail)} + on:input={(e) => updateAttribute(spec, target, e.detail)} disabled={!$uiState.uiUnlocked || !spec.editable} - on:change={(e) => updateProperty(spec, e.detail)} + label={spec.name} + max_lines={spec.multiline ? 5 : 1} /> - {:else if spec.type === "number"} - updateAttribute(spec, target, e.detail)} disabled={!$uiState.uiUnlocked || !spec.editable} - on:change={(e) => updateProperty(spec, e.detail)} + label={spec.name} /> - {:else if spec.type === "enum"} - updateAttribute(spec, target, e.detail)} + /> + {:else if spec.type === "enum"} + updateAttribute(spec, target, e.detail)} + /> + {/if} +
+ {:else if node} + {#if validNodeProperty(spec, node)} +
+ {#if spec.type === "string"} + updateProperty(spec, e.detail)} + on:input={(e) => updateProperty(spec, e.detail)} + label={spec.name} + disabled={!$uiState.uiUnlocked || !spec.editable} + max_lines={spec.multiline ? 5 : 1} + /> + {:else if spec.type === "boolean"} + updateProperty(spec, e.detail)} /> - {/if} -
- {:else if validNodeVar(spec, node)} -
- {#if spec.type === "string"} - updateVar(spec, e.detail)} - on:input={(e) => updateVar(spec, e.detail)} - label={spec.name} - disabled={!$uiState.uiUnlocked || !spec.editable} - max_lines={spec.multiline ? 5 : 1} - /> - {:else if spec.type === "boolean"} - updateVar(spec, e.detail)} - disabled={!$uiState.uiUnlocked || !spec.editable} - label={spec.name} - /> - {:else if spec.type === "number"} - updateProperty(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateProperty(spec, e.detail)} + /> + {/if} +
+ {:else if validNodeVar(spec, node)} +
+ {#if spec.type === "string"} + updateVar(spec, e.detail)} - /> - {:else if spec.type === "enum"} - updateVar(spec, e.detail)} - /> - {/if} -
- {/if} - {:else if !node && !target && validWorkflowAttribute(spec)} -
- {#if spec.type === "string"} - updateWorkflowAttribute(spec, e.detail)} - on:input={(e) => updateWorkflowAttribute(spec, e.detail)} - label={spec.name} - disabled={!$uiState.uiUnlocked || !spec.editable} - max_lines={spec.multiline ? 5 : 1} - /> - {:else if spec.type === "boolean"} - updateWorkflowAttribute(spec, e.detail)} - disabled={!$uiState.uiUnlocked || !spec.editable} - label={spec.name} - /> - {:else if spec.type === "number"} - updateWorkflowAttribute(spec, e.detail)} - /> - {:else if spec.type === "enum"} - updateVar(spec, e.detail)} + label={spec.name} disabled={!$uiState.uiUnlocked || !spec.editable} - on:change={(e) => updateWorkflowAttribute(spec, e.detail)} + max_lines={spec.multiline ? 5 : 1} /> + {:else if spec.type === "boolean"} + updateVar(spec, e.detail)} + disabled={!$uiState.uiUnlocked || !spec.editable} + label={spec.name} + /> + {:else if spec.type === "number"} + updateVar(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateVar(spec, e.detail)} + /> + {/if} +
+ {/if} + {:else if !node && !target && validWorkflowAttribute(spec)} +
+ {#if spec.type === "string"} + updateWorkflowAttribute(spec, e.detail)} + on:input={(e) => updateWorkflowAttribute(spec, e.detail)} + label={spec.name} + disabled={!$uiState.uiUnlocked || !spec.editable} + max_lines={spec.multiline ? 5 : 1} + /> + {:else if spec.type === "boolean"} + updateWorkflowAttribute(spec, e.detail)} + disabled={!$uiState.uiUnlocked || !spec.editable} + label={spec.name} + /> + {:else if spec.type === "number"} + updateWorkflowAttribute(spec, e.detail)} + /> + {:else if spec.type === "enum"} + updateWorkflowAttribute(spec, e.detail)} + /> + {/if} +
{/if} -
- {/if} - {/each} - {/each} - {/key} + {/each} + {/each} + {/key} + {/key} + {/if}
diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index edaaaee..bdf0b2b 100644 --- a/src/lib/components/ComfyQueue.svelte +++ b/src/lib/components/ComfyQueue.svelte @@ -13,6 +13,7 @@ import { tick } from "svelte"; import Modal from "./Modal.svelte"; import DropZone from "./DropZone.svelte"; + import workflowState from "$lib/stores/workflowState"; export let app: ComfyApp; @@ -71,10 +72,17 @@ const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs; let message = "Prompt"; - if (subgraphs?.length > 0) - message = `Prompt: ${subgraphs.join(', ')}` + if (entry.workflowID != null) { + const workflow = workflowState.getWorkflow(entry.workflowID); + if (workflow != null && workflow.attrs.title) { + message = `Workflow: ${workflow.attrs.title}` + } + if (subgraphs?.length > 0) + message += ` (${subgraphs.join(', ')})` + } let submessage = `Nodes: ${Object.keys(entry.prompt).length}` + if (Object.keys(entry.outputs).length > 0) { const imageCount = Object.values(entry.outputs).flatMap(o => o.images).length submessage = `Images: ${imageCount}` @@ -84,7 +92,7 @@ entry, message, submessage, - dateStr, + date: dateStr, status: "pending", images: [] } @@ -387,7 +395,7 @@ &.all_cached, &.interrupted { filter: brightness(80%); - color: var(--neutral-300); + color: var(--comfy-accent-soft); } } diff --git a/src/lib/components/ComfyUIPane.svelte b/src/lib/components/ComfyWorkflowView.svelte similarity index 85% rename from src/lib/components/ComfyUIPane.svelte rename to src/lib/components/ComfyWorkflowView.svelte index b4a2bc8..2338857 100644 --- a/src/lib/components/ComfyUIPane.svelte +++ b/src/lib/components/ComfyWorkflowView.svelte @@ -1,34 +1,23 @@ -
- -
+{#if layoutState != null} +
+ +
+{/if} {#if showMenu} @@ -188,7 +179,7 @@ {/if} diff --git a/src/lib/components/Container.svelte b/src/lib/components/Container.svelte index 40dda45..3f798a1 100644 --- a/src/lib/components/Container.svelte +++ b/src/lib/components/Container.svelte @@ -6,10 +6,11 @@ import TabsContainer from "./TabsContainer.svelte" // notice - fade in works fine but don't add svelte's fade-out (known issue) - import { type ContainerLayout } from "$lib/stores/layoutState"; + import { type ContainerLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates"; import type { Writable } from "svelte/store"; import { isHidden } from "$lib/widgets/utils"; + export let layoutState: WritableLayoutStateStore; export let container: ContainerLayout | null = null; export let zIndex: number = 0; export let classes: string[] = []; @@ -31,11 +32,11 @@ {#key $attrsChanged} {#if edit || !isHidden(container)} {#if container.attrs.variant === "tabs"} - + {:else if container.attrs.variant === "accordion"} - + {:else} - + {/if} {/if} {/key} diff --git a/src/lib/components/PromptDisplay.svelte b/src/lib/components/PromptDisplay.svelte index 9bb539e..a11e245 100644 --- a/src/lib/components/PromptDisplay.svelte +++ b/src/lib/components/PromptDisplay.svelte @@ -162,6 +162,10 @@ position: relative; flex: 1 1 0%; max-width: 30vw; + + > :global(.block) { + height: 100%; + } } .copy-button { diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..13dd99e --- /dev/null +++ b/src/lib/components/Sidebar.svelte @@ -0,0 +1,154 @@ + + + + +