Workflows can receive images from other workflows/historical prompts
This commit is contained in:
Submodule litegraph updated: 42adb8dba1...3ce3a47871
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
79
src/lib/components/modal/ReceiveOutputTargets.svelte
Normal file
79
src/lib/components/modal/ReceiveOutputTargets.svelte
Normal 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>
|
||||||
79
src/lib/components/modal/SendOutputModal.svelte
Normal file
79
src/lib/components/modal/SendOutputModal.svelte
Normal 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
9
src/lib/nodeImports.ts
Normal 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";
|
||||||
@@ -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);
|
||||||
|
|||||||
91
src/lib/nodes/actions/ComfyReceiveOutputNode.ts
Normal file
91
src/lib/nodes/actions/ComfyReceiveOutputNode.ts
Normal 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"
|
||||||
|
})
|
||||||
83
src/lib/nodes/actions/ComfySendOutputAction.ts
Normal file
83
src/lib/nodes/actions/ComfySendOutputAction.ts
Normal 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"
|
||||||
|
})
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user