From 14d0ec73f6e5335eedd6fb3d58059aa8a841b305 Mon Sep 17 00:00:00 2001 From: Luke Mino-Altherr Date: Tue, 6 Jan 2026 11:43:11 -0800 Subject: [PATCH] [feat] Add async model upload with WebSocket progress tracking (#7746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds asynchronous model upload support with HTTP 202 responses - Implements WebSocket-based real-time download progress tracking via `asset_download` events - Creates `assetDownloadStore` for centralized download state management and toast notifications - Updates upload wizard UI to show "processing" state when downloads continue in background ## Changes - **Core**: New `assetDownloadStore` for managing async downloads with WebSocket events - **API**: Support for HTTP 202 async upload responses with task tracking - **UI**: Upload wizard now shows "processing" state and allows closing dialog during download - **Progress**: Periodic toast notifications (every 5s) during active downloads with completion/error toasts - **Schema**: Updated task statuses (`created`, `running`, `completed`, `failed`) and WebSocket message types ## Review Focus - WebSocket event handling and download state management in `assetDownloadStore` - Upload flow UX - users can now close the dialog and download continues in background - Toast notification frequency and timing - Schema alignment with backend async upload API Fixes #7748 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7746-feat-Add-async-model-upload-with-WebSocket-progress-tracking-2d36d73d3650811cb79ae06f470dcded) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Co-authored-by: GitHub Action --- src/composables/useFeatureFlags.ts | 13 +- src/locales/en/main.json | 12 +- .../assets/components/AssetBrowserModal.vue | 12 ++ .../assets/components/UploadModelDialog.vue | 4 +- .../assets/components/UploadModelFooter.vue | 13 +- .../assets/components/UploadModelProgress.vue | 46 +++-- .../assets/components/UploadModelUrlInput.vue | 6 +- .../components/UploadModelUrlInputCivitai.vue | 6 +- .../composables/useUploadModelWizard.ts | 149 +++++++++------ src/platform/assets/schemas/assetSchema.ts | 13 ++ src/platform/assets/services/assetService.ts | 74 +++++++- src/platform/remoteConfig/types.ts | 1 + src/schemas/apiSchema.ts | 12 ++ src/scripts/api.ts | 3 + src/stores/assetDownloadStore.ts | 171 ++++++++++++++++++ src/stores/assetsStore.test.ts | 3 + src/stores/assetsStore.ts | 36 +++- src/views/GraphView.vue | 4 + 18 files changed, 490 insertions(+), 88 deletions(-) create mode 100644 src/stores/assetDownloadStore.ts diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 697818c4e..043721b98 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -14,7 +14,8 @@ export enum ServerFeatureFlag { ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled', PRIVATE_MODELS_ENABLED = 'private_models_enabled', ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled', - HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled' + HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled', + ASYNC_MODEL_UPLOAD_ENABLED = 'async_model_upload_enabled' } /** @@ -65,7 +66,6 @@ export function useFeatureFlags() { ) }, get huggingfaceModelImportEnabled() { - // Check remote config first (from /api/features), fall back to websocket feature flags return ( remoteConfig.value.huggingface_model_import_enabled ?? api.getServerFeature( @@ -73,6 +73,15 @@ export function useFeatureFlags() { false ) ) + }, + get asyncModelUploadEnabled() { + return ( + remoteConfig.value.async_model_upload_enabled ?? + api.getServerFeature( + ServerFeatureFlag.ASYNC_MODEL_UPLOAD_ENABLED, + false + ) + ) } }) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 712865735..df360ad74 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -181,6 +181,7 @@ "missing": "Missing", "inProgress": "In progress", "completed": "Completed", + "downloading": "Downloading", "interrupted": "Interrupted", "queued": "Queued", "running": "Running", @@ -2286,14 +2287,16 @@ "noAssetsFound": "No assets found", "noModelsInFolder": "No {type} available in this folder", "notSureLeaveAsIs": "Not sure? Just leave this as is", + "noValidSourceDetected": "No valid import source detected", "onlyCivitaiUrlsSupported": "Only Civitai URLs are supported", "ownership": "Ownership", "ownershipAll": "All", "ownershipMyModels": "My models", "ownershipPublicModels": "Public models", + "processingModel": "Download started", + "processingModelDescription": "You can close this dialog. The download will continue in the background.", "providerCivitai": "Civitai", "providerHuggingFace": "Hugging Face", - "noValidSourceDetected": "No valid import source detected", "selectFrameworks": "Select Frameworks", "selectModelType": "Select model type", "selectProjects": "Select Projects", @@ -2318,8 +2321,8 @@ "uploadModelDescription1": "Paste a Civitai model download link to add it to your library.", "uploadModelDescription1Generic": "Paste a model download link to add it to your library.", "uploadModelDescription2": "Only links from {link} are supported at the moment", - "uploadModelDescription2Link": "https://civitai.com/models", "uploadModelDescription2Generic": "Only URLs from the following providers are supported:", + "uploadModelDescription2Link": "https://civitai.com/models", "uploadModelDescription3": "Max file size: {size}", "uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.", "uploadModelFromCivitai": "Import a model from Civitai", @@ -2343,6 +2346,11 @@ "complete": "{assetName} has been deleted.", "failed": "{assetName} could not be deleted." }, + "download": { + "complete": "Download complete", + "failed": "Download failed", + "inProgress": "Downloading {assetName}..." + }, "rename": { "failed": "Could not rename asset." } diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue index 40cc1fb1b..cd89591aa 100644 --- a/src/platform/assets/components/AssetBrowserModal.vue +++ b/src/platform/assets/components/AssetBrowserModal.vue @@ -83,6 +83,7 @@ import { useModelUpload } from '@/platform/assets/composables/useModelUpload' import type { AssetItem } from '@/platform/assets/schemas/assetSchema' import { assetService } from '@/platform/assets/services/assetService' import { formatCategoryLabel } from '@/platform/assets/utils/categoryLabel' +import { useAssetDownloadStore } from '@/stores/assetDownloadStore' import { useModelToNodeStore } from '@/stores/modelToNodeStore' import { OnCloseKey } from '@/types/widgetTypes' @@ -132,6 +133,17 @@ watch( { immediate: true } ) +const assetDownloadStore = useAssetDownloadStore() + +watch( + () => assetDownloadStore.hasActiveDownloads, + async (currentlyActive, previouslyActive) => { + if (previouslyActive && !currentlyActive) { + await execute() + } + } +) + const { searchQuery, selectedCategory, diff --git a/src/platform/assets/components/UploadModelDialog.vue b/src/platform/assets/components/UploadModelDialog.vue index d6be9e97e..3a7c76bfe 100644 --- a/src/platform/assets/components/UploadModelDialog.vue +++ b/src/platform/assets/components/UploadModelDialog.vue @@ -25,8 +25,8 @@ {{ $t('assetBrowser.upload') }} () const emit = defineEmits<{ diff --git a/src/platform/assets/components/UploadModelProgress.vue b/src/platform/assets/components/UploadModelProgress.vue index 839b5d3b4..e541e9bc0 100644 --- a/src/platform/assets/components/UploadModelProgress.vue +++ b/src/platform/assets/components/UploadModelProgress.vue @@ -1,22 +1,36 @@