From 3e677e3dbe53cfdd964f38902cc66f043d6a9b3c Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Fri, 19 Sep 2025 09:51:04 -0500 Subject: [PATCH] Add show recommended button, preview work Adds the framework for a system to automate display of a curated list of recommended widgets to the node. As part of this, a return to display of "image previews" was made. This code is causing lots of problems. Much of the logic is dependent upon the actual node going through the draw loop. As nodes in the subgraph don't receive redraws, there's lots of issues with managing the initial display and ensuring that an initial draw occurs. This commit includes support for updating previews, but is more brittle than I would like. --- src/components/selectionbar/SubgraphNode.vue | 27 +++++++++++++++- src/scripts/proxyWidget.ts | 33 ++++++++++++++++---- src/stores/imagePreviewStore.ts | 17 +++------- src/stores/workflowStore.ts | 19 ++++++++++- 4 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/components/selectionbar/SubgraphNode.vue b/src/components/selectionbar/SubgraphNode.vue index 89762d6fc..c23b0488f 100644 --- a/src/components/selectionbar/SubgraphNode.vue +++ b/src/components/selectionbar/SubgraphNode.vue @@ -143,6 +143,28 @@ function hideAll() { node.properties.proxyWidgets = JSON.stringify(toKeep) triggerUpdate.value++ } +const recommendedNodes = [ + 'CLIPTextEncode', + 'LoadImage', + 'SaveImage', + 'PreviewImage' +] +const recommendedWidgetNames = ['seed'] +function showRecommended() { + const node = activeNode.value + if (!node) return //Not reachable + const recommendedWidgets = filteredCandidates.value.filter( + ([node, widget]: WidgetItem) => + recommendedNodes.includes(node.type) || + recommendedWidgetNames.includes(widget.name) + ) + node.properties.proxyWidgets = JSON.stringify( + recommendedWidgets.map(([node, widget]) => [`${node.id}`, widget.name]) + ) + triggerUpdate.value++ + //TODO: Add sort step here + //Input should always be before output by default +} const filteredActive = computed(() => { const query = searchQuery.value.toLowerCase() @@ -227,6 +249,9 @@ const filteredActive = computed(() => { /> + + {{ t('subgraphStore.showRecommended') }} @@ -244,7 +269,7 @@ const filteredActive = computed(() => { font-weight: 600; text-transform: uppercase; } -.widgets-section-header a { +a { cursor: pointer; color: var(--color-blue-100, #0b8ce9); text-align: right; diff --git a/src/scripts/proxyWidget.ts b/src/scripts/proxyWidget.ts index dc7292d54..181a45076 100644 --- a/src/scripts/proxyWidget.ts +++ b/src/scripts/proxyWidget.ts @@ -1,3 +1,4 @@ +import { useNodeImage } from '@/composables/node/useNodeImage' import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph' import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets.ts' @@ -6,6 +7,7 @@ import { parseProxyWidgets } from '@/schemas/proxyWidget' import { DOMWidgetImpl } from '@/scripts/domWidget' import { useDomWidgetStore } from '@/stores/domWidgetStore' import { useCanvasStore } from '@/stores/graphStore' +import { useNodeOutputStore } from '@/stores/imagePreviewStore' const originalConfigureAfterSlots = SubgraphNode.prototype._internalConfigureAfterSlots @@ -63,6 +65,7 @@ type Overlay = Partial & { nodeId: string widgetName: string isProxyWidget: boolean + node?: LGraphNode } type ProxyWidget = IBaseWidget & { _overlay: Overlay } function isProxyWidget(w: IBaseWidget): w is ProxyWidget { @@ -87,12 +90,13 @@ function addProxyWidget( width: undefined, computedHeight: undefined, afterQueued: undefined, - onRemove: undefined, - node: subgraphNode + onRemove: undefined } return addProxyFromOverlay(subgraphNode, overlay) } -function resolveLinkedWidget(overlay: Overlay): IBaseWidget | undefined { +function resolveLinkedWidget( + overlay: Overlay +): [LGraphNode | undefined, IBaseWidget | undefined] { const { graph, nodeId, widgetName } = overlay let g: LGraph | undefined = graph let n: LGraphNode | SubgraphNode | undefined = undefined @@ -100,12 +104,29 @@ function resolveLinkedWidget(overlay: Overlay): IBaseWidget | undefined { n = g?._nodes_by_id?.[id] g = n?.isSubgraphNode?.() ? n.subgraph : undefined } - if (!n) return undefined - return n.widgets?.find((w: IBaseWidget) => w.name === widgetName) + if (!n) return [undefined, undefined] + return [n, n.widgets?.find((w: IBaseWidget) => w.name === widgetName)] } function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) { - let linkedWidget = resolveLinkedWidget(overlay) + let [linkedNode, linkedWidget] = resolveLinkedWidget(overlay) const bw = linkedWidget ?? disconnectedWidget + overlay.node = new Proxy(subgraphNode, { + get(_t, p) { + if (p == 'imgs') { + if (linkedNode) { + const images = + useNodeOutputStore().getNodeOutputs(linkedNode)?.images ?? [] + if (images !== linkedNode.images) { + linkedNode.images = images + useNodeImage(linkedNode).showPreview() + } + return linkedNode.imgs + } + return [] + } + return Reflect.get(subgraphNode, p) + } + }) const handler = Object.fromEntries( ['get', 'set', 'getPrototypeOf', 'ownKeys', 'has'].map((s) => { const func = function (t: object, p: string, ...rest: object[]) { diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts index 6fc8f3076..6747f08f5 100644 --- a/src/stores/imagePreviewStore.ts +++ b/src/stores/imagePreviewStore.ts @@ -1,10 +1,6 @@ import { defineStore } from 'pinia' -import { - LGraphNode, - Subgraph, - SubgraphNode -} from '@/lib/litegraph/src/litegraph' +import { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph' import { ExecutedWsMessage, ResultItem, @@ -37,17 +33,17 @@ interface SetOutputOptions { } export const useNodeOutputStore = defineStore('nodeOutput', () => { - const { nodeIdToNodeLocatorId } = useWorkflowStore() + const { nodeIdToNodeLocatorId, nodeToNodeLocatorId } = useWorkflowStore() const { executionIdToNodeLocatorId } = useExecutionStore() function getNodeOutputs( node: LGraphNode ): ExecutedWsMessage['output'] | undefined { - return app.nodeOutputs[nodeIdToNodeLocatorId(node.id)] + return app.nodeOutputs[nodeToNodeLocatorId(node)] } function getNodePreviews(node: LGraphNode): string[] | undefined { - return app.nodePreviewImages[nodeIdToNodeLocatorId(node.id)] + return app.nodePreviewImages[nodeToNodeLocatorId(node)] } /** @@ -140,10 +136,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { ) { if (!filenames || !node) return - const locatorId = - node.graph instanceof Subgraph - ? nodeIdToNodeLocatorId(node.id, node.graph ?? undefined) - : `${node.id}` + const locatorId = nodeToNodeLocatorId(node) if (!locatorId) return if (typeof filenames === 'string') { setOutputsByLocatorId( diff --git a/src/stores/workflowStore.ts b/src/stores/workflowStore.ts index b0c8e8b37..943787c40 100644 --- a/src/stores/workflowStore.ts +++ b/src/stores/workflowStore.ts @@ -3,7 +3,11 @@ import { defineStore } from 'pinia' import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue' import { t } from '@/i18n' -import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph' +import type { + LGraph, + LGraphNode, + Subgraph +} from '@/lib/litegraph/src/litegraph' import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail' import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema' import type { NodeId } from '@/schemas/comfyWorkflowSchema' @@ -185,6 +189,7 @@ interface WorkflowStore { updateActiveGraph: () => void executionIdToCurrentId: (id: string) => any nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId + nodeToNodeLocatorId: (node: LGraphNode) => NodeLocatorId nodeExecutionIdToNodeLocatorId: ( nodeExecutionId: NodeExecutionId | string ) => NodeLocatorId | null @@ -575,6 +580,17 @@ export const useWorkflowStore = defineStore('workflow', () => { return createNodeLocatorId(targetSubgraph.id, nodeId) } + /** + * Convert a node to a NodeLocatorId + * Does not assume the node resides in the active graph + * @param The actual node instance + * @returns The NodeLocatorId (for root graph nodes, returns the node ID as-is) + */ + const nodeToNodeLocatorId = (node: LGraphNode): NodeLocatorId => { + if (isSubgraph(node.graph)) + return createNodeLocatorId(node.graph.id, node.id) + return String(node.id) + } /** * Convert an execution ID to a NodeLocatorId @@ -717,6 +733,7 @@ export const useWorkflowStore = defineStore('workflow', () => { updateActiveGraph, executionIdToCurrentId, nodeIdToNodeLocatorId, + nodeToNodeLocatorId, nodeExecutionIdToNodeLocatorId, nodeLocatorIdToNodeId, nodeLocatorIdToNodeExecutionId