mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
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:
9
public/assets/images/civitai.svg
Normal file
9
public/assets/images/civitai.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.5 MiB |
@@ -152,7 +152,7 @@ const {
|
||||
popoverMaxWidth?: string
|
||||
}>()
|
||||
|
||||
const selectedItem = defineModel<string | null>({ required: true })
|
||||
const selectedItem = defineModel<string | undefined>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -201,6 +201,12 @@ function handleUploadClick() {
|
||||
onUploadSuccess: async () => {
|
||||
await execute()
|
||||
}
|
||||
},
|
||||
dialogComponentProps: {
|
||||
pt: {
|
||||
header: 'py-0! pl-0!',
|
||||
content: 'p-0!'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user