[feat] Add cloud notification modal for macOS desktop users

Adds a one-time informational modal that introduces Comfy Cloud to macOS desktop users. The modal emphasizes that ComfyUI remains free and open source, with Cloud as an optional service for GPU access.

Key features:
- Shows once on first launch for macOS + Electron users
- Persistent badge in topbar after dismissal for easy re-access
- Clean design with Comfy Cloud branding
- Non-intrusive messaging focused on infrastructure benefits

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Deep Mehta
2025-11-23 20:53:13 +05:30
parent 09c888e338
commit e5ba351dcc
7 changed files with 206 additions and 1 deletions

View File

@@ -17,6 +17,7 @@ import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
@@ -47,7 +48,7 @@ const showContextMenu = (event: MouseEvent) => {
}
}
onMounted(() => {
onMounted(async () => {
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
if (isElectron()) {
@@ -77,5 +78,17 @@ onMounted(() => {
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()
// Show cloud notification for macOS desktop users (one-time)
const isMacOS = navigator.platform.toLowerCase().includes('mac')
const settingStore = useSettingStore()
const hasShownNotification = settingStore.get(
'Comfy.Desktop.CloudNotificationShown'
)
if (isElectron() && isMacOS && !hasShownNotification) {
dialogService.showCloudNotification()
await settingStore.set('Comfy.Desktop.CloudNotificationShown', true)
}
})
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div class="w-[480px] p-6">
<!-- Header with Logo -->
<div class="mb-6">
<div class="mb-2 flex items-center gap-3">
<img
src="/assets/images/comfy-cloud-logo.svg"
alt="Comfy Cloud"
class="h-8 w-8 shrink-0"
/>
<h1 class="text-2xl font-semibold">
{{ t('cloudNotification.title') }}
</h1>
</div>
<p class="text-base text-muted">
{{ t('cloudNotification.message') }}
</p>
</div>
<!-- Features -->
<div class="mb-6 space-y-4">
<div class="flex gap-3">
<i class="pi pi-check-circle mt-0.5 shrink-0 text-xl text-blue-500"></i>
<div class="flex-1">
<div class="mb-1 font-medium">
{{ t('cloudNotification.feature1Title') }}
</div>
<div class="text-sm text-muted">
{{ t('cloudNotification.feature1') }}
</div>
</div>
</div>
<div class="flex gap-3">
<i class="pi pi-server mt-0.5 shrink-0 text-xl text-blue-500"></i>
<div class="flex-1">
<div class="mb-1 font-medium">
{{ t('cloudNotification.feature2Title') }}
</div>
<div class="text-sm text-muted">
{{ t('cloudNotification.feature2') }}
</div>
</div>
</div>
<div class="flex gap-3">
<i class="pi pi-tag mt-0.5 shrink-0 text-xl text-blue-500"></i>
<div class="flex-1">
<div class="mb-1 font-medium">
{{ t('cloudNotification.feature3Title') }}
</div>
<div class="text-sm text-muted">
{{ t('cloudNotification.feature3') }}
</div>
</div>
</div>
</div>
<!-- Footer Note -->
<div
class="mb-6 rounded border-l-2 border-blue-500 bg-blue-500/5 py-2.5 pl-3 pr-4"
>
<p class="whitespace-pre-line text-sm text-muted">
{{ t('cloudNotification.feature4') }}
</p>
</div>
<!-- Actions -->
<div class="flex gap-3">
<Button
:label="t('cloudNotification.continueLocally')"
severity="secondary"
outlined
class="flex-1"
@click="onDismiss"
/>
<Button
:label="t('cloudNotification.exploreCloud')"
icon="pi pi-arrow-right"
icon-pos="right"
class="flex-1"
@click="onExplore"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import { useDialogStore } from '@/stores/dialogStore'
const { t } = useI18n()
const onDismiss = () => {
useDialogStore().closeDialog()
}
const onExplore = () => {
window.open('https://www.comfy.org/cloud', '_blank')
useDialogStore().closeDialog()
}
</script>

View File

@@ -1,5 +1,22 @@
<template>
<div class="flex h-full shrink-0 items-center">
<!-- Cloud Notification Badge for Desktop -->
<div
v-if="cloudBadge"
class="relative inline-flex h-full shrink-0 cursor-pointer items-center justify-center gap-2 px-3 transition-opacity hover:opacity-70"
@click="handleCloudBadgeClick"
>
<div
class="rounded-full bg-white px-1.5 py-0.5 text-xxxs font-semibold text-black"
>
{{ t('cloudNotification.badgeLabel') }}
</div>
<div v-if="displayMode !== 'icon-only'" class="text-sm font-inter">
{{ t('cloudNotification.badgeText') }}
</div>
</div>
<!-- Extension Badges -->
<TopbarBadge
v-for="badge in topbarBadgeStore.badges"
:key="badge.text"
@@ -14,8 +31,13 @@
<script lang="ts" setup>
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useDialogService } from '@/services/dialogService'
import { useTopbarBadgeStore } from '@/stores/topbarBadgeStore'
import type { TopbarBadge as TopbarBadgeType } from '@/types/comfy'
import { isElectron } from '@/utils/envUtil'
import TopbarBadge from './TopbarBadge.vue'
@@ -41,4 +63,35 @@ const displayMode = computed<'full' | 'compact' | 'icon-only'>(() => {
})
const topbarBadgeStore = useTopbarBadgeStore()
// Cloud notification badge
const { t } = useI18n()
const settingStore = useSettingStore()
const dialogService = useDialogService()
const isMacOS = computed(() => navigator.platform.toLowerCase().includes('mac'))
const hasShownNotification = computed(() =>
settingStore.get('Comfy.Desktop.CloudNotificationShown')
)
const shouldShowCloudBadge = computed(
() => isElectron() && isMacOS.value && hasShownNotification.value
)
const cloudBadge = computed<TopbarBadgeType | null>(() => {
if (!shouldShowCloudBadge.value) return null
return {
text: 'Discover Comfy Cloud',
label: 'NEW',
icon: 'pi pi-cloud',
variant: 'info',
tooltip: 'Learn about Comfy Cloud'
}
})
const handleCloudBadgeClick = () => {
dialogService.showCloudNotification()
}
</script>

View File

@@ -2212,5 +2212,21 @@
"description": "This workflow uses custom nodes you haven't installed yet.",
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
}
},
"cloudNotification": {
"title": "Discover Comfy Cloud",
"message": "Get access to industry-grade GPUs and run workflows up to 10x faster",
"feature1Title": "No Setup Required",
"feature1": "Start creating instantly with popular models pre-installed",
"feature2Title": "Powerful GPUs",
"feature2": "A100 and RTX PRO 6000 GPUs for heavy video models",
"feature3Title": "$20/month",
"feature3": "Simple subscription with unlimited workflow runs",
"feature4": "ComfyUI stays free and open source.\nCloud is optional—for instant access to high-end GPUs.",
"continueLocally": "Continue Locally",
"exploreCloud": "Explore Cloud",
"badgeTooltip": "Learn about Comfy Cloud",
"badgeLabel": "NEW",
"badgeText": "Discover Comfy Cloud"
}
}

View File

@@ -293,6 +293,12 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'boolean',
defaultValue: true
},
{
id: 'Comfy.Desktop.CloudNotificationShown',
name: 'Cloud notification shown',
type: 'boolean',
defaultValue: false
},
{
id: 'Comfy.Graph.ZoomSpeed',
category: ['LiteGraph', 'Canvas', 'ZoomSpeed'],

View File

@@ -374,6 +374,7 @@ const zSettings = z.object({
'Comfy.Workflow.ShowMissingNodesWarning': z.boolean(),
'Comfy.Workflow.ShowMissingModelsWarning': z.boolean(),
'Comfy.Workflow.WarnBlueprintOverwrite': z.boolean(),
'Comfy.Desktop.CloudNotificationShown': z.boolean(),
'Comfy.DisableFloatRounding': z.boolean(),
'Comfy.DisableSliders': z.boolean(),
'Comfy.DOMClippingEnabled': z.boolean(),

View File

@@ -2,6 +2,7 @@ import { merge } from 'es-toolkit/compat'
import type { Component } from 'vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import CloudNotificationContent from '@/components/dialog/content/CloudNotificationContent.vue'
import MissingNodesContent from '@/components/dialog/content/MissingNodesContent.vue'
import MissingNodesFooter from '@/components/dialog/content/MissingNodesFooter.vue'
import MissingNodesHeader from '@/components/dialog/content/MissingNodesHeader.vue'
@@ -541,6 +542,16 @@ export const useDialogService = () => {
show()
}
function showCloudNotification() {
dialogStore.showDialog({
key: 'global-cloud-notification',
component: CloudNotificationContent,
dialogComponentProps: {
closable: true
}
})
}
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
@@ -555,6 +566,7 @@ export const useDialogService = () => {
showTopUpCreditsDialog,
showUpdatePasswordDialog,
showExtensionDialog,
showCloudNotification,
prompt,
showErrorDialog,
confirm,