Global modal system

This commit is contained in:
space-nuko
2023-05-21 15:48:38 -05:00
parent 7a8be3d1b4
commit 02afbae406
15 changed files with 306 additions and 59 deletions

View File

@@ -7,11 +7,9 @@
import LightboxModal from "./LightboxModal.svelte"; import LightboxModal from "./LightboxModal.svelte";
import Sidebar from "./Sidebar.svelte"; import Sidebar from "./Sidebar.svelte";
import SidebarItem from "./SidebarItem.svelte"; import SidebarItem from "./SidebarItem.svelte";
// import Modal from "./Modal.svelte";
// import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
import notify from "$lib/notify"; import notify from "$lib/notify";
import ComfyWorkflowsView from "./ComfyWorkflowsView.svelte"; import ComfyWorkflowsView from "./ComfyWorkflowsView.svelte";
import GlobalModal from "./GlobalModal.svelte";
export let app: ComfyApp = undefined; export let app: ComfyApp = undefined;
let hasShownUIHelpToast: boolean = false; let hasShownUIHelpToast: boolean = false;
@@ -36,11 +34,6 @@
document.getElementById("app-root").classList.remove("dark") document.getElementById("app-root").classList.remove("dark")
} }
// let showModal: boolean = false;
//
// $: showModal = $a1111Prompt != null
//
// let selectedTab
</script> </script>
<svelte:head> <svelte:head>
@@ -49,20 +42,6 @@
{/if} {/if}
</svelte:head> </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>
-->
<div id="main" class:dark={uiTheme === "gradio-dark"}> <div id="main" class:dark={uiTheme === "gradio-dark"}>
<div id="container"> <div id="container">
<Sidebar selected="generate"> <Sidebar selected="generate">
@@ -74,6 +53,7 @@
</Sidebar> </Sidebar>
</div> </div>
<LightboxModal /> <LightboxModal />
<GlobalModal/>
</div> </div>
<SvelteToast options={toastOptions} /> <SvelteToast options={toastOptions} />

View File

