Workflow properties

This commit is contained in:
space-nuko
2023-05-05 16:46:28 -05:00
parent 578e38e58b
commit 7ddda80cf6
14 changed files with 489 additions and 179 deletions

View File

@@ -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);

View File

@@ -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>

View File

@@ -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) {

View 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>

View 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>

View File

@@ -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;

View File

@@ -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")
}
}
}

View File

@@ -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++) {

View File

@@ -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({

View File

@@ -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)

View File

@@ -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 =

View File

@@ -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
}

View File

@@ -1,2 +1 @@
@import "gradio";
@import "ux";

View File

@@ -202,6 +202,10 @@ body {
object-fit: contain !important;
}
.icon {
color: var(--ae-input-color);
}
.preview {
background: var(--ae-main-bg-color);