mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +00:00
Add components for asset modal
This commit is contained in:
@@ -196,7 +196,12 @@ function handleUploadClick() {
|
||||
dialogStore.showDialog({
|
||||
key: 'upload-model',
|
||||
headerComponent: UploadModelDialogHeader,
|
||||
component: UploadModelDialog
|
||||
component: UploadModelDialog,
|
||||
props: {
|
||||
onUploadSuccess: async () => {
|
||||
await execute()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
68
src/platform/assets/components/UploadModelConfirmation.vue
Normal file
68
src/platform/assets/components/UploadModelConfirmation.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Model Info Section -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm text-muted mb-0">
|
||||
{{ $t('assetBrowser.modelAssociatedWithLink') }}
|
||||
</p>
|
||||
<p class="text-sm mt-0">
|
||||
{{ metadata?.name || metadata?.filename }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Type Selection -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="model-type" class="text-sm text-muted">
|
||||
{{ $t('assetBrowser.whatTypeOfModel') }}
|
||||
</label>
|
||||
<select
|
||||
id="model-type"
|
||||
:value="modelValue"
|
||||
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
@change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option
|
||||
v-for="option in modelTypeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="flex items-center gap-2 text-sm text-muted">
|
||||
<i class="icon-[lucide--info]" />
|
||||
<span>{{ $t('assetBrowser.notSureLeaveAsIs') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ModelMetadata {
|
||||
content_length: number
|
||||
final_url: string
|
||||
content_type?: string
|
||||
filename?: string
|
||||
name?: string
|
||||
tags?: string[]
|
||||
preview_url?: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
metadata: ModelMetadata | null
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const modelTypeOptions = [
|
||||
{ label: 'LoRA', value: 'lora' },
|
||||
{ label: 'Checkpoint', value: 'checkpoint' },
|
||||
{ label: 'Embedding', value: 'embedding' },
|
||||
{ label: 'VAE', value: 'vae' },
|
||||
{ label: 'Upscale Model', value: 'upscale_model' },
|
||||
{ label: 'ControlNet', value: 'controlnet' }
|
||||
]
|
||||
</script>
|
||||
@@ -1,222 +1,102 @@
|
||||
<template>
|
||||
<div class="upload-model-dialog flex flex-col p-8">
|
||||
<Stepper v-model:value="currentStep" class="flex flex-col gap-6">
|
||||
<StepPanels>
|
||||
<!-- Step 1: Enter URL -->
|
||||
<StepPanel value="1">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-base">
|
||||
{{ $t('assetBrowser.uploadModelDescription1') }}
|
||||
</p>
|
||||
<ul class="list-disc space-y-1 pl-5 text-sm text-muted">
|
||||
<li>{{ $t('assetBrowser.uploadModelDescription2') }}</li>
|
||||
<li>{{ $t('assetBrowser.uploadModelDescription3') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="upload-model-dialog flex flex-col justify-between gap-6 p-4">
|
||||
<!-- Step 1: Enter URL -->
|
||||
<UploadModelUrlInput
|
||||
v-if="currentStep === 1"
|
||||
v-model="wizardData.url"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="civitai-link" class="font-medium">
|
||||
{{ $t('assetBrowser.civitaiLinkLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
id="civitai-link"
|
||||
v-model="wizardData.url"
|
||||
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
|
||||
class="w-full"
|
||||
/>
|
||||
<small class="text-muted">
|
||||
{{ $t('assetBrowser.civitaiLinkExample') }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<!-- Step 2: Confirm Metadata -->
|
||||
<UploadModelConfirmation
|
||||
v-else-if="currentStep === 2"
|
||||
v-model="selectedModelType"
|
||||
:metadata="wizardData.metadata"
|
||||
/>
|
||||
|
||||
<!-- Step 2: Confirm Metadata -->
|
||||
<StepPanel value="2">
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Model Info Section -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-muted">
|
||||
{{ $t('assetBrowser.modelAssociatedWithLink') }}
|
||||
</p>
|
||||
<!-- Step 3: Upload Progress -->
|
||||
<UploadModelProgress
|
||||
v-else-if="currentStep === 3"
|
||||
:status="uploadStatus"
|
||||
:error="uploadError"
|
||||
:metadata="wizardData.metadata"
|
||||
:model-type="selectedModelType"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-lg bg-surface-hover p-4"
|
||||
>
|
||||
<img
|
||||
v-if="wizardData.metadata?.preview_url"
|
||||
:src="wizardData.metadata.preview_url"
|
||||
:alt="wizardData.metadata?.name"
|
||||
class="size-16 rounded object-cover"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex size-16 items-center justify-center rounded bg-surface"
|
||||
>
|
||||
<i class="icon-[lucide--image] text-2xl text-muted" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-base font-medium">
|
||||
{{ wizardData.metadata?.name || wizardData.metadata?.filename }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Navigation Footer -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
v-if="currentStep !== 1 && currentStep !== 3"
|
||||
type="button"
|
||||
:class="getButtonClasses('secondary')"
|
||||
:disabled="isFetchingMetadata || isUploading"
|
||||
@click="goToPreviousStep"
|
||||
>
|
||||
{{ $t('g.back') }}
|
||||
</button>
|
||||
<span v-else />
|
||||
|
||||
<!-- Model Type Selection -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="model-type" class="text-sm text-muted">
|
||||
{{ $t('assetBrowser.whatTypeOfModel') }}
|
||||
</label>
|
||||
<Select
|
||||
id="model-type"
|
||||
v-model="selectedModelType"
|
||||
:options="modelTypeOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:placeholder="$t('assetBrowser.selectModelType')"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="flex items-center gap-2 text-sm text-muted">
|
||||
<i class="icon-[lucide--info]" />
|
||||
<span>{{ $t('assetBrowser.notSureLeaveAsIs') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StepPanel>
|
||||
|
||||
<!-- Step 3: Upload Progress -->
|
||||
<StepPanel value="3">
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Uploading State -->
|
||||
<div
|
||||
v-if="uploadStatus === 'uploading'"
|
||||
class="flex flex-col items-center justify-center gap-6 py-8"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--loader-circle] animate-spin text-6xl text-primary"
|
||||
/>
|
||||
<div class="text-center">
|
||||
<h3 class="mb-2 text-lg font-semibold">
|
||||
{{ uploadStatusMessage }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<div v-else-if="uploadStatus === 'success'" class="flex flex-col gap-6">
|
||||
<div class="flex items-start gap-2">
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ $t('assetBrowser.modelUploaded') }}
|
||||
</h3>
|
||||
<span class="text-lg">🎉</span>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-muted">
|
||||
{{ $t('assetBrowser.findInLibrary', { type: selectedModelType.toUpperCase() }) }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-lg bg-surface-hover p-4"
|
||||
>
|
||||
<img
|
||||
v-if="wizardData.metadata?.preview_url"
|
||||
:src="wizardData.metadata.preview_url"
|
||||
:alt="wizardData.metadata?.name"
|
||||
class="size-16 rounded object-cover"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex size-16 items-center justify-center rounded bg-surface"
|
||||
>
|
||||
<i class="icon-[lucide--image] text-2xl text-muted" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-base font-medium">
|
||||
{{ wizardData.metadata?.name || wizardData.metadata?.filename }}
|
||||
</p>
|
||||
<p class="text-sm text-muted">
|
||||
{{ selectedModelType.toUpperCase() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div
|
||||
v-else-if="uploadStatus === 'error'"
|
||||
class="flex flex-col items-center justify-center gap-6 py-8"
|
||||
>
|
||||
<i class="icon-[lucide--x-circle] text-6xl text-red-500" />
|
||||
<div class="text-center">
|
||||
<h3 class="mb-2 text-lg font-semibold">
|
||||
{{ uploadStatusMessage }}
|
||||
</h3>
|
||||
<p v-if="uploadError" class="text-sm text-muted">
|
||||
{{ uploadError }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
|
||||
<!-- Navigation Footer -->
|
||||
<div class="flex justify-between pt-4">
|
||||
<Button
|
||||
v-if="currentStep !== '1'"
|
||||
:label="$t('g.back')"
|
||||
severity="secondary"
|
||||
:disabled="isFetchingMetadata || isUploading"
|
||||
@click="goToPreviousStep"
|
||||
<button
|
||||
v-if="currentStep === 1"
|
||||
type="button"
|
||||
:class="getButtonClasses('primary')"
|
||||
:disabled="!canProceedStep1 || isFetchingMetadata"
|
||||
@click="handleStep1Continue"
|
||||
>
|
||||
<i
|
||||
v-if="isFetchingMetadata"
|
||||
class="icon-[lucide--loader-circle] mr-2 animate-spin"
|
||||
/>
|
||||
<span v-else />
|
||||
|
||||
<Button
|
||||
v-if="currentStep === '1'"
|
||||
:label="$t('g.continue')"
|
||||
severity="primary"
|
||||
:disabled="!canProceedStep1"
|
||||
:loading="isFetchingMetadata"
|
||||
@click="handleStep1Continue"
|
||||
{{ $t('g.continue') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="currentStep === 2"
|
||||
type="button"
|
||||
:class="getButtonClasses('primary')"
|
||||
:disabled="!canProceedStep2 || isUploading"
|
||||
@click="handleStep2Upload"
|
||||
>
|
||||
<i
|
||||
v-if="isUploading"
|
||||
class="icon-[lucide--loader-circle] mr-2 animate-spin"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="currentStep === '2'"
|
||||
:label="$t('assetBrowser.upload')"
|
||||
severity="primary"
|
||||
:disabled="!canProceedStep2"
|
||||
:loading="isUploading"
|
||||
@click="handleStep2Upload"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="currentStep === '3' && uploadStatus === 'success'"
|
||||
:label="$t('assetBrowser.finish')"
|
||||
severity="primary"
|
||||
@click="handleClose"
|
||||
/>
|
||||
</div>
|
||||
</Stepper>
|
||||
{{ $t('assetBrowser.upload') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="currentStep === 3 && uploadStatus === 'success'"
|
||||
type="button"
|
||||
:class="getButtonClasses('primary')"
|
||||
@click="handleClose"
|
||||
>
|
||||
{{ $t('assetBrowser.finish') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Select from 'primevue/select'
|
||||
import StepPanel from 'primevue/steppanel'
|
||||
import StepPanels from 'primevue/steppanels'
|
||||
import Stepper from 'primevue/stepper'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import UploadModelConfirmation from '@/platform/assets/components/UploadModelConfirmation.vue'
|
||||
import UploadModelProgress from '@/platform/assets/components/UploadModelProgress.vue'
|
||||
import UploadModelUrlInput from '@/platform/assets/components/UploadModelUrlInput.vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getButtonSizeClasses,
|
||||
getButtonTypeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const currentStep = ref('1')
|
||||
const emit = defineEmits<{
|
||||
'upload-success': []
|
||||
}>()
|
||||
|
||||
const currentStep = ref(1)
|
||||
const isFetchingMetadata = ref(false)
|
||||
const isUploading = ref(false)
|
||||
const uploadStatus = ref<'idle' | 'uploading' | 'success' | 'error'>('idle')
|
||||
@@ -245,14 +125,23 @@ const wizardData = ref<{
|
||||
const selectedModelType = ref<string>('lora')
|
||||
|
||||
const modelTypeOptions = [
|
||||
{ label: 'LoRA', value: 'lora' },
|
||||
{ label: 'Checkpoint', value: 'checkpoint' },
|
||||
{ label: 'Embedding', value: 'embedding' },
|
||||
{ label: 'VAE', value: 'vae' },
|
||||
{ label: 'Upscale Model', value: 'upscale_model' },
|
||||
{ label: 'ControlNet', value: 'controlnet' }
|
||||
'lora',
|
||||
'checkpoint',
|
||||
'embedding',
|
||||
'vae',
|
||||
'upscale_model',
|
||||
'controlnet'
|
||||
]
|
||||
|
||||
// Button styling helper
|
||||
function getButtonClasses(type: 'primary' | 'secondary') {
|
||||
return cn(
|
||||
getBaseButtonClasses(),
|
||||
getButtonSizeClasses('md'),
|
||||
getButtonTypeClasses(type)
|
||||
)
|
||||
}
|
||||
|
||||
// Step 1 validation
|
||||
const canProceedStep1 = computed(() => {
|
||||
return wizardData.value.url.trim().length > 0
|
||||
@@ -263,27 +152,6 @@ const canProceedStep2 = computed(() => {
|
||||
return !!selectedModelType.value
|
||||
})
|
||||
|
||||
const uploadStatusMessage = computed(() => {
|
||||
switch (uploadStatus.value) {
|
||||
case 'uploading':
|
||||
return t('assetBrowser.uploadingModel')
|
||||
case 'success':
|
||||
return t('assetBrowser.uploadSuccess')
|
||||
case 'error':
|
||||
return t('assetBrowser.uploadFailed')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
function formatFileSize(bytes: number | undefined): string {
|
||||
if (!bytes || bytes === -1) return 'Unknown'
|
||||
const gb = bytes / (1024 ** 3)
|
||||
if (gb >= 1) return `${gb.toFixed(2)} GB`
|
||||
const mb = bytes / (1024 ** 2)
|
||||
return `${mb.toFixed(2)} MB`
|
||||
}
|
||||
|
||||
async function handleStep1Continue() {
|
||||
if (!canProceedStep1.value) return
|
||||
|
||||
@@ -300,14 +168,14 @@ async function handleStep1Continue() {
|
||||
wizardData.value.tags = metadata.tags
|
||||
// Try to detect model type from tags
|
||||
const typeTag = metadata.tags.find(tag =>
|
||||
modelTypeOptions.some(opt => opt.value === tag)
|
||||
modelTypeOptions.includes(tag)
|
||||
)
|
||||
if (typeTag) {
|
||||
selectedModelType.value = typeTag
|
||||
}
|
||||
}
|
||||
|
||||
currentStep.value = '2'
|
||||
currentStep.value = 2
|
||||
} catch (error) {
|
||||
console.error('Failed to retrieve metadata:', error)
|
||||
uploadError.value = error instanceof Error ? error.message : 'Failed to retrieve metadata'
|
||||
@@ -339,26 +207,27 @@ async function handleStep2Upload() {
|
||||
})
|
||||
|
||||
uploadStatus.value = 'success'
|
||||
currentStep.value = '3'
|
||||
currentStep.value = 3
|
||||
emit('upload-success')
|
||||
} catch (error) {
|
||||
console.error('Failed to upload asset:', error)
|
||||
uploadStatus.value = 'error'
|
||||
uploadError.value = error instanceof Error ? error.message : 'Failed to upload model'
|
||||
currentStep.value = '3'
|
||||
currentStep.value = 3
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function goToPreviousStep() {
|
||||
const currentStepNum = parseInt(currentStep.value)
|
||||
if (currentStepNum > 1) {
|
||||
currentStep.value = (currentStepNum - 1).toString()
|
||||
if (currentStep.value > 1) {
|
||||
currentStep.value = currentStep.value - 1
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
dialogStore.closeDialog('upload-model')
|
||||
dialogStore.closeDialog({ key: 'upload-model' })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-3 px-4 py-2 font-bold">
|
||||
<span>{{ $t('assetBrowser.uploadModelFromCivitai') }}</span>
|
||||
<Badge :value="$t('g.beta')" class="bg-white text-black" />
|
||||
<span
|
||||
class="rounded-full bg-white px-1.5 py-0 text-xxs font-medium uppercase text-black"
|
||||
>
|
||||
{{ $t('g.beta') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Badge from 'primevue/badge'
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
89
src/platform/assets/components/UploadModelProgress.vue
Normal file
89
src/platform/assets/components/UploadModelProgress.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Uploading State -->
|
||||
<div
|
||||
v-if="status === 'uploading'"
|
||||
class="flex flex-col items-center justify-center gap-6 py-8"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--loader-circle] animate-spin text-6xl text-primary"
|
||||
/>
|
||||
<div class="text-center">
|
||||
<h3 class="mb-2 text-lg font-semibold">
|
||||
{{ $t('assetBrowser.uploadingModel') }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<div v-else-if="status === 'success'" class="flex flex-col gap-6">
|
||||
<div class="flex items-start gap-2">
|
||||
<h3 class="text-sm text-muted mb-0">
|
||||
{{ $t('assetBrowser.modelUploaded') }} 🎉
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-muted">
|
||||
{{ $t('assetBrowser.findInLibrary', { type: modelType.toUpperCase() }) }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-3 rounded-lg bg-surface-hover p-4">
|
||||
<img
|
||||
v-if="metadata?.preview_url"
|
||||
:src="metadata.preview_url"
|
||||
:alt="metadata?.name"
|
||||
class="size-16 rounded object-cover"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex size-16 items-center justify-center rounded bg-surface"
|
||||
>
|
||||
<i class="icon-[lucide--image] text-2xl text-muted" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-base font-medium">
|
||||
{{ metadata?.name || metadata?.filename }}
|
||||
</p>
|
||||
<p class="text-sm text-muted">
|
||||
{{ modelType.toUpperCase() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div
|
||||
v-else-if="status === 'error'"
|
||||
class="flex flex-col items-center justify-center gap-6 py-8"
|
||||
>
|
||||
<i class="icon-[lucide--x-circle] text-6xl text-red-500" />
|
||||
<div class="text-center">
|
||||
<h3 class="mb-2 text-lg font-semibold">
|
||||
{{ $t('assetBrowser.uploadFailed') }}
|
||||
</h3>
|
||||
<p v-if="error" class="text-sm text-muted">
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ModelMetadata {
|
||||
content_length: number
|
||||
final_url: string
|
||||
content_type?: string
|
||||
filename?: string
|
||||
name?: string
|
||||
tags?: string[]
|
||||
preview_url?: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
status: 'idle' | 'uploading' | 'success' | 'error'
|
||||
error?: string
|
||||
metadata: ModelMetadata | null
|
||||
modelType: string
|
||||
}>()
|
||||
</script>
|
||||
40
src/platform/assets/components/UploadModelUrlInput.vue
Normal file
40
src/platform/assets/components/UploadModelUrlInput.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm text-muted mb-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>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="civitai-link" class="text-sm text-muted mb-0">
|
||||
{{ $t('assetBrowser.civitaiLinkLabel') }}
|
||||
</label>
|
||||
<input
|
||||
id="civitai-link"
|
||||
:value="modelValue"
|
||||
type="text"
|
||||
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
|
||||
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-muted outline-none focus:border-primary focus:ring-1 focus:ring-primary"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<p class="text-xs text-muted">
|
||||
{{ $t('assetBrowser.civitaiLinkExample') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
</script>
|
||||
Reference in New Issue
Block a user