Skip to content

Commit 0483630

Browse files
christian-byrneclaudeDrJKL
authored
Show sampling previews on Vue nodes (#5579)
* refactor: simplify preview state provider - Remove unnecessary event listeners and manual syncing - Use computed() to directly reference app.nodePreviewImages - Eliminate data duplication and any types - Rely on Vue's reactivity for automatic updates - Follow established patterns from execution state provider * feat: optimize Vue node preview image display with reactive store - Move preview display logic from inline ternaries to computed properties - Add useNodePreviewState composable for preview state management - Implement reactive store approach using Pinia storeToRefs - Use VueUse useTimeoutFn for modern timeout management instead of window.setTimeout - Add v-memo optimization for preview image template rendering - Maintain proper sync between app.nodePreviewImages and reactive store state 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: update props usage for Vue 3.5 destructured props syntax * [refactor] improve code style and architecture based on review feedback - Replace inject pattern with direct store access in useNodePreviewState - Use optional chaining for more concise conditional checks - Use modern Array.at(-1) for accessing last element - Remove provide/inject for nodePreviewImages in favor of direct store refs - Update preview image styling: remove rounded borders, use flexible height - Simplify scheduleRevoke function with optional chaining Co-authored-by: DrJKL <[email protected]> * [cleanup] remove unused NodePreviewImagesKey injection key Addresses knip unused export warning after switching from provide/inject to direct store access pattern. * [test] add mock for useNodePreviewState in LGraphNode test Fixes test failure after adding preview functionality to LGraphNode component. * [fix] update workflowStore import path after rebase Updates import to new location: @/platform/workflow/management/stores/workflowStore --------- Co-authored-by: Claude <[email protected]> Co-authored-by: DrJKL <[email protected]>
1 parent 15cffe9 commit 0483630

File tree

4 files changed

+117
-4
lines changed

4 files changed

+117
-4
lines changed

src/renderer/extensions/vueNodes/components/LGraphNode.vue

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@
114114
:lod-level="lodLevel"
115115
:image-urls="nodeImageUrls"
116116
/>
117+
<!-- Live preview image -->
118+
<div
119+
v-if="shouldShowPreviewImg"
120+
v-memo="[latestPreviewUrl]"
121+
class="px-4"
122+
>
123+
<img
124+
:src="latestPreviewUrl"
125+
alt="preview"
126+
class="w-full max-h-64 object-contain"
127+
/>
128+
</div>
117129
</div>
118130
</template>
119131
</div>
@@ -138,6 +150,7 @@ import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
138150
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
139151
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
140152
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
153+
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
141154
import { ExecutedWsMessage } from '@/schemas/apiSchema'
142155
import { app } from '@/scripts/app'
143156
import { useExecutionStore } from '@/stores/executionStore'
@@ -312,6 +325,14 @@ const separatorClasses =
312325
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full'
313326
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
314327
328+
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
329+
nodeData.id,
330+
{
331+
isMinimalLOD,
332+
isCollapsed
333+
}
334+
)
335+
315336
// Common condition computations to avoid repetition
316337
const shouldShowWidgets = computed(
317338
() => shouldRenderWidgets.value && nodeData.widgets?.length
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { storeToRefs } from 'pinia'
2+
import { type Ref, computed } from 'vue'
3+
4+
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
5+
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
6+
7+
export const useNodePreviewState = (
8+
nodeId: string,
9+
options?: {
10+
isMinimalLOD?: Ref<boolean>
11+
isCollapsed?: Ref<boolean>
12+
}
13+
) => {
14+
const workflowStore = useWorkflowStore()
15+
const { nodePreviewImages } = storeToRefs(useNodeOutputStore())
16+
17+
const locatorId = computed(() => workflowStore.nodeIdToNodeLocatorId(nodeId))
18+
19+
const previewUrls = computed(() => {
20+
const key = locatorId.value
21+
if (!key) return undefined
22+
const urls = nodePreviewImages.value[key]
23+
return urls?.length ? urls : undefined
24+
})
25+
26+
const hasPreview = computed(() => !!previewUrls.value?.length)
27+
28+
const latestPreviewUrl = computed(() => {
29+
const urls = previewUrls.value
30+
return urls?.length ? urls.at(-1) : ''
31+
})
32+
33+
const shouldShowPreviewImg = computed(() => {
34+
if (!options?.isMinimalLOD || !options?.isCollapsed) {
35+
return hasPreview.value
36+
}
37+
return (
38+
!options.isMinimalLOD.value &&
39+
!options.isCollapsed.value &&
40+
hasPreview.value
41+
)
42+
})
43+
44+
return {
45+
locatorId,
46+
previewUrls,
47+
hasPreview,
48+
latestPreviewUrl,
49+
shouldShowPreviewImg
50+
}
51+
}

src/stores/imagePreviewStore.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useTimeoutFn } from '@vueuse/core'
12
import { defineStore } from 'pinia'
23
import { ref } from 'vue'
34

