Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1613978
feat(assets): add async model upload with feature flag gating
luke-mino-altherr Dec 23, 2025
38e0877
feat(assets): add WebSocket support for asset download progress
luke-mino-altherr Dec 23, 2025
13b6f2f
feat(assets): add assetDownloadStore for WebSocket progress and toast…
luke-mino-altherr Dec 23, 2025
aff8e34
feat(assets): add completion callback support for async downloads
luke-mino-altherr Dec 23, 2025
25b146d
refactor(assets): move refresh logic into assetDownloadStore
luke-mino-altherr Dec 23, 2025
70cd736
feat(assets): refine async upload UX with progress toasts and status …
luke-mino-altherr Dec 23, 2025
fb835c7
feat(assets): improve upload dialog UX for async uploads
luke-mino-altherr Dec 24, 2025
50995cc
feat(assets): make download progress toasts dismissible and improve e…
luke-mino-altherr Dec 24, 2025
3c575ce
refactor: remove unused useAssetDownloadProgress composable
luke-mino-altherr Dec 24, 2025
57c91e5
feat(assets): refresh asset browser after downloads complete
luke-mino-altherr Dec 24, 2025
81c5336
fix build errors
luke-mino-altherr Dec 24, 2025
af13d4e
fix(assetDownload): handle race condition and add listener cleanup
luke-mino-altherr Dec 31, 2025
4615430
refactor: invert dependency between assetDownloadStore and assetsStore
luke-mino-altherr Jan 5, 2026
df355de
fix: remove unsafe non-null assertion in UploadModelDialog
luke-mino-altherr Jan 6, 2026
c5f936b
docs: add JSDoc comments to assetDownloadStore internal state
luke-mino-altherr Jan 6, 2026
2fc3526
fix: guard against undefined nodeDef and optimize watch performance
luke-mino-altherr Jan 6, 2026
2be1fe7
fix: add missing api mock methods in assetsStore test
luke-mino-altherr Jan 6, 2026
df75437
code review comments from rabbit
luke-mino-altherr Jan 6, 2026
06660b9
[automated] Apply ESLint and Prettier fixes
actions-user Jan 6, 2026
f45257c
refactor: improve variable naming and error handling in asset upload
luke-mino-altherr Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/composables/useFeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

