Skip to content

Commit ca5729a

Browse files
authored
Add pricing badge when a subgraph contains partner nodes (#6354)
<img width="596" height="213" alt="image" src="https://github.com/user-attachments/assets/174c5461-f638-42de-b3ad-0e108dee3983" /> ![api-badge-subgraph_00003](https://github.com/user-attachments/assets/067d0398-47e9-4e97-9e1d-67fac2935e55) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6354-Add-pricing-badge-when-a-subgraph-contains-partner-nodes-29b6d73d365081c685bec3e9446970eb) by [Unito](https://www.unito.io)
1 parent e606ff3 commit ca5729a

File tree

7 files changed

+175
-30
lines changed

7 files changed

+175
-30
lines changed

src/composables/node/useNodeBadge.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat'
22
import { computed, onMounted, watch } from 'vue'
33

44
import { useNodePricing } from '@/composables/node/useNodePricing'
5+
import { usePriceBadge } from '@/composables/node/usePriceBadge'
56
import { useComputedWithWidgetWatch } from '@/composables/node/useWatchWidget'
67
import { BadgePosition, LGraphBadge } from '@/lib/litegraph/src/litegraph'
78
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
@@ -12,7 +13,6 @@ import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
1213
import { useNodeDefStore } from '@/stores/nodeDefStore'
1314
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
1415
import { NodeBadgeMode } from '@/types/nodeSource'
15-
import { adjustColor } from '@/utils/colorUtil'
1616

1717
/**
1818
* Add LGraphBadge to LGraphNode based on settings.
@@ -27,6 +27,7 @@ export const useNodeBadge = () => {
2727
const settingStore = useSettingStore()
2828
const extensionStore = useExtensionStore()
2929
const colorPaletteStore = useColorPaletteStore()
30+
const priceBadge = usePriceBadge()
3031

3132
const nodeSourceBadgeMode = computed(
3233
() =>
@@ -118,29 +119,7 @@ export const useNodeBadge = () => {
118119
let creditsBadge
119120
const createBadge = () => {
120121
const price = nodePricing.getNodeDisplayPrice(node)
121-
122-
const isLightTheme =
123-
colorPaletteStore.completedActivePalette.light_theme
124-
return new LGraphBadge({
125-
text: price,
126-
iconOptions: {
127-
unicode: '\ue96b',
128-
fontFamily: 'PrimeIcons',
129-
color: isLightTheme
130-
? adjustColor('#FABC25', { lightness: 0.5 })
131-
: '#FABC25',
132-
bgColor: isLightTheme
133-
? adjustColor('#654020', { lightness: 0.5 })
134-
: '#654020',
135-
fontSize: 8
136-
},
137-
fgColor:
138-
colorPaletteStore.completedActivePalette.colors.litegraph_base
139-
.BADGE_FG_COLOR,
140-
bgColor: isLightTheme
141-
? adjustColor('#8D6932', { lightness: 0.5 })
142-
: '#8D6932'
143-
})
122+
return priceBadge.getCreditsBadge(price)
144123
}
145124

146125
if (hasDynamicPricing) {
@@ -162,6 +141,23 @@ export const useNodeBadge = () => {
162141

163142
node.badges.push(() => creditsBadge.value)
164143
}
144+
},
145+
init() {
146+
app.canvas.canvas.addEventListener<'litegraph:set-graph'>(
147+
'litegraph:set-graph',
148+
() => {
149+
for (const node of app.canvas.graph?.nodes ?? [])
150+
priceBadge.updateSubgraphCredits(node)
151+
}
152+
)
153+
app.canvas.canvas.addEventListener<'subgraph-converted'>(
154+
'subgraph-converted',
155+
(e) => priceBadge.updateSubgraphCredits(e.detail.subgraphNode)
156+
)
157+
},
158+
afterConfigureGraph() {
159+
for (const node of app.canvas.graph?.nodes ?? [])
160+
priceBadge.updateSubgraphCredits(node)
165161
}
166162
})
167163
})
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
2+
import { LGraphBadge } from '@/lib/litegraph/src/litegraph'
3+
4+
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
5+
import { adjustColor } from '@/utils/colorUtil'
6+
7+
export const usePriceBadge = () => {
8+
function updateSubgraphCredits(node: LGraphNode) {
9+
if (!node.isSubgraphNode()) return
10+
node.badges = node.badges.filter((b) => !isCreditsBadge(b))
11+
const newBadges = collectCreditsBadges(node.subgraph)
12+
if (newBadges.length > 1) {
13+
node.badges.push(getCreditsBadge('Partner Nodes x ' + newBadges.length))
14+
} else {
15+
node.badges.push(...newBadges)
16+
}
17+
}
18+
function collectCreditsBadges(
19+
graph: LGraph,
20+
visited: Set<string> = new Set()
21+
): (LGraphBadge | (() => LGraphBadge))[] {
22+
if (visited.has(graph.id)) return []
23+
visited.add(graph.id)
24+
const badges = []
25+
for (const node of graph.nodes) {
26+
badges.push(
27+
...(node.isSubgraphNode()
28+
? collectCreditsBadges(node.subgraph, visited)
29+
: node.badges.filter((b) => isCreditsBadge(b)))
30+
)
31+
}
32+
return badges
33+
}
34+
35+
function isCreditsBadge(badge: LGraphBadge | (() => LGraphBadge)): boolean {
36+
return (
37+
(typeof badge === 'function' ? badge() : badge).icon?.unicode === '\ue96b'
38+
)
39+
}
40+
41+
const colorPaletteStore = useColorPaletteStore()
42+
function getCreditsBadge(price: string): LGraphBadge {
43+
const isLightTheme = colorPaletteStore.completedActivePalette.light_theme
44+
return new LGraphBadge({
45+
text: price,
46+
iconOptions: {
47+
unicode: '\ue96b',
48+
fontFamily: 'PrimeIcons',
49+
color: isLightTheme
50+
? adjustColor('#FABC25', { lightness: 0.5 })
51+
: '#FABC25',
52+
bgColor: isLightTheme
53+
? adjustColor('#654020', { lightness: 0.5 })
54+
: '#654020',
55+
fontSize: 8
56+
},
57+
fgColor:
58+
colorPaletteStore.completedActivePalette.colors.litegraph_base
59+
.BADGE_FG_COLOR,
60+
bgColor: isLightTheme
61+
? adjustColor('#8D6932', { lightness: 0.5 })
62+
: '#8D6932'
63+
})
64+
}
65+
return {
66+
getCreditsBadge,
67+
updateSubgraphCredits
68+
}
69+
}

src/composables/useCoreCommands.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
DEFAULT_DARK_COLOR_PALETTE,
66
DEFAULT_LIGHT_COLOR_PALETTE
77
} from '@/constants/coreColorPalettes'
8-
import {
9-
promoteRecommendedWidgets,
10-
tryToggleWidgetPromotion
11-
} from '@/core/graph/subgraph/proxyWidgetUtils'
8+
import { tryToggleWidgetPromotion } from '@/core/graph/subgraph/proxyWidgetUtils'
129
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
1310
import { t } from '@/i18n'
1411
import {
@@ -945,7 +942,6 @@ export function useCoreCommands(): ComfyCommand[] {
945942

946943
const { node } = res
947944
canvas.select(node)
948-
promoteRecommendedWidgets(node)
949945
canvasStore.updateSelectedItems()
950946
}
951947
},

src/core/graph/subgraph/proxyWidget.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { demoteWidget } from '@/core/graph/subgraph/proxyWidgetUtils'
1+
import {
2+
demoteWidget,
3+
promoteRecommendedWidgets
4+
} from '@/core/graph/subgraph/proxyWidgetUtils'
25
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
36
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
47
import type {
@@ -62,6 +65,10 @@ export function registerProxyWidgets(canvas: LGraphCanvas) {
6265
}
6366
}
6467
})
68+
canvas.canvas.addEventListener<'subgraph-converted'>(
69+
'subgraph-converted',
70+
(e) => promoteRecommendedWidgets(e.detail.subgraphNode)
71+
)
6572
SubgraphNode.prototype.onConfigure = onConfigure
6673
}
6774

src/lib/litegraph/src/LGraph.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,14 @@ export class LGraph
17101710

17111711
subgraphNode._setConcreteSlots()
17121712
subgraphNode.arrange()
1713+
this.canvasAction((c) =>
1714+
c.canvas.dispatchEvent(
1715+
new CustomEvent('subgraph-converted', {
1716+
bubbles: true,
1717+
detail: { subgraphNode: subgraphNode as SubgraphNode }
1718+
})
1719+
)
1720+
)
17131721
return { subgraph, node: subgraphNode as SubgraphNode }
17141722
}
17151723

src/lib/litegraph/src/infrastructure/LGraphCanvasEventMap.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export interface LGraphCanvasEventMap {
2121
fromNode: SubgraphNode
2222
}
2323

24+
/** Dispatched after a group of items has been converted to a subgraph*/
25+
'subgraph-converted': {
26+
subgraphNode: SubgraphNode
27+
}
28+
2429
'litegraph:canvas':
2530
| { subType: 'before-change' | 'after-change' }
2631
| {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { describe, expect, vi } from 'vitest'
2+
3+
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
4+
import type { LGraphBadge } from '@/lib/litegraph/src/LGraphBadge'
5+
import type { LGraphIcon } from '@/lib/litegraph/src/LGraphIcon'
6+
7+
import { subgraphTest } from '../../litegraph/subgraph/fixtures/subgraphFixtures'
8+
9+
import { usePriceBadge } from '@/composables/node/usePriceBadge'
10+
11+
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
12+
useColorPaletteStore: () => ({
13+
completedActivePalette: {
14+
light_theme: false,
15+
colors: { litegraph_base: {} }
16+
}
17+
})
18+
}))
19+
20+
const { updateSubgraphCredits } = usePriceBadge()
21+
22+
const mockNode = new LGraphNode('mock node')
23+
const mockIcon: Partial<LGraphIcon> = { unicode: '\ue96b' }
24+
const badge: Partial<LGraphBadge> = {
25+
icon: mockIcon as LGraphIcon,
26+
text: '$0.05/Run'
27+
}
28+
mockNode.badges = [badge as LGraphBadge]
29+
30+
function getBadgeText(node: LGraphNode): string {
31+
const badge = node.badges[0]
32+
return (typeof badge === 'function' ? badge() : badge).text
33+
}
34+
35+
describe('subgraph pricing', () => {
36+
subgraphTest(
37+
'should not display badge for subgraphs without API nodes',
38+
({ subgraphWithNode }) => {
39+
const { subgraphNode } = subgraphWithNode
40+
updateSubgraphCredits(subgraphNode)
41+
expect(subgraphNode.badges.length).toBe(0)
42+
}
43+
)
44+
subgraphTest(
45+
'should return the price of a single contained API node',
46+
({ subgraphWithNode }) => {
47+
const { subgraphNode, subgraph } = subgraphWithNode
48+
subgraph.add(mockNode)
49+
updateSubgraphCredits(subgraphNode)
50+
expect(subgraphNode.badges.length).toBe(1)
51+
expect(getBadgeText(subgraphNode)).toBe('$0.05/Run')
52+
}
53+
)
54+
subgraphTest(
55+
'should return the number of api nodes if more than one exists',
56+
({ subgraphWithNode }) => {
57+
const { subgraphNode, subgraph } = subgraphWithNode
58+
for (let i = 0; i < 5; i++) subgraph.add(mockNode)
59+
updateSubgraphCredits(subgraphNode)
60+
expect(subgraphNode.badges.length).toBe(1)
61+
expect(getBadgeText(subgraphNode)).toBe('Partner Nodes x 5')
62+
}
63+
)
64+
})

0 commit comments

Comments
 (0)