From c6f2ae3130480de01cc3db32263934c401892fef Mon Sep 17 00:00:00 2001
From: Alexander Brown
Date: Mon, 12 Jan 2026 17:25:01 -0800
Subject: [PATCH 01/22] fix(upload-model): UI/UX improvements for Upload Model
Dialog (#7969)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Addresses UI/UX feedback on the Upload Model Dialog (BYOM feature).
## Changes
1. **Standardize link styling** - Use consistent `text-muted-foreground
underline` for all links in both URL input variants
2. **Increase warning/example text font size** - Changed from 12px
(`text-xs`) to 14px (`text-sm`) for better readability
3. **Fix padding inconsistency** - Aligned padding between model name
box and SingleSelect dropdown; moved "Not sure?" help text inline with
the label
4. **Add "Upload Another" button** - Allows users to upload multiple
models without closing and reopening the dialog
## Testing
- Verified link styling consistency across both Civitai and generic URL
input components
- Confirmed padding alignment in confirmation step
- Tested Upload Another button resets wizard to step 1
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7969-fix-upload-model-UI-UX-improvements-for-Upload-Model-Dialog-2e66d73d3650815c8184cedb3d02672d)
by [Unito](https://www.unito.io)
---
src/locales/en/main.json | 1 +
.../components/UploadModelConfirmation.vue | 18 +++++-----
.../assets/components/UploadModelDialog.vue | 4 ++-
.../assets/components/UploadModelFooter.vue | 33 +++++++++++++------
.../assets/components/UploadModelUrlInput.vue | 6 ++--
.../components/UploadModelUrlInputCivitai.vue | 8 ++---
.../composables/useUploadModelWizard.ts | 17 +++++++++-
7 files changed, 60 insertions(+), 27 deletions(-)
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 6ef5343ac..81e73b986 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -2311,6 +2311,7 @@
"filterBy": "Filter by",
"findInLibrary": "Find it in the {type} section of the models library.",
"finish": "Finish",
+ "importAnother": "Import Another",
"genericLinkPlaceholder": "Paste link here",
"jobId": "Job ID",
"loadingModels": "Loading {type}...",
diff --git a/src/platform/assets/components/UploadModelConfirmation.vue b/src/platform/assets/components/UploadModelConfirmation.vue
index 881b47d32..53beb03a1 100644
--- a/src/platform/assets/components/UploadModelConfirmation.vue
+++ b/src/platform/assets/components/UploadModelConfirmation.vue
@@ -5,7 +5,7 @@
{{ $t('assetBrowser.modelAssociatedWithLink') }}
-
+
+
+
+
+ {{ $t('assetBrowser.notSureLeaveAsIs') }}
+
+
-
-
- {{ $t('assetBrowser.notSureLeaveAsIs') }}
-
diff --git a/src/platform/assets/components/UploadModelDialog.vue b/src/platform/assets/components/UploadModelDialog.vue
index 5013e418d..f7ad5de71 100644
--- a/src/platform/assets/components/UploadModelDialog.vue
+++ b/src/platform/assets/components/UploadModelDialog.vue
@@ -48,6 +48,7 @@
@fetch-metadata="handleFetchMetadata"
@upload="handleUploadModel"
@close="handleClose"
+ @import-another="resetWizard"
/>
@@ -85,7 +86,8 @@ const {
canUploadModel,
fetchMetadata,
uploadModel,
- goToPreviousStep
+ goToPreviousStep,
+ resetWizard
} = useUploadModelWizard(modelTypes)
async function handleFetchMetadata() {
diff --git a/src/platform/assets/components/UploadModelFooter.vue b/src/platform/assets/components/UploadModelFooter.vue
index 04c27c394..95ca59e45 100644
--- a/src/platform/assets/components/UploadModelFooter.vue
+++ b/src/platform/assets/components/UploadModelFooter.vue
@@ -80,21 +80,33 @@
{{ $t('assetBrowser.upload') }}
-
+
+
+
()
diff --git a/src/platform/assets/components/UploadModelUrlInput.vue b/src/platform/assets/components/UploadModelUrlInput.vue
index 8b814c0e5..d2c743d7a 100644
--- a/src/platform/assets/components/UploadModelUrlInput.vue
+++ b/src/platform/assets/components/UploadModelUrlInput.vue
@@ -20,7 +20,7 @@
:href="civitaiUrl"
target="_blank"
rel="noopener noreferrer"
- class="text-muted underline"
+ class="text-muted-foreground underline"
>
{{ $t('assetBrowser.providerCivitai') }},
@@ -35,7 +35,7 @@
:href="huggingFaceUrl"
target="_blank"
rel="noopener noreferrer"
- class="text-muted underline"
+ class="text-muted-foreground underline"
>
{{ $t('assetBrowser.providerHuggingFace') }}
@@ -58,7 +58,7 @@
class="icon-[lucide--circle-check-big] absolute top-1/2 right-3 size-5 -translate-y-1/2 text-green-500"
/>
-
+
{{ error }}
diff --git a/src/platform/assets/components/UploadModelUrlInputCivitai.vue b/src/platform/assets/components/UploadModelUrlInputCivitai.vue
index 39e244c86..82c1dcf31 100644
--- a/src/platform/assets/components/UploadModelUrlInputCivitai.vue
+++ b/src/platform/assets/components/UploadModelUrlInputCivitai.vue
@@ -11,7 +11,7 @@
{{ $t('assetBrowser.uploadModelDescription2Link') }}
@@ -51,14 +51,14 @@
class="icon-[lucide--circle-check-big] absolute top-1/2 right-3 size-5 -translate-y-1/2 text-green-500"
/>
-
+
{{ error }}
{{ $t('assetBrowser.civitaiLinkExampleStrong') }}
@@ -67,7 +67,7 @@
{{ $t('assetBrowser.civitaiLinkExampleUrl') }}
diff --git a/src/platform/assets/composables/useUploadModelWizard.ts b/src/platform/assets/composables/useUploadModelWizard.ts
index bac9c7eb2..207fed562 100644
--- a/src/platform/assets/composables/useUploadModelWizard.ts
+++ b/src/platform/assets/composables/useUploadModelWizard.ts
@@ -284,6 +284,20 @@ export function useUploadModelWizard(modelTypes: Ref) {
}
}
+ function resetWizard() {
+ currentStep.value = 1
+ isFetchingMetadata.value = false
+ isUploading.value = false
+ uploadStatus.value = undefined
+ uploadError.value = ''
+ wizardData.value = {
+ url: '',
+ name: '',
+ tags: []
+ }
+ selectedModelType.value = undefined
+ }
+
return {
// State
currentStep,
@@ -302,6 +316,7 @@ export function useUploadModelWizard(modelTypes: Ref) {
// Actions
fetchMetadata,
uploadModel,
- goToPreviousStep
+ goToPreviousStep,
+ resetWizard
}
}
From a166ec91a6e95b6dc4d2d139b929b1d24df5f608 Mon Sep 17 00:00:00 2001
From: Alexander Brown
Date: Mon, 12 Jan 2026 17:57:25 -0800
Subject: [PATCH 02/22] refactor: simplify asset download state and fix
deletion UI (#7974)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Refactors asset download state management and fixes asset deletion UI
issues.
## Changes
### assetDownloadStore simplification
- Replace `pendingModelTypes` Map with `modelType` stored directly on
`AssetDownload`
- Replace `completedDownloads` array with single `lastCompletedDownload`
ref
- `trackDownload()` now creates a placeholder entry immediately
- Use VueUse `whenever` instead of `watch` for cleaner null handling
### Asset refresh on download completion
- Refresh all relevant caches when a download completes:
- Node type caches (e.g., "CheckpointLoaderSimple")
- Tag caches (e.g., "tag:checkpoints")
- "All Models" cache ("tag:models")
### Asset deletion fix
- Remove local `deletedLocal` state that caused blank grid cells
- Emit `deleted` event from AssetCard → AssetGrid → AssetBrowserModal
- Trigger store refresh on deletion to properly remove the asset from
the grid
## Testing
- Added test for out-of-order websocket message handling
- All existing tests pass
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7974-refactor-simplify-asset-download-state-and-fix-deletion-UI-2e76d73d365081c69bcde9150a0d460c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp
---
.../assets/components/AssetBrowserModal.vue | 1 +
src/platform/assets/components/AssetCard.vue | 7 ++-
src/platform/assets/components/AssetGrid.vue | 2 +
.../composables/useUploadModelWizard.ts | 3 +-
src/stores/assetDownloadStore.test.ts | 20 +++++--
src/stores/assetDownloadStore.ts | 54 ++++++++++++-------
src/stores/assetsStore.ts | 30 +++++++----
7 files changed, 78 insertions(+), 39 deletions(-)
diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue
index 001387d05..b7db7494c 100644
--- a/src/platform/assets/components/AssetBrowserModal.vue
+++ b/src/platform/assets/components/AssetBrowserModal.vue
@@ -57,6 +57,7 @@
:assets="filteredAssets"
:loading="isLoading"
@asset-select="handleAssetSelectAndEmit"
+ @asset-deleted="refreshAssets"
/>
diff --git a/src/platform/assets/components/AssetCard.vue b/src/platform/assets/components/AssetCard.vue
index a24610022..11e404d41 100644
--- a/src/platform/assets/components/AssetCard.vue
+++ b/src/platform/assets/components/AssetCard.vue
@@ -1,6 +1,5 @@
()
-defineEmits<{
+const emit = defineEmits<{
select: [asset: AssetDisplayItem]
+ deleted: [asset: AssetDisplayItem]
}>()
const { t } = useI18n()
@@ -158,7 +158,6 @@ const descId = useId()
const isEditing = ref(false)
const newNameRef = ref
()
-const deletedLocal = ref(false)
const displayName = computed(() => newNameRef.value ?? asset.name)
@@ -211,7 +210,7 @@ function confirmDeletion() {
})
// Give a second for the completion message
await new Promise((resolve) => setTimeout(resolve, 1_000))
- deletedLocal.value = true
+ emit('deleted', asset)
} catch (err: unknown) {
console.error(err)
promptText.value = t('assetBrowser.deletion.failed', {
diff --git a/src/platform/assets/components/AssetGrid.vue b/src/platform/assets/components/AssetGrid.vue
index 138003393..da3c609b0 100644
--- a/src/platform/assets/components/AssetGrid.vue
+++ b/src/platform/assets/components/AssetGrid.vue
@@ -35,6 +35,7 @@
:asset="item"
:interactive="true"
@select="$emit('assetSelect', $event)"
+ @deleted="$emit('assetDeleted', $event)"
/>
@@ -56,6 +57,7 @@ const { assets } = defineProps<{
defineEmits<{
assetSelect: [asset: AssetDisplayItem]
+ assetDeleted: [asset: AssetDisplayItem]
}>()
const assetsWithKey = computed(() =>
diff --git a/src/platform/assets/composables/useUploadModelWizard.ts b/src/platform/assets/composables/useUploadModelWizard.ts
index 207fed562..9a945bad9 100644
--- a/src/platform/assets/composables/useUploadModelWizard.ts
+++ b/src/platform/assets/composables/useUploadModelWizard.ts
@@ -245,7 +245,8 @@ export function useUploadModelWizard(modelTypes: Ref) {
if (selectedModelType.value) {
assetDownloadStore.trackDownload(
result.task.task_id,
- selectedModelType.value
+ selectedModelType.value,
+ filename
)
}
uploadStatus.value = 'processing'
diff --git a/src/stores/assetDownloadStore.test.ts b/src/stores/assetDownloadStore.test.ts
index 28d58b9c1..5882c4b9a 100644
--- a/src/stores/assetDownloadStore.test.ts
+++ b/src/stores/assetDownloadStore.test.ts
@@ -117,15 +117,29 @@ describe('useAssetDownloadStore', () => {
it('associates task with model type for completion tracking', () => {
const store = useAssetDownloadStore()
- store.trackDownload('task-123', 'checkpoints')
+ store.trackDownload('task-123', 'checkpoints', 'model.safetensors')
dispatch(createDownloadMessage({ status: 'completed', progress: 100 }))
- expect(store.completedDownloads).toHaveLength(1)
- expect(store.completedDownloads[0]).toMatchObject({
+ expect(store.lastCompletedDownload).toMatchObject({
taskId: 'task-123',
modelType: 'checkpoints'
})
})
+
+ it('handles out-of-order messages where completed arrives before progress', () => {
+ const store = useAssetDownloadStore()
+
+ store.trackDownload('task-123', 'checkpoints', 'model.safetensors')
+
+ dispatch(createDownloadMessage({ status: 'completed', progress: 100 }))
+
+ dispatch(createDownloadMessage({ status: 'running', progress: 50 }))
+
+ expect(store.activeDownloads).toHaveLength(0)
+ expect(store.finishedDownloads).toHaveLength(1)
+ expect(store.finishedDownloads[0].status).toBe('completed')
+ expect(store.lastCompletedDownload?.modelType).toBe('checkpoints')
+ })
})
describe('stale download polling', () => {
diff --git a/src/stores/assetDownloadStore.ts b/src/stores/assetDownloadStore.ts
index 8aa5788bd..88818c31c 100644
--- a/src/stores/assetDownloadStore.ts
+++ b/src/stores/assetDownloadStore.ts
@@ -16,6 +16,7 @@ export interface AssetDownload {
lastUpdate: number
assetId?: string
error?: string
+ modelType?: string
}
interface CompletedDownload {
@@ -23,15 +24,29 @@ interface CompletedDownload {
modelType: string
timestamp: number
}
-
-const MAX_COMPLETED_DOWNLOADS = 10
const STALE_THRESHOLD_MS = 10_000
const POLL_INTERVAL_MS = 10_000
+function generateDownloadTrackingPlaceholder(
+ taskId: string,
+ modelType: string,
+ assetName: string
+): AssetDownload {
+ return {
+ taskId,
+ modelType,
+ assetName,
+ bytesTotal: 0,
+ bytesDownloaded: 0,
+ progress: 0,
+ status: 'created',
+ lastUpdate: Date.now()
+ }
+}
+
export const useAssetDownloadStore = defineStore('assetDownload', () => {
const downloads = ref