mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
[backport cloud/1.34] feat: display and upload Civitai preview images in model upload flow (#7301)
Backport of #7274 to `cloud/1.34` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7301-backport-cloud-1-34-feat-display-and-upload-Civitai-preview-images-in-model-upload-flo-2c56d73d3650814caedbc0b64480cb9c) by [Unito](https://www.unito.io) Co-authored-by: Luke Mino-Altherr <luke@comfy.org> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 text-sm text-muted-foreground">
|
<div class="flex flex-col gap-4 text-sm text-muted-foreground">
|
||||||
<!-- Model Info Section -->
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ $t('assetBrowser.modelAssociatedWithLink') }}
|
{{ $t('assetBrowser.modelAssociatedWithLink') }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0 text-base-foreground rounded-lg">
|
<div
|
||||||
{{ metadata?.filename || metadata?.name }}
|
class="flex items-center gap-3 bg-secondary-background p-3 rounded-lg"
|
||||||
</p>
|
>
|
||||||
|
<img
|
||||||
|
v-if="previewImage"
|
||||||
|
:src="previewImage"
|
||||||
|
:alt="metadata?.filename || metadata?.name || 'Model preview'"
|
||||||
|
class="w-14 h-14 rounded object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<p class="m-0 text-base-foreground">
|
||||||
|
{{ metadata?.filename || metadata?.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Type Selection -->
|
<!-- Model Type Selection -->
|
||||||
@@ -40,7 +49,8 @@ import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
|
|||||||
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
|
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
metadata: AssetMetadata | null
|
metadata?: AssetMetadata
|
||||||
|
previewImage?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modelValue = defineModel<string | undefined>()
|
const modelValue = defineModel<string | undefined>()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
v-else-if="currentStep === 2"
|
v-else-if="currentStep === 2"
|
||||||
v-model="selectedModelType"
|
v-model="selectedModelType"
|
||||||
:metadata="wizardData.metadata"
|
:metadata="wizardData.metadata"
|
||||||
|
:preview-image="wizardData.previewImage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Step 3: Upload Progress -->
|
<!-- Step 3: Upload Progress -->
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
:error="uploadError"
|
:error="uploadError"
|
||||||
:metadata="wizardData.metadata"
|
:metadata="wizardData.metadata"
|
||||||
:model-type="selectedModelType"
|
:model-type="selectedModelType"
|
||||||
|
:preview-image="wizardData.previewImage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Navigation Footer -->
|
<!-- Navigation Footer -->
|
||||||
|
|||||||
@@ -25,8 +25,14 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-row items-start p-4 bg-modal-card-background rounded-lg"
|
class="flex flex-row items-center gap-3 p-4 bg-modal-card-background rounded-lg"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-if="previewImage"
|
||||||
|
:src="previewImage"
|
||||||
|
:alt="metadata?.filename || metadata?.name || 'Model preview'"
|
||||||
|
class="w-14 h-14 rounded object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
<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-base-foreground m-0">
|
<p class="text-base-foreground m-0">
|
||||||
{{ metadata?.filename || metadata?.name }}
|
{{ metadata?.filename || metadata?.name }}
|
||||||
@@ -63,7 +69,8 @@ import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
status: 'idle' | 'uploading' | 'success' | 'error'
|
status: 'idle' | 'uploading' | 'success' | 'error'
|
||||||
error?: string
|
error?: string
|
||||||
metadata: AssetMetadata | null
|
metadata?: AssetMetadata
|
||||||
modelType: string | undefined
|
modelType?: string
|
||||||
|
previewImage?: string
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
|||||||
|
|
||||||
interface WizardData {
|
interface WizardData {
|
||||||
url: string
|
url: string
|
||||||
metadata: AssetMetadata | null
|
metadata?: AssetMetadata
|
||||||
name: string
|
name: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
|
previewImage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelTypeOption {
|
interface ModelTypeOption {
|
||||||
@@ -30,7 +31,6 @@ export function useUploadModelWizard(modelTypes: Ref<ModelTypeOption[]>) {
|
|||||||
|
|
||||||
const wizardData = ref<WizardData>({
|
const wizardData = ref<WizardData>({
|
||||||
url: '',
|
url: '',
|
||||||
metadata: null,
|
|
||||||
name: '',
|
name: '',
|
||||||
tags: []
|
tags: []
|
||||||
})
|
})
|
||||||
@@ -91,6 +91,9 @@ export function useUploadModelWizard(modelTypes: Ref<ModelTypeOption[]>) {
|
|||||||
// Pre-fill name from metadata
|
// Pre-fill name from metadata
|
||||||
wizardData.value.name = metadata.filename || metadata.name || ''
|
wizardData.value.name = metadata.filename || metadata.name || ''
|
||||||
|
|
||||||
|
// Store preview image if available
|
||||||
|
wizardData.value.previewImage = metadata.preview_image
|
||||||
|
|
||||||
// Pre-fill model type from metadata tags if available
|
// Pre-fill model type from metadata tags if available
|
||||||
if (metadata.tags && metadata.tags.length > 0) {
|
if (metadata.tags && metadata.tags.length > 0) {
|
||||||
wizardData.value.tags = metadata.tags
|
wizardData.value.tags = metadata.tags
|
||||||
@@ -134,6 +137,34 @@ export function useUploadModelWizard(modelTypes: Ref<ModelTypeOption[]>) {
|
|||||||
wizardData.value.metadata?.name ||
|
wizardData.value.metadata?.name ||
|
||||||
'model'
|
'model'
|
||||||
|
|
||||||
|
let previewId: string | undefined
|
||||||
|
|
||||||
|
// Upload preview image first if available
|
||||||
|
if (wizardData.value.previewImage) {
|
||||||
|
try {
|
||||||
|
const baseFilename = filename.split('.')[0]
|
||||||
|
|
||||||
|
// Extract extension from data URL MIME type
|
||||||
|
let extension = 'png'
|
||||||
|
const mimeMatch = wizardData.value.previewImage.match(
|
||||||
|
/^data:image\/([^;]+);/
|
||||||
|
)
|
||||||
|
if (mimeMatch) {
|
||||||
|
extension = mimeMatch[1] === 'jpeg' ? 'jpg' : mimeMatch[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewAsset = await assetService.uploadAssetFromBase64({
|
||||||
|
data: wizardData.value.previewImage,
|
||||||
|
name: `${baseFilename}_preview.${extension}`,
|
||||||
|
tags: ['preview']
|
||||||
|
})
|
||||||
|
previewId = previewAsset.id
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to upload preview image:', error)
|
||||||
|
// Continue with model upload even if preview fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await assetService.uploadAssetFromUrl({
|
await assetService.uploadAssetFromUrl({
|
||||||
url: wizardData.value.url,
|
url: wizardData.value.url,
|
||||||
name: filename,
|
name: filename,
|
||||||
@@ -142,7 +173,8 @@ export function useUploadModelWizard(modelTypes: Ref<ModelTypeOption[]>) {
|
|||||||
source: 'civitai',
|
source: 'civitai',
|
||||||
source_url: wizardData.value.url,
|
source_url: wizardData.value.url,
|
||||||
model_type: selectedModelType.value
|
model_type: selectedModelType.value
|
||||||
}
|
},
|
||||||
|
preview_id: previewId
|
||||||
})
|
})
|
||||||
|
|
||||||
uploadStatus.value = 'success'
|
uploadStatus.value = 'success'
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const zAssetMetadata = z.object({
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
preview_url: z.string().optional(),
|
preview_url: z.string().optional(),
|
||||||
|
preview_image: z.string().optional(),
|
||||||
validation: zValidationResult.optional()
|
validation: zValidationResult.optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -392,6 +392,59 @@ function createAssetService() {
|
|||||||
return await res.json()
|
return await res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads an asset from base64 data
|
||||||
|
*
|
||||||
|
* @param params - Upload parameters
|
||||||
|
* @param params.data - Base64 data URL (e.g., "data:image/png;base64,...")
|
||||||
|
* @param params.name - Display name (determines extension)
|
||||||
|
* @param params.tags - Optional freeform tags
|
||||||
|
* @param params.user_metadata - Optional custom metadata object
|
||||||
|
* @returns Promise<AssetItem & { created_new: boolean }> - Asset object with created_new flag
|
||||||
|
* @throws Error if upload fails
|
||||||
|
*/
|
||||||
|
async function uploadAssetFromBase64(params: {
|
||||||
|
data: string
|
||||||
|
name: string
|
||||||
|
tags?: string[]
|
||||||
|
user_metadata?: Record<string, any>
|
||||||
|
}): Promise<AssetItem & { created_new: boolean }> {
|
||||||
|
// Validate that data is a data URL
|
||||||
|
if (!params.data || !params.data.startsWith('data:')) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid data URL: expected a string starting with "data:"'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert base64 data URL to Blob
|
||||||
|
const blob = await fetch(params.data).then((r) => r.blob())
|
||||||
|
|
||||||
|
// Create FormData and append the blob
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', blob, params.name)
|
||||||
|
|
||||||
|
if (params.tags) {
|
||||||
|
formData.append('tags', JSON.stringify(params.tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.user_metadata) {
|
||||||
|
formData.append('user_metadata', JSON.stringify(params.user_metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.fetchApi(ASSETS_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to upload asset from base64: ${res.status} ${res.statusText}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await res.json()
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getAssetModelFolders,
|
getAssetModelFolders,
|
||||||
getAssetModels,
|
getAssetModels,
|
||||||
@@ -402,7 +455,8 @@ function createAssetService() {
|
|||||||
deleteAsset,
|
deleteAsset,
|
||||||
updateAsset,
|
updateAsset,
|
||||||
getAssetMetadata,
|
getAssetMetadata,
|
||||||
uploadAssetFromUrl
|
uploadAssetFromUrl,
|
||||||
|
uploadAssetFromBase64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user