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.
|
||||
|
||||
Also note that the saved workflow format is subject to change until it's been finalized after enough testing, so be prepared to lose some of your work from time to time.
|
||||
|
||||
## Features
|
||||
- *No-Code UI Builder* - A novel system for creating your own Stable Diffusion user interfaces from the basic components.
|
||||
- *Extension Support* - All custom ComfyUI nodes are supported out of the box.
|
||||
|
||||
Submodule litegraph updated: cd4f68ef42...42adb8dba1
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"createdBy": "ComfyBox",
|
||||
"version": 1,
|
||||
"attrs": {
|
||||
"title": "Default",
|
||||
"queuePromptButtonName": "Queue txt2img",
|
||||
"queuePromptButtonRunWorkflow": false
|
||||
},
|
||||
"workflow": {
|
||||
"last_node_id": 0,
|
||||
"last_link_id": 0,
|
||||
@@ -25709,10 +25714,6 @@
|
||||
],
|
||||
"parent": "eae32e42-1ccc-4a4a-923f-7ab4ccdac97a"
|
||||
}
|
||||
},
|
||||
"attrs": {
|
||||
"queuePromptButtonName": "Queue txt2img",
|
||||
"queuePromptButtonRunWorkflow": false
|
||||
}
|
||||
},
|
||||
"canvas": {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, Li
|
||||
import GraphSync from "./GraphSync";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
import layoutState from "./stores/layoutState";
|
||||
import uiState from "./stores/uiState";
|
||||
import { get } from "svelte/store";
|
||||
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
||||
@@ -10,6 +9,11 @@ import type IComfyInputSlot from "./IComfyInputSlot";
|
||||
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
|
||||
import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets";
|
||||
import selectionState from "./stores/selectionState";
|
||||
import type { WritableLayoutStateStore } from "./stores/layoutStates";
|
||||
import type { WorkflowInstID } from "./components/ComfyApp";
|
||||
import layoutStates from "./stores/layoutStates";
|
||||
import type { ComfyWorkflow } from "./stores/workflowState";
|
||||
import workflowState from "./stores/workflowState";
|
||||
|
||||
type ComfyGraphEvents = {
|
||||
configured: (graph: LGraph) => void
|
||||
@@ -25,11 +29,28 @@ type ComfyGraphEvents = {
|
||||
export default class ComfyGraph extends LGraph {
|
||||
eventBus: TypedEmitter<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() {
|
||||
console.debug("Configured");
|
||||
}
|
||||
|
||||
override onBeforeChange(graph: LGraph, info: any) {
|
||||
if (this.workflow != null)
|
||||
this.workflow.notifyModified()
|
||||
|
||||
console.debug("BeforeChange", info);
|
||||
}
|
||||
|
||||
@@ -50,25 +71,33 @@ export default class ComfyGraph extends LGraph {
|
||||
override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||
// Don't add nodes in subgraphs until this callback reaches the root
|
||||
// graph
|
||||
if (node.getRootGraph() == null || this._is_subgraph)
|
||||
return;
|
||||
// Only root graphs will have a workflow ID, so we don't mind subgraphs
|
||||
// missing it
|
||||
if (node.getRootGraph() != null && !this._is_subgraph && this.workflowID != null) {
|
||||
const layoutState = get(layoutStates).all[this.workflowID]
|
||||
if (layoutState === null) {
|
||||
throw new Error(`LGraph with workflow missing layout! ${this.workflowID}`)
|
||||
}
|
||||
|
||||
this.doAddNode(node, options);
|
||||
this.doAddNode(node, layoutState, options);
|
||||
}
|
||||
|
||||
if (this.workflow != null)
|
||||
this.workflow.notifyModified()
|
||||
|
||||
// console.debug("Added", node);
|
||||
this.eventBus.emit("nodeAdded", node);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add widget UI/groups for newly added nodes.
|
||||
*/
|
||||
private doAddNode(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||
private doAddNode(node: LGraphNode, layoutState: WritableLayoutStateStore, options: LGraphAddNodeOptions) {
|
||||
layoutState.nodeAdded(node, options)
|
||||
|
||||
// All nodes whether they come from base litegraph or ComfyBox should
|
||||
// have tags added to them. Can't override serialization for existing
|
||||
// node types to add `tags` as a new field so putting it in properties
|
||||
// is better.
|
||||
// have tags added to them. Can't override serialization for litegraph's
|
||||
// base node types to add `tags` as a new field so putting it in
|
||||
// properties is better.
|
||||
if (node.properties.tags == null)
|
||||
node.properties.tags = []
|
||||
|
||||
@@ -104,7 +133,6 @@ export default class ComfyGraph extends LGraph {
|
||||
}
|
||||
|
||||
if (get(uiState).autoAddUI) {
|
||||
console.warn("ADD", node.type, options)
|
||||
if (!("svelteComponentType" in node) && options.addedBy == null) {
|
||||
console.debug("[ComfyGraph] AutoAdd UI")
|
||||
const comfyNode = node as ComfyGraphNode;
|
||||
@@ -144,28 +172,49 @@ export default class ComfyGraph extends LGraph {
|
||||
// ************** RECURSION ALERT ! **************
|
||||
if (node.is(Subgraph)) {
|
||||
for (const child of node.subgraph.iterateNodesInOrder()) {
|
||||
this.doAddNode(child, options)
|
||||
this.doAddNode(child, layoutState, options)
|
||||
}
|
||||
}
|
||||
// ************** RECURSION ALERT ! **************
|
||||
|
||||
if (this.workflow != null)
|
||||
this.workflow.notifyModified()
|
||||
}
|
||||
|
||||
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||
selectionState.clear(); // safest option
|
||||
layoutState.nodeRemoved(node, options);
|
||||
|
||||
// Handle subgraphs being removed
|
||||
if (node.is(Subgraph)) {
|
||||
for (const child of node.subgraph.iterateNodesInOrder()) {
|
||||
this.onNodeRemoved(child, options)
|
||||
if (!this._is_subgraph && this.workflowID != null) {
|
||||
const layoutState = get(layoutStates).all[this.workflowID]
|
||||
if (layoutState === null) {
|
||||
throw new Error(`ComfyGraph with workflow missing layout! ${this.workflowID}`)
|
||||
}
|
||||
|
||||
layoutState.nodeRemoved(node, options);
|
||||
|
||||
// Handle subgraphs being removed
|
||||
if (node.is(Subgraph)) {
|
||||
for (const child of node.subgraph.iterateNodesInOrder()) {
|
||||
this.onNodeRemoved(child, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console.debug("Removed", node);
|
||||
if (this.workflow != null)
|
||||
this.workflow.notifyModified()
|
||||
|
||||
this.eventBus.emit("nodeRemoved", node);
|
||||
}
|
||||
|
||||
override onInputsOutputsChange() {
|
||||
if (this.workflow != null)
|
||||
this.workflow.notifyModified()
|
||||
}
|
||||
|
||||
override onNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) {
|
||||
if (this.workflow != null)
|
||||
this.workflow.notifyModified()
|
||||
|
||||
// console.debug("ConnectionChange", node);
|
||||
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import type ComfyApp from "./components/ComfyApp";
|
||||
import queueState from "./stores/queueState";
|
||||
import { get, type Unsubscriber } from "svelte/store";
|
||||
import uiState from "./stores/uiState";
|
||||
import layoutState from "./stores/layoutState";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
import { ComfyReroute } from "./nodes";
|
||||
import type { Progress } from "./components/ComfyApp";
|
||||
import selectionState from "./stores/selectionState";
|
||||
import type ComfyGraph from "./ComfyGraph";
|
||||
import layoutStates from "./stores/layoutStates";
|
||||
|
||||
export type SerializedGraphCanvasState = {
|
||||
offset: Vector2,
|
||||
@@ -18,9 +19,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
app: ComfyApp | null;
|
||||
private _unsubscribe: Unsubscriber;
|
||||
|
||||
get comfyGraph(): ComfyGraph | null {
|
||||
return this.graph as ComfyGraph;
|
||||
}
|
||||
|
||||
constructor(
|
||||
app: ComfyApp,
|
||||
canvas: HTMLCanvasElement | string,
|
||||
graph?: ComfyGraph,
|
||||
options: {
|
||||
skip_render?: boolean;
|
||||
skip_events?: boolean;
|
||||
@@ -28,7 +34,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
viewport?: Vector4;
|
||||
} = {}
|
||||
) {
|
||||
super(canvas, app.lGraph, options);
|
||||
super(canvas, graph, options);
|
||||
this.app = app;
|
||||
this._unsubscribe = selectionState.subscribe(ss => {
|
||||
for (const node of Object.values(this.selected_nodes)) {
|
||||
@@ -281,11 +287,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
selectionState.update(ss => {
|
||||
ss.currentSelectionNodes = Object.values(nodes)
|
||||
ss.currentSelection = []
|
||||
const ls = get(layoutState)
|
||||
for (const node of ss.currentSelectionNodes) {
|
||||
const widget = ls.allItemsByNode[node.id]
|
||||
if (widget)
|
||||
ss.currentSelection.push(widget.dragItem.id)
|
||||
const layoutState = layoutStates.getLayoutByGraph(this.graph);
|
||||
if (layoutState) {
|
||||
const ls = get(layoutState)
|
||||
for (const node of ss.currentSelectionNodes) {
|
||||
const widget = ls.allItemsByNode[node.id]
|
||||
if (widget)
|
||||
ss.currentSelection.push(widget.dragItem.id)
|
||||
}
|
||||
}
|
||||
return ss
|
||||
})
|
||||
@@ -298,11 +307,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
ss.currentHoveredNodes.add(node.id)
|
||||
}
|
||||
ss.currentHovered.clear()
|
||||
const ls = get(layoutState)
|
||||
for (const nodeID of ss.currentHoveredNodes) {
|
||||
const widget = ls.allItemsByNode[nodeID]
|
||||
if (widget)
|
||||
ss.currentHovered.add(widget.dragItem.id)
|
||||
const layoutState = layoutStates.getLayoutByGraph(this.graph);
|
||||
if (layoutState) {
|
||||
const ls = get(layoutState)
|
||||
for (const nodeID of ss.currentHoveredNodes) {
|
||||
const widget = ls.allItemsByNode[nodeID]
|
||||
if (widget)
|
||||
ss.currentHovered.add(widget.dragItem.id)
|
||||
}
|
||||
}
|
||||
return ss
|
||||
})
|
||||
|
||||
@@ -27,7 +27,18 @@ export type ComfyNodeDefInput = [ComfyNodeDefInputType, ComfyNodeDefInputOptions
|
||||
export type ComfyNodeDefInputType = any[] | keyof typeof ComfyWidgets | string
|
||||
|
||||
export type ComfyNodeDefInputOptions = {
|
||||
forceInput?: boolean
|
||||
forceInput?: boolean;
|
||||
|
||||
// NOTE: For COMBO type inputs, the default value is always the first entry the list.
|
||||
default?: any,
|
||||
|
||||
// INT/FLOAT options
|
||||
min?: number,
|
||||
max?: number,
|
||||
step?: number,
|
||||
|
||||
// STRING options
|
||||
multiline?: boolean,
|
||||
}
|
||||
|
||||
// TODO if/when comfy refactors
|
||||
|
||||
@@ -3,7 +3,7 @@ import type TypedEmitter from "typed-emitter";
|
||||
import EventEmitter from "events";
|
||||
import type { ComfyImageLocation } from "$lib/utils";
|
||||
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
||||
import type { SerializedLayoutState } from "./stores/layoutState";
|
||||
import type { SerializedLayoutState } from "./stores/layoutStates";
|
||||
import type { ComfyNodeDef } from "./ComfyNodeDef";
|
||||
|
||||
export type ComfyPromptRequest = {
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||
import {cubicIn} from 'svelte/easing';
|
||||
import { flip } from 'svelte/animate';
|
||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||
import { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutStates";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
@@ -59,6 +60,14 @@
|
||||
navigator.vibrate(20)
|
||||
$isOpen = e.detail
|
||||
}
|
||||
|
||||
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||
startDrag(e, layoutState)
|
||||
}
|
||||
|
||||
function _stopDrag(e: MouseEvent | TouchEvent) {
|
||||
stopDrag(e, layoutState)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if container}
|
||||
@@ -93,7 +102,7 @@
|
||||
animate:flip={{duration:flipDurationMs}}
|
||||
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]}
|
||||
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||
{/if}
|
||||
@@ -101,10 +110,18 @@
|
||||
{/each}
|
||||
</div>
|
||||
{#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 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}
|
||||
</Accordion>
|
||||
</Block>
|
||||
@@ -112,7 +129,7 @@
|
||||
<Block elem_classes={["gradio-accordion"]}>
|
||||
<Accordion label={container.attrs.title} open={$isOpen} on:click={handleClick}>
|
||||
{#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}
|
||||
</Accordion>
|
||||
</Block>
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||
import {cubicIn} from 'svelte/easing';
|
||||
import { flip } from 'svelte/animate';
|
||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
@@ -53,6 +54,14 @@
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
};
|
||||
|
||||
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||
startDrag(e, layoutState)
|
||||
}
|
||||
|
||||
function _stopDrag(e: MouseEvent | TouchEvent) {
|
||||
stopDrag(e, layoutState)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if container}
|
||||
@@ -92,7 +101,7 @@
|
||||
animate:flip={{duration:flipDurationMs}}
|
||||
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]}
|
||||
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||
{/if}
|
||||
@@ -100,10 +109,18 @@
|
||||
{/each}
|
||||
</div>
|
||||
{#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 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}
|
||||
</Block>
|
||||
</div>
|
||||
|
||||
@@ -1,44 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
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 { ListIcon as List, ImageIcon as Image, SettingsIcon as Settings } from "svelte-feather-icons";
|
||||
import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp";
|
||||
import { Checkbox, TextBox } from "@gradio/form"
|
||||
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 { LGraph } from "@litegraph-ts/core";
|
||||
import LightboxModal from "./LightboxModal.svelte";
|
||||
import ComfyQueue from "./ComfyQueue.svelte";
|
||||
import ComfyProperties from "./ComfyProperties.svelte";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
|
||||
import ComfyGraphView from "./ComfyGraphView.svelte";
|
||||
import { download, jsonToJsObject } from "$lib/utils";
|
||||
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";
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import SidebarItem from "./SidebarItem.svelte";
|
||||
// import Modal from "./Modal.svelte";
|
||||
// import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
|
||||
import notify from "$lib/notify";
|
||||
import ComfyWorkflowsView from "./ComfyWorkflowsView.svelte";
|
||||
|
||||
|
||||
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 uiTheme: string = "gradio-dark";
|
||||
let fileInput: HTMLInputElement = undefined;
|
||||
|
||||
let debugLayout: boolean = false;
|
||||
|
||||
const toastOptions = {
|
||||
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) {
|
||||
hasShownUIHelpToast = true;
|
||||
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") {
|
||||
document.getElementById("app-root").classList.add("dark")
|
||||
}
|
||||
@@ -197,9 +36,11 @@
|
||||
document.getElementById("app-root").classList.remove("dark")
|
||||
}
|
||||
|
||||
let showModal: boolean = false;
|
||||
|
||||
$: showModal = $a1111Prompt != null
|
||||
// let showModal: boolean = false;
|
||||
//
|
||||
// $: showModal = $a1111Prompt != null
|
||||
//
|
||||
// let selectedTab
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -208,198 +49,39 @@
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<Modal bind:showModal on:close={() => ($a1111Prompt = null)}>
|
||||
<div slot="header" class="prompt-modal-header">
|
||||
<h1 style="padding-bottom: 1rem;">A1111 Prompt Details</h1>
|
||||
</div>
|
||||
<A1111PromptDisplay prompt={$a1111Prompt} />
|
||||
<div slot="buttons" let:closeDialog>
|
||||
<Button variant="secondary" on:click={closeDialog}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<!--
|
||||
<Modal bind:showModal on:close={() => ($a1111Prompt = null)}>
|
||||
<div slot="header" class="prompt-modal-header">
|
||||
<h1 style="padding-bottom: 1rem;">A1111 Prompt Details</h1>
|
||||
</div>
|
||||
<A1111PromptDisplay prompt={$a1111Prompt} />
|
||||
<div slot="buttons" let:closeDialog>
|
||||
<Button variant="secondary" on:click={closeDialog}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
-->
|
||||
|
||||
<div id="main" class:dark={uiTheme === "gradio-dark"}>
|
||||
<div id="container" bind:this={containerElem}>
|
||||
<Splitpanes theme="comfy" on:resize={refreshView}>
|
||||
<Pane bind:size={propsSidebarSize}>
|
||||
<div class="sidebar-wrapper pane-wrapper">
|
||||
<ComfyProperties bind:this={props} />
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane>
|
||||
<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 id="container">
|
||||
<Sidebar selected="generate">
|
||||
<SidebarItem id="generate" name="Generate" icon={Image}>
|
||||
<ComfyWorkflowsView {app} {uiTheme} />
|
||||
</SidebarItem>
|
||||
<SidebarItem id="settings" name="Settings" icon={Settings}>
|
||||
</SidebarItem>
|
||||
</Sidebar>
|
||||
</div>
|
||||
<LightboxModal />
|
||||
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
|
||||
</div>
|
||||
<SvelteToast options={toastOptions} />
|
||||
|
||||
<style lang="scss">
|
||||
$bottom-bar-height: 70px;
|
||||
|
||||
#container {
|
||||
height: calc(100vh - $bottom-bar-height);
|
||||
max-width: 100vw;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#comfy-content {
|
||||
grid-area: content;
|
||||
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;
|
||||
}
|
||||
|
||||
: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;
|
||||
display: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</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 ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||
@@ -21,8 +21,7 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import { type SvelteComponentDev } from "svelte/internal";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
import type { SerializedLayoutState } from "$lib/stores/layoutState";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import type { LayoutState, SerializedLayoutState, WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import ComfyGraph from "$lib/ComfyGraph";
|
||||
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||
@@ -33,7 +32,7 @@ import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, wor
|
||||
import notify from "$lib/notify";
|
||||
import configState from "$lib/stores/configState";
|
||||
import { blankGraph } from "$lib/defaultGraph";
|
||||
import type { ComfyExecutionResult } from "$lib/utils";
|
||||
import type { SerializedPromptOutput } from "$lib/utils";
|
||||
import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer";
|
||||
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
||||
import { ComfyComboNode } from "$lib/nodes/widgets";
|
||||
@@ -41,6 +40,10 @@ import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111";
|
||||
import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt";
|
||||
import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
|
||||
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import layoutStates from "$lib/stores/layoutStates";
|
||||
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||
import workflowState from "$lib/stores/workflowState";
|
||||
|
||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||
|
||||
@@ -51,12 +54,11 @@ if (typeof window !== "undefined") {
|
||||
|
||||
/*
|
||||
* Queued prompt that hasn't been sent to the backend yet.
|
||||
* TODO: Assumes the currently active graph will be serialized, needs to change
|
||||
* for multiple loaded workflow support
|
||||
*/
|
||||
type QueueItem = {
|
||||
type PromptQueueItem = {
|
||||
num: number,
|
||||
batchCount: number
|
||||
workflow: ComfyWorkflow
|
||||
}
|
||||
|
||||
export type A1111PromptAndInfo = {
|
||||
@@ -78,6 +80,8 @@ export type SerializedAppState = {
|
||||
commitHash?: string,
|
||||
/** Graph state */
|
||||
workflow: SerializedLGraph,
|
||||
/** Workflow attributes */
|
||||
attrs: WorkflowAttributes,
|
||||
/** UI state */
|
||||
layout: SerializedLayoutState,
|
||||
/** Position/offset of the canvas at the time of saving */
|
||||
@@ -111,7 +115,7 @@ export type SerializedPrompt = {
|
||||
/*
|
||||
* Outputs for each node.
|
||||
*/
|
||||
export type SerializedPromptOutputs = Record<ComfyNodeID, ComfyExecutionResult>
|
||||
export type SerializedPromptOutputs = Record<ComfyNodeID, SerializedPromptOutput>
|
||||
|
||||
export type Progress = {
|
||||
value: number,
|
||||
@@ -128,15 +132,19 @@ type BackendComboNode = {
|
||||
backendNode: ComfyBackendNode
|
||||
}
|
||||
|
||||
type CanvasState = {
|
||||
canvasEl: HTMLCanvasElement,
|
||||
canvasCtx: CanvasRenderingContext2D,
|
||||
canvas: ComfyGraphCanvas,
|
||||
}
|
||||
|
||||
export default class ComfyApp {
|
||||
api: ComfyAPI;
|
||||
|
||||
rootEl: HTMLDivElement | null = null;
|
||||
canvasEl: HTMLCanvasElement | null = null;
|
||||
canvasCtx: CanvasRenderingContext2D | null = null;
|
||||
lGraph: ComfyGraph | null = null;
|
||||
lCanvas: ComfyGraphCanvas | null = null;
|
||||
dropZone: HTMLElement | null = null;
|
||||
nodeOutputs: Record<string, any> = {};
|
||||
|
||||
shiftDown: boolean = false;
|
||||
ctrlDown: boolean = false;
|
||||
@@ -144,7 +152,7 @@ export default class ComfyApp {
|
||||
alreadySetup: Writable<boolean> = writable(false);
|
||||
a1111Prompt: Writable<A1111PromptAndInfo | null> = writable(null);
|
||||
|
||||
private queueItems: QueueItem[] = [];
|
||||
private queueItems: PromptQueueItem[] = [];
|
||||
private processingQueue: boolean = false;
|
||||
private promptSerializer: ComfyPromptSerializer;
|
||||
private stdPromptSerializer: ComfyBoxStdPromptSerializer;
|
||||
@@ -157,7 +165,7 @@ export default class ComfyApp {
|
||||
|
||||
async setup(): Promise<void> {
|
||||
if (get(this.alreadySetup)) {
|
||||
console.error("Already setup")
|
||||
console.log("Already setup")
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,7 +173,6 @@ export default class ComfyApp {
|
||||
|
||||
this.rootEl = document.getElementById("app-root") as HTMLDivElement;
|
||||
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
|
||||
this.lGraph = new ComfyGraph();
|
||||
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
|
||||
this.canvasCtx = this.canvasEl.getContext("2d");
|
||||
|
||||
@@ -174,17 +181,13 @@ export default class ComfyApp {
|
||||
this.lCanvas.allow_interaction = uiUnlocked;
|
||||
|
||||
// await this.#invokeExtensionsAsync("init");
|
||||
await this.registerNodes();
|
||||
const defs = await this.api.getNodeDefs();
|
||||
await this.registerNodes(defs);
|
||||
|
||||
// Load previous workflow
|
||||
let restored = false;
|
||||
try {
|
||||
const json = localStorage.getItem("workflow");
|
||||
if (json) {
|
||||
const state = JSON.parse(json) as SerializedAppState;
|
||||
await this.deserialize(state)
|
||||
restored = true;
|
||||
}
|
||||
restored = await this.loadStateFromLocalStorage(defs);
|
||||
} catch (err) {
|
||||
console.error("Error loading previous workflow", err);
|
||||
notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null })
|
||||
@@ -192,7 +195,7 @@ export default class ComfyApp {
|
||||
|
||||
// We failed to restore a workflow so load the default
|
||||
if (!restored) {
|
||||
await this.initDefaultGraph();
|
||||
await this.initDefaultWorkflow(defs);
|
||||
}
|
||||
|
||||
// Save current workflow automatically
|
||||
@@ -225,12 +228,37 @@ export default class ComfyApp {
|
||||
this.lCanvas.draw(true, true);
|
||||
}
|
||||
|
||||
serialize(workflow: ComfyWorkflow): SerializedAppState {
|
||||
const layoutState = layoutStates.getLayout(workflow.id);
|
||||
if (layoutState == null)
|
||||
throw new Error("Workflow has no layout!")
|
||||
|
||||
const { graph, layout, attrs } = workflow.serialize(layoutState);
|
||||
const canvas = this.lCanvas.serialize();
|
||||
|
||||
return {
|
||||
createdBy: "ComfyBox",
|
||||
version: COMFYBOX_SERIAL_VERSION,
|
||||
commitHash: __GIT_COMMIT_HASH__,
|
||||
workflow: graph,
|
||||
attrs,
|
||||
layout,
|
||||
canvas
|
||||
}
|
||||
}
|
||||
|
||||
saveStateToLocalStorage() {
|
||||
try {
|
||||
uiState.update(s => { s.isSavingToLocalStorage = true; return s; })
|
||||
const savedWorkflow = this.serialize();
|
||||
const json = JSON.stringify(savedWorkflow);
|
||||
localStorage.setItem("workflow", json)
|
||||
const state = get(workflowState)
|
||||
const workflows = state.openedWorkflows
|
||||
const savedWorkflows = workflows.map(w => this.serialize(w));
|
||||
const activeWorkflowIndex = workflows.findIndex(w => state.activeWorkflowID === w.id);
|
||||
const json = JSON.stringify({ workflows: savedWorkflows, activeWorkflowIndex });
|
||||
localStorage.setItem("workflows", json)
|
||||
for (const workflow of workflows)
|
||||
workflow.isModified = false;
|
||||
workflowState.set(get(workflowState));
|
||||
notify("Saved to local storage.")
|
||||
}
|
||||
catch (err) {
|
||||
@@ -241,13 +269,33 @@ export default class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
async loadStateFromLocalStorage(defs: Record<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 widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
||||
|
||||
private async registerNodes() {
|
||||
// Load node definitions from the backend
|
||||
const defs = await this.api.getNodeDefs();
|
||||
|
||||
private async registerNodes(defs: Record<ComfyNodeID, ComfyNodeDef>) {
|
||||
// Register a node for each definition
|
||||
for (const [nodeId, nodeDef] of Object.entries(defs)) {
|
||||
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
||||
@@ -351,8 +399,12 @@ export default class ComfyApp {
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
if (workflow && workflow.version && workflow.nodes && workflow.extra) {
|
||||
this.loadGraphData(workflow);
|
||||
if (workflow && workflow.createdBy === "ComfyBox") {
|
||||
this.openWorkflow(workflow);
|
||||
}
|
||||
else {
|
||||
// TODO handle vanilla workflows
|
||||
throw new Error("Workflow was not in ComfyBox format!")
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -375,21 +427,29 @@ export default class ComfyApp {
|
||||
|
||||
this.api.addEventListener("progress", (progress: Progress) => {
|
||||
queueState.progressUpdated(progress);
|
||||
this.lGraph.setDirtyCanvas(true, false);
|
||||
workflowState.getActiveWorkflow()?.graph?.setDirtyCanvas(true, false); // TODO PromptID
|
||||
});
|
||||
|
||||
this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => {
|
||||
queueState.executingUpdated(promptID, nodeID);
|
||||
this.lGraph.setDirtyCanvas(true, false);
|
||||
const queueEntry = queueState.executingUpdated(promptID, nodeID);
|
||||
if (queueEntry != null) {
|
||||
const workflow = workflowState.getWorkflow(queueEntry.workflowID);
|
||||
workflow?.graph?.setDirtyCanvas(true, false);
|
||||
}
|
||||
});
|
||||
|
||||
this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => {
|
||||
this.nodeOutputs[nodeID] = output;
|
||||
const node = this.lGraph.getNodeByIdRecursive(nodeID) as ComfyGraphNode;
|
||||
if (node?.onExecuted) {
|
||||
node.onExecuted(output);
|
||||
this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutput) => {
|
||||
const queueEntry = queueState.onExecuted(promptID, nodeID, output)
|
||||
if (queueEntry != null) {
|
||||
const workflow = workflowState.getWorkflow(queueEntry.workflowID);
|
||||
if (workflow != null) {
|
||||
workflow.graph.setDirtyCanvas(true, false);
|
||||
const node = workflow.graph.getNodeByIdRecursive(nodeID) as ComfyGraphNode;
|
||||
if (node?.onExecuted) {
|
||||
node.onExecuted(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
queueState.onExecuted(promptID, nodeID, output)
|
||||
});
|
||||
|
||||
this.api.addEventListener("execution_start", (promptID: PromptID) => {
|
||||
@@ -456,49 +516,49 @@ export default class ComfyApp {
|
||||
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
||||
}
|
||||
|
||||
serialize(): SerializedAppState {
|
||||
const graph = this.lGraph;
|
||||
|
||||
const serializedGraph = graph.serialize()
|
||||
const serializedLayout = layoutState.serialize()
|
||||
const serializedCanvas = this.lCanvas.serialize();
|
||||
|
||||
return {
|
||||
createdBy: "ComfyBox",
|
||||
version: COMFYBOX_SERIAL_VERSION,
|
||||
workflow: serializedGraph,
|
||||
layout: serializedLayout,
|
||||
canvas: serializedCanvas
|
||||
}
|
||||
}
|
||||
|
||||
async deserialize(data: SerializedAppState) {
|
||||
async openWorkflow(data: SerializedAppState, refreshCombos: boolean | Record<string, ComfyNodeDef> = true): Promise<ComfyWorkflow> {
|
||||
if (data.version !== COMFYBOX_SERIAL_VERSION) {
|
||||
throw `Invalid ComfyBox saved data format: ${data.version}`
|
||||
}
|
||||
this.clean();
|
||||
|
||||
// Ensure loadGraphData does not trigger any state changes in layoutState
|
||||
// (isConfiguring is set to true here)
|
||||
// lGraph.configure will add new nodes, triggering onNodeAdded, but we
|
||||
// want to restore the layoutState ourselves
|
||||
layoutState.onStartConfigure();
|
||||
|
||||
this.loadGraphData(data.workflow)
|
||||
|
||||
// Now restore the layout
|
||||
// Subsequent added nodes will add the UI data to layoutState
|
||||
layoutState.deserialize(data.layout, this.lGraph)
|
||||
const workflow = workflowState.openWorkflow(this.lCanvas, data);
|
||||
|
||||
// Restore canvas offset/zoom
|
||||
this.lCanvas.deserialize(data.canvas)
|
||||
|
||||
await this.refreshComboInNodes();
|
||||
if (refreshCombos) {
|
||||
let defs = null;
|
||||
if (typeof refreshCombos === "object")
|
||||
defs = refreshCombos;
|
||||
await this.refreshComboInNodes(workflow, defs);
|
||||
}
|
||||
|
||||
this.lGraph.start();
|
||||
this.lGraph.eventBus.on("afterExecute", () => this.lCanvas.draw(true))
|
||||
return workflow;
|
||||
}
|
||||
|
||||
async initDefaultGraph() {
|
||||
setActiveWorkflow(id: WorkflowInstID) {
|
||||
const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id)
|
||||
if (index === -1)
|
||||
return;
|
||||
workflowState.setActiveWorkflow(this.lCanvas, index);
|
||||
selectionState.clear();
|
||||
}
|
||||
|
||||
createNewWorkflow() {
|
||||
workflowState.createNewWorkflow(this.lCanvas, undefined, true);
|
||||
selectionState.clear();
|
||||
}
|
||||
|
||||
closeWorkflow(id: WorkflowInstID) {
|
||||
const index = get(workflowState).openedWorkflows.findIndex(w => w.id === id)
|
||||
if (index === -1)
|
||||
return;
|
||||
workflowState.closeWorkflow(this.lCanvas, index);
|
||||
selectionState.clear();
|
||||
}
|
||||
|
||||
async initDefaultWorkflow(defs?: Record<string, ComfyNodeDef>) {
|
||||
let state = null;
|
||||
try {
|
||||
const graphResponse = await fetch("/workflows/defaultWorkflow.json");
|
||||
@@ -509,50 +569,14 @@ export default class ComfyApp {
|
||||
notify(`Failed to load default graph: ${error}`, { type: "error" })
|
||||
state = structuredClone(blankGraph)
|
||||
}
|
||||
await this.deserialize(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the graph with the specified workflow data
|
||||
* @param {*} graphData A serialized graph object
|
||||
*/
|
||||
loadGraphData(graphData: SerializedLGraph) {
|
||||
this.clean();
|
||||
|
||||
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||
for (let n of graphData.nodes) {
|
||||
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
||||
}
|
||||
|
||||
this.lGraph.configure(graphData);
|
||||
|
||||
for (const node of this.lGraph._nodes) {
|
||||
const size = node.computeSize();
|
||||
size[0] = Math.max(node.size[0], size[0]);
|
||||
size[1] = Math.max(node.size[1], size[1]);
|
||||
node.size = size;
|
||||
// this.#invokeExtensions("loadedGraphNode", node);
|
||||
}
|
||||
await this.openWorkflow(state, defs)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.clean();
|
||||
|
||||
const blankGraph: SerializedLGraph = {
|
||||
last_node_id: 0,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0
|
||||
}
|
||||
|
||||
layoutState.onStartConfigure();
|
||||
this.lCanvas.closeAllSubgraphs();
|
||||
this.lGraph.configure(blankGraph)
|
||||
layoutState.initDefaultLayout();
|
||||
workflowState.closeAllWorkflows(this.lCanvas);
|
||||
uiState.update(s => {
|
||||
s.uiUnlocked = true;
|
||||
s.uiEditMode = "widgets";
|
||||
@@ -561,13 +585,17 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
runDefaultQueueAction() {
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
const workflow = workflowState.getActiveWorkflow();
|
||||
if (workflow == null)
|
||||
return;
|
||||
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
if ("onDefaultQueueAction" in node) {
|
||||
(node as ComfyGraphNode).onDefaultQueueAction()
|
||||
}
|
||||
}
|
||||
|
||||
if (get(layoutState).attrs.queuePromptButtonRunWorkflow) {
|
||||
if (workflow.attrs.queuePromptButtonRunWorkflow) {
|
||||
// Hold control to queue at the front
|
||||
const num = this.ctrlDown ? -1 : 0;
|
||||
this.queuePrompt(num, 1);
|
||||
@@ -575,6 +603,12 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
querySave() {
|
||||
const workflow = workflowState.getActiveWorkflow();
|
||||
if (workflow == null) {
|
||||
notify("No active workflow!", { type: "error" })
|
||||
return;
|
||||
}
|
||||
|
||||
const promptFilename = get(configState).promptForWorkflowName;
|
||||
|
||||
let filename = "workflow.json";
|
||||
@@ -592,10 +626,13 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
const indent = 2
|
||||
const json = JSON.stringify(this.serialize(), null, indent)
|
||||
const json = JSON.stringify(this.serialize(workflow), null, indent)
|
||||
|
||||
download(filename, json, "application/json")
|
||||
|
||||
workflow.isModified = false;
|
||||
workflowState.set(get(workflowState));
|
||||
|
||||
console.debug(jsonToJsObject(json))
|
||||
}
|
||||
|
||||
@@ -603,12 +640,18 @@ export default class ComfyApp {
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
graphToPrompt(tag: string | null = null): SerializedPrompt {
|
||||
return this.promptSerializer.serialize(this.lGraph, tag)
|
||||
graphToPrompt(workflow: ComfyWorkflow, tag: string | null = null): SerializedPrompt {
|
||||
return this.promptSerializer.serialize(workflow.graph, tag)
|
||||
}
|
||||
|
||||
async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
|
||||
this.queueItems.push({ num, batchCount });
|
||||
const activeWorkflow = workflowState.getActiveWorkflow();
|
||||
if (activeWorkflow == null) {
|
||||
notify("No workflow is opened!", { type: "error" })
|
||||
return;
|
||||
}
|
||||
|
||||
this.queueItems.push({ num, batchCount, workflow: activeWorkflow });
|
||||
|
||||
// Only have one action process the items so each one gets a unique seed correctly
|
||||
if (this.processingQueue) {
|
||||
@@ -619,13 +662,15 @@ export default class ComfyApp {
|
||||
tag = null;
|
||||
|
||||
this.processingQueue = true;
|
||||
let workflow;
|
||||
|
||||
try {
|
||||
while (this.queueItems.length) {
|
||||
({ num, batchCount } = this.queueItems.pop());
|
||||
({ num, batchCount, workflow } = this.queueItems.pop());
|
||||
console.debug(`Queue get! ${num} ${batchCount} ${tag}`);
|
||||
|
||||
const thumbnails = []
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
if (node.mode !== NodeMode.ALWAYS
|
||||
|| (tag != null
|
||||
&& Array.isArray(node.properties.tags)
|
||||
@@ -640,7 +685,7 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
if (node.mode !== NodeMode.ALWAYS)
|
||||
continue;
|
||||
|
||||
@@ -649,9 +694,9 @@ export default class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
const p = this.graphToPrompt(tag);
|
||||
const l = layoutState.serialize();
|
||||
console.debug(graphToGraphVis(this.lGraph))
|
||||
const p = this.graphToPrompt(workflow, tag);
|
||||
const l = workflow.layout.serialize();
|
||||
console.debug(graphToGraphVis(workflow.graph))
|
||||
console.debug(promptToGraphVis(p))
|
||||
|
||||
const stdPrompt = this.stdPromptSerializer.serialize(p);
|
||||
@@ -681,7 +726,7 @@ export default class ComfyApp {
|
||||
error = response.error;
|
||||
}
|
||||
else {
|
||||
queueState.afterQueued(response.promptID, num, p.output, extraData)
|
||||
queueState.afterQueued(workflow.id, response.promptID, num, p.output, extraData)
|
||||
}
|
||||
} catch (err) {
|
||||
error = err?.toString();
|
||||
@@ -690,13 +735,13 @@ export default class ComfyApp {
|
||||
if (error != null) {
|
||||
const mes: string = error;
|
||||
notify(`Error queuing prompt:\n${mes}`, { type: "error" })
|
||||
console.error(graphToGraphVis(this.lGraph))
|
||||
console.error(graphToGraphVis(workflow.graph))
|
||||
console.error(promptToGraphVis(p))
|
||||
console.error("Error queuing prompt", error, num, p)
|
||||
break;
|
||||
}
|
||||
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
if ("afterQueued" in node) {
|
||||
(node as ComfyGraphNode).afterQueued(p, tag);
|
||||
}
|
||||
@@ -719,7 +764,7 @@ export default class ComfyApp {
|
||||
const pngInfo = await getPngMetadata(file);
|
||||
if (pngInfo) {
|
||||
if (pngInfo.comfyBoxConfig) {
|
||||
this.deserialize(JSON.parse(pngInfo.comfyBoxConfig));
|
||||
await this.openWorkflow(JSON.parse(pngInfo.comfyBoxConfig));
|
||||
} else if (pngInfo.parameters) {
|
||||
const parsed = parseA1111(pngInfo.parameters)
|
||||
if ("error" in parsed) {
|
||||
@@ -741,8 +786,8 @@ export default class ComfyApp {
|
||||
}
|
||||
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.deserialize(JSON.parse(reader.result as string));
|
||||
reader.onload = async () => {
|
||||
await this.openWorkflow(JSON.parse(reader.result as string));
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
@@ -761,8 +806,15 @@ export default class ComfyApp {
|
||||
/**
|
||||
* Refresh combo list on whole nodes
|
||||
*/
|
||||
async refreshComboInNodes(flashUI: boolean = false) {
|
||||
const defs = await this.api.getNodeDefs();
|
||||
async refreshComboInNodes(workflow?: ComfyWorkflow, defs?: Record<string, ComfyNodeDef>, flashUI: boolean = false) {
|
||||
workflow ||= workflowState.getActiveWorkflow();
|
||||
if (workflow == null) {
|
||||
notify("No active workflow!", { type: "error" })
|
||||
return
|
||||
}
|
||||
|
||||
if (defs == null)
|
||||
defs = await this.api.getNodeDefs();
|
||||
|
||||
const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => {
|
||||
return node
|
||||
@@ -805,7 +857,7 @@ export default class ComfyApp {
|
||||
return result
|
||||
}
|
||||
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
if (!isActiveBackendNode(node))
|
||||
continue;
|
||||
|
||||
@@ -827,7 +879,7 @@ export default class ComfyApp {
|
||||
console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos)
|
||||
|
||||
// Mark combo nodes without backend configs as being loaded already.
|
||||
for (const node of this.lGraph.iterateNodesOfClassRecursive(ComfyComboNode)) {
|
||||
for (const node of workflow.graph.iterateNodesOfClassRecursive(ComfyComboNode)) {
|
||||
if (backendUpdatedCombos[node.id] != null) {
|
||||
continue;
|
||||
}
|
||||
@@ -859,12 +911,15 @@ export default class ComfyApp {
|
||||
// Load definitions from the backend.
|
||||
for (const { comboNode, comfyInput, backendNode } of Object.values(backendUpdatedCombos)) {
|
||||
const def = defs[backendNode.type];
|
||||
const rawValues = def["input"]["required"][comfyInput.name][0];
|
||||
const [rawValues, opts] = def.input.required[comfyInput.name];
|
||||
|
||||
console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length)
|
||||
comboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false })
|
||||
|
||||
comboNode.formatValues(rawValues as string[], true)
|
||||
const values = rawValues as string[]
|
||||
const defaultValue = rawValues[0];
|
||||
|
||||
comboNode.formatValues(values, defaultValue, true)
|
||||
if (!rawValues?.includes(get(comboNode.value))) {
|
||||
comboNode.setValue(rawValues[0], comfyInput.config.defaultValue)
|
||||
}
|
||||
@@ -875,7 +930,6 @@ export default class ComfyApp {
|
||||
* Clean current state
|
||||
*/
|
||||
clean() {
|
||||
this.nodeOutputs = {};
|
||||
this.a1111Prompt.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,18 @@ export function isActiveBackendNode(node: LGraphNode, tag: string | null = null)
|
||||
if (!(node as any).isBackendNode)
|
||||
return false;
|
||||
|
||||
return isActiveNode(node, tag);
|
||||
if (!isActiveNode(node, tag))
|
||||
return false;
|
||||
|
||||
// Make sure this node is not contained in an inactive subgraph, even if the
|
||||
// node itself is considered active
|
||||
if (node.graph._is_subgraph) {
|
||||
const isInsideDisabledSubgraph = Array.from(node.iterateParentSubgraphNodes()).some(n => !isActiveNode(n, tag))
|
||||
if (isInsideDisabledSubgraph)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export class UpstreamNodeLocator {
|
||||
@@ -166,7 +177,7 @@ export default class ComfyPromptSerializer {
|
||||
// We don't check tags for non-backend nodes.
|
||||
// Just check for node inactivity (so you can toggle groups of
|
||||
// tagged frontend nodes on/off)
|
||||
if (inputNode && inputNode.mode === NodeMode.NEVER) {
|
||||
if (inputNode && inputNode.mode !== NodeMode.ALWAYS) {
|
||||
console.debug("Skipping inactive node", inputNode)
|
||||
continue;
|
||||
}
|
||||
@@ -248,6 +259,8 @@ export default class ComfyPromptSerializer {
|
||||
const inputs = this.serializeInputValues(node);
|
||||
const links = this.serializeBackendLinks(node, tag);
|
||||
|
||||
console.warn("OUTPUT", node.id, node.comfyClass, node.mode)
|
||||
|
||||
output[String(node.id)] = {
|
||||
inputs: { ...inputs, ...links },
|
||||
class_type: node.comfyClass,
|
||||
|
||||
@@ -2,43 +2,50 @@
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { TextBox, Checkbox } from "@gradio/form";
|
||||
import { LGraphNode } from "@litegraph-ts/core"
|
||||
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
||||
import { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
|
||||
import uiState from "$lib/stores/uiState"
|
||||
import workflowState from "$lib/stores/workflowState"
|
||||
import layoutStates from "$lib/stores/layoutStates"
|
||||
import selectionState from "$lib/stores/selectionState"
|
||||
import { get, type Writable, writable } from "svelte/store"
|
||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||
import type { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
|
||||
export let workflow: ComfyWorkflow | null;
|
||||
|
||||
let layoutState: WritableLayoutStateStore | null = null
|
||||
|
||||
$: layoutState = workflow?.layout
|
||||
|
||||
let target: IDragItem | null = null;
|
||||
let node: LGraphNode | null = null;
|
||||
let attrsChanged: Writable<number> | null = null;
|
||||
|
||||
let refreshPropsPanel: Writable<number> | null
|
||||
|
||||
$: refreshPropsPanel = $layoutState.refreshPropsPanel;
|
||||
|
||||
$: if ($selectionState.currentSelection.length > 0) {
|
||||
node = null;
|
||||
const targetId = $selectionState.currentSelection.slice(-1)[0]
|
||||
const entry = $layoutState.allItems[targetId]
|
||||
if (entry != null) {
|
||||
target = entry.dragItem
|
||||
attrsChanged = target.attrsChanged;
|
||||
if (target.type === "widget") {
|
||||
node = (target as WidgetLayout).node
|
||||
$: if (layoutState) {
|
||||
if ($selectionState.currentSelection.length > 0) {
|
||||
node = null;
|
||||
const targetId = $selectionState.currentSelection.slice(-1)[0]
|
||||
const entry = $layoutState.allItems[targetId]
|
||||
if (entry != null) {
|
||||
target = entry.dragItem
|
||||
if (target.type === "widget") {
|
||||
node = (target as WidgetLayout).node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($selectionState.currentSelectionNodes.length > 0) {
|
||||
target = null;
|
||||
node = $selectionState.currentSelectionNodes[0]
|
||||
attrsChanged = null;
|
||||
else if ($selectionState.currentSelectionNodes.length > 0) {
|
||||
target = null;
|
||||
node = $selectionState.currentSelectionNodes[0]
|
||||
}
|
||||
else {
|
||||
target = null
|
||||
node = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
target = null
|
||||
target = null;
|
||||
node = null;
|
||||
attrsChanged = null;
|
||||
}
|
||||
|
||||
$: if (target) {
|
||||
@@ -55,7 +62,7 @@
|
||||
let value = spec.defaultValue;
|
||||
target.attrs[spec.name] = value;
|
||||
if (spec.refreshPanelOnChange)
|
||||
$refreshPropsPanel += 1;
|
||||
doRefreshPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +130,10 @@
|
||||
if (spec.location !== "workflow")
|
||||
return false;
|
||||
|
||||
return spec.name in $layoutState.attrs
|
||||
if (workflow == null)
|
||||
return false;
|
||||
|
||||
return spec.name in workflow.attrs
|
||||
}
|
||||
|
||||
function getAttribute(target: IDragItem, spec: AttributesSpec): any {
|
||||
@@ -162,6 +172,8 @@
|
||||
if (spec.refreshPanelOnChange) {
|
||||
doRefreshPanel()
|
||||
}
|
||||
|
||||
workflow.notifyModified()
|
||||
}
|
||||
|
||||
function getProperty(node: LGraphNode, spec: AttributesSpec) {
|
||||
@@ -197,6 +209,8 @@
|
||||
|
||||
if (spec.refreshPanelOnChange)
|
||||
doRefreshPanel()
|
||||
|
||||
workflow.notifyModified()
|
||||
}
|
||||
|
||||
function getVar(node: LGraphNode, spec: AttributesSpec) {
|
||||
@@ -233,10 +247,15 @@
|
||||
if (spec.refreshPanelOnChange) {
|
||||
doRefreshPanel()
|
||||
}
|
||||
|
||||
workflow.notifyModified();
|
||||
}
|
||||
|
||||
function getWorkflowAttribute(spec: AttributesSpec): any {
|
||||
let value = $layoutState.attrs[spec.name]
|
||||
if (workflow == null)
|
||||
throw new Error("Active workflow is null!");
|
||||
|
||||
let value = workflow.attrs[spec.name]
|
||||
if (value == null)
|
||||
value = spec.defaultValue
|
||||
else if (spec.serialize)
|
||||
@@ -249,23 +268,28 @@
|
||||
if (!spec.editable)
|
||||
return;
|
||||
|
||||
if (workflow == null)
|
||||
throw new Error("Active workflow is null!");
|
||||
|
||||
const name = spec.name
|
||||
// console.warn("[ComfyProperties] updateWorkflowAttribute", name, value)
|
||||
|
||||
const prevValue = value
|
||||
$layoutState.attrs[name] = value
|
||||
$layoutState = $layoutState
|
||||
workflow.attrs[name] = value
|
||||
$workflowState = $workflowState;
|
||||
|
||||
if (spec.onChanged)
|
||||
spec.onChanged($layoutState, value, prevValue)
|
||||
|
||||
if (spec.refreshPanelOnChange)
|
||||
doRefreshPanel()
|
||||
|
||||
workflow.notifyModified();
|
||||
}
|
||||
|
||||
function doRefreshPanel() {
|
||||
console.warn("[ComfyProperties] doRefreshPanel")
|
||||
$refreshPropsPanel += 1;
|
||||
$layoutStates.refreshPropsPanel += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -278,176 +302,180 @@
|
||||
<span class="type">({targetType})</span>
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="props-entries">
|
||||
{#key $refreshPropsPanel}
|
||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||
<div class="category-name">
|
||||
<span>
|
||||
<span class="title">{category.categoryName}</span>
|
||||
</span>
|
||||
</div>
|
||||
{#each category.specs as spec(spec.id)}
|
||||
{#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}
|
||||
{#if workflow != null && layoutState != null}
|
||||
{#key workflow.id}
|
||||
{#key $layoutStates.refreshPropsPanel}
|
||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||
<div class="category-name">
|
||||
<span>
|
||||
<span class="title">{category.categoryName}</span>
|
||||
</span>
|
||||
</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)}
|
||||
label={spec.name}
|
||||
{#each category.specs as spec(spec.id)}
|
||||
{#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}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={spec.multiline ? 5 : 1}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={getProperty(node, spec)}
|
||||
step={spec.step || 1}
|
||||
min={spec.min || -1024}
|
||||
max={spec.max || 1024}
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={getAttribute(target, spec)}
|
||||
on:change={(e) => updateAttribute(spec, target, e.detail)}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
{: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>
|
||||
{: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)}
|
||||
values={spec.values}
|
||||
label={spec.name}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if validNodeVar(spec, node)}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
on:input={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
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}
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={getProperty(node, spec)}
|
||||
step={spec.step || 1}
|
||||
min={spec.min || -1024}
|
||||
max={spec.max || 1024}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={getProperty(node, spec)}
|
||||
values={spec.values}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if validNodeVar(spec, node)}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
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}
|
||||
on:input={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
disabled={!$uiState.uiUnlocked || !spec.editable}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
max_lines={spec.multiline ? 5 : 1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{/key}
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { tick } from "svelte";
|
||||
import Modal from "./Modal.svelte";
|
||||
import DropZone from "./DropZone.svelte";
|
||||
import workflowState from "$lib/stores/workflowState";
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
@@ -71,10 +72,17 @@
|
||||
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs;
|
||||
|
||||
let message = "Prompt";
|
||||
if (subgraphs?.length > 0)
|
||||
message = `Prompt: ${subgraphs.join(', ')}`
|
||||
if (entry.workflowID != null) {
|
||||
const workflow = workflowState.getWorkflow(entry.workflowID);
|
||||
if (workflow != null && workflow.attrs.title) {
|
||||
message = `Workflow: ${workflow.attrs.title}`
|
||||
}
|
||||
if (subgraphs?.length > 0)
|
||||
message += ` (${subgraphs.join(', ')})`
|
||||
}
|
||||
|
||||
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
||||
|
||||
if (Object.keys(entry.outputs).length > 0) {
|
||||
const imageCount = Object.values(entry.outputs).flatMap(o => o.images).length
|
||||
submessage = `Images: ${imageCount}`
|
||||
@@ -84,7 +92,7 @@
|
||||
entry,
|
||||
message,
|
||||
submessage,
|
||||
dateStr,
|
||||
date: dateStr,
|
||||
status: "pending",
|
||||
images: []
|
||||
}
|
||||
@@ -387,7 +395,7 @@
|
||||
|
||||
&.all_cached, &.interrupted {
|
||||
filter: brightness(80%);
|
||||
color: var(--neutral-300);
|
||||
color: var(--comfy-accent-soft);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
<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 type { SerializedPanes } from "./ComfyApp"
|
||||
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 selectionState from "$lib/stores/selectionState";
|
||||
|
||||
import Menu from './menu/Menu.svelte';
|
||||
import MenuOption from './menu/MenuOption.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 workflow: ComfyWorkflow;
|
||||
|
||||
let layoutState: WritableLayoutStateStore | null;
|
||||
|
||||
$: layoutState = workflow?.layout;
|
||||
|
||||
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)) {
|
||||
const dragItemID = $selectionState.currentSelection[0];
|
||||
@@ -149,9 +138,11 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="comfy-ui-panes" on:contextmenu={onRightClick}>
|
||||
<WidgetContainer bind:dragItem={root} classes={["root-container"]} />
|
||||
</div>
|
||||
{#if layoutState != null}
|
||||
<div id="comfy-workflow-view" on:contextmenu={onRightClick}>
|
||||
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showMenu}
|
||||
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
||||
@@ -188,7 +179,7 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
#comfy-ui-panes {
|
||||
#comfy-workflow-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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"
|
||||
|
||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||
import { type ContainerLayout } from "$lib/stores/layoutState";
|
||||
import { type ContainerLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
@@ -31,11 +32,11 @@
|
||||
{#key $attrsChanged}
|
||||
{#if edit || !isHidden(container)}
|
||||
{#if container.attrs.variant === "tabs"}
|
||||
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||
<TabsContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||
{:else if container.attrs.variant === "accordion"}
|
||||
<AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||
<AccordionContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||
{:else}
|
||||
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||
<BlockContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
@@ -162,6 +162,10 @@
|
||||
position: relative;
|
||||
flex: 1 1 0%;
|
||||
max-width: 30vw;
|
||||
|
||||
> :global(.block) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.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)
|
||||
import {cubicIn} from 'svelte/easing';
|
||||
import { flip } from 'svelte/animate';
|
||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
@@ -24,19 +25,17 @@
|
||||
export let dragDisabled: boolean = false;
|
||||
export let isMobile: boolean = false;
|
||||
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
// let attrsChanged: Writable<number> = writable(0);
|
||||
let children: IDragItem[] = [];
|
||||
const flipDurationMs = 100;
|
||||
|
||||
let selectedIndex: number = 0;
|
||||
|
||||
$: if (container) {
|
||||
children = $layoutState.allItems[container.id].children;
|
||||
attrsChanged = container.attrsChanged
|
||||
// attrsChanged = container.attrsChanged
|
||||
}
|
||||
else {
|
||||
children = [];
|
||||
attrsChanged = null
|
||||
// attrsChanged = writable(0)
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
@@ -66,6 +65,14 @@
|
||||
function handleSelect() {
|
||||
navigator.vibrate(20)
|
||||
}
|
||||
|
||||
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||
startDrag(e, layoutState)
|
||||
}
|
||||
|
||||
function _stopDrag(e: MouseEvent | TouchEvent) {
|
||||
stopDrag(e, layoutState)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if container}
|
||||
@@ -103,7 +110,7 @@
|
||||
<label for={String(item.id)}>
|
||||
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
|
||||
</label>
|
||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||
{/if}
|
||||
@@ -112,18 +119,26 @@
|
||||
{/each}
|
||||
</div>
|
||||
{#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 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}
|
||||
</Block>
|
||||
{:else}
|
||||
<Tabs elem_classes={["gradio-tabs"]} on:select={handleSelect}>
|
||||
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
|
||||
{@const tabName = getTabName(container, i)}
|
||||
<TabItem name={tabName}>
|
||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||
<TabItem id={tabName} name={tabName}>
|
||||
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
|
||||
</TabItem>
|
||||
{/each}
|
||||
</Tabs>
|
||||
|
||||
@@ -2,21 +2,22 @@
|
||||
import queueState from "$lib/stores/queueState";
|
||||
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 { startDrag, stopDrag } from "$lib/utils"
|
||||
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 { isHidden } from "$lib/widgets/utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let dragItem: IDragItem | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
export let isMobile: boolean = false;
|
||||
let container: ContainerLayout | null = null;
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let attrsChanged: Writable<number> = writable(0);
|
||||
let propsChanged: Writable<number> = writable(0);
|
||||
let widget: WidgetLayout | null = null;
|
||||
let showHandles: boolean = false;
|
||||
|
||||
@@ -24,8 +25,8 @@
|
||||
dragItem = null;
|
||||
container = null;
|
||||
widget = null;
|
||||
attrsChanged = null;
|
||||
propsChanged = null;
|
||||
attrsChanged = writable(0);
|
||||
propsChanged = writable(0);
|
||||
}
|
||||
else if (dragItem.type === "container") {
|
||||
container = dragItem as ContainerLayout;
|
||||
@@ -40,7 +41,7 @@
|
||||
if (widget.node && "propsChanged" in widget.node)
|
||||
propsChanged = (widget.node as ComfyWidgetNode).propsChanged
|
||||
else
|
||||
propsChanged = null;
|
||||
propsChanged = writable(0);
|
||||
}
|
||||
|
||||
$: showHandles = $uiState.uiUnlocked
|
||||
@@ -57,12 +58,20 @@
|
||||
const title = widget.node.type.replace("/", "-").replace(".", "-")
|
||||
return `widget--${title}`
|
||||
}
|
||||
|
||||
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||
startDrag(e, layoutState)
|
||||
}
|
||||
|
||||
function _stopDrag(e: MouseEvent | TouchEvent) {
|
||||
stopDrag(e, layoutState)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if container}
|
||||
{#key $attrsChanged}
|
||||
<Container {container} {classes} {zIndex} {showHandles} {isMobile} />
|
||||
<Container {layoutState} {container} {classes} {zIndex} {showHandles} {isMobile} />
|
||||
{/key}
|
||||
{:else if widget && widget.node}
|
||||
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"}
|
||||
@@ -78,13 +87,19 @@
|
||||
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id}
|
||||
class:hidden={hidden}
|
||||
>
|
||||
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
|
||||
<svelte:component this={widget.node.svelteComponentType} {layoutState} {widget} {isMobile} />
|
||||
</div>
|
||||
{#if hidden && edit}
|
||||
<div class="handle handle-hidden" class:hidden={!edit} />
|
||||
{/if}
|
||||
{#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}
|
||||
{/key}
|
||||
{/key}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type SerializedAppState from "./ComfyApp"
|
||||
import type { SerializedAppState } from "./components/ComfyApp"
|
||||
|
||||
const blankGraph: SerializedAppState = {
|
||||
createdBy: "ComfyBox",
|
||||
version: 1,
|
||||
attrs: {
|
||||
title: "New Workflow",
|
||||
queuePromptButtonName: "Queue Prompt",
|
||||
queuePromptButtonRunWorkflow: true
|
||||
},
|
||||
workflow: {
|
||||
last_node_id: 0,
|
||||
last_link_id: 0,
|
||||
@@ -13,7 +18,14 @@ const blankGraph: SerializedAppState = {
|
||||
extra: {},
|
||||
version: 0
|
||||
},
|
||||
panes: {}
|
||||
layout: {
|
||||
root: null,
|
||||
allItems: {},
|
||||
},
|
||||
canvas: {
|
||||
offset: [0, 0],
|
||||
scale: 1
|
||||
}
|
||||
}
|
||||
|
||||
export { blankGraph }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import ComfyGraph from '$lib/ComfyGraph';
|
||||
import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
|
||||
import layoutState from './stores/layoutState';
|
||||
import layoutStates from './stores/layoutStates';
|
||||
import { get } from 'svelte/store';
|
||||
import workflowState from './stores/workflowState';
|
||||
|
||||
export function configureLitegraph(isMobile: boolean = false) {
|
||||
LiteGraph.catch_exceptions = false;
|
||||
@@ -28,5 +29,7 @@ export function configureLitegraph(isMobile: boolean = false) {
|
||||
|
||||
(window as any).LiteGraph = LiteGraph;
|
||||
(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 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 { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
||||
import { get } from "svelte/store";
|
||||
@@ -8,7 +8,7 @@ import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"
|
||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||
import type { NotifyOptions } from "$lib/notify";
|
||||
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 {
|
||||
static slotLayout: SlotLayout = {
|
||||
@@ -63,7 +63,7 @@ LiteGraph.registerNodeType({
|
||||
})
|
||||
|
||||
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
|
||||
images: ComfyExecutionResult | null
|
||||
images: SerializedPromptOutput | null
|
||||
}
|
||||
|
||||
export class ComfyStoreImagesAction extends ComfyGraphNode {
|
||||
@@ -90,7 +90,7 @@ export class ComfyStoreImagesAction extends ComfyGraphNode {
|
||||
if (action !== "store" || !param || !("images" in param))
|
||||
return;
|
||||
|
||||
this.setProperty("images", param as ComfyExecutionResult)
|
||||
this.setProperty("images", param as SerializedPromptOutput)
|
||||
this.setOutputData(0, this.properties.images)
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,7 @@ export class ComfyNotifyAction extends ComfyGraphNode {
|
||||
// native notifications.
|
||||
if (param != null && typeof param === "object") {
|
||||
if ("images" in param) {
|
||||
const output = param as ComfyExecutionResult;
|
||||
const output = param as SerializedPromptOutput;
|
||||
const converted = convertComfyOutputToGradio(output);
|
||||
if (converted.length > 0)
|
||||
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") {
|
||||
const container = entry.dragItem;
|
||||
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") {
|
||||
const container = entry.dragItem;
|
||||
const hasTag = container.attrs.tags.indexOf(action.tag) != -1;
|
||||
@@ -532,7 +532,7 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
||||
this.graph.getNodeByIdRecursive(nodeId).changeMode(newMode);
|
||||
}
|
||||
|
||||
const layout = get(layoutState);
|
||||
const layout = get(this.layoutState);
|
||||
for (const [dragItemID, isHidden] of Object.entries(widgetChanges)) {
|
||||
const container = layout.allItems[dragItemID].dragItem
|
||||
container.attrs.hidden = isHidden;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { BuiltInSlotShape, BuiltInSlotType, type SerializedLGraphNode } from "@l
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||
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.
|
||||
@@ -111,7 +111,7 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
override onExecuted(outputData: ComfyExecutionResult) {
|
||||
override onExecuted(outputData: SerializedPromptOutput) {
|
||||
console.warn("onExecuted outputs", 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 { get } from "svelte/store"
|
||||
import ComfyGraphNode from "./ComfyGraphNode"
|
||||
|
||||
export interface ComfyConfigureQueuePromptButtonProperties extends Record<string, any> {
|
||||
}
|
||||
|
||||
export default class ComfyConfigureQueuePromptButton extends LGraphNode {
|
||||
override properties: ComfyConfigureQueuePromptButtonProperties = {
|
||||
}
|
||||
|
||||
export default class ComfyConfigureQueuePromptButton extends ComfyGraphNode {
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ 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 }) {
|
||||
if (action === "config" && param != null) {
|
||||
layoutState.update(state => {
|
||||
if (typeof param === "string")
|
||||
state.attrs.queuePromptButtonName = param || ""
|
||||
else if (typeof param === "object" && "buttonName" in param)
|
||||
state.attrs.queuePromptButtonName = param.buttonName || ""
|
||||
return state
|
||||
})
|
||||
if (this.layoutState == null) {
|
||||
console.error(this, this.getRootGraph(), Object.keys(get(layoutStates).all))
|
||||
throw new Error(`Could not find layout attached to this node! ${this.id}`)
|
||||
}
|
||||
|
||||
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 type { SvelteComponentDev } from "svelte/internal";
|
||||
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 uiState from "$lib/stores/uiState";
|
||||
import { get } from "svelte/store";
|
||||
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 = {
|
||||
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.
|
||||
* 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
|
||||
@@ -100,6 +103,14 @@ export default class ComfyGraphNode extends LGraphNode {
|
||||
return null;
|
||||
}
|
||||
|
||||
get layoutState(): WritableLayoutStateStore | null {
|
||||
return layoutStates.getLayoutByNode(this);
|
||||
}
|
||||
|
||||
get workflow(): ComfyWorkflow | null {
|
||||
return workflowStateStore.getWorkflowByNode(this);
|
||||
}
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title)
|
||||
this.addProperty("tags", [], "array")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
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 { Watch } from "@litegraph-ts/nodes-basic";
|
||||
@@ -269,7 +268,7 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
}
|
||||
|
||||
if (options.setWidgetTitle) {
|
||||
const widget = layoutState.findLayoutForNode(this.id as ComfyNodeID)
|
||||
const widget = this.layoutState.findLayoutForNode(this.id as ComfyNodeID)
|
||||
if (widget && input.name !== "") {
|
||||
widget.attrs.title = input.name;
|
||||
}
|
||||
@@ -288,7 +287,7 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
}
|
||||
|
||||
notifyPropsChanged() {
|
||||
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
||||
const layoutEntry = this.layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
||||
if (layoutEntry && layoutEntry.parent) {
|
||||
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 { 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 notify from "$lib/notify";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
@@ -11,12 +11,13 @@ type QueueStateOps = {
|
||||
historyUpdated: (resp: ComfyAPIHistoryResponse) => void,
|
||||
statusUpdated: (status: ComfyAPIStatusResponse | null) => 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,
|
||||
executionError: (promptID: PromptID, message: string) => void,
|
||||
progressUpdated: (progress: Progress) => void
|
||||
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
||||
onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void
|
||||
getQueueEntry: (promptID: PromptID) => QueueEntry | null;
|
||||
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 ***/
|
||||
|
||||
/* Workflow tab that sent the prompt. */
|
||||
workflowID?: WorkflowInstID,
|
||||
/* Prompt outputs, collected while the prompt is still executing */
|
||||
outputs: SerializedPromptOutputs,
|
||||
/* 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] {
|
||||
const state = get(store);
|
||||
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)
|
||||
}
|
||||
|
||||
function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) {
|
||||
function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null): QueueEntry | null {
|
||||
console.debug("[queueState] executingUpdated", promptID, runningNodeID)
|
||||
let entry_ = null;
|
||||
|
||||
store.update((s) => {
|
||||
s.progress = null;
|
||||
|
||||
@@ -214,8 +234,11 @@ function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null)
|
||||
s.progress = null;
|
||||
s.runningNodeID = null;
|
||||
}
|
||||
entry_ = entry;
|
||||
return s
|
||||
})
|
||||
|
||||
return entry_;
|
||||
}
|
||||
|
||||
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))
|
||||
store.update(s => {
|
||||
const [index, entry, queue] = findEntryInPending(promptID);
|
||||
if (entry == null) {
|
||||
const entry = createNewQueueEntry(promptID, number, prompt, extraData);
|
||||
entry.workflowID = workflowID;
|
||||
s.queuePending.update(qp => { qp.push(entry); return qp })
|
||||
console.debug("[queueState] ADD PROMPT", promptID)
|
||||
}
|
||||
else {
|
||||
entry.workflowID = workflowID;
|
||||
entry.number = number;
|
||||
entry.prompt = prompt
|
||||
entry.extraData = extraData
|
||||
@@ -304,19 +329,22 @@ function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromp
|
||||
})
|
||||
}
|
||||
|
||||
function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) {
|
||||
console.debug("[queueState] onExecuted", promptID, nodeID, output)
|
||||
function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, outputs: ComfyExecutionResult): QueueEntry | null {
|
||||
console.debug("[queueState] onExecuted", promptID, nodeID, outputs)
|
||||
let entry_ = null;
|
||||
store.update(s => {
|
||||
const [index, entry, queue] = findEntryInPending(promptID)
|
||||
if (entry != null) {
|
||||
entry.outputs[nodeID] = output;
|
||||
entry.outputs[nodeID] = outputs;
|
||||
queue.set(get(queue))
|
||||
}
|
||||
else {
|
||||
console.error("[queueState] Could not find in pending! (onExecuted)", promptID)
|
||||
}
|
||||
entry_ = entry;
|
||||
return s
|
||||
})
|
||||
return entry_;
|
||||
}
|
||||
|
||||
const queueStateStore: WritableQueueStateStore =
|
||||
@@ -331,6 +359,7 @@ const queueStateStore: WritableQueueStateStore =
|
||||
executionCached,
|
||||
executionError,
|
||||
afterQueued,
|
||||
getQueueEntry,
|
||||
onExecuted
|
||||
}
|
||||
export default queueStateStore;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { 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';
|
||||
|
||||
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 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 type { ComfyNodeID } from "./api";
|
||||
import { type SerializedPrompt } from "./components/ComfyApp";
|
||||
import workflowState from "./stores/workflowState";
|
||||
|
||||
export function clamp(n: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(n, min), max)
|
||||
@@ -39,7 +40,7 @@ export function download(filename: string, text: string, type: string = "text/pl
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function startDrag(evt: MouseEvent) {
|
||||
export function startDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore) {
|
||||
const dragItemId: string = evt.target.dataset["dragItemId"];
|
||||
const ss = get(selectionState)
|
||||
const ls = get(layoutState)
|
||||
@@ -78,9 +79,11 @@ export function startDrag(evt: MouseEvent) {
|
||||
|
||||
layoutState.set(ls)
|
||||
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 {
|
||||
@@ -175,23 +178,33 @@ export function workflowToGraphVis(workflow: SerializedLGraph): string {
|
||||
export function promptToGraphVis(prompt: SerializedPrompt): string {
|
||||
let out = "digraph {\n"
|
||||
|
||||
const ids: Record<NodeID, number> = {}
|
||||
let nextID = 0;
|
||||
|
||||
for (const pair of Object.entries(prompt.output)) {
|
||||
const [id, o] = pair;
|
||||
const outNode = prompt.workflow.nodes.find(n => n.id == id)
|
||||
if (outNode) {
|
||||
if (ids[id] == null)
|
||||
ids[id] = nextID++;
|
||||
|
||||
if ("class_type" in o) {
|
||||
for (const pair2 of Object.entries(o.inputs)) {
|
||||
const [inpName, i] = pair2;
|
||||
|
||||
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
||||
// Link
|
||||
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
|
||||
const [inpID, inpSlot] = i;
|
||||
if (ids[inpID] == null)
|
||||
ids[inpID] = nextID++;
|
||||
|
||||
const inpNode = prompt.output[inpID]
|
||||
if (inpNode) {
|
||||
out += `"${inpNode.title}" -> "${outNode.title}"\n`
|
||||
out += `"${ids[inpID]}_${inpNode.class_type}" -> "${ids[id]}_${o.class_type}"\n`
|
||||
}
|
||||
}
|
||||
else {
|
||||
const value = String(i).substring(0, 20)
|
||||
// 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 {
|
||||
let app = (window as any).app;
|
||||
if (!app || !app.lGraph)
|
||||
return String(nodeId);
|
||||
const workflow = workflowState.getWorkflowByNodeID(nodeId);
|
||||
if (workflow == null)
|
||||
return nodeId;
|
||||
|
||||
const title = workflow.graph?.getNodeByIdRecursive(nodeId)?.title;
|
||||
if (title == null)
|
||||
return nodeId;
|
||||
|
||||
const displayNodeID = nodeId ? (nodeId.split("-")[0]) : String(nodeId);
|
||||
|
||||
const title = app.lGraph.getNodeByIdRecursive(nodeId)?.title || String(nodeId);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -238,7 +253,7 @@ export function convertComfyOutputEntryToGradio(r: ComfyImageLocation): GradioFi
|
||||
return fileData
|
||||
}
|
||||
|
||||
export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): string {
|
||||
export function convertComfyOutputToComfyURL(output: string | ComfyImageLocation): string {
|
||||
if (typeof output === "string")
|
||||
return output;
|
||||
|
||||
@@ -329,11 +344,16 @@ export async function uploadImageToComfyUI(blob: Blob, filename: string, type: C
|
||||
}
|
||||
|
||||
/** 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
|
||||
// most frequently used as it's output by LoadImage and PreviewImage, the
|
||||
// only two output nodes in base ComfyUI.
|
||||
images: ComfyImageLocation[] | null,
|
||||
|
||||
/*
|
||||
* Other data
|
||||
*/
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -415,7 +435,7 @@ export function comfyFileToAnnotatedFilepath(comfyUIFile: ComfyImageLocation): s
|
||||
return path;
|
||||
}
|
||||
|
||||
export function executionResultToImageMetadata(result: ComfyExecutionResult): ComfyBoxImageMetadata[] {
|
||||
export function executionResultToImageMetadata(result: SerializedPromptOutput): ComfyBoxImageMetadata[] {
|
||||
return result.images.map(comfyFileToComfyBoxMetadata)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ import { LGraphNode, LiteGraph } from "@litegraph-ts/core";
|
||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
||||
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 {
|
||||
let defaultValue = inputData[1]["default"];
|
||||
function getNumberDefaults(inputData: ComfyNodeDefInput, defaultStep: number): ComfyInputConfig {
|
||||
let defaultValue = inputData[1].default;
|
||||
let { min, max, step } = inputData[1];
|
||||
|
||||
if (defaultValue == undefined) defaultValue = 0;
|
||||
@@ -33,25 +34,25 @@ function addComfyInput(node: LGraphNode, inputName: string, extraInfo: Partial<I
|
||||
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);
|
||||
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);
|
||||
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 multiline = !!inputData[1].multiline;
|
||||
|
||||
return addComfyInput(node, inputName, { type: "string", config: { defaultValue, multiline }, defaultWidgetNode: ComfyTextNode })
|
||||
};
|
||||
|
||||
const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||
const type = inputData[0];
|
||||
const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: ComfyNodeDefInput): IComfyInputSlot => {
|
||||
const type = inputData[0] as string[];
|
||||
let defaultValue = type[0];
|
||||
if (inputData[1] && 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 })
|
||||
}
|
||||
|
||||
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: {} })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||
import { Button } from "@gradio/button";
|
||||
import { get, type Writable, writable } from "svelte/store";
|
||||
import { isDisabled } from "./utils"
|
||||
import type { ComfyButtonNode } from "$lib/nodes/widgets";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
|
||||
let node: ComfyButtonNode | null = null;
|
||||
let nodeValue: Writable<boolean> | null = null;
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
// let nodeValue: Writable<boolean> = writable(false);
|
||||
let attrsChanged: Writable<number> = writable(0);
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfyButtonNode
|
||||
nodeValue = node.value;
|
||||
// nodeValue = node.value;
|
||||
attrsChanged = widget.attrsChanged;
|
||||
}
|
||||
};
|
||||
@@ -26,7 +28,7 @@
|
||||
}
|
||||
|
||||
const style = {
|
||||
full_width: "100%",
|
||||
full_width: true
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Checkbox } from "@gradio/form";
|
||||
import { get, type Writable, writable } from "svelte/store";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// import VirtualList from '$lib/components/VirtualList.svelte';
|
||||
import VirtualList from 'svelte-tiny-virtual-list';
|
||||
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 { isDisabled } from "./utils"
|
||||
export let widget: WidgetLayout | null = null;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Image } from "@gradio/icons";
|
||||
import { StaticImage } from "$lib/components/gradio/image";
|
||||
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 type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { TextBox } from "@gradio/form";
|
||||
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { get, type Writable } from "svelte/store";
|
||||
import { debounce } from "$lib/utils";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Radio } from "@gradio/form";
|
||||
import { get, type Writable, writable } from "svelte/store";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { isDisabled } from "./utils"
|
||||
import type { ComfyTextNode } from "$lib/nodes/widgets";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { IDragItem } from "$lib/stores/layoutState";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import type { IDragItem } from "$lib/stores/layoutStates";
|
||||
import { LGraphNode, NodeMode } from "@litegraph-ts/core";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
<script lang="ts">
|
||||
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 workflowState, { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
import { getNodeInfo } from "$lib/utils"
|
||||
|
||||
import { Link, Toolbar } from "framework7-svelte"
|
||||
import ProgressBar from "$lib/components/ProgressBar.svelte";
|
||||
import Indicator from "./Indicator.svelte";
|
||||
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 app: ComfyApp = undefined;
|
||||
let layoutState: WritableLayoutStateStore = null;
|
||||
let fileInput: HTMLInputElement = undefined;
|
||||
let workflow: ComfyWorkflow | null = null;
|
||||
|
||||
$: workflow = $workflowState.activeWorkflow;
|
||||
|
||||
function queuePrompt() {
|
||||
navigator.vibrate(20)
|
||||
app.runDefaultQueueAction()
|
||||
}
|
||||
|
||||
function refreshCombos() {
|
||||
async function refreshCombos() {
|
||||
navigator.vibrate(20)
|
||||
app.refreshComboInNodes()
|
||||
await app.refreshComboInNodes()
|
||||
}
|
||||
|
||||
function doSave(): void {
|
||||
if (!app?.lGraph || !fileInput)
|
||||
if (!fileInput)
|
||||
return;
|
||||
|
||||
navigator.vibrate(20)
|
||||
@@ -35,7 +37,7 @@
|
||||
}
|
||||
|
||||
function doLoad(): void {
|
||||
if (!app?.lGraph || !fileInput)
|
||||
if (!fileInput)
|
||||
return;
|
||||
|
||||
navigator.vibrate(20)
|
||||
@@ -48,9 +50,6 @@
|
||||
}
|
||||
|
||||
function doSaveLocal(): void {
|
||||
if (!app?.lGraph)
|
||||
return;
|
||||
|
||||
navigator.vibrate(20)
|
||||
app.saveStateToLocalStorage();
|
||||
}
|
||||
@@ -74,9 +73,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
<Toolbar bottom>
|
||||
{#if $layoutState.attrs.queuePromptButtonName != ""}
|
||||
{#if workflow != null && workflow.attrs.queuePromptButtonName != ""}
|
||||
<Link on:click={queuePrompt}>
|
||||
{$layoutState.attrs.queuePromptButtonName}
|
||||
{workflow.attrs.queuePromptButtonName}
|
||||
</Link>
|
||||
{/if}
|
||||
<Link on:click={refreshCombos}>🔄</Link>
|
||||
|
||||
@@ -18,21 +18,22 @@
|
||||
lCanvas.draw(true, true);
|
||||
}
|
||||
|
||||
$: if (app != null && app.lGraph && canvasEl != null) {
|
||||
if (!lCanvas) {
|
||||
lCanvas = new ComfyGraphCanvas(app, canvasEl);
|
||||
lCanvas.allow_interaction = false;
|
||||
app.lGraph.eventBus.on("afterExecute", () => lCanvas.draw(true))
|
||||
}
|
||||
resizeCanvas();
|
||||
}
|
||||
// TODO
|
||||
// $: if (app?.activeGraph != null && canvasEl != null) {
|
||||
// if (!lCanvas) {
|
||||
// lCanvas = new ComfyGraphCanvas(app, app.activeGraph, canvasEl);
|
||||
// lCanvas.allow_interaction = false;
|
||||
// app.activeGraph.eventBus.on("afterExecute", () => lCanvas.draw(true))
|
||||
// }
|
||||
// resizeCanvas();
|
||||
// }
|
||||
|
||||
</script>
|
||||
|
||||
<Page>
|
||||
<Navbar title="Node Graph" backLink="Back" />
|
||||
<div class="canvas-wrapper pane-wrapper">
|
||||
<canvas bind:this={canvasEl} id="extra-canvas" />
|
||||
<!-- <canvas bind:this={canvasEl} id="extra-canvas" /> -->
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
export let app: ComfyApp | null = null;
|
||||
|
||||
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) {
|
||||
await app.initDefaultGraph();
|
||||
await app.initDefaultWorkflow();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
<script lang="ts">
|
||||
import layoutState, { type IDragItem } from "$lib/stores/layoutState";
|
||||
|
||||
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
|
||||
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
|
||||
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 app: ComfyApp
|
||||
|
||||
// TODO move
|
||||
let workflow: ComfyWorkflow | null = null
|
||||
let layoutState: WritableLayoutStateStore | null = null;
|
||||
|
||||
$: layoutState = workflow ? workflow.layout : null;
|
||||
</script>
|
||||
|
||||
<Page name="subworkflow">
|
||||
<Navbar title="Workflow {subworkflowID}" backLink="Back" />
|
||||
|
||||
<div class="container">
|
||||
<WidgetContainer bind:dragItem={$layoutState.root} isMobile={true} classes={["root-container", "mobile"]} />
|
||||
</div>
|
||||
{#if layoutState}
|
||||
<div class="container">
|
||||
<WidgetContainer bind:dragItem={$layoutState.root} {layoutState} isMobile={true} classes={["root-container", "mobile"]} />
|
||||
</div>
|
||||
{/if}
|
||||
</Page>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -7,13 +7,14 @@ import ComfyPromptSerializer from "$lib/components/ComfyPromptSerializer";
|
||||
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||
import ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import { graphToGraphVis } from "$lib/utils";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||
import { get } from "svelte/store";
|
||||
import layoutStates from "$lib/stores/layoutStates";
|
||||
import { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
|
||||
export default class ComfyGraphTests extends UnitTest {
|
||||
test__onNodeAdded__updatesLayoutState() {
|
||||
const graph = new ComfyGraph();
|
||||
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||
layoutState.initDefaultLayout() // adds 3 containers
|
||||
|
||||
const state = get(layoutState)
|
||||
@@ -38,7 +39,7 @@ export default class ComfyGraphTests extends UnitTest {
|
||||
}
|
||||
|
||||
test__onNodeAdded__handlesNodesAddedInSubgraphs() {
|
||||
const graph = new ComfyGraph();
|
||||
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||
layoutState.initDefaultLayout()
|
||||
|
||||
const subgraph = LiteGraph.createNode(Subgraph);
|
||||
@@ -57,7 +58,7 @@ export default class ComfyGraphTests extends UnitTest {
|
||||
}
|
||||
|
||||
test__onNodeAdded__handlesSubgraphsWithNodes() {
|
||||
const graph = new ComfyGraph();
|
||||
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||
layoutState.initDefaultLayout()
|
||||
|
||||
const state = get(layoutState)
|
||||
@@ -75,7 +76,7 @@ export default class ComfyGraphTests extends UnitTest {
|
||||
}
|
||||
|
||||
test__onNodeRemoved__updatesLayoutState() {
|
||||
const graph = new ComfyGraph();
|
||||
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||
layoutState.initDefaultLayout()
|
||||
|
||||
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 { expect } from 'vitest'
|
||||
import UnitTest from "./UnitTest"
|
||||
@@ -139,6 +139,38 @@ export default class ComfyPromptSerializerTests extends UnitTest {
|
||||
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() {
|
||||
const ser = new ComfyPromptSerializer();
|
||||
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[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