journeystuff
This commit is contained in:
@@ -4,49 +4,23 @@
|
|||||||
jump between past and present sets of parameters.
|
jump between past and present sets of parameters.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Graph from './graph/Graph.svelte'
|
|
||||||
import GraphNode from './graph/GraphNode.svelte'
|
|
||||||
import GraphEdge from './graph/GraphEdge.svelte'
|
|
||||||
|
|
||||||
import type ComfyApp from './ComfyApp';
|
import type ComfyApp from './ComfyApp';
|
||||||
|
import type { ComfyBoxWorkflow } from '$lib/stores/workflowState';
|
||||||
|
import workflowState from '$lib/stores/workflowState';
|
||||||
|
import type { WritableJourneyStateStore } from '$lib/stores/journeyStates';
|
||||||
|
import JourneyRenderer from './JourneyRenderer.svelte';
|
||||||
|
|
||||||
export let app: ComfyApp;
|
export let app: ComfyApp;
|
||||||
|
|
||||||
const nodes = [
|
let workflow: ComfyBoxWorkflow;
|
||||||
{ id: 'N1', label: 'Start' },
|
let journey: WritableJourneyStateStore
|
||||||
{ id: 'N2', label: '4' },
|
|
||||||
{ id: 'N4', label: '8' },
|
|
||||||
{ id: 'N5', label: '15' },
|
|
||||||
{ id: 'N3', label: '16' },
|
|
||||||
{ id: 'N6', label: '23' },
|
|
||||||
{ id: 'N7', label: '42' },
|
|
||||||
{ id: 'N8', label: 'End' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const edges = [
|
$: workflow = $workflowState.activeWorkflow
|
||||||
{ id: 'E1', source: 'N1', target: 'N2' },
|
$: journey = workflow?.journey
|
||||||
{ id: 'E2', source: 'N2', target: 'N3' },
|
|
||||||
{ id: 'E3', source: 'N3', target: 'N6' },
|
|
||||||
{ id: 'E4', source: 'N2', target: 'N4' },
|
|
||||||
{ id: 'E5', source: 'N4', target: 'N5' },
|
|
||||||
{ id: 'E6', source: 'N5', target: 'N4', label: '2' },
|
|
||||||
{ id: 'E7', source: 'N5', target: 'N6' },
|
|
||||||
{ id: 'E8', source: 'N6', target: 'N7' },
|
|
||||||
{ id: 'E9', source: 'N7', target: 'N7', label: '3' },
|
|
||||||
{ id: 'E10', source: 'N7', target: 'N8' }
|
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="journey-view">
|
<div class="journey-view">
|
||||||
<Graph>
|
<JourneyRenderer {workflow} {journey} />
|
||||||
{#each nodes as node}
|
|
||||||
<GraphNode node={node}/>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#each edges as edge}
|
|
||||||
<GraphEdge edge={edge}/>
|
|
||||||
{/each}
|
|
||||||
</Graph>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
38
src/lib/components/JourneyRenderer.svelte
Normal file
38
src/lib/components/JourneyRenderer.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WritableJourneyStateStore } from '$lib/stores/journeyStates';
|
||||||
|
import { ComfyBoxWorkflow } from '$lib/stores/workflowState';
|
||||||
|
import Graph from './graph/Graph.svelte'
|
||||||
|
import type { NodeDataDefinition, EdgeDataDefinition } from 'cytoscape';
|
||||||
|
import { NodeDataDefinition } from 'cytoscape';
|
||||||
|
|
||||||
|
export let workflow: ComfyBoxWorkflow | null = null
|
||||||
|
export let journey: WritableJourneyStateStore | null = ull
|
||||||
|
|
||||||
|
const nodes: NodeDataDefinition[] = [
|
||||||
|
{ id: 'N1', label: 'Start' },
|
||||||
|
{ id: 'N2', label: '4' },
|
||||||
|
{ id: 'N4', label: '8' },
|
||||||
|
{ id: 'N5', label: '15' },
|
||||||
|
{ id: 'N3', label: '16' },
|
||||||
|
{ id: 'N6', label: '23' },
|
||||||
|
{ id: 'N7', label: '42' },
|
||||||
|
{ id: 'N8', label: 'End' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const edges: EdgeDataDefinition[] = [
|
||||||
|
{ id: 'E1', source: 'N1', target: 'N2' },
|
||||||
|
{ id: 'E2', source: 'N2', target: 'N3' },
|
||||||
|
{ id: 'E3', source: 'N3', target: 'N6' },
|
||||||
|
{ id: 'E4', source: 'N2', target: 'N4' },
|
||||||
|
{ id: 'E5', source: 'N4', target: 'N5' },
|
||||||
|
{ id: 'E6', source: 'N5', target: 'N4', label: '2' },
|
||||||
|
{ id: 'E7', source: 'N5', target: 'N6' },
|
||||||
|
{ id: 'E8', source: 'N6', target: 'N7' },
|
||||||
|
{ id: 'E9', source: 'N7', target: 'N7', label: '3' },
|
||||||
|
{ id: 'E10', source: 'N7', target: 'N8' }
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if workflow && journey}
|
||||||
|
<Graph {nodes} {edges} style="background: var(--neutral-900)" />
|
||||||
|
{/if}
|
||||||
@@ -7,49 +7,70 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, setContext } from "svelte"
|
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 { NodeDataDefinition } from "cytoscape";
|
||||||
|
|
||||||
const graphContext: GraphContext = {
|
export let nodes: ReadonlyArray<NodeDataDefinition>;
|
||||||
getCyInstance: () => cyInstance
|
export let edges: ReadonlyArray<EdgeDataDefinition>;
|
||||||
|
|
||||||
|
export let style: string = ""
|
||||||
|
|
||||||
|
$: if (nodes != null && edges != null && refElement != null) {
|
||||||
|
rebuildGraph()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cyInstance = null;
|
||||||
}
|
}
|
||||||
setContext(GRAPH_STATE, graphContext)
|
|
||||||
|
|
||||||
let refElement = null
|
let _nodes: any[];
|
||||||
let cyInstance: cytoscape.Core = null
|
let _edges: any[];
|
||||||
|
|
||||||
onMount(() => {
|
function rebuildGraph() {
|
||||||
cytoscape.use(dagre)
|
cytoscape.use(dagre)
|
||||||
|
|
||||||
cyInstance = cytoscape({
|
cyInstance = cytoscape({
|
||||||
container: refElement,
|
container: refElement,
|
||||||
style: GraphStyles,
|
style: GraphStyles,
|
||||||
|
wheelSensitivity: 0.1,
|
||||||
|
})
|
||||||
|
|
||||||
})
|
cyInstance.on("add", () => {
|
||||||
|
cyInstance
|
||||||
|
.makeLayout({
|
||||||
|
name: "dagre",
|
||||||
|
rankDir: "TB",
|
||||||
|
nodeSep: 150
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
})
|
||||||
|
|
||||||
cyInstance.on("add", () => {
|
for (const node of nodes) {
|
||||||
cyInstance
|
cyInstance.add({
|
||||||
.makeLayout({
|
group: 'nodes',
|
||||||
name: "dagre",
|
data: { ...node }
|
||||||
rankDir: "TB",
|
})
|
||||||
nodeSep: 150
|
}
|
||||||
})
|
|
||||||
.run()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
cyInstance.add({
|
||||||
|
group: 'edges',
|
||||||
|
data: { ...edge }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let refElement = null
|
||||||
|
let cyInstance: cytoscape.Core = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph" bind:this={refElement}>
|
<div class="cy-graph" {style} bind:this={refElement}>
|
||||||
{#if cyInstance}
|
|
||||||
<slot></slot>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.graph {
|
.cy-graph {
|
||||||
background: white;
|
background: white;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, getContext } from 'svelte'
|
import { onMount, getContext } from 'svelte'
|
||||||
import { GRAPH_STATE, type GraphContext } from './Graph.svelte';
|
import { GRAPH_STATE, type GraphContext } from './Graph.svelte';
|
||||||
|
import type { EdgeDataDefinition } from 'cytoscape';
|
||||||
|
|
||||||
export let edge
|
export let edge: EdgeDataDefinition
|
||||||
|
|
||||||
const { getCyInstance } = getContext(GRAPH_STATE) as GraphContext;
|
const { getCyInstance } = getContext(GRAPH_STATE) as GraphContext;
|
||||||
const cyInstance = getCyInstance()
|
const cyInstance = getCyInstance()
|
||||||
|
|
||||||
cyInstance.add({
|
cyInstance.add({
|
||||||
group: 'edges',
|
group: 'edges',
|
||||||
id: edge.id,
|
|
||||||
data: { ...edge }
|
data: { ...edge }
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, getContext } from 'svelte'
|
import { onMount, getContext } from 'svelte'
|
||||||
import { GRAPH_STATE, type GraphContext } from './Graph.svelte';
|
import { GRAPH_STATE, type GraphContext } from './Graph.svelte';
|
||||||
|
import type { NodeDataDefinition } from 'cytoscape';
|
||||||
|
|
||||||
export let node
|
export let node: NodeDataDefinition
|
||||||
|
|
||||||
const { getCyInstance } = getContext(GRAPH_STATE) as GraphContext
|
const { getCyInstance } = getContext(GRAPH_STATE) as GraphContext
|
||||||
const cyInstance = getCyInstance()
|
const cyInstance = getCyInstance()
|
||||||
|
|
||||||
cyInstance.add({
|
cyInstance.add({
|
||||||
group: 'nodes',
|
group: 'nodes',
|
||||||
id: node.id,
|
data: { ...node }
|
||||||
data: { ...node }
|
})
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
export default [
|
import type { Stylesheet } from "cytoscape";
|
||||||
|
|
||||||
|
const styles: Stylesheet[] = [
|
||||||
{
|
{
|
||||||
selector: 'node',
|
selector: 'node',
|
||||||
style: {
|
style: {
|
||||||
'width': '50',
|
'width': '50',
|
||||||
'height': '50',
|
'height': '50',
|
||||||
|
'font-family': 'Arial',
|
||||||
'font-size': '18',
|
'font-size': '18',
|
||||||
'font-weight': 'bold',
|
'font-weight': 'normal',
|
||||||
'content': `data(label)`,
|
'content': `data(label)`,
|
||||||
'text-valign': 'center',
|
'text-valign': 'center',
|
||||||
'text-wrap': 'wrap',
|
'text-wrap': 'wrap',
|
||||||
'text-max-width': '140',
|
'text-max-width': '140',
|
||||||
'background-color': 'gold',
|
'background-color': '#60a5fa',
|
||||||
'border-color': 'orange',
|
'border-color': '#2563eb',
|
||||||
'border-width': '3',
|
'border-width': '3',
|
||||||
'color': 'darkred'
|
'color': '#1d3660'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
selector: 'node:selected',
|
selector: 'node:selected',
|
||||||
style: {
|
style: {
|
||||||
'background-color': 'darkred',
|
'background-color': '#f97316',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
'border-color': 'darkred',
|
'border-color': '#ea580c',
|
||||||
'line-color': '#0e76ba',
|
'line-color': '#0e76ba',
|
||||||
'target-arrow-color': '#0e76ba'
|
'target-arrow-color': '#0e76ba'
|
||||||
}
|
}
|
||||||
@@ -36,8 +39,8 @@ export default [
|
|||||||
'text-background-padding': '3',
|
'text-background-padding': '3',
|
||||||
'width': '3',
|
'width': '3',
|
||||||
'target-arrow-shape': 'triangle',
|
'target-arrow-shape': 'triangle',
|
||||||
'line-color': 'darkred',
|
'line-color': '#1d4ed8',
|
||||||
'target-arrow-color': 'darkred',
|
'target-arrow-color': '#1d4ed8',
|
||||||
'font-weight': 'bold'
|
'font-weight': 'bold'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -55,3 +58,4 @@ export default [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
export default styles;
|
||||||
|
|||||||
74
src/lib/stores/journeyStates.ts
Normal file
74
src/lib/stores/journeyStates.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
|
import type { DragItemID, IDragItem } from './layoutStates';
|
||||||
|
import type { LGraphNode, NodeID } from '@litegraph-ts/core';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A "journey" is like browser history for prompts, except organized in a
|
||||||
|
* tree-like graph. It lets you save incremental changes to your workflow and
|
||||||
|
* jump between past and present sets of parameters.
|
||||||
|
*/
|
||||||
|
export type JourneyState = {
|
||||||
|
/*
|
||||||
|
* Selected drag items.
|
||||||
|
* NOTE: Order is important, for node grouping actions.
|
||||||
|
*/
|
||||||
|
currentJourney: DragItemID[],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hovered drag items.
|
||||||
|
*/
|
||||||
|
currentHovered: Set<DragItemID>,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Selected LGraphNodes inside the litegraph canvas.
|
||||||
|
* NOTE: Order is important, for node grouping actions.
|
||||||
|
*/
|
||||||
|
currentJourneyNodes: LGraphNode[],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Currently hovered nodes.
|
||||||
|
*/
|
||||||
|
currentHoveredNodes: Set<NodeID>
|
||||||
|
}
|
||||||
|
|
||||||
|
type JourneyStateOps = {
|
||||||
|
clear: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WritableJourneyStateStore = Writable<JourneyState> & JourneyStateOps;
|
||||||
|
|
||||||
|
function create() {
|
||||||
|
const store: Writable<JourneyState> = writable(
|
||||||
|
{
|
||||||
|
currentJourney: [],
|
||||||
|
currentJourneyNodes: [],
|
||||||
|
currentHovered: new Set(),
|
||||||
|
currentHoveredNodes: new Set(),
|
||||||
|
})
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
store.set({
|
||||||
|
currentJourney: [],
|
||||||
|
currentJourneyNodes: [],
|
||||||
|
currentHovered: new Set(),
|
||||||
|
currentHoveredNodes: new Set(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...store,
|
||||||
|
clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JourneyStateStaticOps = {
|
||||||
|
create: () => WritableJourneyStateStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// These will be attached to workflows.
|
||||||
|
const ops: JourneyStateStaticOps = {
|
||||||
|
create
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ops
|
||||||
@@ -12,6 +12,8 @@ import type { SerializedAppState, SerializedPrompt } from '$lib/components/Comfy
|
|||||||
import type ComfyReceiveOutputNode from '$lib/nodes/actions/ComfyReceiveOutputNode';
|
import type ComfyReceiveOutputNode from '$lib/nodes/actions/ComfyReceiveOutputNode';
|
||||||
import type { ComfyBoxPromptExtraData, PromptID } from '$lib/api';
|
import type { ComfyBoxPromptExtraData, PromptID } from '$lib/api';
|
||||||
import type { ComfyAPIPromptErrorResponse, ComfyExecutionError } from '$lib/apiErrors';
|
import type { ComfyAPIPromptErrorResponse, ComfyExecutionError } from '$lib/apiErrors';
|
||||||
|
import type { WritableJourneyStateStore } from './journeyState';
|
||||||
|
import journeyStates from './journeyStates';
|
||||||
|
|
||||||
type ActiveCanvas = {
|
type ActiveCanvas = {
|
||||||
canvas: LGraphCanvas | null;
|
canvas: LGraphCanvas | null;
|
||||||
@@ -115,7 +117,12 @@ export class ComfyBoxWorkflow {
|
|||||||
/*
|
/*
|
||||||
* Completed queue entry ID that holds the last validation/execution error.
|
* Completed queue entry ID that holds the last validation/execution error.
|
||||||
*/
|
*/
|
||||||
lastError?: PromptID
|
lastError?: PromptID;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Saved prompt history ("journey") for this workflow
|
||||||
|
*/
|
||||||
|
journey: WritableJourneyStateStore;
|
||||||
|
|
||||||
get layout(): WritableLayoutStateStore | null {
|
get layout(): WritableLayoutStateStore | null {
|
||||||
return layoutStates.getLayout(this.id)
|
return layoutStates.getLayout(this.id)
|
||||||
@@ -133,6 +140,7 @@ export class ComfyBoxWorkflow {
|
|||||||
title,
|
title,
|
||||||
}
|
}
|
||||||
this.graph = new ComfyGraph(this.id);
|
this.graph = new ComfyGraph(this.id);
|
||||||
|
this.journey = journeyStates.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyModified() {
|
notifyModified() {
|
||||||
|
|||||||
Reference in New Issue
Block a user