temp linear history mode
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
]
|
||||
|
||||
function switchMode(newMode: ComfyPaneMode) {
|
||||
console.warn("switch", mode, newMode)
|
||||
mode = newMode;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
114
src/lib/components/modal/RestoreParamsTable.svelte
Normal file
114
src/lib/components/modal/RestoreParamsTable.svelte
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user