Compare commits

...

7 Commits

Author SHA1 Message Date
bymyself
046e31dadc remove dev testing code 2025-11-26 16:22:58 -08:00
bymyself
4b5de51e99 remove unrelated changes 2025-11-26 15:51:48 -08:00
bymyself
93ec2c74ab use update service 2025-11-26 15:32:21 -08:00
Deep Mehta
f5c26130f2 Remove cloud notification badge from topbar
- Remove persistent 'NEW' badge that appeared after modal dismissal
- Keep one-time modal notification for macOS desktop users
- Clean up unused badge translations
2025-11-25 02:12:08 +05:30
Deep Mehta
7bba8f09f6 Fix: Cloud notification badge reactivity and modal timing
- Fix badge not appearing by accessing settingStore.settingValues directly for proper reactivity
- Increase modal delay to 2s to ensure it appears after missing models dialog
- Move setting update inside setTimeout to only save when modal actually shows

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 04:17:25 +05:30
Deep Mehta
de39ece33f Add analytics tracking for cloud notification feature
Adds Mixpanel event tracking and UTM parameters to measure:
- Modal impression rate (cloud_notification_modal_shown)
- Click-through rate (cloud_notification_explore_cloud_clicked)
- Dismiss rate (cloud_notification_continue_locally_clicked)
- Badge engagement (cloud_notification_badge_clicked)

UTM parameters enable full funnel tracking:
- Desktop users clicking Explore Cloud
- Cloud website signups with utm_source=desktop
- Subscription conversions attributed to desktop

This enables measurement of the desktop→cloud conversion funnel to validate the 5.6% signup-to-paid conversion rate goal.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 02:00:00 +05:30
Deep Mehta
e5ba351dcc [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>
2025-11-23 20:53:58 +05:30
9 changed files with 273 additions and 0 deletions

View File

@@ -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>

View File

@@ -2212,5 +2212,18 @@
"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"
}
}

View File

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

View File

@@ -45,6 +45,7 @@ function onChange(
export const useSettingStore = defineStore('setting', () => {
const settingValues = ref<Record<string, any>>({})
const settingsById = ref<Record<string, SettingParams>>({})
const isInitialized = ref(false)
/**
* Check if a setting's value exists, i.e. if the user has set it manually.
@@ -196,6 +197,7 @@ export const useSettingStore = defineStore('setting', () => {
// Migrate old zoom threshold setting to new font size setting
await migrateZoomThresholdToFontSize()
isInitialized.value = true
}
/**
@@ -240,6 +242,7 @@ export const useSettingStore = defineStore('setting', () => {
return {
settingValues,
settingsById,
isInitialized,
addSetting,
loadSettingValues,
set,

View File

@@ -0,0 +1,68 @@
import { until } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { isElectron } from '@/utils/envUtil'
export const useCloudNotificationStore = defineStore(
'cloudNotification',
() => {
const settingStore = useSettingStore()
const systemStatsStore = useSystemStatsStore()
const isReady = ref(false)
const hasShownThisSession = ref(false)
async function initialize() {
if (isReady.value) return
await until(settingStore.isInitialized)
await until(systemStatsStore.isInitialized)
isReady.value = true
}
const isEligiblePlatform = computed(() => {
if (!isReady.value) return false
if (!isElectron()) return false
const osString =
systemStatsStore.systemStats?.system?.os?.toLowerCase() ?? ''
const platformString =
typeof navigator === 'undefined' ? '' : navigator.platform.toLowerCase()
return (
osString.includes('darwin') ||
osString.includes('mac') ||
platformString.includes('mac')
)
})
const hasSeenNotification = computed(() => {
if (!isReady.value) return true
return !!settingStore.get('Comfy.Desktop.CloudNotificationShown')
})
const shouldShowNotification = computed(() => {
if (!isReady.value) return false
if (!isEligiblePlatform.value) return false
return !hasSeenNotification.value && !hasShownThisSession.value
})
function markSessionShown() {
hasShownThisSession.value = true
}
async function persistNotificationShown() {
await settingStore.set('Comfy.Desktop.CloudNotificationShown', true)
}
return {
initialize,
shouldShowNotification,
markSessionShown,
persistNotificationShown
}
}
)

View File

@@ -0,0 +1,42 @@
<template>
<span class="sr-only" aria-hidden="true" />
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, watch } from 'vue'
import type { WatchStopHandle } from 'vue'
import { useCloudNotificationStore } from '@/platform/updates/common/cloudNotificationStore'
import { useDialogService } from '@/services/dialogService'
const dialogService = useDialogService()
const cloudNotificationStore = useCloudNotificationStore()
let stopWatcher: WatchStopHandle | null = null
onMounted(async () => {
await cloudNotificationStore.initialize()
stopWatcher = watch(
() => cloudNotificationStore.shouldShowNotification,
(shouldShow) => {
if (shouldShow) {
cloudNotificationStore.markSessionShown()
dialogService.showCloudNotification()
void cloudNotificationStore
.persistNotificationShown()
.catch((error) => {
console.error('[CloudNotification] Failed to persist flag', error)
})
}
},
{ immediate: true }
)
})
onBeforeUnmount(() => {
stopWatcher?.()
stopWatcher = null
})
</script>

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,

View File

@@ -18,6 +18,7 @@
<GlobalToast />
<RerouteMigrationToast />
<VueNodesMigrationToast />
<CloudNotificationOrchestrator />
<UnloadWindowConfirmDialog v-if="!isElectron()" />
<MenuHamburger />
</template>
@@ -56,6 +57,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useFrontendVersionMismatchWarning } from '@/platform/updates/common/useFrontendVersionMismatchWarning'
import { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore'
import CloudNotificationOrchestrator from '@/platform/updates/components/CloudNotificationOrchestrator.vue'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'