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);
}
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;
if (get(uiState).autoPushJourney) {
if (get(uiState).saveHistory) {
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;

View File

@@ -3,39 +3,71 @@
tree-like graph. It lets you save incremental changes to your workflow and
jump between past and present sets of parameters.
-->
<script context="module" lang="ts">
export type JourneyMode = "linear" | "tree";
</script>
<script lang="ts">
import type ComfyApp from './ComfyApp';
import type { ComfyBoxWorkflow } from '$lib/stores/workflowState';
import workflowState from '$lib/stores/workflowState';
import uiState from '$lib/stores/uiState';
import { calculateWorkflowParamsPatch, resolvePatch, type JourneyPatchNode, type WritableJourneyStateStore, diffParams } from '$lib/stores/journeyStates';
import JourneyRenderer from './JourneyRenderer.svelte';
import { Plus } from "svelte-bootstrap-icons";
import { calculateWorkflowParamsPatch, resolvePatch, type JourneyPatchNode, type WritableJourneyStateStore, diffParams, JourneyNode } from '$lib/stores/journeyStates';
import JourneyRenderer, { type JourneyNodeEvent } from './JourneyRenderer.svelte';
import { Trash, ClockHistory, Diagram3 } from "svelte-bootstrap-icons";
import { getWorkflowRestoreParams, getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters';
import notify from '$lib/notify';
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;
let workflow: ComfyBoxWorkflow;
let journey: WritableJourneyStateStore
let workflow: ComfyBoxWorkflow | null = null;
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
$: journey = workflow?.journey
function doAdd() {
if (!workflow) {
notify("No active workflow!", { type: "error" })
return;
}
const workflowParams = getWorkflowRestoreParamsFromWorkflow(workflow)
const activeNode = journey.getActiveNode();
journey.pushPatchOntoActive(workflow, activeNode, true)
$: {
journey = workflow?.journey
activeNode = null;
updateActiveNode();
}
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 id = node.id();
@@ -45,6 +77,8 @@
return;
}
console.debug("[ComfyJourneyView] Journey node", journeyNode)
const patch = resolvePatch(journeyNode);
// ensure reactive state is updated
@@ -52,7 +86,40 @@
$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 id = node.id();
@@ -69,28 +136,43 @@
$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();
}
</script>
<div class="journey-view">
<JourneyRenderer {workflow} {journey}
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">
<div class="top">
<button class="mode-button ternary"
title={"Add new"}
disabled={$journey.activeNodeID === null && $journey.root !== null}
on:click={doAdd}>
<Plus width="100%" height="100%" />
disabled={$journey.root == null}
on:click={doClearHistory}>
<Trash width="100%" height="100%" />
</button>
</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>
<style lang="scss">
@@ -98,7 +180,12 @@
.journey-view {
width: 100%;
height: calc(100% - $button-height * 2);
height: calc(100% - $button-height * 3);
}
.top {
height: $button-height;
color: var(--comfy-accent-soft);
}
.bottom {
@@ -107,20 +194,20 @@
flex-direction: row;
color: var(--comfy-accent-soft);
justify-content: center;
}
.mode-button {
height: 100%;
width: 100%;
padding: 0.5rem;
.mode-button {
height: 100%;
width: 100%;
padding: 0.5rem;
@include square-button;
@include square-button;
&:hover {
color: var(--body-text-color);
}
&.selected {
background-color: var(--panel-background-fill);
}
&:hover {
color: var(--body-text-color);
}
&.selected {
background-color: var(--panel-background-fill);
}
}
</style>

View File

@@ -32,7 +32,6 @@
]
function switchMode(newMode: ComfyPaneMode) {
console.warn("switch", mode, newMode)
mode = newMode;
}
</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">
import type { JourneyPatchNode, WritableJourneyStateStore } from '$lib/stores/journeyStates';
import type { JourneyNode, JourneyPatchNode, WritableJourneyStateStore } from '$lib/stores/journeyStates';
import { ComfyBoxWorkflow } from '$lib/stores/workflowState';
import { get } from 'svelte/store';
import { get } from 'svelte/store';
import Graph from './graph/Graph.svelte'
import type { NodeDataDefinition, EdgeDataDefinition } from 'cytoscape';
import { createEventDispatcher } from "svelte";
import selectionState from '$lib/stores/selectionState';
import uiQueueState from '$lib/stores/uiQueueState';
import selectionState from '$lib/stores/selectionState';
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 journey: WritableJourneyStateStore | null = null
export let mode: JourneyMode = "linear";
export let activeNode: JourneyNode | null = null;
const dispatch = createEventDispatcher<{
select_node: { cyto: cytoscape.Core, node: cytoscape.NodeSingular };
hover_node: { cyto: cytoscape.Core, node: cytoscape.NodeSingular };
hover_node_out: { cyto: cytoscape.Core, node: cytoscape.NodeSingular };
select_node: JourneyNodeEvent;
right_click_node: JourneyNodeEvent;
hover_node: JourneyNodeEvent;
hover_node_out: JourneyNodeEvent;
}>();
let lastSelected = null;
let lastMode = null;
let lastVersion = -1;
let nodes = []
let edges = []
$: if ($journey.version !== lastVersion){
$: if ($journey.version !== lastVersion || lastMode !== mode){
[nodes, edges] = buildGraph(journey)
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) {
return [[], []]
}
const journeyState = get(journey);
lastSelected = journeyState.activeNodeID;
const nodes: NodeDataDefinition[] = []
const edges: EdgeDataDefinition[] = []
const nodes: ElementDefinition[] = []
const edges: ElementDefinition[] = []
for (const node of journey.iterateBreadthFirst()) {
if (node.type === "root") {
nodes.push({
id: node.id,
label: "Start",
selected: node.id === journeyState.activeNodeID,
locked: true
data: {
id: node.id,
label: "Start",
},
selected: node.id === activeNode?.id,
classes: "historyNode"
})
continue;
}
else {
const patchNode = node as JourneyPatchNode;
nodes.push({
id: patchNode.id,
label: "P",
selected: node.id === journeyState.activeNodeID,
locked: true
data: {
id: patchNode.id,
label: "P",
},
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({
id: `${patchNode.parent.id}_${patchNode.id}`,
source: patchNode.parent.id,
target: patchNode.id,
data: {
id: `${patchNode.parent.id}_${midNodeID}`,
source: patchNode.parent.id,
target: midNodeID,
},
selectable: false,
})
edges.push({
data: {
id: `${midNodeID}_${patchNode.id}`,
source: midNodeID,
target: patchNode.id,
},
selectable: false,
locked: true
})
@@ -88,6 +136,11 @@
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) {
const node = e.target as cytoscape.NodeSingular;
dispatch("hover_node", { cyto: e.cy, node })
@@ -104,8 +157,6 @@
for (const node of cyto.nodes().components()) {
const nodeID = node.id()
if (nodeID === lastSelected) {
// why doesn't passing `selected` work in the ctor?
node.select();
cyto.zoom(1.25);
cyto.center(node)
}
@@ -113,9 +164,14 @@
const journeyNode = $journey.nodesByID[nodeID]
if (journeyNode) {
if (journeyNode.promptID != null) {
const queueEntry = $uiQueueState.historyUIEntries.find(e => e.entry.promptID === journeyNode.promptID)
if (queueEntry != null && queueEntry.images) {
node.data("bgImage", queueEntry.images[0]);
const queueEntry = queueState.getQueueEntry(journeyNode.promptID)
if (queueEntry) {
const outputs = getQueueEntryImages(queueEntry);
if (outputs) {
node.data("bgImage", outputs[0]);
}
console.warn("node.classes", node.classes())
}
}
}
@@ -126,8 +182,9 @@
cyto.nodes()
.lock()
.on("select", onNodeSelected)
.on("mouseover", onNodeHovered)
.on("cxttapend ", onNodeRightClicked)
.on("mouseout", onNodeHoveredOut)
.on("mouseover", onNodeHovered)
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ const styles: Stylesheet[] = [
}
},
{
selector: "node",
selector: ".historyNode",
style: {
"width": "100",
"height": "100",
@@ -35,7 +35,7 @@ const styles: Stylesheet[] = [
}
},
{
selector: "node[bgImage]",
selector: "node.historyNode[bgImage]",
style: {
"label": "",
"background-image": "data(bgImage)",
@@ -45,13 +45,32 @@ const styles: Stylesheet[] = [
}
},
{
selector: "node:selected",
selector: ".historyNode:selected",
style: {
"background-color": "#f97316",
"color": "white",
"border-color": "#ea580c",
"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 {
return layoutStates.getDragItemByNode(this);
return layoutStates.getDragItemByNode(this) as WidgetLayout;
}
get workflow(): ComfyBoxWorkflow | null {

View File

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

View File

@@ -185,7 +185,8 @@ function create() {
if (activeNode == null) {
// add root node
if (get(store).root != null) {
return;
console.debug("[journeyStates] Root already exists")
return null;
}
journeyNode = addNode(workflowParams, null);
if (showNotification)
@@ -196,9 +197,10 @@ function create() {
const patch = calculateWorkflowParamsPatch(activeNode, workflowParams);
const patchedCount = Object.keys(patch).length;
if (patchedCount === 0) {
console.debug("[journeyStates] Patch had no diff")
if (showNotification)
notify("No changes were made to active parameters yet.", { type: "warning" })
return;
return null;
}
journeyNode = addNode(patch, activeNode);
if (showNotification)
@@ -209,6 +211,7 @@ function create() {
selectNode(journeyNode);
}
console.debug("[journeyStates] added node", journeyNode)
return journeyNode;
}
@@ -268,6 +271,11 @@ function create() {
return;
// TODO
store.update(s => {
s.version += 1;
s.activeNodeID = journeyNode.id;
return s;
})
}
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 {
const result = convertEntry(entry, status);
@@ -97,10 +104,7 @@ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): Que
result.images = thumbnails.map(convertComfyOutputToComfyURL);
}
const outputs = Object.values(entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
const outputs = getQueueEntryImages(entry);
if (outputs) {
result.images = result.images.concat(outputs)
}
@@ -111,11 +115,7 @@ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): Que
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
const result = convertEntry(entry.entry, entry.status);
const images = Object.values(entry.entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
result.images = images
result.images = getQueueEntryImages(entry.entry)
if (entry.message)
result.submessage = entry.message

View File

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