Add components for asset modal

This commit is contained in:
Luke Mino-Altherr
2025-11-12 13:40:29 -08:00
parent 5ece3d6f2e
commit 3fc1d1663b
6 changed files with 313 additions and 240 deletions

View File

@@ -196,7 +196,12 @@ function handleUploadClick() {
dialogStore.showDialog({
key: 'upload-model',
headerComponent: UploadModelDialogHeader,
component: UploadModelDialog
component: UploadModelDialog,
props: {
onUploadSuccess: async () => {
await execute()
}
}
})
}
</script>

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>