Skip to content
Draft
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
23 changes: 22 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -47,7 +48,7 @@ const showContextMenu = (event: MouseEvent) => {
}
}

onMounted(() => {
onMounted(async () => {
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version

if (isElectron()) {
Expand Down Expand Up @@ -77,5 +78,25 @@ 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)
// Delayed to ensure it appears after workflow loading (missing models dialog, etc.)
if (isElectron()) {
const isMacOS = navigator.platform.toLowerCase().includes('mac')
if (isMacOS) {
const settingStore = useSettingStore()
const hasShownNotification = settingStore.get(
'Comfy.Desktop.CloudNotificationShown'
)

if (!hasShownNotification) {
// Delay to show after initial workflow loading completes
setTimeout(async () => {
dialogService.showCloudNotification()
await settingStore.set('Comfy.Desktop.CloudNotificationShown', true)
}, 2000)
}
}
}
})
</script>
126 changes: 126 additions & 0 deletions src/components/dialog/content/CloudNotificationContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<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 { onMounted } from 'vue'
import { useI18n } from 'vue-i18n'

import { useTelemetry } from '@/platform/telemetry'
import { useDialogStore } from '@/stores/dialogStore'

const { t } = useI18n()

// Track when modal is shown
onMounted(() => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'cloud_notification_modal_shown'
})
})

const onDismiss = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'cloud_notification_continue_locally_clicked'
})
useDialogStore().closeDialog()
}

const onExplore = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'cloud_notification_explore_cloud_clicked'
})

// Add UTM parameters for attribution tracking
const url = new URL('https://www.comfy.org/cloud')
url.searchParams.set('utm_source', 'desktop')
url.searchParams.set('utm_medium', 'notification')
url.searchParams.set('utm_campaign', 'macos_first_launch')

window.open(url.toString(), '_blank')
useDialogStore().closeDialog()
}
</script>
59 changes: 59 additions & 0 deletions src/components/topbar/TopbarBadges.vue
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -14,8 +31,14 @@
<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 { useTelemetry } from '@/platform/telemetry'
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'

Expand All @@ -41,4 +64,40 @@ 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'))

// Access the reactive store state directly for proper reactivity
const hasShownNotification = computed(
() =>
settingStore.settingValues['Comfy.Desktop.CloudNotificationShown'] ?? false
)

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 = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'cloud_notification_badge_clicked'
})
dialogService.showCloudNotification()
}
</script>
16 changes: 16 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
6 changes: 6 additions & 0 deletions src/platform/settings/constants/coreSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
1 change: 1 addition & 0 deletions src/schemas/apiSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
12 changes: 12 additions & 0 deletions src/services/dialogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -541,6 +542,16 @@ export const useDialogService = () => {
show()
}

function showCloudNotification() {
dialogStore.showDialog({
key: 'global-cloud-notification',
component: CloudNotificationContent,
dialogComponentProps: {
closable: true
}
})
}

return {
showLoadWorkflowWarning,
showMissingModelsWarning,
Expand All @@ -555,6 +566,7 @@ export const useDialogService = () => {
showTopUpCreditsDialog,
showUpdatePasswordDialog,
showExtensionDialog,
showCloudNotification,
prompt,
showErrorDialog,
confirm,
Expand Down
Loading