/**
Expand Down Expand Up @@ -65,14 +66,22 @@ 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(
ServerFeatureFlag.HUGGINGFACE_MODEL_IMPORT_ENABLED,
false
)
)
},
get asyncModelUploadEnabled() {
return (
remoteConfig.value.async_model_upload_enabled ??
api.getServerFeature(
ServerFeatureFlag.ASYNC_MODEL_UPLOAD_ENABLED,
false
)
)
}
})

Expand Down
12 changes: 10 additions & 2 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"missing": "Missing",
"inProgress": "In progress",
"completed": "Completed",
"downloading": "Downloading",
"interrupted": "Interrupted",
"queued": "Queued",
"running": "Running",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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}..."
},
Comment on lines +2349 to +2353
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider including asset name in complete/failed messages for consistency.

The inProgress message includes {assetName}, but complete and failed messages don't. This creates an inconsistency compared to the deletion messages above (lines 2345-2347), which include the asset name in all states.

Including the asset name in all download states would provide better user feedback, especially when multiple downloads are running simultaneously:

💡 Suggested improvement for consistency
 "download": {
-  "complete": "Download complete",
-  "failed": "Download failed",
+  "complete": "{assetName} download complete",
+  "failed": "{assetName} download failed",
   "inProgress": "Downloading {assetName}..."
 },
🤖 Prompt for AI Agents
In @src/locales/en/main.json around lines 2349 - 2353, The download messages are
inconsistent: "download.inProgress" uses {assetName} but "download.complete" and
"download.failed" don't; update the locale entries "download.complete" and
"download.failed" to include the {assetName} placeholder (e.g., "Download
complete: {assetName}" / "Download failed: {assetName}") so they match
"download.inProgress" and ensure translators/localization consumers are aware of
the new parameter.

"rename": {
"failed": "Could not rename asset."
}
Expand Down
12 changes: 12 additions & 0 deletions src/platform/assets/components/AssetBrowserModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -132,6 +133,17 @@ watch(
{ immediate: true }
)

const assetDownloadStore = useAssetDownloadStore()

watch(
() => assetDownloadStore.hasActiveDownloads,
async (currentlyActive, previouslyActive) => {
if (previouslyActive && !currentlyActive) {
await execute()
}
}
)

const {
searchQuery,
selectedCategory,
Expand Down
4 changes: 2 additions & 2 deletions src/platform/assets/components/UploadModelDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@

<!-- Step 3: Upload Progress -->
<UploadModelProgress
v-else-if="currentStep === 3"
:status="uploadStatus"
v-else-if="currentStep === 3 && uploadStatus != null"
:result="uploadStatus"
:error="uploadError"
:metadata="wizardData.metadata"
:model-type="selectedModelType"
Expand Down
13 changes: 10 additions & 3 deletions src/platform/assets/components/UploadModelFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,19 @@
<span>{{ $t('assetBrowser.upload') }}</span>
</Button>
<Button
v-else-if="currentStep === 3 && uploadStatus === 'success'"
v-else-if="
currentStep === 3 &&
(uploadStatus === 'success' || uploadStatus === 'processing')
"
variant="secondary"
data-attr="upload-model-step3-finish-button"
@click="emit('close')"
>
{{ $t('assetBrowser.finish') }}
{{
uploadStatus === 'processing'
? $t('g.close')
: $t('assetBrowser.finish')
}}
</Button>
<VideoHelpDialog
v-model="showCivitaiHelp"
Expand Down Expand Up @@ -119,7 +126,7 @@ defineProps<{
isUploading: boolean
canFetchMetadata: boolean
canUploadModel: boolean
uploadStatus: 'idle' | 'uploading' | 'success' | 'error'
uploadStatus?: 'processing' | 'success' | 'error'
}>()

const emit = defineEmits<{
Expand Down
46 changes: 30 additions & 16 deletions src/platform/assets/components/UploadModelProgress.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
<template>
<div class="flex flex-1 flex-col gap-6 text-sm text-muted-foreground">
<!-- Uploading State -->
<div
v-if="status === 'uploading'"
class="flex flex-1 flex-col items-center justify-center gap-2"
>
<i
class="icon-[lucide--loader-circle] animate-spin text-6xl text-muted-foreground"
/>
<div class="text-center">
<p class="m-0 font-bold">
{{ $t('assetBrowser.uploadingModel') }}
</p>
<!-- Processing State (202 async download in progress) -->
<div v-if="result === 'processing'" class="flex flex-col gap-2">
<p class="m-0 font-bold">
{{ $t('assetBrowser.processingModel') }}
</p>
<p class="m-0">
{{ $t('assetBrowser.processingModelDescription') }}
</p>

<div
class="flex flex-row items-center gap-3 p-4 bg-modal-card-background rounded-lg"
>
<img
v-if="previewImage"
:src="previewImage"
:alt="metadata?.filename || metadata?.name || 'Model preview'"
class="w-14 h-14 rounded object-cover flex-shrink-0"
/>
<div class="flex flex-col justify-center items-start gap-1 flex-1">
<p class="text-base-foreground m-0">
{{ metadata?.filename || metadata?.name }}
</p>
<p class="text-sm text-muted m-0">
{{ modelType }}
</p>
</div>
</div>
Comment on lines +12 to 29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting duplicated preview card markup.

The model preview card structure (image + metadata) is duplicated between the processing and success states. While functional, consider extracting this into a reusable component to follow DRY principles.

Also applies to: 41-59

🤖 Prompt for AI Agents
In @src/platform/assets/components/UploadModelProgress.vue around lines 12 - 29,
Extract the duplicated preview card markup in UploadModelProgress.vue into a
reusable component (e.g., ModelPreviewCard) that accepts props previewImage,
metadata, and modelType and renders the image, filename/name fallbacks, and
model type; then replace the duplicated blocks (the current markup using
previewImage, metadata, and modelType at lines 12-29 and 41-59) with the new
<ModelPreviewCard> and import/register it in UploadModelProgress.vue so both the
processing and success states reuse the single component.

</div>

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

<!-- Error State -->
<div
v-else-if="status === 'error'"
v-else-if="result === 'error'"
class="flex flex-1 flex-col items-center justify-center gap-6"
>
<i class="icon-[lucide--x-circle] text-6xl text-error" />
Expand All @@ -66,8 +80,8 @@
<script setup lang="ts">
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'

defineProps<{
status: 'idle' | 'uploading' | 'success' | 'error'
const { result } = defineProps<{
result: 'processing' | 'success' | 'error'
error?: string
metadata?: AssetMetadata
modelType?: string
Expand Down
6 changes: 5 additions & 1 deletion src/platform/assets/components/UploadModelUrlInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
<p v-if="error" class="text-xs text-error">
{{ error }}
</p>
<p v-else class="text-foreground">
<p v-else-if="!flags.asyncModelUploadEnabled" class="text-foreground">
<i18n-t keypath="assetBrowser.maxFileSize" tag="span">
<template #size>
<span class="font-bold italic">{{
Expand All @@ -77,6 +77,10 @@
import InputText from 'primevue/inputtext'
import { computed } from 'vue'

import { useFeatureFlags } from '@/composables/useFeatureFlags'

const { flags } = useFeatureFlags()

const props = defineProps<{
modelValue: string
error?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</template>
</i18n-t>
</li>
<li>
<li v-if="!flags.asyncModelUploadEnabled">
<i18n-t keypath="assetBrowser.uploadModelDescription3" tag="span">
<template #size>
<span class="font-bold italic">{{
Expand Down Expand Up @@ -74,6 +74,10 @@
<script setup lang="ts">
import InputText from 'primevue/inputtext'

import { useFeatureFlags } from '@/composables/useFeatureFlags'

const { flags } = useFeatureFlags()

defineProps<{
error?: string
}>()
Expand Down
Loading
Loading