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
}>()
const selectedItem = defineModel<string | null>({ required: true })
const selectedItem = defineModel<string | undefined>({ required: true })
const { t } = useI18n()

View File

@@ -2092,11 +2092,11 @@
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
"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",
"uploadModelDescription3": "Max file size: 1 GB",
"civitaiLinkLabel": "Civitai model download link",
"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: <strong>1 GB</strong>",
"civitaiLinkLabel": "Civitai model <span class=\"font-bold italic\">download</span> link",
"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",
"fileName": "File Name",
"fileSize": "File Size",

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
<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 -->
<UploadModelUrlInput
v-if="currentStep === 1"

View File

@@ -1,12 +1,11 @@
<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
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') }}
</span>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -1,9 +1,26 @@
<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
v-if="currentStep !== 1 && currentStep !== 3"
:label="$t('g.back')"
type="secondary"
type="transparent"
size="md"
:disabled="isFetchingMetadata || isUploading"
@click="emit('back')"
@@ -13,7 +30,7 @@
<IconTextButton
v-if="currentStep === 1"
:label="$t('g.continue')"
type="primary"
type="secondary"
size="md"
:disabled="!canFetchMetadata || isFetchingMetadata"
@click="emit('fetchMetadata')"
@@ -28,7 +45,7 @@
<IconTextButton
v-else-if="currentStep === 2"
:label="$t('assetBrowser.upload')"
type="primary"
type="secondary"
size="md"
:disabled="!canUploadModel || isUploading"
@click="emit('upload')"
@@ -43,7 +60,7 @@
<TextButton
v-else-if="currentStep === 3 && uploadStatus === 'success'"
:label="$t('assetBrowser.finish')"
type="primary"
type="secondary"
size="md"
@click="emit('close')"
/>

View File

@@ -1,37 +1,38 @@
<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 -->
<div
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
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">
<p class="m-0 text-sm font-bold">
<p class="m-0 font-bold">
{{ $t('assetBrowser.uploadingModel') }}
</p>
</div>
</div>
<!-- Success State -->
<div v-else-if="status === 'success'" class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<p class="text-sm text-muted m-0 font-bold">
{{ $t('assetBrowser.modelUploaded') }}
</p>
<p class="text-sm text-muted m-0">
{{ $t('assetBrowser.findInLibrary', { type: modelType }) }}
</p>
</div>
<div v-else-if="status === 'success'" class="flex flex-col gap-2">
<p class="m-0 font-bold">
{{ $t('assetBrowser.modelUploaded') }}
</p>
<p class="m-0">
{{ $t('assetBrowser.findInLibrary', { type: modelType }) }}
</p>
<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">
<p class="text-sm m-0">
<p class="text-base-foreground m-0">
{{ metadata?.name || metadata?.filename }}
</p>
<p class="text-sm text-muted m-0">
<!-- Going to want to add another translation here to get a nice display name. -->
{{ modelType }}
</p>
</div>

View File

@@ -1,30 +1,27 @@
<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">
<p class="text-sm text-muted m-0">
<p class="m-0">
{{ $t('assetBrowser.uploadModelDescription1') }}
</p>
<ul class="list-disc space-y-1 pl-5 mt-0 text-sm text-muted">
<li>{{ $t('assetBrowser.uploadModelDescription2') }}</li>
<li>{{ $t('assetBrowser.uploadModelDescription3') }}</li>
<ul class="list-disc space-y-1 pl-5 mt-0">
<li v-html="$t('assetBrowser.uploadModelDescription2')" />
<li v-html="$t('assetBrowser.uploadModelDescription3')" />
</ul>
</div>
<div class="flex flex-col gap-2">
<label class="text-sm text-muted mb-0">
{{ $t('assetBrowser.civitaiLinkLabel') }}
</label>
<label class="mb-0" v-html="$t('assetBrowser.civitaiLinkLabel')"> </label>
<InputText
v-model="url"
autofocus
: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">
{{ error }}
</p>
<p v-else class="text-xs text-muted">
{{ $t('assetBrowser.civitaiLinkExample') }}
</p>
<p v-else v-html="$t('assetBrowser.civitaiLinkExample')"></p>
</div>
</div>
</template>

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ export const getButtonSizeClasses = (size: ButtonSize = 'md') => {
const sizeClasses = {
'fit-content': '',
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]
}
@@ -30,7 +30,7 @@ export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
'bg-secondary-background border-none text-base-foreground hover:bg-secondary-background-hover'
),
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:
'bg-primary-background hover:bg-primary-background-hover border-none text-white font-bold'