@@ -19,6 +20,8 @@ import type { NodeLocatorId } from '@/types/nodeIdentification'
1920
import { parseFilePath } from '@/utils/formatUtil'
2021
import { isVideoNode } from '@/utils/litegraphUtil'
2122

23+
const PREVIEW_REVOKE_DELAY_MS = 400
24+
2225
const createOutputs = (
2326
filenames: string[],
2427
type: ResultItemType,
@@ -40,9 +43,26 @@ interface SetOutputOptions {
4043
export const useNodeOutputStore = defineStore('nodeOutput', () => {
4144
const { nodeIdToNodeLocatorId } = useWorkflowStore()
4245
const { executionIdToNodeLocatorId } = useExecutionStore()
46+
const scheduledRevoke: Record<NodeLocatorId, { stop: () => void }> = {}
47+
48+
function scheduleRevoke(locator: NodeLocatorId, cb: () => void) {
49+
scheduledRevoke[locator]?.stop()
50+
51+
const { stop } = useTimeoutFn(() => {
52+
delete scheduledRevoke[locator]
53+
cb()
54+
}, PREVIEW_REVOKE_DELAY_MS)
55+
56+
scheduledRevoke[locator] = { stop }
57+
}
4358

4459
const nodeOutputs = ref<Record<string, ExecutedWsMessage['output']>>({})
4560

61+
// Reactive state for node preview images - mirrors app.nodePreviewImages
62+
const nodePreviewImages = ref<Record<string, string[]>>(
63+
app.nodePreviewImages || {}
64+
)
65+
4666
function getNodeOutputs(
4767
node: LGraphNode
4868
): ExecutedWsMessage['output'] | undefined {
@@ -196,8 +216,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
196216
) {
197217
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
198218
if (!nodeLocatorId) return
199-
219+
if (scheduledRevoke[nodeLocatorId]) {
220+
scheduledRevoke[nodeLocatorId].stop()
221+
delete scheduledRevoke[nodeLocatorId]
222+
}
200223
app.nodePreviewImages[nodeLocatorId] = previewImages
224+
nodePreviewImages.value[nodeLocatorId] = previewImages
201225
}
202226

203227
/**
@@ -212,7 +236,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
212236
previewImages: string[]
213237
) {
214238
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
239+
if (scheduledRevoke[nodeLocatorId]) {
240+
scheduledRevoke[nodeLocatorId].stop()
241+
delete scheduledRevoke[nodeLocatorId]
242+
}
215243
app.nodePreviewImages[nodeLocatorId] = previewImages
244+
nodePreviewImages.value[nodeLocatorId] = previewImages
216245
}
217246

218247
/**
@@ -224,8 +253,9 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
224253
function revokePreviewsByExecutionId(executionId: string) {
225254
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
226255
if (!nodeLocatorId) return
227-
228-
revokePreviewsByLocatorId(nodeLocatorId)
256+
scheduleRevoke(nodeLocatorId, () =>
257+
revokePreviewsByLocatorId(nodeLocatorId)
258+
)
229259
}
230260

231261
/**
@@ -243,6 +273,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
243273
}
244274

245275
delete app.nodePreviewImages[nodeLocatorId]
276+
delete nodePreviewImages.value[nodeLocatorId]
246277
}
247278

248279
/**
@@ -259,6 +290,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
259290
}
260291
}
261292
app.nodePreviewImages = {}
293+
nodePreviewImages.value = {}
262294
}
263295

264296
/**
@@ -293,6 +325,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
293325
// Clear preview images
294326
if (app.nodePreviewImages[nodeLocatorId]) {
295327
delete app.nodePreviewImages[nodeLocatorId]
328+
delete nodePreviewImages.value[nodeLocatorId]
296329
}
297330

298331
return hadOutputs
@@ -318,6 +351,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
318351
removeNodeOutputs,
319352

320353
// State
321-
nodeOutputs
354+
nodeOutputs,
355+
nodePreviewImages
322356
}
323357
})

tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ vi.mock(
5656
})
5757
)
5858

59+
vi.mock('@/renderer/extensions/vueNodes/preview/useNodePreviewState', () => ({
60+
useNodePreviewState: vi.fn(() => ({
61+
latestPreviewUrl: computed(() => ''),
62+
shouldShowPreviewImg: computed(() => false)
63+
}))
64+
}))
65+
5966
const i18n = createI18n({
6067
legacy: false,
6168
locale: 'en',

0 commit comments

Comments
 (0)