feat: display and upload Civitai preview images in model upload flow (#7274)

## Summary
Stores and displays base64-encoded preview images from Civitai during
the model upload flow, uploading the preview as a separate asset linked
to the model.

## Changes
- **Schema**: Added `preview_image` field to `AssetMetadata` schema
- **Service**: Added `uploadAssetFromBase64` method to convert base64
data to blob and upload via FormData
- **Upload Flow**: Modified wizard to first upload preview image as
asset, then link it to model via `preview_id`
- **UI**: Display 56x56px preview thumbnail alongside model filename in
confirmation and success steps

## Review Focus
- Base64 to blob conversion and FormData upload implementation
- Sequential upload flow (preview first, then model with preview_id
reference)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7274-feat-display-and-upload-Civitai-preview-images-in-model-upload-flow-2c46d73d365081ff9b74c1791d23f6dd)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Luke Mino-Altherr
2025-12-09 16:32:55 -08:00
committed by GitHub
parent 2903560416
commit ce4837a57c
6 changed files with 118 additions and 12 deletions

View File

@@ -392,6 +392,59 @@ function createAssetService() {
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 {
getAssetModelFolders,
getAssetModels,
@@ -402,7 +455,8 @@ function createAssetService() {
deleteAsset,
updateAsset,
getAssetMetadata,
uploadAssetFromUrl
uploadAssetFromUrl,
uploadAssetFromBase64
}
}