Files
ComfyUI_frontend/src/utils/queueDisplay.ts
Benjamin Lu 63f68543e4 [feat] Show "Finished in" duration for completed jobs in cloud (#6895)
## Summary
In cloud distribution, completed jobs now show "Finished in Xh Ym Zs" as
the primary text instead of the filename.

- Uses `formatDuration` to display time as `1h 30m 45s`, `30m 45s`, or
`45s`
- Gated with `isCloud` - non-cloud continues to show filename
- Added i18n key `queue.completedIn` for localization

Filename is not fetchable right now in cloud. This is what design wanted
as the alternative.

<img width="679" height="1097" alt="image"
src="https://github.com/user-attachments/assets/291deb42-77d8-4de9-b4f8-ee65f3c25011"
/>

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-27 16:41:45 -07:00

161 lines
4.3 KiB
TypeScript

import type { TaskItemImpl } from '@/stores/queueStore'
import type { JobState } from '@/types/queue'
import { formatDuration } from '@/utils/formatUtil'
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
/** Whether the app is running in cloud distribution */
isCloud?: 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--loader-circle]'
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
// Cloud shows "Completed in Xh Ym Zs", non-cloud shows filename
const primary = ctx.isCloud
? ctx.t('queue.completedIn', {
duration: formatDuration(task.executionTime ?? 0)
})
: preview?.filename && preview.filename.length
? preview.filename
: buildTitle(task, ctx.t)
return {
iconName: iconForJobState(state),
iconImageUrl,
primary,
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
}
}