+
+
+
+ {{ $t('assetBrowser.uploadModelDescription1Generic') }}
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+ {{
+ $t('assetBrowser.maxFileSizeValue')
+ }}
+
+
+
+
-
-
-
-
- {{ error }}
-
-
+
+ {{ $t('assetBrowser.uploadModelHelpFooterText') }}
@@ -44,4 +90,9 @@ const url = computed({
get: () => props.modelValue,
set: (value: string) => emit('update:modelValue', value)
})
+
+const civitaiIcon = '/assets/images/civitai.svg'
+const civitaiUrl = 'https://civitai.com/models'
+const huggingFaceIcon = '/assets/images/hf-logo.svg'
+const huggingFaceUrl = 'https://huggingface.co'
diff --git a/src/platform/assets/components/UploadModelUrlInputCivitai.vue b/src/platform/assets/components/UploadModelUrlInputCivitai.vue
new file mode 100644
index 000000000..b89ee5329
--- /dev/null
+++ b/src/platform/assets/components/UploadModelUrlInputCivitai.vue
@@ -0,0 +1,82 @@
+
+
+
+
+ {{ $t('assetBrowser.uploadModelDescription1') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/platform/assets/composables/useModelTypes.ts b/src/platform/assets/composables/useModelTypes.ts
index 12aa8d419..8f578c926 100644
--- a/src/platform/assets/composables/useModelTypes.ts
+++ b/src/platform/assets/composables/useModelTypes.ts
@@ -50,10 +50,12 @@ export const useModelTypes = createSharedComposable(() => {
} = useAsyncState(
async (): Promise
=> {
const response = await api.getModelFolders()
- return response.map((folder) => ({
- name: formatDisplayName(folder.name),
- value: folder.name
- }))
+ return response
+ .map((folder) => ({
+ name: formatDisplayName(folder.name),
+ value: folder.name
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name))
},
[] as ModelTypeOption[],
{
diff --git a/src/platform/assets/composables/useUploadModelWizard.ts b/src/platform/assets/composables/useUploadModelWizard.ts
index 73341c0f2..2d97efd9c 100644
--- a/src/platform/assets/composables/useUploadModelWizard.ts
+++ b/src/platform/assets/composables/useUploadModelWizard.ts
@@ -1,9 +1,15 @@
import type { Ref } from 'vue'
import { computed, ref, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { st } from '@/i18n'
+import { civitaiImportSource } from '@/platform/assets/importSources/civitaiImportSource'
+import { huggingfaceImportSource } from '@/platform/assets/importSources/huggingfaceImportSource'
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
+import type { ImportSource } from '@/platform/assets/types/importSource'
+import { validateSourceUrl } from '@/platform/assets/utils/importSourceUtil'
import { useAssetsStore } from '@/stores/assetsStore'
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
@@ -21,8 +27,10 @@ interface ModelTypeOption {
}
export function useUploadModelWizard(modelTypes: Ref) {
+ const { t } = useI18n()
const assetsStore = useAssetsStore()
const modelToNodeStore = useModelToNodeStore()
+ const { flags } = useFeatureFlags()
const currentStep = ref(1)
const isFetchingMetadata = ref(false)
const isUploading = ref(false)
@@ -37,6 +45,20 @@ export function useUploadModelWizard(modelTypes: Ref) {
const selectedModelType = ref()
+ // Available import sources
+ const importSources: ImportSource[] = flags.huggingfaceModelImportEnabled
+ ? [civitaiImportSource, huggingfaceImportSource]
+ : [civitaiImportSource]
+
+ // Detected import source based on URL
+ const detectedSource = computed(() => {
+ const url = wizardData.value.url.trim()
+ if (!url) return null
+ return (
+ importSources.find((source) => validateSourceUrl(url, source)) ?? null
+ )
+ })
+
// Clear error when URL changes
watch(
() => wizardData.value.url,
@@ -54,15 +76,6 @@ export function useUploadModelWizard(modelTypes: Ref) {
return !!selectedModelType.value
})
- function isCivitaiUrl(url: string): boolean {
- try {
- const hostname = new URL(url).hostname.toLowerCase()
- return hostname === 'civitai.com' || hostname.endsWith('.civitai.com')
- } catch {
- return false
- }
- }
-
async function fetchMetadata() {
if (!canFetchMetadata.value) return
@@ -75,17 +88,36 @@ export function useUploadModelWizard(modelTypes: Ref) {
}
wizardData.value.url = cleanedUrl
- if (!isCivitaiUrl(wizardData.value.url)) {
- uploadError.value = st(
- 'assetBrowser.onlyCivitaiUrlsSupported',
- 'Only Civitai URLs are supported'
- )
+ // Validate URL belongs to a supported import source
+ const source = detectedSource.value
+ if (!source) {
+ const supportedSources = importSources.map((s) => s.name).join(', ')
+ uploadError.value = t('assetBrowser.unsupportedUrlSource', {
+ sources: supportedSources
+ })
return
}
isFetchingMetadata.value = true
try {
const metadata = await assetService.getAssetMetadata(wizardData.value.url)
+
+ // Decode URL-encoded filenames (e.g., Chinese characters)
+ if (metadata.filename) {
+ try {
+ metadata.filename = decodeURIComponent(metadata.filename)
+ } catch {
+ // Keep original if decoding fails
+ }
+ }
+ if (metadata.name) {
+ try {
+ metadata.name = decodeURIComponent(metadata.name)
+ } catch {
+ // Keep original if decoding fails
+ }
+ }
+
wizardData.value.metadata = metadata
// Pre-fill name from metadata
@@ -125,6 +157,14 @@ export function useUploadModelWizard(modelTypes: Ref) {
async function uploadModel() {
if (!canUploadModel.value) return
+ // Defensive check: detectedSource should be valid after fetchMetadata validation,
+ // but guard against edge cases (e.g., URL modified between steps)
+ const source = detectedSource.value
+ if (!source) {
+ uploadError.value = t('assetBrowser.noValidSourceDetected')
+ return false
+ }
+
isUploading.value = true
uploadStatus.value = 'uploading'
@@ -170,7 +210,7 @@ export function useUploadModelWizard(modelTypes: Ref) {
name: filename,
tags,
user_metadata: {
- source: 'civitai',
+ source: source.type,
source_url: wizardData.value.url,
model_type: selectedModelType.value
},
@@ -224,6 +264,7 @@ export function useUploadModelWizard(modelTypes: Ref) {
// Computed
canFetchMetadata,
canUploadModel,
+ detectedSource,
// Actions
fetchMetadata,
diff --git a/src/platform/assets/importSources/civitaiImportSource.ts b/src/platform/assets/importSources/civitaiImportSource.ts
new file mode 100644
index 000000000..5ff324d00
--- /dev/null
+++ b/src/platform/assets/importSources/civitaiImportSource.ts
@@ -0,0 +1,10 @@
+import type { ImportSource } from '@/platform/assets/types/importSource'
+
+/**
+ * Civitai model import source configuration
+ */
+export const civitaiImportSource: ImportSource = {
+ type: 'civitai',
+ name: 'Civitai',
+ hostnames: ['civitai.com']
+}
diff --git a/src/platform/assets/importSources/huggingfaceImportSource.ts b/src/platform/assets/importSources/huggingfaceImportSource.ts
new file mode 100644
index 000000000..310e170af
--- /dev/null
+++ b/src/platform/assets/importSources/huggingfaceImportSource.ts
@@ -0,0 +1,10 @@
+import type { ImportSource } from '@/platform/assets/types/importSource'
+
+/**
+ * Hugging Face model import source configuration
+ */
+export const huggingfaceImportSource: ImportSource = {
+ type: 'huggingface',
+ name: 'Hugging Face',
+ hostnames: ['huggingface.co']
+}
diff --git a/src/platform/assets/types/importSource.ts b/src/platform/assets/types/importSource.ts
new file mode 100644
index 000000000..12aa5e3db
--- /dev/null
+++ b/src/platform/assets/types/importSource.ts
@@ -0,0 +1,24 @@
+/**
+ * Supported model import sources
+ */
+type ImportSourceType = 'civitai' | 'huggingface'
+
+/**
+ * Configuration for a model import source
+ */
+export interface ImportSource {
+ /**
+ * Unique identifier for this import source
+ */
+ readonly type: ImportSourceType
+
+ /**
+ * Display name for the source
+ */
+ readonly name: string
+
+ /**
+ * Hostname(s) that identify this source
+ */
+ readonly hostnames: readonly string[]
+}
diff --git a/src/platform/assets/utils/importSourceUtil.ts b/src/platform/assets/utils/importSourceUtil.ts
new file mode 100644
index 000000000..2628593cc
--- /dev/null
+++ b/src/platform/assets/utils/importSourceUtil.ts
@@ -0,0 +1,15 @@
+import type { ImportSource } from '@/platform/assets/types/importSource'
+
+/**
+ * Check if a URL belongs to a specific import source
+ */
+export function validateSourceUrl(url: string, source: ImportSource): boolean {
+ try {
+ const hostname = new URL(url).hostname.toLowerCase()
+ return source.hostnames.some(
+ (h) => hostname === h || hostname.endsWith(`.${h}`)
+ )
+ } catch {
+ return false
+ }
+}
diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts
index cbe1247ce..2d3e2c17c 100644
--- a/src/platform/remoteConfig/types.ts
+++ b/src/platform/remoteConfig/types.ts
@@ -40,4 +40,5 @@ export type RemoteConfig = {
onboarding_survey_enabled?: boolean
stripe_publishable_key?: string
stripe_pricing_table_id?: string
+ huggingface_model_import_enabled?: boolean
}