Skip to content

Commit 5c14227

Browse files
authored
Move price badges to python nodes (#7816)
## Summary Backend part: Comfy-Org/ComfyUI#11582 - Move API node pricing definitions from hardcoded frontend functions to backend-defined JSONata expressions - Add `price_badge` field to node definition schema containing JSONata expression and dependency declarations - Implement async JSONata evaluation with signature-based caching for efficient reactive updates - Show one decimal in credit badges when meaningful (e.g., 1.5 credits instead of 2 credits) ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7816-Move-price-badges-to-python-nodes-2da6d73d365081ec815ef61f7e3c65f7) by [Unito](https://www.unito.io)
1 parent 4a5e7c8 commit 5c14227

File tree

9 files changed

+1369
-4792
lines changed

9 files changed

+1369
-4792
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
"firebase": "catalog:",
170170
"fuse.js": "^7.0.0",
171171
"glob": "^11.0.3",
172+
"jsonata": "catalog:",
172173
"jsondiffpatch": "^0.6.0",
173174
"loglevel": "^1.9.2",
174175
"marked": "^15.0.11",

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ catalog:
6262
happy-dom: ^20.0.11
6363
husky: ^9.1.7
6464
jiti: 2.6.1
65+
jsonata: ^2.1.0
6566
jsdom: ^27.4.0
6667
knip: ^5.75.1
6768
lint-staged: ^16.2.7

src/composables/node/useNodeBadge.ts

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ export const useNodeBadge = () => {
7373
onMounted(() => {
7474
const nodePricing = useNodePricing()
7575

76+
watch(
77+
() => nodePricing.pricingRevision.value,
78+
() => {
79+
if (!showApiPricingBadge.value) return
80+
app.canvas?.setDirty(true, true)
81+
}
82+
)
83+
7684
extensionStore.registerExtension({
7785
name: 'Comfy.NodeBadge',
7886
nodeCreated(node: LGraphNode) {
@@ -111,17 +119,16 @@ export const useNodeBadge = () => {
111119
node.badges.push(() => badge.value)
112120

113121
if (node.constructor.nodeData?.api_node && showApiPricingBadge.value) {
114-
// Get the pricing function to determine if this node has dynamic pricing
122+
// JSONata rules are dynamic if they depend on any widgets/inputs/input_groups
115123
const pricingConfig = nodePricing.getNodePricingConfig(node)
116124
const hasDynamicPricing =
117-
typeof pricingConfig?.displayPrice === 'function'
118-
119-
let creditsBadge
120-
const createBadge = () => {
121-
const price = nodePricing.getNodeDisplayPrice(node)
122-
return priceBadge.getCreditsBadge(price)
123-
}
125+
!!pricingConfig &&
126+
((pricingConfig.depends_on?.widgets?.length ?? 0) > 0 ||
127+
(pricingConfig.depends_on?.inputs?.length ?? 0) > 0 ||
128+
(pricingConfig.depends_on?.input_groups?.length ?? 0) > 0)
124129

130+
// Keep the existing widget-watch wiring ONLY to trigger redraws on widget change.
131+
// (We no longer rely on it to hold the current badge value.)
125132
if (hasDynamicPricing) {
126133
// For dynamic pricing nodes, use computed that watches widget changes
127134
const relevantWidgetNames = nodePricing.getRelevantWidgetNames(
@@ -133,13 +140,63 @@ export const useNodeBadge = () => {
133140
triggerCanvasRedraw: true
134141
})
135142

136-
creditsBadge = computedWithWidgetWatch(createBadge)
137-
} else {
138-
// For static pricing nodes, use regular computed
139-
creditsBadge = computed(createBadge)
143+
// Ensure watchers are installed; ignore the returned value.
144+
// (This call is what registers the widget listeners in most implementations.)
145+
computedWithWidgetWatch(() => 0)
146+
147+
// Hook into connection changes to trigger price recalculation
148+
// This handles both connect and disconnect in VueNodes mode
149+
const relevantInputs = pricingConfig?.depends_on?.inputs ?? []
150+
const inputGroupPrefixes =
151+
pricingConfig?.depends_on?.input_groups ?? []
152+
const hasRelevantInputs =
153+
relevantInputs.length > 0 || inputGroupPrefixes.length > 0
154+
155+
if (hasRelevantInputs) {
156+
const originalOnConnectionsChange = node.onConnectionsChange
157+
node.onConnectionsChange = function (
158+
type,
159+
slotIndex,
160+
isConnected,
161+
link,
162+
ioSlot
163+
) {
164+
originalOnConnectionsChange?.call(
165+
this,
166+
type,
167+
slotIndex,
168+
isConnected,
169+
link,
170+
ioSlot
171+
)
172+
// Only trigger if this input affects pricing
173+
const inputName = ioSlot?.name
174+
if (!inputName) return
175+
const isRelevantInput =
176+
relevantInputs.includes(inputName) ||
177+
inputGroupPrefixes.some((prefix) =>
178+
inputName.startsWith(prefix + '.')
179+
)
180+
if (isRelevantInput) {
181+
nodePricing.triggerPriceRecalculation(node)
182+
}
183+
}
184+
}
185+
}
186+
187+
let lastLabel = nodePricing.getNodeDisplayPrice(node)
188+
let lastBadge = priceBadge.getCreditsBadge(lastLabel)
189+
190+
const creditsBadgeGetter: () => LGraphBadge = () => {
191+
const label = nodePricing.getNodeDisplayPrice(node)
192+
if (label !== lastLabel) {
193+
lastLabel = label
194+
lastBadge = priceBadge.getCreditsBadge(label)
195+
}
196+
return lastBadge
140197
}
141198

142-
node.badges.push(() => creditsBadge.value)
199+
node.badges.push(creditsBadgeGetter)
143200
}
144201
},
145202
init() {

0 commit comments

Comments
 (0)