Workflows can receive images from other workflows/historical prompts

This commit is contained in:
space-nuko
2023-05-22 18:30:25 -05:00
parent b5512e6673
commit 6817e6ad95
24 changed files with 689 additions and 142 deletions

View File

@@ -6,7 +6,7 @@ export class ImageViewer {
currentImages: string[] = [] currentImages: string[] = []
selectedIndex: number = -1; selectedIndex: number = -1;
currentGallery: HTMLDivElement | null = null; currentGallery: HTMLDivElement | null = null;
private static _instance: ImageViewer; static _instance: ImageViewer;
static get instance(): ImageViewer { static get instance(): ImageViewer {
if (!ImageViewer._instance) if (!ImageViewer._instance)
@@ -62,6 +62,18 @@ export class ImageViewer {
this.currentGallery = galleryElem; this.currentGallery = galleryElem;
this.setModalImageSrc(imageUrls[index]) this.setModalImageSrc(imageUrls[index])
this.lightboxModal.style.display = "flex"; this.lightboxModal.style.display = "flex";
const left = this.lightboxModal.querySelector<HTMLElement>(".modalPrev")
const right = this.lightboxModal.querySelector<HTMLElement>(".modalNext")
if (imageUrls.length <= 1) {
left.style.display = "none"
right.style.display = "none"
}
else {
left.style.display = "block"
right.style.display = "block"
}
setTimeout(() => { setTimeout(() => {
this.modalImage.focus() this.modalImage.focus()
}, 200) }, 200)
@@ -138,6 +150,9 @@ export class ImageViewer {
} }
showLightbox(source: HTMLImageElement) { showLightbox(source: HTMLImageElement) {
if (this.lightboxModal.style.display != "none")
this.closeModal();
const initiallyZoomed = true const initiallyZoomed = true
this.modalZoomSet(this.modalImage, initiallyZoomed) this.modalZoomSet(this.modalImage, initiallyZoomed)

View File

@@ -1,57 +1,39 @@
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 ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyNodeID, type ComfyPromptRequest, type PromptID, type QueueItemType } from "$lib/api";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; import { parsePNGMetadata } from "$lib/pnginfo";
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID, type QueueItemType } from "$lib/api" import { BuiltInSlotType, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type INodeInputSlot, type LGraphNodeConstructor, type NodeID, type NodeTypeOpts, type SerializedLGraph, type SlotIndex } from "@litegraph-ts/core";
import { importA1111, parsePNGMetadata } from "$lib/pnginfo";
import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
import A1111PromptModal from "./modal/A1111PromptModal.svelte"; import A1111PromptModal from "./modal/A1111PromptModal.svelte";
import ConfirmConvertWithMissingNodeTypesModal from "./modal/ConfirmConvertWithMissingNodeTypesModal.svelte";
import MissingNodeTypesModal from "./modal/MissingNodeTypesModal.svelte"; import MissingNodeTypesModal from "./modal/MissingNodeTypesModal.svelte";
import WorkflowLoadErrorModal from "./modal/WorkflowLoadErrorModal.svelte"; import WorkflowLoadErrorModal from "./modal/WorkflowLoadErrorModal.svelte";
import ConfirmConvertWithMissingNodeTypesModal from "./modal/ConfirmConvertWithMissingNodeTypesModal.svelte";
import * as nodes from "$lib/nodes/index";
// Import nodes
import "@litegraph-ts/nodes-basic"
import "@litegraph-ts/nodes-events"
import "@litegraph-ts/nodes-logic"
import "@litegraph-ts/nodes-math"
import "@litegraph-ts/nodes-strings"
import "$lib/nodes/index"
import "$lib/nodes/widgets/index"
import "$lib/nodes/actions/index"
import * as nodes from "$lib/nodes/index"
import * as widgets from "$lib/nodes/widgets/index"
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
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 { defaultWorkflowAttributes, type LayoutState, type SerializedLayoutState, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import { toast } from '@zerodevx/svelte-toast'
import ComfyGraph from "$lib/ComfyGraph";
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 { basename, capitalize, 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";
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";
import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111";
import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt";
import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer"; import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
import selectionState from "$lib/stores/selectionState"; import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
import layoutStates from "$lib/stores/layoutStates"; import { isBackendNodeDefInputType, iterateNodeDefInputs, iterateNodeDefOutputs, type ComfyNodeDef } from "$lib/ComfyNodeDef";
import { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState"; import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt";
import workflowState from "$lib/stores/workflowState"; import convertVanillaWorkflow from "$lib/convertVanillaWorkflow";
import convertVanillaWorkflow, { type ComfyVanillaWorkflow } from "$lib/convertVanillaWorkflow"; import { blankGraph } from "$lib/defaultGraph";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import { ComfyComboNode } from "$lib/nodes/widgets";
import notify from "$lib/notify";
import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111";
import configState from "$lib/stores/configState";
import layoutStates, { defaultWorkflowAttributes, type SerializedLayoutState } from "$lib/stores/layoutStates";
import modalState from "$lib/stores/modalState"; import modalState from "$lib/stores/modalState";
import queueState from "$lib/stores/queueState";
import selectionState from "$lib/stores/selectionState";
import uiState from "$lib/stores/uiState";
import workflowState, { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
import type { SerializedPromptOutput } from "$lib/utils";
import { basename, capitalize, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range } from "$lib/utils";
import { tick } from "svelte";
import { type SvelteComponentDev } from "svelte/internal";
import { get, writable, type Writable } from "svelte/store";
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -556,9 +538,14 @@ export default class ComfyApp {
this.ctrlDown = e.ctrlKey; this.ctrlDown = e.ctrlKey;
// Queue prompt using ctrl or command + enter // Queue prompt using ctrl or command + enter
if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) { if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.code === "Enter" || e.keyCode === 10)) {
e.preventDefault();
this.runDefaultQueueAction(); this.runDefaultQueueAction();
} }
else if ((e.ctrlKey) && (e.key === "s" || e.code === "KeyS")) {
e.preventDefault();
this.saveStateToLocalStorage();
}
}); });
window.addEventListener("keyup", (e) => { window.addEventListener("keyup", (e) => {
this.shiftDown = e.shiftKey; this.shiftDown = e.shiftKey;

View File

@@ -223,8 +223,13 @@
expandAll = false expandAll = false
} }
$: if(!showModal) function closeModal() {
selectedPrompt = null; selectedPrompt = null
selectedImages = []
showModal = false;
expandAll = false;
console.warn("CLOSEMODAL")
}
let queued = false let queued = false
$: queued = Boolean($queueState.runningNodeID || $queueState.progress); $: queued = Boolean($queueState.runningNodeID || $queueState.progress);
@@ -238,9 +243,11 @@
<div slot="header" class="prompt-modal-header"> <div slot="header" class="prompt-modal-header">
<h1 style="padding-bottom: 1rem;">Prompt Details</h1> <h1 style="padding-bottom: 1rem;">Prompt Details</h1>
</div> </div>
{#if selectedPrompt} <svelte:fragment let:closeDialog>
<PromptDisplay prompt={selectedPrompt} images={selectedImages} {expandAll} /> {#if selectedPrompt}
{/if} <PromptDisplay closeModal={() => { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} images={selectedImages} {expandAll} />
{/if}
</svelte:fragment>
<div slot="buttons" let:closeDialog> <div slot="buttons" let:closeDialog>
<Button variant="secondary" on:click={closeDialog}> <Button variant="secondary" on:click={closeDialog}>
Close Close

View File

@@ -179,7 +179,7 @@
top:0; top:0;
right:0.5rem; right:0.5rem;
margin: 0.5rem; margin: 0.5rem;
z-index: 1000000000; z-index: var(--layer-1);
opacity: 70%; opacity: 70%;
background: var(--neutral-700); background: var(--neutral-700);

View File

@@ -2,19 +2,18 @@
import modalState, { type ModalButton, type ModalData } from "$lib/stores/modalState"; import modalState, { type ModalButton, type ModalData } from "$lib/stores/modalState";
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import Modal from "./Modal.svelte"; import Modal from "./Modal.svelte";
import { SvelteComponentDev } from "svelte/internal";
import { get } from "svelte/store";
function onClose(modal: ModalData | null) { function onClose(modal: ModalData | null) {
if (modal == null) if (modal == null)
return; return;
if (modal.onClose)
modal.onClose()
modalState.closeModal(modal.id) modalState.closeModal(modal.id)
} }
function onButtonClicked(button: ModalButton, closeDialog: Function) { function onButtonClicked(modal: ModalData, button: ModalButton, closeDialog: Function) {
button.onClick(); button.onClick(modal);
if (button.closeOnClick !== false) { if (button.closeOnClick !== false) {
closeDialog() closeDialog()
@@ -31,13 +30,13 @@
</div> </div>
<svelte:fragment> <svelte:fragment>
{#if modal != null && modal.svelteComponent != null} {#if modal != null && modal.svelteComponent != null}
<svelte:component this={modal.svelteComponent} {...modal.svelteProps}/> <svelte:component this={modal.svelteComponent} {...modal.svelteProps} _modal={modal}/>
{/if} {/if}
</svelte:fragment> </svelte:fragment>
<div slot="buttons" class="buttons" let:closeDialog> <div slot="buttons" class="buttons" let:closeDialog>
{#if modal != null && modal.buttons?.length > 0} {#if modal != null && modal.buttons?.length > 0}
{#each modal.buttons as button} {#each modal.buttons as button}
<Button variant={button.variant} on:click={() => onButtonClicked(button, closeDialog)}> <Button variant={button.variant} on:click={() => onButtonClicked(modal, button, closeDialog)}>
{button.name} {button.name}
</Button> </Button>
{/each} {/each}

View File

@@ -4,7 +4,7 @@
export let showModal; // boolean export let showModal; // boolean
export let closeOnClick = true; // boolean export let closeOnClick = true; // boolean
export const closeDialog = _ => doClose(); export const closeDialog = () => doClose();
let dialog; // HTMLDialogElement let dialog; // HTMLDialogElement
@@ -40,7 +40,7 @@
> >
<div on:click|stopPropagation> <div on:click|stopPropagation>
<slot name="header" /> <slot name="header" />
<slot /> <slot {closeDialog} />
<div class="button-row"> <div class="button-row">
<slot name="buttons" {closeDialog}> <slot name="buttons" {closeDialog}>
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->

View File

@@ -8,14 +8,23 @@
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"; import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines } from "$lib/utils";
import ReceiveOutputTargets from "./modal/ReceiveOutputTargets.svelte";
import workflowState, { type ComfyWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
import type ComfyApp from "./ComfyApp";
const splitLength = 50; const splitLength = 50;
export let prompt: SerializedPromptInputsAll; export let prompt: SerializedPromptInputsAll;
export let images: string[] = []; export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint
export let isMobile: boolean = false; export let isMobile: boolean = false;
export let expandAll: boolean = false; export let expandAll: boolean = false;
export let closeModal: () => void;
export let app: ComfyApp;
let isPromptOpen = expandAll;
let selected_image: number | null = null;
let galleryStyle: Styles = { let galleryStyle: Styles = {
grid_cols: [2], grid_cols: [2],
@@ -23,6 +32,32 @@
height: "var(--size-96)" height: "var(--size-96)"
} }
let receiveTargets: WorkflowReceiveOutputTargets[] = [];
let comfyBoxImages = []
let litegraphType = "(none)"
$: if (images.length > 0) {
// since the image links come from gradio, have to parse the URL for the
// ComfyImageLocation params
comfyBoxImages = images.map(comfyURLToComfyFile)
.map(comfyFileToComfyBoxMetadata);
}
else {
comfyBoxImages = []
}
$: if (comfyBoxImages.length > 0) {
if (selected_image != null)
litegraphType = "COMFYBOX_IMAGE"
else
litegraphType = "COMFYBOX_IMAGES"
receiveTargets = workflowState.findReceiveOutputTargets(litegraphType)
}
else {
litegraphType = "(none)"
receiveTargets = []
}
function isInputLink(input: SerializedPromptInput): boolean { function isInputLink(input: SerializedPromptInput): boolean {
return Array.isArray(input) return Array.isArray(input)
&& input.length === 2 && input.length === 2
@@ -65,56 +100,98 @@
// TODO dialog renders over it // TODO dialog renders over it
// ImageViewer.instance.showLightbox(e.detail) // ImageViewer.instance.showLightbox(e.detail)
} }
function sendOutput(workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode) {
if (workflow == null || targetNode == null)
return
let value = null;
if (targetNode.properties.type === "COMFYBOX_IMAGE") {
if (selected_image != null)
value = comfyBoxImages[selected_image]
else
value = comfyBoxImages[0]
}
else if (targetNode.properties.type === "COMFYBOX_IMAGES") {
value = comfyBoxImages
}
if (value == null)
return;
targetNode.receiveOutput(value);
workflowState.setActiveWorkflow(app.lCanvas, workflow.id)
closeModal();
}
</script> </script>
<div class="prompt-display"> <div class="prompt-display">
<div class="scroll-container"> <div class="prompt-and-sends">
<Block> <Block>
{#each Object.entries(prompt) as [nodeID, inputs], i} <Accordion label="Prompt" open={isPromptOpen}>
{@const classType = inputs.class_type} <div class="scroll-container">
{@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))} <Block>
{#if filtered.length > 0} {#each Object.entries(prompt) as [nodeID, inputs], i}
<div class="accordion"> {@const classType = inputs.class_type}
<Block padding={true}> {@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))}
<Accordion label="Node {i+1}: {classType}" open={expandAll}> {#if filtered.length > 0}
{#each filtered as [inputName, input]} <div class="accordion">
<Block> <Block padding={true}>
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}> <Accordion label="Node {i+1}: {classType}" open={expandAll}>
{#if copiedNodeID === nodeID && copiedInputName === inputName} {#each filtered as [inputName, input]}
<span class="copied-icon">
<Check />
</span>
{:else}
<span class="copy-text"><Copy /></span>
{/if}
</button>
<div>
{#if isInputLink(input)}
Link {input[0]} -> {input[1]}
{:else if typeof input === "object"}
<Block> <Block>
<BlockLabel <button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
Icon={JSONIcon} {#if copiedNodeID === nodeID && copiedInputName === inputName}
show_label={true} <span class="copied-icon">
label={inputName} <Check />
float={true} </span>
/> {:else}
<JSONComponent value={input} /> <span class="copy-text"><Copy /></span>
{/if}
</button>
<div>
{#if isInputLink(input)}
Link {input[0]} -> {input[1]}
{:else if typeof input === "object"}
<Block>
<BlockLabel
Icon={JSONIcon}
show_label={true}
label={inputName}
float={true}
/>
<JSONComponent value={input} />
</Block>
{:else if isMultiline(input)}
{@const lines = Math.max(countNewLines(input), input.length / splitLength)}
<TextBox label={inputName} value={formatInput(input)} {lines} max_lines={lines} />
{:else}
<TextBox label={inputName} value={formatInput(input)} lines={1} max_lines={1} />
{/if}
</div>
</Block> </Block>
{:else if isMultiline(input)} {/each}
{@const lines = Math.max(countNewLines(input), input.length / splitLength)} </Accordion>
<TextBox label={inputName} value={formatInput(input)} {lines} max_lines={lines} />
{:else}
<TextBox label={inputName} value={formatInput(input)} lines={1} max_lines={1} />
{/if}
</div>
</Block> </Block>
{/each} </div>
</Accordion> {/if}
</Block> {/each}
</div> </Block>
{/if} </div>
{/each} </Accordion>
</Block>
<Block>
<Accordion label="Send Outputs To..." open={true}>
<Block>
<BlockTitle>Output type: {litegraphType}</BlockTitle>
{#if receiveTargets.length > 0}
<ReceiveOutputTargets {receiveTargets} on:select={(e) => sendOutput(e.detail.workflow, e.detail.targetNode)} />
{:else}
<div class="outputs-message">No receive output targets found across all workflows.</div>
{/if}
</Block>
</Accordion>
</Block> </Block>
</div> </div>
{#if images.length > 0} {#if images.length > 0}
@@ -128,6 +205,7 @@
root={""} root={""}
root_url={""} root_url={""}
on:clicked={onGalleryImageClicked} on:clicked={onGalleryImageClicked}
bind:selected_image
/> />
</Block> </Block>
</div> </div>
@@ -148,6 +226,10 @@
@media (min-width: 1600px) { @media (min-width: 1600px) {
flex-direction: row; flex-direction: row;
} }
}
.prompt-and-sends {
width: 50%;
.scroll-container { .scroll-container {
position: relative; position: relative;
@@ -155,21 +237,6 @@
flex: 1 1 0%; flex: 1 1 0%;
} }
.image-container {
position: relative;
flex: 1 1 0%;
max-width: 30vw;
> :global(.block) {
height: 100%;
:global(> .preview) {
height: 100%;
max-height: none !important;
}
}
}
.copy-button { .copy-button {
display: flex; display: flex;
position: absolute; position: absolute;
@@ -220,4 +287,23 @@
} }
} }
} }
.outputs-message {
padding: 0.5rem;
}
.image-container {
position: relative;
flex: 1 1 0%;
width: 50%;
> :global(.block) {
height: 100%;
:global(> .preview) {
height: 100%;
max-height: none !important;
}
}
}
</style> </style>

View File

@@ -104,7 +104,6 @@
.accordion { .accordion {
background: var(--panel-background-fill); background: var(--panel-background-fill);
:global(> .block .block) { :global(> .block .block) {
background: var(--panel-background-fill); background: var(--panel-background-fill);
} }

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
import type { ComfyWorkflow, WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
import { Block, BlockTitle } from "@gradio/atoms";
import { Button } from "@gradio/button";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher<{
select: { workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode };
}>();
export let receiveTargets: WorkflowReceiveOutputTargets[] = [];
function onSelected( workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode ) {
dispatch("select", {
workflow,
targetNode
})
}
</script>
<div class="scroll-container">
{#if receiveTargets.length > 0}
{#each receiveTargets as { workflow, targetNodes }}
<Block>
<BlockTitle>{workflow.attrs.title}</BlockTitle>
{#each targetNodes as targetNode}
<Block>
<div class="target">
<div class="target-name-and-desc">
<div class="target-name">{targetNode.properties.name}</div>
{#if targetNode.properties.description}
<div class="target-desc">{targetNode.properties.description}</div>
{/if}
</div>
<div class="send-button">
<Button variant="primary" on:click={() => onSelected(workflow, targetNode)}>
Send
</Button>
</div>
</div>
</Block>
{/each}
</Block>
{/each}
{:else}
<div>(No receive targets found.)</div>
{/if}
</div>
<style lang="scss">
.scroll-container {
overflow: auto;
position: relative;
flex: 1 1 0%;
height: 100%;
> :global(.block) {
background: var(--panel-background-fill);
}
}
.target {
display: flex;
flex-direction: row;
justify-content: center;
text-align: left;
.target-name-and-desc {
margin: auto auto auto 0;
left: 0px;
.target-desc {
opacity: 65%;
font-size: 11pt;
}
}
}
</style>

View File

@@ -0,0 +1,79 @@
<script lang="ts" context="module">
export type SendOutputModalResult = {
workflow?: ComfyWorkflow,
targetNode?: ComfyReceiveOutputNode,
}
</script>
<script lang="ts">
import type { ModalData, ModalState } from "$lib/stores/modalState";
import { Block, BlockTitle } from "@gradio/atoms";
import type { SlotType } from "@litegraph-ts/core";
import type { Writable } from "svelte/store";
import { StaticImage } from "$lib/components/gradio/image";
import type { ComfyWorkflow, WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
import { comfyBoxImageToComfyURL } from "$lib/utils";
import { Button } from "@gradio/button";
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
import ReceiveOutputTargets from "./ReceiveOutputTargets.svelte";
export let value: any;
export let type: SlotType;
export let receiveTargets: WorkflowReceiveOutputTargets[] = [];
export let _modal: ModalData;
let images = []
if (type === "COMFYBOX_IMAGE") {
images = [comfyBoxImageToComfyURL(value)];
}
function sendOutput(workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode) {
const result: SendOutputModalResult = {
workflow,
targetNode
}
_modal.state.set(result)
_modal.close();
}
</script>
<div class="send-output-modal">
<div class="targets-container">
<Block>
<span>Type: {type}</span>
</Block>
<ReceiveOutputTargets {receiveTargets} on:select={(e) => sendOutput(e.detail.workflow, e.detail.targetNode)} />
</div>
<div class="output-display">
<Block>
{#if type === "COMFYBOX_IMAGE"}
<StaticImage show_label={false} label="Image" value={images[0]} />
{/if}
</Block>
</div>
</div>
<style lang="scss">
.send-output-modal {
width: 60vw;
height: 70vh;
color: none;
display: flex;
flex-wrap: nowrap;
overflow-y: none;
flex-direction: row;
}
.targets-container {
width: 100%;
flex-direction: column;
}
.output-display {
width: 40vw;
}
</style>

9
src/lib/nodeImports.ts Normal file
View File

@@ -0,0 +1,9 @@
import "@litegraph-ts/nodes-basic";
import "@litegraph-ts/nodes-events";
import "@litegraph-ts/nodes-logic";
import "@litegraph-ts/nodes-math";
import "@litegraph-ts/nodes-strings";
import "$lib/nodes/index";
import "$lib/nodes/widgets/index";
import "$lib/nodes/actions/index";

View File

@@ -150,9 +150,7 @@ export default class ComfyGraphNode extends LGraphNode {
const link = currentNode.getUpstreamLink(); const link = currentNode.getUpstreamLink();
if (link !== null) { if (link !== null) {
const node = this.graph.getNodeById(link.origin_id) as ComfyGraphNode; const node = this.graph.getNodeById(link.origin_id) as ComfyGraphNode;
console.warn(node.type)
if (node.canInheritSlotTypes) { if (node.canInheritSlotTypes) {
console.log("REROUTE2", node)
if (node === this) { if (node === this) {
// We've found a circle // We've found a circle
currentNode.disconnectInput(link.target_slot); currentNode.disconnectInput(link.target_slot);
@@ -193,7 +191,6 @@ export default class ComfyGraphNode extends LGraphNode {
const node = this.graph.getNodeById(link.target_id) as ComfyGraphNode; const node = this.graph.getNodeById(link.target_id) as ComfyGraphNode;
if (node.canInheritSlotTypes) { if (node.canInheritSlotTypes) {
console.log("REROUTE", node)
// Follow reroute nodes // Follow reroute nodes
nodes.push(node); nodes.push(node);
updateNodes.push(node); updateNodes.push(node);

View File

@@ -0,0 +1,91 @@
import { BuiltInSlotType, LiteGraph, type IComboWidget, type SlotLayout, type SlotType, type ITextWidget, BASE_SLOT_TYPES, LGraphNode, type Vector2, BuiltInSlotShape, LGraphCanvas } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode";
import { getLitegraphType } from "$lib/utils";
import notify from "$lib/notify";
export interface ComfyReceiveOutputNodeProperties extends ComfyGraphNodeProperties {
name: string,
description: string,
type: SlotType
}
function getOutputTypes(widget: IComboWidget, node: LGraphNode): string[] {
let result = []
result = result.concat(Array.from(BASE_SLOT_TYPES))
result.push("COMFYBOX_IMAGE")
result.push("COMFYBOX_IMAGES")
return result
}
export default class ComfyReceiveOutputNode extends ComfyGraphNode {
override properties: ComfyReceiveOutputNodeProperties = {
tags: [],
name: "Image",
description: "Generic image input.",
type: "COMFYBOX_IMAGE"
}
static slotLayout: SlotLayout = {
outputs: [
{ name: "received", type: BuiltInSlotType.EVENT }
]
}
override size: Vector2 = [180, 90];
nameWidget: ITextWidget;
descriptionWidget: ITextWidget;
typeWidget: IComboWidget;
isActive: boolean = false;
private _queue: any[] = []
constructor(title?: string) {
super(title)
this.nameWidget = this.addWidget("text", "Name", this.properties.name, "name");
this.descriptionWidget = this.addWidget("text", "Desc.", this.properties.description, "description", { multiline: true });
this.typeWidget = this.addWidget<IComboWidget>("combo", "Type", "" + this.properties.type, "type", { values: getOutputTypes });
}
override onPropertyChanged(property: any, value: any) {
if (property === "type") {
const color = LGraphCanvas.DEFAULT_CONNECTION_COLORS_BY_TYPE[value] || LGraphCanvas.DEFAULT_CONNECTION_COLORS_BY_TYPE[BuiltInSlotType.EVENT];
this.outputs[0].color_on = color
this.outputs[0].color_off = color
}
}
override getTitle(): string {
if (this.flags.collapsed) {
return this.properties.name;
}
return this.title;
}
override onExecute() {
while (this._queue.length > 0)
this.triggerSlot(0, this._queue.splice(0, 1))
}
receiveOutput(value: any) {
const type = getLitegraphType(value);
console.warn("receive", this.id, value, type)
if (type !== this.properties.type) {
console.error(`Output type mismatch! ${type} != ${this.properties.type}`)
notify("Output type mismatch!", { type: "error" })
return;
}
this._queue.push(value);
}
}
LiteGraph.registerNodeType({
class: ComfyReceiveOutputNode,
title: "Comfy.ReceiveOutput",
desc: "Receives a workflow output sent from elsewhere",
type: "events/receive_output"
})

View File

@@ -0,0 +1,83 @@
import modalState, { type ModalData, type ModalState } from "$lib/stores/modalState";
import { getLitegraphType } from "$lib/utils";
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode";
import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte";
import notify from "$lib/notify";
import workflowState from "$lib/stores/workflowState";
import { get } from "svelte/store";
import type ComfyApp from "$lib/components/ComfyApp";
export interface ComfySendOutputActionProperties extends ComfyGraphNodeProperties {
}
export default class ComfySendOutputAction extends ComfyGraphNode {
override properties: ComfySendOutputActionProperties = {
tags: [],
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "value", type: "*" },
{ name: "trigger", type: BuiltInSlotType.ACTION }
],
}
isActive: boolean = false;
override onAction(action: any, param: any) {
const value = this.getInputData(0);
if (value == null) {
notify("No workflow data to send!", { type: "error" })
return;
}
if (this.isActive)
return;
let type = getLitegraphType(value);
const receiveTargets = workflowState.findReceiveOutputTargets(type);
this.isActive = true;
const doSend = (modal: ModalData) => {
this.isActive = false;
const { workflow, targetNode } = get(modal.state) as SendOutputModalResult;
console.warn("send", workflow, targetNode);
if (workflow == null || targetNode == null)
return
const app = (window as any).app as ComfyApp;
if (app == null) {
console.error("Couldn't get app!")
return
}
targetNode.receiveOutput(value);
workflowState.setActiveWorkflow(app.lCanvas, workflow.id)
}
modalState.pushModal({
title: "Send Output",
closeOnClick: true,
showCloseButton: true,
svelteComponent: SendOutputModal,
svelteProps: {
value,
type,
receiveTargets
},
onClose: doSend
})
};
}
LiteGraph.registerNodeType({
class: ComfySendOutputAction,
title: "Comfy.SendOutputAction",
desc: "Sends a workflow output elsewhere",
type: "actions/send_output"
})

View File

@@ -8,3 +8,5 @@ export { default as ComfySetNodeModeAdvancedAction } from "./ComfySetNodeModeAdv
export { default as ComfySetPromptThumbnailsAction } from "./ComfySetPromptThumbnailsAction" export { default as ComfySetPromptThumbnailsAction } from "./ComfySetPromptThumbnailsAction"
export { default as ComfyStoreImagesAction } from "./ComfyStoreImagesAction" export { default as ComfyStoreImagesAction } from "./ComfyStoreImagesAction"
export { default as ComfySwapAction } from "./ComfySwapAction" export { default as ComfySwapAction } from "./ComfySwapAction"
export { default as ComfySendOutputAction } from "./ComfySendOutputAction"
export { default as ComfyReceiveOutputNode } from "./ComfyReceiveOutputNode"

View File

@@ -1,3 +1,5 @@
import "$lib/nodes/ComfyGraphNode";
export { default as ComfyReroute } from "./ComfyReroute" export { default as ComfyReroute } from "./ComfyReroute"
export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode" export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode"
export { default as ComfyValueControl } from "./ComfyValueControl" export { default as ComfyValueControl } from "./ComfyValueControl"

View File

@@ -1,4 +1,5 @@
export { default as ComfyWidgetNode } from "./ComfyWidgetNode" export { default as ComfyWidgetNode } from "./ComfyWidgetNode"
export { default as ComfyButtonNode } from "./ComfyButtonNode" export { default as ComfyButtonNode } from "./ComfyButtonNode"
export { default as ComfyCheckboxNode } from "./ComfyCheckboxNode" export { default as ComfyCheckboxNode } from "./ComfyCheckboxNode"
export { default as ComfyComboNode } from "./ComfyComboNode" export { default as ComfyComboNode } from "./ComfyComboNode"

View File

@@ -1,24 +1,28 @@
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import { writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
export type ModalState = Record<string, any>;
export type ModalButton = { export type ModalButton = {
name: string, name: string,
variant: "primary" | "secondary", variant: "primary" | "secondary",
onClick: () => void, onClick: (state: ModalData) => void,
closeOnClick?: boolean closeOnClick?: boolean
} }
export interface ModalData { export interface ModalData {
id: string, id: string,
title?: string, title?: string,
onClose?: () => void, onClose?: (modal: ModalState) => void,
svelteComponent?: typeof SvelteComponentDev, svelteComponent?: typeof SvelteComponentDev,
svelteProps: Record<string, any>, svelteProps: Record<string, any>,
buttons: ModalButton[], buttons: ModalButton[],
showCloseButton: boolean, showCloseButton: boolean,
closeOnClick: boolean closeOnClick: boolean,
state: Writable<ModalState>,
close: () => void,
} }
export interface ModalState { export interface ModalStates {
activeModals: ModalData[] activeModals: ModalData[]
} }
@@ -28,20 +32,23 @@ export interface ModalStateOps {
closeAllModals: () => void, closeAllModals: () => void,
} }
export type WritableModalStateStore = Writable<ModalState> & ModalStateOps; export type WritableModalStateStore = Writable<ModalStates> & ModalStateOps;
const store: Writable<ModalState> = writable( const store: Writable<ModalStates> = writable(
{ {
activeModals: [] activeModals: []
}) })
function pushModal(data: Partial<ModalData>) { function pushModal(data: Partial<ModalData>) {
const id = uuidv4()
const modal: ModalData = { const modal: ModalData = {
showCloseButton: true, showCloseButton: true,
closeOnClick: true, closeOnClick: true,
buttons: [], buttons: [],
svelteProps: {}, svelteProps: {},
state: writable({}),
...data, ...data,
id: uuidv4(), id,
close: () => closeModal(id)
} }
store.update(s => { store.update(s => {
@@ -51,7 +58,13 @@ function pushModal(data: Partial<ModalData>) {
} }
function closeModal(id: string) { function closeModal(id: string) {
const modal = get(store).activeModals.find(m => m.id === id);
if (modal == null)
return;
store.update(s => { store.update(s => {
if (modal.onClose)
modal.onClose(modal)
s.activeModals = s.activeModals.filter(m => m.id !== id) s.activeModals = s.activeModals.filter(m => m.id !== id)
return s; return s;
}) })

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, LiteGraph } from '@litegraph-ts/core'; import { clamp, LGraphNode, type LGraphCanvas, type NodeID, type SerializedLGraph, type UUID, LGraph, LiteGraph, type SlotType, NodeMode } 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';
@@ -9,6 +9,7 @@ import { v4 as uuidv4 } from "uuid";
import type ComfyGraphCanvas from '$lib/ComfyGraphCanvas'; import type ComfyGraphCanvas from '$lib/ComfyGraphCanvas';
import { blankGraph } from '$lib/defaultGraph'; import { blankGraph } from '$lib/defaultGraph';
import type { SerializedAppState } from '$lib/components/ComfyApp'; import type { SerializedAppState } from '$lib/components/ComfyApp';
import type ComfyReceiveOutputNode from '$lib/nodes/actions/ComfyReceiveOutputNode';
type ActiveCanvas = { type ActiveCanvas = {
canvas: LGraphCanvas | null; canvas: LGraphCanvas | null;
@@ -232,6 +233,11 @@ export type WorkflowState = {
activeWorkflow: ComfyWorkflow | null, activeWorkflow: ComfyWorkflow | null,
} }
export type WorkflowReceiveOutputTargets = {
workflow: ComfyWorkflow,
targetNodes: ComfyReceiveOutputNode[]
}
type WorkflowStateOps = { type WorkflowStateOps = {
getWorkflow: (id: WorkflowInstID) => ComfyWorkflow | null getWorkflow: (id: WorkflowInstID) => ComfyWorkflow | null
getWorkflowByGraph: (graph: LGraph) => ComfyWorkflow | null getWorkflowByGraph: (graph: LGraph) => ComfyWorkflow | null
@@ -243,7 +249,8 @@ type WorkflowStateOps = {
addWorkflow: (canvas: ComfyGraphCanvas, data: ComfyWorkflow) => void, addWorkflow: (canvas: ComfyGraphCanvas, data: ComfyWorkflow) => void,
closeWorkflow: (canvas: ComfyGraphCanvas, index: number) => void, closeWorkflow: (canvas: ComfyGraphCanvas, index: number) => void,
closeAllWorkflows: (canvas: ComfyGraphCanvas) => void, closeAllWorkflows: (canvas: ComfyGraphCanvas) => void,
setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number) => ComfyWorkflow | null setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number | WorkflowInstID) => ComfyWorkflow | null,
findReceiveOutputTargets: (type: SlotType | SlotType[]) => WorkflowReceiveOutputTargets[]
} }
export type WritableWorkflowStateStore = Writable<WorkflowState> & WorkflowStateOps; export type WritableWorkflowStateStore = Writable<WorkflowState> & WorkflowStateOps;
@@ -345,7 +352,7 @@ function closeAllWorkflows(canvas: ComfyGraphCanvas) {
closeWorkflow(canvas, 0) closeWorkflow(canvas, 0)
} }
function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkflow | null { function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number | WorkflowInstID): ComfyWorkflow | null {
const state = get(store); const state = get(store);
if (state.openedWorkflows.length === 0) { if (state.openedWorkflows.length === 0) {
@@ -354,6 +361,10 @@ function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkfl
return null; return null;
} }
if (typeof index === "string") {
index = state.openedWorkflows.findIndex(w => w.id === index)
}
if (index < 0 || index >= state.openedWorkflows.length) if (index < 0 || index >= state.openedWorkflows.length)
return state.activeWorkflow; return state.activeWorkflow;
@@ -375,6 +386,31 @@ function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number): ComfyWorkfl
return workflow; return workflow;
} }
function findReceiveOutputTargets(type: SlotType | SlotType[]): WorkflowReceiveOutputTargets[] {
let result = []
const state = get(store);
if (!Array.isArray(type))
type = [type]
const types = new Set(type);
for (const workflow of state.openedWorkflows) {
const targetNodes = workflow.graph
// can't use class here because of circular import
.findNodesByTypeRecursive("events/receive_output")
.filter(n => {
return types.has(n.properties.type) && n.mode === NodeMode.ALWAYS
})
if (targetNodes.length > 0)
result.push({ workflow, targetNodes });
}
return result;
}
const workflowStateStore: WritableWorkflowStateStore = const workflowStateStore: WritableWorkflowStateStore =
{ {
...store, ...store,
@@ -389,5 +425,6 @@ const workflowStateStore: WritableWorkflowStateStore =
closeWorkflow, closeWorkflow,
closeAllWorkflows, closeAllWorkflows,
setActiveWorkflow, setActiveWorkflow,
findReceiveOutputTargets
} }
export default workflowStateStore; export default workflowStateStore;

View File

@@ -1,7 +1,7 @@
import { type WidgetLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates"; import { type WidgetLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import selectionState from "$lib/stores/selectionState"; import selectionState from "$lib/stores/selectionState";
import type { FileData as GradioFileData } from "@gradio/upload"; import type { FileData as GradioFileData } from "@gradio/upload";
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID } from "@litegraph-ts/core"; import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID, type SlotType } from "@litegraph-ts/core";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { ComfyNodeID } from "./api"; import type { ComfyNodeID } from "./api";
import { type SerializedPrompt } from "./components/ComfyApp"; import { type SerializedPrompt } from "./components/ComfyApp";
@@ -507,6 +507,21 @@ export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string {
return convertComfyOutputToComfyURL(image.comfyUIFile) return convertComfyOutputToComfyURL(image.comfyUIFile)
} }
export function comfyURLToComfyFile(urlString: string): ComfyImageLocation | null {
const url = new URL(urlString);
const params = new URLSearchParams(url.search);
const filename = params.get("filename")
const type = params.get("type") as ComfyUploadImageType;
const subfolder = params.get("subfolder") || ""
// If at least filename and type exist then we're good
if (filename != null && type != null) {
return { filename, type, subfolder }
}
return null;
}
export function showLightbox(images: string[], index: number, e: Event) { export function showLightbox(images: string[], index: number, e: Event) {
e.preventDefault() e.preventDefault()
if (!images) if (!images)
@@ -516,3 +531,31 @@ export function showLightbox(images: string[], index: number, e: Event) {
e.stopPropagation() e.stopPropagation()
} }
export function getLitegraphType(param: any): SlotType | null {
if (param == null)
return null;
switch (typeof param) {
case "string":
return "string"
case "number":
case "bigint":
return "number"
case "boolean":
return "boolean"
case "object":
if (isComfyBoxImageMetadata(param)) {
return "COMFYBOX_IMAGE"
}
else if (isComfyBoxImageMetadataArray(param)) {
return "COMFYBOX_IMAGES"
}
return null;
case "symbol":
case "undefined":
case "function":
default:
return null;
}
}

View File

@@ -14,6 +14,7 @@
import notify from "$lib/notify"; import notify from "$lib/notify";
import NumberInput from "$lib/components/NumberInput.svelte"; import NumberInput from "$lib/components/NumberInput.svelte";
import type { ComfyImageEditorNode } from "$lib/nodes/widgets"; import type { ComfyImageEditorNode } from "$lib/nodes/widgets";
import { ImageViewer } from "$lib/ImageViewer";
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
@@ -176,6 +177,17 @@
}, 1000); }, 1000);
} }
function openLightbox() {
if (!$nodeValue || $nodeValue.length === 0)
return;
const comfyImage = $nodeValue[0];
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
const images = [comfyURL]
ImageViewer.instance.showModal(images, 0);
}
let status = "empty"; let status = "empty";
let uploadError = null; let uploadError = null;
@@ -233,7 +245,7 @@
on:upload_error={onUploadError} on:upload_error={onUploadError}
on:clear={onClear} on:clear={onClear}
on:change={onChange} on:change={onChange}
on:image_clicked={openImageEditor} on:image_clicked={openLightbox}
/> />
{:else} {:else}
<div class="comfy-image-editor-panel"> <div class="comfy-image-editor-panel">
@@ -249,7 +261,7 @@
on:upload_error={onUploadError} on:upload_error={onUploadError}
on:clear={onClear} on:clear={onClear}
on:change={onChange} on:change={onChange}
on:image_clicked={openImageEditor} on:image_clicked={openLightbox}
/> />
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog> <Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
<div> <div>

View File

@@ -1,3 +1,6 @@
// Run node registration before anthing else, in the proper order
import "$lib/nodeImports";
import ComfyApp from '$lib/components/ComfyApp'; import ComfyApp from '$lib/components/ComfyApp';
import { configureLitegraph } from '$lib/init'; import { configureLitegraph } from '$lib/init';
import App from './App.svelte'; import App from './App.svelte';

View File

@@ -1,3 +1,6 @@
// Run node registration before anthing else, in the proper order
import "$lib/nodeImports";
import AppMobile from './AppMobile.svelte'; import AppMobile from './AppMobile.svelte';
import Framework7 from 'framework7/lite-bundle'; import Framework7 from 'framework7/lite-bundle';
import Framework7Svelte from 'framework7-svelte'; import Framework7Svelte from 'framework7-svelte';