mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
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:
@@ -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="
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user