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
13 changes: 13 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,19 @@
"imageAlt": "Comfy Cloud nodes announcement",
"unmute": "Unmute video",
"mute": "Mute video"
},
"comfyRouter": {
"title": "Introducing Comfy Router",
"lead": "One API for the latest frontier media models",
"highlights": [
"Call the latest image, video, 3D, and audio models from your own scripts, tools, and apps using the Comfy credits you already have.",
"Add new models without setting up a separate account or integration for every provider.",
"When a model has multiple providers, choose based on price or availability and switch without rewriting your integration."
],
"primaryCta": "Learn About Comfy Router",
"imageAlt": "Comfy Router announcement",
"unmute": "Unmute video",
"mute": "Mute video"
}
},
"tray": {
Expand Down
13 changes: 13 additions & 0 deletions locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,19 @@
"imageAlt": "Comfy Cloud \u8282\u70b9\u516c\u544a",
"unmute": "\u53d6\u6d88\u9759\u97f3",
"mute": "\u9759\u97f3"
},
"comfyRouter": {
"title": "Comfy Router \u73b0\u5df2\u63a8\u51fa",
"lead": "\u4e00\u4e2a API\uff0c\u8c03\u7528\u6700\u65b0\u7684\u524d\u6cbf\u5a92\u4f53\u6a21\u578b",
"highlights": [
"\u7528\u4f60\u5df2\u6709\u7684 Comfy \u79ef\u5206\uff0c\u4ece\u81ea\u5df1\u7684\u811a\u672c\u3001\u5de5\u5177\u548c\u5e94\u7528\u4e2d\u8c03\u7528\u6700\u65b0\u7684\u56fe\u50cf\u3001\u89c6\u9891\u30013D \u548c\u97f3\u9891\u6a21\u578b\u3002",
"\u65e0\u9700\u4e3a\u6bcf\u4e2a\u63d0\u4f9b\u5546\u5355\u72ec\u6ce8\u518c\u8d26\u53f7\u6216\u505a\u96c6\u6210\uff0c\u5373\u53ef\u6dfb\u52a0\u65b0\u6a21\u578b\u3002",
"\u5f53\u4e00\u4e2a\u6a21\u578b\u6709\u591a\u4e2a\u63d0\u4f9b\u5546\u65f6\uff0c\u53ef\u6309\u4ef7\u683c\u6216\u53ef\u7528\u6027\u9009\u62e9\uff0c\u5e76\u5728\u4e0d\u91cd\u5199\u96c6\u6210\u7684\u60c5\u51b5\u4e0b\u5207\u6362\u3002"
],
"primaryCta": "\u4e86\u89e3 Comfy Router",
"imageAlt": "Comfy Router \u516c\u544a",
"unmute": "\u53d6\u6d88\u9759\u97f3",
"mute": "\u9759\u97f3"
}
},
"tray": {
Expand Down
5 changes: 5 additions & 0 deletions src/main/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export interface KnownSettings {
* rather than a reset of minimaxAnnouncementSeen: everyone who dismissed the
* previous announcement must still get the bell for this one. */
cloudNodesAnnouncementSeen?: boolean
/** Seen-flag for the Comfy Router announcement. New key again, same reasoning
* as cloudNodesAnnouncementSeen: everyone who dismissed the previous
* announcement must still get the bell for this one. */
comfyRouterAnnouncementSeen?: boolean
/** When true, hide the Cloud tile (and the Try-Cloud CTA) from the
* Dashboard / Instance Picker. Local-only users who never use Cloud
* can opt out of seeing it without us removing the feature. Default
Expand Down Expand Up @@ -274,6 +278,7 @@ const SETTINGS_SCHEMA = {
firstUseCompleted: { nullable: false, telemetry: { policy: 'omit' } },
minimaxAnnouncementSeen: { nullable: false, telemetry: { policy: 'omit' } },
cloudNodesAnnouncementSeen: { nullable: false, telemetry: { policy: 'omit' } },
comfyRouterAnnouncementSeen: { nullable: false, telemetry: { policy: 'omit' } },
hideCloudFromPicker: {
nullable: false,
telemetry: { policy: 'value', toTelemetry: (raw) => raw === true }
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/src/comfyTitleBar/TitleBarApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ function handleResetZoom(): void {
}

// News bell with a subtle unread dot until the announcement is opened. Its state
// lives in the `cloudNodesAnnouncementSeen` setting (shared with the panel view);
// lives in the `comfyRouterAnnouncementSeen` setting (shared with the panel view);
// opening the modal writes it, and the settings-changed broadcast clears the
// dot here without a dedicated channel.
const announcementUnread = ref(false)
Expand All @@ -308,7 +308,7 @@ function handleAnnouncement(): void {
async function refreshAnnouncementUnread(): Promise<void> {
try {
announcementUnread.value =
(await window.api?.getSetting?.('cloudNodesAnnouncementSeen')) !== true
(await window.api?.getSetting?.('comfyRouterAnnouncementSeen')) !== true
} catch {
announcementUnread.value = false
}
Expand All @@ -318,7 +318,7 @@ onMounted(() => {
void refreshAnnouncementUnread()
unsubAnnouncementSettings =
window.api?.onSettingsChanged?.(({ key }) => {
if (key === 'cloudNodesAnnouncementSeen') void refreshAnnouncementUnread()
if (key === 'comfyRouterAnnouncementSeen') void refreshAnnouncementUnread()
}) ?? null
})

Expand Down
50 changes: 20 additions & 30 deletions src/renderer/src/components/AnnouncementModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,35 @@ import { Check, Volume2, VolumeX, X } from 'lucide-vue-next'
import { emitTelemetryAction } from '../lib/telemetry'

/**
* One-off announcement modal (currently the Comfy Cloud nodes launch).
* One-off announcement modal (currently the Comfy Router launch).
* Deliberately mirrors `WhyTryCloudModal.vue`'s layout and styling so it feels
* native. Swapping to the next announcement means changing ANNOUNCEMENT_ID, the
* URLs, the hero media and the i18n namespace below, plus a new seen-flag in
* settings so previously-dismissed users still get the bell.
*
* Strings live in `announcement.cloudNodes.*` (locales/en.json).
* Strings live in `announcement.comfyRouter.*` (locales/en.json).
*/

// Announcement id, carried in telemetry so this component can be reused for a
// future announcement by swapping the id + copy.
const ANNOUNCEMENT_ID = 'comfy_cloud_nodes'
const ANNOUNCEMENT_ID = 'comfy_router'

// CTA destinations. The UTM tags clicks as Desktop-origin so they're
// CTA destination. The UTM tags clicks as Desktop-origin so they're
// attributable in analytics.
const UTM = '?utm_source=comfy_desktop&utm_medium=announcement&utm_campaign=comfy_cloud_nodes'
const PRIMARY_CTA_URL = `https://comfy.org/cloud-nodes${UTM}`
const DOCS_CTA_URL = `https://docs.comfy.org/cloud-nodes/overview${UTM}`
const UTM = '?utm_source=comfy_desktop&utm_medium=announcement&utm_campaign=comfy_router'
const PRIMARY_CTA_URL = `https://comfy.org/platform/router${UTM}`

// Same 15s launch film the /cloud-nodes landing page leads with. Poster paints
// immediately; the video muted-autoplays + loops. media.comfy.org objects are
// cached for an hour, so bump the `_v1` suffix rather than re-uploading a key.
const HERO_VIDEO_URL = 'https://media.comfy.org/website/cloud-nodes/hero_v1.mp4'
const HERO_POSTER_URL = 'https://media.comfy.org/website/cloud-nodes/hero-poster_v1.webp'
// Router launch hero on media.comfy.org. media.comfy.org caches for an hour, so
// bump the version in the filename rather than re-uploading a key. Poster paints
// immediately; the video muted-autoplays + loops.
const HERO_VIDEO_URL = 'https://media.comfy.org/website/router/router-animatic-v019.mp4'
const HERO_POSTER_URL = 'https://media.comfy.org/website/router/router-animatic-v019-poster.webp'

const emit = defineEmits<{ close: [] }>()
const { tm } = useI18n()

const highlights = computed<string[]>(() => {
const raw = tm('announcement.cloudNodes.highlights')
const raw = tm('announcement.comfyRouter.highlights')
return Array.isArray(raw) ? (raw as unknown as string[]) : []
})

Expand Down Expand Up @@ -99,7 +98,7 @@ onUnmounted(() => {
class="announce-overlay"
role="dialog"
aria-modal="true"
:aria-label="$t('announcement.cloudNodes.title')"
:aria-label="$t('announcement.comfyRouter.title')"
tabindex="-1"
@mousedown="onOverlayMouseDown"
@click="onOverlayClick"
Expand All @@ -124,7 +123,7 @@ onUnmounted(() => {
muted
loop
playsinline
:aria-label="$t('announcement.cloudNodes.imageAlt')"
:aria-label="$t('announcement.comfyRouter.imageAlt')"
>
<source :src="HERO_VIDEO_URL" type="video/mp4" />
</video>
Expand All @@ -133,8 +132,8 @@ onUnmounted(() => {
type="button"
:aria-label="
isMuted
? $t('announcement.cloudNodes.unmute')
: $t('announcement.cloudNodes.mute')
? $t('announcement.comfyRouter.unmute')
: $t('announcement.comfyRouter.mute')
"
:aria-pressed="!isMuted"
data-testid="announcement-sound-toggle"
Expand All @@ -147,33 +146,24 @@ onUnmounted(() => {
<div class="announce-body">
<div class="announce-body-main">
<header class="announce-header">
<h2 class="announce-title">{{ $t('announcement.cloudNodes.title') }}</h2>
<h2 class="announce-title">{{ $t('announcement.comfyRouter.title') }}</h2>
</header>
<p class="announce-lead">{{ $t('announcement.cloudNodes.lead') }}</p>
<p class="announce-lead">{{ $t('announcement.comfyRouter.lead') }}</p>
<ul v-if="highlights.length" class="announce-list">
<li v-for="h in highlights" :key="h">
<Check :size="16" class="announce-check" />
<span>{{ h }}</span>
</li>
</ul>
</div>
<p class="announce-fine">{{ $t('announcement.cloudNodes.beta') }}</p>
<footer class="announce-footer">
<button
class="brand-ghost"
type="button"
data-testid="announcement-docs"
@click="openCta('docs', DOCS_CTA_URL)"
>
{{ $t('announcement.cloudNodes.docsCta') }}
</button>
<button
class="brand-primary"
type="button"
data-testid="announcement-primary-cta"
@click="openCta('get_started', PRIMARY_CTA_URL)"
@click="openCta('learn_more', PRIMARY_CTA_URL)"
>
{{ $t('announcement.cloudNodes.primaryCta') }}
{{ $t('announcement.comfyRouter.primaryCta') }}
</button>
</footer>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/composables/useAnnouncement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface UseAnnouncementApi {
/**
* Title-bar news bell → main forwards `onOpenAnnouncement` here, mirroring the
* Send Feedback flow (`useSendFeedback`). Opening marks the announcement seen
* (persisted via `cloudNodesAnnouncementSeen`); the settings-changed broadcast
* (persisted via `comfyRouterAnnouncementSeen`); the settings-changed broadcast
* then clears the title-bar's unread dot. The modal owns its own telemetry.
*/
export function useAnnouncement(): UseAnnouncementApi {
Expand All @@ -26,7 +26,7 @@ export function useAnnouncement(): UseAnnouncementApi {

onMounted(() => {
unsubOpen = window.api.onOpenAnnouncement(() => {
void window.api.setSetting('cloudNodesAnnouncementSeen', true)
void window.api.setSetting('comfyRouterAnnouncementSeen', true)
announcementOpen.value = true
})
})
Expand Down
Loading