From d965fb01fc1156ee84d470683a34be57eec22930 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sun, 7 Dec 2025 00:27:11 -0800 Subject: [PATCH] Add progress bars and progress text --- src/components/TopMenuSection.vue | 125 ++++++------- .../QueueInlineProgressSummary.stories.ts | 169 ++++++++++++++++++ .../queue/QueueInlineProgressSummary.vue | 64 +++++++ src/composables/queue/useCurrentNodeName.ts | 23 +++ src/composables/queue/useJobList.ts | 14 +- src/locales/en/main.json | 2 + 6 files changed, 325 insertions(+), 72 deletions(-) create mode 100644 src/components/queue/QueueInlineProgressSummary.stories.ts create mode 100644 src/components/queue/QueueInlineProgressSummary.vue create mode 100644 src/composables/queue/useCurrentNodeName.ts diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 9197a7f67..27a0a8f71 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -1,71 +1,75 @@ @@ -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' diff --git a/src/components/queue/QueueInlineProgressSummary.stories.ts b/src/components/queue/QueueInlineProgressSummary.stories.ts new file mode 100644 index 000000000..b17715a13 --- /dev/null +++ b/src/components/queue/QueueInlineProgressSummary.stories.ts @@ -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 + 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 = { + title: 'Queue/QueueInlineProgressSummary', + component: QueueInlineProgressSummary, + parameters: { + layout: 'padded', + backgrounds: { + default: 'light' + } + } +} + +export default meta +type Story = StoryObj + +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: ` +
+ +
+ ` + }) +} + +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: ` +
+ +
+ ` + }) +} + +export const ProgressWithoutCurrentNode: Story = { + render: () => ({ + components: { QueueInlineProgressSummary }, + setup() { + seedExecutionState({ + promptId: 'prompt-progress-only', + nodes: { '21': true, '22': true, '23': true, '24': false } + }) + + return {} + }, + template: ` +
+ +
+ ` + }) +} diff --git a/src/components/queue/QueueInlineProgressSummary.vue b/src/components/queue/QueueInlineProgressSummary.vue new file mode 100644 index 000000000..a81ede9e1 --- /dev/null +++ b/src/components/queue/QueueInlineProgressSummary.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/composables/queue/useCurrentNodeName.ts b/src/composables/queue/useCurrentNodeName.ts new file mode 100644 index 000000000..ffe1cceef --- /dev/null +++ b/src/composables/queue/useCurrentNodeName.ts @@ -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 } +} diff --git a/src/composables/queue/useJobList.ts b/src/composables/queue/useJobList.ts index 47996636b..50b37ba2b 100644 --- a/src/composables/queue/useJobList.ts +++ b/src/composables/queue/useJobList.ts @@ -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('All') const selectedWorkflowFilter = ref<'all' | 'current'>('all') const selectedSortMode = ref('mostRecent') diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 39d1b07f4..63193b603 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -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",