Skip to content

Commit 3bc25b7

Browse files
Add Asset Widget (#5475)
* [feat] carve out path to call asset browser in combo widget * Add Asset Widget * [feat] add fallback "Select model" label --------- Co-authored-by: Arjan Singh <[email protected]>
1 parent 08fe282 commit 3bc25b7

File tree

5 files changed

+139
-24
lines changed

5 files changed

+139
-24
lines changed

src/lib/litegraph/src/types/widgets.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export type IWidget =
7676
| IImageCompareWidget
7777
| ISelectButtonWidget
7878
| ITextareaWidget
79+
| IAssetWidget
7980

8081
export interface IBooleanWidget extends IBaseWidget<boolean, 'toggle'> {
8182
type: 'toggle'
@@ -224,6 +225,12 @@ export interface ITextareaWidget extends IBaseWidget<string, 'textarea'> {
224225
value: string
225226
}
226227

228+
export interface IAssetWidget
229+
extends IBaseWidget<string, 'asset', IWidgetOptions<string[]>> {
230+
type: 'asset'
231+
value: string
232+
}
233+
227234
/**
228235
* Valid widget types. TS cannot provide easily extensible type safety for this at present.
229236
* Override linkedWidgets[]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
2+
import type { IAssetWidget } from '@/lib/litegraph/src/types/widgets'
3+
4+
import { BaseWidget, type DrawWidgetOptions } from './BaseWidget'
5+
6+
export class AssetWidget
7+
extends BaseWidget<IAssetWidget>
8+
implements IAssetWidget
9+
{
10+
constructor(widget: IAssetWidget, node: LGraphNode) {
11+
super(widget, node)
12+
this.type ??= 'asset'
13+
this.value = widget.value?.toString() ?? ''
14+
}
15+
16+
override get _displayValue(): string {
17+
return String(this.value) //FIXME: Resolve asset name
18+
}
19+
20+
override drawWidget(
21+
ctx: CanvasRenderingContext2D,
22+
{ width, showText = true }: DrawWidgetOptions
23+
) {
24+
// Store original context attributes
25+
const { fillStyle, strokeStyle, textAlign } = ctx
26+
27+
this.drawWidgetShape(ctx, { width, showText })
28+
29+
if (showText) {
30+
this.drawTruncatingText({ ctx, width, leftPadding: 0, rightPadding: 0 })
31+
}
32+
33+
// Restore original context attributes
34+
Object.assign(ctx, { textAlign, strokeStyle, fillStyle })
35+
}
36+
37+
override onClick() {
38+
//Open Modal
39+
this.callback?.(this.value)
40+
}
41+
}

src/lib/litegraph/src/widgets/widgetMap.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
} from '@/lib/litegraph/src/types/widgets'
88
import { toClass } from '@/lib/litegraph/src/utils/type'
99

10+
import { AssetWidget } from './AssetWidget'
1011
import { BaseWidget } from './BaseWidget'
1112
import { BooleanWidget } from './BooleanWidget'
1213
import { ButtonWidget } from './ButtonWidget'
@@ -47,6 +48,7 @@ export type WidgetTypeMap = {
4748
imagecompare: ImageCompareWidget
4849
selectbutton: SelectButtonWidget
4950
textarea: TextareaWidget
51+
asset: AssetWidget
5052
[key: string]: BaseWidget
5153
}
5254

@@ -115,6 +117,8 @@ export function toConcreteWidget<TWidget extends IWidget | IBaseWidget>(
115117
return toClass(SelectButtonWidget, narrowedWidget, node)
116118
case 'textarea':
117119
return toClass(TextareaWidget, narrowedWidget, node)
120+
case 'asset':
121+
return toClass(AssetWidget, narrowedWidget, node)
118122
default: {
119123
if (wrapLegacyWidgets) return toClass(LegacyWidget, widget, node)
120124
}

src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,15 @@ const addComboWidget = (
6969
)
7070

7171
if (isUsingAssetAPI && isEligible) {
72-
// Create button widget for Asset Browser
72+
// Get the default value for the button text (currently selected model)
7373
const currentValue = getDefaultValue(inputSpec)
74+
const displayLabel = currentValue ?? t('widgets.selectModel')
7475

75-
const widget = node.addWidget(
76-
'button',
77-
inputSpec.name,
78-
t('widgets.selectModel'),
79-
() => {
80-
console.log(
81-
`Asset Browser would open here for:\nNode: ${node.type}\nWidget: ${inputSpec.name}\nCurrent Value:${currentValue}`
82-
)
83-
}
84-
)
76+
const widget = node.addWidget('asset', inputSpec.name, displayLabel, () => {
77+
console.log(
78+
`Asset Browser would open here for:\nNode: ${node.type}\nWidget: ${inputSpec.name}\nCurrent Value:${currentValue}`
79+
)
80+
})
8581

8682
return widget
8783
}
@@ -129,7 +125,7 @@ const addComboWidget = (
129125
)
130126
}
131127

132-
return widget
128+
return widget as IBaseWidget
133129
}
134130

135131
export const useComboWidget = () => {

tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useComboWidget.test.ts

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ describe('useComboWidget', () => {
8888
})
8989

9090
it('should create normal combo widget when asset API is disabled', () => {
91-
mockSettingStoreGet.mockReturnValue(false)
92-
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
91+
mockSettingStoreGet.mockReturnValue(false) // Asset API disabled
92+
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true) // Widget is eligible
9393

9494
const constructor = useComboWidget()
9595
const mockWidget = createMockWidget()
@@ -101,6 +101,7 @@ describe('useComboWidget', () => {
101101
})
102102

103103
const widget = constructor(mockNode, inputSpec)
104+
expect(widget).toBe(mockWidget)
104105

105106
expect(mockNode.addWidget).toHaveBeenCalledWith(
106107
'combo',
@@ -142,15 +143,15 @@ describe('useComboWidget', () => {
142143
expect(widget).toBe(mockWidget)
143144
})
144145

145-
it('should create asset browser button widget when API enabled and widget eligible', () => {
146+
it('should create asset browser widget when API enabled and widget eligible', () => {
146147
mockSettingStoreGet.mockReturnValue(true)
147148
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
148149

149150
const constructor = useComboWidget()
150151
const mockWidget = createMockWidget({
151-
type: 'button',
152+
type: 'asset',
152153
name: 'ckpt_name',
153-
value: 'Select model'
154+
value: 'model1.safetensors'
154155
})
155156
const mockNode = createMockNode('CheckpointLoaderSimple')
156157
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
@@ -162,9 +163,9 @@ describe('useComboWidget', () => {
162163
const widget = constructor(mockNode, inputSpec)
163164

164165
expect(mockNode.addWidget).toHaveBeenCalledWith(
165-
'button',
166+
'asset',
166167
'ckpt_name',
167-
'Select model',
168+
'model1.safetensors',
168169
expect.any(Function)
169170
)
170171
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
@@ -175,15 +176,48 @@ describe('useComboWidget', () => {
175176
expect(widget).toBe(mockWidget)
176177
})
177178

178-
it('should use asset browser button even when inputSpec has a default value but no options', () => {
179+
it('should create asset browser widget with options when API enabled and widget eligible', () => {
179180
mockSettingStoreGet.mockReturnValue(true)
180181
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
181182

182183
const constructor = useComboWidget()
183184
const mockWidget = createMockWidget({
184-
type: 'button',
185+
type: 'asset',
185186
name: 'ckpt_name',
186-
value: 'Select model'
187+
value: 'model1.safetensors'
188+
})
189+
const mockNode = createMockNode('CheckpointLoaderSimple')
190+
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
191+
const inputSpec = createMockInputSpec({
192+
name: 'ckpt_name',
193+
options: ['model1.safetensors', 'model2.safetensors']
194+
})
195+
196+
const widget = constructor(mockNode, inputSpec)
197+
198+
expect(mockNode.addWidget).toHaveBeenCalledWith(
199+
'asset',
200+
'ckpt_name',
201+
'model1.safetensors',
202+
expect.any(Function)
203+
)
204+
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
205+
expect(vi.mocked(assetService.isAssetBrowserEligible)).toHaveBeenCalledWith(
206+
'ckpt_name',
207+
'CheckpointLoaderSimple'
208+
)
209+
expect(widget).toBe(mockWidget)
210+
})
211+
212+
it('should use asset browser widget even when inputSpec has a default value but no options', () => {
213+
mockSettingStoreGet.mockReturnValue(true)
214+
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
215+
216+
const constructor = useComboWidget()
217+
const mockWidget = createMockWidget({
218+
type: 'asset',
219+
name: 'ckpt_name',
220+
value: 'fallback.safetensors'
187221
})
188222
const mockNode = createMockNode('CheckpointLoaderSimple')
189223
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
@@ -196,9 +230,42 @@ describe('useComboWidget', () => {
196230
const widget = constructor(mockNode, inputSpec)
197231

198232
expect(mockNode.addWidget).toHaveBeenCalledWith(
199-
'button',
233+
'asset',
234+
'ckpt_name',
235+
'fallback.safetensors',
236+
expect.any(Function)
237+
)
238+
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
239+
expect(vi.mocked(assetService.isAssetBrowserEligible)).toHaveBeenCalledWith(
240+
'ckpt_name',
241+
'CheckpointLoaderSimple'
242+
)
243+
expect(widget).toBe(mockWidget)
244+
})
245+
246+
it('should show Select model when asset widget has undefined current value', () => {
247+
mockSettingStoreGet.mockReturnValue(true)
248+
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
249+
250+
const constructor = useComboWidget()
251+
const mockWidget = createMockWidget({
252+
type: 'asset',
253+
name: 'ckpt_name',
254+
value: 'Select model'
255+
})
256+
const mockNode = createMockNode('CheckpointLoaderSimple')
257+
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
258+
const inputSpec = createMockInputSpec({
259+
name: 'ckpt_name'
260+
// Note: no default, no options, not remote - getDefaultValue returns undefined
261+
})
262+
263+
const widget = constructor(mockNode, inputSpec)
264+
265+
expect(mockNode.addWidget).toHaveBeenCalledWith(
266+
'asset',
200267
'ckpt_name',
201-
'Select model',
268+
'Select model', // Should fallback to this instead of undefined
202269
expect.any(Function)
203270
)
204271
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')

0 commit comments

Comments
 (0)