temp linear history mode

This commit is contained in:
space-nuko
2023-06-03 16:14:30 -05:00
parent b3f2f9093f
commit 5a98561fe9
15 changed files with 468 additions and 162 deletions

View File

@@ -612,6 +612,7 @@ export default class ComfyApp {
node.onExecuted(output); node.onExecuted(output);
} }
workflow.journey.onExecuted(promptID, nodeID, output, queueEntry); workflow.journey.onExecuted(promptID, nodeID, output, queueEntry);
workflow.journey.set(get(workflow.journey))
} }
} }
}); });
@@ -1032,11 +1033,14 @@ export default class ComfyApp {
let journeyNode: JourneyNode | null; let journeyNode: JourneyNode | null;
if (get(uiState).autoPushJourney) { if (get(uiState).saveHistory) {
const activeNode = targetWorkflow.journey.getActiveNode(); const activeNode = targetWorkflow.journey.getActiveNode();
if (activeNode != null) { journeyNode = targetWorkflow.journey.pushPatchOntoActive(targetWorkflow, activeNode);
journeyNode = targetWorkflow.journey.pushPatchOntoActive(targetWorkflow, activeNode);
} // if no patch was applied, use currently selected node for prompt image
// output purposes
if (journeyNode == null)
journeyNode = activeNode;
} }
this.processingQueue = true; this.processingQueue = true;

View File

@@ -3,39 +3,71 @@
tree-like graph. It lets you save incremental changes to your workflow and tree-like graph. It lets you save incremental changes to your workflow and
jump between past and present sets of parameters. jump between past and present sets of parameters.
--> -->
<script context="module" lang="ts">
export type JourneyMode = "linear" | "tree";
</script>
<script lang="ts"> <script lang="ts">
import type ComfyApp from './ComfyApp'; import type ComfyApp from './ComfyApp';
import type { ComfyBoxWorkflow } from '$lib/stores/workflowState'; import type { ComfyBoxWorkflow } from '$lib/stores/workflowState';
import workflowState from '$lib/stores/workflowState'; import workflowState from '$lib/stores/workflowState';
import uiState from '$lib/stores/uiState'; import uiState from '$lib/stores/uiState';
import { calculateWorkflowParamsPatch, resolvePatch, type JourneyPatchNode, type WritableJourneyStateStore, diffParams } from '$lib/stores/journeyStates'; import { calculateWorkflowParamsPatch, resolvePatch, type JourneyPatchNode, type WritableJourneyStateStore, diffParams, JourneyNode } from '$lib/stores/journeyStates';
import JourneyRenderer from './JourneyRenderer.svelte'; import JourneyRenderer, { type JourneyNodeEvent } from './JourneyRenderer.svelte';
import { Plus } from "svelte-bootstrap-icons"; import { Trash, ClockHistory, Diagram3 } from "svelte-bootstrap-icons";
import { getWorkflowRestoreParams, getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters'; import { getWorkflowRestoreParams, getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters';
import notify from '$lib/notify'; import notify from '$lib/notify';
import selectionState from '$lib/stores/selectionState'; import selectionState from '$lib/stores/selectionState';
import { Checkbox } from '@gradio/form'; import { Checkbox } from '@gradio/form';
import modalState from '$lib/stores/modalState';
import queueState from '$lib/stores/queueState';
import PromptDisplay from "$lib/components/PromptDisplay.svelte"
import { getQueueEntryImages } from '$lib/stores/uiQueueState';
import { SvelteComponent } from 'svelte';
import { capitalize } from '$lib/utils';
export let app: ComfyApp; export let app: ComfyApp;
let workflow: ComfyBoxWorkflow; let workflow: ComfyBoxWorkflow | null = null;
let journey: WritableJourneyStateStore let journey: WritableJourneyStateStore | null = null;
let activeNode: JourneyNode | null = null;
let mode: JourneyMode = "linear";
const MODES: [JourneyMode, typeof SvelteComponent][] = [
["linear", ClockHistory],
["tree", Diagram3],
]
$: workflow = $workflowState.activeWorkflow $: workflow = $workflowState.activeWorkflow
$: journey = workflow?.journey $: {
journey = workflow?.journey
function doAdd() { activeNode = null;
if (!workflow) { updateActiveNode();
notify("No active workflow!", { type: "error" })
return;
}
const workflowParams = getWorkflowRestoreParamsFromWorkflow(workflow)
const activeNode = journey.getActiveNode();
journey.pushPatchOntoActive(workflow, activeNode, true)
} }
function onSelectNode(e: CustomEvent<{ cyto: cytoscape.Core, node: cytoscape.NodeSingular }>) { function updateActiveNode() {
activeNode = journey?.getActiveNode()
}
// function doAdd() {
// if (!workflow) {
// notify("No active workflow!", { type: "error" })
// return;
// }
//
// const activeNode = journey.getActiveNode();
// journey.pushPatchOntoActive(workflow, activeNode, true)
// }
function doClearHistory() {
if (!confirm("Clear history?"))
return;
journey.clear();
notify("History cleared.", { type: "info" })
}
function onSelectNode(e: CustomEvent<JourneyNodeEvent>) {
const { node } = e.detail; const { node } = e.detail;
const id = node.id(); const id = node.id();
@@ -45,6 +77,8 @@
return; return;
} }
console.debug("[ComfyJourneyView] Journey node", journeyNode)
const patch = resolvePatch(journeyNode); const patch = resolvePatch(journeyNode);
// ensure reactive state is updated // ensure reactive state is updated
@@ -52,7 +86,40 @@
$workflowState = $workflowState $workflowState = $workflowState
} }
function onHoverNode(e: CustomEvent<{ cyto: cytoscape.Core, node: cytoscape.NodeSingular }>) { function onRightClickNode(e: CustomEvent<JourneyNodeEvent>) {
const { node } = e.detail;
const id = node.id();
const journeyNode = $journey.nodesByID[id];
if (journeyNode == null) {
console.error("[ComfyJourneyView] Missing journey node!", id)
return;
}
const promptID = journeyNode.promptID;
if (promptID != null) {
const queueEntry = queueState.getQueueEntry(journeyNode.promptID)
if (queueEntry?.prompt != null) {
modalState.pushModal({
title: "Prompt Details",
svelteComponent: PromptDisplay,
svelteProps: {
prompt: queueEntry.prompt,
workflow: queueEntry.extraData?.extra_pnginfo?.comfyBoxWorkflow,
images: getQueueEntryImages(queueEntry),
closeModal: () => modalState.closeAllModals(),
expandAll: false,
app
},
})
}
}
else {
notify("This journey entry has no prompts yet.", { type: "warning" })
}
}
function onHoverNode(e: CustomEvent<JourneyNodeEvent>) {
const { node } = e.detail; const { node } = e.detail;
const id = node.id(); const id = node.id();
@@ -69,28 +136,43 @@
$selectionState.currentPatchHoveredNodes = new Set(Object.keys(diff)); $selectionState.currentPatchHoveredNodes = new Set(Object.keys(diff));
} }
function onHoverNodeOut(e: CustomEvent<{ cyto: cytoscape.Core, node: cytoscape.NodeSingular }>) { function onHoverNodeOut(e: CustomEvent<JourneyNodeEvent>) {
$selectionState.currentPatchHoveredNodes = new Set(); $selectionState.currentPatchHoveredNodes = new Set();
} }
</script> </script>
<div class="journey-view"> <div class="journey-view">
<JourneyRenderer {workflow} {journey} <div class="top">
on:select_node={onSelectNode}
on:hover_node={onHoverNode}
on:hover_node_out={onHoverNodeOut}
/>
<div class="bottom" style:border-top="1px solid var(--panel-border-color)">
<Checkbox label="Auto-Push" disabled={$journey.root == null} bind:value={$uiState.autoPushJourney}/>
</div>
<div class="bottom">
<button class="mode-button ternary" <button class="mode-button ternary"
title={"Add new"} title={"Add new"}
disabled={$journey.activeNodeID === null && $journey.root !== null} disabled={$journey.root == null}
on:click={doAdd}> on:click={doClearHistory}>
<Plus width="100%" height="100%" /> <Trash width="100%" height="100%" />
</button> </button>
</div> </div>
{#key $journey.version}
<JourneyRenderer {workflow} {journey} {mode} {activeNode}
on:select_node={onSelectNode}
on:right_click_node={onRightClickNode}
on:hover_node={onHoverNode}
on:hover_node_out={onHoverNodeOut}
/>
{/key}
<div class="bottom" style:border-top="1px solid var(--panel-border-color)">
<Checkbox label="Save History" bind:value={$uiState.saveHistory}/>
</div>
<div class="bottom">
{#each MODES as [theMode, icon]}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<button class="mode-button ternary"
disabled={mode === theMode}
title={capitalize(theMode)}
class:selected={mode === theMode}
on:click={() => { mode = theMode; }}>
<svelte:component this={icon} width="100%" height="100%" />
</button>
{/each}
</div>
</div> </div>
<style lang="scss"> <style lang="scss">
@@ -98,7 +180,12 @@
.journey-view { .journey-view {
width: 100%; width: 100%;
height: calc(100% - $button-height * 2); height: calc(100% - $button-height * 3);
}
.top {
height: $button-height;
color: var(--comfy-accent-soft);
} }
.bottom { .bottom {
@@ -107,20 +194,20 @@
flex-direction: row; flex-direction: row;
color: var(--comfy-accent-soft); color: var(--comfy-accent-soft);
justify-content: center; justify-content: center;
}
.mode-button { .mode-button {
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 0.5rem; padding: 0.5rem;
@include square-button; @include square-button;
&:hover { &:hover {
color: var(--body-text-color); color: var(--body-text-color);
} }
&.selected { &.selected {
background-color: var(--panel-background-fill); background-color: var(--panel-background-fill);
}
} }
} }
</style> </style>

View File

@@ -32,7 +32,6 @@
] ]
function switchMode(newMode: ComfyPaneMode) { function switchMode(newMode: ComfyPaneMode) {
console.warn("switch", mode, newMode)
mode = newMode; mode = newMode;
} }
</script> </script>

