mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
Update the frontend to support async nodes.
This commit is contained in:
@@ -71,7 +71,6 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
|||||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||||
import { i18n, t } from '@/i18n'
|
import { i18n, t } from '@/i18n'
|
||||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
|
||||||
import { UnauthorizedError, api } from '@/scripts/api'
|
import { UnauthorizedError, api } from '@/scripts/api'
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||||
@@ -189,22 +188,46 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update the progress of the executing node
|
// Update the progress of executing nodes
|
||||||
watch(
|
watch(
|
||||||
() =>
|
() => executionStore.nodeProgressStates,
|
||||||
[executionStore.executingNodeId, executionStore.executingNodeProgress] as [
|
(nodeProgressStates) => {
|
||||||
NodeId | null,
|
// Clear progress for all nodes first
|
||||||
number | null
|
|
||||||
],
|
|
||||||
([executingNodeId, executingNodeProgress]) => {
|
|
||||||
for (const node of comfyApp.graph.nodes) {
|
for (const node of comfyApp.graph.nodes) {
|
||||||
if (node.id == executingNodeId) {
|
node.progress = undefined
|
||||||
node.progress = executingNodeProgress ?? undefined
|
}
|
||||||
} else {
|
|
||||||
node.progress = undefined
|
// Then set progress for nodes with progress states
|
||||||
|
for (const nodeId in nodeProgressStates) {
|
||||||
|
const progressState = nodeProgressStates[nodeId]
|
||||||
|
const node = comfyApp.graph.getNodeById(progressState.display_node_id)
|
||||||
|
|
||||||
|
if (node && progressState) {
|
||||||
|
// Only show progress for running nodes
|
||||||
|
if (progressState.state === 'running') {
|
||||||
|
if (node.progress === undefined || node.progress === 0.0) {
|
||||||
|
console.log(
|
||||||
|
`${Date.now()} Setting progress for node ${node.id} to ${progressState.value / progressState.max}=${progressState.value}/${progressState.max} due to ${nodeId}`
|
||||||
|
)
|
||||||
|
node.progress = progressState.value / progressState.max
|
||||||
|
} else {
|
||||||
|
// Update progress if it was already set
|
||||||
|
console.log(
|
||||||
|
`${Date.now()} Setting progress for node ${node.id} to Math.min(${node.progress}, ${progressState.value / progressState.max}=${progressState.value}/${progressState.max}) due to ${nodeId}`
|
||||||
|
)
|
||||||
|
node.progress = Math.min(
|
||||||
|
node.progress,
|
||||||
|
progressState.value / progressState.max
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// TODO - Do we need to force canvas redraw here?
|
||||||
|
// comfyApp.graph.setDirtyCanvas(true, true)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update node slot errors
|
// Update node slot errors
|
||||||
|
|||||||
@@ -20,33 +20,44 @@ import { useExecutionStore } from '@/stores/executionStore'
|
|||||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||||
|
|
||||||
const modelValue = defineModel<string>({ required: true })
|
const modelValue = defineModel<string>({ required: true })
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
widget?: object
|
widget?: object
|
||||||
|
nodeId: NodeId
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
const isParentNodeExecuting = ref(true)
|
const isParentNodeExecuting = ref(true)
|
||||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||||
|
|
||||||
let executingNodeId: NodeId | null = null
|
let parentNodeId: NodeId | null = null
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
executingNodeId = executionStore.executingNodeId
|
// Get the parent node ID from props if provided
|
||||||
|
// For backward compatibility, fall back to the first executing node
|
||||||
|
parentNodeId = props.nodeId
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for either a new node has starting execution or overall execution ending
|
// Watch for either a new node has starting execution or overall execution ending
|
||||||
const stopWatching = watch(
|
const stopWatching = watch(
|
||||||
[() => executionStore.executingNode, () => executionStore.isIdle],
|
[() => executionStore.executingNodeIds, () => executionStore.isIdle],
|
||||||
() => {
|
() => {
|
||||||
|
if (executionStore.isIdle) {
|
||||||
|
isParentNodeExecuting.value = false
|
||||||
|
stopWatching()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if parent node is no longer in the executing nodes list
|
||||||
if (
|
if (
|
||||||
executionStore.isIdle ||
|
parentNodeId &&
|
||||||
(executionStore.executingNode &&
|
!executionStore.executingNodeIds.includes(parentNodeId)
|
||||||
executionStore.executingNode.id !== executingNodeId)
|
|
||||||
) {
|
) {
|
||||||
isParentNodeExecuting.value = false
|
isParentNodeExecuting.value = false
|
||||||
stopWatching()
|
stopWatching()
|
||||||
}
|
}
|
||||||
if (!executingNodeId) {
|
|
||||||
executingNodeId = executionStore.executingNodeId
|
// Set parent node ID if not set yet
|
||||||
|
if (!parentNodeId && executionStore.executingNodeIds.length > 0) {
|
||||||
|
parentNodeId = executionStore.executingNodeIds[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTitle } from '@vueuse/core'
|
import { useTitle } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { t } from '@/i18n'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||||
@@ -36,11 +37,35 @@ export const useBrowserTabTitle = () => {
|
|||||||
: DEFAULT_TITLE
|
: DEFAULT_TITLE
|
||||||
})
|
})
|
||||||
|
|
||||||
const nodeExecutionTitle = computed(() =>
|
const nodeExecutionTitle = computed(() => {
|
||||||
executionStore.executingNode && executionStore.executingNodeProgress
|
// Check if any nodes are in progress
|
||||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
const nodeProgressEntries = Object.entries(
|
||||||
: ''
|
executionStore.nodeProgressStates
|
||||||
)
|
)
|
||||||
|
const runningNodes = nodeProgressEntries.filter(
|
||||||
|
([_, state]) => state.state === 'running'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (runningNodes.length > 0) {
|
||||||
|
// If multiple nodes are running
|
||||||
|
if (runningNodes.length > 1) {
|
||||||
|
return `${executionText.value}[${runningNodes.length} ${t('g.nodesRunning', 'nodes running')}]`
|
||||||
|
}
|
||||||
|
// If only one node is running
|
||||||
|
else {
|
||||||
|
const [nodeId, state] = runningNodes[0]
|
||||||
|
const progress = Math.round((state.value / state.max) * 100)
|
||||||
|
const nodeType =
|
||||||
|
executionStore.activePrompt?.workflow?.changeTracker?.activeState?.nodes.find(
|
||||||
|
(n) => String(n.id) === nodeId
|
||||||
|
)?.type || 'Node'
|
||||||
|
|
||||||
|
return `${executionText.value}[${progress}%] ${nodeType}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
const workflowTitle = computed(
|
const workflowTitle = computed(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export const useTextPreviewWidget = (
|
|||||||
name: inputSpec.name,
|
name: inputSpec.name,
|
||||||
component: TextPreviewWidget,
|
component: TextPreviewWidget,
|
||||||
inputSpec,
|
inputSpec,
|
||||||
|
componentProps: {
|
||||||
|
nodeId: node.id
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
getValue: () => widgetValue.value,
|
getValue: () => widgetValue.value,
|
||||||
setValue: (value: string | object) => {
|
setValue: (value: string | object) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@/schemas/comfyWorkflowSchema'
|
} from '@/schemas/comfyWorkflowSchema'
|
||||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { useToastStore } from '@/stores/toastStore'
|
import { useToastStore } from '@/stores/toastStore'
|
||||||
import { useWidgetStore } from '@/stores/widgetStore'
|
import { useWidgetStore } from '@/stores/widgetStore'
|
||||||
@@ -1178,9 +1179,10 @@ export class GroupNodeHandler {
|
|||||||
node.onDrawForeground = function (ctx) {
|
node.onDrawForeground = function (ctx) {
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
onDrawForeground?.apply?.(this, arguments)
|
onDrawForeground?.apply?.(this, arguments)
|
||||||
|
const executionStore = useExecutionStore()
|
||||||
if (
|
if (
|
||||||
// @ts-expect-error fixme ts strict error
|
executionStore.nodeProgressStates[this.id] &&
|
||||||
+app.runningNodeId === this.id &&
|
executionStore.nodeProgressStates[this.id].state === 'running' &&
|
||||||
this.runningInternalNodeId !== null
|
this.runningInternalNodeId !== null
|
||||||
) {
|
) {
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
@@ -1275,6 +1277,45 @@ export class GroupNodeHandler {
|
|||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
(_, id) => id
|
(_, id) => id
|
||||||
)
|
)
|
||||||
|
/*
|
||||||
|
// Handle progress_state events for multiple executing nodes
|
||||||
|
const progress_state = handleEvent.call(
|
||||||
|
this,
|
||||||
|
'progress_state',
|
||||||
|
(d) => {
|
||||||
|
// Check if any of our inner nodes are in this progress state update
|
||||||
|
for (const nodeId in d.nodes) {
|
||||||
|
const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == nodeId);
|
||||||
|
if (innerNodeIndex > -1) return nodeId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(d, id, node) => {
|
||||||
|
// Create a new progress_state event with just our group node
|
||||||
|
const newProgressState = { ...d };
|
||||||
|
newProgressState.nodes = { [id]: {
|
||||||
|
node: id,
|
||||||
|
state: 'running',
|
||||||
|
value: 0,
|
||||||
|
max: 1,
|
||||||
|
prompt_id: d.prompt_id
|
||||||
|
}};
|
||||||
|
|
||||||
|
// If we have a specific running internal node, update its state
|
||||||
|
if (node.runningInternalNodeId !== null) {
|
||||||
|
const innerNodeId = this.innerNodes[node.runningInternalNodeId].id;
|
||||||
|
if (d.nodes[innerNodeId]) {
|
||||||
|
newProgressState.nodes[id] = {
|
||||||
|
...d.nodes[innerNodeId],
|
||||||
|
node: id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newProgressState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
const executed = handleEvent.call(
|
const executed = handleEvent.call(
|
||||||
this,
|
this,
|
||||||
@@ -1294,6 +1335,7 @@ export class GroupNodeHandler {
|
|||||||
this.node.onRemoved = function () {
|
this.node.onRemoved = function () {
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
onRemoved?.apply(this, arguments)
|
onRemoved?.apply(this, arguments)
|
||||||
|
// api.removeEventListener('progress_state', progress_state)
|
||||||
api.removeEventListener('executing', executing)
|
api.removeEventListener('executing', executing)
|
||||||
api.removeEventListener('executed', executed)
|
api.removeEventListener('executed', executed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,8 @@
|
|||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"imageUrl": "Image URL",
|
"imageUrl": "Image URL",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"copyURL": "Copy URL"
|
"copyURL": "Copy URL",
|
||||||
|
"nodesRunning": "nodes running"
|
||||||
},
|
},
|
||||||
"manager": {
|
"manager": {
|
||||||
"title": "Custom Nodes Manager",
|
"title": "Custom Nodes Manager",
|
||||||
|
|||||||
@@ -45,6 +45,22 @@ const zProgressWsMessage = z.object({
|
|||||||
node: zNodeId
|
node: zNodeId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const zNodeProgressState = z.object({
|
||||||
|
value: z.number(),
|
||||||
|
max: z.number(),
|
||||||
|
state: z.enum(['pending', 'running', 'finished', 'error']),
|
||||||
|
node_id: zNodeId,
|
||||||
|
prompt_id: zPromptId,
|
||||||
|
display_node_id: zNodeId.optional(),
|
||||||
|
parent_node_id: zNodeId.optional(),
|
||||||
|
real_node_id: zNodeId.optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
const zProgressStateWsMessage = z.object({
|
||||||
|
prompt_id: zPromptId,
|
||||||
|
nodes: z.record(zNodeId, zNodeProgressState)
|
||||||
|
})
|
||||||
|
|
||||||
const zExecutingWsMessage = z.object({
|
const zExecutingWsMessage = z.object({
|
||||||
node: zNodeId,
|
node: zNodeId,
|
||||||
display_node: zNodeId,
|
display_node: zNodeId,
|
||||||
@@ -129,6 +145,8 @@ export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
|
|||||||
export type DisplayComponentWsMessage = z.infer<
|
export type DisplayComponentWsMessage = z.infer<
|
||||||
typeof zDisplayComponentWsMessage
|
typeof zDisplayComponentWsMessage
|
||||||
>
|
>
|
||||||
|
export type NodeProgressState = z.infer<typeof zNodeProgressState>
|
||||||
|
export type ProgressStateWsMessage = z.infer<typeof zProgressStateWsMessage>
|
||||||
// End of ws messages
|
// End of ws messages
|
||||||
|
|
||||||
const zPromptInputItem = z.object({
|
const zPromptInputItem = z.object({
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
LogsRawResponse,
|
LogsRawResponse,
|
||||||
LogsWsMessage,
|
LogsWsMessage,
|
||||||
PendingTaskItem,
|
PendingTaskItem,
|
||||||
|
ProgressStateWsMessage,
|
||||||
ProgressTextWsMessage,
|
ProgressTextWsMessage,
|
||||||
ProgressWsMessage,
|
ProgressWsMessage,
|
||||||
PromptResponse,
|
PromptResponse,
|
||||||
@@ -103,7 +104,17 @@ interface BackendApiCalls {
|
|||||||
logs: LogsWsMessage
|
logs: LogsWsMessage
|
||||||
/** Binary preview/progress data */
|
/** Binary preview/progress data */
|
||||||
b_preview: Blob
|
b_preview: Blob
|
||||||
|
/** Binary preview with metadata (node_id, prompt_id) */
|
||||||
|
b_preview_with_metadata: {
|
||||||
|
blob: Blob
|
||||||
|
nodeId: string
|
||||||
|
parentNodeId: string
|
||||||
|
displayNodeId: string
|
||||||
|
realNodeId: string
|
||||||
|
promptId: string
|
||||||
|
}
|
||||||
progress_text: ProgressTextWsMessage
|
progress_text: ProgressTextWsMessage
|
||||||
|
progress_state: ProgressStateWsMessage
|
||||||
display_component: DisplayComponentWsMessage
|
display_component: DisplayComponentWsMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,6 +443,33 @@ export class ComfyApi extends EventTarget {
|
|||||||
})
|
})
|
||||||
this.dispatchCustomEvent('b_preview', imageBlob)
|
this.dispatchCustomEvent('b_preview', imageBlob)
|
||||||
break
|
break
|
||||||
|
case 4:
|
||||||
|
// PREVIEW_IMAGE_WITH_METADATA
|
||||||
|
const decoder4 = new TextDecoder()
|
||||||
|
const metadataLength = view.getUint32(4)
|
||||||
|
const metadataBytes = event.data.slice(8, 8 + metadataLength)
|
||||||
|
const metadata = JSON.parse(decoder4.decode(metadataBytes))
|
||||||
|
const imageData4 = event.data.slice(8 + metadataLength)
|
||||||
|
|
||||||
|
let imageMime4 = metadata.image_type
|
||||||
|
|
||||||
|
const imageBlob4 = new Blob([imageData4], {
|
||||||
|
type: imageMime4
|
||||||
|
})
|
||||||
|
|
||||||
|
// Dispatch enhanced preview event with metadata
|
||||||
|
this.dispatchCustomEvent('b_preview_with_metadata', {
|
||||||
|
blob: imageBlob4,
|
||||||
|
nodeId: metadata.node_id,
|
||||||
|
displayNodeId: metadata.display_node_id,
|
||||||
|
parentNodeId: metadata.parent_node_id,
|
||||||
|
realNodeId: metadata.real_node_id,
|
||||||
|
promptId: metadata.prompt_id
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also dispatch legacy b_preview for backward compatibility
|
||||||
|
this.dispatchCustomEvent('b_preview', imageBlob4)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown binary websocket message of type ${eventType}`
|
`Unknown binary websocket message of type ${eventType}`
|
||||||
@@ -461,6 +499,7 @@ export class ComfyApi extends EventTarget {
|
|||||||
case 'execution_cached':
|
case 'execution_cached':
|
||||||
case 'execution_success':
|
case 'execution_success':
|
||||||
case 'progress':
|
case 'progress':
|
||||||
|
case 'progress_state':
|
||||||
case 'executed':
|
case 'executed':
|
||||||
case 'graphChanged':
|
case 'graphChanged':
|
||||||
case 'promptQueued':
|
case 'promptQueued':
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ export class ComfyApp {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use useExecutionStore().executingNodeId instead
|
* @deprecated Use useExecutionStore().executingNodeId instead
|
||||||
|
* TODO: Update to support multiple executing nodes. This getter returns only the first executing node.
|
||||||
|
* Consider updating consumers to handle multiple nodes or use executingNodeIds array.
|
||||||
*/
|
*/
|
||||||
get runningNodeId(): NodeId | null {
|
get runningNodeId(): NodeId | null {
|
||||||
return useExecutionStore().executingNodeId
|
return useExecutionStore().executingNodeId
|
||||||
@@ -634,10 +636,6 @@ export class ComfyApp {
|
|||||||
|
|
||||||
api.addEventListener('executing', () => {
|
api.addEventListener('executing', () => {
|
||||||
this.graph.setDirtyCanvas(true, false)
|
this.graph.setDirtyCanvas(true, false)
|
||||||
// @ts-expect-error fixme ts strict error
|
|
||||||
this.revokePreviews(this.runningNodeId)
|
|
||||||
// @ts-expect-error fixme ts strict error
|
|
||||||
delete this.nodePreviewImages[this.runningNodeId]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
api.addEventListener('executed', ({ detail }) => {
|
api.addEventListener('executed', ({ detail }) => {
|
||||||
@@ -686,15 +684,13 @@ export class ComfyApp {
|
|||||||
this.canvas.draw(true, true)
|
this.canvas.draw(true, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
api.addEventListener('b_preview', ({ detail }) => {
|
api.addEventListener('b_preview_with_metadata', ({ detail }) => {
|
||||||
const id = this.runningNodeId
|
// Enhanced preview with explicit node context
|
||||||
if (id == null) return
|
const { blob, displayNodeId } = detail
|
||||||
|
this.revokePreviews(displayNodeId)
|
||||||
const blob = detail
|
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
const blobUrl = URL.createObjectURL(blob)
|
||||||
// Ensure clean up if `executing` event is missed.
|
// Preview cleanup is now handled in progress_state event to support multiple concurrent previews
|
||||||
this.revokePreviews(id)
|
this.nodePreviewImages[displayNodeId] = [blobUrl]
|
||||||
this.nodePreviewImages[id] = [blobUrl]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
api.init()
|
api.init()
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ export class ComponentWidgetImpl<
|
|||||||
component: Component
|
component: Component
|
||||||
inputSpec: InputSpec
|
inputSpec: InputSpec
|
||||||
props?: P
|
props?: P
|
||||||
|
componentProps?: Record<string, unknown>
|
||||||
options: DOMWidgetOptions<V>
|
options: DOMWidgetOptions<V>
|
||||||
}) {
|
}) {
|
||||||
super({
|
super({
|
||||||
@@ -245,7 +246,9 @@ export class ComponentWidgetImpl<
|
|||||||
})
|
})
|
||||||
this.component = obj.component
|
this.component = obj.component
|
||||||
this.inputSpec = obj.inputSpec
|
this.inputSpec = obj.inputSpec
|
||||||
this.props = obj.props
|
this.props = obj.componentProps
|
||||||
|
? ({ ...obj.props, ...obj.componentProps } as P)
|
||||||
|
: obj.props
|
||||||
}
|
}
|
||||||
|
|
||||||
override computeLayoutSize() {
|
override computeLayoutSize() {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import type {
|
|||||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||||
import { ComfyApp, app } from '@/scripts/app'
|
import { ComfyApp, app } from '@/scripts/app'
|
||||||
import { $el } from '@/scripts/ui'
|
import { $el } from '@/scripts/ui'
|
||||||
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||||
@@ -96,7 +97,12 @@ export const useLitegraphService = () => {
|
|||||||
*/
|
*/
|
||||||
#setupStrokeStyles() {
|
#setupStrokeStyles() {
|
||||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||||
if (this.id == app.runningNodeId) {
|
const nodeProgressStates = useExecutionStore().nodeProgressStates
|
||||||
|
const nodeId = String(this.id)
|
||||||
|
if (
|
||||||
|
nodeProgressStates[nodeId] &&
|
||||||
|
nodeProgressStates[nodeId].state === 'running'
|
||||||
|
) {
|
||||||
return { color: '#0f0' }
|
return { color: '#0f0' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import type {
|
|||||||
ExecutionErrorWsMessage,
|
ExecutionErrorWsMessage,
|
||||||
ExecutionStartWsMessage,
|
ExecutionStartWsMessage,
|
||||||
NodeError,
|
NodeError,
|
||||||
|
NodeProgressState,
|
||||||
|
ProgressStateWsMessage,
|
||||||
ProgressTextWsMessage,
|
ProgressTextWsMessage,
|
||||||
ProgressWsMessage
|
ProgressWsMessage
|
||||||
} from '@/schemas/apiSchema'
|
} from '@/schemas/apiSchema'
|
||||||
@@ -42,7 +44,22 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
||||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
||||||
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
||||||
const executingNodeId = ref<NodeId | null>(null)
|
// This is the progress of all nodes in the currently executing workflow
|
||||||
|
const nodeProgressStates = ref<Record<string, NodeProgressState>>({})
|
||||||
|
|
||||||
|
// Easily access all currently executing node IDs
|
||||||
|
const executingNodeIds = computed<NodeId[]>(() => {
|
||||||
|
return Object.entries(nodeProgressStates)
|
||||||
|
.filter(([_, state]) => state.state === 'running')
|
||||||
|
.map(([nodeId, _]) => nodeId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// For backward compatibility - stores the primary executing node ID
|
||||||
|
const executingNodeId = computed<NodeId | null>(() => {
|
||||||
|
return executingNodeIds.value.length > 0 ? executingNodeIds.value[0] : null
|
||||||
|
})
|
||||||
|
|
||||||
|
// For backward compatibility - returns the primary executing node
|
||||||
const executingNode = computed<ComfyNode | null>(() => {
|
const executingNode = computed<ComfyNode | null>(() => {
|
||||||
if (!executingNodeId.value) return null
|
if (!executingNodeId.value) return null
|
||||||
|
|
||||||
@@ -60,7 +77,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// This is the progress of the currently executing node, if any
|
// This is the progress of the currently executing node (for backward compatibility)
|
||||||
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
||||||
const executingNodeProgress = computed(() =>
|
const executingNodeProgress = computed(() =>
|
||||||
_executingNodeProgress.value
|
_executingNodeProgress.value
|
||||||
@@ -97,6 +114,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
api.addEventListener('executed', handleExecuted)
|
api.addEventListener('executed', handleExecuted)
|
||||||
api.addEventListener('executing', handleExecuting)
|
api.addEventListener('executing', handleExecuting)
|
||||||
api.addEventListener('progress', handleProgress)
|
api.addEventListener('progress', handleProgress)
|
||||||
|
api.addEventListener('progress_state', handleProgressState)
|
||||||
api.addEventListener('status', handleStatus)
|
api.addEventListener('status', handleStatus)
|
||||||
api.addEventListener('execution_error', handleExecutionError)
|
api.addEventListener('execution_error', handleExecutionError)
|
||||||
}
|
}
|
||||||
@@ -109,6 +127,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
api.removeEventListener('executed', handleExecuted)
|
api.removeEventListener('executed', handleExecuted)
|
||||||
api.removeEventListener('executing', handleExecuting)
|
api.removeEventListener('executing', handleExecuting)
|
||||||
api.removeEventListener('progress', handleProgress)
|
api.removeEventListener('progress', handleProgress)
|
||||||
|
api.removeEventListener('progress_state', handleProgressState)
|
||||||
api.removeEventListener('status', handleStatus)
|
api.removeEventListener('status', handleStatus)
|
||||||
api.removeEventListener('execution_error', handleExecutionError)
|
api.removeEventListener('execution_error', handleExecutionError)
|
||||||
api.removeEventListener('progress_text', handleProgressText)
|
api.removeEventListener('progress_text', handleProgressText)
|
||||||
@@ -138,12 +157,8 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
|
|
||||||
if (!activePrompt.value) return
|
if (!activePrompt.value) return
|
||||||
|
|
||||||
if (executingNodeId.value && activePrompt.value) {
|
// Update the executing nodes list
|
||||||
// Seems sometimes nodes that are cached fire executing but not executed
|
if (e.detail === null) {
|
||||||
activePrompt.value.nodes[executingNodeId.value] = true
|
|
||||||
}
|
|
||||||
executingNodeId.value = e.detail
|
|
||||||
if (executingNodeId.value === null) {
|
|
||||||
if (activePromptId.value) {
|
if (activePromptId.value) {
|
||||||
delete queuedPrompts.value[activePromptId.value]
|
delete queuedPrompts.value[activePromptId.value]
|
||||||
}
|
}
|
||||||
@@ -151,6 +166,37 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) {
|
||||||
|
const { nodes } = e.detail
|
||||||
|
|
||||||
|
// Revoke previews for nodes that are starting to execute
|
||||||
|
for (const nodeId in nodes) {
|
||||||
|
const nodeState = nodes[nodeId]
|
||||||
|
if (nodeState.state === 'running' && !nodeProgressStates.value[nodeId]) {
|
||||||
|
// This node just started executing, revoke its previews
|
||||||
|
// Note that we're doing the *actual* node id instead of the display node id
|
||||||
|
// here intentionally. That way, we don't clear the preview every time a new node
|
||||||
|
// within an expanded graph starts executing.
|
||||||
|
app.revokePreviews(nodeId)
|
||||||
|
delete app.nodePreviewImages[nodeId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the progress states for all nodes
|
||||||
|
nodeProgressStates.value = nodes
|
||||||
|
|
||||||
|
// If we have progress for the currently executing node, update it for backwards compatibility
|
||||||
|
if (executingNodeId.value && nodes[executingNodeId.value]) {
|
||||||
|
const nodeState = nodes[executingNodeId.value]
|
||||||
|
_executingNodeProgress.value = {
|
||||||
|
value: nodeState.value,
|
||||||
|
max: nodeState.max,
|
||||||
|
prompt_id: nodeState.prompt_id,
|
||||||
|
node: nodeState.display_node_id || nodeState.node_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleProgress(e: CustomEvent<ProgressWsMessage>) {
|
function handleProgress(e: CustomEvent<ProgressWsMessage>) {
|
||||||
_executingNodeProgress.value = e.detail
|
_executingNodeProgress.value = e.detail
|
||||||
}
|
}
|
||||||
@@ -238,9 +284,13 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
*/
|
*/
|
||||||
lastExecutionError,
|
lastExecutionError,
|
||||||
/**
|
/**
|
||||||
* The id of the node that is currently being executed
|
* The id of the node that is currently being executed (backward compatibility)
|
||||||
*/
|
*/
|
||||||
executingNodeId,
|
executingNodeId,
|
||||||
|
/**
|
||||||
|
* The list of all nodes that are currently executing
|
||||||
|
*/
|
||||||
|
executingNodeIds,
|
||||||
/**
|
/**
|
||||||
* The prompt that is currently being executed
|
* The prompt that is currently being executed
|
||||||
*/
|
*/
|
||||||
@@ -258,13 +308,17 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
*/
|
*/
|
||||||
executionProgress,
|
executionProgress,
|
||||||
/**
|
/**
|
||||||
* The node that is currently being executed
|
* The node that is currently being executed (backward compatibility)
|
||||||
*/
|
*/
|
||||||
executingNode,
|
executingNode,
|
||||||
/**
|
/**
|
||||||
* The progress of the executing node (if the node reports progress)
|
* The progress of the executing node (backward compatibility)
|
||||||
*/
|
*/
|
||||||
executingNodeProgress,
|
executingNodeProgress,
|
||||||
|
/**
|
||||||
|
* All node progress states from progress_state events
|
||||||
|
*/
|
||||||
|
nodeProgressStates,
|
||||||
bindExecutionEvents,
|
bindExecutionEvents,
|
||||||
unbindExecutionEvents,
|
unbindExecutionEvents,
|
||||||
storePrompt,
|
storePrompt,
|
||||||
|
|||||||
@@ -3,12 +3,20 @@ import { nextTick, reactive } from 'vue'
|
|||||||
|
|
||||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||||
|
|
||||||
|
// Mock i18n module
|
||||||
|
vi.mock('@/i18n', () => ({
|
||||||
|
t: (key: string, fallback: string) =>
|
||||||
|
key === 'g.nodesRunning' ? 'nodes running' : fallback
|
||||||
|
}))
|
||||||
|
|
||||||
// Mock the execution store
|
// Mock the execution store
|
||||||
const executionStore = reactive({
|
const executionStore = reactive({
|
||||||
isIdle: true,
|
isIdle: true,
|
||||||
executionProgress: 0,
|
executionProgress: 0,
|
||||||
executingNode: null as any,
|
executingNode: null as any,
|
||||||
executingNodeProgress: 0
|
executingNodeProgress: 0,
|
||||||
|
nodeProgressStates: {} as any,
|
||||||
|
activePrompt: null as any
|
||||||
})
|
})
|
||||||
vi.mock('@/stores/executionStore', () => ({
|
vi.mock('@/stores/executionStore', () => ({
|
||||||
useExecutionStore: () => executionStore
|
useExecutionStore: () => executionStore
|
||||||
@@ -37,6 +45,8 @@ describe('useBrowserTabTitle', () => {
|
|||||||
executionStore.executionProgress = 0
|
executionStore.executionProgress = 0
|
||||||
executionStore.executingNode = null as any
|
executionStore.executingNode = null as any
|
||||||
executionStore.executingNodeProgress = 0
|
executionStore.executingNodeProgress = 0
|
||||||
|
executionStore.nodeProgressStates = {}
|
||||||
|
executionStore.activePrompt = null
|
||||||
|
|
||||||
// reset setting and workflow stores
|
// reset setting and workflow stores
|
||||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||||
@@ -97,13 +107,41 @@ describe('useBrowserTabTitle', () => {
|
|||||||
expect(document.title).toBe('[30%]ComfyUI')
|
expect(document.title).toBe('[30%]ComfyUI')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows node execution title when executing a node', async () => {
|
it('shows node execution title when executing a node using nodeProgressStates', async () => {
|
||||||
executionStore.isIdle = false
|
executionStore.isIdle = false
|
||||||
executionStore.executionProgress = 0.4
|
executionStore.executionProgress = 0.4
|
||||||
executionStore.executingNodeProgress = 0.5
|
executionStore.nodeProgressStates = {
|
||||||
executionStore.executingNode = { type: 'Foo' }
|
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
|
||||||
|
}
|
||||||
|
executionStore.activePrompt = {
|
||||||
|
workflow: {
|
||||||
|
changeTracker: {
|
||||||
|
activeState: {
|
||||||
|
nodes: [{ id: 1, type: 'Foo' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
useBrowserTabTitle()
|
useBrowserTabTitle()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(document.title).toBe('[40%][50%] Foo')
|
expect(document.title).toBe('[40%][50%] Foo')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows multiple nodes running when multiple nodes are executing', async () => {
|
||||||
|
executionStore.isIdle = false
|
||||||
|
executionStore.executionProgress = 0.4
|
||||||
|
executionStore.nodeProgressStates = {
|
||||||
|
'1': {
|
||||||
|
state: 'running',
|
||||||
|
value: 5,
|
||||||
|
max: 10,
|
||||||
|
node: '1',
|
||||||
|
prompt_id: 'test'
|
||||||
|
},
|
||||||
|
'2': { state: 'running', value: 8, max: 10, node: '2', prompt_id: 'test' }
|
||||||
|
}
|
||||||
|
useBrowserTabTitle()
|
||||||
|
await nextTick()
|
||||||
|
expect(document.title).toBe('[40%][2 nodes running]')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user