BYOM: Model Import Wizard (#6949)

## Summary

Design alignment for the model import wizard.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6949-BYOM-Model-Import-Wizard-2b76d73d365081a48632c40430e05c93)
by [Unito](https://www.unito.io)
This commit is contained in:
Alexander Brown
2025-11-25 19:19:16 -08:00
committed by GitHub
parent 5fa76e23d9
commit e6332046b0
13 changed files with 92 additions and 69 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -152,7 +152,7 @@ const {
popoverMaxWidth?: string popoverMaxWidth?: string
}>() }>()
const selectedItem = defineModel<string | null>({ required: true }) const selectedItem = defineModel<string | undefined>({ required: true })
const { t } = useI18n() const { t } = useI18n()

View File

@@ -2092,11 +2092,11 @@
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.", "uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported", "onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
"uploadModelDescription1": "Paste a Civitai model download link to add it to your library.", "uploadModelDescription1": "Paste a Civitai model download link to add it to your library.",
"uploadModelDescription2": "Only links from https://civitai.com are supported at the moment", "uploadModelDescription2": "Only links from <a href=\"https://civitai.com\" target=\"_blank\" class=\"text-muted-foreground\">https://civitai.com</a> are supported at the moment",
"uploadModelDescription3": "Max file size: 1 GB", "uploadModelDescription3": "Max file size: <strong>1 GB</strong>",
"civitaiLinkLabel": "Civitai model download link", "civitaiLinkLabel": "Civitai model <span class=\"font-bold italic\">download</span> link",
"civitaiLinkPlaceholder": "Paste link here", "civitaiLinkPlaceholder": "Paste link here",
"civitaiLinkExample": "Example: https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor", "civitaiLinkExample": "<strong>Example:</strong> <a href=\"https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor\" target=\"_blank\" class=\"text-muted-foreground\">https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor</a>",
"confirmModelDetails": "Confirm Model Details", "confirmModelDetails": "Confirm Model Details",
"fileName": "File Name", "fileName": "File Name",
"fileSize": "File Size", "fileSize": "File Size",

View File

@@ -201,6 +201,12 @@ function handleUploadClick() {
onUploadSuccess: async () => { onUploadSuccess: async () => {
await execute() await execute()
} }
},
dialogComponentProps: {
pt: {
header: 'py-0! pl-0!',
content: 'p-0!'
}
} }
}) })
} }

View File

@@ -1,22 +1,24 @@
<template> <template>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4 text-sm text-muted-foreground">
<!-- Model Info Section --> <!-- Model Info Section -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="text-sm text-muted m-0"> <p class="m-0">
{{ $t('assetBrowser.modelAssociatedWithLink') }} {{ $t('assetBrowser.modelAssociatedWithLink') }}
</p> </p>
<p class="text-sm mt-0"> <p
class="mt-0 bg-modal-card-background text-base-foreground p-3 rounded-lg"
>
{{ metadata?.name || metadata?.filename }} {{ metadata?.name || metadata?.filename }}
</p> </p>
</div> </div>
<!-- Model Type Selection --> <!-- Model Type Selection -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="text-sm text-muted"> <label class="">
{{ $t('assetBrowser.modelTypeSelectorLabel') }} {{ $t('assetBrowser.modelTypeSelectorLabel') }}
</label> </label>
<SingleSelect <SingleSelect
v-model="selectedModelType" v-model="modelValue"
:label=" :label="
isLoading isLoading
? $t('g.loading') ? $t('g.loading')
@@ -25,8 +27,8 @@
:options="modelTypes" :options="modelTypes"
:disabled="isLoading" :disabled="isLoading"
/> />
<div class="flex items-center gap-2 text-sm text-muted"> <div class="flex items-center gap-2">
<i class="icon-[lucide--info]" /> <i class="icon-[lucide--circle-question-mark]" />
<span>{{ $t('assetBrowser.notSureLeaveAsIs') }}</span> <span>{{ $t('assetBrowser.notSureLeaveAsIs') }}</span>
</div> </div>
</div> </div>
@@ -34,25 +36,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import SingleSelect from '@/components/input/SingleSelect.vue' import SingleSelect from '@/components/input/SingleSelect.vue'
import { useModelTypes } from '@/platform/assets/composables/useModelTypes' import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema' import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
const props = defineProps<{ defineProps<{
modelValue: string | undefined
metadata: AssetMetadata | null metadata: AssetMetadata | null
}>() }>()
const emit = defineEmits<{ const modelValue = defineModel<string | undefined>()
'update:modelValue': [value: string | undefined]
}>()
const { modelTypes, isLoading } = useModelTypes() const { modelTypes, isLoading } = useModelTypes()
const selectedModelType = computed({
get: () => props.modelValue ?? null,
set: (value: string | null) => emit('update:modelValue', value ?? undefined)
})
</script> </script>

View File

