From 02afbae40641990f7e7216262c0ed44def1f1102 Mon Sep 17 00:00:00 2001
From: space-nuko <24979496+space-nuko@users.noreply.github.com>
Date: Sun, 21 May 2023 15:48:38 -0500
Subject: [PATCH] Global modal system
---
src/lib/components/ComfyApp.svelte | 28 +------
src/lib/components/ComfyApp.ts | 37 ++++++---
src/lib/components/ComfyGraphView.svelte | 2 +
src/lib/components/ComfyQueue.svelte | 7 +-
src/lib/components/ComfyWorkflowsView.svelte | 3 +-
src/lib/components/DropZone.svelte | 7 +-
src/lib/components/GlobalModal.svelte | 44 ++++++++++
src/lib/components/PromptDisplay.svelte | 5 +-
src/lib/convertVanillaWorkflow.ts | 86 ++++++++++++++++++--
src/lib/nodes/widgets/ComfyTextNode.ts | 6 +-
src/lib/stores/layoutStates.ts | 32 ++++++++
src/lib/stores/modalState.ts | 66 +++++++++++++++
src/lib/stores/workflowState.ts | 19 ++++-
src/lib/utils.ts | 19 +++++
src/lib/widgets/TextWidget.svelte | 4 +-
15 files changed, 306 insertions(+), 59 deletions(-)
create mode 100644 src/lib/components/GlobalModal.svelte
create mode 100644 src/lib/stores/modalState.ts
diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte
index 28e77bb..bab2f5c 100644
--- a/src/lib/components/ComfyApp.svelte
+++ b/src/lib/components/ComfyApp.svelte
@@ -7,11 +7,9 @@
import LightboxModal from "./LightboxModal.svelte";
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";
-
+ import notify from "$lib/notify";
+ import ComfyWorkflowsView from "./ComfyWorkflowsView.svelte";
+ import GlobalModal from "./GlobalModal.svelte";
export let app: ComfyApp = undefined;
let hasShownUIHelpToast: boolean = false;
@@ -36,11 +34,6 @@
document.getElementById("app-root").classList.remove("dark")
}
- // let showModal: boolean = false;
- //
- // $: showModal = $a1111Prompt != null
- //
- // let selectedTab
@@ -49,20 +42,6 @@
{/if}
-
-
diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts
index 46606cf..8da6407 100644
--- a/src/lib/components/ComfyApp.ts
+++ b/src/lib/components/ComfyApp.ts
@@ -1,9 +1,11 @@
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, parsePNGMetadata } from "$lib/pnginfo";
+import { importA1111, parsePNGMetadata } from "$lib/pnginfo";
import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
+import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
+
// Import nodes
import "@litegraph-ts/nodes-basic"
@@ -28,7 +30,7 @@ import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import { get, writable, type Writable } from "svelte/store";
import { tick } from "svelte";
import uiState from "$lib/stores/uiState";
-import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils";
+import { basename, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils";
import notify from "$lib/notify";
import configState from "$lib/stores/configState";
import { blankGraph } from "$lib/defaultGraph";
@@ -45,7 +47,7 @@ import layoutStates from "$lib/stores/layoutStates";
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
import workflowState from "$lib/stores/workflowState";
import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from "$lib/convertVanillaWorkflow";
-import convertVanillaWorkflow from "$lib/convertVanillaWorkflow";
+import modalState from "$lib/stores/modalState";
export const COMFYBOX_SERIAL_VERSION = 1;
@@ -142,6 +144,11 @@ type CanvasState = {
canvas: ComfyGraphCanvas,
}
+export type WorkflowLoadError = {
+ message: string,
+ error: Error
+}
+
function isComfyBoxWorkflow(data: any): data is SerializedAppState {
return data != null && (typeof data === "object") && data.comfyBoxWorkflow;
}
@@ -167,7 +174,6 @@ export default class ComfyApp {
ctrlDown: boolean = false;
selectedGroupMoving: boolean = false;
alreadySetup: Writable = writable(false);
- a1111Prompt: Writable = writable(null);
private queueItems: PromptQueueItem[] = [];
private processingQueue: boolean = false;
@@ -266,10 +272,10 @@ export default class ComfyApp {
}
- convertVanillaWorkflow(workflow: ComfyVanillaWorkflow): SerializedAppState {
+ convertVanillaWorkflow(workflow: ComfyVanillaWorkflow, title: string): SerializedAppState {
const attrs: WorkflowAttributes = {
...defaultWorkflowAttributes,
- title: "ComfyUI Workflow"
+ title
}
const canvas: SerializedGraphCanvasState = {
@@ -579,8 +585,8 @@ export default class ComfyApp {
return workflow;
}
- async openVanillaWorkflow(data: SerializedLGraph) {
- const converted = this.convertVanillaWorkflow(data)
+ async openVanillaWorkflow(data: SerializedLGraph, filename: string) {
+ const converted = this.convertVanillaWorkflow(data, basename(filename))
console.info("WORKFLWO", converted)
notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" })
await this.openWorkflow(converted);
@@ -818,7 +824,7 @@ export default class ComfyApp {
await this.openWorkflow(JSON.parse(pngInfo.comfyBoxWorkflow));
} else if (pngInfo.workflow) {
const workflow = JSON.parse(pngInfo.workflow);
- await this.openVanillaWorkflow(workflow);
+ await this.openVanillaWorkflow(workflow, file.name);
} else if (pngInfo.parameters) {
const parsed = parseA1111(pngInfo.parameters)
if ("error" in parsed) {
@@ -826,11 +832,19 @@ export default class ComfyApp {
return;
}
const converted = convertA1111ToStdPrompt(parsed)
- this.a1111Prompt.set({
+ const a1111Info: A1111PromptAndInfo = {
infotext: pngInfo.parameters,
parsedInfotext: parsed,
stdPrompt: converted,
imageFile: file
+ }
+ modalState.pushModal({
+ title: "A1111 Prompt Details",
+ svelteComponent: A1111PromptDisplay,
+ svelteProps: {
+ prompt: a1111Info
+ },
+ showCloseButton: true
})
}
else {
@@ -846,7 +860,7 @@ export default class ComfyApp {
await this.openWorkflow(result);
}
else if (isVanillaWorkflow(result)) {
- await this.openVanillaWorkflow(result);
+ await this.openVanillaWorkflow(result, file.name);
}
};
reader.readAsText(file);
@@ -990,6 +1004,5 @@ export default class ComfyApp {
* Clean current state
*/
clean() {
- this.a1111Prompt.set(null);
}
}
diff --git a/src/lib/components/ComfyGraphView.svelte b/src/lib/components/ComfyGraphView.svelte
index 76ba6d5..a71e497 100644
--- a/src/lib/components/ComfyGraphView.svelte
+++ b/src/lib/components/ComfyGraphView.svelte
@@ -1,6 +1,7 @@
+
+{#each $modalState.activeModals as modal(modal.id)}
+ onClose(modal)}>
+
+
+ {#if modal != null && modal.svelteComponent != null}
+
+ {/if}
+
+
+ {#if modal != null && modal.buttons?.length > 0}
+ {#each modal.buttons as button}
+
+ {/each}
+ {/if}
+ {#if modal.showCloseButton}
+
+ {/if}
+
+
+{/each}
diff --git a/src/lib/components/PromptDisplay.svelte b/src/lib/components/PromptDisplay.svelte
index 7504d1a..9f86644 100644
--- a/src/lib/components/PromptDisplay.svelte
+++ b/src/lib/components/PromptDisplay.svelte
@@ -8,6 +8,7 @@
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
import { ImageViewer } from "$lib/ImageViewer";
import type { Styles } from "@gradio/utils";
+ import { countNewLines } from "$lib/utils";
const splitLength = 50;
@@ -29,10 +30,6 @@
&& typeof input[1] === "number"
}
- function countNewLines(str: string): number {
- return str.split(/\r\n|\r|\n/).length
- }
-
function isMultiline(input: any): boolean {
return typeof input === "string" && (input.length > splitLength || countNewLines(input) > 1);
}
diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts
index 31c3e1b..50933f0 100644
--- a/src/lib/convertVanillaWorkflow.ts
+++ b/src/lib/convertVanillaWorkflow.ts
@@ -1,6 +1,6 @@
import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot } from "@litegraph-ts/core";
import type { SerializedAppState } from "./components/ComfyApp";
-import layoutStates, { defaultWorkflowAttributes, type DragItemID, type SerializedDragEntry, type SerializedLayoutState } from "./stores/layoutStates";
+import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState } from "./stores/layoutStates";
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
import ComfyApp from "./components/ComfyApp";
@@ -12,6 +12,7 @@ import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode"
import { v4 as uuidv4 } from "uuid"
import type ComfyWidgetNode from "./nodes/widgets/ComfyWidgetNode";
import { ComfyGalleryNode } from "./nodes/widgets";
+import { countNewLines } from "./utils";
/*
* The workflow type used by base ComfyUI
@@ -40,7 +41,6 @@ const vanillaToComfyBoxNodeMapping: Record = {
/*
* Version of LGraphNode.getConnectionPos but for serialized nodes.
- *
* TODO handle other node types! (horizontal, hardcoded slot pos, collapsed...)
*/
function getConnectionPos(node: SerializedLGraphNode, is_input: boolean, slotNumber: number, out: Vector2 = [0, 0]): Vector2 {
@@ -63,7 +63,7 @@ function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node:
comfyWidgetNode.flags.collapsed = true;
const size: Vector2 = [0, 0];
- // Compute collapsed size, sinze computeSize() ignores the collapsed flag
+ // Compute collapsed size, since computeSize() ignores the collapsed flag
// LiteGraph only computes it if the node is rendered
const fontSize = LiteGraph.NODE_TEXT_SIZE;
size[0] = Math.min(
@@ -81,6 +81,14 @@ function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node:
else
serWidgetNode.pos[0] += 20;
serWidgetNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2;
+
+ if (widgetNodeType === "ui/text" && typeof value === "string" && value.indexOf("\n") != -1) {
+ const lineCount = countNewLines(value);
+ serWidgetNode.properties.multiline = true;
+ serWidgetNode.properties.lines = lineCount + 2
+ serWidgetNode.properties.maxLines = lineCount + 2
+ }
+
vanillaWorkflow.nodes.push(serWidgetNode)
return [comfyWidgetNode, serWidgetNode];
@@ -97,6 +105,59 @@ function connectSerializedNodes(vanillaWorkflow: ComfyVanillaWorkflow, originNod
vanillaWorkflow.links.push([newLinkID, originNode.id, originSlot, targetNode.id, targetSlot, connInput.type])
}
+/*
+ * Converts all the IDs in the serialized graph into UUID format
+ */
+function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
+ const nodeIDs: Record = {};
+ const linkIDs: Record = {};
+
+ const getNodeID = (id: NodeID): UUID => {
+ if (typeof id === "string")
+ return id
+ nodeIDs[id] ||= uuidv4();
+ return nodeIDs[id];
+ }
+
+ const getLinkID = (id: LinkID): UUID => {
+ if (typeof id === "string")
+ return id
+ linkIDs[id] ||= uuidv4();
+ return linkIDs[id];
+ }
+
+ for (const node of vanillaWorkflow.nodes) {
+ node.id = getNodeID(node.id);
+ if (node.inputs != null) {
+ for (const input of node.inputs) {
+ if (input.link != null) {
+ input.link = getLinkID(input.link)
+ }
+ }
+ }
+ if (node.outputs != null) {
+ for (const output of node.outputs) {
+ if (output.links != null)
+ output.links = output.links.map(getLinkID);
+ }
+ }
+ }
+
+ for (const link of vanillaWorkflow.links) {
+ link[0] = getLinkID(link[0])
+ link[1] = getNodeID(link[1])
+ link[3] = getNodeID(link[3])
+ }
+
+
+ // Recurse!
+ for (const node of vanillaWorkflow.nodes) {
+ if (node.type === "graph/subgraph") {
+ rewriteIDsInGraph((node as any).subgraph as SerializedLGraph)
+ }
+ }
+}
+
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): ComfyWorkflow {
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
const { root, left, right } = layoutState.initDefaultLayout();
@@ -104,6 +165,8 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
// TODO will need to convert IDs to UUIDs
const idToUUID: Record = {}
+ rewriteIDsInGraph(vanillaWorkflow);
+
for (const [id, node] of Object.entries(vanillaWorkflow.nodes)) {
const newType = vanillaToComfyBoxNodeMapping[node.type];
if (newType != null) {
@@ -201,10 +264,22 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
widgetNodeType,
value);
+ switch (widgetNodeType) {
+ case "ui/number":
+ serWidgetNode.properties.min = inputOpts?.min || 0;
+ serWidgetNode.properties.max = inputOpts?.max || 100;
+ serWidgetNode.properties.step = inputOpts?.step || 1;
+ break;
+ case "ui/text":
+ serWidgetNode.properties.multiline = inputOpts?.multiline || false;
+ break;
+ }
+
if (group == null)
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
- layoutState.addWidget(group, comfyWidgetNode)
+ const widget = layoutState.addWidget(group, comfyWidgetNode)
+ widget.attrs.title = inputName;
const connOutputIndex = serWidgetNode.outputs?.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
if (connOutputIndex != null) {
@@ -244,7 +319,8 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
if (group == null)
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
- layoutState.addWidget(group, comfyGalleryNode)
+ const widget = layoutState.addWidget(group, comfyGalleryNode)
+ widget.attrs.title = "Output"
const connInputIndex = serGalleryNode.inputs?.findIndex(o => o.name === comfyGalleryNode.storeActionName)
if (connInputIndex != null) {
diff --git a/src/lib/nodes/widgets/ComfyTextNode.ts b/src/lib/nodes/widgets/ComfyTextNode.ts
index 4a8d35e..baab9a2 100644
--- a/src/lib/nodes/widgets/ComfyTextNode.ts
+++ b/src/lib/nodes/widgets/ComfyTextNode.ts
@@ -5,13 +5,17 @@ import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode";
export interface ComfyTextProperties extends ComfyWidgetProperties {
multiline: boolean;
+ lines: number;
+ maxLines: number;
}
export default class ComfyTextNode extends ComfyWidgetNode {
override properties: ComfyTextProperties = {
tags: [],
defaultValue: "",
- multiline: false
+ multiline: false,
+ lines: 5,
+ maxLines: 5,
}
static slotLayout: SlotLayout = {
diff --git a/src/lib/stores/layoutStates.ts b/src/lib/stores/layoutStates.ts
index 4b022dd..652edcb 100644
--- a/src/lib/stores/layoutStates.ts
+++ b/src/lib/stores/layoutStates.ts
@@ -428,6 +428,38 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "gallery",
refreshPanelOnChange: true
},
+
+ // Text
+ {
+ name: "multiline",
+ type: "boolean",
+ location: "nodeProps",
+ editable: true,
+ validNodeTypes: ["ui/text"],
+ defaultValue: false
+ },
+ {
+ name: "lines",
+ type: "number",
+ location: "nodeProps",
+ editable: true,
+ validNodeTypes: ["ui/text"],
+ defaultValue: 5,
+ min: 1,
+ max: 100,
+ step: 1
+ },
+ {
+ name: "maxLines",
+ type: "number",
+ location: "nodeProps",
+ editable: true,
+ validNodeTypes: ["ui/text"],
+ defaultValue: 5,
+ min: 1,
+ max: 100,
+ step: 1
+ },
]
},
{
diff --git a/src/lib/stores/modalState.ts b/src/lib/stores/modalState.ts
new file mode 100644
index 0000000..cf4ab6c
--- /dev/null
+++ b/src/lib/stores/modalState.ts
@@ -0,0 +1,66 @@
+import type { SvelteComponentDev } from "svelte/internal";
+import { writable, type Writable } from "svelte/store";
+import { v4 as uuidv4 } from "uuid";
+
+export type ModalButton = {
+ name: string,
+ variant: "primary" | "secondary",
+ onClick: () => void
+}
+export interface ModalData {
+ id: string,
+ title: string,
+ onClose?: () => void,
+ svelteComponent?: typeof SvelteComponentDev,
+ svelteProps?: Record,
+ buttons?: ModalButton[],
+ showCloseButton?: boolean
+}
+export interface ModalState {
+ activeModals: ModalData[]
+}
+
+export interface ModalStateOps {
+ pushModal: (data: Partial) => void,
+ closeModal: (id: string) => void,
+ closeAllModals: () => void,
+}
+
+export type WritableModalStateStore = Writable & ModalStateOps;
+const store: Writable = writable(
+ {
+ activeModals: []
+ })
+
+function pushModal(data: Partial) {
+ const modal: ModalData = {
+ title: "Modal",
+ ...data,
+ id: uuidv4(),
+ }
+
+ store.update(s => {
+ s.activeModals.push(modal);
+ return s;
+ })
+}
+
+function closeModal(id: string) {
+ store.update(s => {
+ s.activeModals = s.activeModals.filter(m => m.id !== id)
+ return s;
+ })
+}
+
+function closeAllModals() {
+ store.set({ activeModals: [] })
+}
+
+const modalStateStore: WritableModalStateStore =
+{
+ ...store,
+ pushModal,
+ closeModal,
+ closeAllModals
+}
+export default modalStateStore;
diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts
index 9efca6f..67ac863 100644
--- a/src/lib/stores/workflowState.ts
+++ b/src/lib/stores/workflowState.ts
@@ -1,5 +1,5 @@
import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas';
-import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph } from '@litegraph-ts/core';
+import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph, LiteGraph } 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';
@@ -76,6 +76,11 @@ export class ComfyWorkflow {
*/
isModified: boolean = false;
+ /*
+ * Missing node types encountered when deserializing the graph
+ */
+ missingNodeTypes: string[];
+
get layout(): WritableLayoutStateStore | null {
return layoutStates.getLayout(this.id)
}
@@ -170,6 +175,18 @@ export class ComfyWorkflow {
}
deserialize(layoutState: WritableLayoutStateStore, data: SerializedWorkflowState) {
+ this.missingNodeTypes = []
+
+ for (let n of data.graph.nodes) {
+ // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
+ if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
+
+ // Find missing node types
+ if (!(n.type in LiteGraph.registered_node_types)) {
+ this.missingNodeTypes.push(n.type);
+ }
+ }
+
// 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
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 35c2162..f28c646 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -19,6 +19,25 @@ export function range(size: number, startAt: number = 0): ReadonlyArray
return [...Array(size).keys()].map(i => i + startAt);
}
+export function countNewLines(str: string): number {
+ return str.split(/\r\n|\r|\n/).length
+}
+
+export function basename(filepath: string): string {
+ const filename = filepath.split('/').pop().split('\\').pop();
+ return filename.split('.').slice(0, -1).join('.');
+}
+
+export function truncateString(str: string, num: number): string {
+ if (num <= 0)
+ return "…";
+
+ if (str.length <= num) {
+ return str;
+ }
+ return str.slice(0, num) + "…";
+}
+
export function* enumerate(iterable: Iterable): Iterable<[number, T]> {
let index = 0;
for (const value of iterable) {
diff --git a/src/lib/widgets/TextWidget.svelte b/src/lib/widgets/TextWidget.svelte
index 2106278..edad00b 100644
--- a/src/lib/widgets/TextWidget.svelte
+++ b/src/lib/widgets/TextWidget.svelte
@@ -35,8 +35,8 @@
bind:value={$nodeValue}
label={widget.attrs.title}
disabled={isDisabled(widget)}
- lines={node.properties.multiline ? 5 : 1}
- max_lines={node.properties.multiline ? 5 : 1}
+ lines={node.properties.multiline ? node.properties.lines : 1}
+ max_lines={node.properties.multiline ? node.properties.maxLines : 1}
show_label={widget.attrs.title !== ""}
on:change
on:submit