mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-03 04:00:31 +00:00
feat: add model download progress dialog (#7897)
## Summary Add a progress dialog for model downloads that appears when downloads are active. ## Changes - Add `ModelImportProgressDialog` component for showing download progress - Add `ProgressToastItem` component for individual download job display - Add `StatusBadge` component for status indicators - Extend `assetDownloadStore` with: - `finishedDownloads` computed for completed/failed jobs - `hasDownloads` computed for dialog visibility - `clearFinishedDownloads()` to dismiss finished downloads - Dialog visibility driven by store state - Closing dialog clears finished downloads - Filter dropdown to show all/completed/failed downloads - Expandable/collapsible UI with animated transitions - Update AGENTS.md with import type convention and pluralization note ## Testing - Start a model download and verify the dialog appears - Verify expand/collapse animation works - Verify filter dropdown works - Verify closing the dialog clears finished downloads - Verify dialog hides when no downloads remain ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7897-feat-add-model-download-progress-dialog-2e26d73d36508116960eff6fbe7dc392) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { AssetDownloadWsMessage } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
interface AssetDownload {
|
||||
export interface AssetDownload {
|
||||
taskId: string
|
||||
assetId: string
|
||||
assetName: string
|
||||
@@ -24,32 +21,35 @@ interface CompletedDownload {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const PROGRESS_TOAST_INTERVAL_MS = 5000
|
||||
const PROCESSED_TASK_CLEANUP_MS = 60000
|
||||
const MAX_COMPLETED_DOWNLOADS = 10
|
||||
|
||||
export const useAssetDownloadStore = defineStore('assetDownload', () => {
|
||||
const toastStore = useToastStore()
|
||||
|
||||
/** Map of task IDs to their download progress data */
|
||||
const activeDownloads = ref<Map<string, AssetDownload>>(new Map())
|
||||
const downloads = ref<Map<string, AssetDownload>>(new Map())
|
||||
|
||||
/** Map of task IDs to model types, used to track which model type to refresh after download completes */
|
||||
const pendingModelTypes = new Map<string, string>()
|
||||
|
||||
/** Map of task IDs to timestamps, used to throttle progress toast notifications */
|
||||
const lastToastTime = new Map<string, number>()
|
||||
|
||||
/** Set of task IDs that have reached a terminal state (completed/failed), prevents duplicate processing */
|
||||
const processedTaskIds = new Set<string>()
|
||||
|
||||
/** Reactive signal for completed downloads */
|
||||
const completedDownloads = ref<CompletedDownload[]>([])
|
||||
|
||||
const hasActiveDownloads = computed(() => activeDownloads.value.size > 0)
|
||||
const downloadList = computed(() =>
|
||||
Array.from(activeDownloads.value.values())
|
||||
const downloadList = computed(() => Array.from(downloads.value.values()))
|
||||
const activeDownloads = computed(() =>
|
||||
downloadList.value.filter(
|
||||
(d) => d.status === 'created' || d.status === 'running'
|
||||
)
|
||||
)
|
||||
const finishedDownloads = computed(() =>
|
||||
downloadList.value.filter(
|
||||
(d) => d.status === 'completed' || d.status === 'failed'
|
||||
)
|
||||
)
|
||||
const hasActiveDownloads = computed(() => activeDownloads.value.length > 0)
|
||||
const hasDownloads = computed(() => downloads.value.size > 0)
|
||||
|
||||
/**
|
||||
* Associates a download task with its model type for later use when the download completes.
|
||||
@@ -82,19 +82,17 @@ export const useAssetDownloadStore = defineStore('assetDownload', () => {
|
||||
error: data.error
|
||||
}
|
||||
|
||||
downloads.value.set(data.task_id, download)
|
||||
|
||||
if (data.status === 'completed') {
|
||||
activeDownloads.value.delete(data.task_id)
|
||||
lastToastTime.delete(data.task_id)
|
||||
const modelType = pendingModelTypes.get(data.task_id)
|
||||
if (modelType) {
|
||||
// Emit completed download signal for other stores to react to
|
||||
const newDownload: CompletedDownload = {
|
||||
taskId: data.task_id,
|
||||
modelType,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
// Keep only the last MAX_COMPLETED_DOWNLOADS items (FIFO)
|
||||
const updated = [...completedDownloads.value, newDownload]
|
||||
if (updated.length > MAX_COMPLETED_DOWNLOADS) {
|
||||
updated.shift()
|
||||
@@ -107,65 +105,31 @@ export const useAssetDownloadStore = defineStore('assetDownload', () => {
|
||||
() => processedTaskIds.delete(data.task_id),
|
||||
PROCESSED_TASK_CLEANUP_MS
|
||||
)
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: st('assetBrowser.download.complete', 'Download complete'),
|
||||
detail: data.asset_name,
|
||||
life: 5000
|
||||
})
|
||||
} else if (data.status === 'failed') {
|
||||
activeDownloads.value.delete(data.task_id)
|
||||
lastToastTime.delete(data.task_id)
|
||||
pendingModelTypes.delete(data.task_id)
|
||||
setTimeout(
|
||||
() => processedTaskIds.delete(data.task_id),
|
||||
PROCESSED_TASK_CLEANUP_MS
|
||||
)
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: st('assetBrowser.download.failed', 'Download failed'),
|
||||
detail: data.error || data.asset_name,
|
||||
life: 8000
|
||||
})
|
||||
} else {
|
||||
activeDownloads.value.set(data.task_id, download)
|
||||
|
||||
const now = Date.now()
|
||||
const lastTime = lastToastTime.get(data.task_id) ?? 0
|
||||
const shouldShowToast = now - lastTime >= PROGRESS_TOAST_INTERVAL_MS
|
||||
|
||||
if (shouldShowToast) {
|
||||
lastToastTime.set(data.task_id, now)
|
||||
const progressPercent = Math.round(data.progress * 100)
|
||||
toastStore.add({
|
||||
severity: 'info',
|
||||
summary: st('assetBrowser.download.inProgress', 'Downloading...'),
|
||||
detail: `${data.asset_name} (${progressPercent}%)`,
|
||||
life: PROGRESS_TOAST_INTERVAL_MS,
|
||||
closable: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stopListener: (() => void) | undefined
|
||||
api.addEventListener('asset_download', handleAssetDownload)
|
||||
|
||||
function setup() {
|
||||
stopListener = useEventListener(api, 'asset_download', handleAssetDownload)
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
stopListener?.()
|
||||
stopListener = undefined
|
||||
function clearFinishedDownloads() {
|
||||
for (const download of finishedDownloads.value) {
|
||||
downloads.value.delete(download.taskId)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeDownloads,
|
||||
finishedDownloads,
|
||||
hasActiveDownloads,
|
||||
hasDownloads,
|
||||
downloadList,
|
||||
completedDownloads,
|
||||
trackDownload,
|
||||
setup,
|
||||
teardown
|
||||
clearFinishedDownloads
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user