Skip to content

Commit 15794a8

Browse files
comfy-pr-botluke-mino-altherrclaude
authored
[backport core/1.32] [feat] Add Civitai model upload wizard (#6770)
Backport of #6694 to `core/1.32` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6770-backport-core-1-32-feat-Add-Civitai-model-upload-wizard-2b16d73d365081f8a732f69713f95b61) by [Unito](https://www.unito.io) Co-authored-by: Luke Mino-Altherr <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 689634e commit 15794a8

File tree

13 files changed

+799
-2
lines changed

13 files changed

+799
-2
lines changed

src/components/common/UrlInput.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { ValidationState } from '@/utils/validationUtil'
3535
const props = defineProps<{
3636
modelValue: string
3737
validateUrlFn?: (url: string) => Promise<boolean>
38+
disableValidation?: boolean
3839
}>()
3940
4041
const emit = defineEmits<{
@@ -101,6 +102,8 @@ const defaultValidateUrl = async (url: string): Promise<boolean> => {
101102
}
102103
103104
const validateUrl = async (value: string) => {
105+
if (props.disableValidation) return
106+
104107
if (validationState.value === ValidationState.LOADING) return
105108
106109
const url = cleanInput(value)

src/locales/en/main.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,6 +2086,35 @@
20862086
"noModelsInFolder": "No {type} available in this folder",
20872087
"searchAssetsPlaceholder": "Type to search...",
20882088
"uploadModel": "Upload model",
2089+
"uploadModelFromCivitai": "Upload a model from Civitai",
2090+
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
2091+
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
2092+
"uploadModelDescription1": "Paste a Civitai model download link to add it to your library.",
2093+
"uploadModelDescription2": "Only links from https://civitai.com are supported at the moment",
2094+
"uploadModelDescription3": "Max file size: 1 GB",
2095+
"civitaiLinkLabel": "Civitai model download link",
2096+
"civitaiLinkPlaceholder": "Paste link here",
2097+
"civitaiLinkExample": "Example: https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor",
2098+
"confirmModelDetails": "Confirm Model Details",
2099+
"fileName": "File Name",
2100+
"fileSize": "File Size",
2101+
"modelName": "Model Name",
2102+
"modelNamePlaceholder": "Enter a name for this model",
2103+
"tags": "Tags",
2104+
"tagsPlaceholder": "e.g., models, checkpoint",
2105+
"tagsHelp": "Separate tags with commas",
2106+
"upload": "Upload",
2107+
"uploadingModel": "Uploading model...",
2108+
"uploadSuccess": "Model uploaded successfully!",
2109+
"uploadFailed": "Upload failed",
2110+
"modelAssociatedWithLink": "The model associated with the link you provided:",
2111+
"modelTypeSelectorLabel": "What type of model is this?",
2112+
"modelTypeSelectorPlaceholder": "Select model type",
2113+
"selectModelType": "Select model type",
2114+
"notSureLeaveAsIs": "Not sure? Just leave this as is",
2115+
"modelUploaded": "Model uploaded! 🎉",
2116+
"findInLibrary": "Find it in the {type} section of the models library.",
2117+
"finish": "Finish",
20892118
"allModels": "All Models",
20902119
"allCategory": "All {category}",
20912120
"unknown": "Unknown",
@@ -2097,6 +2126,13 @@
20972126
"sortZA": "Z-A",
20982127
"sortRecent": "Recent",
20992128
"sortPopular": "Popular",
2129+
"errorFileTooLarge": "File exceeds the maximum allowed size limit",
2130+
"errorFormatNotAllowed": "Only SafeTensor format is allowed",
2131+
"errorUnsafePickleScan": "CivitAI detected potentially unsafe code in this file",
2132+
"errorUnsafeVirusScan": "CivitAI detected malware or suspicious content in this file",
2133+
"errorModelTypeNotSupported": "This model type is not supported",
2134+
"errorUnknown": "An unexpected error occurred",
2135+
"errorUploadFailed": "Failed to upload asset. Please try again.",
21002136
"ariaLabel": {
21012137
"assetCard": "{name} - {type} asset",
21022138
"loadingAsset": "Loading asset"

src/platform/assets/components/AssetBrowserModal.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,14 @@ import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
7373
import { useFeatureFlags } from '@/composables/useFeatureFlags'
7474
import AssetFilterBar from '@/platform/assets/components/AssetFilterBar.vue'
7575
import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
76+
import UploadModelDialog from '@/platform/assets/components/UploadModelDialog.vue'
77+
import UploadModelDialogHeader from '@/platform/assets/components/UploadModelDialogHeader.vue'
7678
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
7779
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
7880
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
7981
import { assetService } from '@/platform/assets/services/assetService'
8082
import { formatCategoryLabel } from '@/platform/assets/utils/categoryLabel'
83+
import { useDialogStore } from '@/stores/dialogStore'
8184
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
8285
import { OnCloseKey } from '@/types/widgetTypes'
8386
@@ -92,6 +95,7 @@ const props = defineProps<{
9295
}>()
9396
9497
const { t } = useI18n()
98+
const dialogStore = useDialogStore()
9599
96100
const emit = defineEmits<{
97101
'asset-select': [asset: AssetDisplayItem]
@@ -189,6 +193,15 @@ const { flags } = useFeatureFlags()
189193
const isUploadButtonEnabled = computed(() => flags.modelUploadButtonEnabled)
190194
191195
function handleUploadClick() {
192-
// Will be implemented in the future commit
196+
dialogStore.showDialog({
197+
key: 'upload-model',
198+
headerComponent: UploadModelDialogHeader,
199+
component: UploadModelDialog,
200+
props: {
201+
onUploadSuccess: async () => {
202+
await execute()
203+
}
204+
}
205+
})
193206
}
194207
</script>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<template>
2+
<div class="flex flex-col gap-4">
3+
<!-- Model Info Section -->
4+
<div class="flex flex-col gap-2">
5+
<p class="text-sm text-muted m-0">
6+
{{ $t('assetBrowser.modelAssociatedWithLink') }}
7+
</p>
8+
<p class="text-sm mt-0">
9+
{{ metadata?.name || metadata?.filename }}
10+
</p>
11+
</div>
12+
13+
<!-- Model Type Selection -->
14+
<div class="flex flex-col gap-2">
15+
<label class="text-sm text-muted">
16+
{{ $t('assetBrowser.modelTypeSelectorLabel') }}
17+
</label>
18+
<SingleSelect
19+
v-model="selectedModelType"
20+
:label="
21+
isLoading
22+
? $t('g.loading')
23+
: $t('assetBrowser.modelTypeSelectorPlaceholder')
24+
"
25+
:options="modelTypes"
26+
:disabled="isLoading"
27+
/>
28+
<div class="flex items-center gap-2 text-sm text-muted">
29+
<i class="icon-[lucide--info]" />
30+
<span>{{ $t('assetBrowser.notSureLeaveAsIs') }}</span>
31+
</div>
32+
</div>
33+
</div>
34+
</template>
35+
36+
<script setup lang="ts">
37+
import { computed } from 'vue'
38+
39+
import SingleSelect from '@/components/input/SingleSelect.vue'
40+
import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
41+
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
42+
43+
const props = defineProps<{
44+
modelValue: string | undefined
45+
metadata: AssetMetadata | null
46+
}>()
47+
48+
const emit = defineEmits<{
49+
'update:modelValue': [value: string | undefined]
50+
}>()
51+
52+
const { modelTypes, isLoading } = useModelTypes()
53+
54+
const selectedModelType = computed({
55+
get: () => props.modelValue ?? null,
56+
set: (value: string | null) => emit('update:modelValue', value ?? undefined)
57+
})
58+
</script>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<template>
2+
<div class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6">
3+
<!-- Step 1: Enter URL -->
4+
<UploadModelUrlInput
5+
v-if="currentStep === 1"
6+
v-model="wizardData.url"
7+
:error="uploadError"
8+
/>
9+
10+
<!-- Step 2: Confirm Metadata -->
11+
<UploadModelConfirmation
12+
v-else-if="currentStep === 2"
13+
v-model="selectedModelType"
14+
:metadata="wizardData.metadata"
15+
/>
16+
17+
<!-- Step 3: Upload Progress -->
18+
<UploadModelProgress
19+
v-else-if="currentStep === 3"
20+
:status="uploadStatus"
21+
:error="uploadError"
22+
:metadata="wizardData.metadata"
23+
:model-type="selectedModelType"
24+
/>
25+
26+
<!-- Navigation Footer -->
27+
<UploadModelFooter
28+
:current-step="currentStep"
29+
:is-fetching-metadata="isFetchingMetadata"
30+
:is-uploading="isUploading"
31+
:can-fetch-metadata="canFetchMetadata"
32+
:can-upload-model="canUploadModel"
33+
:upload-status="uploadStatus"
34+
@back="goToPreviousStep"
35+
@fetch-metadata="handleFetchMetadata"
36+
@upload="handleUploadModel"
37+
@close="handleClose"
38+
/>
39+
</div>
40+
</template>
41+
42+
<script setup lang="ts">
43+
import { onMounted } from 'vue'
44+
45+
import UploadModelConfirmation from '@/platform/assets/components/UploadModelConfirmation.vue'
46+
import UploadModelFooter from '@/platform/assets/components/UploadModelFooter.vue'
47+
import UploadModelProgress from '@/platform/assets/components/UploadModelProgress.vue'
48+
import UploadModelUrlInput from '@/platform/assets/components/UploadModelUrlInput.vue'
49+
import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
50+
import { useUploadModelWizard } from '@/platform/assets/composables/useUploadModelWizard'
51+
import { useDialogStore } from '@/stores/dialogStore'
52+
53+
const dialogStore = useDialogStore()
54+
const { modelTypes, fetchModelTypes } = useModelTypes()
55+
56+
const emit = defineEmits<{
57+
'upload-success': []
58+
}>()
59+
60+
const {
61+
currentStep,
62+
isFetchingMetadata,
63+
isUploading,
64+
uploadStatus,
65+
uploadError,
66+
wizardData,
67+
selectedModelType,
68+
canFetchMetadata,
69+
canUploadModel,
70+
fetchMetadata,
71+
uploadModel,
72+
goToPreviousStep
73+
} = useUploadModelWizard(modelTypes)
74+
75+
async function handleFetchMetadata() {
76+
await fetchMetadata()
77+
}
78+
79+
async function handleUploadModel() {
80+
const success = await uploadModel()
81+
if (success) {
82+
emit('upload-success')
83+
}
84+
}
85+
86+
function handleClose() {
87+
dialogStore.closeDialog({ key: 'upload-model' })
88+
}
89+
90+
onMounted(() => {
91+
fetchModelTypes()
92+
})
93+
</script>
94+
95+
<style scoped>
96+
.upload-model-dialog {
97+
width: 90vw;
98+
max-width: 800px;
99+
min-height: 400px;
100+
}
101+
102+
@media (min-width: 640px) {
103+
.upload-model-dialog {
104+
width: auto;
105+
min-width: 600px;
106+
}
107+
}
108+
</style>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<template>
2+
<div class="flex items-center gap-3 px-4 py-2 font-bold">
3+
<span>{{ $t('assetBrowser.uploadModelFromCivitai') }}</span>
4+
<span
5+
class="rounded-full bg-white px-1.5 py-0 text-xxs font-medium uppercase text-black"
6+
>
7+
{{ $t('g.beta') }}
8+
</span>
9+
</div>
10+
</template>
11+
12+
<script setup lang="ts"></script>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<template>
2+
<div class="flex justify-end gap-2">
3+
<TextButton
4+
v-if="currentStep !== 1 && currentStep !== 3"
5+
:label="$t('g.back')"
6+
type="secondary"
7+
size="md"
8+
:disabled="isFetchingMetadata || isUploading"
9+
@click="emit('back')"
10+
/>
11+
<span v-else />
12+
13+
<IconTextButton
14+
v-if="currentStep === 1"
15+
:label="$t('g.continue')"
16+
type="primary"
17+
size="md"
18+
:disabled="!canFetchMetadata || isFetchingMetadata"
19+
@click="emit('fetchMetadata')"
20+
>
21+
<template #icon>
22+
<i
23+
v-if="isFetchingMetadata"
24+
class="icon-[lucide--loader-circle] animate-spin"
25+
/>
26+
</template>
27+
</IconTextButton>
28+
<IconTextButton
29+
v-else-if="currentStep === 2"
30+
:label="$t('assetBrowser.upload')"
31+
type="primary"
32+
size="md"
33+
:disabled="!canUploadModel || isUploading"
34+
@click="emit('upload')"
35+
>
36+
<template #icon>
37+
<i
38+
v-if="isUploading"
39+
class="icon-[lucide--loader-circle] animate-spin"
40+
/>
41+
</template>
42+
</IconTextButton>
43+
<TextButton
44+
v-else-if="currentStep === 3 && uploadStatus === 'success'"
45+
:label="$t('assetBrowser.finish')"
46+
type="primary"
47+
size="md"
48+
@click="emit('close')"
49+
/>
50+
</div>
51+
</template>
52+
53+
<script setup lang="ts">
54+
import IconTextButton from '@/components/button/IconTextButton.vue'
55+
import TextButton from '@/components/button/TextButton.vue'
56+
57+
defineProps<{
58+
currentStep: number
59+
isFetchingMetadata: boolean
60+
isUploading: boolean
61+
canFetchMetadata: boolean
62+
canUploadModel: boolean
63+
uploadStatus: 'idle' | 'uploading' | 'success' | 'error'
64+
}>()
65+
66+
const emit = defineEmits<{
67+
(e: 'back'): void
68+
(e: 'fetchMetadata'): void
69+
(e: 'upload'): void
70+
(e: 'close'): void
71+
}>()
72+
</script>

0 commit comments

Comments
 (0)