fix: Model upload UI improvements (#7938)

## Summary

Polishing improvements for the model upload (BYOM) experience.

## Changes

- **HoneyToast z-index**: Increased from `z-50` to `z-9999` so the
ModelImportProgressDialog appears above modal backdrops
- **VideoHelpDialog**: Removed pixel-based max-width constraint, now
uses `90vw` to fill more of the viewport
- **UploadModelDialog responsive layout**: Added `max-height: 90vh` and
scrollable content area to prevent footer buttons from underflowing on
small screens
- **URL validity indicator**: Added green checkmark icon inside the URL
input when a valid Civitai or HuggingFace URL is entered

## Testing

- Open the model upload dialog and verify buttons remain accessible on
small viewport heights
- Enter a valid Civitai/HuggingFace URL and confirm the green checkmark
appears
- Open the help video and verify it uses more of the viewport
- Start a model download and verify the progress toast appears above any
open modals

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7938-fix-Model-upload-UI-improvements-2e46d73d365081a292f5fda70c6db0f5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Alexander Brown
2026-01-09 19:25:34 -08:00
committed by GitHub
parent c8e181c841
commit 4b095f3701
8 changed files with 110 additions and 66 deletions

View File

@@ -26,7 +26,7 @@ function toggle() {
v-if="visible"
role="status"
aria-live="polite"
class="fixed inset-x-0 bottom-6 z-50 mx-auto w-4/5 max-w-3xl overflow-hidden rounded-lg border border-border-default bg-base-background shadow-lg"
class="fixed inset-x-0 bottom-6 z-9999 mx-auto w-4/5 max-w-3xl overflow-hidden rounded-lg border border-border-default bg-base-background shadow-lg"
>
<div
:class="

View File

@@ -28,12 +28,13 @@ const isPending = computed(() => job.status === 'created')
)
"
>
<div class="flex flex-col">
<span class="text-sm text-base-foreground">{{ job.assetName }}</span>
<span v-if="isRunning" class="text-xs text-muted-foreground"> </span>
<div class="min-w-0 flex-1">
<span class="block truncate text-sm text-base-foreground">{{
job.assetName
}}</span>
</div>
<div class="flex items-center gap-2">
<div class="flex flex-shrink-0 items-center gap-2">
<template v-if="isFailed">
<i
class="icon-[lucide--circle-alert] size-4 text-destructive-background"

View File

@@ -104,10 +104,10 @@ function closeDialog() {
</Button>
<Popover
ref="filterPopoverRef"
append-to="body"
:dismissable="true"
:close-on-escape="true"
unstyled
:base-z-index="9999"
:pt="{
root: { class: 'absolute z-50' },
content: {
@@ -171,22 +171,24 @@ function closeDialog() {
<template #footer="{ toggle }">
<div
class="flex h-12 items-center justify-between border-t border-border-default px-4"
class="flex h-12 items-center justify-between gap-2 border-t border-border-default px-4"
>
<div class="flex items-center gap-2 text-sm">
<div class="flex min-w-0 flex-1 items-center gap-2 text-sm">
<template v-if="isInProgress">
<i
class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground"
class="icon-[lucide--loader-circle] size-4 flex-shrink-0 animate-spin text-muted-foreground"
/>
<span class="font-bold text-base-foreground">{{
currentJobName
}}</span>
<span
class="min-w-0 flex-1 truncate font-bold text-base-foreground"
>
{{ currentJobName }}
</span>
</template>
<template v-else-if="failedJobs.length > 0">
<i
class="icon-[lucide--circle-alert] size-4 text-destructive-background"
class="icon-[lucide--circle-alert] size-4 flex-shrink-0 text-destructive-background"
/>
<span class="font-bold text-base-foreground">
<span class="min-w-0 truncate font-bold text-base-foreground">
{{
t('progressToast.downloadsFailed', {
count: failedJobs.length
@@ -195,15 +197,20 @@ function closeDialog() {
</span>
</template>
<template v-else>
<i class="icon-[lucide--check-circle] size-4 text-jade-600" />
<span class="font-bold text-base-foreground">
<i
class="icon-[lucide--check-circle] size-4 flex-shrink-0 text-jade-600"
/>
<span class="min-w-0 truncate font-bold text-base-foreground">
{{ t('progressToast.allDownloadsCompleted') }}
</span>
</template>
</div>
<div class="flex items-center gap-2">
<span v-if="isInProgress" class="text-sm text-muted-foreground">
<div class="flex flex-shrink-0 items-center gap-2">
<span
v-if="isInProgress"
class="whitespace-nowrap text-sm text-muted-foreground"
>
{{
t('progressToast.progressCount', {
completed: completedCount,

View File

@@ -1,40 +1,43 @@
<template>
<div
class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6 border-t border-border-default"
class="upload-model-dialog flex flex-col gap-6 border-t border-border-default p-4 pt-6"
>
<!-- Step 1: Enter URL -->
<UploadModelUrlInput
v-if="currentStep === 1 && flags.huggingfaceModelImportEnabled"
v-model="wizardData.url"
:error="uploadError"
class="flex-1"
/>
<UploadModelUrlInputCivitai
v-else-if="currentStep === 1"
v-model="wizardData.url"
:error="uploadError"
/>
<!-- Scrollable content area -->
<div class="min-h-0 flex-auto basis-0 overflow-y-auto">
<!-- Step 1: Enter URL -->
<UploadModelUrlInput
v-if="currentStep === 1 && flags.huggingfaceModelImportEnabled"
v-model="wizardData.url"
:error="uploadError"
/>
<UploadModelUrlInputCivitai
v-else-if="currentStep === 1"
v-model="wizardData.url"
:error="uploadError"
/>
<!-- Step 2: Confirm Metadata -->
<UploadModelConfirmation
v-else-if="currentStep === 2"
v-model="selectedModelType"
:metadata="wizardData.metadata"
:preview-image="wizardData.previewImage"
/>
<!-- Step 2: Confirm Metadata -->
<UploadModelConfirmation
v-else-if="currentStep === 2"
v-model="selectedModelType"
:metadata="wizardData.metadata"
:preview-image="wizardData.previewImage"
/>
<!-- Step 3: Upload Progress -->
<UploadModelProgress
v-else-if="currentStep === 3 && uploadStatus != null"
:result="uploadStatus"
:error="uploadError"
:metadata="wizardData.metadata"
:model-type="selectedModelType"
:preview-image="wizardData.previewImage"
/>
<!-- Step 3: Upload Progress -->
<UploadModelProgress
v-else-if="currentStep === 3 && uploadStatus != null"
:result="uploadStatus"
:error="uploadError"
:metadata="wizardData.metadata"
:model-type="selectedModelType"
:preview-image="wizardData.previewImage"
/>
</div>
<!-- Navigation Footer -->
<!-- Navigation Footer - always visible -->
<UploadModelFooter
class="flex-shrink-0"
:current-step="currentStep"
:is-fetching-metadata="isFetchingMetadata"
:is-uploading="isUploading"
@@ -109,7 +112,8 @@ onMounted(() => {
.upload-model-dialog {
width: 90vw;
max-width: 800px;
min-height: 400px;
min-height: min(400px, 80vh);
max-height: 90vh;
}
@media (min-width: 640px) {

View File

@@ -45,13 +45,19 @@
</div>
<div class="flex flex-col gap-2">
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.genericLinkPlaceholder')"
class="w-full bg-secondary-background border-0 p-4"
data-attr="upload-model-step1-url-input"
/>
<div class="relative">
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.genericLinkPlaceholder')"
class="w-full border-0 bg-secondary-background p-4 pr-10"
data-attr="upload-model-step1-url-input"
/>
<i
v-if="isValidUrl"
class="icon-[lucide--circle-check-big] absolute top-1/2 right-3 size-5 -translate-y-1/2 text-green-500"
/>
</div>
<p v-if="error" class="text-xs text-error">
{{ error }}
</p>
@@ -78,6 +84,9 @@ import InputText from 'primevue/inputtext'
import { computed } from 'vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { civitaiImportSource } from '@/platform/assets/importSources/civitaiImportSource'
import { huggingfaceImportSource } from '@/platform/assets/importSources/huggingfaceImportSource'
import { validateSourceUrl } from '@/platform/assets/utils/importSourceUtil'
const { flags } = useFeatureFlags()
@@ -95,6 +104,14 @@ const url = computed({
set: (value: string) => emit('update:modelValue', value)
})
const importSources = [civitaiImportSource, huggingfaceImportSource]
const isValidUrl = computed(() => {
const trimmedUrl = url.value.trim()
if (!trimmedUrl) return false
return importSources.some((source) => validateSourceUrl(trimmedUrl, source))
})
const civitaiIcon = '/assets/images/civitai.svg'
const civitaiUrl = 'https://civitai.com/models'
const huggingFaceIcon = '/assets/images/hf-logo.svg'

View File

@@ -38,13 +38,19 @@
}}</span>
</template>
</i18n-t>
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
class="w-full bg-secondary-background border-0 p-4"
data-attr="upload-model-step1-url-input"
/>
<div class="relative">
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
class="w-full border-0 bg-secondary-background p-4 pr-10"
data-attr="upload-model-step1-url-input"
/>
<i
v-if="isValidUrl"
class="icon-[lucide--circle-check-big] absolute top-1/2 right-3 size-5 -translate-y-1/2 text-green-500"
/>
</div>
<p v-if="error" class="text-xs text-error">
{{ error }}
</p>
@@ -73,8 +79,11 @@
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { computed } from 'vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { civitaiImportSource } from '@/platform/assets/importSources/civitaiImportSource'
import { validateSourceUrl } from '@/platform/assets/utils/importSourceUtil'
const { flags } = useFeatureFlags()
@@ -83,4 +92,10 @@ defineProps<{
}>()
const url = defineModel<string>({ required: true })
const isValidUrl = computed(() => {
const trimmedUrl = url.value.trim()
if (!trimmedUrl) return false
return validateSourceUrl(trimmedUrl, civitaiImportSource)
})
</script>

View File

@@ -11,7 +11,7 @@
content: { class: '!p-0' },
mask: { class: '!bg-black/70' }
}"
:style="{ width: '90vw', maxWidth: '800px' }"
:style="{ width: '90vw' }"
>
<div class="relative">
<Button

View File

@@ -23,7 +23,7 @@ export function useModelUpload(
dialogComponentProps: {
pt: {
header: 'py-0! pl-0!',
content: 'p-0!'
content: 'p-0! overflow-y-hidden!'
}
}
})
@@ -41,7 +41,7 @@ export function useModelUpload(
dialogComponentProps: {
pt: {
header: 'py-0! pl-0!',
content: 'p-0!'
content: 'p-0! overflow-y-hidden!'
}
}
})