Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions public/assets/images/hf-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion src/composables/useFeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export enum ServerFeatureFlag {
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled',
PRIVATE_MODELS_ENABLED = 'private_models_enabled',
SUBSCRIPTION_TIERS_ENABLED = 'subscription_tiers_enabled',
ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled'
ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled',
HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled'
}

/**
Expand Down Expand Up @@ -73,6 +74,16 @@ export function useFeatureFlags() {
remoteConfig.value.onboarding_survey_enabled ??
api.getServerFeature(ServerFeatureFlag.ONBOARDING_SURVEY_ENABLED, true)
)
},
get huggingfaceModelImportEnabled() {
// Check remote config first (from /api/features), fall back to websocket feature flags
return (
remoteConfig.value.huggingface_model_import_enabled ??
api.getServerFeature(
ServerFeatureFlag.HUGGINGFACE_MODEL_IMPORT_ENABLED,
false
)
)
}
})

Expand Down
7 changes: 7 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2230,6 +2230,7 @@
"civitaiLinkExample": "<strong>Example:</strong> <a href=\"https://civitai.com/models/10706/luisap-z-image-and-qwen-pixel-art-refiner?modelVersionId=2225295\" target=\"_blank\" class=\"text-muted-foreground\">https://civitai.com/models/10706/luisap-z-image-and-qwen-pixel-art-refiner?modelVersionId=2225295</a>",
"civitaiLinkLabel": "Civitai model <span class=\"font-bold italic\">download</span> link",
"civitaiLinkPlaceholder": "Paste link here",
"genericLinkPlaceholder": "Paste link here",
"confirmModelDetails": "Confirm Model Details",
"connectionError": "Please check your connection and try again",
"errorFileTooLarge": "File exceeds the maximum allowed size limit",
Expand All @@ -2248,6 +2249,7 @@
"finish": "Finish",
"jobId": "Job ID",
"loadingModels": "Loading {type}...",
"maxFileSize": "Max file size: <span class=\"font-bold italic\">1 GB</span>",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid embedded HTML in i18n strings.

Line 2308 contains embedded HTML markup in the translation string:

"maxFileSize": "Max file size: <span class=\"font-bold italic\">1 GB</span>"

This pattern was previously flagged and marked as addressed (commits 6e291e6 to ae03cc9), but has reappeared in the new keys. Embedding HTML in translations breaks i18n best practices because:

  1. Translators must preserve exact HTML syntax and classes
  2. Styling should be separated from content
  3. HTML in translation strings makes them fragile

Refactor to use template parameters:

"maxFileSize": "Max file size: {size}"

Then apply the styling in the Vue component that uses this string. For example:

<template>
  <span>
    {{ $t('assetBrowser.maxFileSize', { size: '' }) }}
    <span class="font-bold italic">1 GB</span>
  </span>
</template>

Or use i18n's built-in formatting to inject styled content.

🤖 Prompt for AI Agents
In src/locales/en/main.json around line 2308, the translation contains embedded
HTML ("Max file size: <span class=\"font-bold italic\">1 GB</span>"); remove the
HTML and change the string to use a placeholder, e.g. "Max file size: {size}",
then update the Vue component(s) that consume this key to inject the size value
and apply the font-bold italic classes in the template (or use i18n message
formatting/HTML-safe components) so styling is handled in the view layer rather
than inside the i18n string.

"modelAssociatedWithLink": "The model associated with the link you provided:",
"modelName": "Model Name",
"modelNamePlaceholder": "Enter a name for this model",
Expand All @@ -2258,6 +2260,7 @@
"noModelsInFolder": "No {type} available in this folder",
"notSureLeaveAsIs": "Not sure? Just leave this as is",
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
"unsupportedUrlSource": "Only URLs from {sources} are supported",
"ownership": "Ownership",
"ownershipAll": "All",
"ownershipMyModels": "My models",
Expand Down Expand Up @@ -2285,9 +2288,13 @@
"uploadModelDescription1": "Paste a Civitai model download link to add it to your library.",
"uploadModelDescription2": "Only links from <a href=\"https://civitai.com/models\" target=\"_blank\" class=\"text-muted-foreground\">https://civitai.com/models</a> are supported at the moment",
"uploadModelDescription3": "Max file size: <strong>1 GB</strong>",
"uploadModelDescription1Generic": "Paste a model download link to add it to your library.",
"uploadModelDescription2Generic": "Only URLs from the following providers are supported:",
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
"uploadModelFromCivitai": "Import a model from Civitai",
"uploadModelGeneric": "Import a model",
"uploadModelHelpVideo": "Upload Model Help Video",
"uploadModelHelpFooterText": "Need help finding the URLs? Click on a provider below to see a how-to video.",
"uploadModelHowDoIFindThis": "How do I find this?",
"uploadSuccess": "Model imported successfully!",
"ariaLabel": {
Expand Down
11 changes: 10 additions & 1 deletion src/platform/assets/components/UploadModelDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
>
<!-- Step 1: Enter URL -->
<UploadModelUrlInput
v-if="currentStep === 1"
v-if="currentStep === 1 && flags.huggingfaceModelImportEnabled"
v-model="wizardData.url"
:error="uploadError"
class="flex-1"
/>
<UploadModelUrlInputCivitai
v-else-if="currentStep === 1"
v-model="wizardData.url"
:error="uploadError"
/>
Comment on lines 6 to 16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add consistent styling to both conditional rendering paths.

The feature-flagged UploadModelUrlInput includes class="flex-1" (line 10), but the fallback UploadModelUrlInputCivitai (line 12-16) does not. This creates layout inconsistency depending on the feature flag state.

Apply this diff to maintain consistent layout:

     <UploadModelUrlInputCivitai
       v-else-if="currentStep === 1"
       v-model="wizardData.url"
       :error="uploadError"
+      class="flex-1"
     />
🤖 Prompt for AI Agents
In src/platform/assets/components/UploadModelDialog.vue around lines 6 to 16,
the feature-flagged UploadModelUrlInput has class="flex-1" but the fallback
UploadModelUrlInputCivitai does not, causing layout shifts; add the same
class="flex-1" prop to the UploadModelUrlInputCivitai element so both
conditional branches have identical styling and preserve consistent layout.

Expand Down Expand Up @@ -46,14 +52,17 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import UploadModelConfirmation from '@/platform/assets/components/UploadModelConfirmation.vue'
import UploadModelFooter from '@/platform/assets/components/UploadModelFooter.vue'
import UploadModelProgress from '@/platform/assets/components/UploadModelProgress.vue'
import UploadModelUrlInput from '@/platform/assets/components/UploadModelUrlInput.vue'
import UploadModelUrlInputCivitai from '@/platform/assets/components/UploadModelUrlInputCivitai.vue'
import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
import { useUploadModelWizard } from '@/platform/assets/composables/useUploadModelWizard'
import { useDialogStore } from '@/stores/dialogStore'
const { flags } = useFeatureFlags()
const dialogStore = useDialogStore()
const { modelTypes, fetchModelTypes } = useModelTypes()
Expand Down
22 changes: 20 additions & 2 deletions src/platform/assets/components/UploadModelDialogHeader.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
<template>
<div class="flex items-center gap-2 p-4 font-bold">
<img src="/assets/images/civitai.svg" class="size-4" />
<span>{{ $t('assetBrowser.uploadModelFromCivitai') }}</span>
<img
v-if="!flags.huggingfaceModelImportEnabled"
src="/assets/images/civitai.svg"
class="size-4"
/>
<span>{{ $t(titleKey) }}</span>
<span
class="rounded-full bg-white px-1.5 py-0 text-xxs font-inter font-semibold uppercase text-black"
>
{{ $t('g.beta') }}
</span>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

import { useFeatureFlags } from '@/composables/useFeatureFlags'

const { flags } = useFeatureFlags()

const titleKey = computed(() => {
return flags.huggingfaceModelImportEnabled
? 'assetBrowser.uploadModelGeneric'
: 'assetBrowser.uploadModelFromCivitai'
})
</script>
4 changes: 2 additions & 2 deletions src/platform/assets/components/UploadModelFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
size="md"
data-attr="upload-model-step1-continue-button"
:disabled="!canFetchMetadata || isFetchingMetadata"
@click="emit('fetchMetadata')"
:on-click="() => emit('fetchMetadata')"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

# Find IconTextButton.vue and examine its props/emits
fd -e vue IconTextButton src/components/button/
cat src/components/button/IconTextButton.vue

# Also check UploadModelFooter.vue context
cat src/platform/assets/components/UploadModelFooter.vue | head -80

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3799


IconTextButton uses an onClick prop handler, not native click events—inline arrow functions can be optimized.

The :on-click binding is correct and follows the component's interface. However, wrapping emit() calls with inline arrow functions creates new function instances on each render. Extract these to named handler methods for better performance and readability.

Consider refactoring to:

const handleFetchMetadata = () => emit('fetchMetadata')
const handleUpload = () => emit('upload')

Then use :on-click="handleFetchMetadata" instead of :on-click="() => emit('fetchMetadata')".

🤖 Prompt for AI Agents
In src/platform/assets/components/UploadModelFooter.vue around line 45, inline
arrow functions are being passed to the IconTextButton :on-click prop which
creates a new function on every render; replace these with named handler
functions (e.g. handleFetchMetadata and handleUpload) defined in the component
setup, each calling emit('fetchMetadata') and emit('upload') respectively,
return those handlers from setup so the template can use
:on-click="handleFetchMetadata" and :on-click="handleUpload" instead of inline
arrow functions to improve performance and readability.

>
<template #icon>
<i
Expand All @@ -58,7 +58,7 @@
size="md"
data-attr="upload-model-step2-confirm-button"
:disabled="!canUploadModel || isUploading"
@click="emit('upload')"
:on-click="() => emit('upload')"
>
<template #icon>
<i
Expand Down
90 changes: 68 additions & 22 deletions src/platform/assets/components/UploadModelUrlInput.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,74 @@
<template>
<div class="flex flex-col gap-6 text-sm text-muted-foreground">
<div class="flex flex-col gap-2">
<p class="m-0">
{{ $t('assetBrowser.uploadModelDescription1') }}
</p>
<ul class="list-disc space-y-1 pl-5 mt-0">
<li v-html="$t('assetBrowser.uploadModelDescription2')" />
<li v-html="$t('assetBrowser.uploadModelDescription3')" />
</ul>
<div
class="flex flex-col justify-between h-full gap-6 text-sm text-muted-foreground"
>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2">
<p class="m-0 text-white">
{{ $t('assetBrowser.uploadModelDescription1Generic') }}
</p>
<div class="m-0">
<p class="m-0">
{{ $t('assetBrowser.uploadModelDescription2Generic') }}
</p>
<span class="inline-flex items-center gap-1 flex-wrap mt-2">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<span class="inline-flex items-center gap-1">
<img
src="/assets/images/civitai.svg"
alt="Civitai"
class="w-4 h-4"
/>
<a
href="https://civitai.com"
target="_blank"
rel="noopener noreferrer"
class="text-muted underline"
>
Civitai</a
><span>,</span>
</span>
<span class="inline-flex items-center gap-1">
<img
src="/assets/images/hf-logo.svg"
alt="Hugging Face"
class="w-4 h-4"
/>
<a
href="https://huggingface.co"
target="_blank"
rel="noopener noreferrer"
class="text-muted underline"
>
Hugging Face
</a>
</span>
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
Comment on lines +14 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider i18n for brand name presentation.

While "Civitai" and "Hugging Face" are proper nouns, the presentation structure (including punctuation and formatting) may vary across locales. The current implementation uses eslint-disable to bypass i18n requirements.

Consider moving this to an i18n template for better localization control:

// In src/locales/en/main.json
"supportedPlatforms": "Supported platforms: <a href='https://civitai.com'>Civitai</a>, <a href='https://huggingface.co'>Hugging Face</a>"

Then in template:

<p v-html="$t('assetBrowser.supportedPlatforms')"></p>

This approach allows locales to control the presentation format while keeping brand names intact.

As per coding guidelines, all user-facing strings should use vue-i18n.

</div>
</div>

<div class="flex flex-col gap-2">
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.genericLinkPlaceholder')"
class="w-full bg-secondary-background border-0 p-4"
data-attr="upload-model-step1-url-input"
/>
<p v-if="error" class="text-xs text-error">
{{ error }}
</p>
<p
v-else
class="text-white"
v-html="$t('assetBrowser.maxFileSize')"
></p>
</div>
</div>

<div class="flex flex-col gap-2">
<label class="mb-0" v-html="$t('assetBrowser.civitaiLinkLabel')"> </label>
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
class="w-full bg-secondary-background border-0 p-4"
data-attr="upload-model-step1-url-input"
/>
<p v-if="error" class="text-xs text-error">
{{ error }}
</p>
<p v-else v-html="$t('assetBrowser.civitaiLinkExample')"></p>
<div class="text-sm text-muted">
{{ $t('assetBrowser.uploadModelHelpFooterText') }}
</div>
</div>
</template>
Expand Down
47 changes: 47 additions & 0 deletions src/platform/assets/components/UploadModelUrlInputCivitai.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<div class="flex flex-col gap-6 text-sm text-muted-foreground">
<div class="flex flex-col gap-2">
<p class="m-0">
{{ $t('assetBrowser.uploadModelDescription1') }}
</p>
<ul class="list-disc space-y-1 pl-5 mt-0">
<li v-html="$t('assetBrowser.uploadModelDescription2')" />
<li v-html="$t('assetBrowser.uploadModelDescription3')" />
</ul>
</div>

<div class="flex flex-col gap-2">
<label class="mb-0" v-html="$t('assetBrowser.civitaiLinkLabel')"> </label>
<InputText
v-model="url"
autofocus
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
class="w-full bg-secondary-background border-0 p-4"
data-attr="upload-model-step1-url-input"
/>
<p v-if="error" class="text-xs text-error">
{{ error }}
</p>
<p v-else v-html="$t('assetBrowser.civitaiLinkExample')"></p>
</div>
</div>
</template>

<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { computed } from 'vue'
const props = defineProps<{
modelValue: string
error?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const url = computed({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could use defineModel here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on url*

get: () => props.modelValue,
set: (value: string) => emit('update:modelValue', value)
})
</script>
10 changes: 6 additions & 4 deletions src/platform/assets/composables/useModelTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ export const useModelTypes = createSharedComposable(() => {
} = useAsyncState(
async (): Promise<ModelTypeOption[]> => {
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[],
{
Expand Down
Loading