@@ -1,5 +1,7 @@
<template> <template>
<div class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6"> <div
class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6 border-t-[1px] border-border-default"
>
<!-- Step 1: Enter URL --> <!-- Step 1: Enter URL -->
<UploadModelUrlInput <UploadModelUrlInput
v-if="currentStep === 1" v-if="currentStep === 1"

View File

@@ -1,12 +1,11 @@
<template> <template>
<div class="flex items-center gap-3 px-4 py-2 font-bold"> <div class="flex items-center gap-2 p-4 font-bold">
<img src="/assets/images/civitai.svg" class="size-4" />
<span>{{ $t('assetBrowser.uploadModelFromCivitai') }}</span> <span>{{ $t('assetBrowser.uploadModelFromCivitai') }}</span>
<span <span
class="rounded-full bg-white px-1.5 py-0 text-xxs font-medium uppercase text-black" class="rounded-full bg-white px-1.5 py-0 text-xxs font-inter font-semibold uppercase text-black"
> >
{{ $t('g.beta') }} {{ $t('g.beta') }}
</span> </span>
</div> </div>
</template> </template>
<script setup lang="ts"></script>

View File

@@ -1,9 +1,26 @@
<template> <template>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2 w-full">
<span
v-if="currentStep === 1"
class="text-muted-foreground mr-auto underline flex items-center gap-2"
>
<i class="icon-[lucide--circle-question-mark]" />
<a href="#" target="_blank" class="text-muted-foreground">{{
$t('How do I find this?')
}}</a>
</span>
<TextButton
v-if="currentStep === 1"
:label="$t('g.cancel')"
type="transparent"
size="md"
:disabled="isFetchingMetadata || isUploading"
@click="emit('close')"
/>
<TextButton <TextButton
v-if="currentStep !== 1 && currentStep !== 3" v-if="currentStep !== 1 && currentStep !== 3"
:label="$t('g.back')" :label="$t('g.back')"
type="secondary" type="transparent"
size="md" size="md"
:disabled="isFetchingMetadata || isUploading" :disabled="isFetchingMetadata || isUploading"
@click="emit('back')" @click="emit('back')"
@@ -13,7 +30,7 @@
<IconTextButton <IconTextButton
v-if="currentStep === 1" v-if="currentStep === 1"
:label="$t('g.continue')" :label="$t('g.continue')"
type="primary" type="secondary"
size="md" size="md"
:disabled="!canFetchMetadata || isFetchingMetadata" :disabled="!canFetchMetadata || isFetchingMetadata"
@click="emit('fetchMetadata')" @click="emit('fetchMetadata')"
@@ -28,7 +45,7 @@
<IconTextButton <IconTextButton
v-else-if="currentStep === 2" v-else-if="currentStep === 2"
:label="$t('assetBrowser.upload')" :label="$t('assetBrowser.upload')"
type="primary" type="secondary"
size="md" size="md"
:disabled="!canUploadModel || isUploading" :disabled="!canUploadModel || isUploading"
@click="emit('upload')" @click="emit('upload')"
@@ -43,7 +60,7 @@
<TextButton <TextButton
v-else-if="currentStep === 3 && uploadStatus === 'success'" v-else-if="currentStep === 3 && uploadStatus === 'success'"
:label="$t('assetBrowser.finish')" :label="$t('assetBrowser.finish')"
type="primary" type="secondary"
size="md" size="md"
@click="emit('close')" @click="emit('close')"
/> />

View File

@@ -1,37 +1,38 @@
<template> <template>
<div class="flex flex-1 flex-col gap-6"> <div class="flex flex-1 flex-col gap-6 text-sm text-muted-foreground">
<!-- Uploading State --> <!-- Uploading State -->
<div <div
v-if="status === 'uploading'" v-if="status === 'uploading'"
class="flex flex-1 flex-col items-center justify-center gap-6" class="flex flex-1 flex-col items-center justify-center gap-2"
> >
<i <i
class="icon-[lucide--loader-circle] animate-spin text-6xl text-primary" class="icon-[lucide--loader-circle] animate-spin text-6xl text-muted-foreground"
/> />
<div class="text-center"> <div class="text-center">
<p class="m-0 text-sm font-bold"> <p class="m-0 font-bold">
{{ $t('assetBrowser.uploadingModel') }} {{ $t('assetBrowser.uploadingModel') }}
</p> </p>
</div> </div>
</div> </div>
<!-- Success State --> <!-- Success State -->
<div v-else-if="status === 'success'" class="flex flex-col gap-8"> <div v-else-if="status === 'success'" class="flex flex-col gap-2">
<div class="flex flex-col gap-4"> <p class="m-0 font-bold">
<p class="text-sm text-muted m-0 font-bold"> {{ $t('assetBrowser.modelUploaded') }}
{{ $t('assetBrowser.modelUploaded') }} </p>
</p> <p class="m-0">
<p class="text-sm text-muted m-0"> {{ $t('assetBrowser.findInLibrary', { type: modelType }) }}
{{ $t('assetBrowser.findInLibrary', { type: modelType }) }} </p>
</p>
</div>
<div class="flex flex-row items-start p-8 bg-neutral-800 rounded-lg"> <div
class="flex flex-row items-start p-4 bg-modal-card-background rounded-lg"
>
<div class="flex flex-col justify-center items-start gap-1 flex-1"> <div class="flex flex-col justify-center items-start gap-1 flex-1">
<p class="text-sm m-0"> <p class="text-base-foreground m-0">
{{ metadata?.name || metadata?.filename }} {{ metadata?.name || metadata?.filename }}
</p> </p>
<p class="text-sm text-muted m-0"> <p class="text-sm text-muted m-0">
<!-- Going to want to add another translation here to get a nice display name. -->
{{ modelType }} {{ modelType }}
</p> </p>
</div> </div>

