diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 295f112..bc2331a 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -45,25 +45,49 @@ if (typeof window !== "undefined") { nodes.ComfyReroute.setDefaultTextVisibility(!!localStorage["Comfy.ComfyReroute.DefaultVisibility"]); } -type QueueItem = { num: number, batchCount: number } +/* + * 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 = { + num: number, + batchCount: number +} +/* + * Represents a single workflow that can be loaded into the program from JSON. + */ export type SerializedAppState = { + /** Program identifier, should always be "ComfyBox" */ createdBy: "ComfyBox", + /** Serial version, should be incremented on breaking changes */ version: number, + /** Commit hash if found */ + commitHash?: string, + /** Graph state */ workflow: SerializedLGraph, + /** UI state */ layout: SerializedLayoutState, + /** Position/offset of the canvas at the time of saving */ canvas: SerializedGraphCanvasState } -/** [link origin, link index] | value */ +/** [link_origin, link_slot_index] | input_value */ export type SerializedPromptInput = [ComfyNodeID, number] | any +/* + * A single node in the prompt and its input values. + */ export type SerializedPromptInputs = { /* property name -> value or link */ inputs: Record, class_type: string } +/* + * All nodes in the graph and their input values. + */ export type SerializedPromptInputsAll = Record export type SerializedPrompt = { @@ -71,6 +95,9 @@ export type SerializedPrompt = { output: SerializedPromptInputsAll } +/* + * Outputs for each node. + */ export type SerializedPromptOutputs = Record export type Progress = { @@ -78,6 +105,10 @@ export type Progress = { max: number } +/* + * A combo node and the backend node that will send an updated config over, for + * refreshing lists of model files + */ type BackendComboNode = { comboNode: ComfyComboNode, comfyInput: IComfyInputSlot, @@ -95,6 +126,7 @@ export default class ComfyApp { nodeOutputs: Record = {}; shiftDown: boolean = false; + ctrlDown: boolean = false; selectedGroupMoving: boolean = false; private queueItems: QueueItem[] = []; @@ -406,6 +438,7 @@ export default class ComfyApp { private addKeyboardHandler() { window.addEventListener("keydown", (e) => { this.shiftDown = e.shiftKey; + this.ctrlDown = e.ctrlKey; // Queue prompt using ctrl or command + enter if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) { @@ -414,6 +447,7 @@ export default class ComfyApp { }); window.addEventListener("keyup", (e) => { this.shiftDown = e.shiftKey; + this.ctrlDown = e.ctrlKey; }); } @@ -561,7 +595,9 @@ export default class ComfyApp { } if (get(layoutState).attrs.queuePromptButtonRunWorkflow) { - this.queuePrompt(0, 1); + // Hold control to queue at the front + const num = this.ctrlDown ? -1 : 0; + this.queuePrompt(num, 1); } } diff --git a/src/lib/components/ComfyPromptSerializer.ts b/src/lib/components/ComfyPromptSerializer.ts index 878504d..253d40d 100644 --- a/src/lib/components/ComfyPromptSerializer.ts +++ b/src/lib/components/ComfyPromptSerializer.ts @@ -113,7 +113,7 @@ export class UpstreamNodeLocator { } // If there are non-target nodes between us and another - // backend node, we have to traverse them first. This + // target node, we have to traverse them first. This // behavior is dependent on the type of node. Reroute nodes // will simply follow their single input, while branching // nodes have conditional logic that determines which link diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts index 2da838a..9e083a3 100644 --- a/src/lib/nodes/ComfyActionNodes.ts +++ b/src/lib/nodes/ComfyActionNodes.ts @@ -312,7 +312,9 @@ export class ComfyExecuteSubgraphAction extends ComfyGraphNode { if (!app) return; - app.queuePrompt(0, 1, tag); + // Hold control to queue at the front + const num = app.ctrlDown ? -1 : 0; + app.queuePrompt(num, 1, tag); } } diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts index e3568c6..82b7c42 100644 --- a/src/lib/stores/layoutState.ts +++ b/src/lib/stores/layoutState.ts @@ -39,8 +39,10 @@ export type LayoutAttributes = { queuePromptButtonName: string, /* - * If true, clicking the "Queue Prompt" button will run the default subgraph. - * Set this to false if you need special behavior before running any subgraphs. + * If true, clicking the "Queue Prompt" button will run the default + * subgraph. Set this to false if you need special behavior before running + * any subgraphs, and instead use the `onDefaultQueueAction` event of the + * Comfy.QueueEvents node. */ queuePromptButtonRunWorkflow: boolean, } @@ -84,6 +86,9 @@ export type LayoutState = { */ attrs: LayoutAttributes + /* + * Increment to force Svelte to re-render the props panel + */ refreshPropsPanel: Writable } @@ -102,7 +107,7 @@ export type Attributes = { title: string, /* - * List of classes to apply to the component. + * List of CSS classes to apply to the component. */ classes: string, @@ -204,22 +209,22 @@ export type AttributesSpec = { values?: string[], /* - * If `type` is "number", step for the slider + * If `type` is "number", step for the slider that edits this attribute */ step?: number, /* - * If `type` is "number", min for the slider + * If `type` is "number", min for the slider that edits this attribute */ min?: number, /* - * If `type` is "number", max for the slider + * If `type` is "number", max for the slider that edits this attribute */ max?: number, /* - * If `type` is "string", display as a textarea. + * If `type` is "string", display as a textarea instead of an input. */ multiline?: boolean, @@ -863,8 +868,9 @@ function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { let prevWidget = state.allItemsByNode[node.id] if (prevWidget == null) { // If a subgraph was cloned, try looking for the original widget node corresponding to the new widget node being added. - // `node` is the new ComfyWidgetNode instance to copy attrs to. - // `options.cloneData` should contain the results of Subgraph.clone(), called "subgraphNewIDMapping". + // `node` is the new ComfyWidgetNode instance to copy layout attrs to. + // `options.cloneData` should contain the results of Subgraph.clone(), which is named "subgraphNewIDMapping" in an + // entry of the `forNode` Record. // `options.cloneData` is attached to the onNodeAdded options if a node is added to a graph after being // selection-cloned or pasted, as they both call clone() internally. const cloneData = options.cloneData.forNode[options.prevNodeID] @@ -879,7 +885,7 @@ function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { if (nodeIDInLayoutState) { // Gottem. prevWidget = state.allItemsByNode[nodeIDInLayoutState] - console.warn("FOUND CLONED SUBGRAPH NODE", node.id, "=>", nodeIDInLayoutState, prevWidget) + // console.warn("FOUND CLONED SUBGRAPH NODE", node.id, "=>", nodeIDInLayoutState, prevWidget) } } } diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts index bcc2b6c..ebe8a3d 100644 --- a/src/lib/stores/queueState.ts +++ b/src/lib/stores/queueState.ts @@ -4,10 +4,6 @@ import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes"; import notify from "$lib/notify"; import { get, writable, type Writable } from "svelte/store"; -export type QueueItem = { - name: string -} - export type QueueEntryStatus = "success" | "error" | "interrupted" | "all_cached" | "unknown"; type QueueStateOps = { @@ -23,8 +19,13 @@ type QueueStateOps = { onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void } +/* + * Single job that the backend keeps track of. + */ export type QueueEntry = { - /* Data preserved on page refresh */ + /*** Data preserved on page refresh ***/ + + /** Priority of the prompt. -1 means to queue at the front. */ number: number, queuedAt?: Date, finishedAt?: Date, @@ -33,23 +34,34 @@ export type QueueEntry = { extraData: ComfyBoxPromptExtraData, goodOutputs: ComfyNodeID[], - /* Data not sent by ComfyUI's API, lost on page refresh */ + /*** Data not sent by ComfyUI's API, lost on page refresh ***/ /* Prompt outputs, collected while the prompt is still executing */ outputs: SerializedPromptOutputs, - - /* Nodes in of the workflow that have finished running so far. */ + /* Nodes of the workflow that have finished running so far. */ nodesRan: Set, + /* Nodes of the workflow the backend reported as cached. */ cachedNodes: Set } +/* + * Represents a queue entry that has finished executing (suceeded or failed) and + * has been moved to the history. + */ export type CompletedQueueEntry = { + /** Corresponding entry in the queue, for the prompt/extra data */ entry: QueueEntry, + /** The result of this prompt, success/failed/cached */ status: QueueEntryStatus, + /** Message to display in the frontend */ message?: string, + /** Detailed error/stacktrace, perhaps inspectible with a popup */ error?: string, } +/* + * Keeps track of queued and completed (history) prompts. + */ export type QueueState = { queueRunning: Writable, queuePending: Writable, @@ -57,6 +69,11 @@ export type QueueState = { queueRemaining: number | "X" | null; runningNodeID: ComfyNodeID | null; progress: Progress | null, + /** + * If true, user pressed the "Interrupt" button in the frontend. Disable the + * button and wait until the next prompt starts running to re-enable it + * again + */ isInterrupting: boolean } type WritableQueueStateStore = Writable & QueueStateOps; @@ -159,6 +176,7 @@ function moveToCompleted(index: number, queue: Writable, status: Q return qc }) + state.isInterrupting = false; store.set(state) }