mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
change credits icons and tooltips (conditional on feature flag) (#7276)
This PR changes the credits icons and tooltips based on state of the `subscription_tiers_enabled` feature flag. When the flag is enabled (or undefined -- for local), the dollar icon is replaced with the lucide-component icon in UserCredit and node price badges (Partner Nodes), and a new tooltip row appears in CurrentUserPopover displaying "Credits have been unified" with a detailed hover tooltip explaining the credit unification across Partner Nodes and Cloud workflows. <img width="539" height="535" alt="image" src="https://github.com/user-attachments/assets/7e952f9b-0abb-4979-85b7-0eecdeaf808c" /> Related: - https://github.com/Comfy-Org/ComfyUI_frontend/pull/6115 (borrows badge implementation from this PR) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7276-change-credits-icons-and-tooltips-conditional-on-feature-flag-2c46d73d365081809a6afd5861018a15) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -7,12 +7,17 @@
|
||||
<Skeleton width="8rem" height="2rem" />
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="p-1 text-amber-400"
|
||||
/>
|
||||
<Tag severity="secondary" rounded class="p-1 text-amber-400">
|
||||
<template #icon>
|
||||
<i
|
||||
:class="
|
||||
flags.subscriptionTiersEnabled
|
||||
? 'icon-[lucide--component]'
|
||||
: 'pi pi-dollar'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</Tag>
|
||||
<div :class="textClass">{{ formattedBalance }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,6 +27,7 @@ import Skeleton from 'primevue/skeleton'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
@@ -30,6 +36,7 @@ const { textClass } = defineProps<{
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const { flags } = useFeatureFlags()
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
const formattedBalance = computed(() => {
|
||||
|
||||
@@ -26,6 +26,22 @@
|
||||
<div v-if="isActiveSubscription" class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<UserCredit text-class="text-2xl" />
|
||||
<div
|
||||
v-if="flags.subscriptionTiersEnabled"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<i
|
||||
v-tooltip="{
|
||||
value: $t('credits.unified.tooltip'),
|
||||
showDelay: 300,
|
||||
hideDelay: 300
|
||||
}"
|
||||
class="icon-[lucide--circle-help] text-muted cursor-help text-xs"
|
||||
/>
|
||||
<span class="text-xs text-muted">{{
|
||||
$t('credits.unified.message')
|
||||
}}</span>
|
||||
</div>
|
||||
<Button
|
||||
:label="$t('subscription.partnerNodesCredits')"
|
||||
severity="secondary"
|
||||
@@ -102,6 +118,7 @@ import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
@@ -123,6 +140,7 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const dialogService = useDialogService()
|
||||
const { isActiveSubscription, fetchStatus } = useSubscription()
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphBadge } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
const componentIconSvg = new Image()
|
||||
componentIconSvg.src =
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='oklch(83.01%25 0.163 83.16)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0zm-13.239 0a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0zm6.619 6.619a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0zm0-13.238a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z'/%3E%3C/svg%3E"
|
||||
|
||||
export const usePriceBadge = () => {
|
||||
const { flags } = useFeatureFlags()
|
||||
function updateSubgraphCredits(node: LGraphNode) {
|
||||
if (!node.isSubgraphNode()) return
|
||||
node.badges = node.badges.filter((b) => !isCreditsBadge(b))
|
||||
@@ -33,34 +39,54 @@ export const usePriceBadge = () => {
|
||||
}
|
||||
|
||||
function isCreditsBadge(badge: LGraphBadge | (() => LGraphBadge)): boolean {
|
||||
return (
|
||||
(typeof badge === 'function' ? badge() : badge).icon?.unicode === '\ue96b'
|
||||
)
|
||||
const badgeInstance = typeof badge === 'function' ? badge() : badge
|
||||
if (flags.subscriptionTiersEnabled) {
|
||||
return badgeInstance.icon?.image === componentIconSvg
|
||||
} else {
|
||||
return badgeInstance.icon?.unicode === '\ue96b'
|
||||
}
|
||||
}
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
function getCreditsBadge(price: string): LGraphBadge {
|
||||
const isLightTheme = colorPaletteStore.completedActivePalette.light_theme
|
||||
return new LGraphBadge({
|
||||
text: price,
|
||||
iconOptions: {
|
||||
unicode: '\ue96b',
|
||||
fontFamily: 'PrimeIcons',
|
||||
color: isLightTheme
|
||||
? adjustColor('#FABC25', { lightness: 0.5 })
|
||||
: '#FABC25',
|
||||
|
||||
if (flags.subscriptionTiersEnabled) {
|
||||
return new LGraphBadge({
|
||||
text: price,
|
||||
iconOptions: {
|
||||
image: componentIconSvg,
|
||||
size: 8
|
||||
},
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor: isLightTheme
|
||||
? adjustColor('#654020', { lightness: 0.5 })
|
||||
: '#654020',
|
||||
fontSize: 8
|
||||
},
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor: isLightTheme
|
||||
? adjustColor('#8D6932', { lightness: 0.5 })
|
||||
: '#8D6932'
|
||||
})
|
||||
? adjustColor('#8D6932', { lightness: 0.5 })
|
||||
: '#8D6932'
|
||||
})
|
||||
} else {
|
||||
return new LGraphBadge({
|
||||
text: price,
|
||||
iconOptions: {
|
||||
unicode: '\ue96b',
|
||||
fontFamily: 'PrimeIcons',
|
||||
color: isLightTheme
|
||||
? adjustColor('#FABC25', { lightness: 0.5 })
|
||||
: '#FABC25',
|
||||
bgColor: isLightTheme
|
||||
? adjustColor('#654020', { lightness: 0.5 })
|
||||
: '#654020',
|
||||
fontSize: 8
|
||||
},
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor: isLightTheme
|
||||
? adjustColor('#8D6932', { lightness: 0.5 })
|
||||
: '#8D6932'
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
getCreditsBadge,
|
||||
|
||||
@@ -12,7 +12,8 @@ export enum ServerFeatureFlag {
|
||||
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
|
||||
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
|
||||
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled',
|
||||
PRIVATE_MODELS_ENABLED = 'private_models_enabled'
|
||||
PRIVATE_MODELS_ENABLED = 'private_models_enabled',
|
||||
SUBSCRIPTION_TIERS_ENABLED = 'subscription_tiers_enabled'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,6 +56,16 @@ export function useFeatureFlags() {
|
||||
remoteConfig.value.private_models_enabled ??
|
||||
api.getServerFeature(ServerFeatureFlag.PRIVATE_MODELS_ENABLED, false)
|
||||
)
|
||||
},
|
||||
get subscriptionTiersEnabled() {
|
||||
// Check remote config first (from /api/features), fall back to websocket feature flags
|
||||
return (
|
||||
remoteConfig.value.subscription_tiers_enabled ??
|
||||
api.getServerFeature(
|
||||
ServerFeatureFlag.SUBSCRIPTION_TIERS_ENABLED,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -66,8 +66,12 @@ export class LGraphBadge {
|
||||
const { font } = ctx
|
||||
let iconWidth = 0
|
||||
if (this.icon) {
|
||||
ctx.font = `${this.icon.fontSize}px '${this.icon.fontFamily}'`
|
||||
iconWidth = ctx.measureText(this.icon.unicode).width + this.padding
|
||||
if (this.icon.image) {
|
||||
iconWidth = this.icon.size + this.padding
|
||||
} else if (this.icon.unicode) {
|
||||
ctx.font = `${this.icon.fontSize}px '${this.icon.fontFamily}'`
|
||||
iconWidth = ctx.measureText(this.icon.unicode).width + this.padding
|
||||
}
|
||||
}
|
||||
ctx.font = `${this.fontSize}px sans-serif`
|
||||
const textWidth = this.text ? ctx.measureText(this.text).width : 0
|
||||
@@ -104,7 +108,8 @@ export class LGraphBadge {
|
||||
// Draw icon if present
|
||||
if (this.icon) {
|
||||
this.icon.draw(ctx, drawX, centerY)
|
||||
drawX += this.icon.fontSize + this.padding / 2 + 4
|
||||
const iconWidth = this.icon.image ? this.icon.size : this.icon.fontSize
|
||||
drawX += iconWidth + this.padding / 2 + 4
|
||||
}
|
||||
|
||||
// Draw badge text
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
export interface LGraphIconOptions {
|
||||
unicode: string
|
||||
unicode?: string
|
||||
fontFamily?: string
|
||||
image?: HTMLImageElement
|
||||
color?: string
|
||||
bgColor?: string
|
||||
fontSize?: number
|
||||
size?: number
|
||||
circlePadding?: number
|
||||
xOffset?: number
|
||||
yOffset?: number
|
||||
}
|
||||
|
||||
export class LGraphIcon {
|
||||
unicode: string
|
||||
unicode?: string
|
||||
fontFamily: string
|
||||
image?: HTMLImageElement
|
||||
color: string
|
||||
bgColor?: string
|
||||
fontSize: number
|
||||
size: number
|
||||
circlePadding: number
|
||||
xOffset: number
|
||||
yOffset: number
|
||||
@@ -22,18 +26,22 @@ export class LGraphIcon {
|
||||
constructor({
|
||||
unicode,
|
||||
fontFamily = 'PrimeIcons',
|
||||
image,
|
||||
color = '#e6c200',
|
||||
bgColor,
|
||||
fontSize = 16,
|
||||
size,
|
||||
circlePadding = 2,
|
||||
xOffset = 0,
|
||||
yOffset = 0
|
||||
}: LGraphIconOptions) {
|
||||
this.unicode = unicode
|
||||
this.fontFamily = fontFamily
|
||||
this.image = image
|
||||
this.color = color
|
||||
this.bgColor = bgColor
|
||||
this.fontSize = fontSize
|
||||
this.size = size ?? fontSize
|
||||
this.circlePadding = circlePadding
|
||||
this.xOffset = xOffset
|
||||
this.yOffset = yOffset
|
||||
@@ -43,26 +51,44 @@ export class LGraphIcon {
|
||||
x += this.xOffset
|
||||
y += this.yOffset
|
||||
|
||||
const { font, textBaseline, textAlign, fillStyle } = ctx
|
||||
if (this.image) {
|
||||
const iconSize = this.size
|
||||
const iconRadius = iconSize / 2 + this.circlePadding
|
||||
|
||||
ctx.font = `${this.fontSize}px '${this.fontFamily}'`
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'center'
|
||||
const iconRadius = this.fontSize / 2 + this.circlePadding
|
||||
// Draw icon background circle if bgColor is set
|
||||
if (this.bgColor) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(x + iconRadius, y, iconRadius, 0, 2 * Math.PI)
|
||||
ctx.fillStyle = this.bgColor
|
||||
ctx.fill()
|
||||
if (this.bgColor) {
|
||||
const { fillStyle } = ctx
|
||||
ctx.beginPath()
|
||||
ctx.arc(x + iconRadius, y, iconRadius, 0, 2 * Math.PI)
|
||||
ctx.fillStyle = this.bgColor
|
||||
ctx.fill()
|
||||
ctx.fillStyle = fillStyle
|
||||
}
|
||||
|
||||
const imageX = x + this.circlePadding
|
||||
const imageY = y - iconSize / 2
|
||||
ctx.drawImage(this.image, imageX, imageY, iconSize, iconSize)
|
||||
} else if (this.unicode) {
|
||||
const { font, textBaseline, textAlign, fillStyle } = ctx
|
||||
|
||||
ctx.font = `${this.fontSize}px '${this.fontFamily}'`
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'center'
|
||||
const iconRadius = this.fontSize / 2 + this.circlePadding
|
||||
|
||||
if (this.bgColor) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(x + iconRadius, y, iconRadius, 0, 2 * Math.PI)
|
||||
ctx.fillStyle = this.bgColor
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.fillStyle = this.color
|
||||
ctx.fillText(this.unicode, x + iconRadius, y)
|
||||
|
||||
ctx.font = font
|
||||
ctx.textBaseline = textBaseline
|
||||
ctx.textAlign = textAlign
|
||||
ctx.fillStyle = fillStyle
|
||||
}
|
||||
// Draw icon
|
||||
ctx.fillStyle = this.color
|
||||
ctx.fillText(this.unicode, x + iconRadius, y)
|
||||
|
||||
ctx.font = font
|
||||
ctx.textBaseline = textBaseline
|
||||
ctx.textAlign = textAlign
|
||||
ctx.fillStyle = fillStyle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1852,7 +1852,11 @@
|
||||
"additionalInfo": "Additional Info",
|
||||
"model": "Model",
|
||||
"added": "Added",
|
||||
"accountInitialized": "Account initialized"
|
||||
"accountInitialized": "Account initialized",
|
||||
"unified": {
|
||||
"message": "Credits have been unified",
|
||||
"tooltip": "We've unified payments across Comfy. Everything now runs on Comfy Credits:\n- Partner Nodes (formerly API nodes)\n- Cloud workflows\n\nYour existing Partner node balance has been converted into credits.\nLearn more here."
|
||||
}
|
||||
},
|
||||
"subscription": {
|
||||
"title": "Subscription",
|
||||
|
||||
@@ -37,4 +37,5 @@ export type RemoteConfig = {
|
||||
model_upload_button_enabled?: boolean
|
||||
asset_update_options_enabled?: boolean
|
||||
private_models_enabled?: boolean
|
||||
subscription_tiers_enabled?: boolean
|
||||
}
|
||||
|
||||
@@ -38,7 +38,16 @@
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div v-if="isApiNode" class="icon-[lucide--dollar-sign] size-4" />
|
||||
<div v-if="isSubgraphNode" class="icon-[comfy--workflow] size-4" />
|
||||
<div
|
||||
v-if="isApiNode"
|
||||
:class="
|
||||
flags.subscriptionTiersEnabled
|
||||
? 'icon-[lucide--component]'
|
||||
: 'icon-[lucide--dollar-sign]'
|
||||
"
|
||||
class="size-4"
|
||||
/>
|
||||
|
||||
<!-- Node Title -->
|
||||
<div
|
||||
@@ -98,6 +107,7 @@ import IconButton from '@/components/button/IconButton.vue'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { st } from '@/i18n'
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -128,6 +138,8 @@ const emit = defineEmits<{
|
||||
'enter-subgraph': []
|
||||
}>()
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
// Error boundary implementation
|
||||
const renderError = ref<string | null>(null)
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
|
||||
Reference in New Issue
Block a user