diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
index c66d7cabe..c2596a1b4 100644
--- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue
+++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
@@ -114,6 +114,18 @@
:lod-level="lodLevel"
:image-urls="nodeImageUrls"
/>
+
+
+
![preview]()
+
@@ -138,6 +150,7 @@ import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
+import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
import { ExecutedWsMessage } from '@/schemas/apiSchema'
import { app } from '@/scripts/app'
import { useExecutionStore } from '@/stores/executionStore'
@@ -312,6 +325,14 @@ const separatorClasses =
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full'
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
+const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
+ nodeData.id,
+ {
+ isMinimalLOD,
+ isCollapsed
+ }
+)
+
// Common condition computations to avoid repetition
const shouldShowWidgets = computed(
() => shouldRenderWidgets.value && nodeData.widgets?.length
diff --git a/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts b/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts
new file mode 100644
index 000000000..427cf20c0
--- /dev/null
+++ b/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts
@@ -0,0 +1,51 @@
+import { storeToRefs } from 'pinia'
+import { type Ref, computed } from 'vue'
+
+import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
+import { useNodeOutputStore } from '@/stores/imagePreviewStore'
+
+export const useNodePreviewState = (
+ nodeId: string,
+ options?: {
+ isMinimalLOD?: Ref
+ isCollapsed?: Ref
+ }
+) => {
+ const workflowStore = useWorkflowStore()
+ const { nodePreviewImages } = storeToRefs(useNodeOutputStore())
+
+ const locatorId = computed(() => workflowStore.nodeIdToNodeLocatorId(nodeId))
+
+ const previewUrls = computed(() => {
+ const key = locatorId.value
+ if (!key) return undefined
+ const urls = nodePreviewImages.value[key]
+ return urls?.length ? urls : undefined
+ })
+
+ const hasPreview = computed(() => !!previewUrls.value?.length)
+
+ const latestPreviewUrl = computed(() => {
+ const urls = previewUrls.value
+ return urls?.length ? urls.at(-1) : ''
+ })
+
+ const shouldShowPreviewImg = computed(() => {
+ if (!options?.isMinimalLOD || !options?.isCollapsed) {
+ return hasPreview.value
+ }
+ return (
+ !options.isMinimalLOD.value &&
+ !options.isCollapsed.value &&
+ hasPreview.value
+ )
+ })
+
+ return {
+ locatorId,
+ previewUrls,
+ hasPreview,
+ latestPreviewUrl,
+ shouldShowPreviewImg
+ }
+}
diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts
index 15920c1bd..4fc3adb80 100644
--- a/src/stores/imagePreviewStore.ts
+++ b/src/stores/imagePreviewStore.ts
@@ -1,3 +1,4 @@
+import { useTimeoutFn } from '@vueuse/core'
import { defineStore } from 'pinia'
import { ref } from 'vue'
@@ -19,6 +20,8 @@ import type { NodeLocatorId } from '@/types/nodeIdentification'
import { parseFilePath } from '@/utils/formatUtil'
import { isVideoNode } from '@/utils/litegraphUtil'
+const PREVIEW_REVOKE_DELAY_MS = 400
+
const createOutputs = (
filenames: string[],
type: ResultItemType,
@@ -40,9 +43,26 @@ interface SetOutputOptions {
export const useNodeOutputStore = defineStore('nodeOutput', () => {
const { nodeIdToNodeLocatorId } = useWorkflowStore()
const { executionIdToNodeLocatorId } = useExecutionStore()
+ const scheduledRevoke: Record void }> = {}
+
+ function scheduleRevoke(locator: NodeLocatorId, cb: () => void) {
+ scheduledRevoke[locator]?.stop()
+
+ const { stop } = useTimeoutFn(() => {
+ delete scheduledRevoke[locator]
+ cb()
+ }, PREVIEW_REVOKE_DELAY_MS)
+
+ scheduledRevoke[locator] = { stop }
+ }
const nodeOutputs = ref>({})
+ // Reactive state for node preview images - mirrors app.nodePreviewImages
+ const nodePreviewImages = ref>(
+ app.nodePreviewImages || {}
+ )
+
function getNodeOutputs(
node: LGraphNode
): ExecutedWsMessage['output'] | undefined {
@@ -196,8 +216,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
) {
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
if (!nodeLocatorId) return
-
+ if (scheduledRevoke[nodeLocatorId]) {
+ scheduledRevoke[nodeLocatorId].stop()
+ delete scheduledRevoke[nodeLocatorId]
+ }
app.nodePreviewImages[nodeLocatorId] = previewImages
+ nodePreviewImages.value[nodeLocatorId] = previewImages
}
/**
@@ -212,7 +236,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
previewImages: string[]
) {
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
+ if (scheduledRevoke[nodeLocatorId]) {
+ scheduledRevoke[nodeLocatorId].stop()
+ delete scheduledRevoke[nodeLocatorId]
+ }
app.nodePreviewImages[nodeLocatorId] = previewImages
+ nodePreviewImages.value[nodeLocatorId] = previewImages
}
/**
@@ -224,8 +253,9 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
function revokePreviewsByExecutionId(executionId: string) {
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
if (!nodeLocatorId) return
-
- revokePreviewsByLocatorId(nodeLocatorId)
+ scheduleRevoke(nodeLocatorId, () =>
+ revokePreviewsByLocatorId(nodeLocatorId)
+ )
}
/**
@@ -243,6 +273,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
}
delete app.nodePreviewImages[nodeLocatorId]
+ delete nodePreviewImages.value[nodeLocatorId]
}
/**
@@ -259,6 +290,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
}
}
app.nodePreviewImages = {}
+ nodePreviewImages.value = {}
}
/**
@@ -293,6 +325,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
// Clear preview images
if (app.nodePreviewImages[nodeLocatorId]) {
delete app.nodePreviewImages[nodeLocatorId]
+ delete nodePreviewImages.value[nodeLocatorId]
}
return hadOutputs
@@ -318,6 +351,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
removeNodeOutputs,
// State
- nodeOutputs
+ nodeOutputs,
+ nodePreviewImages
}
})
diff --git a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts
index 6e34e2450..07c0a3081 100644
--- a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts
+++ b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts
@@ -56,6 +56,13 @@ vi.mock(
})
)
+vi.mock('@/renderer/extensions/vueNodes/preview/useNodePreviewState', () => ({
+ useNodePreviewState: vi.fn(() => ({
+ latestPreviewUrl: computed(() => ''),
+ shouldShowPreviewImg: computed(() => false)
+ }))
+}))
+
const i18n = createI18n({
legacy: false,
locale: 'en',