View File

@@ -1,30 +1,27 @@
<template> <template>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-6 text-sm text-muted-foreground">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="text-sm text-muted m-0"> <p class="m-0">
{{ $t('assetBrowser.uploadModelDescription1') }} {{ $t('assetBrowser.uploadModelDescription1') }}
</p> </p>
<ul class="list-disc space-y-1 pl-5 mt-0 text-sm text-muted"> <ul class="list-disc space-y-1 pl-5 mt-0">
<li>{{ $t('assetBrowser.uploadModelDescription2') }}</li> <li v-html="$t('assetBrowser.uploadModelDescription2')" />
<li>{{ $t('assetBrowser.uploadModelDescription3') }}</li> <li v-html="$t('assetBrowser.uploadModelDescription3')" />
</ul> </ul>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="text-sm text-muted mb-0"> <label class="mb-0" v-html="$t('assetBrowser.civitaiLinkLabel')"> </label>
{{ $t('assetBrowser.civitaiLinkLabel') }}
</label>
<InputText <InputText
v-model="url" v-model="url"
autofocus
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')" :placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
class="w-full" class="w-full bg-secondary-background border-0 p-4"
/> />
<p v-if="error" class="text-xs text-error"> <p v-if="error" class="text-xs text-error">
{{ error }} {{ error }}
</p> </p>
<p v-else class="text-xs text-muted"> <p v-else v-html="$t('assetBrowser.civitaiLinkExample')"></p>
{{ $t('assetBrowser.civitaiLinkExample') }}
</p>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -4,18 +4,18 @@ import { api } from '@/scripts/api'
/** /**
* Format folder name to display name * Format folder name to display name
* Converts "upscale_models" -> "Upscale Models" * Converts "upscale_models" -> "Upscale Model"
* Converts "loras" -> "LoRAs" * Converts "loras" -> "LoRA"
*/ */
function formatDisplayName(folderName: string): string { function formatDisplayName(folderName: string): string {
// Special cases for acronyms and proper nouns // Special cases for acronyms and proper nouns
const specialCases: Record<string, string> = { const specialCases: Record<string, string> = {
loras: 'LoRAs', loras: 'LoRA',
ipadapter: 'IP-Adapter', ipadapter: 'IP-Adapter',
sams: 'SAMs', sams: 'SAM',
clip_vision: 'CLIP Vision', clip_vision: 'CLIP Vision',
animatediff_motion_lora: 'AnimateDiff Motion LoRA', animatediff_motion_lora: 'AnimateDiff Motion LoRA',
animatediff_models: 'AnimateDiff Models', animatediff_models: 'AnimateDiff Model',
vae: 'VAE', vae: 'VAE',
sam2: 'SAM 2', sam2: 'SAM 2',
controlnet: 'ControlNet', controlnet: 'ControlNet',

View File

@@ -31,7 +31,7 @@ export function useUploadModelWizard(modelTypes: Ref<ModelTypeOption[]>) {
tags: [] tags: []
}) })
const selectedModelType = ref<string | undefined>(undefined) const selectedModelType = ref<string>()
// Clear error when URL changes // Clear error when URL changes
watch( watch(

View File

@@ -17,7 +17,7 @@ export const getButtonSizeClasses = (size: ButtonSize = 'md') => {
const sizeClasses = { const sizeClasses = {
'fit-content': '', 'fit-content': '',
sm: 'px-2 py-1.5 text-xs', sm: 'px-2 py-1.5 text-xs',
md: 'px-2.5 py-2 text-sm' md: 'px-4 py-2 text-sm'
} }
return sizeClasses[size] return sizeClasses[size]
} }
@@ -30,7 +30,7 @@ export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
'bg-secondary-background border-none text-base-foreground hover:bg-secondary-background-hover' 'bg-secondary-background border-none text-base-foreground hover:bg-secondary-background-hover'
), ),
transparent: cn( transparent: cn(
'bg-transparent border-none text-base-foreground hover:bg-secondary-background-hover' 'bg-transparent border-none text-muted-foreground hover:bg-secondary-background-hover'
), ),
accent: accent:
'bg-primary-background hover:bg-primary-background-hover border-none text-white font-bold' 'bg-primary-background hover:bg-primary-background-hover border-none text-white font-bold'