Skip to content

Commit 14d0ec7

Browse files
luke-mino-altherrclaudeactions-user
authored
[feat] Add async model upload with WebSocket progress tracking (#7746)
## 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 <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com>
1 parent fbdaf5d commit 14d0ec7

File tree

18 files changed

+491
-89
lines changed

18 files changed

+491
-89
lines changed

src/composables/useFeatureFlags.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export enum ServerFeatureFlag {
1414
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled',
1515
PRIVATE_MODELS_ENABLED = 'private_models_enabled',
1616
ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled',
17-
HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled'
17+
HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled',
18+
ASYNC_MODEL_UPLOAD_ENABLED = 'async_model_upload_enabled'
1819
}
1920

2021
/**
@@ -65,14 +66,22 @@ export function useFeatureFlags() {
6566
)
6667
},
6768
get huggingfaceModelImportEnabled() {
68-
// Check remote config first (from /api/features), fall back to websocket feature flags
6969
return (
7070
remoteConfig.value.huggingface_model_import_enabled ??
7171
api.getServerFeature(
7272
ServerFeatureFlag.HUGGINGFACE_MODEL_IMPORT_ENABLED,
7373
false
7474
)
7575
)
76+
},
77+
get asyncModelUploadEnabled() {
78+
return (
79+
remoteConfig.value.async_model_upload_enabled ??
80+
api.getServerFeature(
81+
ServerFeatureFlag.ASYNC_MODEL_UPLOAD_ENABLED,
82+
false
83+
)
84+
)
7685
}
7786
})
7887

src/locales/en/main.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@
181181
"missing": "Missing",
182182
"inProgress": "In progress",
183183
"completed": "Completed",
184+
"downloading": "Downloading",
184185
"interrupted": "Interrupted",
185186
"queued": "Queued",
186187
"running": "Running",
@@ -2286,14 +2287,16 @@
22862287
"noAssetsFound": "No assets found",
22872288
"noModelsInFolder": "No {type} available in this folder",
22882289
"notSureLeaveAsIs": "Not sure? Just leave this as is",
2290+
"noValidSourceDetected": "No valid import source detected",
22892291
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
22902292
"ownership": "Ownership",
22912293
"ownershipAll": "All",
22922294
"ownershipMyModels": "My models",
22932295
"ownershipPublicModels": "Public models",
2296+
"processingModel": "Download started",
2297+
"processingModelDescription": "You can close this dialog. The download will continue in the background.",
22942298
"providerCivitai": "Civitai",
22952299
"providerHuggingFace": "Hugging Face",
2296-
"noValidSourceDetected": "No valid import source detected",
22972300
"selectFrameworks": "Select Frameworks",
22982301
"selectModelType": "Select model type",
22992302
"selectProjects": "Select Projects",
@@ -2318,8 +2321,8 @@
23182321
"uploadModelDescription1": "Paste a Civitai model download link to add it to your library.",
23192322
"uploadModelDescription1Generic": "Paste a model download link to add it to your library.",
23202323
"uploadModelDescription2": "Only links from {link} are supported at the moment",
2321-
"uploadModelDescription2Link": "https://civitai.com/models",
23222324
"uploadModelDescription2Generic": "Only URLs from the following providers are supported:",
2325+
"uploadModelDescription2Link": "https://civitai.com/models",
23232326
"uploadModelDescription3": "Max file size: {size}",
23242327
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
23252328
"uploadModelFromCivitai": "Import a model from Civitai",
@@ -2343,6 +2346,11 @@
23432346
"complete": "{assetName} has been deleted.",
23442347
"failed": "{assetName} could not be deleted."
23452348
},
2349+
"download": {
2350+
"complete": "Download complete",
2351+
"failed": "Download failed",
2352+
"inProgress": "Downloading {assetName}..."
2353+
},
23462354
"rename": {
23472355
"failed": "Could not rename asset."
23482356
}

src/platform/assets/components/AssetBrowserModal.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import { useModelUpload } from '@/platform/assets/composables/useModelUpload'
8383
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
8484
import { assetService } from '@/platform/assets/services/assetService'
8585
import { formatCategoryLabel } from '@/platform/assets/utils/categoryLabel'
86+
import { useAssetDownloadStore } from '@/stores/assetDownloadStore'
8687
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
8788
import { OnCloseKey } from '@/types/widgetTypes'
8889
@@ -132,6 +133,17 @@ watch(
132133
{ immediate: true }
133134
)
134135
136+
const assetDownloadStore = useAssetDownloadStore()
137+
138+
watch(
139+
() => assetDownloadStore.hasActiveDownloads,
140+
async (currentlyActive, previouslyActive) => {
141+
if (previouslyActive && !currentlyActive) {
142+
await execute()
143+
}
144+
}
145+
)
146+
135147
const {
136148
searchQuery,
137149
selectedCategory,

src/platform/assets/components/UploadModelDialog.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525

2626
<!-- Step 3: Upload Progress -->
2727
<UploadModelProgress
28-
v-else-if="currentStep === 3"
29-
:status="uploadStatus"
28+
v-else-if="currentStep === 3 && uploadStatus != null"
29+
:result="uploadStatus"
3030
:error="uploadError"
3131
:metadata="wizardData.metadata"
3232
:model-type="selectedModelType"

src/platform/assets/components/UploadModelFooter.vue

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,19 @@
8181
<span>{{ $t('assetBrowser.upload') }}</span>
8282
</Button>
8383
<Button
84-
v-else-if="currentStep === 3 && uploadStatus === 'success'"
84+
v-else-if="
85+
currentStep === 3 &&
86+
(uploadStatus === 'success' || uploadStatus === 'processing')
87+
"
8588
variant="secondary"
8689
data-attr="upload-model-step3-finish-button"
8790
@click="emit('close')"
8891
>
89-
{{ $t('assetBrowser.finish') }}
92+
{{
93+
uploadStatus === 'processing'
94+
? $t('g.close')
95+
: $t('assetBrowser.finish')
96+
}}
9097
</Button>
9198
<VideoHelpDialog
9299
v-model="showCivitaiHelp"
@@ -119,7 +126,7 @@ defineProps<{
119126
isUploading: boolean
120127
canFetchMetadata: boolean
121128
canUploadModel: boolean
122-
uploadStatus: 'idle' | 'uploading' | 'success' | 'error'
129+
uploadStatus?: 'processing' | 'success' | 'error'
123130
}>()
124131
125132
const emit = defineEmits<{

src/platform/assets/components/UploadModelProgress.vue

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
<template>
22
<div class="flex flex-1 flex-col gap-6 text-sm text-muted-foreground">
3-
<!-- Uploading State -->
4-
<div
5-
v-if="status === 'uploading'"
6-
class="flex flex-1 flex-col items-center justify-center gap-2"
7-
>
8-
<i
9-
class="icon-[lucide--loader-circle] animate-spin text-6xl text-muted-foreground"
10-
/>
11-
<div class="text-center">
12-
<p class="m-0 font-bold">
13-
{{ $t('assetBrowser.uploadingModel') }}
14-
</p>
3+
<!-- Processing State (202 async download in progress) -->
4+
<div v-if="result === 'processing'" class="flex flex-col gap-2">
5+
<p class="m-0 font-bold">
6+
{{ $t('assetBrowser.processingModel') }}
7+
</p>
8+
<p class="m-0">
9+
{{ $t('assetBrowser.processingModelDescription') }}
10+
</p>
11+
12+
<div
13+
class="flex flex-row items-center gap-3 p-4 bg-modal-card-background rounded-lg"
14+
>
15+
<img
16+
v-if="previewImage"
17+
:src="previewImage"
18+
:alt="metadata?.filename || metadata?.name || 'Model preview'"
19+
class="w-14 h-14 rounded object-cover flex-shrink-0"
20+
/>
21+
<div class="flex flex-col justify-center items-start gap-1 flex-1">
22+
<p class="text-base-foreground m-0">
23+
{{ metadata?.filename || metadata?.name }}
24+
</p>
25+
<p class="text-sm text-muted m-0">
26+
{{ modelType }}
27+
</p>
28+
</div>
1529
</div>
1630
</div>
1731

1832
<!-- Success State -->
19-
<div v-else-if="status === 'success'" class="flex flex-col gap-2">
33+
<div v-else-if="result === 'success'" class="flex flex-col gap-2">
2034
<p class="m-0 font-bold">
2135
{{ $t('assetBrowser.modelUploaded') }}
2236
</p>
@@ -47,7 +61,7 @@
4761

4862
<!-- Error State -->
4963
<div
50-
v-else-if="status === 'error'"
64+
v-else-if="result === 'error'"
5165
class="flex flex-1 flex-col items-center justify-center gap-6"
5266
>
5367
<i class="icon-[lucide--x-circle] text-6xl text-error" />
@@ -66,8 +80,8 @@
6680
<script setup lang="ts">
6781
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
6882
69-
defineProps<{
70-
status: 'idle' | 'uploading' | 'success' | 'error'
83+
const { result } = defineProps<{
84+
result: 'processing' | 'success' | 'error'
7185
error?: string
7286
metadata?: AssetMetadata
7387
modelType?: string

src/platform/assets/components/UploadModelUrlInput.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
<p v-if="error" class="text-xs text-error">
5656
{{ error }}
5757
</p>
58-
<p v-else class="text-foreground">
58+
<p v-else-if="!flags.asyncModelUploadEnabled" class="text-foreground">
5959
<i18n-t keypath="assetBrowser.maxFileSize" tag="span">
6060
<template #size>
6161
<span class="font-bold italic">{{
@@ -77,6 +77,10 @@
7777
import InputText from 'primevue/inputtext'
7878
import { computed } from 'vue'
7979
80+
import { useFeatureFlags } from '@/composables/useFeatureFlags'
81+
82+
const { flags } = useFeatureFlags()
83+
8084
const props = defineProps<{
8185
modelValue: string
8286
error?: string

src/platform/assets/components/UploadModelUrlInputCivitai.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</template>
1919
</i18n-t>
2020
</li>
21-
<li>
21+
<li v-if="!flags.asyncModelUploadEnabled">
2222
<i18n-t keypath="assetBrowser.uploadModelDescription3" tag="span">
2323
<template #size>
2424
<span class="font-bold italic">{{
@@ -74,6 +74,10 @@
7474
<script setup lang="ts">
7575
import InputText from 'primevue/inputtext'
7676
77+
import { useFeatureFlags } from '@/composables/useFeatureFlags'
78+
79+
const { flags } = useFeatureFlags()
80+
7781
defineProps<{
7882
error?: string
7983
}>()

0 commit comments

Comments
 (0)