View File

@@ -1,67 +1,115 @@
<script context="module" lang="ts">
export type JourneyNodeEvent = {
cyto: cytoscape.Core,
node: cytoscape.NodeSingular
}
</script>
<script lang="ts"> <script lang="ts">
import type { JourneyPatchNode, WritableJourneyStateStore } from '$lib/stores/journeyStates'; import type { JourneyNode, JourneyPatchNode, WritableJourneyStateStore } from '$lib/stores/journeyStates';
import { ComfyBoxWorkflow } from '$lib/stores/workflowState'; import { ComfyBoxWorkflow } from '$lib/stores/workflowState';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import Graph from './graph/Graph.svelte' import Graph from './graph/Graph.svelte'
import type { NodeDataDefinition, EdgeDataDefinition } from 'cytoscape'; import type { NodeDataDefinition, EdgeDataDefinition } from 'cytoscape';
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import selectionState from '$lib/stores/selectionState'; import selectionState from '$lib/stores/selectionState';
import uiQueueState from '$lib/stores/uiQueueState'; import uiQueueState, { getQueueEntryImages } from '$lib/stores/uiQueueState';
import queueState from '$lib/stores/queueState';
import { convertComfyOutputToComfyURL, countNewLines } from '$lib/utils';
import type { ElementDefinition } from 'cytoscape';
import type { JourneyMode } from './ComfyJourneyView.svelte';
export let workflow: ComfyBoxWorkflow | null = null export let workflow: ComfyBoxWorkflow | null = null
export let journey: WritableJourneyStateStore | null = null export let journey: WritableJourneyStateStore | null = null
export let mode: JourneyMode = "linear";
export let activeNode: JourneyNode | null = null;
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
select_node: { cyto: cytoscape.Core, node: cytoscape.NodeSingular }; select_node: JourneyNodeEvent;
hover_node: { cyto: cytoscape.Core, node: cytoscape.NodeSingular }; right_click_node: JourneyNodeEvent;
hover_node_out: { cyto: cytoscape.Core, node: cytoscape.NodeSingular }; hover_node: JourneyNodeEvent;
hover_node_out: JourneyNodeEvent;
}>(); }>();
let lastSelected = null; let lastSelected = null;
let lastMode = null;
let lastVersion = -1; let lastVersion = -1;
let nodes = [] let nodes = []
let edges = [] let edges = []
$: if ($journey.version !== lastVersion){ $: if ($journey.version !== lastVersion || lastMode !== mode){
[nodes, edges] = buildGraph(journey) [nodes, edges] = buildGraph(journey)
lastVersion = $journey.version lastVersion = $journey.version
lastMode = mode;
} }
function buildGraph(journey: WritableJourneyStateStore | null): [NodeDataDefinition[], EdgeDataDefinition[]] { /*
* Converts the journey tree into the renderable graph format Cytoscape expects
*/
function buildGraph(journey: WritableJourneyStateStore | null): [ElementDefinition[], ElementDefinition[]] {
if (!journey) { if (!journey) {
return [[], []] return [[], []]
} }
const journeyState = get(journey); const journeyState = get(journey);
lastSelected = journeyState.activeNodeID;
const nodes: NodeDataDefinition[] = [] const nodes: ElementDefinition[] = []
const edges: EdgeDataDefinition[] = [] const edges: ElementDefinition[] = []
for (const node of journey.iterateBreadthFirst()) { for (const node of journey.iterateBreadthFirst()) {
if (node.type === "root") { if (node.type === "root") {
nodes.push({ nodes.push({
id: node.id, data: {
label: "Start", id: node.id,
selected: node.id === journeyState.activeNodeID, label: "Start",
locked: true },
selected: node.id === activeNode?.id,
classes: "historyNode"
}) })
continue; continue;
} }
else { else {
const patchNode = node as JourneyPatchNode; const patchNode = node as JourneyPatchNode;
nodes.push({ nodes.push({
id: patchNode.id, data: {
label: "P", id: patchNode.id,
selected: node.id === journeyState.activeNodeID, label: "P",
locked: true },
selected: node.id === activeNode?.id,
classes: "historyNode"
}) })
// Display a small node between with the patch details
const midNodeID = `${patchNode.id}_patch`;
const patchText = "cfg: 8 -> 10\nsteps: 20->30\na";
const patchNodeHeight = countNewLines(patchText) * 11 + 22;
nodes.push({
data: {
id: midNodeID,
label: patchText,
patchNodeHeight
},
selectable: false,
classes: "patchNode"
})
edges.push({ edges.push({
id: `${patchNode.parent.id}_${patchNode.id}`, data: {
source: patchNode.parent.id, id: `${patchNode.parent.id}_${midNodeID}`,
target: patchNode.id, source: patchNode.parent.id,
target: midNodeID,
},
selectable: false,
})
edges.push({
data: {
id: `${midNodeID}_${patchNode.id}`,
source: midNodeID,
target: patchNode.id,
},
selectable: false, selectable: false,
locked: true locked: true
}) })
@@ -88,6 +136,11 @@
dispatch("select_node", { cyto: e.cy, node }) dispatch("select_node", { cyto: e.cy, node })
} }
function onNodeRightClicked(e: cytoscape.InputEventObject) {
const node = e.target as cytoscape.NodeSingular;
dispatch("right_click_node", { cyto: e.cy, node })
}
function onNodeHovered(e: cytoscape.InputEventObject) { function onNodeHovered(e: cytoscape.InputEventObject) {
const node = e.target as cytoscape.NodeSingular; const node = e.target as cytoscape.NodeSingular;
dispatch("hover_node", { cyto: e.cy, node }) dispatch("hover_node", { cyto: e.cy, node })
@@ -104,8 +157,6 @@
for (const node of cyto.nodes().components()) { for (const node of cyto.nodes().components()) {
const nodeID = node.id() const nodeID = node.id()
if (nodeID === lastSelected) { if (nodeID === lastSelected) {
// why doesn't passing `selected` work in the ctor?
node.select();
cyto.zoom(1.25); cyto.zoom(1.25);
cyto.center(node) cyto.center(node)
} }
@@ -113,9 +164,14 @@
const journeyNode = $journey.nodesByID[nodeID] const journeyNode = $journey.nodesByID[nodeID]
if (journeyNode) { if (journeyNode) {
if (journeyNode.promptID != null) { if (journeyNode.promptID != null) {
const queueEntry = $uiQueueState.historyUIEntries.find(e => e.entry.promptID === journeyNode.promptID) const queueEntry = queueState.getQueueEntry(journeyNode.promptID)
if (queueEntry != null && queueEntry.images) { if (queueEntry) {
node.data("bgImage", queueEntry.images[0]); const outputs = getQueueEntryImages(queueEntry);
if (outputs) {
node.data("bgImage", outputs[0]);
}
console.warn("node.classes", node.classes())
} }
} }
} }
@@ -126,8 +182,9 @@
cyto.nodes() cyto.nodes()
.lock() .lock()
.on("select", onNodeSelected) .on("select", onNodeSelected)
.on("mouseover", onNodeHovered) .on("cxttapend ", onNodeRightClicked)
.on("mouseout", onNodeHoveredOut) .on("mouseout", onNodeHoveredOut)
.on("mouseover", onNodeHovered)
} }
</script> </script>

View File

@@ -37,8 +37,11 @@
on:close={close} on:close={close}
on:cancel={doClose} on:cancel={doClose}
on:click|self={close} on:click|self={close}
on:contextmenu|preventDefault|stopPropagation
> >
<div on:click|stopPropagation> <div on:click|stopPropagation
on:contextmenu|stopPropagation
>
<slot name="header" /> <slot name="header" />
<slot {closeDialog} /> <slot {closeDialog} />
<div class="button-row"> <div class="button-row">

View File

@@ -10,19 +10,22 @@
import type { Styles } from "@gradio/utils"; import type { Styles } from "@gradio/utils";
import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines } from "$lib/utils"; import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines } from "$lib/utils";
import ReceiveOutputTargets from "./modal/ReceiveOutputTargets.svelte"; import ReceiveOutputTargets from "./modal/ReceiveOutputTargets.svelte";
import RestoreParamsTable from "./modal/RestoreParamsTable.svelte";
import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState"; import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions"; import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
import type ComfyApp from "./ComfyApp"; import type ComfyApp from "./ComfyApp";
import { TabItem, Tabs } from "@gradio/tabs"; import { TabItem, Tabs } from "@gradio/tabs";
import { type ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; import { type ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer"; import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
import JsonView from "./JsonView.svelte"; import JsonView from "./JsonView.svelte";
import type { ZodError } from "zod"; import type { ZodError } from "zod";
import { concatRestoreParams, getWorkflowRestoreParams, type RestoreParamTargets, type RestoreParamWorkflowNodeTargets } from "$lib/restoreParameters";
const splitLength = 50; const splitLength = 50;
export let prompt: SerializedPromptInputsAll; export let prompt: SerializedPromptInputsAll;
export let workflow: SerializedAppState | null; export let workflow: SerializedAppState | null;
export let restoreParams: RestoreParamTargets = {}
export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint 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;
@@ -33,6 +36,14 @@
let stdPromptError: ZodError<any> | null; let stdPromptError: ZodError<any> | null;
$: { $: {
restoreParams = {}
// TODO other sources than serialized workflow
if (workflow != null) {
const workflowParams = getWorkflowRestoreParams(workflow.workflow)
restoreParams = concatRestoreParams(restoreParams, workflowParams);
}
const [result, orig] = new ComfyBoxStdPromptSerializer().serialize(prompt, workflow); const [result, orig] = new ComfyBoxStdPromptSerializer().serialize(prompt, workflow);
if (result.success === true) { if (result.success === true) {
stdPrompt = result.data; stdPrompt = result.data;
@@ -44,7 +55,9 @@
} }
} }
let selectedTab: "restore-parameters" | "send-outputs" | "standard-prompt" | "prompt" = "standard-prompt"; type PromptDisplayTabID = "restore-parameters" | "send-outputs" | "standard-prompt" | "prompt"
let selectedTab: PromptDisplayTabID = "restore-parameters"
let selected_image: number | null = null; let selected_image: number | null = null;
@@ -146,15 +159,16 @@
closeModal(); closeModal();
} }
function doRestoreParams(e: CustomEvent) {
}
</script> </script>
<div class="prompt-display"> <div class="prompt-display">
<div class="prompt-and-sends"> <div class="prompt-and-sends">
<Tabs bind:selected={selectedTab}> <Tabs bind:selected={selectedTab}>
<TabItem id="restore-parameters" name="Restore Parameters"> <TabItem id="restore-parameters" name="Restore Parameters">
<Block> <RestoreParamsTable {restoreParams} on:restore={doRestoreParams} />
<BlockTitle>Parameters</BlockTitle>
</Block>
</TabItem> </TabItem>
{#if comfyBoxImages.length > 0} {#if comfyBoxImages.length > 0}
<TabItem id="send-outputs" name="Send Outputs"> <TabItem id="send-outputs" name="Send Outputs">

View File

@@ -70,40 +70,34 @@
{#if container} {#if container}
{#key $attrsChanged} <Container {layoutState} {container} {classes} {zIndex} {showHandles} {isMobile} />
<Container {layoutState} {container} {classes} {zIndex} {showHandles} {isMobile} />
{/key}
{:else if widget && widget.node} {:else if widget && widget.node}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"}
{@const hidden = isHidden(widget)} {@const hidden = isHidden(widget)}
{@const hovered = $uiState.uiUnlocked && $selectionState.currentHovered.has(widget.id)} {@const hovered = $uiState.uiUnlocked && $selectionState.currentHovered.has(widget.id)}
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(widget.id)} {@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(widget.id)}
{#key $attrsChanged} <div class="widget {widget.attrs.classes} {getWidgetClass()}"
{#key $propsChanged} class:edit={edit}
<div class="widget {widget.attrs.classes} {getWidgetClass()}" class:hovered
class:edit={edit} class:selected
class:hovered class:patch-affected={$selectionState.currentPatchHoveredNodes.has(widget.node.id)}
class:selected class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id}
class:patch-affected={$selectionState.currentPatchHoveredNodes.has(widget.node.id)} class:hidden={hidden}
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id} >
class:hidden={hidden} <svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
> </div>
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} /> {#if hidden && edit}
</div> <div class="handle handle-hidden" class:hidden={!edit} />
{#if hidden && edit} {/if}
<div class="handle handle-hidden" class:hidden={!edit} /> {#if showHandles || hovered}
{/if} <div class="handle handle-widget"
{#if showHandles || hovered} class:hovered
<div class="handle handle-widget" data-drag-item-id={widget.id}
class:hovered on:mousedown={_startDrag}
data-drag-item-id={widget.id} on:touchstart={_startDrag}
on:mousedown={_startDrag} on:mouseup={_stopDrag}
on:touchstart={_startDrag} on:touchend={_stopDrag}/>
on:mouseup={_stopDrag} {/if}
on:touchend={_stopDrag}/>
{/if}
{/key}
{/key}
{/if} {/if}
<style lang="scss"> <style lang="scss">

View File

@@ -10,12 +10,11 @@
import cytoscape from "cytoscape" import cytoscape from "cytoscape"
import dagre from "cytoscape-dagre" import dagre from "cytoscape-dagre"
import GraphStyles from "./GraphStyles" import GraphStyles from "./GraphStyles"
import type { EdgeDataDefinition } from "cytoscape"; import type { ElementDefinition } from "cytoscape";
import type { NodeDataDefinition } from "cytoscape";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
export let nodes: ReadonlyArray<NodeDataDefinition>; export let nodes: ReadonlyArray<ElementDefinition>;
export let edges: ReadonlyArray<EdgeDataDefinition>; export let edges: ReadonlyArray<ElementDefinition>;
export let style: string = "" export let style: string = ""
@@ -32,6 +31,7 @@
function rebuildGraph() { function rebuildGraph() {
cytoscape.use(dagre) cytoscape.use(dagre)
cytoscape.warnings(false)
cyInstance = cytoscape({ cyInstance = cytoscape({
container: refElement, container: refElement,
@@ -39,6 +39,7 @@
wheelSensitivity: 0.1, wheelSensitivity: 0.1,
maxZoom: 3, maxZoom: 3,
minZoom: 0.5, minZoom: 0.5,
selectionType: "single"
}) })
cyInstance.on("add", () => { cyInstance.on("add", () => {
@@ -51,18 +52,25 @@
.run() .run()
}) })
// Prevents the unselection of nodes when clicking on the background
cyInstance.on('click', (event) => {
if (event.target === cyInstance) {
// click on the background
cyInstance.nodes(".historyNode").unselectify();
} else {
cyInstance.nodes(".historyNode").selectify();
}
});
for (const node of nodes) { for (const node of nodes) {
cyInstance.add({ node.group = "nodes"
group: 'nodes', cyInstance.add(node)
data: { ...node }
})
} }
for (const edge of edges) { for (const edge of edges) {
cyInstance.add({ edge.group = "edges";
group: 'edges', console.warn(edge)
data: { ...edge } cyInstance.add(edge)
})
} }
dispatch("rebuilt", { cyto: cyInstance }) dispatch("rebuilt", { cyto: cyInstance })

View File

@@ -16,7 +16,7 @@ const styles: Stylesheet[] = [
} }
}, },
{ {
selector: "node", selector: ".historyNode",
style: { style: {
"width": "100", "width": "100",
"height": "100", "height": "100",
@@ -35,7 +35,7 @@ const styles: Stylesheet[] = [
} }
}, },
{ {
selector: "node[bgImage]", selector: "node.historyNode[bgImage]",
style: { style: {
"label": "", "label": "",
"background-image": "data(bgImage)", "background-image": "data(bgImage)",
@@ -45,13 +45,32 @@ const styles: Stylesheet[] = [
} }
}, },
{ {
selector: "node:selected", selector: ".historyNode:selected",
style: { style: {
"background-color": "#f97316", "background-color": "#f97316",
"color": "white", "color": "white",
"border-color": "#ea580c", "border-color": "#ea580c",
"line-color": "#0e76ba", "line-color": "#0e76ba",
"target-arrow-color": "#0e76ba" "target-arrow-color": "#0e76ba",
}
},
{
selector: ".patchNode",
style: {
"width": "100",
"height": "data(patchNodeHeight)",
"shape": "round-rectangle",
"font-family": "Arial",
"font-size": "11",
"font-weight": "normal",
"content": `data(label)`,
"text-valign": "center",
"text-wrap": "wrap",
"text-max-width": "140",
"background-color": "#333",
"border-color": "#black",
"border-width": "1",
"color": "white",
} }
}, },
{ {

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
import type { RestoreParamTargets } from "$lib/restoreParameters";
import { isComfyWidgetNode } from "$lib/stores/layoutStates";
import type { ComfyBoxWorkflow, WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
import workflowState from "$lib/stores/workflowState";
import { Block, BlockTitle } from "@gradio/atoms";
import { Button } from "@gradio/button";
import { createEventDispatcher } from "svelte";
type UIRestoreParam = {
node: ComfyWidgetNode,
widget: WidgetLayout,
sources: RestoreParamSource[]
}
const dispatch = createEventDispatcher<{
restore: {};
}>();
export let restoreParams: RestoreParamTargets = {};
let uiRestoreParams: UIRestoreParam[] = []
$: uiRestoreParams = buildForUI(restoreParams);
function buildForUI(restoreParams: RestoreParamTargets): UIRestoreParam[] {
const result = []
for (const [nodeID, sources] of Object.entries(restoreParams)) {
const node = workflow.graph.getNodeByIdRecursive(nodeID);
if (node == null || !isComfyWidgetNode(node))
continue;
const widget = node.dragItem;
if (widget == null) {
console.error("[RestoreParamsTable] Node missing layoutState widget!!!", node)
}
result.push({ node, widget, sources })
}
return result
}
let workflow: ComfyBoxWorkflow;
$: workflow = workflowState.getActiveWorkflow();
function doRestore(e: MouseEvent) {
dispatch("restore", {})
}
</script>
<div class="scroll-container">
{#if workflow == null}
<div>No workflow is active.</div>
{:else if Object.keys(uiRestoreParams).length === 0}
<div>
<p>No parameters to restore found in this workflow.</p>
<p>(TODO: Only parameters compatible with the currently active workflow can be restored right now)</p>
</div>
{:else}
<Block>
<BlockTitle>Parameters</BlockTitle>
<Block>
<Button variant="primary" on:click={doRestore}>
Restore
</Button>
</Block>
{#each uiRestoreParams as { node, widget, sources }}
<Block>
<BlockTitle>{widget.attrs.title || node.title}</BlockTitle>
{#each sources as source}
<div class="target">
<div class="target-name-and-desc">
<div class="target-name">{source.type}</div>
</div>
</div>
{/each}
</Block>
{/each}
</Block>
{/if}
</div>
<style lang="scss">
.scroll-container {
overflow: auto;
position: relative;
flex: 1 1 0%;
height: 100%;
> :global(.block) {
background: var(--panel-background-fill);
}
}
.target {
display: flex;
flex-direction: row;
justify-content: center;
text-align: left;
.target-name-and-desc {
margin: auto auto auto 0;
left: 0px;
.target-desc {
opacity: 65%;
font-size: 11pt;
}
}
}
</style>

View File

@@ -118,7 +118,7 @@ export default class ComfyGraphNode extends LGraphNode {
} }
get dragItem(): WidgetLayout | null { get dragItem(): WidgetLayout | null {
return layoutStates.getDragItemByNode(this); return layoutStates.getDragItemByNode(this) as WidgetLayout;
} }
get workflow(): ComfyBoxWorkflow | null { get workflow(): ComfyBoxWorkflow | null {

View File

@@ -185,24 +185,23 @@ export function getWorkflowRestoreParamsFromWorkflow(workflow: ComfyBoxWorkflow,
return result return result
} }
export function getWorkflowRestoreParams(workflow: ComfyBoxWorkflow, prompt: SerializedLGraph): RestoreParamWorkflowNodeTargets { export function getWorkflowRestoreParams(serGraph: SerializedLGraph, noExclude: boolean = false): RestoreParamWorkflowNodeTargets {
const result = {} const result = {}
const graph = workflow.graph; for (const node of serGraph.nodes) {
if (!isSerializedComfyWidgetNode(node))
continue;
// Find nodes that correspond to *this* workflow exactly, since we can if (!noExclude && node.properties.excludeFromJourney)
// easily match up the nodes between each (their IDs will be the same) continue;
for (const serNode of prompt.nodes) {
const foundNode = graph.getNodeByIdRecursive(serNode.id); const finalValue = node.comfyValue
if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) { if (finalValue != null) {
const finalValue = (serNode as SerializedComfyWidgetNode).comfyValue; const source: RestoreParamSourceWorkflowNode = {
if (finalValue != null) { type: "workflow",
const source: RestoreParamSourceWorkflowNode = { finalValue,
type: "workflow",
finalValue,
}
result[foundNode.id] = source;
} }
result[node.id] = source;
} }
} }
@@ -250,10 +249,10 @@ export function getBackendRestoreParams(workflow: ComfyBoxWorkflow, prompt: Seri
return result return result
} }
export default function restoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets { export default function getRestoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets {
const result = {} const result = {}
const workflowParams = getWorkflowRestoreParams(workflow, prompt.workflow); const workflowParams = getWorkflowRestoreParams(prompt.workflow);
concatRestoreParams(result, workflowParams); concatRestoreParams(result, workflowParams);
const backendParams = getBackendRestoreParams(workflow, prompt); const backendParams = getBackendRestoreParams(workflow, prompt);

View File

@@ -185,7 +185,8 @@ function create() {
if (activeNode == null) { if (activeNode == null) {
// add root node // add root node
if (get(store).root != null) { if (get(store).root != null) {
return; console.debug("[journeyStates] Root already exists")
return null;
} }
journeyNode = addNode(workflowParams, null); journeyNode = addNode(workflowParams, null);
if (showNotification) if (showNotification)
@@ -196,9 +197,10 @@ function create() {
const patch = calculateWorkflowParamsPatch(activeNode, workflowParams); const patch = calculateWorkflowParamsPatch(activeNode, workflowParams);
const patchedCount = Object.keys(patch).length; const patchedCount = Object.keys(patch).length;
if (patchedCount === 0) { if (patchedCount === 0) {
console.debug("[journeyStates] Patch had no diff")
if (showNotification) if (showNotification)
notify("No changes were made to active parameters yet.", { type: "warning" }) notify("No changes were made to active parameters yet.", { type: "warning" })
return; return null;
} }
journeyNode = addNode(patch, activeNode); journeyNode = addNode(patch, activeNode);
if (showNotification) if (showNotification)
@@ -209,6 +211,7 @@ function create() {
selectNode(journeyNode); selectNode(journeyNode);
} }
console.debug("[journeyStates] added node", journeyNode)
return journeyNode; return journeyNode;
} }
@@ -268,6 +271,11 @@ function create() {
return; return;
// TODO // TODO
store.update(s => {
s.version += 1;
s.activeNodeID = journeyNode.id;
return s;
})
} }
return { return {

View File

@@ -89,6 +89,13 @@ function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEnt
} }
} }
export function getQueueEntryImages(queueEntry: QueueEntry): string[] {
return Object.values(queueEntry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
}
function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry { function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
const result = convertEntry(entry, status); const result = convertEntry(entry, status);
@@ -97,10 +104,7 @@ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): Que
result.images = thumbnails.map(convertComfyOutputToComfyURL); result.images = thumbnails.map(convertComfyOutputToComfyURL);
} }
const outputs = Object.values(entry.outputs) const outputs = getQueueEntryImages(entry);
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
if (outputs) { if (outputs) {
result.images = result.images.concat(outputs) result.images = result.images.concat(outputs)
} }
@@ -111,11 +115,7 @@ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): Que
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry { function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
const result = convertEntry(entry.entry, entry.status); const result = convertEntry(entry.entry, entry.status);
const images = Object.values(entry.entry.outputs) result.images = getQueueEntryImages(entry.entry)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
result.images = images
if (entry.message) if (entry.message)
result.submessage = entry.message result.submessage = entry.message

View File

@@ -16,7 +16,7 @@ export type UIState = {
activeError: PromptID | null activeError: PromptID | null
autoPushJourney: boolean saveHistory: boolean
} }
type UIStateOps = { type UIStateOps = {
@@ -38,7 +38,7 @@ const store: Writable<UIState> = writable(
activeError: null, activeError: null,
autoPushJourney: true saveHistory: true
}) })
function reconnecting() { function reconnecting() {