Workflow properties
This commit is contained in:
@@ -3,6 +3,7 @@ import type ComfyApp from "./components/ComfyApp";
|
||||
import queueState from "./stores/queueState";
|
||||
import { get } from "svelte/store";
|
||||
import uiState from "./stores/uiState";
|
||||
import layoutState from "./stores/layoutState";
|
||||
|
||||
export type SerializedGraphCanvasState = {
|
||||
offset: Vector2,
|
||||
@@ -230,6 +231,13 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
return res;
|
||||
}
|
||||
|
||||
override onNodeSelected(node: LGraphNode) {
|
||||
const ls = get(layoutState)
|
||||
ls.currentSelectionNodes = [node]
|
||||
ls.currentSelection = []
|
||||
layoutState.set(ls)
|
||||
}
|
||||
|
||||
override onNodeMoved(node: LGraphNode) {
|
||||
if (super.onNodeMoved)
|
||||
super.onNodeMoved(node);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { BlockTitle } from "@gradio/atoms";
|
||||
import ComfyUIPane from "./ComfyUIPane.svelte";
|
||||
import ComfyApp, { type SerializedAppState } from "./ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import { Checkbox, TextBox } from "@gradio/form"
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
@@ -29,6 +29,7 @@
|
||||
let containerElem: HTMLDivElement;
|
||||
let resizeTimeout: NodeJS.Timeout | null;
|
||||
let hasShownUIHelpToast: boolean = false;
|
||||
let uiTheme: string = "";
|
||||
|
||||
let debugLayout: boolean = false;
|
||||
|
||||
@@ -46,7 +47,10 @@
|
||||
|
||||
function queuePrompt() {
|
||||
console.log("Queuing!");
|
||||
app.queuePrompt(0, 1);
|
||||
let subworkflow = $uiState.subWorkflow;
|
||||
if (subworkflow === "")
|
||||
subworkflow = null
|
||||
app.queuePrompt(0, 1, subworkflow);
|
||||
}
|
||||
|
||||
$: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked;
|
||||
@@ -164,6 +168,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if uiTheme === "anapnoe"}
|
||||
<link rel="stylesheet" href="/src/scss/ux.scss">
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div id="main">
|
||||
<div id="dropzone" class="dropzone"></div>
|
||||
<div id="container" bind:this={containerElem}>
|
||||
@@ -220,16 +230,24 @@
|
||||
<Button variant="secondary" on:click={doRefreshCombos}>
|
||||
🔄
|
||||
</Button>
|
||||
<Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/>
|
||||
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
|
||||
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
||||
<TextBox bind:value={$uiState.subWorkflow} label="Subworkflow" show_label={true} lines={1} max_lines={1}/>
|
||||
<label class="label" for="enable-ui-editing">
|
||||
<BlockTitle>Enable UI Editing</BlockTitle>
|
||||
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="widgets">Widgets</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label" for="ui-theme">
|
||||
<BlockTitle>Theme</BlockTitle>
|
||||
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
|
||||
<option value="">None</option>
|
||||
<option value="anapnoe">Anapnoe</option>
|
||||
</select>
|
||||
</label>
|
||||
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="widgets">Widgets</option>
|
||||
</select>
|
||||
</div>
|
||||
<LightboxModal />
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ import ComfyGraph from "$lib/ComfyGraph";
|
||||
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||
import { get } from "svelte/store";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { promptToGraphVis, toGraphVis } from "$lib/utils";
|
||||
|
||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||
|
||||
@@ -54,9 +55,11 @@ export type SerializedPromptInputs = {
|
||||
class_type: string
|
||||
}
|
||||
|
||||
export type SerializedPromptOutput = Record<string, SerializedPromptInputs>
|
||||
|
||||
export type SerializedPrompt = {
|
||||
workflow: SerializedLGraph,
|
||||
output: Record<string, SerializedPromptInputs>
|
||||
output: SerializedPromptOutput
|
||||
}
|
||||
|
||||
export type Progress = {
|
||||
@@ -420,7 +423,7 @@ export default class ComfyApp {
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt(): Promise<SerializedPrompt> {
|
||||
async graphToPrompt(tag: string | null = null): Promise<SerializedPrompt> {
|
||||
// Run frontend-only logic
|
||||
this.lGraph.runStep(1)
|
||||
|
||||
@@ -438,6 +441,11 @@ export default class ComfyApp {
|
||||
|
||||
const node = node_ as ComfyBackendNode;
|
||||
|
||||
if (tag && node.tags.indexOf(tag) === -1) {
|
||||
console.debug("Skipping tagged node", tag, node.tags)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.mode === NodeMode.NEVER) {
|
||||
// Don't serialize muted nodes
|
||||
continue;
|
||||
@@ -451,6 +459,11 @@ export default class ComfyApp {
|
||||
const inp = node.inputs[i];
|
||||
const inputLink = node.getInputLink(i)
|
||||
const inputNode = node.getInputNode(i)
|
||||
|
||||
if (inputNode && tag && "tags" in inputNode && (inputNode.tags as string[]).indexOf(tag) === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inputLink || !inputNode) {
|
||||
if ("config" in inp) {
|
||||
const defaultValue = (inp as IComfyInputSlot).config?.defaultValue
|
||||
@@ -489,17 +502,36 @@ export default class ComfyApp {
|
||||
if (parent) {
|
||||
const seen = {}
|
||||
let link = node.getInputLink(i);
|
||||
while (parent && !parent.isBackendNode) {
|
||||
|
||||
const isValidParent = (parent: ComfyGraphNode) => {
|
||||
if (!parent || parent.isBackendNode)
|
||||
return false;
|
||||
if ("tags" in parent && (parent.tags as string[]).indexOf(tag) === -1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
while (isValidParent(parent)) {
|
||||
link = parent.getInputLink(link.origin_slot);
|
||||
if (link && !seen[link.id]) {
|
||||
seen[link.id] = true
|
||||
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
|
||||
const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
|
||||
if (inputNode && "tags" in inputNode && tag && (inputNode.tags as string[]).indexOf(tag) === -1) {
|
||||
console.debug("Skipping tagged parent node", tag, node.tags)
|
||||
parent = null;
|
||||
}
|
||||
else {
|
||||
parent = inputNode;
|
||||
}
|
||||
} else {
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (link && parent && parent.isBackendNode) {
|
||||
if ("tags" in parent && tag && (parent.tags as string[]).indexOf(tag) === -1)
|
||||
continue;
|
||||
|
||||
const input = node.inputs[i]
|
||||
// TODO can null be a legitimate value in some cases?
|
||||
// Nodes like CLIPLoader will never have a value in the frontend, hence "null".
|
||||
@@ -522,15 +554,19 @@ export default class ComfyApp {
|
||||
if (Array.isArray(output[o].inputs[i])
|
||||
&& output[o].inputs[i].length === 2
|
||||
&& !output[output[o].inputs[i][0]]) {
|
||||
console.debug("Prune removed node link", o, i, output[o].inputs[i])
|
||||
delete output[o].inputs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn({ workflow, output })
|
||||
console.warn(promptToGraphVis({ workflow, output }))
|
||||
|
||||
return { workflow, output };
|
||||
}
|
||||
|
||||
async queuePrompt(num: number, batchCount: number = 1) {
|
||||
async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
|
||||
this.queueItems.push({ num, batchCount });
|
||||
|
||||
// Only have one action process the items so each one gets a unique seed correctly
|
||||
@@ -545,7 +581,7 @@ export default class ComfyApp {
|
||||
console.log(`Queue get! ${num} ${batchCount}`);
|
||||
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
const p = await this.graphToPrompt();
|
||||
const p = await this.graphToPrompt(tag);
|
||||
|
||||
try {
|
||||
await this.api.queuePrompt(num, p);
|
||||
@@ -626,12 +662,8 @@ export default class ComfyApp {
|
||||
if ("config" in input) {
|
||||
const comfyInput = input as IComfyInputSlot;
|
||||
|
||||
console.warn("RefreshCombo", comfyInput.defaultWidgetNode, comfyInput)
|
||||
|
||||
if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) {
|
||||
comfyInput.config.values = def["input"]["required"][comfyInput.name][0];
|
||||
|
||||
console.warn("RefreshCombo", comfyInput.config.values, def["input"]["required"][comfyInput.name])
|
||||
const inputNode = node.getInputNode(index)
|
||||
|
||||
if (inputNode && "doAutoConfig" in inputNode) {
|
||||
|
||||
54
src/lib/components/ComfyComboProperty.svelte
Normal file
54
src/lib/components/ComfyComboProperty.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { BlockTitle } from "@gradio/atoms";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let value: string = "";
|
||||
export let values: string[] = [""];
|
||||
export let name: string = "";
|
||||
let value_: string = ""
|
||||
|
||||
$: handleChange(value);
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: string;
|
||||
submit: undefined;
|
||||
blur: undefined;
|
||||
}>();
|
||||
|
||||
function handleChange(val: string) {
|
||||
if (val != value_)
|
||||
dispatch("change", val);
|
||||
value_ = val
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="select-wrapper">
|
||||
<BlockTitle>{name}</BlockTitle>
|
||||
<div class="select">
|
||||
<select on:blur bind:value>
|
||||
{#each values as value}
|
||||
<option {value}>
|
||||
{value}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style lang="scss">
|
||||
.select-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
|
||||
select {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select-title {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
</style>
|
||||
45
src/lib/components/ComfyNumberProperty.svelte
Normal file
45
src/lib/components/ComfyNumberProperty.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { BlockTitle } from "@gradio/atoms";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let value: number = 0;
|
||||
export let step: number = 1;
|
||||
export let name: string = "";
|
||||
let value_: number = 0;
|
||||
|
||||
$: value;
|
||||
$: handleChange(value);
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: number;
|
||||
submit: undefined;
|
||||
blur: undefined;
|
||||
}>();
|
||||
|
||||
function handleChange(val: number) {
|
||||
if (val != value_)
|
||||
dispatch("change", val);
|
||||
value_ = val
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="number-wrapper">
|
||||
<BlockTitle>{name}</BlockTitle>
|
||||
<div class="number">
|
||||
<input type="number" bind:value {step}>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<style lang="scss">
|
||||
.number-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.number {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,10 +2,11 @@
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { TextBox, Checkbox } from "@gradio/form";
|
||||
import { LGraphNode } from "@litegraph-ts/core"
|
||||
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES } from "$lib/stores/layoutState"
|
||||
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
||||
import { get } from "svelte/store"
|
||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||
|
||||
let target: IDragItem | null = null;
|
||||
let node: LGraphNode | null = null;
|
||||
@@ -20,6 +21,10 @@
|
||||
node = null;
|
||||
}
|
||||
}
|
||||
else if ($layoutState.currentSelectionNodes.length > 0) {
|
||||
target = null;
|
||||
node = $layoutState.currentSelectionNodes[0]
|
||||
}
|
||||
else {
|
||||
target = null
|
||||
node = null;
|
||||
@@ -29,14 +34,14 @@
|
||||
|
||||
$: {
|
||||
if (node != null)
|
||||
targetType = node.type || "Widget"
|
||||
targetType = node.type || "Node"
|
||||
else if (target)
|
||||
targetType = "group"
|
||||
targetType = "Group"
|
||||
else
|
||||
targetType = "???"
|
||||
targetType = ""
|
||||
}
|
||||
|
||||
function updateAttribute(entry: any, value: any) {
|
||||
function updateAttribute(entry: AttributesSpec, value: any) {
|
||||
if (target) {
|
||||
const name = entry.name
|
||||
console.warn("updateAttribute", name, value)
|
||||
@@ -51,7 +56,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateProperty(entry: any, value: any) {
|
||||
function updateProperty(entry: AttributesSpec, value: any) {
|
||||
if (node) {
|
||||
const name = entry.name
|
||||
console.warn("updateProperty", name, value)
|
||||
@@ -64,125 +69,195 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getVar(node: LGraphNode, entry: AttributesSpec) {
|
||||
let value = node[entry.name]
|
||||
if (entry.serialize)
|
||||
value = entry.serialize(value)
|
||||
console.debug("[ComfyProperties] getVar", entry, value, node)
|
||||
return value
|
||||
}
|
||||
|
||||
function updateVar(entry: any, value: any) {
|
||||
if (node) {
|
||||
const name = entry.name
|
||||
console.warn("updateProperty", name, value)
|
||||
|
||||
if (entry.deserialize)
|
||||
value = entry.deserialize(value)
|
||||
|
||||
console.debug("[ComfyProperties] updateVar", entry, value, name, node)
|
||||
node[name] = value;
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateWorkflowAttribute(entry: AttributesSpec, value: any) {
|
||||
const name = entry.name
|
||||
console.warn("updateWorkflowAttribute", name, value)
|
||||
|
||||
$layoutState.attrs[name] = value
|
||||
$layoutState = $layoutState
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="props">
|
||||
{#if target}
|
||||
<div class="top">
|
||||
<div class="target-name">
|
||||
<span>
|
||||
<span class="title">{target.attrs.title}</span>
|
||||
<div class="top">
|
||||
<div class="target-name">
|
||||
<span>
|
||||
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
|
||||
{#if targetType !== ""}
|
||||
<span class="type">({targetType})</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="props-entries">
|
||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||
<div class="category-name">
|
||||
<span>
|
||||
<span class="title">{category.categoryName}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="props-entries">
|
||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||
<div class="category-name">
|
||||
<span>
|
||||
<span class="title">{category.categoryName}</span>
|
||||
</span>
|
||||
</div>
|
||||
{#each category.specs as spec(spec.name)}
|
||||
{#if spec.location === "widget" && spec.name in target.attrs}
|
||||
{#each category.specs as spec(spec.name)}
|
||||
{#if spec.location === "widget" && target && spec.name in target.attrs}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, e.detail)}
|
||||
on:input={(e) => updateAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={target.attrs[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateAttribute(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={target.attrs[spec.name]}
|
||||
values={spec.values}
|
||||
on:changed={(e) => updateAttribute(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if node}
|
||||
{#if spec.location === "nodeProps" && spec.name in node.properties}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, e.detail)}
|
||||
on:input={(e) => updateAttribute(spec, e.detail)}
|
||||
value={node.properties[spec.name]}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
on:input={(e) => updateProperty(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, e.detail)}
|
||||
value={node.properties[spec.name]}
|
||||
label={spec.name}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={node.properties[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={node.properties[spec.name]}
|
||||
values={spec.values}
|
||||
on:changed={(e) => updateProperty(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if spec.location === "nodeVars" && spec.name in node}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
on:input={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={getVar(node, spec)}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<label class="number-wrapper">
|
||||
<BlockTitle>{spec.name}</BlockTitle>
|
||||
<div class="number">
|
||||
<input
|
||||
type="number"
|
||||
value={target.attrs[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateAttribute(spec, e.currentTarget.valueAsNumber)}
|
||||
on:input={(e) => updateAttribute(spec, e.currentTarget.valueAsNumber)}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={getVar(node, spec)}
|
||||
step={1}
|
||||
on:change={(e) => updateVar(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<label class="select-wrapper">
|
||||
<BlockTitle>{spec.name}</BlockTitle>
|
||||
<div class="select">
|
||||
<select
|
||||
value={target.attrs[spec.name]}
|
||||
on:change={(e) => updateAttribute(spec, e.currentTarget.options[e.currentTarget.selectedIndex].value)}>
|
||||
{#each spec.values as value}
|
||||
<option value={value}>
|
||||
{value}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={getVar(node, spec)}
|
||||
values={spec.values}
|
||||
on:changed={(e) => updateVar(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if node}
|
||||
{#if spec.location === "nodeProps" && spec.name in node.properties}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={node.properties[spec.name]}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
on:input={(e) => updateProperty(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={node.properties[spec.name]}
|
||||
on:change={(e) => updateProperty(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<label class="number-wrapper">
|
||||
<BlockTitle>{spec.name}</BlockTitle>
|
||||
<div class="number">
|
||||
<input
|
||||
type="number"
|
||||
value={node.properties[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateProperty(spec, e.currentTarget.valueAsNumber)}
|
||||
on:input={(e) => updateProperty(spec, e.currentTarget.valueAsNumber)}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
{:else if spec.type === "enum"}
|
||||
<label class="select-wrapper">
|
||||
<BlockTitle>{spec.name}</BlockTitle>
|
||||
<div class="select">
|
||||
<select
|
||||
value={node.properties[spec.name]}
|
||||
on:change={(e) => updateProperty(spec, e.currentTarget.options[e.currentTarget.selectedIndex].value)}>
|
||||
{#each spec.values as value}
|
||||
<option value={value}>
|
||||
{value}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if spec.location === "workflow" && spec.name in $layoutState.attrs}
|
||||
<div class="props-entry">
|
||||
{#if spec.type === "string"}
|
||||
<TextBox
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
max_lines={1}
|
||||
/>
|
||||
{:else if spec.type === "boolean"}
|
||||
<Checkbox
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
label={spec.name}
|
||||
/>
|
||||
{:else if spec.type === "number"}
|
||||
<ComfyNumberProperty
|
||||
name={spec.name}
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
step={1}
|
||||
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
/>
|
||||
{:else if spec.type === "enum"}
|
||||
<ComfyComboProperty
|
||||
name={spec.name}
|
||||
value={$layoutState.attrs[spec.name]}
|
||||
values={spec.values}
|
||||
on:changed={(e) => updateWorkflowAttribute(spec, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -218,34 +293,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.number-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.number {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
|
||||
select {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select-title {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
/* width: 100%;
|
||||
height: auto;
|
||||
|
||||
@@ -3,6 +3,7 @@ import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||
|
||||
export interface ComfyAfterQueuedEventProperties extends Record<any, any> {
|
||||
prompt: SerializedPrompt
|
||||
@@ -49,10 +50,12 @@ LiteGraph.registerNodeType({
|
||||
})
|
||||
|
||||
export interface ComfyOnExecutedEventProperties extends Record<any, any> {
|
||||
images: GalleryOutput | null
|
||||
}
|
||||
|
||||
export class ComfyOnExecutedEvent extends ComfyGraphNode {
|
||||
override properties: ComfyOnExecutedEventProperties = {
|
||||
images: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
@@ -60,17 +63,20 @@ export class ComfyOnExecutedEvent extends ComfyGraphNode {
|
||||
{ name: "images", type: "IMAGE" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "images", type: "IMAGE" },
|
||||
{ name: "onExecuted", type: BuiltInSlotType.EVENT },
|
||||
],
|
||||
}
|
||||
|
||||
private _output: any = null;
|
||||
override onExecute() {
|
||||
if (this.properties.images !== null)
|
||||
this.setOutputData(0, this.properties.images)
|
||||
}
|
||||
|
||||
override receiveOutput(output: any) {
|
||||
if (this._output !== output) {
|
||||
console.error(output)
|
||||
this.triggerSlot(0, "bang")
|
||||
this._output = output
|
||||
if (output && "images" in output) {
|
||||
this.setProperty("images", output as GalleryOutput)
|
||||
this.triggerSlot(1, "bang")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import ComfyWidgets from "$lib/widgets"
|
||||
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
|
||||
import type { SerializedLGraphNode } from "@litegraph-ts/core";
|
||||
|
||||
/*
|
||||
* Base class for any node with configuration sent by the backend.
|
||||
@@ -29,6 +30,12 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Tags this node belongs to
|
||||
* Allows you to run subsections of the graph
|
||||
*/
|
||||
tags: string[] = []
|
||||
|
||||
private setup(nodeData: any) {
|
||||
var inputs = nodeData["input"]["required"];
|
||||
if (nodeData["input"]["optional"] != undefined) {
|
||||
@@ -74,6 +81,16 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
||||
// app.#invokeExtensionsAsync("nodeCreated", this);
|
||||
}
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
super.onSerialize(o);
|
||||
(o as any).tags = this.tags
|
||||
}
|
||||
|
||||
override onConfigure(o: SerializedLGraphNode) {
|
||||
super.onConfigure(o);
|
||||
this.tags = (o as any).tags || []
|
||||
}
|
||||
|
||||
override onExecuted(outputData: any) {
|
||||
console.warn("onExecuted outputs", outputData)
|
||||
for (let index = 0; index < this.outputs.length; index++) {
|
||||
|
||||
@@ -407,7 +407,8 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "images", type: "IMAGE" }
|
||||
{ name: "images", type: "IMAGE" },
|
||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -425,6 +426,19 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
}
|
||||
}
|
||||
|
||||
override onAction() {
|
||||
const link = this.getInputLink(0)
|
||||
if (link.data && "images" in link.data) {
|
||||
const data = link.data as GalleryOutput
|
||||
console.debug("[ComfyGalleryNode] Received output!", data)
|
||||
|
||||
const galleryItems: GradioFileData[] = this.convertItems(link.data)
|
||||
|
||||
const currentValue = get(this.value)
|
||||
this.setValue(currentValue.concat(galleryItems))
|
||||
}
|
||||
}
|
||||
|
||||
override formatValue(value: GradioFileData[] | null): string {
|
||||
return `Images: ${value?.length || 0}`
|
||||
}
|
||||
@@ -449,19 +463,6 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
super.setValue([])
|
||||
}
|
||||
}
|
||||
|
||||
receiveOutput() {
|
||||
const link = this.getInputLink(0)
|
||||
if (link.data && "images" in link.data) {
|
||||
const data = link.data as GalleryOutput
|
||||
console.debug("[ComfyGalleryNode] Received output!", data)
|
||||
|
||||
const galleryItems: GradioFileData[] = this.convertItems(link.data)
|
||||
|
||||
const currentValue = get(this.value)
|
||||
this.setValue(currentValue.concat(galleryItems))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
|
||||
@@ -11,14 +11,20 @@ type DragItemEntry = {
|
||||
parent: IDragItem | null
|
||||
}
|
||||
|
||||
export type LayoutAttributes = {
|
||||
defaultWorkflow: string
|
||||
}
|
||||
|
||||
export type LayoutState = {
|
||||
root: IDragItem | null,
|
||||
allItems: Record<DragItemID, DragItemEntry>,
|
||||
allItemsByNode: Record<number, DragItemEntry>,
|
||||
currentId: number,
|
||||
currentSelection: DragItemID[],
|
||||
currentSelectionNodes: LGraphNode[],
|
||||
isConfiguring: boolean,
|
||||
isMenuOpen: boolean
|
||||
isMenuOpen: boolean,
|
||||
attrs: LayoutAttributes
|
||||
}
|
||||
|
||||
export type Attributes = {
|
||||
@@ -34,11 +40,14 @@ export type Attributes = {
|
||||
export type AttributesSpec = {
|
||||
name: string,
|
||||
type: string,
|
||||
location: "widget" | "nodeProps"
|
||||
location: "widget" | "nodeProps" | "nodeVars" | "workflow"
|
||||
editable: boolean,
|
||||
|
||||
values?: string[],
|
||||
hidden?: boolean
|
||||
hidden?: boolean,
|
||||
|
||||
serialize?: (arg: any) => string,
|
||||
deserialize?: (arg: string) => any,
|
||||
}
|
||||
|
||||
export type AttributesCategorySpec = {
|
||||
@@ -95,6 +104,18 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
{
|
||||
categoryName: "behavior",
|
||||
specs: [
|
||||
{
|
||||
name: "tags",
|
||||
type: "string",
|
||||
location: "nodeVars",
|
||||
editable: true,
|
||||
serialize: (arg: string[]) => arg.join(","),
|
||||
deserialize: (arg: string) => {
|
||||
if (arg === "")
|
||||
return []
|
||||
return arg.split(",").map(s => s.trim())
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "min",
|
||||
type: "number",
|
||||
@@ -113,6 +134,12 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "defaultWorkflow",
|
||||
type: "string",
|
||||
location: "workflow",
|
||||
editable: true
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -161,8 +188,12 @@ const store: Writable<LayoutState> = writable({
|
||||
allItemsByNode: {},
|
||||
currentId: 0,
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: true
|
||||
isConfiguring: true,
|
||||
attrs: {
|
||||
defaultWorkflow: ""
|
||||
}
|
||||
})
|
||||
|
||||
function findDefaultContainerForInsertion(): ContainerLayout | null {
|
||||
@@ -425,6 +456,7 @@ function initDefaultLayout() {
|
||||
allItemsByNode: {},
|
||||
currentId: 0,
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false
|
||||
})
|
||||
@@ -444,6 +476,7 @@ export type SerializedLayoutState = {
|
||||
root: DragItemID | null,
|
||||
allItems: Record<DragItemID, SerializedDragEntry>,
|
||||
currentId: number,
|
||||
attrs: LayoutAttributes
|
||||
}
|
||||
|
||||
export type SerializedDragEntry = {
|
||||
@@ -481,6 +514,7 @@ function serialize(): SerializedLayoutState {
|
||||
root: state.root?.id,
|
||||
allItems,
|
||||
currentId: state.currentId,
|
||||
attrs: state.attrs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,8 +570,10 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
allItemsByNode,
|
||||
currentId: data.currentId,
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false
|
||||
isConfiguring: false,
|
||||
attrs: data.attrs
|
||||
}
|
||||
|
||||
console.debug("[layoutState] deserialize", data, state)
|
||||
|
||||
@@ -9,7 +9,8 @@ export type UIState = {
|
||||
nodesLocked: boolean,
|
||||
graphLocked: boolean,
|
||||
autoAddUI: boolean,
|
||||
uiEditMode: UIEditMode
|
||||
uiEditMode: UIEditMode,
|
||||
subWorkflow: string
|
||||
}
|
||||
|
||||
export type WritableUIStateStore = Writable<UIState>;
|
||||
@@ -19,7 +20,8 @@ const store: WritableUIStateStore = writable(
|
||||
graphLocked: false,
|
||||
nodesLocked: false,
|
||||
autoAddUI: true,
|
||||
uiEditMode: "disabled"
|
||||
uiEditMode: "disabled",
|
||||
subWorkflow: "default"
|
||||
})
|
||||
|
||||
const uiStateStore: WritableUIStateStore =
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import ComfyApp from "./components/ComfyApp";
|
||||
import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
|
||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||
import { get } from "svelte/store"
|
||||
import layoutState from "$lib/stores/layoutState"
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import type { SerializedLGraph } from "@litegraph-ts/core";
|
||||
|
||||
export function clamp(n: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(n, min), max)
|
||||
@@ -49,8 +50,48 @@ export function startDrag(evt: MouseEvent) {
|
||||
else {
|
||||
ls.currentSelection = [item.id]
|
||||
}
|
||||
ls.currentSelectionNodes = [];
|
||||
|
||||
layoutState.set(ls)
|
||||
};
|
||||
|
||||
export function stopDrag(evt: MouseEvent) {
|
||||
};
|
||||
|
||||
export function workflowToGraphVis(workflow: SerializedLGraph): string {
|
||||
let out = "digraph {\n"
|
||||
|
||||
for (const link of workflow.links) {
|
||||
const nodeA = workflow.nodes.find(n => n.id === link[1])
|
||||
const nodeB = workflow.nodes.find(n => n.id === link[3])
|
||||
out += `"${link[1]}_${nodeA.title}" -> "${link[3]}_${nodeB.title}"\n`;
|
||||
}
|
||||
|
||||
out += "}"
|
||||
return out
|
||||
}
|
||||
|
||||
export function promptToGraphVis(prompt: SerializedPrompt): string {
|
||||
let out = "digraph {\n"
|
||||
|
||||
for (const pair of Object.entries(prompt.output)) {
|
||||
const [id, o] = pair;
|
||||
const outNode = prompt.workflow.nodes.find(n => n.id == id)
|
||||
for (const pair2 of Object.entries(o.inputs)) {
|
||||
const [inpName, i] = pair2;
|
||||
|
||||
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
||||
// Link
|
||||
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
|
||||
out += `"${inpNode.title}" -> "${outNode.title}"\n`
|
||||
}
|
||||
else {
|
||||
// Value
|
||||
out += `"${id}-${inpName}-${typeof i}" -> "${outNode.title}"\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out += "}"
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
@import "gradio";
|
||||
@import "ux";
|
||||
|
||||
@@ -202,6 +202,10 @@ body {
|
||||
object-fit: contain !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--ae-input-color);
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: var(--ae-main-bg-color);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user