feat: add upgrade modal for model upload when private models disabled (#7124)

## Summary
Adds a dedicated upgrade modal that appears when users without private
models access try to upload models, providing a clear path to upgrade
their subscription.

## Changes
- **New upgrade modal**: Created `UploadModelUpgradeModal` with
dedicated body, header, and footer components
- **Conditional rendering**: Modified `AssetBrowserModal` to show
upgrade modal when `privateModelsEnabled` flag is false
- **Subscription integration**: Connected upgrade flow to existing
subscription system via `showSubscriptionDialog()`
- **Localization**: Added localization keys for upgrade messaging

## Review Focus
- Conditional logic in `AssetBrowserModal.handleUploadClick()` based on
feature flags
- Component naming consistency (all upgrade-related components prefixed
with `UploadModelUpgrade`)
- Footer component refactoring maintains existing upload wizard behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7124-feat-add-upgrade-modal-for-model-upload-when-private-models-disabled-2be6d73d36508147b72eea8a1d6ab772)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
Luke Mino-Altherr
2025-12-03 20:14:00 -08:00
committed by GitHub
parent 52e915baf0
commit 8d4a6df7f8
8 changed files with 122 additions and 15 deletions

View File

@@ -11,7 +11,8 @@ export enum ServerFeatureFlag {
MAX_UPLOAD_SIZE = 'max_upload_size',
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled'
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled',
PRIVATE_MODELS_ENABLED = 'private_models_enabled'
}
/**
@@ -47,6 +48,13 @@ export function useFeatureFlags() {
false
)
)
},
get privateModelsEnabled() {
// Check remote config first (from /api/features), fall back to websocket feature flags
return (
remoteConfig.value.private_models_enabled ??
api.getServerFeature(ServerFeatureFlag.PRIVATE_MODELS_ENABLED, false)
)
}
})

View File

@@ -2121,6 +2121,8 @@
"modelUploaded": "Model imported! 🎉",
"findInLibrary": "Find it in the {type} section of the models library.",
"finish": "Finish",
"upgradeToUnlockFeature": "Upgrade to unlock this feature",
"upgradeFeatureDescription": "This feature is only available with Creator or Pro plans.",
"allModels": "All Models",
"allCategory": "All {category}",
"unknown": "Unknown",

View File

@@ -0,0 +1,30 @@
<template>
<div
class="flex flex-col justify-between gap-10 p-4 border-t border-border-default w-auto max-w-[min(500px,90vw)]"
>
<UploadModelUpgradeModalBody />
<UploadModelUpgradeModalFooter
@close="handleClose"
@subscribe="handleSubscribe"
/>
</div>
</template>
<script setup lang="ts">
import UploadModelUpgradeModalBody from '@/platform/assets/components/UploadModelUpgradeModalBody.vue'
import UploadModelUpgradeModalFooter from '@/platform/assets/components/UploadModelUpgradeModalFooter.vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useDialogStore } from '@/stores/dialogStore'
const dialogStore = useDialogStore()
const { showSubscriptionDialog } = useSubscription()
function handleClose() {
dialogStore.closeDialog({ key: 'upload-model-upgrade' })
}
function handleSubscribe() {
showSubscriptionDialog()
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<div
class="flex flex-1 flex-col items-center justify-center text-base text-muted-foreground"
>
<p class="m-0 max-w-md">
{{ $t('assetBrowser.upgradeFeatureDescription') }}
</p>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<template>
<div class="flex flex-wrap justify-end gap-2 w-full">
<a
href="https://blog.comfy.org/p/comfy-cloud-new-features-and-pricing"
target="_blank"
rel="noopener noreferrer"
class="text-muted-foreground mr-auto underline flex items-center gap-2"
>
<i class="icon-[lucide--external-link]" />
<span>{{ $t('g.learnMore') }}</span>
</a>
<TextButton
:label="$t('g.close')"
type="transparent"
size="md"
@click="emit('close')"
/>
<TextButton
:label="$t('subscription.required.subscribe')"
type="secondary"
size="md"
@click="emit('subscribe')"
/>
</div>
</template>
<script setup lang="ts">
import TextButton from '@/components/button/TextButton.vue'
const emit = defineEmits<{
close: []
subscribe: []
}>()
</script>

View File

@@ -0,0 +1,5 @@
<template>
<div class="flex items-center gap-2 p-4 font-bold">
<span>{{ $t('assetBrowser.upgradeToUnlockFeature') }}</span>
</div>
</template>

View File

@@ -1,6 +1,8 @@
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import UploadModelDialog from '@/platform/assets/components/UploadModelDialog.vue'
import UploadModelDialogHeader from '@/platform/assets/components/UploadModelDialogHeader.vue'
import UploadModelUpgradeModal from '@/platform/assets/components/UploadModelUpgradeModal.vue'
import UploadModelUpgradeModalHeader from '@/platform/assets/components/UploadModelUpgradeModalHeader.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useDialogStore } from '@/stores/dialogStore'
import type { UseAsyncStateReturn } from '@vueuse/core'
@@ -14,22 +16,38 @@ export function useModelUpload(
const isUploadButtonEnabled = computed(() => flags.modelUploadButtonEnabled)
function showUploadDialog() {
dialogStore.showDialog({
key: 'upload-model',
headerComponent: UploadModelDialogHeader,
component: UploadModelDialog,
props: {
onUploadSuccess: async () => {
await execute?.()
if (!flags.privateModelsEnabled) {
// Show upgrade modal if private models are disabled
dialogStore.showDialog({
key: 'upload-model-upgrade',
headerComponent: UploadModelUpgradeModalHeader,
component: UploadModelUpgradeModal,
dialogComponentProps: {
pt: {
header: 'py-0! pl-0!',
content: 'p-0!'
}
}
},
dialogComponentProps: {
pt: {
header: 'py-0! pl-0!',
content: 'p-0!'
})
} else {
// Show regular upload modal
dialogStore.showDialog({
key: 'upload-model',
headerComponent: UploadModelDialogHeader,
component: UploadModelDialog,
props: {
onUploadSuccess: async () => {
await execute?.()
}
},
dialogComponentProps: {
pt: {
header: 'py-0! pl-0!',
content: 'p-0!'
}
}
}
})
})
}
}
return { isUploadButtonEnabled, showUploadDialog }
}

View File

@@ -36,4 +36,5 @@ export type RemoteConfig = {
telemetry_disabled_events?: TelemetryEventName[]
model_upload_button_enabled?: boolean
asset_update_options_enabled?: boolean
private_models_enabled?: boolean
}