Merge pull request #55 from space-nuko/multiple-workflows
Multiple workflows
This commit is contained in:
@@ -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.
|
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
|
## Features
|
||||||
- *No-Code UI Builder* - A novel system for creating your own Stable Diffusion user interfaces from the basic components.
|
- *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.
|
- *Extension Support* - All custom ComfyUI nodes are supported out of the box.
|
||||||
|
|||||||
Submodule litegraph updated: cd4f68ef42...42adb8dba1
@@ -1,6 +1,11 @@
|
|||||||
{
|
{
|
||||||
"createdBy": "ComfyBox",
|
"createdBy": "ComfyBox",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
"attrs": {
|
||||||
|
"title": "Default",
|
||||||
|
"queuePromptButtonName": "Queue txt2img",
|
||||||
|
"queuePromptButtonRunWorkflow": false
|
||||||
|
},
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"last_node_id": 0,
|
"last_node_id": 0,
|
||||||
"last_link_id": 0,
|
"last_link_id": 0,
|
||||||
@@ -25709,10 +25714,6 @@
|
|||||||
],
|
],
|
||||||
"parent": "eae32e42-1ccc-4a4a-923f-7ab4ccdac97a"
|
"parent": "eae32e42-1ccc-4a4a-923f-7ab4ccdac97a"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"attrs": {
|
|
||||||
"queuePromptButtonName": "Queue txt2img",
|
|
||||||
"queuePromptButtonRunWorkflow": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"canvas": {
|
"canvas": {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, Li
|
|||||||
import GraphSync from "./GraphSync";
|
import GraphSync from "./GraphSync";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
import layoutState from "./stores/layoutState";
|
|
||||||
import uiState from "./stores/uiState";
|
import uiState from "./stores/uiState";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
||||||
@@ -10,6 +9,11 @@ import type IComfyInputSlot from "./IComfyInputSlot";
|
|||||||
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
|
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
|
||||||
import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets";
|
import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets";
|
||||||
import selectionState from "./stores/selectionState";
|
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 = {
|
type ComfyGraphEvents = {
|
||||||
configured: (graph: LGraph) => void
|
configured: (graph: LGraph) => void
|
||||||
@@ -25,11 +29,28 @@ type ComfyGraphEvents = {
|
|||||||
export default class ComfyGraph extends LGraph {
|
export default class ComfyGraph extends LGraph {
|
||||||
eventBus: TypedEmitter<ComfyGraphEvents> = new EventEmitter() as TypedEmitter<ComfyGraphEvents>;
|
eventBus: TypedEmitter<ComfyGraphEvents> = new EventEmitter() as TypedEmitter<ComfyGraphEvents>;
|
||||||
|
|
||||||
|
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() {
|
override onConfigure() {
|
||||||
console.debug("Configured");
|
console.debug("Configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
override onBeforeChange(graph: LGraph, info: any) {
|
override onBeforeChange(graph: LGraph, info: any) {
|
||||||
|
if (this.workflow != null)
|
||||||
|
this.workflow.notifyModified()
|
||||||
|
|
||||||
console.debug("BeforeChange", info);
|
console.debug("BeforeChange", info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,25 +71,33 @@ export default class ComfyGraph extends LGraph {
|
|||||||
override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||||
// Don't add nodes in subgraphs until this callback reaches the root
|
// Don't add nodes in subgraphs until this callback reaches the root
|
||||||
// graph
|
// graph
|
||||||
if (node.getRootGraph() == null || this._is_subgraph)
|
// Only root graphs will have a workflow ID, so we don't mind subgraphs
|
||||||
return;
|
// 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);
|
this.eventBus.emit("nodeAdded", node);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add widget UI/groups for newly added nodes.
|
* 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)
|
layoutState.nodeAdded(node, options)
|
||||||
|
|
||||||
// All nodes whether they come from base litegraph or ComfyBox should
|
// All nodes whether they come from base litegraph or ComfyBox should
|
||||||
// have tags added to them. Can't override serialization for existing
|
// have tags added to them. Can't override serialization for litegraph's
|
||||||
// node types to add `tags` as a new field so putting it in properties
|
// base node types to add `tags` as a new field so putting it in
|
||||||
// is better.
|
// properties is better.
|
||||||
if (node.properties.tags == null)
|
if (node.properties.tags == null)
|
||||||
node.properties.tags = []
|
node.properties.tags = []
|
||||||
|
|
||||||
@@ -104,7 +133,6 @@ export default class ComfyGraph extends LGraph {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (get(uiState).autoAddUI) {
|
if (get(uiState).autoAddUI) {
|
||||||
console.warn("ADD", node.type, options)
|
|
||||||
if (!("svelteComponentType" in node) && options.addedBy == null) {
|
if (!("svelteComponentType" in node) && options.addedBy == null) {
|
||||||
console.debug("[ComfyGraph] AutoAdd UI")
|
console.debug("[ComfyGraph] AutoAdd UI")
|
||||||
const comfyNode = node as ComfyGraphNode;
|
const comfyNode = node as ComfyGraphNode;
|
||||||
@@ -144,28 +172,49 @@ export default class ComfyGraph extends LGraph {
|
|||||||
// ************** RECURSION ALERT ! **************
|
// ************** RECURSION ALERT ! **************
|
||||||
if (node.is(Subgraph)) {
|
if (node.is(Subgraph)) {
|
||||||
for (const child of node.subgraph.iterateNodesInOrder()) {
|
for (const child of node.subgraph.iterateNodesInOrder()) {
|
||||||
this.doAddNode(child, options)
|
this.doAddNode(child, layoutState, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ************** RECURSION ALERT ! **************
|
// ************** RECURSION ALERT ! **************
|
||||||
|
|
||||||
|
if (this.workflow != null)
|
||||||
|
this.workflow.notifyModified()
|
||||||
}
|
}
|
||||||
|
|
||||||
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||||
selectionState.clear(); // safest option
|
selectionState.clear(); // safest option
|
||||||
layoutState.nodeRemoved(node, options);
|
|
||||||
|
|
||||||
// Handle subgraphs being removed
|
if (!this._is_subgraph && this.workflowID != null) {
|
||||||
if (node.is(Subgraph)) {
|
const layoutState = get(layoutStates).all[this.workflowID]
|
||||||
for (const child of node.subgraph.iterateNodesInOrder()) {
|
if (layoutState === null) {
|
||||||
this.onNodeRemoved(child, options)
|
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);
|
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) {
|
override onNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) {
|
||||||
|
if (this.workflow != null)
|
||||||
|
this.workflow.notifyModified()
|
||||||
|
|
||||||
// console.debug("ConnectionChange", node);
|
// console.debug("ConnectionChange", node);
|
||||||
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import type ComfyApp from "./components/ComfyApp";
|
|||||||
import queueState from "./stores/queueState";
|
import queueState from "./stores/queueState";
|
||||||
import { get, type Unsubscriber } from "svelte/store";
|
import { get, type Unsubscriber } from "svelte/store";
|
||||||
import uiState from "./stores/uiState";
|
import uiState from "./stores/uiState";
|
||||||
import layoutState from "./stores/layoutState";
|
|
||||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
import { ComfyReroute } from "./nodes";
|
import { ComfyReroute } from "./nodes";
|
||||||
import type { Progress } from "./components/ComfyApp";
|
import type { Progress } from "./components/ComfyApp";
|
||||||
import selectionState from "./stores/selectionState";
|
import selectionState from "./stores/selectionState";
|
||||||
|
import type ComfyGraph from "./ComfyGraph";
|
||||||
|
import layoutStates from "./stores/layoutStates";
|
||||||
|
|
||||||
export type SerializedGraphCanvasState = {
|
export type SerializedGraphCanvasState = {
|
||||||
offset: Vector2,
|
offset: Vector2,
|
||||||
@@ -18,9 +19,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
app: ComfyApp | null;
|
app: ComfyApp | null;
|
||||||
private _unsubscribe: Unsubscriber;
|
private _unsubscribe: Unsubscriber;
|
||||||
|
|
||||||
|
get comfyGraph(): ComfyGraph | null {
|
||||||
|
return this.graph as ComfyGraph;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
app: ComfyApp,
|
app: ComfyApp,
|
||||||
canvas: HTMLCanvasElement | string,
|
canvas: HTMLCanvasElement | string,
|
||||||
|
graph?: ComfyGraph,
|
||||||
options: {
|
options: {
|
||||||
skip_render?: boolean;
|
skip_render?: boolean;
|
||||||
skip_events?: boolean;
|
skip_events?: boolean;
|
||||||
@@ -28,7 +34,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
viewport?: Vector4;
|
viewport?: Vector4;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
super(canvas, app.lGraph, options);
|
super(canvas, graph, options);
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this._unsubscribe = selectionState.subscribe(ss => {
|
this._unsubscribe = selectionState.subscribe(ss => {
|
||||||
for (const node of Object.values(this.selected_nodes)) {
|
for (const node of Object.values(this.selected_nodes)) {
|
||||||
@@ -281,11 +287,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
selectionState.update(ss => {
|
selectionState.update(ss => {
|
||||||
ss.currentSelectionNodes = Object.values(nodes)
|
ss.currentSelectionNodes = Object.values(nodes)
|
||||||
ss.currentSelection = []
|
ss.currentSelection = []
|
||||||
const ls = get(layoutState)
|
const layoutState = layoutStates.getLayoutByGraph(this.graph);
|
||||||
for (const node of ss.currentSelectionNodes) {
|
if (layoutState) {
|
||||||
const widget = ls.allItemsByNode[node.id]
|
const ls = get(layoutState)
|
||||||
if (widget)
|
for (const node of ss.currentSelectionNodes) {
|
||||||
ss.currentSelection.push(widget.dragItem.id)
|
const widget = ls.allItemsByNode[node.id]
|
||||||
|
if (widget)
|
||||||
|
ss.currentSelection.push(widget.dragItem.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ss
|
return ss
|
||||||
})
|
})
|
||||||
@@ -298,11 +307,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
ss.currentHoveredNodes.add(node.id)
|
ss.currentHoveredNodes.add(node.id)
|
||||||
}
|
}
|
||||||
ss.currentHovered.clear()
|
ss.currentHovered.clear()
|
||||||
const ls = get(layoutState)
|
const layoutState = layoutStates.getLayoutByGraph(this.graph);
|
||||||
for (const nodeID of ss.currentHoveredNodes) {
|
if (layoutState) {
|
||||||
const widget = ls.allItemsByNode[nodeID]
|
const ls = get(layoutState)
|
||||||
if (widget)
|
for (const nodeID of ss.currentHoveredNodes) {
|
||||||
ss.currentHovered.add(widget.dragItem.id)
|
const widget = ls.allItemsByNode[nodeID]
|
||||||
|
if (widget)
|
||||||
|
ss.currentHovered.add(widget.dragItem.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ss
|
return ss
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,18 @@ export type ComfyNodeDefInput = [ComfyNodeDefInputType, ComfyNodeDefInputOptions
|
|||||||
export type ComfyNodeDefInputType = any[] | keyof typeof ComfyWidgets | string
|
export type ComfyNodeDefInputType = any[] | keyof typeof ComfyWidgets | string
|
||||||
|
|
||||||
export type ComfyNodeDefInputOptions = {
|
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
|
// TODO if/when comfy refactors
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type TypedEmitter from "typed-emitter";
|
|||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type { ComfyImageLocation } from "$lib/utils";
|
import type { ComfyImageLocation } from "$lib/utils";
|
||||||
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
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";
|
import type { ComfyNodeDef } from "./ComfyNodeDef";
|
||||||
|
|
||||||
export type ComfyPromptRequest = {
|
export type ComfyPromptRequest = {
|
||||||
|
|||||||
@@ -11,11 +11,12 @@
|
|||||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||||
import {cubicIn} from 'svelte/easing';
|
import {cubicIn} from 'svelte/easing';
|
||||||
import { flip } from 'svelte/animate';
|
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 { startDrag, stopDrag } from "$lib/utils"
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
|
||||||
|
export let layoutState: WritableLayoutStateStore;
|
||||||
export let container: ContainerLayout | null = null;
|
export let container: ContainerLayout | null = null;
|
||||||
export let zIndex: number = 0;
|
export let zIndex: number = 0;
|
||||||
export let classes: string[] = [];
|
export let classes: string[] = [];
|
||||||
@@ -59,6 +60,14 @@
|
|||||||
navigator.vibrate(20)
|
navigator.vibrate(20)
|
||||||
$isOpen = e.detail
|
$isOpen = e.detail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
startDrag(e, layoutState)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _stopDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
stopDrag(e, layoutState)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container}
|
{#if container}
|
||||||
@@ -93,7 +102,7 @@
|
|||||||
animate:flip={{duration:flipDurationMs}}
|
animate:flip={{duration:flipDurationMs}}
|
||||||
style={item?.attrs?.style || ""}
|
style={item?.attrs?.style || ""}
|
||||||
>
|
>
|
||||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||||
{#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'/>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -101,10 +110,18 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if isHidden(container) && edit}
|
{#if isHidden(container) && edit}
|
||||||
<div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
|
<div class="handle handle-hidden"
|
||||||
|
style:z-index={zIndex+100}
|
||||||
|
class:hidden={!edit} />
|
||||||
{/if}
|
{/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}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</Block>
|
</Block>
|
||||||
@@ -112,7 +129,7 @@
|
|||||||
<Block elem_classes={["gradio-accordion"]}>
|
<Block elem_classes={["gradio-accordion"]}>
|
||||||
<Accordion label={container.attrs.title} open={$isOpen} on:click={handleClick}>
|
<Accordion label={container.attrs.title} open={$isOpen} on:click={handleClick}>
|
||||||
{#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)}
|
||||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||||
{/each}
|
{/each}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</Block>
|
</Block>
|
||||||
|
|||||||
@@ -10,11 +10,12 @@
|
|||||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||||
import {cubicIn} from 'svelte/easing';
|
import {cubicIn} from 'svelte/easing';
|
||||||
import { flip } from 'svelte/animate';
|
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 { startDrag, stopDrag } from "$lib/utils"
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
|
||||||
|
export let layoutState: WritableLayoutStateStore;
|
||||||
export let container: ContainerLayout | null = null;
|
export let container: ContainerLayout | null = null;
|
||||||
export let zIndex: number = 0;
|
export let zIndex: number = 0;
|
||||||
export let classes: string[] = [];
|
export let classes: string[] = [];
|
||||||
@@ -53,6 +54,14 @@
|
|||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = layoutState.updateChildren(container, evt.detail.items)
|
||||||
// Ensure dragging is stopped on drag finish
|
// Ensure dragging is stopped on drag finish
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
startDrag(e, layoutState)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _stopDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
stopDrag(e, layoutState)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container}
|
{#if container}
|
||||||
@@ -92,7 +101,7 @@
|
|||||||
animate:flip={{duration:flipDurationMs}}
|
animate:flip={{duration:flipDurationMs}}
|
||||||
style={item?.attrs?.style || ""}
|
style={item?.attrs?.style || ""}
|
||||||
>
|
>
|
||||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||||
{#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'/>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -100,10 +109,18 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if isHidden(container) && edit}
|
{#if isHidden(container) && edit}
|
||||||
<div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
|
<div class="handle handle-hidden"
|
||||||
|
style:z-index={zIndex+100}
|
||||||
|
class:hidden={!edit} />
|
||||||
{/if}
|
{/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>
|
||||||
|
|||||||
@@ -1,44 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { ListIcon as List, ImageIcon as Image, SettingsIcon as Settings } from "svelte-feather-icons";
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
|
||||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
|
||||||
import { Button } from "@gradio/button";
|
|
||||||
import { BlockTitle } from "@gradio/atoms";
|
|
||||||
import ComfyUIPane from "./ComfyUIPane.svelte";
|
|
||||||
import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp";
|
import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp";
|
||||||
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 selectionState from "$lib/stores/selectionState";
|
|
||||||
import { ImageViewer } from "$lib/ImageViewer";
|
|
||||||
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
|
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
|
||||||
|
|
||||||
import { LGraph } from "@litegraph-ts/core";
|
|
||||||
import LightboxModal from "./LightboxModal.svelte";
|
import LightboxModal from "./LightboxModal.svelte";
|
||||||
import ComfyQueue from "./ComfyQueue.svelte";
|
import Sidebar from "./Sidebar.svelte";
|
||||||
import ComfyProperties from "./ComfyProperties.svelte";
|
import SidebarItem from "./SidebarItem.svelte";
|
||||||
import queueState from "$lib/stores/queueState";
|
// import Modal from "./Modal.svelte";
|
||||||
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
|
// import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
|
||||||
import ComfyGraphView from "./ComfyGraphView.svelte";
|
import notify from "$lib/notify";
|
||||||
import { download, jsonToJsObject } from "$lib/utils";
|
import ComfyWorkflowsView from "./ComfyWorkflowsView.svelte";
|
||||||
import notify from "$lib/notify";
|
|
||||||
import Modal from "./Modal.svelte";
|
|
||||||
import ComfyBoxStdPrompt from "$lib/ComfyBoxStdPrompt";
|
|
||||||
import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
|
|
||||||
import type { A1111ParsedInfotext } from "$lib/parseA1111";
|
|
||||||
|
|
||||||
export let app: ComfyApp = undefined;
|
export let app: ComfyApp = undefined;
|
||||||
let alreadySetup: Writable<boolean> = writable(false);
|
|
||||||
let a1111Prompt: Writable<A1111PromptAndInfo | null> = writable(null);
|
|
||||||
let mainElem: HTMLDivElement;
|
|
||||||
let props: ComfyProperties = undefined;
|
|
||||||
let containerElem: HTMLDivElement;
|
|
||||||
let resizeTimeout: NodeJS.Timeout | null;
|
|
||||||
let hasShownUIHelpToast: boolean = false;
|
let hasShownUIHelpToast: boolean = false;
|
||||||
let uiTheme: string = "gradio-dark";
|
let uiTheme: string = "gradio-dark";
|
||||||
let fileInput: HTMLInputElement = undefined;
|
|
||||||
|
|
||||||
let debugLayout: boolean = false;
|
|
||||||
|
|
||||||
const toastOptions = {
|
const toastOptions = {
|
||||||
intro: { duration: 200 },
|
intro: { duration: 200 },
|
||||||
@@ -47,149 +24,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if(app) {
|
|
||||||
alreadySetup = app.alreadySetup;
|
|
||||||
a1111Prompt = app.a1111Prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshView(event?: Event) {
|
|
||||||
clearTimeout(resizeTimeout);
|
|
||||||
resizeTimeout = setTimeout(app.resizeCanvas.bind(app), 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (app?.lCanvas) {
|
|
||||||
app.lCanvas.allow_dragnodes = $uiState.uiUnlocked;
|
|
||||||
app.lCanvas.allow_interaction = $uiState.uiUnlocked;
|
|
||||||
|
|
||||||
if (!$uiState.uiUnlocked) {
|
|
||||||
app.lCanvas.deselectAllNodes();
|
|
||||||
$selectionState.currentSelectionNodes = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if ($uiState.uiEditMode)
|
|
||||||
$selectionState.currentSelection = []
|
|
||||||
|
|
||||||
let graphSize = 0;
|
|
||||||
let graphTransitioning = false;
|
|
||||||
|
|
||||||
function queuePrompt() {
|
|
||||||
app.runDefaultQueueAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleGraph() {
|
|
||||||
if (graphSize == 0) {
|
|
||||||
graphSize = 50;
|
|
||||||
app.resizeCanvas();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
graphSize = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let propsSidebarSize = 0;
|
|
||||||
|
|
||||||
function toggleProps() {
|
|
||||||
if (propsSidebarSize == 0) {
|
|
||||||
propsSidebarSize = 15;
|
|
||||||
app.resizeCanvas();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
propsSidebarSize = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let queueSidebarSize = 20;
|
|
||||||
|
|
||||||
function toggleQueue() {
|
|
||||||
if (queueSidebarSize == 0) {
|
|
||||||
queueSidebarSize = 20;
|
|
||||||
app.resizeCanvas();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
queueSidebarSize = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSave(): void {
|
|
||||||
if (!app?.lGraph)
|
|
||||||
return;
|
|
||||||
|
|
||||||
app.querySave()
|
|
||||||
}
|
|
||||||
|
|
||||||
function doLoad(): void {
|
|
||||||
if (!app?.lGraph || !fileInput)
|
|
||||||
return;
|
|
||||||
|
|
||||||
fileInput.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadWorkflow(): void {
|
|
||||||
app.handleFile(fileInput.files[0]);
|
|
||||||
fileInput.files = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSaveLocal(): void {
|
|
||||||
if (!app?.lGraph)
|
|
||||||
return;
|
|
||||||
|
|
||||||
app.saveStateToLocalStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doLoadDefault() {
|
|
||||||
var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?");
|
|
||||||
if (confirmed) {
|
|
||||||
await app.initDefaultGraph();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doClear(): void {
|
|
||||||
var confirmed = confirm("Are you sure you want to clear the current workflow?");
|
|
||||||
if (confirmed) {
|
|
||||||
app.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if ($uiState.uiUnlocked && !hasShownUIHelpToast) {
|
$: if ($uiState.uiUnlocked && !hasShownUIHelpToast) {
|
||||||
hasShownUIHelpToast = true;
|
hasShownUIHelpToast = true;
|
||||||
notify("Right-click to open context menu.")
|
notify("Right-click to open context menu.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugLayout) {
|
|
||||||
layoutState.subscribe(s => {
|
|
||||||
console.warn("UPDATESTATE", s)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (containerElem) {
|
|
||||||
const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas")
|
|
||||||
if (canvas) {
|
|
||||||
const paneNode = canvas.closest(".splitpanes__pane")
|
|
||||||
if (paneNode) {
|
|
||||||
(paneNode as HTMLElement).ontransitionstart = () => {
|
|
||||||
graphTransitioning = true
|
|
||||||
}
|
|
||||||
(paneNode as HTMLElement).ontransitionend = () => {
|
|
||||||
graphTransitioning = false
|
|
||||||
app.resizeCanvas()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await app.setup();
|
|
||||||
|
|
||||||
// await import('../../scss/ux.scss');
|
|
||||||
|
|
||||||
refreshView();
|
|
||||||
})
|
|
||||||
|
|
||||||
async function doRefreshCombos() {
|
|
||||||
await app.refreshComboInNodes(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (uiTheme === "gradio-dark") {
|
$: if (uiTheme === "gradio-dark") {
|
||||||
document.getElementById("app-root").classList.add("dark")
|
document.getElementById("app-root").classList.add("dark")
|
||||||
}
|
}
|
||||||
@@ -197,9 +36,11 @@
|
|||||||
document.getElementById("app-root").classList.remove("dark")
|
document.getElementById("app-root").classList.remove("dark")
|
||||||
}
|
}
|
||||||
|
|
||||||
let showModal: boolean = false;
|
// let showModal: boolean = false;
|
||||||
|
//
|
||||||
$: showModal = $a1111Prompt != null
|
// $: showModal = $a1111Prompt != null
|
||||||
|
//
|
||||||
|
// let selectedTab
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -208,198 +49,39 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Modal bind:showModal on:close={() => ($a1111Prompt = null)}>
|
<!--
|
||||||
<div slot="header" class="prompt-modal-header">
|
<Modal bind:showModal on:close={() => ($a1111Prompt = null)}>
|
||||||
<h1 style="padding-bottom: 1rem;">A1111 Prompt Details</h1>
|
<div slot="header" class="prompt-modal-header">
|
||||||
</div>
|
<h1 style="padding-bottom: 1rem;">A1111 Prompt Details</h1>
|
||||||
<A1111PromptDisplay prompt={$a1111Prompt} />
|
</div>
|
||||||
<div slot="buttons" let:closeDialog>
|
<A1111PromptDisplay prompt={$a1111Prompt} />
|
||||||
<Button variant="secondary" on:click={closeDialog}>
|
<div slot="buttons" let:closeDialog>
|
||||||
Close
|
<Button variant="secondary" on:click={closeDialog}>
|
||||||
</Button>
|
Close
|
||||||
</div>
|
</Button>
|
||||||
</Modal>
|
</div>
|
||||||
|
</Modal>
|
||||||
|
-->
|
||||||
|
|
||||||
<div id="main" class:dark={uiTheme === "gradio-dark"}>
|
<div id="main" class:dark={uiTheme === "gradio-dark"}>
|
||||||
<div id="container" bind:this={containerElem}>
|
<div id="container">
|
||||||
<Splitpanes theme="comfy" on:resize={refreshView}>
|
<Sidebar selected="generate">
|
||||||
<Pane bind:size={propsSidebarSize}>
|
<SidebarItem id="generate" name="Generate" icon={Image}>
|
||||||
<div class="sidebar-wrapper pane-wrapper">
|
<ComfyWorkflowsView {app} {uiTheme} />
|
||||||
<ComfyProperties bind:this={props} />
|
</SidebarItem>
|
||||||
</div>
|
<SidebarItem id="settings" name="Settings" icon={Settings}>
|
||||||
</Pane>
|
</SidebarItem>
|
||||||
<Pane>
|
</Sidebar>
|
||||||
<Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}">
|
|
||||||
<Pane>
|
|
||||||
<ComfyUIPane {app} />
|
|
||||||
</Pane>
|
|
||||||
<Pane bind:size={graphSize}>
|
|
||||||
<ComfyGraphView {app} transitioning={graphTransitioning} />
|
|
||||||
</Pane>
|
|
||||||
</Splitpanes>
|
|
||||||
</Pane>
|
|
||||||
<Pane bind:size={queueSidebarSize}>
|
|
||||||
<div class="sidebar-wrapper pane-wrapper">
|
|
||||||
<ComfyQueue {app} />
|
|
||||||
</div>
|
|
||||||
</Pane>
|
|
||||||
</Splitpanes>
|
|
||||||
</div>
|
|
||||||
<div id="bottombar">
|
|
||||||
<div class="left">
|
|
||||||
{#if $layoutState.attrs.queuePromptButtonName != ""}
|
|
||||||
<Button variant="primary" disabled={!$alreadySetup} on:click={queuePrompt}>
|
|
||||||
{$layoutState.attrs.queuePromptButtonName}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleGraph}>
|
|
||||||
Toggle Graph
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleProps}>
|
|
||||||
Toggle Props
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleQueue}>
|
|
||||||
Toggle Queue
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={doSave}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={doSaveLocal}>
|
|
||||||
Save Local
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={doLoad}>
|
|
||||||
Load
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={doClear}>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={doLoadDefault}>
|
|
||||||
Load Default
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" disabled={!$alreadySetup} on:click={doRefreshCombos}>
|
|
||||||
🔄
|
|
||||||
</Button>
|
|
||||||
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
|
||||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
|
|
||||||
<span style="display: inline-flex !important">
|
|
||||||
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
|
||||||
</span>
|
|
||||||
<span class="label" for="ui-edit-mode">
|
|
||||||
<BlockTitle>UI Edit mode</BlockTitle>
|
|
||||||
<select id="ui-edit-mode" name="ui-edit-mode" bind:value={$uiState.uiEditMode}>
|
|
||||||
<option value="widgets">Widgets</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<span class="label" for="ui-theme">
|
|
||||||
<BlockTitle>Theme</BlockTitle>
|
|
||||||
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
|
|
||||||
<option value="gradio-dark">Gradio Dark</option>
|
|
||||||
<option value="gradio-light">Gradio Light</option>
|
|
||||||
<option value="anapnoe">Anapnoe</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<ComfyUnlockUIButton bind:toggled={$uiState.uiUnlocked} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<LightboxModal />
|
<LightboxModal />
|
||||||
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
|
|
||||||
</div>
|
</div>
|
||||||
<SvelteToast options={toastOptions} />
|
<SvelteToast options={toastOptions} />
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
$bottom-bar-height: 70px;
|
|
||||||
|
|
||||||
#container {
|
#container {
|
||||||
height: calc(100vh - $bottom-bar-height);
|
|
||||||
max-width: 100vw;
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#comfy-content {
|
|
||||||
grid-area: content;
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
|
||||||
|
|
||||||
#bottombar {
|
|
||||||
padding-top: 0.5em;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
gap: var(--layout-gap);
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
margin-top: auto;
|
|
||||||
overflow-x: auto;
|
|
||||||
height: $bottom-bar-height;
|
|
||||||
|
|
||||||
> .left {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .right {
|
|
||||||
margin-left: auto
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html, body) {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0px;
|
|
||||||
font-family: Arial;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.splitpanes.comfy>.splitpanes__splitter) {
|
|
||||||
background: var(--comfy-splitpanes-background-fill);
|
|
||||||
|
|
||||||
&:hover:not([disabled]) {
|
|
||||||
background: var(--comfy-splitpanes-background-fill-hover);
|
|
||||||
}
|
|
||||||
&:active:not([disabled]) {
|
|
||||||
background: var(--comfy-splitpanes-background-fill-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$splitter-size: 1rem;
|
|
||||||
|
|
||||||
:global(.splitpanes.comfy.splitpanes--horizontal>.splitpanes__splitter) {
|
|
||||||
min-height: $splitter-size;
|
|
||||||
cursor: row-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.splitpanes.comfy.splitpanes--vertical>.splitpanes__splitter) {
|
|
||||||
min-width: $splitter-size;
|
|
||||||
cursor: col-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.splitpanes.comfy) {
|
|
||||||
max-height: calc(100vh - $bottom-bar-height);
|
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
}
|
display: relative;
|
||||||
|
width: 100%;
|
||||||
:global(.splitpanes__pane) {
|
|
||||||
box-shadow: 0 0 3px rgba(0, 0, 0, .2) inset;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.label > :global(span) {
|
|
||||||
top: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.left {
|
|
||||||
right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#comfy-file-input {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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 type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
||||||
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
||||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||||
@@ -21,8 +21,7 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
|||||||
import queueState from "$lib/stores/queueState";
|
import queueState from "$lib/stores/queueState";
|
||||||
import { type SvelteComponentDev } from "svelte/internal";
|
import { type SvelteComponentDev } from "svelte/internal";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import type { SerializedLayoutState } from "$lib/stores/layoutState";
|
import type { LayoutState, SerializedLayoutState, WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||||
import layoutState from "$lib/stores/layoutState";
|
|
||||||
import { toast } from '@zerodevx/svelte-toast'
|
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";
|
||||||
@@ -33,7 +32,7 @@ import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, wor
|
|||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import configState from "$lib/stores/configState";
|
import configState from "$lib/stores/configState";
|
||||||
import { blankGraph } from "$lib/defaultGraph";
|
import { blankGraph } from "$lib/defaultGraph";
|
||||||
import type { ComfyExecutionResult } from "$lib/utils";
|
import type { SerializedPromptOutput } from "$lib/utils";
|
||||||
import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer";
|
import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer";
|
||||||
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
||||||
import { ComfyComboNode } from "$lib/nodes/widgets";
|
import { ComfyComboNode } from "$lib/nodes/widgets";
|
||||||
@@ -41,6 +40,10 @@ import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111";
|
|||||||
import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt";
|
import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt";
|
||||||
import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
|
import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
|
||||||
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
|
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;
|
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.
|
* 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,
|
num: number,
|
||||||
batchCount: number
|
batchCount: number
|
||||||
|
workflow: ComfyWorkflow
|
||||||
}
|
}
|
||||||
|
|
||||||
export type A1111PromptAndInfo = {
|
export type A1111PromptAndInfo = {
|
||||||
@@ -78,6 +80,8 @@ export type SerializedAppState = {
|
|||||||
commitHash?: string,
|
commitHash?: string,
|
||||||
/** Graph state */
|
/** Graph state */
|
||||||
workflow: SerializedLGraph,
|
workflow: SerializedLGraph,
|
||||||
|
/** Workflow attributes */
|
||||||
|
attrs: WorkflowAttributes,
|
||||||
/** UI state */
|
/** UI state */
|
||||||
layout: SerializedLayoutState,
|
layout: SerializedLayoutState,
|
||||||
/** Position/offset of the canvas at the time of saving */
|
/** Position/offset of the canvas at the time of saving */
|
||||||
@@ -111,7 +115,7 @@ export type SerializedPrompt = {
|
|||||||
/*
|
/*
|
||||||
* Outputs for each node.
|
* Outputs for each node.
|
||||||
*/
|
*/
|
||||||
export type SerializedPromptOutputs = Record<ComfyNodeID, ComfyExecutionResult>
|
export type SerializedPromptOutputs = Record<ComfyNodeID, SerializedPromptOutput>
|
||||||
|
|
||||||
export type Progress = {
|
export type Progress = {
|
||||||
value: number,
|
value: number,
|
||||||
@@ -128,15 +132,19 @@ type BackendComboNode = {
|
|||||||
backendNode: ComfyBackendNode
|
backendNode: ComfyBackendNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CanvasState = {
|
||||||
|
canvasEl: HTMLCanvasElement,
|
||||||
|
canvasCtx: CanvasRenderingContext2D,
|
||||||
|
canvas: ComfyGraphCanvas,
|
||||||
|
}
|
||||||
|
|
||||||
export default class ComfyApp {
|
export default class ComfyApp {
|
||||||
api: ComfyAPI;
|
api: ComfyAPI;
|
||||||
|
|
||||||
rootEl: HTMLDivElement | null = null;
|
rootEl: HTMLDivElement | null = null;
|
||||||
canvasEl: HTMLCanvasElement | null = null;
|
canvasEl: HTMLCanvasElement | null = null;
|
||||||
canvasCtx: CanvasRenderingContext2D | null = null;
|
canvasCtx: CanvasRenderingContext2D | null = null;
|
||||||
lGraph: ComfyGraph | null = null;
|
|
||||||
lCanvas: ComfyGraphCanvas | null = null;
|
lCanvas: ComfyGraphCanvas | null = null;
|
||||||
dropZone: HTMLElement | null = null;
|
|
||||||
nodeOutputs: Record<string, any> = {};
|
|
||||||
|
|
||||||
shiftDown: boolean = false;
|
shiftDown: boolean = false;
|
||||||
ctrlDown: boolean = false;
|
ctrlDown: boolean = false;
|
||||||
@@ -144,7 +152,7 @@ export default class ComfyApp {
|
|||||||
alreadySetup: Writable<boolean> = writable(false);
|
alreadySetup: Writable<boolean> = writable(false);
|
||||||
a1111Prompt: Writable<A1111PromptAndInfo | null> = writable(null);
|
a1111Prompt: Writable<A1111PromptAndInfo | null> = writable(null);
|
||||||
|
|
||||||
private queueItems: QueueItem[] = [];
|
private queueItems: PromptQueueItem[] = [];
|
||||||
private processingQueue: boolean = false;
|
private processingQueue: boolean = false;
|
||||||
private promptSerializer: ComfyPromptSerializer;
|
private promptSerializer: ComfyPromptSerializer;
|
||||||
private stdPromptSerializer: ComfyBoxStdPromptSerializer;
|
private stdPromptSerializer: ComfyBoxStdPromptSerializer;
|
||||||
@@ -157,7 +165,7 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
async setup(): Promise<void> {
|
async setup(): Promise<void> {
|
||||||
if (get(this.alreadySetup)) {
|
if (get(this.alreadySetup)) {
|
||||||
console.error("Already setup")
|
console.log("Already setup")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +173,6 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
this.rootEl = document.getElementById("app-root") as HTMLDivElement;
|
this.rootEl = document.getElementById("app-root") as HTMLDivElement;
|
||||||
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
|
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
|
||||||
this.lGraph = new ComfyGraph();
|
|
||||||
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
|
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
|
||||||
this.canvasCtx = this.canvasEl.getContext("2d");
|
this.canvasCtx = this.canvasEl.getContext("2d");
|
||||||
|
|
||||||
@@ -174,17 +181,13 @@ export default class ComfyApp {
|
|||||||
this.lCanvas.allow_interaction = uiUnlocked;
|
this.lCanvas.allow_interaction = uiUnlocked;
|
||||||
|
|
||||||
// await this.#invokeExtensionsAsync("init");
|
// await this.#invokeExtensionsAsync("init");
|
||||||
await this.registerNodes();
|
const defs = await this.api.getNodeDefs();
|
||||||
|
await this.registerNodes(defs);
|
||||||
|
|
||||||
// Load previous workflow
|
// Load previous workflow
|
||||||
let restored = false;
|
let restored = false;
|
||||||
try {
|
try {
|
||||||
const json = localStorage.getItem("workflow");
|
restored = await this.loadStateFromLocalStorage(defs);
|
||||||
if (json) {
|
|
||||||
const state = JSON.parse(json) as SerializedAppState;
|
|
||||||
await this.deserialize(state)
|
|
||||||
restored = true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading previous workflow", err);
|
console.error("Error loading previous workflow", err);
|
||||||
notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null })
|
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
|
// We failed to restore a workflow so load the default
|
||||||
if (!restored) {
|
if (!restored) {
|
||||||
await this.initDefaultGraph();
|
await this.initDefaultWorkflow(defs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current workflow automatically
|
// Save current workflow automatically
|
||||||
@@ -225,12 +228,37 @@ export default class ComfyApp {
|
|||||||
this.lCanvas.draw(true, true);
|
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() {
|
saveStateToLocalStorage() {
|
||||||
try {
|
try {
|
||||||
uiState.update(s => { s.isSavingToLocalStorage = true; return s; })
|
uiState.update(s => { s.isSavingToLocalStorage = true; return s; })
|
||||||
const savedWorkflow = this.serialize();
|
const state = get(workflowState)
|
||||||
const json = JSON.stringify(savedWorkflow);
|
const workflows = state.openedWorkflows
|
||||||
localStorage.setItem("workflow", json)
|
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.")
|
notify("Saved to local storage.")
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -241,13 +269,33 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadStateFromLocalStorage(defs: Record<ComfyNodeID, ComfyNodeDef>): Promise<boolean> {
|
||||||
|
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<string, typeof ComfyBackendNode> = {}
|
static node_type_overrides: Record<string, typeof ComfyBackendNode> = {}
|
||||||
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
||||||
|
|
||||||
private async registerNodes() {
|
private async registerNodes(defs: Record<ComfyNodeID, ComfyNodeDef>) {
|
||||||
// Load node definitions from the backend
|
|
||||||
const defs = await this.api.getNodeDefs();
|
|
||||||
|
|
||||||
// Register a node for each definition
|
// Register a node for each definition
|
||||||
for (const [nodeId, nodeDef] of Object.entries(defs)) {
|
for (const [nodeId, nodeDef] of Object.entries(defs)) {
|
||||||
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
||||||
@@ -351,8 +399,12 @@ export default class ComfyApp {
|
|||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflow && workflow.version && workflow.nodes && workflow.extra) {
|
if (workflow && workflow.createdBy === "ComfyBox") {
|
||||||
this.loadGraphData(workflow);
|
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) => {
|
this.api.addEventListener("progress", (progress: Progress) => {
|
||||||
queueState.progressUpdated(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) => {
|
this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => {
|
||||||
queueState.executingUpdated(promptID, nodeID);
|
const queueEntry = queueState.executingUpdated(promptID, nodeID);
|
||||||
this.lGraph.setDirtyCanvas(true, false);
|
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.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutput) => {
|
||||||
this.nodeOutputs[nodeID] = output;
|
const queueEntry = queueState.onExecuted(promptID, nodeID, output)
|
||||||
const node = this.lGraph.getNodeByIdRecursive(nodeID) as ComfyGraphNode;
|
if (queueEntry != null) {
|
||||||
if (node?.onExecuted) {
|
const workflow = workflowState.getWorkflow(queueEntry.workflowID);
|
||||||
node.onExecuted(output);
|
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) => {
|
this.api.addEventListener("execution_start", (promptID: PromptID) => {
|
||||||
@@ -456,49 +516,49 @@ export default class ComfyApp {
|
|||||||
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(): SerializedAppState {
|
async openWorkflow(data: SerializedAppState, refreshCombos: boolean | Record<string, ComfyNodeDef> = true): Promise<ComfyWorkflow> {
|
||||||
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) {
|
|
||||||
if (data.version !== COMFYBOX_SERIAL_VERSION) {
|
if (data.version !== COMFYBOX_SERIAL_VERSION) {
|
||||||
throw `Invalid ComfyBox saved data format: ${data.version}`
|
throw `Invalid ComfyBox saved data format: ${data.version}`
|
||||||
}
|
}
|
||||||
|
this.clean();
|
||||||
|
|
||||||
// Ensure loadGraphData does not trigger any state changes in layoutState
|
const workflow = workflowState.openWorkflow(this.lCanvas, data);
|
||||||
// (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)
|
|
||||||
|
|
||||||
// Restore canvas offset/zoom
|
// Restore canvas offset/zoom
|
||||||
this.lCanvas.deserialize(data.canvas)
|
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();
|
return workflow;
|
||||||
this.lGraph.eventBus.on("afterExecute", () => this.lCanvas.draw(true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<string, ComfyNodeDef>) {
|
||||||
let state = null;
|
let state = null;
|
||||||
try {
|
try {
|
||||||
const graphResponse = await fetch("/workflows/defaultWorkflow.json");
|
const graphResponse = await fetch("/workflows/defaultWorkflow.json");
|
||||||
@@ -509,50 +569,14 @@ export default class ComfyApp {
|
|||||||
notify(`Failed to load default graph: ${error}`, { type: "error" })
|
notify(`Failed to load default graph: ${error}`, { type: "error" })
|
||||||
state = structuredClone(blankGraph)
|
state = structuredClone(blankGraph)
|
||||||
}
|
}
|
||||||
await this.deserialize(state)
|
await this.openWorkflow(state, defs)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.clean();
|
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.lCanvas.closeAllSubgraphs();
|
||||||
this.lGraph.configure(blankGraph)
|
workflowState.closeAllWorkflows(this.lCanvas);
|
||||||
layoutState.initDefaultLayout();
|
|
||||||
uiState.update(s => {
|
uiState.update(s => {
|
||||||
s.uiUnlocked = true;
|
s.uiUnlocked = true;
|
||||||
s.uiEditMode = "widgets";
|
s.uiEditMode = "widgets";
|
||||||
@@ -561,13 +585,17 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runDefaultQueueAction() {
|
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) {
|
if ("onDefaultQueueAction" in node) {
|
||||||
(node as ComfyGraphNode).onDefaultQueueAction()
|
(node as ComfyGraphNode).onDefaultQueueAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get(layoutState).attrs.queuePromptButtonRunWorkflow) {
|
if (workflow.attrs.queuePromptButtonRunWorkflow) {
|
||||||
// Hold control to queue at the front
|
// Hold control to queue at the front
|
||||||
const num = this.ctrlDown ? -1 : 0;
|
const num = this.ctrlDown ? -1 : 0;
|
||||||
this.queuePrompt(num, 1);
|
this.queuePrompt(num, 1);
|
||||||
@@ -575,6 +603,12 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
querySave() {
|
querySave() {
|
||||||
|
const workflow = workflowState.getActiveWorkflow();
|
||||||
|
if (workflow == null) {
|
||||||
|
notify("No active workflow!", { type: "error" })
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const promptFilename = get(configState).promptForWorkflowName;
|
const promptFilename = get(configState).promptForWorkflowName;
|
||||||
|
|
||||||
let filename = "workflow.json";
|
let filename = "workflow.json";
|
||||||
@@ -592,10 +626,13 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const indent = 2
|
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")
|
download(filename, json, "application/json")
|
||||||
|
|
||||||
|
workflow.isModified = false;
|
||||||
|
workflowState.set(get(workflowState));
|
||||||
|
|
||||||
console.debug(jsonToJsObject(json))
|
console.debug(jsonToJsObject(json))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,12 +640,18 @@ 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
|
||||||
*/
|
*/
|
||||||
graphToPrompt(tag: string | null = null): SerializedPrompt {
|
graphToPrompt(workflow: ComfyWorkflow, tag: string | null = null): SerializedPrompt {
|
||||||
return this.promptSerializer.serialize(this.lGraph, tag)
|
return this.promptSerializer.serialize(workflow.graph, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
|
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
|
// Only have one action process the items so each one gets a unique seed correctly
|
||||||
if (this.processingQueue) {
|
if (this.processingQueue) {
|
||||||
@@ -619,13 +662,15 @@ export default class ComfyApp {
|
|||||||
tag = null;
|
tag = null;
|
||||||
|
|
||||||
this.processingQueue = true;
|
this.processingQueue = true;
|
||||||
|
let workflow;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (this.queueItems.length) {
|
while (this.queueItems.length) {
|
||||||
({ num, batchCount } = this.queueItems.pop());
|
({ num, batchCount, workflow } = this.queueItems.pop());
|
||||||
console.debug(`Queue get! ${num} ${batchCount} ${tag}`);
|
console.debug(`Queue get! ${num} ${batchCount} ${tag}`);
|
||||||
|
|
||||||
const thumbnails = []
|
const thumbnails = []
|
||||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||||
if (node.mode !== NodeMode.ALWAYS
|
if (node.mode !== NodeMode.ALWAYS
|
||||||
|| (tag != null
|
|| (tag != null
|
||||||
&& Array.isArray(node.properties.tags)
|
&& Array.isArray(node.properties.tags)
|
||||||
@@ -640,7 +685,7 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < batchCount; i++) {
|
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)
|
if (node.mode !== NodeMode.ALWAYS)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -649,9 +694,9 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = this.graphToPrompt(tag);
|
const p = this.graphToPrompt(workflow, tag);
|
||||||
const l = layoutState.serialize();
|
const l = workflow.layout.serialize();
|
||||||
console.debug(graphToGraphVis(this.lGraph))
|
console.debug(graphToGraphVis(workflow.graph))
|
||||||
console.debug(promptToGraphVis(p))
|
console.debug(promptToGraphVis(p))
|
||||||
|
|
||||||
const stdPrompt = this.stdPromptSerializer.serialize(p);
|
const stdPrompt = this.stdPromptSerializer.serialize(p);
|
||||||
@@ -681,7 +726,7 @@ export default class ComfyApp {
|
|||||||
error = response.error;
|
error = response.error;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
queueState.afterQueued(response.promptID, num, p.output, extraData)
|
queueState.afterQueued(workflow.id, response.promptID, num, p.output, extraData)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err?.toString();
|
error = err?.toString();
|
||||||
@@ -690,13 +735,13 @@ export default class ComfyApp {
|
|||||||
if (error != null) {
|
if (error != null) {
|
||||||
const mes: string = error;
|
const mes: string = error;
|
||||||
notify(`Error queuing prompt:\n${mes}`, { type: "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(promptToGraphVis(p))
|
||||||
console.error("Error queuing prompt", error, num, p)
|
console.error("Error queuing prompt", error, num, p)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||||
if ("afterQueued" in node) {
|
if ("afterQueued" in node) {
|
||||||
(node as ComfyGraphNode).afterQueued(p, tag);
|
(node as ComfyGraphNode).afterQueued(p, tag);
|
||||||
}
|
}
|
||||||
@@ -719,7 +764,7 @@ export default class ComfyApp {
|
|||||||
const pngInfo = await getPngMetadata(file);
|
const pngInfo = await getPngMetadata(file);
|
||||||
if (pngInfo) {
|
if (pngInfo) {
|
||||||
if (pngInfo.comfyBoxConfig) {
|
if (pngInfo.comfyBoxConfig) {
|
||||||
this.deserialize(JSON.parse(pngInfo.comfyBoxConfig));
|
await this.openWorkflow(JSON.parse(pngInfo.comfyBoxConfig));
|
||||||
} else if (pngInfo.parameters) {
|
} else if (pngInfo.parameters) {
|
||||||
const parsed = parseA1111(pngInfo.parameters)
|
const parsed = parseA1111(pngInfo.parameters)
|
||||||
if ("error" in parsed) {
|
if ("error" in parsed) {
|
||||||
@@ -741,8 +786,8 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = async () => {
|
||||||
this.deserialize(JSON.parse(reader.result as string));
|
await this.openWorkflow(JSON.parse(reader.result as string));
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
@@ -761,8 +806,15 @@ export default class ComfyApp {
|
|||||||
/**
|
/**
|
||||||
* Refresh combo list on whole nodes
|
* Refresh combo list on whole nodes
|
||||||
*/
|
*/
|
||||||
async refreshComboInNodes(flashUI: boolean = false) {
|
async refreshComboInNodes(workflow?: ComfyWorkflow, defs?: Record<string, ComfyNodeDef>, flashUI: boolean = false) {
|
||||||
const defs = await this.api.getNodeDefs();
|
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 => {
|
const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => {
|
||||||
return node
|
return node
|
||||||
@@ -805,7 +857,7 @@ export default class ComfyApp {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||||
if (!isActiveBackendNode(node))
|
if (!isActiveBackendNode(node))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -827,7 +879,7 @@ export default class ComfyApp {
|
|||||||
console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos)
|
console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos)
|
||||||
|
|
||||||
// Mark combo nodes without backend configs as being loaded already.
|
// 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) {
|
if (backendUpdatedCombos[node.id] != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -859,12 +911,15 @@ export default class ComfyApp {
|
|||||||
// Load definitions from the backend.
|
// Load definitions from the backend.
|
||||||
for (const { comboNode, comfyInput, backendNode } of Object.values(backendUpdatedCombos)) {
|
for (const { comboNode, comfyInput, backendNode } of Object.values(backendUpdatedCombos)) {
|
||||||
const def = defs[backendNode.type];
|
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)
|
console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length)
|
||||||
comboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false })
|
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))) {
|
if (!rawValues?.includes(get(comboNode.value))) {
|
||||||
comboNode.setValue(rawValues[0], comfyInput.config.defaultValue)
|
comboNode.setValue(rawValues[0], comfyInput.config.defaultValue)
|
||||||
}
|
}
|
||||||
@@ -875,7 +930,6 @@ export default class ComfyApp {
|
|||||||
* Clean current state
|
* Clean current state
|
||||||
*/
|
*/
|
||||||
clean() {
|
clean() {
|
||||||
this.nodeOutputs = {};
|
|
||||||
this.a1111Prompt.set(null);
|
this.a1111Prompt.set(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,18 @@ export function isActiveBackendNode(node: LGraphNode, tag: string | null = null)
|
|||||||
if (!(node as any).isBackendNode)
|
if (!(node as any).isBackendNode)
|
||||||
return false;
|
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 {
|
export class UpstreamNodeLocator {
|
||||||
@@ -166,7 +177,7 @@ export default class ComfyPromptSerializer {
|
|||||||
// We don't check tags for non-backend nodes.
|
// We don't check tags for non-backend nodes.
|
||||||
// Just check for node inactivity (so you can toggle groups of
|
// Just check for node inactivity (so you can toggle groups of
|
||||||
// tagged frontend nodes on/off)
|
// tagged frontend nodes on/off)
|
||||||
if (inputNode && inputNode.mode === NodeMode.NEVER) {
|
if (inputNode && inputNode.mode !== NodeMode.ALWAYS) {
|
||||||
console.debug("Skipping inactive node", inputNode)
|
console.debug("Skipping inactive node", inputNode)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -248,6 +259,8 @@ export default class ComfyPromptSerializer {
|
|||||||
const inputs = this.serializeInputValues(node);
|
const inputs = this.serializeInputValues(node);
|
||||||
const links = this.serializeBackendLinks(node, tag);
|
const links = this.serializeBackendLinks(node, tag);
|
||||||
|
|
||||||
|
console.warn("OUTPUT", node.id, node.comfyClass, node.mode)
|
||||||
|
|
||||||
output[String(node.id)] = {
|
output[String(node.id)] = {
|
||||||
inputs: { ...inputs, ...links },
|
inputs: { ...inputs, ...links },
|
||||||
class_type: node.comfyClass,
|
class_type: node.comfyClass,
|
||||||
|
|||||||
@@ -2,43 +2,50 @@
|
|||||||
import { Block, BlockTitle } from "@gradio/atoms";
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
import { TextBox, Checkbox } from "@gradio/form";
|
import { TextBox, Checkbox } from "@gradio/form";
|
||||||
import { LGraphNode } from "@litegraph-ts/core"
|
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 uiState from "$lib/stores/uiState"
|
||||||
|
import workflowState from "$lib/stores/workflowState"
|
||||||
|
import layoutStates from "$lib/stores/layoutStates"
|
||||||
import selectionState from "$lib/stores/selectionState"
|
import selectionState from "$lib/stores/selectionState"
|
||||||
import { get, type Writable, writable } from "svelte/store"
|
import { get, type Writable, writable } from "svelte/store"
|
||||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||||
import ComfyComboProperty from "./ComfyComboProperty.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 target: IDragItem | null = null;
|
||||||
let node: LGraphNode | null = null;
|
let node: LGraphNode | null = null;
|
||||||
let attrsChanged: Writable<number> | null = null;
|
|
||||||
|
|
||||||
let refreshPropsPanel: Writable<number> | null
|
$: if (layoutState) {
|
||||||
|
if ($selectionState.currentSelection.length > 0) {
|
||||||
$: refreshPropsPanel = $layoutState.refreshPropsPanel;
|
node = null;
|
||||||
|
const targetId = $selectionState.currentSelection.slice(-1)[0]
|
||||||
$: if ($selectionState.currentSelection.length > 0) {
|
const entry = $layoutState.allItems[targetId]
|
||||||
node = null;
|
if (entry != null) {
|
||||||
const targetId = $selectionState.currentSelection.slice(-1)[0]
|
target = entry.dragItem
|
||||||
const entry = $layoutState.allItems[targetId]
|
if (target.type === "widget") {
|
||||||
if (entry != null) {
|
node = (target as WidgetLayout).node
|
||||||
target = entry.dragItem
|
}
|
||||||
attrsChanged = target.attrsChanged;
|
|
||||||
if (target.type === "widget") {
|
|
||||||
node = (target as WidgetLayout).node
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
else if ($selectionState.currentSelectionNodes.length > 0) {
|
||||||
else if ($selectionState.currentSelectionNodes.length > 0) {
|
target = null;
|
||||||
target = null;
|
node = $selectionState.currentSelectionNodes[0]
|
||||||
node = $selectionState.currentSelectionNodes[0]
|
}
|
||||||
attrsChanged = null;
|
else {
|
||||||
|
target = null
|
||||||
|
node = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
target = null
|
target = null;
|
||||||
node = null;
|
node = null;
|
||||||
attrsChanged = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (target) {
|
$: if (target) {
|
||||||
@@ -55,7 +62,7 @@
|
|||||||
let value = spec.defaultValue;
|
let value = spec.defaultValue;
|
||||||
target.attrs[spec.name] = value;
|
target.attrs[spec.name] = value;
|
||||||
if (spec.refreshPanelOnChange)
|
if (spec.refreshPanelOnChange)
|
||||||
$refreshPropsPanel += 1;
|
doRefreshPanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,7 +130,10 @@
|
|||||||
if (spec.location !== "workflow")
|
if (spec.location !== "workflow")
|
||||||
return false;
|
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 {
|
function getAttribute(target: IDragItem, spec: AttributesSpec): any {
|
||||||
@@ -162,6 +172,8 @@
|
|||||||
if (spec.refreshPanelOnChange) {
|
if (spec.refreshPanelOnChange) {
|
||||||
doRefreshPanel()
|
doRefreshPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workflow.notifyModified()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProperty(node: LGraphNode, spec: AttributesSpec) {
|
function getProperty(node: LGraphNode, spec: AttributesSpec) {
|
||||||
@@ -197,6 +209,8 @@
|
|||||||
|
|
||||||
if (spec.refreshPanelOnChange)
|
if (spec.refreshPanelOnChange)
|
||||||
doRefreshPanel()
|
doRefreshPanel()
|
||||||
|
|
||||||
|
workflow.notifyModified()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVar(node: LGraphNode, spec: AttributesSpec) {
|
function getVar(node: LGraphNode, spec: AttributesSpec) {
|
||||||
@@ -233,10 +247,15 @@
|
|||||||
if (spec.refreshPanelOnChange) {
|
if (spec.refreshPanelOnChange) {
|
||||||
doRefreshPanel()
|
doRefreshPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workflow.notifyModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWorkflowAttribute(spec: AttributesSpec): any {
|
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)
|
if (value == null)
|
||||||
value = spec.defaultValue
|
value = spec.defaultValue
|
||||||
else if (spec.serialize)
|
else if (spec.serialize)
|
||||||
@@ -249,23 +268,28 @@
|
|||||||
if (!spec.editable)
|
if (!spec.editable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (workflow == null)
|
||||||
|
throw new Error("Active workflow is null!");
|
||||||
|
|
||||||
const name = spec.name
|
const name = spec.name
|
||||||
// console.warn("[ComfyProperties] updateWorkflowAttribute", name, value)
|
// console.warn("[ComfyProperties] updateWorkflowAttribute", name, value)
|
||||||
|
|
||||||
const prevValue = value
|
const prevValue = value
|
||||||
$layoutState.attrs[name] = value
|
workflow.attrs[name] = value
|
||||||
$layoutState = $layoutState
|
$workflowState = $workflowState;
|
||||||
|
|
||||||
if (spec.onChanged)
|
if (spec.onChanged)
|
||||||
spec.onChanged($layoutState, value, prevValue)
|
spec.onChanged($layoutState, value, prevValue)
|
||||||
|
|
||||||
if (spec.refreshPanelOnChange)
|
if (spec.refreshPanelOnChange)
|
||||||
doRefreshPanel()
|
doRefreshPanel()
|
||||||
|
|
||||||
|
workflow.notifyModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
function doRefreshPanel() {
|
function doRefreshPanel() {
|
||||||
console.warn("[ComfyProperties] doRefreshPanel")
|
console.warn("[ComfyProperties] doRefreshPanel")
|
||||||
$refreshPropsPanel += 1;
|
$layoutStates.refreshPropsPanel += 1;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -278,176 +302,180 @@
|
|||||||
<span class="type">({targetType})</span>
|
<span class="type">({targetType})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="props-entries">
|
<div class="props-entries">
|
||||||
{#key $refreshPropsPanel}
|
{#if workflow != null && layoutState != null}
|
||||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
{#key workflow.id}
|
||||||
<div class="category-name">
|
{#key $layoutStates.refreshPropsPanel}
|
||||||
<span>
|
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||||
<span class="title">{category.categoryName}</span>
|
<div class="category-name">
|
||||||
</span>
|
<span>
|
||||||
</div>
|
<span class="title">{category.categoryName}</span>
|
||||||
{#each category.specs as spec(spec.id)}
|
</span>
|
||||||
{#if validWidgetAttribute(spec, target)}
|
|
||||||
<div class="props-entry">
|
|
||||||
{#if spec.type === "string"}
|
|
||||||
<TextBox
|
|
||||||
value={getAttribute(target, spec)}
|
|
||||||
on:change={(e) => 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"}
|
|
||||||
<Checkbox
|
|
||||||
value={getAttribute(target, spec)}
|
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
label={spec.name}
|
|
||||||
/>
|
|
||||||
{:else if spec.type === "number"}
|
|
||||||
<ComfyNumberProperty
|
|
||||||
name={spec.name}
|
|
||||||
value={getAttribute(target, spec)}
|
|
||||||
step={spec.step || 1}
|
|
||||||
min={spec.min || -1024}
|
|
||||||
max={spec.max || 1024}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
|
||||||
/>
|
|
||||||
{:else if spec.type === "enum"}
|
|
||||||
<ComfyComboProperty
|
|
||||||
name={spec.name}
|
|
||||||
value={getAttribute(target, spec)}
|
|
||||||
values={spec.values}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:else if node}
|
{#each category.specs as spec(spec.id)}
|
||||||
{#if validNodeProperty(spec, node)}
|
{#if validWidgetAttribute(spec, target)}
|
||||||
<div class="props-entry">
|
<div class="props-entry">
|
||||||
{#if spec.type === "string"}
|
{#if spec.type === "string"}
|
||||||
<TextBox
|
<TextBox
|
||||||
value={getProperty(node, spec)}
|
value={getAttribute(target, spec)}
|
||||||
on:change={(e) => updateProperty(spec, e.detail)}
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
on:input={(e) => updateProperty(spec, e.detail)}
|
on:input={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
label={spec.name}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
max_lines={spec.multiline ? 5 : 1}
|
|
||||||
/>
|
|
||||||
{:else if spec.type === "boolean"}
|
|
||||||
<Checkbox
|
|
||||||
value={getProperty(node, spec)}
|
|
||||||
label={spec.name}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
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"}
|
{:else if spec.type === "boolean"}
|
||||||
<ComfyNumberProperty
|
<Checkbox
|
||||||
name={spec.name}
|
value={getAttribute(target, spec)}
|
||||||
value={getProperty(node, spec)}
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
step={spec.step || 1}
|
|
||||||
min={spec.min || -1024}
|
|
||||||
max={spec.max || 1024}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateProperty(spec, e.detail)}
|
label={spec.name}
|
||||||
/>
|
/>
|
||||||
{:else if spec.type === "enum"}
|
{:else if spec.type === "number"}
|
||||||
<ComfyComboProperty
|
<ComfyNumberProperty
|
||||||
name={spec.name}
|
name={spec.name}
|
||||||
|
value={getAttribute(target, spec)}
|
||||||
|
step={spec.step || 1}
|
||||||
|
min={spec.min || -1024}
|
||||||
|
max={spec.max || 1024}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
|
/>
|
||||||
|
{:else if spec.type === "enum"}
|
||||||
|
<ComfyComboProperty
|
||||||
|
name={spec.name}
|
||||||
|
value={getAttribute(target, spec)}
|
||||||
|
values={spec.values}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if node}
|
||||||
|
{#if validNodeProperty(spec, node)}
|
||||||
|
<div class="props-entry">
|
||||||
|
{#if spec.type === "string"}
|
||||||
|
<TextBox
|
||||||
|
value={getProperty(node, spec)}
|
||||||
|
on:change={(e) => 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"}
|
||||||
|
<Checkbox
|
||||||
value={getProperty(node, spec)}
|
value={getProperty(node, spec)}
|
||||||
values={spec.values}
|
label={spec.name}
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateProperty(spec, e.detail)}
|
on:change={(e) => updateProperty(spec, e.detail)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{:else if spec.type === "number"}
|
||||||
</div>
|
<ComfyNumberProperty
|
||||||
{:else if validNodeVar(spec, node)}
|
name={spec.name}
|
||||||
<div class="props-entry">
|
value={getProperty(node, spec)}
|
||||||
{#if spec.type === "string"}
|
step={spec.step || 1}
|
||||||
<TextBox
|
min={spec.min || -1024}
|
||||||
value={getVar(node, spec)}
|
max={spec.max || 1024}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:input={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateProperty(spec, e.detail)}
|
||||||
label={spec.name}
|
/>
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
{:else if spec.type === "enum"}
|
||||||
max_lines={spec.multiline ? 5 : 1}
|
<ComfyComboProperty
|
||||||
/>
|
name={spec.name}
|
||||||
{:else if spec.type === "boolean"}
|
value={getProperty(node, spec)}
|
||||||
<Checkbox
|
values={spec.values}
|
||||||
value={getVar(node, spec)}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateProperty(spec, e.detail)}
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
/>
|
||||||
label={spec.name}
|
{/if}
|
||||||
/>
|
</div>
|
||||||
{:else if spec.type === "number"}
|
{:else if validNodeVar(spec, node)}
|
||||||
<ComfyNumberProperty
|
<div class="props-entry">
|
||||||
name={spec.name}
|
{#if spec.type === "string"}
|
||||||
|
<TextBox
|
||||||
value={getVar(node, spec)}
|
value={getVar(node, spec)}
|
||||||
step={spec.step || 1}
|
|
||||||
min={spec.min || -1024}
|
|
||||||
max={spec.max || 1024}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
on:change={(e) => updateVar(spec, e.detail)}
|
||||||
/>
|
on:input={(e) => updateVar(spec, e.detail)}
|
||||||
{:else if spec.type === "enum"}
|
label={spec.name}
|
||||||
<ComfyComboProperty
|
|
||||||
name={spec.name}
|
|
||||||
value={getVar(node, spec)}
|
|
||||||
values={spec.values}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
on:change={(e) => updateVar(spec, e.detail)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else if !node && !target && validWorkflowAttribute(spec)}
|
|
||||||
<div class="props-entry">
|
|
||||||
{#if spec.type === "string"}
|
|
||||||
<TextBox
|
|
||||||
value={getWorkflowAttribute(spec)}
|
|
||||||
on:change={(e) => 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"}
|
|
||||||
<Checkbox
|
|
||||||
value={getWorkflowAttribute(spec)}
|
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
label={spec.name}
|
|
||||||
/>
|
|
||||||
{:else if spec.type === "number"}
|
|
||||||
<ComfyNumberProperty
|
|
||||||
name={spec.name}
|
|
||||||
value={getWorkflowAttribute(spec)}
|
|
||||||
step={spec.step || 1}
|
|
||||||
min={spec.min || -1024}
|
|
||||||
max={spec.max || 1024}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
|
||||||
/>
|
|
||||||
{:else if spec.type === "enum"}
|
|
||||||
<ComfyComboProperty
|
|
||||||
name={spec.name}
|
|
||||||
value={getWorkflowAttribute(spec)}
|
|
||||||
values={spec.values}
|
|
||||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
max_lines={spec.multiline ? 5 : 1}
|
||||||
/>
|
/>
|
||||||
|
{:else if spec.type === "boolean"}
|
||||||
|
<Checkbox
|
||||||
|
value={getVar(node, spec)}
|
||||||
|
on:change={(e) => updateVar(spec, e.detail)}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
label={spec.name}
|
||||||
|
/>
|
||||||
|
{:else if spec.type === "number"}
|
||||||
|
<ComfyNumberProperty
|
||||||
|
name={spec.name}
|
||||||
|
value={getVar(node, spec)}
|
||||||
|
step={spec.step || 1}
|
||||||
|
min={spec.min || -1024}
|
||||||
|
max={spec.max || 1024}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
on:change={(e) => updateVar(spec, e.detail)}
|
||||||
|
/>
|
||||||
|
{:else if spec.type === "enum"}
|
||||||
|
<ComfyComboProperty
|
||||||
|
name={spec.name}
|
||||||
|
value={getVar(node, spec)}
|
||||||
|
values={spec.values}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
on:change={(e) => updateVar(spec, e.detail)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if !node && !target && validWorkflowAttribute(spec)}
|
||||||
|
<div class="props-entry">
|
||||||
|
{#if spec.type === "string"}
|
||||||
|
<TextBox
|
||||||
|
value={getWorkflowAttribute(spec)}
|
||||||
|
on:change={(e) => 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"}
|
||||||
|
<Checkbox
|
||||||
|
value={getWorkflowAttribute(spec)}
|
||||||
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
label={spec.name}
|
||||||
|
/>
|
||||||
|
{:else if spec.type === "number"}
|
||||||
|
<ComfyNumberProperty
|
||||||
|
name={spec.name}
|
||||||
|
value={getWorkflowAttribute(spec)}
|
||||||
|
step={spec.step || 1}
|
||||||
|
min={spec.min || -1024}
|
||||||
|
max={spec.max || 1024}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
|
/>
|
||||||
|
{:else if spec.type === "enum"}
|
||||||
|
<ComfyComboProperty
|
||||||
|
name={spec.name}
|
||||||
|
value={getWorkflowAttribute(spec)}
|
||||||
|
values={spec.values}
|
||||||
|
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||||
|
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||||
|
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/each}
|
||||||
{/if}
|
{/each}
|
||||||
{/each}
|
{/key}
|
||||||
{/each}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import Modal from "./Modal.svelte";
|
import Modal from "./Modal.svelte";
|
||||||
import DropZone from "./DropZone.svelte";
|
import DropZone from "./DropZone.svelte";
|
||||||
|
import workflowState from "$lib/stores/workflowState";
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
|
|
||||||
@@ -71,10 +72,17 @@
|
|||||||
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs;
|
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs;
|
||||||
|
|
||||||
let message = "Prompt";
|
let message = "Prompt";
|
||||||
if (subgraphs?.length > 0)
|
if (entry.workflowID != null) {
|
||||||
message = `Prompt: ${subgraphs.join(', ')}`
|
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}`
|
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
||||||
|
|
||||||
if (Object.keys(entry.outputs).length > 0) {
|
if (Object.keys(entry.outputs).length > 0) {
|
||||||
const imageCount = Object.values(entry.outputs).flatMap(o => o.images).length
|
const imageCount = Object.values(entry.outputs).flatMap(o => o.images).length
|
||||||
submessage = `Images: ${imageCount}`
|
submessage = `Images: ${imageCount}`
|
||||||
@@ -84,7 +92,7 @@
|
|||||||
entry,
|
entry,
|
||||||
message,
|
message,
|
||||||
submessage,
|
submessage,
|
||||||
dateStr,
|
date: dateStr,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
images: []
|
images: []
|
||||||
}
|
}
|
||||||
@@ -387,7 +395,7 @@
|
|||||||
|
|
||||||
&.all_cached, &.interrupted {
|
&.all_cached, &.interrupted {
|
||||||
filter: brightness(80%);
|
filter: brightness(80%);
|
||||||
color: var(--neutral-300);
|
color: var(--comfy-accent-soft);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte'
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import { LGraphNode, LGraph } from "@litegraph-ts/core";
|
|
||||||
import type { IWidget } from "@litegraph-ts/core";
|
|
||||||
import ComfyApp from "./ComfyApp";
|
import ComfyApp from "./ComfyApp";
|
||||||
import type { SerializedPanes } from "./ComfyApp"
|
|
||||||
import WidgetContainer from "./WidgetContainer.svelte";
|
import WidgetContainer from "./WidgetContainer.svelte";
|
||||||
import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState";
|
import { type ContainerLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import selectionState from "$lib/stores/selectionState";
|
import selectionState from "$lib/stores/selectionState";
|
||||||
|
|
||||||
import Menu from './menu/Menu.svelte';
|
import Menu from './menu/Menu.svelte';
|
||||||
import MenuOption from './menu/MenuOption.svelte';
|
import MenuOption from './menu/MenuOption.svelte';
|
||||||
import MenuDivider from './menu/MenuDivider.svelte';
|
import MenuDivider from './menu/MenuDivider.svelte';
|
||||||
import Icon from './menu/Icon.svelte'
|
import type { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
|
export let workflow: ComfyWorkflow;
|
||||||
|
|
||||||
|
let layoutState: WritableLayoutStateStore | null;
|
||||||
|
|
||||||
|
$: layoutState = workflow?.layout;
|
||||||
|
|
||||||
let root: IDragItem | null;
|
let root: IDragItem | null;
|
||||||
let dragConfigured: boolean = false;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Serialize UI panel order so it can be restored when workflow is loaded
|
|
||||||
*/
|
|
||||||
export function serialize(): any {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
export function restore(panels: SerializedPanes) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveTo(delta: number | ((cur: number, total: number) => number)) {
|
function moveTo(delta: number | ((cur: number, total: number) => number)) {
|
||||||
const dragItemID = $selectionState.currentSelection[0];
|
const dragItemID = $selectionState.currentSelection[0];
|
||||||
@@ -149,9 +138,11 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="comfy-ui-panes" on:contextmenu={onRightClick}>
|
{#if layoutState != null}
|
||||||
<WidgetContainer bind:dragItem={root} classes={["root-container"]} />
|
<div id="comfy-workflow-view" on:contextmenu={onRightClick}>
|
||||||
</div>
|
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
||||||
@@ -188,7 +179,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#comfy-ui-panes {
|
#comfy-workflow-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
555
src/lib/components/ComfyWorkflowsView.svelte
Normal file
555
src/lib/components/ComfyWorkflowsView.svelte
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||||
|
import { PlusSquareIcon as PlusSquare } from 'svelte-feather-icons';
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import { BlockTitle } from "@gradio/atoms";
|
||||||
|
import ComfyWorkflowView from "./ComfyWorkflowView.svelte";
|
||||||
|
import { Checkbox, TextBox } from "@gradio/form"
|
||||||
|
import ComfyQueue from "./ComfyQueue.svelte";
|
||||||
|
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
|
||||||
|
import ComfyGraphView from "./ComfyGraphView.svelte";
|
||||||
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
import ComfyProperties from "./ComfyProperties.svelte";
|
||||||
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import workflowState, { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||||
|
import selectionState from "$lib/stores/selectionState";
|
||||||
|
import type ComfyApp from './ComfyApp';
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { cubicIn } from 'svelte/easing';
|
||||||
|
|
||||||
|
export let app: ComfyApp;
|
||||||
|
export let uiTheme: string = "gradio-dark" // TODO config
|
||||||
|
|
||||||
|
let workflow: ComfyWorkflow | null = null;
|
||||||
|
let openedWorkflows = []
|
||||||
|
|
||||||
|
let containerElem: HTMLDivElement;
|
||||||
|
let resizeTimeout: NodeJS.Timeout | null;
|
||||||
|
let alreadySetup: Writable<boolean> = writable(false);
|
||||||
|
let fileInput: HTMLInputElement = undefined;
|
||||||
|
let loading = true;
|
||||||
|
|
||||||
|
let appSetupPromise: Promise<void> = null;
|
||||||
|
|
||||||
|
$: workflow = $workflowState.activeWorkflow;
|
||||||
|
$: openedWorkflows = $workflowState.openedWorkflows.map(w => { return { id: w.id } })
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
appSetupPromise = app.setup().then(() => {
|
||||||
|
loading = false;
|
||||||
|
refreshView();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
refreshView();
|
||||||
|
})
|
||||||
|
|
||||||
|
$: if (app) {
|
||||||
|
alreadySetup = app.alreadySetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRefreshCombos() {
|
||||||
|
await app.refreshComboInNodes(undefined, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshView(event?: Event) {
|
||||||
|
clearTimeout(resizeTimeout);
|
||||||
|
resizeTimeout = setTimeout(app.resizeCanvas.bind(app), 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (app?.lCanvas) {
|
||||||
|
app.lCanvas.allow_dragnodes = $uiState.uiUnlocked;
|
||||||
|
app.lCanvas.allow_interaction = $uiState.uiUnlocked;
|
||||||
|
|
||||||
|
if (!$uiState.uiUnlocked) {
|
||||||
|
app.lCanvas.deselectAllNodes();
|
||||||
|
$selectionState.currentSelectionNodes = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($uiState.uiEditMode)
|
||||||
|
$selectionState.currentSelection = []
|
||||||
|
|
||||||
|
let graphSize = 0;
|
||||||
|
let graphTransitioning = false;
|
||||||
|
|
||||||
|
$: if (containerElem) {
|
||||||
|
const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas")
|
||||||
|
if (canvas) {
|
||||||
|
const paneNode = canvas.closest(".splitpanes__pane")
|
||||||
|
if (paneNode) {
|
||||||
|
(paneNode as HTMLElement).ontransitionstart = () => {
|
||||||
|
graphTransitioning = true
|
||||||
|
}
|
||||||
|
(paneNode as HTMLElement).ontransitionend = () => {
|
||||||
|
graphTransitioning = false
|
||||||
|
app.resizeCanvas()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queuePrompt() {
|
||||||
|
app.runDefaultQueueAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGraph() {
|
||||||
|
if (graphSize == 0) {
|
||||||
|
graphSize = 50;
|
||||||
|
app.resizeCanvas();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
graphSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let propsSidebarSize = 0;
|
||||||
|
|
||||||
|
function toggleProps() {
|
||||||
|
if (propsSidebarSize == 0) {
|
||||||
|
propsSidebarSize = 15;
|
||||||
|
app.resizeCanvas();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
propsSidebarSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let queueSidebarSize = 20;
|
||||||
|
|
||||||
|
function toggleQueue() {
|
||||||
|
if (queueSidebarSize == 0) {
|
||||||
|
queueSidebarSize = 20;
|
||||||
|
app.resizeCanvas();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
queueSidebarSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSave(): void {
|
||||||
|
app.querySave()
|
||||||
|
}
|
||||||
|
|
||||||
|
function doLoad(): void {
|
||||||
|
if (!fileInput)
|
||||||
|
return;
|
||||||
|
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWorkflow(): void {
|
||||||
|
app.handleFile(fileInput.files[0]);
|
||||||
|
fileInput.files = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSaveLocal(): void {
|
||||||
|
app.saveStateToLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doLoadDefault() {
|
||||||
|
var confirmed = confirm("Would you like to load the default workflow in a new tab?");
|
||||||
|
if (confirmed) {
|
||||||
|
await app.initDefaultWorkflow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewWorkflow() {
|
||||||
|
app.createNewWorkflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWorkflow(event: Event, workflow: ComfyWorkflow) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
|
||||||
|
if (workflow.isModified) {
|
||||||
|
if (!confirm("This workflow has unsaved changes. Are you sure you want to close it?"))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.closeWorkflow(workflow.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConsider(evt: any) {
|
||||||
|
console.warn(openedWorkflows.length, openedWorkflows, evt.detail.items.length, evt.detail.items)
|
||||||
|
openedWorkflows = evt.detail.items;
|
||||||
|
// openedWorkflows = evt.detail.items.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID);
|
||||||
|
// workflowState.update(s => {
|
||||||
|
// s.openedWorkflows = openedWorkflows.map(w => workflowState.getWorkflow(w.id));
|
||||||
|
// return s;
|
||||||
|
// })
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleFinalize(evt: any) {
|
||||||
|
openedWorkflows = evt.detail.items;
|
||||||
|
workflowState.update(s => {
|
||||||
|
s.openedWorkflows = openedWorkflows.filter(w => w.id !== SHADOW_PLACEHOLDER_ITEM_ID).map(w => workflowState.getWorkflow(w.id));
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="comfy-content" bind:this={containerElem} class:loading>
|
||||||
|
<Splitpanes theme="comfy" on:resize={refreshView}>
|
||||||
|
<Pane bind:size={propsSidebarSize}>
|
||||||
|
<div class="sidebar-wrapper pane-wrapper">
|
||||||
|
<ComfyProperties workflow={$workflowState.activeWorkflow} />
|
||||||
|
</div>
|
||||||
|
</Pane>
|
||||||
|
<Pane>
|
||||||
|
<Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}">
|
||||||
|
<Pane>
|
||||||
|
{#if $workflowState.activeWorkflow != null}
|
||||||
|
<ComfyWorkflowView {app} workflow={$workflowState.activeWorkflow} />
|
||||||
|
{:else}
|
||||||
|
<span>No workflow loaded</span>
|
||||||
|
{/if}
|
||||||
|
</Pane>
|
||||||
|
<Pane bind:size={graphSize}>
|
||||||
|
<ComfyGraphView {app} transitioning={graphTransitioning} />
|
||||||
|
</Pane>
|
||||||
|
</Splitpanes>
|
||||||
|
</Pane>
|
||||||
|
<Pane bind:size={queueSidebarSize}>
|
||||||
|
<div class="sidebar-wrapper pane-wrapper">
|
||||||
|
<ComfyQueue {app} />
|
||||||
|
</div>
|
||||||
|
</Pane>
|
||||||
|
</Splitpanes>
|
||||||
|
<div id="workflow-tabs">
|
||||||
|
<div class="workflow-tab-items"
|
||||||
|
use:dndzone="{{
|
||||||
|
items: openedWorkflows,
|
||||||
|
flipDurationMs: 200,
|
||||||
|
type: "workflow-tab",
|
||||||
|
morphDisabled: true,
|
||||||
|
dropFromOthersDisabled: true,
|
||||||
|
dropTargetStyle: {outline: "none"},
|
||||||
|
}}"
|
||||||
|
on:consider={handleConsider}
|
||||||
|
on:finalize={handleFinalize}>
|
||||||
|
{#each openedWorkflows.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
||||||
|
{@const workflow = workflowState.getWorkflow(item.id)}
|
||||||
|
<button class="workflow-tab"
|
||||||
|
class:selected={item.id === $workflowState.activeWorkflowID}
|
||||||
|
on:click={() => app.setActiveWorkflow(item.id)}>
|
||||||
|
<span class="workflow-tab-title">
|
||||||
|
{workflow.attrs.title}
|
||||||
|
{#if workflow.isModified}
|
||||||
|
*
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<button class="workflow-close-button"
|
||||||
|
on:click={(e) => closeWorkflow(e, workflow)}>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
{#if workflow[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||||
|
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button class="workflow-add-new-button"
|
||||||
|
on:click={createNewWorkflow}>
|
||||||
|
<PlusSquare size="100%" strokeWidth={1.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="bottombar">
|
||||||
|
<div class="bottombar-content">
|
||||||
|
<div class="left">
|
||||||
|
{#if workflow != null && workflow.attrs.queuePromptButtonName != ""}
|
||||||
|
<Button variant="primary" disabled={!$alreadySetup} on:click={queuePrompt}>
|
||||||
|
{workflow.attrs.queuePromptButtonName}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleGraph}>
|
||||||
|
Toggle Graph
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleProps}>
|
||||||
|
Toggle Props
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleQueue}>
|
||||||
|
Toggle Queue
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" disabled={!$alreadySetup} on:click={doSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" disabled={!$alreadySetup} on:click={doSaveLocal}>
|
||||||
|
Save Local
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" disabled={!$alreadySetup} on:click={doLoad}>
|
||||||
|
Load
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" disabled={!$alreadySetup} on:click={doLoadDefault}>
|
||||||
|
Load Default
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" disabled={!$alreadySetup} on:click={doRefreshCombos}>
|
||||||
|
🔄
|
||||||
|
</Button>
|
||||||
|
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||||
|
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
|
||||||
|
<span style="display: inline-flex !important">
|
||||||
|
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
||||||
|
</span>
|
||||||
|
<span class="label" for="ui-edit-mode">
|
||||||
|
<BlockTitle>UI Edit mode</BlockTitle>
|
||||||
|
<select id="ui-edit-mode" name="ui-edit-mode" bind:value={$uiState.uiEditMode}>
|
||||||
|
<option value="widgets">Widgets</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<span class="label" for="ui-theme">
|
||||||
|
<BlockTitle>Theme</BlockTitle>
|
||||||
|
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
|
||||||
|
<option value="gradio-dark">Gradio Dark</option>
|
||||||
|
<option value="gradio-light">Gradio Light</option>
|
||||||
|
<option value="anapnoe">Anapnoe</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<ComfyUnlockUIButton bind:toggled={$uiState.uiUnlocked} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
|
||||||
|
|
||||||
|
{#if appSetupPromise}
|
||||||
|
{#await appSetupPromise}
|
||||||
|
<div class="comfy-app-loading">
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
{:catch error}
|
||||||
|
<div class="comfy-loading-error">
|
||||||
|
<div>
|
||||||
|
Error loading app
|
||||||
|
</div>
|
||||||
|
<div>{error}</div>
|
||||||
|
{#if error.stack}
|
||||||
|
{@const lines = error.stack.split("\n")}
|
||||||
|
{#each lines as line}
|
||||||
|
<div style:font-size="16px">{line}</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$top-bar-height: 3.5rem;
|
||||||
|
$workflow-tabs-height: 2.5rem;
|
||||||
|
$bottom-bar-height: 5rem;
|
||||||
|
|
||||||
|
.comfy-app-loading, .comfy-loading-error {
|
||||||
|
font-size: 40px;
|
||||||
|
color: var(--body-text-color);
|
||||||
|
justify-content: center;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100000000;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-app-loading > span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#comfy-content {
|
||||||
|
grid-area: content;
|
||||||
|
height: calc(100vh - $bottom-bar-height - $workflow-tabs-height);
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#workflow-tabs, .workflow-tab-items {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#workflow-tabs {
|
||||||
|
background: var(--neutral-800);
|
||||||
|
padding-right: 1em;
|
||||||
|
height: 3rem;
|
||||||
|
.workflow-tab {
|
||||||
|
background: var(--neutral-800);
|
||||||
|
color: var(--neutral-500);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-top: 3px solid var(--neutral-600);
|
||||||
|
border-left: 1px solid var(--neutral-600);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: var(--size-4);
|
||||||
|
cursor: pointer !important;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:has(.workflow-close-button:hover)) {
|
||||||
|
background: var(--neutral-700);
|
||||||
|
color: var(--neutral-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: var(--neutral-700);
|
||||||
|
color: var(--neutral-300);
|
||||||
|
border-top-color: var(--primary-500);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .workflow-close-button {
|
||||||
|
display:block;
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 50%;
|
||||||
|
background: var(--neutral-500);
|
||||||
|
color: var(--neutral-300);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 100%;
|
||||||
|
color: var(--neutral-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-add-new-button {
|
||||||
|
background: var(--neutral-700);
|
||||||
|
color: var(--neutral-400);
|
||||||
|
opacity: 50%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-top: 3px solid var(--neutral-600);
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--size-4);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(100%);
|
||||||
|
opacity: 100%;
|
||||||
|
border-top-color: var(--neutral-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottombar {
|
||||||
|
background: var(--neutral-900);
|
||||||
|
border-left: 2px solid var(--neutral-700);
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--layout-gap);
|
||||||
|
overflow-x: hidden;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: $bottom-bar-height;
|
||||||
|
|
||||||
|
> .bottombar-content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: auto 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
|
||||||
|
> .left {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .right {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 1rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html, body) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
font-family: Arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.splitpanes.comfy>.splitpanes__splitter) {
|
||||||
|
background: var(--comfy-splitpanes-background-fill);
|
||||||
|
|
||||||
|
&:hover:not([disabled]) {
|
||||||
|
background: var(--comfy-splitpanes-background-fill-hover);
|
||||||
|
}
|
||||||
|
&:active:not([disabled]) {
|
||||||
|
background: var(--comfy-splitpanes-background-fill-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$splitter-size: 1rem;
|
||||||
|
|
||||||
|
:global(.splitpanes.comfy.splitpanes--horizontal>.splitpanes__splitter) {
|
||||||
|
min-height: $splitter-size;
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.splitpanes.comfy.splitpanes--vertical>.splitpanes__splitter) {
|
||||||
|
min-width: $splitter-size;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.splitpanes.comfy) {
|
||||||
|
max-height: calc(100vh - $bottom-bar-height);
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.splitpanes__pane) {
|
||||||
|
box-shadow: 0 0 3px rgba(0, 0, 0, .2) inset;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.label > :global(span) {
|
||||||
|
top: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.left {
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#comfy-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-item-shadow {
|
||||||
|
visibility: visible;
|
||||||
|
border: 1px dashed grey;
|
||||||
|
background: lightblue;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,10 +6,11 @@
|
|||||||
import TabsContainer from "./TabsContainer.svelte"
|
import TabsContainer from "./TabsContainer.svelte"
|
||||||
|
|
||||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
// 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 type { Writable } from "svelte/store";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
|
||||||
|
export let layoutState: WritableLayoutStateStore;
|
||||||
export let container: ContainerLayout | null = null;
|
export let container: ContainerLayout | null = null;
|
||||||
export let zIndex: number = 0;
|
export let zIndex: number = 0;
|
||||||
export let classes: string[] = [];
|
export let classes: string[] = [];
|
||||||
@@ -31,11 +32,11 @@
|
|||||||
{#key $attrsChanged}
|
{#key $attrsChanged}
|
||||||
{#if edit || !isHidden(container)}
|
{#if edit || !isHidden(container)}
|
||||||
{#if container.attrs.variant === "tabs"}
|
{#if container.attrs.variant === "tabs"}
|
||||||
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
<TabsContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||||
{:else if container.attrs.variant === "accordion"}
|
{:else if container.attrs.variant === "accordion"}
|
||||||
<AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
<AccordionContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||||
{:else}
|
{:else}
|
||||||
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
<BlockContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
|
|||||||
@@ -162,6 +162,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
max-width: 30vw;
|
max-width: 30vw;
|
||||||
|
|
||||||
|
> :global(.block) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-button {
|
.copy-button {
|
||||||
|
|||||||
154
src/lib/components/Sidebar.svelte
Normal file
154
src/lib/components/Sidebar.svelte
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<script context="module">
|
||||||
|
export const TABS = {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { setContext, createEventDispatcher } from "svelte";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
import type { SelectData } from "@gradio/utils";
|
||||||
|
import { SvelteComponentDev } from "svelte/internal";
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
name: string;
|
||||||
|
id: object;
|
||||||
|
icon: typeof SvelteComponentDev | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let visible: boolean = true;
|
||||||
|
export let elem_id: string = "id";
|
||||||
|
export let elem_classes: Array<string> = [];
|
||||||
|
export let selected: number | string | object;
|
||||||
|
|
||||||
|
let tabs: Array<Tab> = [];
|
||||||
|
|
||||||
|
const selected_tab = writable<false | object | number | string>(false);
|
||||||
|
const selected_tab_index = writable<number>(0);
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
change: undefined;
|
||||||
|
select: SelectData;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
setContext(TABS, {
|
||||||
|
register_tab: (tab: Tab) => {
|
||||||
|
tabs.push({ name: tab.name, id: tab.id, icon: tab.icon });
|
||||||
|
selected_tab.update((current) => current ?? tab.id);
|
||||||
|
tabs = tabs;
|
||||||
|
return tabs.length - 1;
|
||||||
|
},
|
||||||
|
unregister_tab: (tab: Tab) => {
|
||||||
|
const i = tabs.findIndex((t) => t.id === tab.id);
|
||||||
|
tabs.splice(i, 1);
|
||||||
|
selected_tab.update((current) =>
|
||||||
|
current === tab.id ? tabs[i]?.id || tabs[tabs.length - 1]?.id : current
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selected_tab,
|
||||||
|
selected_tab_index
|
||||||
|
});
|
||||||
|
|
||||||
|
function change_tab(id: object | string | number) {
|
||||||
|
selected = id;
|
||||||
|
$selected_tab = id;
|
||||||
|
$selected_tab_index = tabs.findIndex((t) => t.id === id);
|
||||||
|
dispatch("change");
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selected !== null && change_tab(selected);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sidebar {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
|
||||||
|
<div class="sidebar-nav scroll-hide">
|
||||||
|
{#each tabs as t, i (t.id)}
|
||||||
|
{#if t.id === $selected_tab}
|
||||||
|
<button class="selected">
|
||||||
|
{#if t.icon !== null}
|
||||||
|
<svelte:component this={t.icon} size="100%" strokeWidth={1.5} />
|
||||||
|
{:else}
|
||||||
|
{t.name}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
change_tab(t.id);
|
||||||
|
dispatch("select", { value: t.name, index: i });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if t.icon !== null}
|
||||||
|
<svelte:component this={t.icon} size="100%" strokeWidth={1.5} />
|
||||||
|
{:else}
|
||||||
|
{t.name}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<div class="sidebar-rest"/>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.sidebar {
|
||||||
|
background: var(--neutral-900);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
float: left;
|
||||||
|
top: 0px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav, .sidebar-rest {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 4rem;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--neutral-800);
|
||||||
|
|
||||||
|
> button {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: var(--neutral-600);
|
||||||
|
border-right: 3px solid transparent;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--neutral-700);
|
||||||
|
color: var(--neutral-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: var(--neutral-700);
|
||||||
|
color: var(--neutral-300);
|
||||||
|
border-right-color: var(--primary-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-rest {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
z-index: 999;
|
||||||
|
background: var(--background-fill-primary);
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
48
src/lib/components/SidebarItem.svelte
Normal file
48
src/lib/components/SidebarItem.svelte
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount, createEventDispatcher, tick } from "svelte";
|
||||||
|
import { TABS } from "./Sidebar.svelte";
|
||||||
|
import Column from "$lib/components/gradio/app/Column.svelte"
|
||||||
|
import type { SelectData } from "@gradio/utils";
|
||||||
|
import { SvelteComponentDev } from "svelte/internal";
|
||||||
|
|
||||||
|
export let elem_id: string = "";
|
||||||
|
export let elem_classes: Array<string> = [];
|
||||||
|
export let name: string;
|
||||||
|
export let id: string | number | object = {};
|
||||||
|
export let icon: typeof SvelteComponentDev | null = null;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ select: SelectData }>();
|
||||||
|
|
||||||
|
const { register_tab, unregister_tab, selected_tab, selected_tab_index } =
|
||||||
|
getContext(TABS) as any;
|
||||||
|
|
||||||
|
let tab_index = register_tab({ name, id, icon });
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
return () => unregister_tab({ name, id, icon });
|
||||||
|
});
|
||||||
|
|
||||||
|
$: $selected_tab_index === tab_index &&
|
||||||
|
tick().then(() => dispatch("select", { value: name, index: tab_index }));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id={elem_id}
|
||||||
|
class="sidebar-item {elem_classes.join(' ')}"
|
||||||
|
style:display={$selected_tab === id ? "block" : "none"}
|
||||||
|
>
|
||||||
|
<div style:height="100%">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
width: calc(100% - 4rem);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,11 +11,12 @@
|
|||||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||||
import {cubicIn} from 'svelte/easing';
|
import {cubicIn} from 'svelte/easing';
|
||||||
import { flip } from 'svelte/animate';
|
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 { startDrag, stopDrag } from "$lib/utils"
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
|
||||||
|
export let layoutState: WritableLayoutStateStore;
|
||||||
export let container: ContainerLayout | null = null;
|
export let container: ContainerLayout | null = null;
|
||||||
export let zIndex: number = 0;
|
export let zIndex: number = 0;
|
||||||
export let classes: string[] = [];
|
export let classes: string[] = [];
|
||||||
@@ -24,19 +25,17 @@
|
|||||||
export let dragDisabled: boolean = false;
|
export let dragDisabled: boolean = false;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|
||||||
let attrsChanged: Writable<boolean> | null = null;
|
// let attrsChanged: Writable<number> = writable(0);
|
||||||
let children: IDragItem[] = [];
|
let children: IDragItem[] = [];
|
||||||
const flipDurationMs = 100;
|
const flipDurationMs = 100;
|
||||||
|
|
||||||
let selectedIndex: number = 0;
|
|
||||||
|
|
||||||
$: if (container) {
|
$: if (container) {
|
||||||
children = $layoutState.allItems[container.id].children;
|
children = $layoutState.allItems[container.id].children;
|
||||||
attrsChanged = container.attrsChanged
|
// attrsChanged = container.attrsChanged
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
children = [];
|
children = [];
|
||||||
attrsChanged = null
|
// attrsChanged = writable(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConsider(evt: any) {
|
function handleConsider(evt: any) {
|
||||||
@@ -66,6 +65,14 @@
|
|||||||
function handleSelect() {
|
function handleSelect() {
|
||||||
navigator.vibrate(20)
|
navigator.vibrate(20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
startDrag(e, layoutState)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _stopDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
stopDrag(e, layoutState)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container}
|
{#if container}
|
||||||
@@ -103,7 +110,7 @@
|
|||||||
<label for={String(item.id)}>
|
<label for={String(item.id)}>
|
||||||
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
|
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
|
||||||
</label>
|
</label>
|
||||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||||
{#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'/>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -112,18 +119,26 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if isHidden(container) && edit}
|
{#if isHidden(container) && edit}
|
||||||
<div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
|
<div class="handle handle-hidden"
|
||||||
|
style:z-index={zIndex+100}
|
||||||
|
class:hidden={!edit} />
|
||||||
{/if}
|
{/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>
|
||||||
{:else}
|
{:else}
|
||||||
<Tabs elem_classes={["gradio-tabs"]} on:select={handleSelect}>
|
<Tabs elem_classes={["gradio-tabs"]} on:select={handleSelect}>
|
||||||
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
|
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
|
||||||
{@const tabName = getTabName(container, i)}
|
{@const tabName = getTabName(container, i)}
|
||||||
<TabItem name={tabName}>
|
<TabItem id={tabName} name={tabName}>
|
||||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||||
</TabItem>
|
</TabItem>
|
||||||
{/each}
|
{/each}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -2,21 +2,22 @@
|
|||||||
import queueState from "$lib/stores/queueState";
|
import queueState from "$lib/stores/queueState";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
|
|
||||||
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 selectionState from "$lib/stores/selectionState";
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import { startDrag, stopDrag } from "$lib/utils"
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
import Container from "./Container.svelte"
|
import Container from "./Container.svelte"
|
||||||
import { type Writable } from "svelte/store"
|
import { writable, type Writable } from "svelte/store"
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
|
||||||
|
export let layoutState: WritableLayoutStateStore;
|
||||||
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[] = [];
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
let container: ContainerLayout | null = null;
|
let container: ContainerLayout | null = null;
|
||||||
let attrsChanged: Writable<boolean> | null = null;
|
let attrsChanged: Writable<number> = writable(0);
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> = writable(0);
|
||||||
let widget: WidgetLayout | null = null;
|
let widget: WidgetLayout | null = null;
|
||||||
let showHandles: boolean = false;
|
let showHandles: boolean = false;
|
||||||
|
|
||||||
@@ -24,8 +25,8 @@
|
|||||||
dragItem = null;
|
dragItem = null;
|
||||||
container = null;
|
container = null;
|
||||||
widget = null;
|
widget = null;
|
||||||
attrsChanged = null;
|
attrsChanged = writable(0);
|
||||||
propsChanged = null;
|
propsChanged = writable(0);
|
||||||
}
|
}
|
||||||
else if (dragItem.type === "container") {
|
else if (dragItem.type === "container") {
|
||||||
container = dragItem as ContainerLayout;
|
container = dragItem as ContainerLayout;
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
if (widget.node && "propsChanged" in widget.node)
|
if (widget.node && "propsChanged" in widget.node)
|
||||||
propsChanged = (widget.node as ComfyWidgetNode).propsChanged
|
propsChanged = (widget.node as ComfyWidgetNode).propsChanged
|
||||||
else
|
else
|
||||||
propsChanged = null;
|
propsChanged = writable(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: showHandles = $uiState.uiUnlocked
|
$: showHandles = $uiState.uiUnlocked
|
||||||
@@ -57,12 +58,20 @@
|
|||||||
const title = widget.node.type.replace("/", "-").replace(".", "-")
|
const title = widget.node.type.replace("/", "-").replace(".", "-")
|
||||||
return `widget--${title}`
|
return `widget--${title}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
startDrag(e, layoutState)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _stopDrag(e: MouseEvent | TouchEvent) {
|
||||||
|
stopDrag(e, layoutState)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
{#if container}
|
{#if container}
|
||||||
{#key $attrsChanged}
|
{#key $attrsChanged}
|
||||||
<Container {container} {classes} {zIndex} {showHandles} {isMobile} />
|
<Container {layoutState} {container} {classes} {zIndex} {showHandles} {isMobile} />
|
||||||
{/key}
|
{/key}
|
||||||
{:else if widget && widget.node}
|
{:else if widget && widget.node}
|
||||||
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"}
|
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"}
|
||||||
@@ -78,13 +87,19 @@
|
|||||||
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id}
|
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id}
|
||||||
class:hidden={hidden}
|
class:hidden={hidden}
|
||||||
>
|
>
|
||||||
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
|
<svelte:component this={widget.node.svelteComponentType} {layoutState} {widget} {isMobile} />
|
||||||
</div>
|
</div>
|
||||||
{#if hidden && edit}
|
{#if hidden && edit}
|
||||||
<div class="handle handle-hidden" class:hidden={!edit} />
|
<div class="handle handle-hidden" class:hidden={!edit} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if showHandles || hovered}
|
{#if showHandles || hovered}
|
||||||
<div class="handle handle-widget" class:hovered data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
<div class="handle handle-widget"
|
||||||
|
class:hovered
|
||||||
|
data-drag-item-id={widget.id}
|
||||||
|
on:mousedown={_startDrag}
|
||||||
|
on:touchstart={_startDrag}
|
||||||
|
on:mouseup={_stopDrag}
|
||||||
|
on:touchend={_stopDrag}/>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
{/key}
|
{/key}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import type SerializedAppState from "./ComfyApp"
|
import type { SerializedAppState } from "./components/ComfyApp"
|
||||||
|
|
||||||
const blankGraph: SerializedAppState = {
|
const blankGraph: SerializedAppState = {
|
||||||
createdBy: "ComfyBox",
|
createdBy: "ComfyBox",
|
||||||
version: 1,
|
version: 1,
|
||||||
|
attrs: {
|
||||||
|
title: "New Workflow",
|
||||||
|
queuePromptButtonName: "Queue Prompt",
|
||||||
|
queuePromptButtonRunWorkflow: true
|
||||||
|
},
|
||||||
workflow: {
|
workflow: {
|
||||||
last_node_id: 0,
|
last_node_id: 0,
|
||||||
last_link_id: 0,
|
last_link_id: 0,
|
||||||
@@ -13,7 +18,14 @@ const blankGraph: SerializedAppState = {
|
|||||||
extra: {},
|
extra: {},
|
||||||
version: 0
|
version: 0
|
||||||
},
|
},
|
||||||
panes: {}
|
layout: {
|
||||||
|
root: null,
|
||||||
|
allItems: {},
|
||||||
|
},
|
||||||
|
canvas: {
|
||||||
|
offset: [0, 0],
|
||||||
|
scale: 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { blankGraph }
|
export { blankGraph }
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import ComfyGraph from '$lib/ComfyGraph';
|
import ComfyGraph from '$lib/ComfyGraph';
|
||||||
import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
|
import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
|
||||||
import layoutState from './stores/layoutState';
|
import layoutStates from './stores/layoutStates';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
import workflowState from './stores/workflowState';
|
||||||
|
|
||||||
export function configureLitegraph(isMobile: boolean = false) {
|
export function configureLitegraph(isMobile: boolean = false) {
|
||||||
LiteGraph.catch_exceptions = false;
|
LiteGraph.catch_exceptions = false;
|
||||||
@@ -28,5 +29,7 @@ export function configureLitegraph(isMobile: boolean = false) {
|
|||||||
|
|
||||||
(window as any).LiteGraph = LiteGraph;
|
(window as any).LiteGraph = LiteGraph;
|
||||||
(window as any).LGraphCanvas = LGraphCanvas;
|
(window as any).LGraphCanvas = LGraphCanvas;
|
||||||
(window as any).layoutState = get(layoutState)
|
(window as any).layoutStates = layoutStates;
|
||||||
|
(window as any).workflowState = workflowState;
|
||||||
|
(window as any).svelteGet = get;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import layoutState, { type DragItemID } from "$lib/stores/layoutState";
|
import { type DragItemID } from "$lib/stores/layoutStates";
|
||||||
import queueState from "$lib/stores/queueState";
|
import queueState from "$lib/stores/queueState";
|
||||||
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
@@ -8,7 +8,7 @@ import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"
|
|||||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import type { NotifyOptions } from "$lib/notify";
|
import type { NotifyOptions } from "$lib/notify";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import { type ComfyExecutionResult, type ComfyImageLocation, convertComfyOutputToGradio, type ComfyUploadImageAPIResponse, parseWhateverIntoComfyImageLocations } from "$lib/utils";
|
import { type SerializedPromptOutput, type ComfyImageLocation, convertComfyOutputToGradio, type ComfyUploadImageAPIResponse, parseWhateverIntoComfyImageLocations } from "$lib/utils";
|
||||||
|
|
||||||
export class ComfyQueueEvents extends ComfyGraphNode {
|
export class ComfyQueueEvents extends ComfyGraphNode {
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -63,7 +63,7 @@ LiteGraph.registerNodeType({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
|
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
|
||||||
images: ComfyExecutionResult | null
|
images: SerializedPromptOutput | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyStoreImagesAction extends ComfyGraphNode {
|
export class ComfyStoreImagesAction extends ComfyGraphNode {
|
||||||
@@ -90,7 +90,7 @@ export class ComfyStoreImagesAction extends ComfyGraphNode {
|
|||||||
if (action !== "store" || !param || !("images" in param))
|
if (action !== "store" || !param || !("images" in param))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.setProperty("images", param as ComfyExecutionResult)
|
this.setProperty("images", param as SerializedPromptOutput)
|
||||||
this.setOutputData(0, this.properties.images)
|
this.setOutputData(0, this.properties.images)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ export class ComfyNotifyAction extends ComfyGraphNode {
|
|||||||
// native notifications.
|
// native notifications.
|
||||||
if (param != null && typeof param === "object") {
|
if (param != null && typeof param === "object") {
|
||||||
if ("images" in param) {
|
if ("images" in param) {
|
||||||
const output = param as ComfyExecutionResult;
|
const output = param as SerializedPromptOutput;
|
||||||
const converted = convertComfyOutputToGradio(output);
|
const converted = convertComfyOutputToGradio(output);
|
||||||
if (converted.length > 0)
|
if (converted.length > 0)
|
||||||
options.imageUrl = converted[0].data;
|
options.imageUrl = converted[0].data;
|
||||||
@@ -389,7 +389,7 @@ export class ComfySetNodeModeAction extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of Object.values(get(layoutState).allItems)) {
|
for (const entry of Object.values(get(this.layoutState).allItems)) {
|
||||||
if (entry.dragItem.type === "container") {
|
if (entry.dragItem.type === "container") {
|
||||||
const container = entry.dragItem;
|
const container = entry.dragItem;
|
||||||
const hasTag = tags.some(t => container.attrs.tags.indexOf(t) != -1);
|
const hasTag = tags.some(t => container.attrs.tags.indexOf(t) != -1);
|
||||||
@@ -488,7 +488,7 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of Object.values(get(layoutState).allItems)) {
|
for (const entry of Object.values(get(this.layoutState).allItems)) {
|
||||||
if (entry.dragItem.type === "container") {
|
if (entry.dragItem.type === "container") {
|
||||||
const container = entry.dragItem;
|
const container = entry.dragItem;
|
||||||
const hasTag = container.attrs.tags.indexOf(action.tag) != -1;
|
const hasTag = container.attrs.tags.indexOf(action.tag) != -1;
|
||||||
@@ -532,7 +532,7 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
|||||||
this.graph.getNodeByIdRecursive(nodeId).changeMode(newMode);
|
this.graph.getNodeByIdRecursive(nodeId).changeMode(newMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const layout = get(layoutState);
|
const layout = get(this.layoutState);
|
||||||
for (const [dragItemID, isHidden] of Object.entries(widgetChanges)) {
|
for (const [dragItemID, isHidden] of Object.entries(widgetChanges)) {
|
||||||
const container = layout.allItems[dragItemID].dragItem
|
const container = layout.allItems[dragItemID].dragItem
|
||||||
container.attrs.hidden = isHidden;
|
container.attrs.hidden = isHidden;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { BuiltInSlotShape, BuiltInSlotType, type SerializedLGraphNode } from "@l
|
|||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||||
import { iterateNodeDefOutputs, type ComfyNodeDef, iterateNodeDefInputs } from "$lib/ComfyNodeDef";
|
import { iterateNodeDefOutputs, type ComfyNodeDef, iterateNodeDefInputs } from "$lib/ComfyNodeDef";
|
||||||
import type { ComfyExecutionResult } from "$lib/utils";
|
import type { SerializedPromptOutput } from "$lib/utils";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Base class for any node with configuration sent by the backend.
|
* Base class for any node with configuration sent by the backend.
|
||||||
@@ -111,7 +111,7 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExecuted(outputData: ComfyExecutionResult) {
|
override onExecuted(outputData: SerializedPromptOutput) {
|
||||||
console.warn("onExecuted outputs", outputData)
|
console.warn("onExecuted outputs", outputData)
|
||||||
this.triggerSlot(0, outputData)
|
this.triggerSlot(0, outputData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import layoutState from "$lib/stores/layoutState"
|
import layoutStates from "$lib/stores/layoutStates"
|
||||||
import { BuiltInSlotType, LGraphNode, LiteGraph, type ITextWidget, type OptionalSlots, type PropertyLayout, type SlotLayout, type Vector2 } from "@litegraph-ts/core"
|
import { BuiltInSlotType, LGraphNode, LiteGraph, type ITextWidget, type OptionalSlots, type PropertyLayout, type SlotLayout, type Vector2 } from "@litegraph-ts/core"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import ComfyGraphNode from "./ComfyGraphNode"
|
||||||
|
|
||||||
export interface ComfyConfigureQueuePromptButtonProperties extends Record<string, any> {
|
export default class ComfyConfigureQueuePromptButton extends ComfyGraphNode {
|
||||||
}
|
|
||||||
|
|
||||||
export default class ComfyConfigureQueuePromptButton extends LGraphNode {
|
|
||||||
override properties: ComfyConfigureQueuePromptButtonProperties = {
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: "config", type: BuiltInSlotType.ACTION },
|
{ name: "config", type: BuiltInSlotType.ACTION },
|
||||||
@@ -28,13 +24,15 @@ export default class ComfyConfigureQueuePromptButton extends LGraphNode {
|
|||||||
|
|
||||||
override onAction(action: any, param: any, options: { action_call?: string }) {
|
override onAction(action: any, param: any, options: { action_call?: string }) {
|
||||||
if (action === "config" && param != null) {
|
if (action === "config" && param != null) {
|
||||||
layoutState.update(state => {
|
if (this.layoutState == null) {
|
||||||
if (typeof param === "string")
|
console.error(this, this.getRootGraph(), Object.keys(get(layoutStates).all))
|
||||||
state.attrs.queuePromptButtonName = param || ""
|
throw new Error(`Could not find layout attached to this node! ${this.id}`)
|
||||||
else if (typeof param === "object" && "buttonName" in param)
|
}
|
||||||
state.attrs.queuePromptButtonName = param.buttonName || ""
|
|
||||||
return state
|
if (typeof param === "string")
|
||||||
})
|
this.workflow.setAttribute("queuePromptButtonName", param || "")
|
||||||
|
else if (typeof param === "object" && "buttonName" in param)
|
||||||
|
this.workflow.setAttribute("queuePromptButtonName", param.buttonName || "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
|||||||
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import type { ComfyExecutionResult, ComfyImageLocation } from "$lib/utils"
|
import type { SerializedPromptOutput, ComfyImageLocation } from "$lib/utils"
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import configState from "$lib/stores/configState";
|
import configState from "$lib/stores/configState";
|
||||||
|
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||||
|
import layoutStates from "$lib/stores/layoutStates";
|
||||||
|
import workflowStateStore, { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||||
|
|
||||||
export type DefaultWidgetSpec = {
|
export type DefaultWidgetSpec = {
|
||||||
defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
|
defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
|
||||||
@@ -48,7 +51,7 @@ export default class ComfyGraphNode extends LGraphNode {
|
|||||||
* Triggered when the backend sends a finished output back with this node's ID.
|
* Triggered when the backend sends a finished output back with this node's ID.
|
||||||
* Valid for output nodes like SaveImage and PreviewImage.
|
* Valid for output nodes like SaveImage and PreviewImage.
|
||||||
*/
|
*/
|
||||||
onExecuted?(output: ComfyExecutionResult): void;
|
onExecuted?(output: SerializedPromptOutput): void;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* When a prompt is queued, this will be called on the node if it can
|
* When a prompt is queued, this will be called on the node if it can
|
||||||
@@ -100,6 +103,14 @@ export default class ComfyGraphNode extends LGraphNode {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get layoutState(): WritableLayoutStateStore | null {
|
||||||
|
return layoutStates.getLayoutByNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get workflow(): ComfyWorkflow | null {
|
||||||
|
return workflowStateStore.getWorkflowByNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(title?: string) {
|
constructor(title?: string) {
|
||||||
super(title)
|
super(title)
|
||||||
this.addProperty("tags", [], "array")
|
this.addProperty("tags", [], "array")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import layoutState from "$lib/stores/layoutState";
|
|
||||||
import { range } from "$lib/utils";
|
import { range } from "$lib/utils";
|
||||||
import { LConnectionKind, LGraphCanvas, LLink, LiteGraph, NodeMode, type INodeInputSlot, type INodeOutputSlot, type ITextWidget, type LGraphNode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
|
import { LConnectionKind, LGraphCanvas, LLink, LiteGraph, NodeMode, type INodeInputSlot, type INodeOutputSlot, type ITextWidget, type LGraphNode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
|
||||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
@@ -269,7 +268,7 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.setWidgetTitle) {
|
if (options.setWidgetTitle) {
|
||||||
const widget = layoutState.findLayoutForNode(this.id as ComfyNodeID)
|
const widget = this.layoutState.findLayoutForNode(this.id as ComfyNodeID)
|
||||||
if (widget && input.name !== "") {
|
if (widget && input.name !== "") {
|
||||||
widget.attrs.title = input.name;
|
widget.attrs.title = input.name;
|
||||||
}
|
}
|
||||||
@@ -288,7 +287,7 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notifyPropsChanged() {
|
notifyPropsChanged() {
|
||||||
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
const layoutEntry = this.layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
||||||
if (layoutEntry && layoutEntry.parent) {
|
if (layoutEntry && layoutEntry.parent) {
|
||||||
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyNodeID, PromptID } from "$lib/api";
|
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyNodeID, PromptID } from "$lib/api";
|
||||||
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
|
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs, WorkflowInstID } from "$lib/components/ComfyApp";
|
||||||
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
@@ -11,12 +11,13 @@ type QueueStateOps = {
|
|||||||
historyUpdated: (resp: ComfyAPIHistoryResponse) => void,
|
historyUpdated: (resp: ComfyAPIHistoryResponse) => void,
|
||||||
statusUpdated: (status: ComfyAPIStatusResponse | null) => void,
|
statusUpdated: (status: ComfyAPIStatusResponse | null) => void,
|
||||||
executionStart: (promptID: PromptID) => void,
|
executionStart: (promptID: PromptID) => void,
|
||||||
executingUpdated: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => void,
|
executingUpdated: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => QueueEntry | null;
|
||||||
executionCached: (promptID: PromptID, nodes: ComfyNodeID[]) => void,
|
executionCached: (promptID: PromptID, nodes: ComfyNodeID[]) => void,
|
||||||
executionError: (promptID: PromptID, message: string) => void,
|
executionError: (promptID: PromptID, message: string) => void,
|
||||||
progressUpdated: (progress: Progress) => void
|
progressUpdated: (progress: Progress) => void
|
||||||
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
getQueueEntry: (promptID: PromptID) => QueueEntry | null;
|
||||||
onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void
|
afterQueued: (workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
||||||
|
onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => QueueEntry | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -36,6 +37,8 @@ export type QueueEntry = {
|
|||||||
|
|
||||||
/*** Data not sent by ComfyUI's API, lost on page refresh ***/
|
/*** Data not sent by ComfyUI's API, lost on page refresh ***/
|
||||||
|
|
||||||
|
/* Workflow tab that sent the prompt. */
|
||||||
|
workflowID?: WorkflowInstID,
|
||||||
/* Prompt outputs, collected while the prompt is still executing */
|
/* Prompt outputs, collected while the prompt is still executing */
|
||||||
outputs: SerializedPromptOutputs,
|
outputs: SerializedPromptOutputs,
|
||||||
/* Nodes of the workflow that have finished running so far. */
|
/* Nodes of the workflow that have finished running so far. */
|
||||||
@@ -150,6 +153,21 @@ function statusUpdated(status: ComfyAPIStatusResponse | null) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQueueEntry(promptID: PromptID): QueueEntry | null {
|
||||||
|
const state = get(store);
|
||||||
|
|
||||||
|
let found = get(state.queuePending).find(e => e.promptID === promptID)
|
||||||
|
if (found != null) return found;
|
||||||
|
|
||||||
|
found = get(state.queueRunning).find(e => e.promptID === promptID)
|
||||||
|
if (found != null) return found;
|
||||||
|
|
||||||
|
let foundCompleted = get(state.queueCompleted).find(e => e.entry.promptID === promptID)
|
||||||
|
if (foundCompleted != null) return foundCompleted.entry;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function findEntryInPending(promptID: PromptID): [number, QueueEntry | null, Writable<QueueEntry[]> | null] {
|
function findEntryInPending(promptID: PromptID): [number, QueueEntry | null, Writable<QueueEntry[]> | null] {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
let index = get(state.queuePending).findIndex(e => e.promptID === promptID)
|
let index = get(state.queuePending).findIndex(e => e.promptID === promptID)
|
||||||
@@ -180,8 +198,10 @@ function moveToCompleted(index: number, queue: Writable<QueueEntry[]>, status: Q
|
|||||||
store.set(state)
|
store.set(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) {
|
function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null): QueueEntry | null {
|
||||||
console.debug("[queueState] executingUpdated", promptID, runningNodeID)
|
console.debug("[queueState] executingUpdated", promptID, runningNodeID)
|
||||||
|
let entry_ = null;
|
||||||
|
|
||||||
store.update((s) => {
|
store.update((s) => {
|
||||||
s.progress = null;
|
s.progress = null;
|
||||||
|
|
||||||
@@ -214,8 +234,11 @@ function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null)
|
|||||||
s.progress = null;
|
s.progress = null;
|
||||||
s.runningNodeID = null;
|
s.runningNodeID = null;
|
||||||
}
|
}
|
||||||
|
entry_ = entry;
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return entry_;
|
||||||
}
|
}
|
||||||
|
|
||||||
function executionCached(promptID: PromptID, nodes: ComfyNodeID[]) {
|
function executionCached(promptID: PromptID, nodes: ComfyNodeID[]) {
|
||||||
@@ -283,16 +306,18 @@ function executionStart(promptID: PromptID) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) {
|
function afterQueued(workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) {
|
||||||
console.debug("[queueState] afterQueued", promptID, Object.keys(prompt))
|
console.debug("[queueState] afterQueued", promptID, Object.keys(prompt))
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
const [index, entry, queue] = findEntryInPending(promptID);
|
const [index, entry, queue] = findEntryInPending(promptID);
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
const entry = createNewQueueEntry(promptID, number, prompt, extraData);
|
const entry = createNewQueueEntry(promptID, number, prompt, extraData);
|
||||||
|
entry.workflowID = workflowID;
|
||||||
s.queuePending.update(qp => { qp.push(entry); return qp })
|
s.queuePending.update(qp => { qp.push(entry); return qp })
|
||||||
console.debug("[queueState] ADD PROMPT", promptID)
|
console.debug("[queueState] ADD PROMPT", promptID)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
entry.workflowID = workflowID;
|
||||||
entry.number = number;
|
entry.number = number;
|
||||||
entry.prompt = prompt
|
entry.prompt = prompt
|
||||||
entry.extraData = extraData
|
entry.extraData = extraData
|
||||||
@@ -304,19 +329,22 @@ function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) {
|
function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, outputs: ComfyExecutionResult): QueueEntry | null {
|
||||||
console.debug("[queueState] onExecuted", promptID, nodeID, output)
|
console.debug("[queueState] onExecuted", promptID, nodeID, outputs)
|
||||||
|
let entry_ = null;
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
const [index, entry, queue] = findEntryInPending(promptID)
|
const [index, entry, queue] = findEntryInPending(promptID)
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
entry.outputs[nodeID] = output;
|
entry.outputs[nodeID] = outputs;
|
||||||
queue.set(get(queue))
|
queue.set(get(queue))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.error("[queueState] Could not find in pending! (onExecuted)", promptID)
|
console.error("[queueState] Could not find in pending! (onExecuted)", promptID)
|
||||||
}
|
}
|
||||||
|
entry_ = entry;
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
return entry_;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queueStateStore: WritableQueueStateStore =
|
const queueStateStore: WritableQueueStateStore =
|
||||||
@@ -331,6 +359,7 @@ const queueStateStore: WritableQueueStateStore =
|
|||||||
executionCached,
|
executionCached,
|
||||||
executionError,
|
executionError,
|
||||||
afterQueued,
|
afterQueued,
|
||||||
|
getQueueEntry,
|
||||||
onExecuted
|
onExecuted
|
||||||
}
|
}
|
||||||
export default queueStateStore;
|
export default queueStateStore;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import type { Readable, Writable } from 'svelte/store';
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
import type { DragItemID, IDragItem } from './layoutState';
|
import type { DragItemID, IDragItem } from './layoutStates';
|
||||||
import type { LGraphNode, NodeID } from '@litegraph-ts/core';
|
import type { LGraphNode, NodeID } from '@litegraph-ts/core';
|
||||||
|
|
||||||
export type SelectionState = {
|
export type SelectionState = {
|
||||||
|
|||||||
361
src/lib/stores/workflowState.ts
Normal file
361
src/lib/stores/workflowState.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas';
|
||||||
|
import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph } from '@litegraph-ts/core';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
|
import { defaultWorkflowAttributes, type SerializedLayoutState, type WritableLayoutStateStore } from './layoutStates';
|
||||||
|
import ComfyGraph from '$lib/ComfyGraph';
|
||||||
|
import layoutStates from './layoutStates';
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import type ComfyGraphCanvas from '$lib/ComfyGraphCanvas';
|
||||||
|
import { blankGraph } from '$lib/defaultGraph';
|
||||||
|
import type { SerializedAppState } from '$lib/components/ComfyApp';
|
||||||
|
|
||||||
|
type ActiveCanvas = {
|
||||||
|
canvas: LGraphCanvas | null;
|
||||||
|
canvasHandler: () => void | null;
|
||||||
|
state: SerializedGraphCanvasState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedWorkflowState = {
|
||||||
|
graph: SerializedLGraph,
|
||||||
|
layout: SerializedLayoutState,
|
||||||
|
attrs: WorkflowAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ID for an opened workflow.
|
||||||
|
*
|
||||||
|
* Unlike NodeID and PromptID, these are *not* saved to the workflow itself.
|
||||||
|
* They are only used for identifying an open workflow in the program. If the
|
||||||
|
* workflow is closed and reopened, a different workflow ID will be assigned to
|
||||||
|
* it.
|
||||||
|
*/
|
||||||
|
export type WorkflowInstID = UUID;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global workflow attributes
|
||||||
|
*/
|
||||||
|
export type WorkflowAttributes = {
|
||||||
|
/*
|
||||||
|
* Title of the workflow.
|
||||||
|
*/
|
||||||
|
title: string,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name of the "Queue Prompt" button. Set to blank to hide the button.
|
||||||
|
*/
|
||||||
|
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, and instead use the `onDefaultQueueAction` event of the
|
||||||
|
* Comfy.QueueEvents node.
|
||||||
|
*/
|
||||||
|
queuePromptButtonRunWorkflow: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ComfyWorkflow {
|
||||||
|
/*
|
||||||
|
* Used for uniquely identifying the instance of the opened workflow in the frontend.
|
||||||
|
*/
|
||||||
|
id: WorkflowInstID;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Graph of this workflow, whose nodes are bound to the UI layout
|
||||||
|
*/
|
||||||
|
graph: ComfyGraph;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global workflow attributes
|
||||||
|
*/
|
||||||
|
attrs: WorkflowAttributes;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* True if an unsaved modification has been detected on this workflow
|
||||||
|
*/
|
||||||
|
isModified: boolean = false;
|
||||||
|
|
||||||
|
get layout(): WritableLayoutStateStore | null {
|
||||||
|
return layoutStates.getLayout(this.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Graph canvases attached to the graph of this workflow
|
||||||
|
*/
|
||||||
|
canvases: Record<string, ActiveCanvas> = {};
|
||||||
|
|
||||||
|
constructor(title: string) {
|
||||||
|
this.id = uuidv4();
|
||||||
|
this.attrs = {
|
||||||
|
...defaultWorkflowAttributes,
|
||||||
|
title,
|
||||||
|
}
|
||||||
|
this.graph = new ComfyGraph(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyModified() {
|
||||||
|
this.isModified = true;
|
||||||
|
store.set(get(store));
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttribute<K extends keyof WorkflowAttributes>(key: K, value: WorkflowAttributes[K]) {
|
||||||
|
this.attrs[key] = value;
|
||||||
|
this.notifyModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
start(key: string, canvas: ComfyGraphCanvas) {
|
||||||
|
if (this.canvases[key] != null)
|
||||||
|
throw new Error(`This workflow is already being displayed on canvas ${key}`)
|
||||||
|
|
||||||
|
const canvasHandler = () => canvas.draw(true);
|
||||||
|
|
||||||
|
this.canvases[key] = {
|
||||||
|
canvas,
|
||||||
|
canvasHandler,
|
||||||
|
state: {
|
||||||
|
// TODO
|
||||||
|
offset: [0, 0],
|
||||||
|
scale: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.graph.attachCanvas(canvas);
|
||||||
|
this.graph.eventBus.on("afterExecute", canvasHandler)
|
||||||
|
|
||||||
|
if (Object.keys(this.canvases).length === 1)
|
||||||
|
this.graph.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(key: string) {
|
||||||
|
const canvas = this.canvases[key]
|
||||||
|
if (canvas == null) {
|
||||||
|
console.debug("This workflow is not being displayed on canvas ${key}")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.graph.detachCanvas(canvas.canvas);
|
||||||
|
this.graph.eventBus.removeListener("afterExecute", canvas.canvasHandler)
|
||||||
|
|
||||||
|
delete this.canvases[key]
|
||||||
|
|
||||||
|
if (Object.keys(this.canvases).length === 0)
|
||||||
|
this.graph.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAll() {
|
||||||
|
for (const key of Object.keys(this.canvases))
|
||||||
|
this.stop(key)
|
||||||
|
this.graph.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(layoutState: WritableLayoutStateStore): SerializedWorkflowState {
|
||||||
|
const graph = this.graph;
|
||||||
|
|
||||||
|
const serializedGraph = graph.serialize()
|
||||||
|
const serializedLayout = layoutState.serialize()
|
||||||
|
|
||||||
|
return {
|
||||||
|
graph: serializedGraph,
|
||||||
|
layout: serializedLayout,
|
||||||
|
attrs: this.attrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(title: string = "New Workflow"): [ComfyWorkflow, WritableLayoutStateStore] {
|
||||||
|
const workflow = new ComfyWorkflow(title);
|
||||||
|
const layoutState = layoutStates.create(workflow);
|
||||||
|
return [workflow, layoutState]
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(layoutState: WritableLayoutStateStore, data: SerializedWorkflowState) {
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||||
|
for (let n of data.graph.nodes) {
|
||||||
|
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.graph.configure(data.graph);
|
||||||
|
this.graph.workflowID = this.id;
|
||||||
|
|
||||||
|
for (const node of this.graph._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);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attrs = data.attrs;
|
||||||
|
|
||||||
|
// Now restore the layout
|
||||||
|
// Subsequent added nodes will add the UI data to layoutState
|
||||||
|
// TODO
|
||||||
|
layoutState.deserialize(data.layout, this.graph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkflowState = {
|
||||||
|
openedWorkflows: ComfyWorkflow[],
|
||||||
|
openedWorkflowsByID: Record<WorkflowInstID, ComfyWorkflow>,
|
||||||
|
activeWorkflowID: WorkflowInstID | null,
|
||||||
|
activeWorkflow: ComfyWorkflow | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkflowStateOps = {
|
||||||
|
getWorkflow: (id: WorkflowInstID) => ComfyWorkflow | null
|
||||||
|
getWorkflowByGraph: (graph: LGraph) => ComfyWorkflow | null
|
||||||
|
getWorkflowByNode: (node: LGraphNode) => ComfyWorkflow | null
|
||||||
|
getWorkflowByNodeID: (id: NodeID) => ComfyWorkflow | null
|
||||||
|
getActiveWorkflow: () => ComfyWorkflow | null
|
||||||
|
createNewWorkflow: (canvas: ComfyGraphCanvas, title?: string, setActive?: boolean) => ComfyWorkflow,
|
||||||
|
openWorkflow: (canvas: ComfyGraphCanvas, data: SerializedAppState) => ComfyWorkflow,
|
||||||
|
closeWorkflow: (canvas: ComfyGraphCanvas, index: number) => void,
|
||||||
|
closeAllWorkflows: (canvas: ComfyGraphCanvas) => void,
|
||||||
|
setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number) => ComfyWorkflow | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WritableWorkflowStateStore = Writable<WorkflowState> & WorkflowStateOps;
|
||||||
|
const store: Writable<WorkflowState> = writable(
|
||||||
|
{
|
||||||
|
openedWorkflows: [],
|
||||||
|
openedWorkflowsByID: {},
|
||||||
|
activeWorkflowID: null,
|
||||||
|
activeWorkflow: null
|
||||||
|
})
|
||||||
|
|
||||||
|
function getWorkflow(id: WorkflowInstID): ComfyWorkflow | null {
|
||||||
|
return get(store).openedWorkflowsByID[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowByGraph(graph: LGraph): ComfyWorkflow | null {
|
||||||
|
const workflowID = (graph.getRootGraph() as ComfyGraph)?.workflowID;
|
||||||
|
if (workflowID == null)
|
||||||
|
return null;
|
||||||
|
return getWorkflow(workflowID);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowByNode(node: LGraphNode): ComfyWorkflow | null {
|
||||||
|
return getWorkflowByGraph(node.graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowByNodeID(id: NodeID): ComfyWorkflow | null {
|
||||||
|
return Object.values(get(store).openedWorkflows).find(w => {
|
||||||
|
return w.graph.getNodeByIdRecursive(id) != null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveWorkflow(): ComfyWorkflow | null {
|
||||||
|
const state = get(store);
|
||||||
|
if (state.activeWorkflowID == null)
|
||||||
|
return null;
|
||||||
|
return state.openedWorkflowsByID[state.activeWorkflowID];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewWorkflow(canvas: ComfyGraphCanvas, title: string = "New Workflow", setActive: boolean = false): ComfyWorkflow {
|
||||||
|
const workflow = new ComfyWorkflow(title);
|
||||||
|
const layoutState = layoutStates.create(workflow);
|
||||||
|
layoutState.initDefaultLayout();
|
||||||
|
|
||||||
|
const state = get(store);
|
||||||
|
state.openedWorkflows.push(workflow);
|
||||||
|
state.openedWorkflowsByID[workflow.id] = workflow;
|
||||||
|
|
||||||
|
if (setActive)
|
||||||
|
setActiveWorkflow(canvas, state.openedWorkflows.length - 1)
|
||||||
|
|
||||||
|
store.set(state)
|
||||||
|
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWorkflow(canvas: ComfyGraphCanvas, data: SerializedAppState): ComfyWorkflow {
|
||||||
|
const [workflow, layoutState] = ComfyWorkflow.create("Workflow")
|
||||||
|
workflow.deserialize(layoutState, { graph: data.workflow, layout: data.layout, attrs: data.attrs })
|
||||||
|
|
||||||
|
const state = get(store);
|
||||||
|
state.openedWorkflows.push(workflow);
|
||||||
|
state.openedWorkflowsByID[workflow.id] = workflow;
|
||||||
|
setActiveWorkflow(canvas, state.openedWorkflows.length - 1)
|
||||||
|
|
||||||
|
store.set(state)
|
||||||
|
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWorkflow(canvas: ComfyGraphCanvas, index: number) {
|
||||||
|
const state = get(store);
|
||||||
|
|
||||||
|
if (index < 0 || index >= state.openedWorkflows.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const workflow = state.openedWorkflows[index];
|
||||||
|
workflow.stopAll();
|
||||||
|
|
||||||
|
layoutStates.remove(workflow.id)
|
||||||
|
|
||||||
|
|
||||||
|
state.openedWorkflows.splice(index, 1)
|
||||||
|
delete state.openedWorkflowsByID[workflow.id]
|
||||||
|
let newIndex = clamp(index, 0, state.openedWorkflows.length - 1)
|
||||||
|
setActiveWorkflow(canvas, newIndex);
|
||||||
|
|
||||||
|
store.set(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllWorkflows(canvas: ComfyGraphCanvas) {
|
||||||
|
const state = get(store)
|
||||||
|
while (state.openedWorkflows.length > 0)
|
||||||
|
closeWorkflow(canvas, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkflow | null {
|
||||||
|
const state = get(store);
|
||||||
|
|
||||||
|
if (state.openedWorkflows.length === 0) {
|
||||||
|
state.activeWorkflowID = null;
|
||||||
|
state.activeWorkflow = null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= state.openedWorkflows.length)
|
||||||
|
return state.activeWorkflow;
|
||||||
|
|
||||||
|
const workflow = state.openedWorkflows[index]
|
||||||
|
if (workflow.id === state.activeWorkflowID)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (state.activeWorkflow != null)
|
||||||
|
state.activeWorkflow.stop("app")
|
||||||
|
|
||||||
|
state.activeWorkflowID = workflow.id;
|
||||||
|
state.activeWorkflow = workflow;
|
||||||
|
|
||||||
|
workflow.start("app", canvas);
|
||||||
|
canvas.deserialize(workflow.canvases["app"].state)
|
||||||
|
|
||||||
|
store.set(state)
|
||||||
|
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowStateStore: WritableWorkflowStateStore =
|
||||||
|
{
|
||||||
|
...store,
|
||||||
|
getWorkflow,
|
||||||
|
getWorkflowByGraph,
|
||||||
|
getWorkflowByNode,
|
||||||
|
getWorkflowByNodeID,
|
||||||
|
getActiveWorkflow,
|
||||||
|
createNewWorkflow,
|
||||||
|
openWorkflow,
|
||||||
|
closeWorkflow,
|
||||||
|
closeAllWorkflows,
|
||||||
|
setActiveWorkflow,
|
||||||
|
}
|
||||||
|
export default workflowStateStore;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import layoutState, { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||||
import selectionState from "$lib/stores/selectionState";
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID } from "@litegraph-ts/core";
|
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID } from "@litegraph-ts/core";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import type { ComfyNodeID } from "./api";
|
import type { ComfyNodeID } from "./api";
|
||||||
import { type SerializedPrompt } from "./components/ComfyApp";
|
import { type SerializedPrompt } from "./components/ComfyApp";
|
||||||
|
import workflowState from "./stores/workflowState";
|
||||||
|
|
||||||
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)
|
||||||
@@ -39,7 +40,7 @@ export function download(filename: string, text: string, type: string = "text/pl
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startDrag(evt: MouseEvent) {
|
export function startDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore) {
|
||||||
const dragItemId: string = evt.target.dataset["dragItemId"];
|
const dragItemId: string = evt.target.dataset["dragItemId"];
|
||||||
const ss = get(selectionState)
|
const ss = get(selectionState)
|
||||||
const ls = get(layoutState)
|
const ls = get(layoutState)
|
||||||
@@ -78,9 +79,11 @@ export function startDrag(evt: MouseEvent) {
|
|||||||
|
|
||||||
layoutState.set(ls)
|
layoutState.set(ls)
|
||||||
selectionState.set(ss)
|
selectionState.set(ss)
|
||||||
|
layoutState.notifyWorkflowModified();
|
||||||
};
|
};
|
||||||
|
|
||||||
export function stopDrag(evt: MouseEvent) {
|
export function stopDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore) {
|
||||||
|
layoutState.notifyWorkflowModified();
|
||||||
};
|
};
|
||||||
|
|
||||||
export function graphToGraphVis(graph: LGraph): string {
|
export function graphToGraphVis(graph: LGraph): string {
|
||||||
@@ -175,23 +178,33 @@ export function workflowToGraphVis(workflow: SerializedLGraph): string {
|
|||||||
export function promptToGraphVis(prompt: SerializedPrompt): string {
|
export function promptToGraphVis(prompt: SerializedPrompt): string {
|
||||||
let out = "digraph {\n"
|
let out = "digraph {\n"
|
||||||
|
|
||||||
|
const ids: Record<NodeID, number> = {}
|
||||||
|
let nextID = 0;
|
||||||
|
|
||||||
for (const pair of Object.entries(prompt.output)) {
|
for (const pair of Object.entries(prompt.output)) {
|
||||||
const [id, o] = pair;
|
const [id, o] = pair;
|
||||||
const outNode = prompt.workflow.nodes.find(n => n.id == id)
|
if (ids[id] == null)
|
||||||
if (outNode) {
|
ids[id] = nextID++;
|
||||||
|
|
||||||
|
if ("class_type" in o) {
|
||||||
for (const pair2 of Object.entries(o.inputs)) {
|
for (const pair2 of Object.entries(o.inputs)) {
|
||||||
const [inpName, i] = pair2;
|
const [inpName, i] = pair2;
|
||||||
|
|
||||||
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
||||||
// Link
|
// Link
|
||||||
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
|
const [inpID, inpSlot] = i;
|
||||||
|
if (ids[inpID] == null)
|
||||||
|
ids[inpID] = nextID++;
|
||||||
|
|
||||||
|
const inpNode = prompt.output[inpID]
|
||||||
if (inpNode) {
|
if (inpNode) {
|
||||||
out += `"${inpNode.title}" -> "${outNode.title}"\n`
|
out += `"${ids[inpID]}_${inpNode.class_type}" -> "${ids[id]}_${o.class_type}"\n`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
const value = String(i).substring(0, 20)
|
||||||
// Value
|
// Value
|
||||||
out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n`
|
out += `"${ids[id]}-${inpName}-${value}" -> "${ids[id]}_${o.class_type}"\n`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,13 +215,15 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getNodeInfo(nodeId: ComfyNodeID): string {
|
export function getNodeInfo(nodeId: ComfyNodeID): string {
|
||||||
let app = (window as any).app;
|
const workflow = workflowState.getWorkflowByNodeID(nodeId);
|
||||||
if (!app || !app.lGraph)
|
if (workflow == null)
|
||||||
return String(nodeId);
|
return nodeId;
|
||||||
|
|
||||||
|
const title = workflow.graph?.getNodeByIdRecursive(nodeId)?.title;
|
||||||
|
if (title == null)
|
||||||
|
return nodeId;
|
||||||
|
|
||||||
const displayNodeID = nodeId ? (nodeId.split("-")[0]) : String(nodeId);
|
const displayNodeID = nodeId ? (nodeId.split("-")[0]) : String(nodeId);
|
||||||
|
|
||||||
const title = app.lGraph.getNodeByIdRecursive(nodeId)?.title || String(nodeId);
|
|
||||||
return title + " (" + displayNodeID + ")"
|
return title + " (" + displayNodeID + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +237,7 @@ export const debounce = (callback: Function, wait = 250) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function convertComfyOutputToGradio(output: ComfyExecutionResult): GradioFileData[] {
|
export function convertComfyOutputToGradio(output: SerializedPromptOutput): GradioFileData[] {
|
||||||
return output.images.map(convertComfyOutputEntryToGradio);
|
return output.images.map(convertComfyOutputEntryToGradio);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +253,7 @@ export function convertComfyOutputEntryToGradio(r: ComfyImageLocation): GradioFi
|
|||||||
return fileData
|
return fileData
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): string {
|
export function convertComfyOutputToComfyURL(output: string | ComfyImageLocation): string {
|
||||||
if (typeof output === "string")
|
if (typeof output === "string")
|
||||||
return output;
|
return output;
|
||||||
|
|
||||||
@@ -329,11 +344,16 @@ export async function uploadImageToComfyUI(blob: Blob, filename: string, type: C
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Raw output as received from ComfyUI's backend */
|
/** Raw output as received from ComfyUI's backend */
|
||||||
export interface ComfyExecutionResult {
|
export interface SerializedPromptOutput {
|
||||||
// Technically this response can contain arbitrary data, but "images" is the
|
// Technically this response can contain arbitrary data, but "images" is the
|
||||||
// most frequently used as it's output by LoadImage and PreviewImage, the
|
// most frequently used as it's output by LoadImage and PreviewImage, the
|
||||||
// only two output nodes in base ComfyUI.
|
// only two output nodes in base ComfyUI.
|
||||||
images: ComfyImageLocation[] | null,
|
images: ComfyImageLocation[] | null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Other data
|
||||||
|
*/
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Raw output entry as received from ComfyUI's backend */
|
/** Raw output entry as received from ComfyUI's backend */
|
||||||
@@ -375,7 +395,7 @@ export function isComfyBoxImageMetadataArray(value: any): value is ComfyBoxImage
|
|||||||
return Array.isArray(value) && value.every(isComfyBoxImageMetadata);
|
return Array.isArray(value) && value.every(isComfyBoxImageMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isComfyExecutionResult(value: any): value is ComfyExecutionResult {
|
export function isComfyExecutionResult(value: any): value is SerializedPromptOutput {
|
||||||
return value && typeof value === "object" && Array.isArray(value.images)
|
return value && typeof value === "object" && Array.isArray(value.images)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +435,7 @@ export function comfyFileToAnnotatedFilepath(comfyUIFile: ComfyImageLocation): s
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function executionResultToImageMetadata(result: ComfyExecutionResult): ComfyBoxImageMetadata[] {
|
export function executionResultToImageMetadata(result: SerializedPromptOutput): ComfyBoxImageMetadata[] {
|
||||||
return result.images.map(comfyFileToComfyBoxMetadata)
|
return result.images.map(comfyFileToComfyBoxMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { LGraphNode, LiteGraph } from "@litegraph-ts/core";
|
|||||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||||
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
||||||
import { ComfyComboNode, ComfyNumberNode, ComfyTextNode } from "./nodes/widgets";
|
import { ComfyComboNode, ComfyNumberNode, ComfyTextNode } from "./nodes/widgets";
|
||||||
|
import type { ComfyNodeDefInput } from "./ComfyNodeDef";
|
||||||
|
|
||||||
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any) => IComfyInputSlot;
|
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput) => IComfyInputSlot;
|
||||||
|
|
||||||
function getNumberDefaults(inputData: any, defaultStep: number): ComfyInputConfig {
|
function getNumberDefaults(inputData: ComfyNodeDefInput, defaultStep: number): ComfyInputConfig {
|
||||||
let defaultValue = inputData[1]["default"];
|
let defaultValue = inputData[1].default;
|
||||||
let { min, max, step } = inputData[1];
|
let { min, max, step } = inputData[1];
|
||||||
|
|
||||||
if (defaultValue == undefined) defaultValue = 0;
|
if (defaultValue == undefined) defaultValue = 0;
|
||||||
@@ -33,25 +34,25 @@ function addComfyInput(node: LGraphNode, inputName: string, extraInfo: Partial<I
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => {
|
||||||
const config = getNumberDefaults(inputData, 0.5);
|
const config = getNumberDefaults(inputData, 0.5);
|
||||||
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
|
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
|
||||||
}
|
}
|
||||||
|
|
||||||
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => {
|
||||||
const config = getNumberDefaults(inputData, 1);
|
const config = getNumberDefaults(inputData, 1);
|
||||||
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
|
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
|
||||||
};
|
};
|
||||||
|
|
||||||
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => {
|
||||||
const defaultValue = inputData[1].default || "";
|
const defaultValue = inputData[1].default || "";
|
||||||
const multiline = !!inputData[1].multiline;
|
const multiline = !!inputData[1].multiline;
|
||||||
|
|
||||||
return addComfyInput(node, inputName, { type: "string", config: { defaultValue, multiline }, defaultWidgetNode: ComfyTextNode })
|
return addComfyInput(node, inputName, { type: "string", config: { defaultValue, multiline }, defaultWidgetNode: ComfyTextNode })
|
||||||
};
|
};
|
||||||
|
|
||||||
const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => {
|
||||||
const type = inputData[0];
|
const type = inputData[0] as string[];
|
||||||
let defaultValue = type[0];
|
let defaultValue = type[0];
|
||||||
if (inputData[1] && inputData[1].default) {
|
if (inputData[1] && inputData[1].default) {
|
||||||
defaultValue = inputData[1].default;
|
defaultValue = inputData[1].default;
|
||||||
@@ -59,7 +60,7 @@ const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: an
|
|||||||
return addComfyInput(node, inputName, { type: "string", config: { values: type, defaultValue }, defaultWidgetNode: ComfyComboNode })
|
return addComfyInput(node, inputName, { type: "string", config: { values: type, defaultValue }, defaultWidgetNode: ComfyComboNode })
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => {
|
||||||
return addComfyInput(node, inputName, { type: "number", config: {} })
|
return addComfyInput(node, inputName, { type: "number", config: {} })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { Button } from "@gradio/button";
|
import { Button } from "@gradio/button";
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
import type { ComfyButtonNode } from "$lib/nodes/widgets";
|
import type { ComfyButtonNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|
||||||
let node: ComfyButtonNode | null = null;
|
let node: ComfyButtonNode | null = null;
|
||||||
let nodeValue: Writable<boolean> | null = null;
|
// let nodeValue: Writable<boolean> = writable(false);
|
||||||
let attrsChanged: Writable<boolean> | null = null;
|
let attrsChanged: Writable<number> = writable(0);
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
if (widget) {
|
if (widget) {
|
||||||
node = widget.node as ComfyButtonNode
|
node = widget.node as ComfyButtonNode
|
||||||
nodeValue = node.value;
|
// nodeValue = node.value;
|
||||||
attrsChanged = widget.attrsChanged;
|
attrsChanged = widget.attrsChanged;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
full_width: "100%",
|
full_width: true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { Block } from "@gradio/atoms";
|
import { Block } from "@gradio/atoms";
|
||||||
import { Checkbox } from "@gradio/form";
|
import { Checkbox } from "@gradio/form";
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// import VirtualList from '$lib/components/VirtualList.svelte';
|
// import VirtualList from '$lib/components/VirtualList.svelte';
|
||||||
import VirtualList from 'svelte-tiny-virtual-list';
|
import VirtualList from 'svelte-tiny-virtual-list';
|
||||||
import type { ComfyComboNode } from "$lib/nodes/widgets";
|
import type { ComfyComboNode } from "$lib/nodes/widgets";
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { Image } from "@gradio/icons";
|
import { Image } from "@gradio/icons";
|
||||||
import { StaticImage } from "$lib/components/gradio/image";
|
import { StaticImage } from "$lib/components/gradio/image";
|
||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
import type { WidgetLayout } from "$lib/stores/layoutState";
|
import type { WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
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 type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { Block } from "@gradio/atoms";
|
import { Block } from "@gradio/atoms";
|
||||||
import { TextBox } from "@gradio/form";
|
import { TextBox } from "@gradio/form";
|
||||||
import Row from "$lib/components/gradio/app/Row.svelte";
|
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfyNumberNode } from "$lib/nodes/widgets";
|
import type { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { Range } from "$lib/components/gradio/form";
|
import { Range } from "$lib/components/gradio/form";
|
||||||
import { get, type Writable } from "svelte/store";
|
import { get, type Writable } from "svelte/store";
|
||||||
import { debounce } from "$lib/utils";
|
import { debounce } from "$lib/utils";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { Block } from "@gradio/atoms";
|
import { Block } from "@gradio/atoms";
|
||||||
import { Radio } from "@gradio/form";
|
import { Radio } from "@gradio/form";
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TextBox } from "@gradio/form";
|
import { TextBox } from "@gradio/form";
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||||
import { type Writable } from "svelte/store";
|
import { type Writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
import type { ComfyTextNode } from "$lib/nodes/widgets";
|
import type { ComfyTextNode } from "$lib/nodes/widgets";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { IDragItem } from "$lib/stores/layoutState";
|
import type { IDragItem } from "$lib/stores/layoutStates";
|
||||||
import layoutState from "$lib/stores/layoutState";
|
|
||||||
import { LGraphNode, NodeMode } from "@litegraph-ts/core";
|
import { LGraphNode, NodeMode } from "@litegraph-ts/core";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||||
import notify from "$lib/notify";
|
|
||||||
import uiState from "$lib/stores/uiState";
|
|
||||||
import layoutState from "$lib/stores/layoutState";
|
|
||||||
import queueState from "$lib/stores/queueState";
|
import queueState from "$lib/stores/queueState";
|
||||||
|
import workflowState, { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||||
import { getNodeInfo } from "$lib/utils"
|
import { getNodeInfo } from "$lib/utils"
|
||||||
|
|
||||||
import { Link, Toolbar } from "framework7-svelte"
|
import { Link, Toolbar } from "framework7-svelte"
|
||||||
import ProgressBar from "$lib/components/ProgressBar.svelte";
|
import ProgressBar from "$lib/components/ProgressBar.svelte";
|
||||||
import Indicator from "./Indicator.svelte";
|
import Indicator from "./Indicator.svelte";
|
||||||
import interfaceState from "$lib/stores/interfaceState";
|
import interfaceState from "$lib/stores/interfaceState";
|
||||||
import LightboxModal from "$lib/components/LightboxModal.svelte";
|
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||||
|
|
||||||
export let subworkflowID: number = -1;
|
export let subworkflowID: number = -1;
|
||||||
export let app: ComfyApp = undefined;
|
export let app: ComfyApp = undefined;
|
||||||
|
let layoutState: WritableLayoutStateStore = null;
|
||||||
let fileInput: HTMLInputElement = undefined;
|
let fileInput: HTMLInputElement = undefined;
|
||||||
|
let workflow: ComfyWorkflow | null = null;
|
||||||
|
|
||||||
|
$: workflow = $workflowState.activeWorkflow;
|
||||||
|
|
||||||
function queuePrompt() {
|
function queuePrompt() {
|
||||||
navigator.vibrate(20)
|
navigator.vibrate(20)
|
||||||
app.runDefaultQueueAction()
|
app.runDefaultQueueAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCombos() {
|
async function refreshCombos() {
|
||||||
navigator.vibrate(20)
|
navigator.vibrate(20)
|
||||||
app.refreshComboInNodes()
|
await app.refreshComboInNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
function doSave(): void {
|
function doSave(): void {
|
||||||
if (!app?.lGraph || !fileInput)
|
if (!fileInput)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
navigator.vibrate(20)
|
navigator.vibrate(20)
|
||||||
@@ -35,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function doLoad(): void {
|
function doLoad(): void {
|
||||||
if (!app?.lGraph || !fileInput)
|
if (!fileInput)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
navigator.vibrate(20)
|
navigator.vibrate(20)
|
||||||
@@ -48,9 +50,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function doSaveLocal(): void {
|
function doSaveLocal(): void {
|
||||||
if (!app?.lGraph)
|
|
||||||
return;
|
|
||||||
|
|
||||||
navigator.vibrate(20)
|
navigator.vibrate(20)
|
||||||
app.saveStateToLocalStorage();
|
app.saveStateToLocalStorage();
|
||||||
}
|
}
|
||||||
@@ -74,9 +73,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Toolbar bottom>
|
<Toolbar bottom>
|
||||||
{#if $layoutState.attrs.queuePromptButtonName != ""}
|
{#if workflow != null && workflow.attrs.queuePromptButtonName != ""}
|
||||||
<Link on:click={queuePrompt}>
|
<Link on:click={queuePrompt}>
|
||||||
{$layoutState.attrs.queuePromptButtonName}
|
{workflow.attrs.queuePromptButtonName}
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<Link on:click={refreshCombos}>🔄</Link>
|
<Link on:click={refreshCombos}>🔄</Link>
|
||||||
|
|||||||
@@ -18,21 +18,22 @@
|
|||||||
lCanvas.draw(true, true);
|
lCanvas.draw(true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (app != null && app.lGraph && canvasEl != null) {
|
// TODO
|
||||||
if (!lCanvas) {
|
// $: if (app?.activeGraph != null && canvasEl != null) {
|
||||||
lCanvas = new ComfyGraphCanvas(app, canvasEl);
|
// if (!lCanvas) {
|
||||||
lCanvas.allow_interaction = false;
|
// lCanvas = new ComfyGraphCanvas(app, app.activeGraph, canvasEl);
|
||||||
app.lGraph.eventBus.on("afterExecute", () => lCanvas.draw(true))
|
// lCanvas.allow_interaction = false;
|
||||||
}
|
// app.activeGraph.eventBus.on("afterExecute", () => lCanvas.draw(true))
|
||||||
resizeCanvas();
|
// }
|
||||||
}
|
// resizeCanvas();
|
||||||
|
// }
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<Navbar title="Node Graph" backLink="Back" />
|
<Navbar title="Node Graph" backLink="Back" />
|
||||||
<div class="canvas-wrapper pane-wrapper">
|
<div class="canvas-wrapper pane-wrapper">
|
||||||
<canvas bind:this={canvasEl} id="extra-canvas" />
|
<!-- <canvas bind:this={canvasEl} id="extra-canvas" /> -->
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
export let app: ComfyApp | null = null;
|
export let app: ComfyApp | null = null;
|
||||||
|
|
||||||
async function doLoadDefault() {
|
async function doLoadDefault() {
|
||||||
var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?");
|
var confirmed = confirm("Would you like to load the default workflow in a new tab?");
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await app.initDefaultGraph();
|
await app.initDefaultWorkflow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import layoutState, { type IDragItem } from "$lib/stores/layoutState";
|
|
||||||
|
|
||||||
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
|
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
|
||||||
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
|
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
|
||||||
import type ComfyApp from "$lib/components/ComfyApp";
|
import type ComfyApp from "$lib/components/ComfyApp";
|
||||||
|
import type { ComfyWorkflow } from "$lib/components/ComfyApp";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||||
|
|
||||||
export let subworkflowID: number = -1;
|
export let subworkflowID: number = -1;
|
||||||
export let app: ComfyApp
|
export let app: ComfyApp
|
||||||
|
|
||||||
|
// TODO move
|
||||||
|
let workflow: ComfyWorkflow | null = null
|
||||||
|
let layoutState: WritableLayoutStateStore | null = null;
|
||||||
|
|
||||||
|
$: layoutState = workflow ? workflow.layout : null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page name="subworkflow">
|
<Page name="subworkflow">
|
||||||
<Navbar title="Workflow {subworkflowID}" backLink="Back" />
|
<Navbar title="Workflow {subworkflowID}" backLink="Back" />
|
||||||
|
|
||||||
<div class="container">
|
{#if layoutState}
|
||||||
<WidgetContainer bind:dragItem={$layoutState.root} isMobile={true} classes={["root-container", "mobile"]} />
|
<div class="container">
|
||||||
</div>
|
<WidgetContainer bind:dragItem={$layoutState.root} {layoutState} isMobile={true} classes={["root-container", "mobile"]} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import ComfyPromptSerializer from "$lib/components/ComfyPromptSerializer";
|
|||||||
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||||
import ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
import ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||||
import { graphToGraphVis } from "$lib/utils";
|
import { graphToGraphVis } from "$lib/utils";
|
||||||
import layoutState from "$lib/stores/layoutState";
|
|
||||||
import { ComfyNumberNode } from "$lib/nodes/widgets";
|
import { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
import layoutStates from "$lib/stores/layoutStates";
|
||||||
|
import { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||||
|
|
||||||
export default class ComfyGraphTests extends UnitTest {
|
export default class ComfyGraphTests extends UnitTest {
|
||||||
test__onNodeAdded__updatesLayoutState() {
|
test__onNodeAdded__updatesLayoutState() {
|
||||||
const graph = new ComfyGraph();
|
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||||
layoutState.initDefaultLayout() // adds 3 containers
|
layoutState.initDefaultLayout() // adds 3 containers
|
||||||
|
|
||||||
const state = get(layoutState)
|
const state = get(layoutState)
|
||||||
@@ -38,7 +39,7 @@ export default class ComfyGraphTests extends UnitTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test__onNodeAdded__handlesNodesAddedInSubgraphs() {
|
test__onNodeAdded__handlesNodesAddedInSubgraphs() {
|
||||||
const graph = new ComfyGraph();
|
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||||
layoutState.initDefaultLayout()
|
layoutState.initDefaultLayout()
|
||||||
|
|
||||||
const subgraph = LiteGraph.createNode(Subgraph);
|
const subgraph = LiteGraph.createNode(Subgraph);
|
||||||
@@ -57,7 +58,7 @@ export default class ComfyGraphTests extends UnitTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test__onNodeAdded__handlesSubgraphsWithNodes() {
|
test__onNodeAdded__handlesSubgraphsWithNodes() {
|
||||||
const graph = new ComfyGraph();
|
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||||
layoutState.initDefaultLayout()
|
layoutState.initDefaultLayout()
|
||||||
|
|
||||||
const state = get(layoutState)
|
const state = get(layoutState)
|
||||||
@@ -75,7 +76,7 @@ export default class ComfyGraphTests extends UnitTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test__onNodeRemoved__updatesLayoutState() {
|
test__onNodeRemoved__updatesLayoutState() {
|
||||||
const graph = new ComfyGraph();
|
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||||
layoutState.initDefaultLayout()
|
layoutState.initDefaultLayout()
|
||||||
|
|
||||||
const widget = LiteGraph.createNode(ComfyNumberNode);
|
const widget = LiteGraph.createNode(ComfyNumberNode);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LGraph, LiteGraph, Subgraph, type SlotLayout } from "@litegraph-ts/core"
|
import { LGraph, LiteGraph, Subgraph, type SlotLayout, NodeMode } from "@litegraph-ts/core"
|
||||||
import { Watch } from "@litegraph-ts/nodes-basic"
|
import { Watch } from "@litegraph-ts/nodes-basic"
|
||||||
import { expect } from 'vitest'
|
import { expect } from 'vitest'
|
||||||
import UnitTest from "./UnitTest"
|
import UnitTest from "./UnitTest"
|
||||||
@@ -139,6 +139,38 @@ export default class ComfyPromptSerializerTests extends UnitTest {
|
|||||||
expect(result.output[output.id].inputs).toEqual({})
|
expect(result.output[output.id].inputs).toEqual({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test__serialize__shouldIgnoreInactiveSubgraphs() {
|
||||||
|
const ser = new ComfyPromptSerializer();
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
|
||||||
|
const output = LiteGraph.createNode(MockBackendOutput)
|
||||||
|
const link = LiteGraph.createNode(MockBackendLink)
|
||||||
|
const input = LiteGraph.createNode(MockBackendInput)
|
||||||
|
|
||||||
|
const subgraph = LiteGraph.createNode(Subgraph)
|
||||||
|
const graphInput = subgraph.addGraphInput("testIn", "number")
|
||||||
|
const graphOutput = subgraph.addGraphOutput("testOut", "number")
|
||||||
|
|
||||||
|
graph.add(subgraph)
|
||||||
|
graph.add(output)
|
||||||
|
subgraph.subgraph.add(link)
|
||||||
|
graph.add(input)
|
||||||
|
|
||||||
|
output.connect(0, subgraph, 0)
|
||||||
|
graphInput.innerNode.connect(0, link, 0)
|
||||||
|
link.connect(0, graphOutput.innerNode, 0)
|
||||||
|
subgraph.connect(0, input, 0)
|
||||||
|
|
||||||
|
subgraph.mode = NodeMode.NEVER;
|
||||||
|
|
||||||
|
const result = ser.serialize(graph)
|
||||||
|
|
||||||
|
expect(Object.keys(result.output)).toHaveLength(2);
|
||||||
|
expect(result.output[input.id].inputs["in"]).toBeUndefined();
|
||||||
|
expect(result.output[link.id]).toBeUndefined();
|
||||||
|
expect(result.output[output.id].inputs).toEqual({})
|
||||||
|
}
|
||||||
|
|
||||||
test__serialize__shouldFollowSubgraphsRecursively() {
|
test__serialize__shouldFollowSubgraphsRecursively() {
|
||||||
const ser = new ComfyPromptSerializer();
|
const ser = new ComfyPromptSerializer();
|
||||||
const graph = new ComfyGraph();
|
const graph = new ComfyGraph();
|
||||||
@@ -178,4 +210,42 @@ export default class ComfyPromptSerializerTests extends UnitTest {
|
|||||||
expect(result.output[link.id].inputs["in"][0]).toEqual(output.id)
|
expect(result.output[link.id].inputs["in"][0]).toEqual(output.id)
|
||||||
expect(result.output[output.id].inputs).toEqual({})
|
expect(result.output[output.id].inputs).toEqual({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test__serialize__shouldIgnoreInactiveSubgraphsRecursively() {
|
||||||
|
const ser = new ComfyPromptSerializer();
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
|
||||||
|
const output = LiteGraph.createNode(MockBackendOutput)
|
||||||
|
const link = LiteGraph.createNode(MockBackendLink)
|
||||||
|
const input = LiteGraph.createNode(MockBackendInput)
|
||||||
|
|
||||||
|
const subgraphA = LiteGraph.createNode(Subgraph)
|
||||||
|
const subgraphB = LiteGraph.createNode(Subgraph)
|
||||||
|
const graphInputA = subgraphA.addGraphInput("testIn", "number")
|
||||||
|
const graphOutputA = subgraphA.addGraphOutput("testOut", "number")
|
||||||
|
const graphInputB = subgraphB.addGraphInput("testIn", "number")
|
||||||
|
const graphOutputB = subgraphB.addGraphOutput("testOut", "number")
|
||||||
|
|
||||||
|
graph.add(subgraphA)
|
||||||
|
subgraphA.subgraph.add(subgraphB)
|
||||||
|
graph.add(output)
|
||||||
|
subgraphB.subgraph.add(link)
|
||||||
|
graph.add(input)
|
||||||
|
|
||||||
|
output.connect(0, subgraphA, 0)
|
||||||
|
graphInputA.innerNode.connect(0, subgraphB, 0)
|
||||||
|
graphInputB.innerNode.connect(0, link, 0)
|
||||||
|
link.connect(0, graphOutputB.innerNode, 0)
|
||||||
|
subgraphB.connect(0, graphOutputA.innerNode, 0)
|
||||||
|
subgraphA.connect(0, input, 0)
|
||||||
|
|
||||||
|
subgraphA.mode = NodeMode.NEVER;
|
||||||
|
|
||||||
|
const result = ser.serialize(graph)
|
||||||
|
|
||||||
|
expect(Object.keys(result.output)).toHaveLength(2);
|
||||||
|
expect(result.output[input.id].inputs["in"]).toBeUndefined();
|
||||||
|
expect(result.output[link.id]).toBeUndefined();
|
||||||
|
expect(result.output[output.id].inputs).toEqual({})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user