@@ -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 { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex, type UUID } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api" import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
import { getPngMetadata, importA1111, parsePNGMetadata } from "$lib/pnginfo"; import { importA1111, parsePNGMetadata } from "$lib/pnginfo";
import EventEmitter from "events"; import EventEmitter from "events";
import type TypedEmitter from "typed-emitter"; import type TypedEmitter from "typed-emitter";
import A1111PromptDisplay from "./A1111PromptDisplay.svelte";
// Import nodes // Import nodes
import "@litegraph-ts/nodes-basic" import "@litegraph-ts/nodes-basic"
@@ -28,7 +30,7 @@ import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { tick } from "svelte"; import { tick } from "svelte";
import uiState from "$lib/stores/uiState"; 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 notify from "$lib/notify";
import configState from "$lib/stores/configState"; import configState from "$lib/stores/configState";
import { blankGraph } from "$lib/defaultGraph"; import { blankGraph } from "$lib/defaultGraph";
@@ -45,7 +47,7 @@ import layoutStates from "$lib/stores/layoutStates";
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState"; import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
import workflowState from "$lib/stores/workflowState"; import workflowState from "$lib/stores/workflowState";
import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from "$lib/convertVanillaWorkflow"; import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from "$lib/convertVanillaWorkflow";
import convertVanillaWorkflow from "$lib/convertVanillaWorkflow"; import modalState from "$lib/stores/modalState";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -142,6 +144,11 @@ type CanvasState = {
canvas: ComfyGraphCanvas, canvas: ComfyGraphCanvas,
} }
export type WorkflowLoadError = {
message: string,
error: Error
}
function isComfyBoxWorkflow(data: any): data is SerializedAppState { function isComfyBoxWorkflow(data: any): data is SerializedAppState {
return data != null && (typeof data === "object") && data.comfyBoxWorkflow; return data != null && (typeof data === "object") && data.comfyBoxWorkflow;
} }
@@ -167,7 +174,6 @@ export default class ComfyApp {
ctrlDown: boolean = false; ctrlDown: boolean = false;
selectedGroupMoving: boolean = false; selectedGroupMoving: boolean = false;
alreadySetup: Writable<boolean> = writable(false); alreadySetup: Writable<boolean> = writable(false);
a1111Prompt: Writable<A1111PromptAndInfo | null> = writable(null);
private queueItems: PromptQueueItem[] = []; private queueItems: PromptQueueItem[] = [];
private processingQueue: boolean = false; 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 = { const attrs: WorkflowAttributes = {
...defaultWorkflowAttributes, ...defaultWorkflowAttributes,
title: "ComfyUI Workflow" title
} }
const canvas: SerializedGraphCanvasState = { const canvas: SerializedGraphCanvasState = {
@@ -579,8 +585,8 @@ export default class ComfyApp {
return workflow; return workflow;
} }
async openVanillaWorkflow(data: SerializedLGraph) { async openVanillaWorkflow(data: SerializedLGraph, filename: string) {
const converted = this.convertVanillaWorkflow(data) const converted = this.convertVanillaWorkflow(data, basename(filename))
console.info("WORKFLWO", converted) console.info("WORKFLWO", converted)
notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" }) notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" })
await this.openWorkflow(converted); await this.openWorkflow(converted);
@@ -818,7 +824,7 @@ export default class ComfyApp {
await this.openWorkflow(JSON.parse(pngInfo.comfyBoxWorkflow)); await this.openWorkflow(JSON.parse(pngInfo.comfyBoxWorkflow));
} else if (pngInfo.workflow) { } else if (pngInfo.workflow) {
const workflow = JSON.parse(pngInfo.workflow); const workflow = JSON.parse(pngInfo.workflow);
await this.openVanillaWorkflow(workflow); await this.openVanillaWorkflow(workflow, file.name);
} else if (pngInfo.parameters) { } else if (pngInfo.parameters) {
const parsed = parseA1111(pngInfo.parameters) const parsed = parseA1111(pngInfo.parameters)
if ("error" in parsed) { if ("error" in parsed) {
@@ -826,11 +832,19 @@ export default class ComfyApp {
return; return;
} }
const converted = convertA1111ToStdPrompt(parsed) const converted = convertA1111ToStdPrompt(parsed)
this.a1111Prompt.set({ const a1111Info: A1111PromptAndInfo = {
infotext: pngInfo.parameters, infotext: pngInfo.parameters,
parsedInfotext: parsed, parsedInfotext: parsed,
stdPrompt: converted, stdPrompt: converted,
imageFile: file imageFile: file
}
modalState.pushModal({
title: "A1111 Prompt Details",
svelteComponent: A1111PromptDisplay,
svelteProps: {
prompt: a1111Info
},
showCloseButton: true
}) })
} }
else { else {
@@ -846,7 +860,7 @@ export default class ComfyApp {
await this.openWorkflow(result); await this.openWorkflow(result);
} }
else if (isVanillaWorkflow(result)) { else if (isVanillaWorkflow(result)) {
await this.openVanillaWorkflow(result); await this.openVanillaWorkflow(result, file.name);
} }
}; };
reader.readAsText(file); reader.readAsText(file);
@@ -990,6 +1004,5 @@ export default class ComfyApp {
* Clean current state * Clean current state
*/ */
clean() { clean() {
this.a1111Prompt.set(null);
} }
} }

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import type ComfyApp from "./ComfyApp"; import type ComfyApp from "./ComfyApp";
import DropZone from "./DropZone.svelte";
export let app: ComfyApp; export let app: ComfyApp;
export let transitioning: boolean = false; export let transitioning: boolean = false;
@@ -13,6 +14,7 @@
<div class="wrapper litegraph"> <div class="wrapper litegraph">
<div class="canvas-wrapper pane-wrapper"> <div class="canvas-wrapper pane-wrapper">
<canvas id="graph-canvas" /> <canvas id="graph-canvas" />
<DropZone {app} />
</div> </div>
<div class="bar"> <div class="bar">
{#if !transitioning} {#if !transitioning}

View File

@@ -4,7 +4,7 @@
import Spinner from "./Spinner.svelte"; import Spinner from "./Spinner.svelte";
import PromptDisplay from "./PromptDisplay.svelte"; import PromptDisplay from "./PromptDisplay.svelte";
import { ListIcon as List } from "svelte-feather-icons"; import { ListIcon as List } from "svelte-feather-icons";
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo } from "$lib/utils" import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo, truncateString } from "$lib/utils"
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { QueueItemType } from "$lib/api"; import type { QueueItemType } from "$lib/api";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
@@ -75,7 +75,7 @@
if (entry.workflowID != null) { if (entry.workflowID != null) {
const workflow = workflowState.getWorkflow(entry.workflowID); const workflow = workflowState.getWorkflow(entry.workflowID);
if (workflow != null && workflow.attrs.title) { if (workflow != null && workflow.attrs.title) {
message = `Workflow: ${workflow.attrs.title}` message = `${workflow.attrs.title}`
} }
if (subgraphs?.length > 0) if (subgraphs?.length > 0)
message += ` (${subgraphs.join(', ')})` message += ` (${subgraphs.join(', ')})`
@@ -208,7 +208,6 @@
</Modal> </Modal>
<div class="queue"> <div class="queue">
<!-- <DropZone {app} /> -->
<div class="queue-entries {mode}-mode" bind:this={queueList}> <div class="queue-entries {mode}-mode" bind:this={queueList}>
{#if _entries.length > 0} {#if _entries.length > 0}
{#each _entries as entry} {#each _entries as entry}
@@ -230,7 +229,7 @@
{/if} {/if}
<div class="queue-entry-details"> <div class="queue-entry-details">
<div class="queue-entry-message"> <div class="queue-entry-message">
{entry.message} {truncateString(entry.message, 20)}
</div> </div>
<div class="queue-entry-submessage"> <div class="queue-entry-submessage">
{entry.submessage} {entry.submessage}

View File

@@ -18,6 +18,7 @@
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { cubicIn } from 'svelte/easing'; import { cubicIn } from 'svelte/easing';
import { truncateString } from '$lib/utils';
export let app: ComfyApp; export let app: ComfyApp;
export let uiTheme: string = "gradio-dark" // TODO config export let uiTheme: string = "gradio-dark" // TODO config
@@ -235,7 +236,7 @@
class:selected={item.id === $workflowState.activeWorkflowID} class:selected={item.id === $workflowState.activeWorkflowID}
on:click={() => app.setActiveWorkflow(item.id)}> on:click={() => app.setActiveWorkflow(item.id)}>
<span class="workflow-tab-title"> <span class="workflow-tab-title">
{workflow.attrs.title} {truncateString(workflow.attrs.title, 32)}
{#if workflow.isModified} {#if workflow.isModified}
* *
{/if} {/if}

View File

@@ -1,15 +1,12 @@
<script lang="ts"> <script lang="ts">
import { writable, type Writable } from "svelte/store"; import modalState from "$lib/stores/modalState";
import type ComfyApp from "./ComfyApp"; import type ComfyApp from "./ComfyApp";
export let app: ComfyApp; export let app: ComfyApp;
let a1111Prompt: Writable<any | null> = writable(null);
let dropZone: HTMLDivElement | null = null; let dropZone: HTMLDivElement | null = null;
let disabled = false; let disabled = false;
$: a1111Prompt = app.a1111Prompt; $: disabled = $modalState.activeModals.length > 0;
$: disabled = a1111Prompt && $a1111Prompt;
$: if (disabled) { $: if (disabled) {
hideDropZone(); hideDropZone();

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import modalState, { type ModalData } from "$lib/stores/modalState";
import { Button } from "@gradio/button";
import Modal from "./Modal.svelte";
function onClose(modal: ModalData | null) {
if (modal == null)
return;
if (modal.onClose)
modal.onClose()
modalState.closeModal(modal.id)
}
</script>
{#each $modalState.activeModals as modal(modal.id)}
<Modal showModal={true} on:close={() => onClose(modal)}>
<div slot="header" class="modal-header">
{#if modal != null}
<h1 style="padding-bottom: 1rem;">{modal.title}</h1>
{/if}
</div>
<svelte:fragment>
{#if modal != null && modal.svelteComponent != null}
<svelte:component this={modal.svelteComponent} {...modal.svelteProps}/>
{/if}
</svelte:fragment>
<div slot="buttons" let:closeDialog>
{#if modal != null && modal.buttons?.length > 0}
{#each modal.buttons as button}
<Button variant={button.variant} on:click={button.onClick}>
{button.name}
</Button>
{/each}
{/if}
{#if modal.showCloseButton}
<Button variant="secondary" on:click={closeDialog}>
Close
</Button>
{/if}
</div>
</Modal>
{/each}

View File

@@ -8,6 +8,7 @@
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte"; import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import type { Styles } from "@gradio/utils"; import type { Styles } from "@gradio/utils";
import { countNewLines } from "$lib/utils";
const splitLength = 50; const splitLength = 50;
@@ -29,10 +30,6 @@
&& typeof input[1] === "number" && typeof input[1] === "number"
} }
function countNewLines(str: string): number {
return str.split(/\r\n|\r|\n/).length
}
function isMultiline(input: any): boolean { function isMultiline(input: any): boolean {
return typeof input === "string" && (input.length > splitLength || countNewLines(input) > 1); return typeof input === "string" && (input.length > splitLength || countNewLines(input) > 1);
} }

View File

@@ -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 { 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 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 { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas"; import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
import ComfyApp from "./components/ComfyApp"; import ComfyApp from "./components/ComfyApp";
@@ -12,6 +12,7 @@ import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode"
import { v4 as uuidv4 } from "uuid" import { v4 as uuidv4 } from "uuid"
import type ComfyWidgetNode from "./nodes/widgets/ComfyWidgetNode"; import type ComfyWidgetNode from "./nodes/widgets/ComfyWidgetNode";
import { ComfyGalleryNode } from "./nodes/widgets"; import { ComfyGalleryNode } from "./nodes/widgets";
import { countNewLines } from "./utils";
/* /*
* The workflow type used by base ComfyUI * The workflow type used by base ComfyUI
@@ -40,7 +41,6 @@ const vanillaToComfyBoxNodeMapping: Record<string, string> = {
/* /*
* Version of LGraphNode.getConnectionPos but for serialized nodes. * Version of LGraphNode.getConnectionPos but for serialized nodes.
*
* TODO handle other node types! (horizontal, hardcoded slot pos, collapsed...) * TODO handle other node types! (horizontal, hardcoded slot pos, collapsed...)
*/ */
function getConnectionPos(node: SerializedLGraphNode, is_input: boolean, slotNumber: number, out: Vector2 = [0, 0]): Vector2 { 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; comfyWidgetNode.flags.collapsed = true;
const size: Vector2 = [0, 0]; 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 // LiteGraph only computes it if the node is rendered
const fontSize = LiteGraph.NODE_TEXT_SIZE; const fontSize = LiteGraph.NODE_TEXT_SIZE;
size[0] = Math.min( size[0] = Math.min(
@@ -81,6 +81,14 @@ function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node:
else else
serWidgetNode.pos[0] += 20; serWidgetNode.pos[0] += 20;
serWidgetNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2; 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) vanillaWorkflow.nodes.push(serWidgetNode)
return [comfyWidgetNode, 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]) 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<NodeID, UUID> = {};
const linkIDs: Record<LinkID, UUID> = {};
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 { export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): ComfyWorkflow {
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create(); const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
const { root, left, right } = layoutState.initDefaultLayout(); const { root, left, right } = layoutState.initDefaultLayout();
@@ -104,6 +165,8 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
// TODO will need to convert IDs to UUIDs // TODO will need to convert IDs to UUIDs
const idToUUID: Record<NodeID | LinkID, UUID> = {} const idToUUID: Record<NodeID | LinkID, UUID> = {}
rewriteIDsInGraph(vanillaWorkflow);
for (const [id, node] of Object.entries(vanillaWorkflow.nodes)) { for (const [id, node] of Object.entries(vanillaWorkflow.nodes)) {
const newType = vanillaToComfyBoxNodeMapping[node.type]; const newType = vanillaToComfyBoxNodeMapping[node.type];
if (newType != null) { if (newType != null) {
@@ -201,10 +264,22 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
widgetNodeType, widgetNodeType,
value); 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) if (group == null)
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type }) 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) const connOutputIndex = serWidgetNode.outputs?.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
if (connOutputIndex != null) { if (connOutputIndex != null) {
@@ -244,7 +319,8 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
if (group == null) if (group == null)
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type }) 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) const connInputIndex = serGalleryNode.inputs?.findIndex(o => o.name === comfyGalleryNode.storeActionName)
if (connInputIndex != null) { if (connInputIndex != null) {

View File

@@ -5,13 +5,17 @@ import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode";
export interface ComfyTextProperties extends ComfyWidgetProperties { export interface ComfyTextProperties extends ComfyWidgetProperties {
multiline: boolean; multiline: boolean;
lines: number;
maxLines: number;
} }
export default class ComfyTextNode extends ComfyWidgetNode<string> { export default class ComfyTextNode extends ComfyWidgetNode<string> {
override properties: ComfyTextProperties = { override properties: ComfyTextProperties = {
tags: [], tags: [],
defaultValue: "", defaultValue: "",
multiline: false multiline: false,
lines: 5,
maxLines: 5,
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {

View File

@@ -428,6 +428,38 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "gallery", defaultValue: "gallery",
refreshPanelOnChange: true 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
},
] ]
}, },
{ {

View File

@@ -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<string, any>,
buttons?: ModalButton[],
showCloseButton?: boolean
}
export interface ModalState {
activeModals: ModalData[]
}
export interface ModalStateOps {
pushModal: (data: Partial<ModalData>) => void,
closeModal: (id: string) => void,
closeAllModals: () => void,
}
export type WritableModalStateStore = Writable<ModalState> & ModalStateOps;
const store: Writable<ModalState> = writable(
{
activeModals: []
})
function pushModal(data: Partial<ModalData>) {
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;

View File

@@ -1,5 +1,5 @@
import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas'; 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 { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import { defaultWorkflowAttributes, type SerializedLayoutState, type WritableLayoutStateStore } from './layoutStates'; import { defaultWorkflowAttributes, type SerializedLayoutState, type WritableLayoutStateStore } from './layoutStates';
@@ -76,6 +76,11 @@ export class ComfyWorkflow {
*/ */
isModified: boolean = false; isModified: boolean = false;
/*
* Missing node types encountered when deserializing the graph
*/
missingNodeTypes: string[];
get layout(): WritableLayoutStateStore | null { get layout(): WritableLayoutStateStore | null {
return layoutStates.getLayout(this.id) return layoutStates.getLayout(this.id)
} }
@@ -170,6 +175,18 @@ export class ComfyWorkflow {
} }
deserialize(layoutState: WritableLayoutStateStore, data: SerializedWorkflowState) { 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 // Ensure loadGraphData does not trigger any state changes in layoutState
// (isConfiguring is set to true here) // (isConfiguring is set to true here)
// lGraph.configure will add new nodes, triggering onNodeAdded, but we // lGraph.configure will add new nodes, triggering onNodeAdded, but we

View File

@@ -19,6 +19,25 @@ export function range(size: number, startAt: number = 0): ReadonlyArray<number>
return [...Array(size).keys()].map(i => i + startAt); 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<T>(iterable: Iterable<T>): Iterable<[number, T]> { export function* enumerate<T>(iterable: Iterable<T>): Iterable<[number, T]> {
let index = 0; let index = 0;
for (const value of iterable) { for (const value of iterable) {

View File

@@ -35,8 +35,8 @@
bind:value={$nodeValue} bind:value={$nodeValue}
label={widget.attrs.title} label={widget.attrs.title}
disabled={isDisabled(widget)} disabled={isDisabled(widget)}
lines={node.properties.multiline ? 5 : 1} lines={node.properties.multiline ? node.properties.lines : 1}
max_lines={node.properties.multiline ? 5 : 1} max_lines={node.properties.multiline ? node.properties.maxLines : 1}
show_label={widget.attrs.title !== ""} show_label={widget.attrs.title !== ""}
on:change on:change
on:submit on:submit