mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 23:04:06 +00:00
Implement workflow progress panel (#6092)
Adds a workflow progress panel component underneath the `actionbar-container`. I suggest starting a review at the extraneous changes that were needed. Including but not limited to: - `get createTime()` in queueStore - `promptIdToWorkflowId`, `initializingPromptIds`, and `nodeProgressStatesByPrompt` in executionStore - `create_time` handling in v2ToV1Adapter - `pointer-events-auto` on ComfyActionbar.vue The rest of the changes should be contained under `QueueProgressOverlay.vue`, and has less of a blast radius in case something goes wrong. --------- Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Jin Yi <jin12cc@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com> Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
This commit is contained in:
77
src/utils/dateTimeUtil.ts
Normal file
77
src/utils/dateTimeUtil.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Return a local date key in YYYY-MM-DD format for grouping.
|
||||
*
|
||||
* @param ts Unix timestamp in milliseconds
|
||||
* @returns Local date key string
|
||||
*/
|
||||
export const dateKey = (ts: number): string => {
|
||||
const d = new Date(ts)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a timestamp is on the same local day as today.
|
||||
*
|
||||
* @param ts Unix timestamp in milliseconds
|
||||
* @returns True if today
|
||||
*/
|
||||
export const isToday = (ts: number): boolean => {
|
||||
const d = new Date(ts)
|
||||
const now = new Date()
|
||||
return (
|
||||
d.getFullYear() === now.getFullYear() &&
|
||||
d.getMonth() === now.getMonth() &&
|
||||
d.getDate() === now.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a timestamp corresponds to yesterday in local time.
|
||||
*
|
||||
* @param ts Unix timestamp in milliseconds
|
||||
* @returns True if yesterday
|
||||
*/
|
||||
export const isYesterday = (ts: number): boolean => {
|
||||
const d = new Date(ts)
|
||||
const now = new Date()
|
||||
const yest = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
|
||||
return (
|
||||
d.getFullYear() === yest.getFullYear() &&
|
||||
d.getMonth() === yest.getMonth() &&
|
||||
d.getDate() === yest.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Localized short month + day label, e.g. "Jan 2".
|
||||
*
|
||||
* @param ts Unix timestamp in milliseconds
|
||||
* @param locale BCP-47 locale string
|
||||
* @returns Localized month/day label
|
||||
*/
|
||||
export const formatShortMonthDay = (ts: number, locale: string): string => {
|
||||
const d = new Date(ts)
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).format(d)
|
||||
}
|
||||
|
||||
/**
|
||||
* Localized clock time, e.g. "10:05:06" with locale defaults for 12/24 hour.
|
||||
*
|
||||
* @param ts Unix timestamp in milliseconds
|
||||
* @param locale BCP-47 locale string
|
||||
* @returns Localized time string
|
||||
*/
|
||||
export const formatClockTime = (ts: number, locale: string): string => {
|
||||
const d = new Date(ts)
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).format(d)
|
||||
}
|
||||
27
src/utils/numberUtil.ts
Normal file
27
src/utils/numberUtil.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { clamp } from 'es-toolkit/math'
|
||||
|
||||
/**
|
||||
* Clamp a numeric value to an integer percent in the range [0, 100].
|
||||
*
|
||||
* @param value Numeric value expected to be a percentage (0-100)
|
||||
* @returns Integer percent between 0 and 100
|
||||
*/
|
||||
export const clampPercentInt = (value?: number): number => {
|
||||
const v = Math.round(value ?? 0)
|
||||
return clamp(v, 0, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a percentage (0-100) using the provided locale with 0 fraction digits.
|
||||
*
|
||||
* @param locale BCP-47 locale string
|
||||
* @param value0to100 Percent value in [0, 100]
|
||||
* @returns Localized percent string, e.g. "42%"
|
||||
*/
|
||||
export const formatPercent0 = (locale: string, value0to100: number): string => {
|
||||
const v = clampPercentInt(value0to100)
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'percent',
|
||||
maximumFractionDigits: 0
|
||||
}).format((v || 0) / 100)
|
||||
}
|
||||
150
src/utils/queueDisplay.ts
Normal file
150
src/utils/queueDisplay.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import type { JobState } from '@/types/queue'
|
||||
import { clampPercentInt, formatPercent0 } from '@/utils/numberUtil'
|
||||
|
||||
type BuildJobDisplayCtx = {
|
||||
t: (k: string, v?: Record<string, any>) => string
|
||||
locale: string
|
||||
formatClockTimeFn: (ts: number, locale: string) => string
|
||||
isActive: boolean
|
||||
totalPercent?: number
|
||||
currentNodePercent?: number
|
||||
currentNodeName?: string
|
||||
showAddedHint?: boolean
|
||||
}
|
||||
|
||||
type JobDisplay = {
|
||||
iconName: string
|
||||
iconImageUrl?: string
|
||||
primary: string
|
||||
secondary: string
|
||||
showClear: boolean
|
||||
}
|
||||
|
||||
export const iconForJobState = (state: JobState): string => {
|
||||
switch (state) {
|
||||
case 'pending':
|
||||
return 'icon-[lucide--clock]'
|
||||
case 'initialization':
|
||||
return 'icon-[lucide--server-crash]'
|
||||
case 'running':
|
||||
return 'icon-[lucide--zap]'
|
||||
case 'completed':
|
||||
return 'icon-[lucide--check-check]'
|
||||
case 'failed':
|
||||
return 'icon-[lucide--alert-circle]'
|
||||
default:
|
||||
return 'icon-[lucide--circle]'
|
||||
}
|
||||
}
|
||||
|
||||
const buildTitle = (task: TaskItemImpl, t: (k: string) => string): string => {
|
||||
const prefix = t('g.job')
|
||||
const shortId = String(task.promptId ?? '').split('-')[0]
|
||||
const idx = task.queueIndex
|
||||
if (typeof idx === 'number') return `${prefix} #${idx}`
|
||||
if (shortId) return `${prefix} ${shortId}`
|
||||
return prefix
|
||||
}
|
||||
|
||||
const buildQueuedTime = (
|
||||
task: TaskItemImpl,
|
||||
locale: string,
|
||||
formatClockTimeFn: (ts: number, locale: string) => string
|
||||
): string => {
|
||||
const ts = task.createTime
|
||||
return ts !== undefined ? formatClockTimeFn(ts, locale) : ''
|
||||
}
|
||||
|
||||
export const buildJobDisplay = (
|
||||
task: TaskItemImpl,
|
||||
state: JobState,
|
||||
ctx: BuildJobDisplayCtx
|
||||
): JobDisplay => {
|
||||
if (state === 'pending') {
|
||||
if (ctx.showAddedHint) {
|
||||
return {
|
||||
iconName: 'icon-[lucide--check]',
|
||||
primary: ctx.t('queue.jobAddedToQueue'),
|
||||
secondary: buildQueuedTime(task, ctx.locale, ctx.formatClockTimeFn),
|
||||
showClear: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
iconName: iconForJobState(state),
|
||||
primary: ctx.t('queue.inQueue'),
|
||||
secondary: buildQueuedTime(task, ctx.locale, ctx.formatClockTimeFn),
|
||||
showClear: true
|
||||
}
|
||||
}
|
||||
if (state === 'initialization') {
|
||||
return {
|
||||
iconName: iconForJobState(state),
|
||||
primary: ctx.t('queue.initializingAlmostReady'),
|
||||
secondary: buildQueuedTime(task, ctx.locale, ctx.formatClockTimeFn),
|
||||
showClear: true
|
||||
}
|
||||
}
|
||||
if (state === 'running') {
|
||||
if (ctx.isActive) {
|
||||
const total = formatPercent0(
|
||||
ctx.locale,
|
||||
clampPercentInt(ctx.totalPercent)
|
||||
)
|
||||
const curr = formatPercent0(
|
||||
ctx.locale,
|
||||
clampPercentInt(ctx.currentNodePercent)
|
||||
)
|
||||
const primary = ctx.t('sideToolbar.queueProgressOverlay.total', {
|
||||
percent: total
|
||||
})
|
||||
const right = ctx.currentNodeName
|
||||
? `${ctx.currentNodeName} ${ctx.t(
|
||||
'sideToolbar.queueProgressOverlay.colonPercent',
|
||||
{ percent: curr }
|
||||
)}`
|
||||
: ''
|
||||
return {
|
||||
iconName: iconForJobState(state),
|
||||
primary,
|
||||
secondary: right,
|
||||
showClear: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
iconName: iconForJobState(state),
|
||||
primary: ctx.t('g.running'),
|
||||
secondary: '',
|
||||
showClear: true
|
||||
}
|
||||
}
|
||||
if (state === 'completed') {
|
||||
const time = task.executionTimeInSeconds
|
||||
const preview = task.previewOutput
|
||||
const iconImageUrl = preview && preview.isImage ? preview.url : undefined
|
||||
return {
|
||||
iconName: iconForJobState(state),
|
||||
iconImageUrl,
|
||||
primary:
|
||||
preview?.filename && preview.filename.length
|
||||
? preview.filename
|
||||
: buildTitle(task, ctx.t),
|
||||
secondary: time !== undefined ? `${time.toFixed(2)}s` : '',
|
||||
showClear: false
|
||||
}
|
||||
}
|
||||
if (state === 'failed') {
|
||||
return {
|
||||
iconName: iconForJobState(state),
|
||||
primary: ctx.t('g.failed'),
|
||||
secondary: ctx.t('g.failed'),
|
||||
showClear: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
iconName: iconForJobState(state),
|
||||
primary: buildTitle(task, ctx.t),
|
||||
secondary: '',
|
||||
showClear: true
|
||||
}
|
||||
}
|
||||
29
src/utils/queueUtil.ts
Normal file
29
src/utils/queueUtil.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import type { JobState } from '@/types/queue'
|
||||
|
||||
/**
|
||||
* Map a task to a UI job state, including initialization override.
|
||||
*
|
||||
* @param task Task item from the queue store
|
||||
* @param isInitializing True if the prompt is currently initializing
|
||||
* @returns JobState for UI
|
||||
*/
|
||||
export const jobStateFromTask = (
|
||||
task: TaskItemImpl,
|
||||
isInitializing: boolean
|
||||
): JobState => {
|
||||
if (isInitializing) return 'initialization'
|
||||
const status = task.displayStatus
|
||||
switch (status) {
|
||||
case 'Running':
|
||||
return 'running'
|
||||
case 'Pending':
|
||||
return 'pending'
|
||||
case 'Completed':
|
||||
return 'completed'
|
||||
case 'Failed':
|
||||
case 'Cancelled':
|
||||
return 'failed'
|
||||
}
|
||||
return 'failed'
|
||||
}
|
||||
Reference in New Issue
Block a user