mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 06:19:58 +00:00
Add progress bars and progress text
This commit is contained in:
@@ -1,71 +1,75 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!workspaceStore.focusMode"
|
||||
class="ml-1 flex gap-x-0.5 pt-1"
|
||||
class="ml-1 flex flex-col gap-1 pt-1"
|
||||
@mouseenter="isTopMenuHovered = true"
|
||||
@mouseleave="isTopMenuHovered = false"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
<div class="flex gap-x-0.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
|
||||
<div class="mx-1 flex flex-col items-end gap-1">
|
||||
<div
|
||||
class="actionbar-container relative pointer-events-auto flex h-12 items-center overflow-hidden rounded-lg border border-interface-stroke px-2 shadow-interface"
|
||||
>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<IconButton
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 bg-destructive-background text-base-foreground transition-colors duration-200 ease-in-out hover:bg-destructive-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-destructive-background"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
<IconTextButton
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
size="sm"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
class="mr-2 h-8 border-0 px-3 text-sm font-medium text-base-foreground cursor-pointer"
|
||||
:aria-pressed="isQueueOverlayExpanded"
|
||||
:aria-label="queueToggleLabel"
|
||||
:label="queueToggleLabel"
|
||||
@click="toggleQueueOverlay"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--chevron-down] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
<IconButton
|
||||
v-if="!isRightSidePanelOpen"
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
<QueueInlineProgress :hidden="isQueueOverlayExpanded" />
|
||||
</div>
|
||||
<QueueProgressOverlay
|
||||
v-model:expanded="isQueueOverlayExpanded"
|
||||
:menu-hovered="isTopMenuHovered"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-1 flex flex-col items-end gap-1">
|
||||
<div
|
||||
class="actionbar-container relative pointer-events-auto flex h-12 items-center overflow-hidden rounded-lg border border-interface-stroke px-2 shadow-interface"
|
||||
>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<IconButton
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 bg-destructive-background text-base-foreground transition-colors duration-200 ease-in-out hover:bg-destructive-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-destructive-background"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
<IconTextButton
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
size="sm"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
class="mr-2 h-8 border-0 px-3 text-sm font-medium text-base-foreground cursor-pointer"
|
||||
:aria-pressed="isQueueOverlayExpanded"
|
||||
:aria-label="queueToggleLabel"
|
||||
:label="queueToggleLabel"
|
||||
@click="toggleQueueOverlay"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--chevron-down] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
<IconButton
|
||||
v-if="!isRightSidePanelOpen"
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
<QueueInlineProgress :hidden="isQueueOverlayExpanded" />
|
||||
</div>
|
||||
<QueueProgressOverlay
|
||||
v-model:expanded="isQueueOverlayExpanded"
|
||||
:menu-hovered="isTopMenuHovered"
|
||||
/>
|
||||
</div>
|
||||
<QueueInlineProgressSummary class="pr-1" :hidden="isQueueOverlayExpanded" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -79,6 +83,7 @@ import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
import QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.vue'
|
||||
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
|
||||
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
|
||||
169
src/components/queue/QueueInlineProgressSummary.stories.ts
Normal file
169
src/components/queue/QueueInlineProgressSummary.stories.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import QueueInlineProgressSummary from './QueueInlineProgressSummary.vue'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
type SeedOptions = {
|
||||
promptId: string
|
||||
nodes: Record<string, boolean>
|
||||
runningNodeId?: string
|
||||
runningNodeTitle?: string
|
||||
runningNodeType?: string
|
||||
currentValue?: number
|
||||
currentMax?: number
|
||||
}
|
||||
|
||||
function resetExecutionStore() {
|
||||
const exec = useExecutionStore()
|
||||
exec.activePromptId = null
|
||||
exec.queuedPrompts = {}
|
||||
exec.nodeProgressStates = {}
|
||||
exec.nodeProgressStatesByPrompt = {}
|
||||
exec._executingNodeProgress = null
|
||||
exec.lastExecutionError = null
|
||||
exec.lastNodeErrors = null
|
||||
exec.initializingPromptIds = new Set()
|
||||
exec.promptIdToWorkflowId = new Map()
|
||||
}
|
||||
|
||||
function seedExecutionState({
|
||||
promptId,
|
||||
nodes,
|
||||
runningNodeId,
|
||||
runningNodeTitle,
|
||||
runningNodeType,
|
||||
currentValue = 0,
|
||||
currentMax = 100
|
||||
}: SeedOptions) {
|
||||
resetExecutionStore()
|
||||
|
||||
const exec = useExecutionStore()
|
||||
const workflow = runningNodeId
|
||||
? ({
|
||||
changeTracker: {
|
||||
activeState: {
|
||||
nodes: Object.keys(nodes).map((id) => ({
|
||||
id,
|
||||
title:
|
||||
id === runningNodeId ? (runningNodeTitle ?? '') : `Node ${id}`,
|
||||
type: id === runningNodeId ? (runningNodeType ?? 'Node') : 'Node'
|
||||
}))
|
||||
}
|
||||
}
|
||||
} as any)
|
||||
: undefined
|
||||
|
||||
exec.activePromptId = promptId
|
||||
exec.queuedPrompts = {
|
||||
[promptId]: {
|
||||
nodes,
|
||||
...(workflow ? { workflow } : {})
|
||||
}
|
||||
} as any
|
||||
|
||||
const nodeProgress = runningNodeId
|
||||
? {
|
||||
[runningNodeId]: {
|
||||
value: currentValue,
|
||||
max: currentMax,
|
||||
state: 'running',
|
||||
node_id: runningNodeId,
|
||||
prompt_id: promptId
|
||||
}
|
||||
}
|
||||
: {}
|
||||
|
||||
exec.nodeProgressStates = nodeProgress as any
|
||||
exec.nodeProgressStatesByPrompt = runningNodeId
|
||||
? ({ [promptId]: nodeProgress } as any)
|
||||
: {}
|
||||
exec._executingNodeProgress = runningNodeId
|
||||
? ({
|
||||
value: currentValue,
|
||||
max: currentMax,
|
||||
prompt_id: promptId,
|
||||
node: runningNodeId
|
||||
} as any)
|
||||
: null
|
||||
}
|
||||
|
||||
const meta: Meta<typeof QueueInlineProgressSummary> = {
|
||||
title: 'Queue/QueueInlineProgressSummary',
|
||||
component: QueueInlineProgressSummary,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
backgrounds: {
|
||||
default: 'light'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const RunningKSampler: Story = {
|
||||
render: () => ({
|
||||
components: { QueueInlineProgressSummary },
|
||||
setup() {
|
||||
seedExecutionState({
|
||||
promptId: 'prompt-running',
|
||||
nodes: { '1': true, '2': false, '3': false, '4': true },
|
||||
runningNodeId: '2',
|
||||
runningNodeTitle: 'KSampler',
|
||||
runningNodeType: 'KSampler',
|
||||
currentValue: 12,
|
||||
currentMax: 100
|
||||
})
|
||||
|
||||
return {}
|
||||
},
|
||||
template: `
|
||||
<div style="background: var(--color-surface-primary); width: 420px; padding: 12px;">
|
||||
<QueueInlineProgressSummary />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const RunningWithFallbackName: Story = {
|
||||
render: () => ({
|
||||
components: { QueueInlineProgressSummary },
|
||||
setup() {
|
||||
seedExecutionState({
|
||||
promptId: 'prompt-fallback',
|
||||
nodes: { '10': true, '11': true, '12': false, '13': true },
|
||||
runningNodeId: '12',
|
||||
runningNodeTitle: '',
|
||||
runningNodeType: 'custom_node',
|
||||
currentValue: 78,
|
||||
currentMax: 100
|
||||
})
|
||||
|
||||
return {}
|
||||
},
|
||||
template: `
|
||||
<div style="background: var(--color-surface-primary); width: 420px; padding: 12px;">
|
||||
<QueueInlineProgressSummary />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const ProgressWithoutCurrentNode: Story = {
|
||||
render: () => ({
|
||||
components: { QueueInlineProgressSummary },
|
||||
setup() {
|
||||
seedExecutionState({
|
||||
promptId: 'prompt-progress-only',
|
||||
nodes: { '21': true, '22': true, '23': true, '24': false }
|
||||
})
|
||||
|
||||
return {}
|
||||
},
|
||||
template: `
|
||||
<div style="background: var(--color-surface-primary); width: 420px; padding: 12px;">
|
||||
<QueueInlineProgressSummary />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
64
src/components/queue/QueueInlineProgressSummary.vue
Normal file
64
src/components/queue/QueueInlineProgressSummary.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div v-if="shouldShow" class="flex justify-end">
|
||||
<div
|
||||
class="flex items-center gap-4 whitespace-nowrap text-[0.75rem] leading-[normal] drop-shadow-[1px_1px_8px_rgba(0,0,0,0.4)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="flex items-center gap-1 text-base-foreground">
|
||||
<span class="font-normal">{{ totalLabel }}:</span>
|
||||
<span class="w-[5ch] shrink-0 text-right font-bold tabular-nums">
|
||||
{{ totalPercentFormatted }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 text-muted-foreground">
|
||||
<span
|
||||
class="w-[16ch] shrink-0 truncate text-right"
|
||||
:title="currentNodeLabel"
|
||||
>
|
||||
{{ currentNodeLabel }}:
|
||||
</span>
|
||||
<span class="w-[5ch] shrink-0 text-right tabular-nums">
|
||||
{{ currentNodePercentFormatted }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCurrentNodeName } from '@/composables/queue/useCurrentNodeName'
|
||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
const props = defineProps<{
|
||||
hidden?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const executionStore = useExecutionStore()
|
||||
const { currentNodeName } = useCurrentNodeName()
|
||||
const {
|
||||
totalPercent,
|
||||
totalPercentFormatted,
|
||||
currentNodePercent,
|
||||
currentNodePercentFormatted
|
||||
} = useQueueProgress()
|
||||
|
||||
const totalLabel = computed<string>(() =>
|
||||
t('sideToolbar.queueProgressOverlay.inlineTotalLabel')
|
||||
)
|
||||
|
||||
const currentNodeLabel = computed<string>(() => currentNodeName.value)
|
||||
|
||||
const shouldShow = computed(
|
||||
() =>
|
||||
!props.hidden &&
|
||||
(!executionStore.isIdle ||
|
||||
totalPercent.value > 0 ||
|
||||
currentNodePercent.value > 0)
|
||||
)
|
||||
</script>
|
||||
23
src/composables/queue/useCurrentNodeName.ts
Normal file
23
src/composables/queue/useCurrentNodeName.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
export function useCurrentNodeName() {
|
||||
const { t } = useI18n()
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
const currentNodeName = computed(() => {
|
||||
const node = executionStore.executingNode
|
||||
if (!node) return t('g.emDash')
|
||||
const title = (node.title ?? '').toString().trim()
|
||||
if (title) return title
|
||||
const nodeType = (node.type ?? '').toString().trim() || t('g.untitled')
|
||||
const key = `nodeDefs.${normalizeI18nKey(nodeType)}.display_name`
|
||||
return st(key, nodeType)
|
||||
})
|
||||
|
||||
return { currentNodeName }
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCurrentNodeName } from '@/composables/queue/useCurrentNodeName'
|
||||
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
|
||||
import { st } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
isToday,
|
||||
isYesterday
|
||||
} from '@/utils/dateTimeUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { buildJobDisplay } from '@/utils/queueDisplay'
|
||||
import { jobStateFromTask } from '@/utils/queueUtil'
|
||||
|
||||
@@ -168,6 +167,7 @@ export function useJobList() {
|
||||
})
|
||||
|
||||
const { totalPercent, currentNodePercent } = useQueueProgress()
|
||||
const { currentNodeName } = useCurrentNodeName()
|
||||
|
||||
const relativeTimeFormatter = computed(() => {
|
||||
const localeValue = locale.value
|
||||
@@ -183,16 +183,6 @@ export function useJobList() {
|
||||
const isJobInitializing = (promptId: string | number | undefined) =>
|
||||
executionStore.isPromptInitializing(promptId)
|
||||
|
||||
const currentNodeName = computed(() => {
|
||||
const node = executionStore.executingNode
|
||||
if (!node) return t('g.emDash')
|
||||
const title = (node.title ?? '').toString().trim()
|
||||
if (title) return title
|
||||
const nodeType = (node.type ?? '').toString().trim() || t('g.untitled')
|
||||
const key = `nodeDefs.${normalizeI18nKey(nodeType)}.display_name`
|
||||
return st(key, nodeType)
|
||||
})
|
||||
|
||||
const selectedJobTab = ref<JobTab>('All')
|
||||
const selectedWorkflowFilter = ref<'all' | 'current'>('all')
|
||||
const selectedSortMode = ref<JobSortMode>('mostRecent')
|
||||
|
||||
@@ -697,6 +697,8 @@
|
||||
"total": "Total: {percent}",
|
||||
"colonPercent": ": {percent}",
|
||||
"currentNode": "Current node:",
|
||||
"inlineTotalLabel": "Total",
|
||||
"inlineCurrentNodeLabel": "Current node",
|
||||
"viewAllJobs": "View all jobs",
|
||||
"running": "running",
|
||||
"preview": "Preview",
|
||||
|
||||
Reference in New Issue
Block a user