mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-24 06:17:32 +00:00
Compare commits
12 Commits
refactor/e
...
qpo-progre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f62edda7e9 | ||
|
|
7ce6408e7f | ||
|
|
3185004a20 | ||
|
|
98d56bdada | ||
|
|
fafaba09d1 | ||
|
|
f349184290 | ||
|
|
530717983f | ||
|
|
6866bd4792 | ||
|
|
1bfd617265 | ||
|
|
bd1e15ad70 | ||
|
|
a8458c6508 | ||
|
|
e4110dd254 |
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<tr
|
||||
class="border-neutral-700 border-solid border-y"
|
||||
class="border-y border-solid border-neutral-700"
|
||||
:class="{
|
||||
'opacity-50': runner.resolved,
|
||||
'opacity-75': isLoading && runner.resolved
|
||||
}"
|
||||
>
|
||||
<td class="text-center w-16">
|
||||
<td class="w-16 text-center">
|
||||
<TaskListStatusIcon :state="runner.state" :loading="isLoading" />
|
||||
</td>
|
||||
<td>
|
||||
@@ -14,7 +14,7 @@
|
||||
{{ task.name }}
|
||||
</p>
|
||||
<Button
|
||||
class="inline-block mx-2"
|
||||
class="mx-2 inline-block"
|
||||
type="button"
|
||||
:icon="PrimeIcons.INFO_CIRCLE"
|
||||
severity="secondary"
|
||||
@@ -22,11 +22,11 @@
|
||||
@click="toggle"
|
||||
/>
|
||||
|
||||
<Popover ref="infoPopover" class="block m-1 max-w-64 min-w-32">
|
||||
<Popover ref="infoPopover" class="m-1 block max-w-64 min-w-32">
|
||||
<span class="whitespace-pre-line">{{ task.description }}</span>
|
||||
</Popover>
|
||||
</td>
|
||||
<td class="text-right px-4">
|
||||
<td class="px-4 text-right">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
|
||||
@@ -14,25 +14,18 @@ vi.mock('@/i18n', () => ({
|
||||
const executionStore = reactive<{
|
||||
isIdle: boolean
|
||||
executionProgress: number
|
||||
executingNode: unknown
|
||||
executingNode: null | {
|
||||
title?: string
|
||||
type?: string
|
||||
}
|
||||
executingNodeProgress: number
|
||||
nodeProgressStates: Record<string, unknown>
|
||||
activeJob: {
|
||||
workflow: {
|
||||
changeTracker: {
|
||||
activeState: {
|
||||
nodes: { id: number; type: string }[]
|
||||
}
|
||||
}
|
||||
}
|
||||
} | null
|
||||
}>({
|
||||
isIdle: true,
|
||||
executionProgress: 0,
|
||||
executingNode: null,
|
||||
executingNodeProgress: 0,
|
||||
nodeProgressStates: {},
|
||||
activeJob: null
|
||||
nodeProgressStates: {}
|
||||
})
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => executionStore
|
||||
@@ -76,7 +69,6 @@ describe('useBrowserTabTitle', () => {
|
||||
executionStore.executingNode = null
|
||||
executionStore.executingNodeProgress = 0
|
||||
executionStore.nodeProgressStates = {}
|
||||
executionStore.activeJob = null
|
||||
|
||||
// reset setting and workflow stores
|
||||
vi.mocked(settingStore.get).mockReturnValue('Enabled')
|
||||
@@ -184,18 +176,12 @@ describe('useBrowserTabTitle', () => {
|
||||
it('shows node execution title when executing a node using nodeProgressStates', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.executingNode = {
|
||||
type: 'Foo'
|
||||
}
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
|
||||
}
|
||||
executionStore.activeJob = {
|
||||
workflow: {
|
||||
changeTracker: {
|
||||
activeState: {
|
||||
nodes: [{ id: 1, type: 'Foo' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const scope: EffectScope = effectScope()
|
||||
scope.run(() => useBrowserTabTitle())
|
||||
await nextTick()
|
||||
|
||||
@@ -74,14 +74,14 @@ export const useBrowserTabTitle = () => {
|
||||
}
|
||||
|
||||
// If only one node is running
|
||||
const [nodeId, state] = runningNodes[0]
|
||||
const [, state] = runningNodes[0]
|
||||
const progress = Math.round((state.value / state.max) * 100)
|
||||
const nodeType =
|
||||
executionStore.activeJob?.workflow?.changeTracker?.activeState.nodes.find(
|
||||
(n) => String(n.id) === nodeId
|
||||
)?.type || 'Node'
|
||||
const nodeLabel =
|
||||
executionStore.executingNode?.type?.trim() ||
|
||||
executionStore.executingNode?.title?.trim() ||
|
||||
'Node'
|
||||
|
||||
return `${executionText.value}[${progress}%] ${nodeType}`
|
||||
return `${executionText.value}[${progress}%] ${nodeLabel}`
|
||||
})
|
||||
|
||||
const workflowTitle = computed(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseModalLayout content-title="" data-testid="settings-dialog" size="md">
|
||||
<BaseModalLayout content-title="" data-testid="settings-dialog" size="sm">
|
||||
<template #leftPanelHeaderTitle>
|
||||
<i class="icon-[lucide--settings]" />
|
||||
<h2 class="text-neutral text-base">{{ $t('g.settings') }}</h2>
|
||||
@@ -12,6 +12,7 @@
|
||||
size="md"
|
||||
:placeholder="$t('g.searchSettings') + '...'"
|
||||
:debounce-time="128"
|
||||
autofocus
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1634,6 +1634,7 @@ export class ComfyApp {
|
||||
executionStore.storeJob({
|
||||
id: res.prompt_id,
|
||||
nodes: Object.keys(p.output),
|
||||
promptOutput: p.output,
|
||||
workflow: queuedWorkflow
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { MAX_PROGRESS_JOBS, useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil'
|
||||
import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { NodeProgressState } from '@/schemas/apiSchema'
|
||||
|
||||
type WorkflowConstructor = typeof WorkflowStoreModule.ComfyWorkflow
|
||||
|
||||
// Create mock functions that will be shared
|
||||
const mockNodeExecutionIdToNodeLocatorId = vi.fn()
|
||||
const mockNodeIdToNodeLocatorId = vi.fn()
|
||||
const mockNodeLocatorIdToNodeExecutionId = vi.fn()
|
||||
|
||||
import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { NodeProgressState } from '@/schemas/apiSchema'
|
||||
const workflowModuleState = vi.hoisted(() => ({
|
||||
WorkflowClass: undefined as WorkflowConstructor | undefined
|
||||
}))
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
|
||||
@@ -20,6 +25,7 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', async () => {
|
||||
const { ComfyWorkflow } = await vi.importActual<typeof WorkflowStoreModule>(
|
||||
'@/platform/workflow/management/stores/workflowStore'
|
||||
)
|
||||
workflowModuleState.WorkflowClass = ComfyWorkflow
|
||||
return {
|
||||
ComfyWorkflow,
|
||||
useWorkflowStore: vi.fn(() => ({
|
||||
@@ -60,7 +66,7 @@ vi.mock('@/scripts/api', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/imagePreviewStore', () => ({
|
||||
vi.mock('@/stores/nodeOutputStore', () => ({
|
||||
useNodeOutputStore: () => ({
|
||||
revokePreviewsByExecutionId: vi.fn()
|
||||
})
|
||||
@@ -84,6 +90,29 @@ vi.mock('@/scripts/app', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
function createQueuedWorkflow(path: string = 'workflows/test.json') {
|
||||
const { WorkflowClass } = workflowModuleState
|
||||
if (!WorkflowClass) {
|
||||
throw new Error('ComfyWorkflow mock class is not available')
|
||||
}
|
||||
|
||||
return new WorkflowClass({
|
||||
path,
|
||||
modified: 0,
|
||||
size: 0
|
||||
})
|
||||
}
|
||||
|
||||
function createPromptNode(title: string, classType: string) {
|
||||
return {
|
||||
inputs: {},
|
||||
class_type: classType,
|
||||
_meta: {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('useExecutionStore - NodeLocatorId conversions', () => {
|
||||
let store: ReturnType<typeof useExecutionStore>
|
||||
|
||||
@@ -598,6 +627,103 @@ describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('useExecutionStore - executingNode with subgraphs', () => {
|
||||
let store: ReturnType<typeof useExecutionStore>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
store = useExecutionStore()
|
||||
})
|
||||
|
||||
it('should find executing node info in root graph from queued prompt data', () => {
|
||||
store.storeJob({
|
||||
id: 'test-prompt',
|
||||
nodes: ['123'],
|
||||
promptOutput: {
|
||||
'123': createPromptNode('Test Node', 'TestNode')
|
||||
},
|
||||
workflow: createQueuedWorkflow()
|
||||
})
|
||||
store.activeJobId = 'test-prompt'
|
||||
|
||||
store.nodeProgressStates = {
|
||||
'123': {
|
||||
state: 'running',
|
||||
value: 0,
|
||||
max: 100,
|
||||
display_node_id: '123',
|
||||
prompt_id: 'test-prompt',
|
||||
node_id: '123'
|
||||
}
|
||||
}
|
||||
|
||||
expect(store.executingNode).toEqual({
|
||||
title: 'Test Node',
|
||||
type: 'TestNode'
|
||||
})
|
||||
})
|
||||
|
||||
it('should find executing node info in subgraph using execution ID', () => {
|
||||
store.storeJob({
|
||||
id: 'test-prompt',
|
||||
nodes: ['456:789'],
|
||||
promptOutput: {
|
||||
'456:789': createPromptNode('Nested Node', 'NestedNode')
|
||||
},
|
||||
workflow: createQueuedWorkflow()
|
||||
})
|
||||
store.activeJobId = 'test-prompt'
|
||||
|
||||
store.nodeProgressStates = {
|
||||
'456:789': {
|
||||
state: 'running',
|
||||
value: 0,
|
||||
max: 100,
|
||||
display_node_id: '456:789',
|
||||
prompt_id: 'test-prompt',
|
||||
node_id: '456:789'
|
||||
}
|
||||
}
|
||||
|
||||
expect(store.executingNode).toEqual({
|
||||
title: 'Nested Node',
|
||||
type: 'NestedNode'
|
||||
})
|
||||
})
|
||||
|
||||
it('should return null when no node is executing', () => {
|
||||
store.nodeProgressStates = {}
|
||||
|
||||
expect(store.executingNode).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null when executing node metadata cannot be found', () => {
|
||||
store.storeJob({
|
||||
id: 'test-prompt',
|
||||
nodes: ['123'],
|
||||
promptOutput: {
|
||||
'123': createPromptNode('Test Node', 'TestNode')
|
||||
},
|
||||
workflow: createQueuedWorkflow()
|
||||
})
|
||||
store.activeJobId = 'test-prompt'
|
||||
|
||||
store.nodeProgressStates = {
|
||||
'999': {
|
||||
state: 'running',
|
||||
value: 0,
|
||||
max: 100,
|
||||
display_node_id: '999',
|
||||
prompt_id: 'test-prompt',
|
||||
node_id: '999'
|
||||
}
|
||||
}
|
||||
|
||||
expect(store.executingNode).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useExecutionErrorStore - setMissingNodeTypes', () => {
|
||||
let store: ReturnType<typeof useExecutionErrorStore>
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON,
|
||||
ComfyApiWorkflow,
|
||||
NodeId
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
@@ -34,6 +33,11 @@ import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { classifyCloudValidationError } from '@/utils/executionErrorUtil'
|
||||
import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
interface ExecutionNodeInfo {
|
||||
title?: string | null
|
||||
type?: string | null
|
||||
}
|
||||
|
||||
interface QueuedJob {
|
||||
/**
|
||||
* The nodes that are queued to be executed. The key is the node id and the
|
||||
@@ -44,6 +48,25 @@ interface QueuedJob {
|
||||
* The workflow that is queued to be executed
|
||||
*/
|
||||
workflow?: ComfyWorkflow
|
||||
/**
|
||||
* Queue-time node metadata keyed by execution ID.
|
||||
* This stays stable even if the user switches workflows or edits the canvas.
|
||||
*/
|
||||
nodeLookup?: Record<string, ExecutionNodeInfo>
|
||||
}
|
||||
|
||||
function buildExecutionNodeLookup(
|
||||
promptOutput: ComfyApiWorkflow
|
||||
): Record<string, ExecutionNodeInfo> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(promptOutput).map(([executionId, node]) => [
|
||||
executionId,
|
||||
{
|
||||
title: node._meta.title,
|
||||
type: node.class_type
|
||||
}
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,21 +189,11 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
() => new Set(executingNodeIds.value.map(String))
|
||||
)
|
||||
|
||||
// For backward compatibility - returns the primary executing node
|
||||
const executingNode = computed<ComfyNode | null>(() => {
|
||||
// For backward compatibility - returns the primary executing node info
|
||||
const executingNode = computed<ExecutionNodeInfo | null>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
|
||||
const workflow: ComfyWorkflow | undefined = activeJob.value?.workflow
|
||||
if (!workflow) return null
|
||||
|
||||
const canvasState: ComfyWorkflowJSON | null =
|
||||
workflow.changeTracker?.activeState ?? null
|
||||
if (!canvasState) return null
|
||||
|
||||
return (
|
||||
canvasState.nodes.find((n) => String(n.id) === executingNodeId.value) ??
|
||||
null
|
||||
)
|
||||
return activeJob.value?.nodeLookup?.[String(executingNodeId.value)] ?? null
|
||||
})
|
||||
|
||||
// This is the progress of the currently executing node (for backward compatibility)
|
||||
@@ -536,10 +549,12 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
function storeJob({
|
||||
nodes,
|
||||
id,
|
||||
promptOutput,
|
||||
workflow
|
||||
}: {
|
||||
nodes: string[]
|
||||
id: string
|
||||
promptOutput: ComfyApiWorkflow
|
||||
workflow: ComfyWorkflow
|
||||
}) {
|
||||
queuedJobs.value[id] ??= { nodes: {} }
|
||||
@@ -551,6 +566,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
}, {}),
|
||||
...queuedJob.nodes
|
||||
}
|
||||
queuedJob.nodeLookup = buildExecutionNodeLookup(promptOutput)
|
||||
queuedJob.workflow = workflow
|
||||
const wid = workflow?.activeState?.id ?? workflow?.initialState?.id
|
||||
if (wid) {
|
||||
|
||||
Reference in New Issue
Block a user